refactor: extract image-helpers (#2261)

This commit is contained in:
Ted Liang
2025-12-09 09:19:49 +11:00
committed by GitHub
parent 3bd34964cd
commit c5032d0c43
12 changed files with 69 additions and 56 deletions
@@ -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: {
+16
View File
@@ -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,
};
};
+10
View File
@@ -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();
};
+5
View File
@@ -0,0 +1,5 @@
import sharp from 'sharp';
export const svgToPng = async (svg: string) => {
return await sharp(Buffer.from(svg)).toFormat('png').toBuffer();
};