From e7c1efaaf325e3e9ee51a152160cd3278b7ce8c3 Mon Sep 17 00:00:00 2001 From: stirling-image Date: Tue, 14 Apr 2026 15:58:31 +0800 Subject: [PATCH] feat(passport-photo): true WYSIWYG - zoom and drag affect the download - Zoom now controls the actual crop: zoom in = tighter crop, zoom out = more body - Drag adjusts face position within the frame - Backend receives zoom value and applies the same zoomed crop - Download produces exactly what the user sees on the canvas - Zoom range 0.5x-3x (can zoom out to show more body) - "What you see is what you download" label --- apps/api/src/routes/tools/passport-photo.ts | 17 +++++++++++++---- .../tools/passport-photo-settings.tsx | 14 +++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/api/src/routes/tools/passport-photo.ts b/apps/api/src/routes/tools/passport-photo.ts index a7a6e3a0..7c33fbf6 100644 --- a/apps/api/src/routes/tools/passport-photo.ts +++ b/apps/api/src/routes/tools/passport-photo.ts @@ -37,6 +37,7 @@ const generateSettingsSchema = z.object({ dpi: z.number().min(72).max(600).default(300), customWidthMm: z.number().optional(), customHeightMm: z.number().optional(), + zoom: z.number().min(0.5).max(3).default(1), adjustX: z.number().default(0), adjustY: z.number().default(0), landmarks: landmarksSchema, @@ -295,6 +296,7 @@ export function registerPassportPhoto(app: FastifyInstance) { dpi: userDpi, customWidthMm, customHeightMm, + zoom: userZoom, adjustX, adjustY, landmarks: rawLandmarks, @@ -358,8 +360,15 @@ export function registerPassportPhoto(app: FastifyInstance) { const photoWidthPx = photoHeightPx * aspectRatio; // Position: eye line should be at eyeLineFromBottom from photo bottom - const topY = eyeYPx - photoHeightPx * (1 - docSpec.eyeLineFromBottom); - const leftX = faceCenterXPx - photoWidthPx / 2; + const baseTopY = eyeYPx - photoHeightPx * (1 - docSpec.eyeLineFromBottom); + const baseLeftX = faceCenterXPx - photoWidthPx / 2; + + // Apply zoom: zoom > 1 = tighter crop (less body), zoom < 1 = wider (more body) + // The zoomed region is centered on the base crop + const zoomedW = photoWidthPx / userZoom; + const zoomedH = photoHeightPx / userZoom; + const leftX = baseLeftX + (photoWidthPx - zoomedW) / 2; + const topY = baseTopY + (photoHeightPx - zoomedH) / 2; // Parse background color const hex = bgColor.replace("#", ""); @@ -381,8 +390,8 @@ export function registerPassportPhoto(app: FastifyInstance) { // image with background color so the full intended region is available. const rawLeft = Math.round(leftX); const rawTop = Math.round(topY); - const rawW = Math.round(photoWidthPx); - const rawH = Math.round(photoHeightPx); + const rawW = Math.round(zoomedW); + const rawH = Math.round(zoomedH); const padLeft = Math.max(0, -rawLeft); const padTop = Math.max(0, -rawTop); diff --git a/apps/web/src/components/tools/passport-photo-settings.tsx b/apps/web/src/components/tools/passport-photo-settings.tsx index be2cc5ae..112deac9 100644 --- a/apps/web/src/components/tools/passport-photo-settings.tsx +++ b/apps/web/src/components/tools/passport-photo-settings.tsx @@ -361,6 +361,7 @@ export function PassportPhotoSettings() { setAnalyzing, generating, setGenerating, + zoom, adjustX, adjustY, } = usePassportPhotoStore(); @@ -485,6 +486,7 @@ export function PassportPhotoSettings() { bgColor, maxFileSizeKb, dpi, + zoom, adjustX, adjustY, landmarks: analyzeResult.landmarks, @@ -1063,7 +1065,7 @@ export function PassportPhotoPreview() { const handleWheel = useCallback( (e: React.WheelEvent) => { e.preventDefault(); - setZoom(Math.max(1, Math.min(3, zoom + (e.deltaY > 0 ? -0.1 : 0.1)))); + setZoom(Math.max(0.5, Math.min(3, zoom + (e.deltaY > 0 ? -0.1 : 0.1)))); }, [zoom, setZoom], ); @@ -1107,7 +1109,7 @@ export function PassportPhotoPreview() {
- {zoom !== 1 && ( -

- Zoom is for inspection only. Download is always the full photo. -

- )} +

+ What you see is what you download +

{/* Canvas preview */}