← back to Dw Photo Capture
iOS freeze — close the contrarian gap: ALL photo flows now decode-free. downscale() itself = FileReader raw (fixes Add New SKU / Update SKU / gallery-batch, which still froze via createImageBitmap that v99 proved unsafe). Server normalizes at every ingest: create-item + /api/photo + /api/photos via normB64 (HEIC→JPEG for Shopify; box IM has libheif). Scan regression green
dd49fa4b09c8e320bb41a713484040f5ea29137d · 2026-07-08 15:59:51 -0700 · Steve Abrams
Files touched
M public/index.htmlM server.js
Diff
commit dd49fa4b09c8e320bb41a713484040f5ea29137d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 8 15:59:51 2026 -0700
iOS freeze — close the contrarian gap: ALL photo flows now decode-free. downscale() itself = FileReader raw (fixes Add New SKU / Update SKU / gallery-batch, which still froze via createImageBitmap that v99 proved unsafe). Server normalizes at every ingest: create-item + /api/photo + /api/photos via normB64 (HEIC→JPEG for Shopify; box IM has libheif). Scan regression green
---
public/index.html | 22 ++++++++--------------
server.js | 10 +++++++---
2 files changed, 15 insertions(+), 17 deletions(-)
diff --git a/public/index.html b/public/index.html
index 52a4c34..ad03f1e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -831,24 +831,18 @@ function lazyThumbs(){
_thumbObs=new IntersectionObserver((es,o)=>{ for(const e of es){ if(e.isIntersecting){ const t=e.target,u=t.getAttribute('data-bg'); if(u){ t.style.backgroundImage=`url('${u}')`; t.removeAttribute('data-bg'); } o.unobserve(t); } } },{rootMargin:'400px'});
grid.querySelectorAll('.thumb[data-bg]').forEach(t=>_thumbObs.observe(t));
}
-// iOS FREEZE FIX: a full 12MP camera photo decoded via <img>+canvas.toDataURL is a memory hog that
-// hangs/OOM-crashes iOS Safari. createImageBitmap decodes OFF the main thread + low-memory, so try it
-// first; fall back to <img> only if unavailable. Also cap smaller (OCR needs ~1200px, not 2000).
-async function downscale(file,max=1200){
- try{
- if('createImageBitmap' in window){
- const bmp=await createImageBitmap(file);
- const s=Math.min(1,max/Math.max(bmp.width,bmp.height));
- const c=document.createElement('canvas'); c.width=Math.max(1,Math.round(bmp.width*s)); c.height=Math.max(1,Math.round(bmp.height*s));
- c.getContext('2d').drawImage(bmp,0,0,c.width,c.height); if(bmp.close)bmp.close();
- return c.toDataURL('image/jpeg',0.82);
- }
- }catch(e){ /* fall through to <img> path */ }
+// iOS FREEZE FIX (DTD verdict B): NO client-side decode anywhere. v99 proved even createImageBitmap
+// OOM-freezes iOS on a 12MP camera photo. downscale() now just reads the RAW file to a data URL via
+// FileReader (no <img>/canvas/bitmap) — the SERVER (normalizeImage: ImageMagick → auto-orient → ≤1600px
+// JPEG, handles HEIC) resizes at every ingest endpoint. `max` is ignored (kept for call-site compat).
+async function downscale(file,max){
+ const raw=await new Promise(r=>{ const fr=new FileReader(); fr.onload=()=>r(fr.result); fr.onerror=()=>r(null); fr.readAsDataURL(file); });
+ if(raw) return raw;
return new Promise((res)=>{
const img=new Image(), url=URL.createObjectURL(file);
let done=false; const finish=v=>{ if(done)return; done=true; try{URL.revokeObjectURL(url);}catch(e){} res(v); };
img.onload=()=>{
- let{width:w,height:h}=img; const s=Math.min(1,max/Math.max(w,h));
+ let{width:w,height:h}=img; const s=Math.min(1,(max||1200)/Math.max(w,h));
const c=document.createElement('canvas'); c.width=Math.round(w*s); c.height=Math.round(h*s);
c.getContext('2d').drawImage(img,0,0,c.width,c.height); finish(c.toDataURL('image/jpeg',0.82));
};
diff --git a/server.js b/server.js
index d0e8e60..227c1d1 100644
--- a/server.js
+++ b/server.js
@@ -637,6 +637,9 @@ function normalizeImage(buf) {
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
+// 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'];
async function backOcrFused(buf) {
const engines = OCR_FUSION_ORDER.filter(engineAvailable);
@@ -1359,8 +1362,9 @@ const appHandler = (req, res) => {
if (!TOKEN) return send(res, 200, { ok: false, err: 'no Shopify token' });
const strip = s => s ? s.replace(/^data:image\/\w+;base64,/, '') : null;
// up to 3 photos (front pattern / back label / detail); front is the primary Shopify image.
- const photos64 = Array.isArray(p.photos) && p.photos.length ? p.photos.map(strip).filter(Boolean)
+ const rawPhotos64 = Array.isArray(p.photos) && p.photos.length ? p.photos.map(strip).filter(Boolean)
: (p.dataUrl ? [strip(p.dataUrl)].filter(Boolean) : []);
+ const photos64 = await Promise.all(rawPhotos64.map(normB64)); // raw camera photos → clean JPEG for Shopify
p._photos64 = photos64;
const b64 = photos64[0] || null;
try { send(res, 200, await createNewItem(p, b64, p.commit !== true)); }
@@ -1870,7 +1874,7 @@ load();
let p; try { p = JSON.parse(body); } catch (e) { return send(res, 400, { err: 'bad json' }); }
const { dw_sku, product_id, dataUrl, push, keep_images, meta } = p;
if (!dw_sku || !dataUrl) return send(res, 400, { err: 'dw_sku + dataUrl required' });
- const b64 = dataUrl.replace(/^data:image\/\w+;base64,/, '');
+ const b64 = await normB64(dataUrl.replace(/^data:image\/\w+;base64,/, '')); // raw photo → clean JPEG
const safe = String(dw_sku).replace(/[^A-Za-z0-9._-]/g, '');
const fname = `${safe}-${Date.now()}.jpg`;
fs.writeFileSync(path.join(PHOTOS, fname), Buffer.from(b64, 'base64'));
@@ -1923,7 +1927,7 @@ load();
const urls = dataUrls.slice(0, 12); // cap a single batch
let added = 0; const errors = []; let lastPhoto = null;
for (let i = 0; i < urls.length; i++) {
- const b64 = String(urls[i]).replace(/^data:image\/\w+;base64,/, '');
+ const b64 = await normB64(String(urls[i]).replace(/^data:image\/\w+;base64,/, '')); // raw photo → clean JPEG
const safe = String(dw_sku).replace(/[^A-Za-z0-9._-]/g, '');
const fname = `${safe}-${Date.now()}-${i}.jpg`;
try { fs.writeFileSync(path.join(PHOTOS, fname), Buffer.from(b64, 'base64')); lastPhoto = `/photos/${fname}`; } catch (e) {}
← 71b8574 iOS freeze — eliminate ALL client-side image decode (DTD ver
·
back to Dw Photo Capture
·
Scan upload-progress (final DTD FIX-THEN-SHIP): identify-mul ab7c20f →