← 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 →