A 20480x13312 upscale is past Pillow's pixel limit, so its card image failed and the catch-all fallback served the 324 MB original as the thumbnail: the gallery and the AI workbench decoded it every time. - A picture past the limit is shrunk once by a child process (huge_picture.py, low priority, one at a time) and cached; a card is never a large original, one with no card image gets an SVG with its size. - /anomalous/output_preview gives the viewers the picture or a cached 4096 px copy of a very large one; the viewer (moved to ui_gallery_viewer.js), the image workbench's stage and the AI pages' larger pictures use it, with a note and "Open the original". - The listing carries each file's size (and big ones' pixel size); such a card says "Very large". - The browser-side PNG metadata read stops at the image data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.0 KiB
JavaScript
98 lines
4.0 KiB
JavaScript
/**
|
|
* The workflow (or API prompt) a ComfyUI PNG carries, read in the browser from its text
|
|
* chunks (tEXt, uncompressed iTXt) before the image data: the gallery's detail falls back to
|
|
* it when the server could not say. Null for anything that is not such a PNG. No DOM.
|
|
*/
|
|
|
|
/**
|
|
* The response's bytes up to the first IDAT chunk's header, where the text chunks end: the
|
|
* download stops there, so a picture of hundreds of megabytes is not fetched whole.
|
|
*/
|
|
async function headBeforePixels(response) {
|
|
const reader = response.body?.getReader?.();
|
|
if (!reader) return response.arrayBuffer();
|
|
let bytes = new Uint8Array(0);
|
|
let next = 8; // where the next chunk's header starts
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) return bytes.buffer;
|
|
const joined = new Uint8Array(bytes.length + value.length);
|
|
joined.set(bytes);
|
|
joined.set(value, bytes.length);
|
|
bytes = joined;
|
|
const view = new DataView(bytes.buffer);
|
|
while (next + 8 <= bytes.length) {
|
|
if (view.getUint32(next + 4) === 0x49444154) { // 'IDAT'
|
|
void reader.cancel();
|
|
return bytes.buffer;
|
|
}
|
|
next += 12 + view.getUint32(next);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function parsePngMetadataFromUrl(url, options = {}) {
|
|
if (!url) return null;
|
|
try {
|
|
const response = await fetch(url, { cache: 'force-cache', signal: options.signal });
|
|
if (!response.ok) return null;
|
|
const buffer = await headBeforePixels(response);
|
|
const view = new DataView(buffer);
|
|
if (view.byteLength < 32) return null;
|
|
// PNG magic bytes: 137, 80, 78, 71, 13, 10, 26, 10
|
|
if (view.getUint32(0) !== 0x89504E47 || view.getUint32(4) !== 0x0D0A1A0A) return null;
|
|
|
|
let offset = 8;
|
|
const utf8 = new TextDecoder('utf-8');
|
|
const latin1 = new TextDecoder('iso-8859-1');
|
|
const result = {};
|
|
|
|
while (offset + 8 <= buffer.byteLength) {
|
|
const length = view.getUint32(offset);
|
|
const type = String.fromCharCode(
|
|
view.getUint8(offset + 4),
|
|
view.getUint8(offset + 5),
|
|
view.getUint8(offset + 6),
|
|
view.getUint8(offset + 7),
|
|
);
|
|
const dataOffset = offset + 8;
|
|
if (type === 'IDAT') break; // Reached image raster data, text metadata is prior
|
|
|
|
if (dataOffset + length > buffer.byteLength) break;
|
|
|
|
if (type === 'tEXt') {
|
|
const bytes = new Uint8Array(buffer, dataOffset, length);
|
|
const nullIdx = bytes.indexOf(0);
|
|
if (nullIdx > -1) {
|
|
const key = latin1.decode(bytes.subarray(0, nullIdx));
|
|
const val = latin1.decode(bytes.subarray(nullIdx + 1));
|
|
try { result[key] = JSON.parse(val); } catch { result[key] = val; }
|
|
}
|
|
} else if (type === 'iTXt') {
|
|
const bytes = new Uint8Array(buffer, dataOffset, length);
|
|
const nullIdx = bytes.indexOf(0);
|
|
if (nullIdx > -1) {
|
|
const key = latin1.decode(bytes.subarray(0, nullIdx));
|
|
let ptr = nullIdx + 1;
|
|
const compFlag = bytes[ptr++];
|
|
const compMethod = bytes[ptr++];
|
|
while (ptr < bytes.length && bytes[ptr] !== 0) ptr++;
|
|
ptr++;
|
|
while (ptr < bytes.length && bytes[ptr] !== 0) ptr++;
|
|
ptr++;
|
|
if (compFlag === 0 && ptr <= bytes.length) {
|
|
const val = utf8.decode(bytes.subarray(ptr));
|
|
try { result[key] = JSON.parse(val); } catch { result[key] = val; }
|
|
}
|
|
}
|
|
}
|
|
offset += 12 + length;
|
|
}
|
|
return result.workflow || result.prompt || null;
|
|
} catch (e) {
|
|
if (e?.name === 'AbortError') return null;
|
|
console.warn('Client-side PNG metadata read skipped:', e);
|
|
return null;
|
|
}
|
|
}
|