CMMS fork: robust board-photo encoding for the AI bridge
read_board_image reported "no board photos available" - buildProjectImages was returning [] (both photos failed to encode) with no visibility. - split encodeImage into downscale() + blobToB64(); on any failure (createImageBitmap / canvas.toBlob returning null / EXIF-heavy JPEGs) fall back to sending the raw blob base64 if it's under ~3MB. - coerce a non-Blob img.blob through new Blob([...]). - per-image [cmms-pcb-bridge] tracing: "downscaled ok" / "downscale failed: <msg> - trying raw" / "raw encode also failed" etc.
This commit is contained in:
parent
40373493b5
commit
8976c52f42
1 changed files with 51 additions and 22 deletions
|
|
@ -162,40 +162,69 @@
|
||||||
|
|
||||||
// The board photos live in pcb-retrace's IndexedDB (imported by the user or
|
// 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
|
// unpacked from the .pcbretrace.zip), NOT as CMMS attachments, so the
|
||||||
// assistant can't reach them with read_attachment_image. Downscale + JPEG
|
// assistant can't reach them with read_attachment_image. Hand them along
|
||||||
// each and hand them along with the buffer so a `read_board_image` tool can
|
// with the buffer so a `read_board_image` tool can surface them.
|
||||||
// surface them. Long edge ~1400px, quality 0.8 - a phone photo lands well
|
const MAX_IMG_B64 = 4 * 1024 * 1024; // ~3MB raw; matches message.php's cap
|
||||||
// under 300KB.
|
|
||||||
async function encodeImage(blob, maxEdge) {
|
function blobToB64(blob) {
|
||||||
const bmp = await createImageBitmap(blob);
|
return new Promise((res, rej) => {
|
||||||
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) => {
|
|
||||||
const r = new FileReader();
|
const r = new FileReader();
|
||||||
r.onload = () => res(String(r.result).split(',', 2)[1] || '');
|
r.onload = () => res(String(r.result).split(',', 2)[1] || '');
|
||||||
r.onerror = rej;
|
r.onerror = () => rej(new Error('FileReader failed'));
|
||||||
r.readAsDataURL(out);
|
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() {
|
async function buildProjectImages() {
|
||||||
if (!currentBomId || !bomImages.length) return [];
|
if (!currentBomId || !bomImages.length) return [];
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const img of bomImages.slice(0, 6)) {
|
for (const img of bomImages.slice(0, 6)) {
|
||||||
try {
|
let blob = img.blob;
|
||||||
const enc = await encodeImage(img.blob, 1400);
|
if (!(blob instanceof Blob)) {
|
||||||
if (enc.b64.length > 3 * 1024 * 1024) { trace('image too large after encode, skipping', img.id); continue; }
|
try { blob = new Blob([blob], { type: (img.type || 'image/jpeg') }); }
|
||||||
out.push({ id: img.id, name: img.name || (img.id + '.jpg'), mime: 'image/jpeg', data_base64: enc.b64 });
|
catch (e) { trace('image', img.id, 'has no usable blob'); continue; }
|
||||||
} catch (e) {
|
|
||||||
trace('image encode failed', img.id, e && e.message);
|
|
||||||
}
|
}
|
||||||
|
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');
|
trace('buildProjectImages:', out.length, 'of', bomImages.length, 'photos encoded');
|
||||||
return out;
|
return out;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue