mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
refactor: extract image-helpers (#2261)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import satori from 'satori';
|
||||
import sharp from 'sharp';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getRecipientOrSenderByShareLinkSlug } from '@documenso/lib/server-only/document/get-recipient-or-sender-by-share-link-slug';
|
||||
import { svgToPng } from '@documenso/lib/utils/images/svg-to-png';
|
||||
|
||||
import type { Route } from './+types/share.$slug.opengraph';
|
||||
|
||||
@@ -181,8 +181,7 @@ export const loader = async ({ params }: Route.LoaderArgs) => {
|
||||
},
|
||||
);
|
||||
|
||||
// Convert SVG to PNG using sharp
|
||||
const pngBuffer = await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
||||
const pngBuffer = await svgToPng(svg.toString());
|
||||
|
||||
return new Response(pngBuffer, {
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import type { Route } from './+types/branding.logo.organisation.$orgId';
|
||||
@@ -63,16 +62,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const img = await sharp(file)
|
||||
.toFormat('png', {
|
||||
quality: 80,
|
||||
})
|
||||
.toBuffer();
|
||||
const { content, contentType } = await loadLogo(file);
|
||||
|
||||
return new Response(Buffer.from(img), {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': img.length.toString(),
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': content.length.toString(),
|
||||
// Stale while revalidate for 1 hours to 24 hours
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||
import { loadLogo } from '@documenso/lib/utils/images/logo';
|
||||
|
||||
import type { Route } from './+types/branding.logo.team.$teamId';
|
||||
|
||||
@@ -56,16 +55,12 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const img = await sharp(file)
|
||||
.toFormat('png', {
|
||||
quality: 80,
|
||||
})
|
||||
.toBuffer();
|
||||
const { content, contentType } = await loadLogo(file);
|
||||
|
||||
return new Response(img, {
|
||||
return new Response(content, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': img.length.toString(),
|
||||
'Content-Type': contentType,
|
||||
'Content-Length': content.length.toString(),
|
||||
// Stale while revalidate for 1 hours to 24 hours
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
},
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"remeda": "^2.32.0",
|
||||
"remix-themes": "^2.0.4",
|
||||
"satori": "^0.18.3",
|
||||
"sharp": "0.34.5",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"ua-parser-js": "^1.0.41",
|
||||
|
||||
Generated
-1
@@ -167,7 +167,6 @@
|
||||
"remeda": "^2.32.0",
|
||||
"remix-themes": "^2.0.4",
|
||||
"satori": "^0.18.3",
|
||||
"sharp": "0.34.5",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"ts-pattern": "^5.9.0",
|
||||
"ua-parser-js": "^1.0.41",
|
||||
|
||||
@@ -2,12 +2,12 @@ import { createCanvas, loadImage } from '@napi-rs/canvas';
|
||||
import { DocumentStatus, type Field, RecipientRole } from '@prisma/client';
|
||||
import { generateObject } from 'ai';
|
||||
import pMap from 'p-map';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../../../errors/app-error';
|
||||
import { getFileServerSide } from '../../../../universal/upload/get-file.server';
|
||||
import { resizeImageToGeminiImage } from '../../../../utils/images/resize-image-to-gemini-image';
|
||||
import { getEnvelopeById } from '../../../envelope/get-envelope-by-id';
|
||||
import { createEnvelopeRecipients } from '../../../recipient/create-envelope-recipients';
|
||||
import { vertex } from '../../google';
|
||||
@@ -238,21 +238,6 @@ const maskFieldsOnImage = async ({ image, width, height, fields }: MaskFieldsOnI
|
||||
return canvas.encode('jpeg');
|
||||
};
|
||||
|
||||
const TARGET_SIZE = 1000;
|
||||
|
||||
type ResizeImageOptions = {
|
||||
image: Buffer;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resize image to 1000x1000 using fill strategy.
|
||||
* Scales to cover the target area and crops any overflow.
|
||||
*/
|
||||
const resizeImageToSquare = async ({ image, size = TARGET_SIZE }: ResizeImageOptions) => {
|
||||
return await sharp(image).resize(size, size, { fit: 'fill' }).toBuffer();
|
||||
};
|
||||
|
||||
type DetectFieldsFromPageOptions = {
|
||||
image: Buffer;
|
||||
pageNumber: number;
|
||||
@@ -267,7 +252,7 @@ const detectFieldsFromPage = async ({
|
||||
context,
|
||||
}: DetectFieldsFromPageOptions) => {
|
||||
// Resize to 1000x1000 for consistent coordinate mapping
|
||||
const resizedImage = await resizeImageToSquare({ image });
|
||||
const resizedImage = await resizeImageToGeminiImage({ image });
|
||||
|
||||
// Build messages array
|
||||
const messages: Parameters<typeof generateObject>[0]['messages'] = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { loadAvatar } from '../../utils/images/avatar';
|
||||
|
||||
export type GetAvatarImageOptions = {
|
||||
id: string;
|
||||
};
|
||||
@@ -17,10 +17,5 @@ export const getAvatarImage = async ({ id }: GetAvatarImageOptions) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = Buffer.from(avatarImage.bytes, 'base64');
|
||||
|
||||
return {
|
||||
contentType: 'image/jpeg',
|
||||
content: await sharp(bytes).toFormat('jpeg').toBuffer(),
|
||||
};
|
||||
return await loadAvatar(avatarImage.bytes);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/organisations';
|
||||
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { optimiseAvatar } from '../../utils/images/avatar';
|
||||
import { buildOrganisationWhereQuery } from '../../utils/organisations';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
|
||||
@@ -100,10 +99,7 @@ export const setAvatarImage = async ({
|
||||
let newAvatarImageId: string | null = null;
|
||||
|
||||
if (bytes) {
|
||||
const optimisedBytes = await sharp(Buffer.from(bytes, 'base64'))
|
||||
.resize(512, 512)
|
||||
.toFormat('jpeg', { quality: 75 })
|
||||
.toBuffer();
|
||||
const optimisedBytes = await optimiseAvatar(bytes);
|
||||
|
||||
const avatarImage = await prisma.avatarImage.create({
|
||||
data: {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const optimiseAvatar = async (bytes: string) => {
|
||||
return await sharp(Buffer.from(bytes, 'base64'))
|
||||
.resize(512, 512)
|
||||
.toFormat('jpeg', { quality: 75 })
|
||||
.toBuffer();
|
||||
};
|
||||
|
||||
export const loadAvatar = async (bytes: string) => {
|
||||
const content = await sharp(Buffer.from(bytes, 'base64')).toFormat('jpeg').toBuffer();
|
||||
return {
|
||||
contentType: 'image/jpeg',
|
||||
content,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const loadLogo = async (file: Uint8Array) => {
|
||||
const content = await sharp(file).toFormat('png', { quality: 80 }).toBuffer();
|
||||
|
||||
return {
|
||||
contentType: 'image/png',
|
||||
content,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const TARGET_SIZE = 1000;
|
||||
|
||||
type ResizeImageToGeminiImageOptions = {
|
||||
image: Buffer;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resize image to 1000x1000 using fill strategy.
|
||||
* Scales to cover the target area and crops any overflow.
|
||||
*/
|
||||
export const resizeImageToGeminiImage = async ({
|
||||
image,
|
||||
size = TARGET_SIZE,
|
||||
}: ResizeImageToGeminiImageOptions) => {
|
||||
return await sharp(image).resize(size, size, { fit: 'fill' }).toBuffer();
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import sharp from 'sharp';
|
||||
|
||||
export const svgToPng = async (svg: string) => {
|
||||
return await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
|
||||
};
|
||||
Reference in New Issue
Block a user