diff --git a/docs/cmms-bridge.js b/docs/cmms-bridge.js index b2ca6f9..e086c18 100644 --- a/docs/cmms-bridge.js +++ b/docs/cmms-bridge.js @@ -162,40 +162,69 @@ // The board photos live in pcb-retrace's IndexedDB (imported by the user or // unpacked from the .pcbretrace.zip), NOT as CMMS attachments, so the - // assistant can't reach them with read_attachment_image. Downscale + JPEG - // each and hand them along with the buffer so a `read_board_image` tool can - // surface them. Long edge ~1400px, quality 0.8 - a phone photo lands well - // under 300KB. - async function encodeImage(blob, maxEdge) { - const bmp = await createImageBitmap(blob); - const s = Math.min(1, maxEdge / Math.max(bmp.width, bmp.height)); - const w = Math.max(1, Math.round(bmp.width * s)); - const h = Math.max(1, Math.round(bmp.height * s)); - const c = document.createElement('canvas'); - c.width = w; c.height = h; - c.getContext('2d').drawImage(bmp, 0, 0, w, h); - bmp.close(); - const out = await new Promise(res => c.toBlob(res, 'image/jpeg', 0.8)); - const b64 = await new Promise((res, rej) => { + // assistant can't reach them with read_attachment_image. Hand them along + // with the buffer so a `read_board_image` tool can surface them. + const MAX_IMG_B64 = 4 * 1024 * 1024; // ~3MB raw; matches message.php's cap + + function blobToB64(blob) { + return new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(String(r.result).split(',', 2)[1] || ''); - r.onerror = rej; - r.readAsDataURL(out); + r.onerror = () => rej(new Error('FileReader failed')); + r.readAsDataURL(blob); }); - return { w, h, b64 }; + } + + // Downscale a photo to ~maxEdge on the long side as JPEG. Returns a Blob. + async function downscale(blob, maxEdge) { + const bmp = await createImageBitmap(blob); + try { + const s = Math.min(1, maxEdge / Math.max(bmp.width, bmp.height)); + const w = Math.max(1, Math.round(bmp.width * s)); + const h = Math.max(1, Math.round(bmp.height * s)); + const c = document.createElement('canvas'); + c.width = w; c.height = h; + c.getContext('2d').drawImage(bmp, 0, 0, w, h); + const out = await new Promise(r => c.toBlob(r, 'image/jpeg', 0.8)); + if (!out) throw new Error('canvas.toBlob returned null'); + return out; + } finally { + try { bmp.close(); } catch (e) {} + } } async function buildProjectImages() { if (!currentBomId || !bomImages.length) return []; const out = []; for (const img of bomImages.slice(0, 6)) { - try { - const enc = await encodeImage(img.blob, 1400); - if (enc.b64.length > 3 * 1024 * 1024) { trace('image too large after encode, skipping', img.id); continue; } - out.push({ id: img.id, name: img.name || (img.id + '.jpg'), mime: 'image/jpeg', data_base64: enc.b64 }); - } catch (e) { - trace('image encode failed', img.id, e && e.message); + let blob = img.blob; + if (!(blob instanceof Blob)) { + try { blob = new Blob([blob], { type: (img.type || 'image/jpeg') }); } + catch (e) { trace('image', img.id, 'has no usable blob'); continue; } } + let b64 = null; + try { + b64 = await blobToB64(await downscale(blob, 1400)); + trace('image', img.id, 'downscaled ok'); + } catch (e) { + trace('image', img.id, 'downscale failed:', e && e.message, '- trying raw'); + // Fall back to the raw blob (some builds / EXIF-heavy JPEGs break + // createImageBitmap or toBlob). Only if it's not huge. + try { + if (blob.size <= 3 * 1024 * 1024) b64 = await blobToB64(blob); + else trace('image', img.id, 'raw blob too large (', blob.size, ')'); + } catch (e2) { + trace('image', img.id, 'raw encode also failed:', e2 && e2.message); + } + } + if (!b64) continue; + if (b64.length > MAX_IMG_B64) { trace('image', img.id, 'over size cap after encode'); continue; } + out.push({ + id: img.id, + name: img.name || (img.id + '.jpg'), + mime: (blob.type && blob.type.indexOf('image/') === 0) ? blob.type : 'image/jpeg', + data_base64: b64, + }); } trace('buildProjectImages:', out.length, 'of', bomImages.length, 'photos encoded'); return out;