[object Object]

← back to Dw Photo Capture

Fix grainy Shopify product photos: raise server-side JPEG normalize cap+quality

126f917dcb1e63de519f8866ac752c77c571662b · 2026-09-25 08:59:08 -0700 · Steve

TK-12224. Root cause: normalizeImage() (server.js) runs every uploaded camera
photo through ImageMagick before it's attached to the live Shopify product
(createNewItem / /api/photo / /api/photos all pipe through normB64). It was
downscaling to <=1600x1600 at quality 85 -- a SECOND, lower-quality JPEG
generation stacked on top of the client's own capture (which already caps to
2000px @ quality 0.92 in index.html/cam.html, or native-res @ 0.92 in
batch.html). On busy, high-frequency wallcovering/fabric patterns that shows
up as visible JPEG blocking/mosquito noise -- exactly the "grainy" symptom.

Fix: raise the resize cap to 3000x3000 (a no-op for size against every
client's actual output, still guards oversized/HEIC originals) and the
quality factor to 92 (matches the client's own export quality, removing the
double-compression quality loss). Auto-orient/HEIC normalization for OCR is
unchanged.

Client-side getUserMedia constraints, canvas capture sizing, and JPEG export
quality were already correct (4096x3072 ideal, videoWidth/videoHeight-sized
canvases, 0.90-0.92 export quality) from prior TK-12115/12124/12127/12128
work -- this was the one remaining lossy step, confirmed by tracing
/api/photo, /api/photos, and /api/create-item to normB64() -> normalizeImage().

Files touched

Diff

commit 126f917dcb1e63de519f8866ac752c77c571662b
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 25 08:59:08 2026 -0700

    Fix grainy Shopify product photos: raise server-side JPEG normalize cap+quality
    
    TK-12224. Root cause: normalizeImage() (server.js) runs every uploaded camera
    photo through ImageMagick before it's attached to the live Shopify product
    (createNewItem / /api/photo / /api/photos all pipe through normB64). It was
    downscaling to <=1600x1600 at quality 85 -- a SECOND, lower-quality JPEG
    generation stacked on top of the client's own capture (which already caps to
    2000px @ quality 0.92 in index.html/cam.html, or native-res @ 0.92 in
    batch.html). On busy, high-frequency wallcovering/fabric patterns that shows
    up as visible JPEG blocking/mosquito noise -- exactly the "grainy" symptom.
    
    Fix: raise the resize cap to 3000x3000 (a no-op for size against every
    client's actual output, still guards oversized/HEIC originals) and the
    quality factor to 92 (matches the client's own export quality, removing the
    double-compression quality loss). Auto-orient/HEIC normalization for OCR is
    unchanged.
    
    Client-side getUserMedia constraints, canvas capture sizing, and JPEG export
    quality were already correct (4096x3072 ideal, videoWidth/videoHeight-sized
    canvases, 0.90-0.92 export quality) from prior TK-12115/12124/12127/12128
    work -- this was the one remaining lossy step, confirmed by tracing
    /api/photo, /api/photos, and /api/create-item to normB64() -> normalizeImage().
---
 server.js | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index aa9f3c0..7555e10 100644
--- a/server.js
+++ b/server.js
@@ -802,19 +802,28 @@ function resolveEngines(req) {
 // order is GCV → gemini → macvision (literal transcribers first). Plug-and-play: keying GCV_API_KEY auto-
 // enables it here with no code change. Cost = sum of the engines that run (shown per scan).
 // Client now sends the RAW camera photo (no client-side decode — iOS freeze fix). Normalize server-side:
-// ImageMagick converts ANY format (HEIC/JPEG/PNG), auto-orients, and downscales to ≤1600px JPEG so OCR
+// ImageMagick converts ANY format (HEIC/JPEG/PNG), auto-orients, and downscales to ≤3000px JPEG so OCR
 // engines (GCV needs JPEG/PNG, not HEIC) + storage get a sane image. Best-effort — original on failure.
+// TK-12224 (grainy Shopify product photos): this same normalize path is also the LAST step before the
+// image is attached to a live Shopify product (createNewItem / /api/photo / /api/photos all pipe their
+// b64 through normB64 below). The client already caps + compresses once (index.html tsCapture / cam.html
+// both cap the longest edge to 2000px at JPEG quality 0.92; batch.html's master is native-res at 0.92).
+// The old 1600x1600 + quality 85 here was a SECOND, LOWER-quality JPEG generation stacked on top of the
+// client's own compression — for busy, high-frequency wallcovering/fabric patterns that shows up exactly
+// as visible blocking/mosquito-noise ("grainy"). Raising the cap above every client's output makes this
+// step a no-op for size (">" never upscales) and matching the quality factor to the client's 0.92 removes
+// the double-compression quality loss, while still normalizing HEIC/orientation/format for OCR + storage.
 function normalizeImage(buf) {
   return new Promise(resolve => {
     let out = [];
-    const cp = execFile('/usr/bin/convert', ['-', '-auto-orient', '-resize', '1600x1600>', '-quality', '85', 'jpg:-'],
+    const cp = execFile('/usr/bin/convert', ['-', '-auto-orient', '-resize', '3000x3000>', '-quality', '92', 'jpg:-'],
       { maxBuffer: 48 * 1024 * 1024, timeout: 12000, encoding: 'buffer' }, (err, stdout) => {
         resolve(err || !stdout || !stdout.length ? buf : stdout);   // fall back to the original bytes
       });
     try { cp.stdin.on('error', () => {}); cp.stdin.write(buf); cp.stdin.end(); } catch (e) { resolve(buf); }
   });
 }
-// normalize a base64 image (any format/size, incl. iOS HEIC) → clean ≤1600px JPEG base64. The client now
+// normalize a base64 image (any format/size, incl. iOS HEIC) → clean ≤3000px JPEG base64. The client now
 // sends RAW camera photos (no client decode — iOS freeze fix), so EVERY ingest point normalizes here.
 async function normB64(b64) { if (!b64) return b64; try { return (await normalizeImage(Buffer.from(b64, 'base64'))).toString('base64'); } catch (e) { return b64; } }
 const OCR_FUSION_ORDER = ['gcv', 'gemini', 'macvision'];

← afa2777 auto-data-snapshot: 2026-09-25T04:38:13 (1 data files) — dat  ·  back to Dw Photo Capture  ·  deploy: exclude .claude/ from rsync so agent worktree logs o e82585f →