Add batch cleaning with per-file review and ZIP download

Up to 50 images at once; single-image flow unchanged. Includes a
dependency-free stored-ZIP writer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
SharifandClaude Sonnet 5.5 committed 2026-10-20 07:25:12 +06:00
1 parent 28a8151cc0
commit 1a9b726354
5 files changed
+421 -11

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
/**
* Minimal ZIP writer (stored, no compression). Images are already compressed,
* so deflating them gains nothing, and this avoids a dependency.
*/
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c >>> 0;
}
return table;
})();
function crc32(bytes) {
let crc = 0xffffffff;
for (let i = 0; i < bytes.length; i++) {
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function header(size, write) {
const view = new DataView(new ArrayBuffer(size));
write(view);
return new Uint8Array(view.buffer);
}
/** @param {{name: string, data: Uint8Array}[]} files @returns {Blob} */
export function createZip(files) {
const encoder = new TextEncoder();
const now = new Date();
const time = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
const date = ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
const parts = [];
const central = [];
let offset = 0;
for (const { name, data } of files) {
const nameBytes = encoder.encode(name);
const crc = crc32(data);
const local = header(30, (v) => {
v.setUint32(0, 0x04034b50, true);
v.setUint16(4, 20, true);
v.setUint16(6, 0x0800, true); // UTF-8 names
v.setUint16(8, 0, true); // stored
v.setUint16(10, time, true);
v.setUint16(12, date, true);
v.setUint32(14, crc, true);
v.setUint32(18, data.length, true);
v.setUint32(22, data.length, true);
v.setUint16(26, nameBytes.length, true);
});
parts.push(local, nameBytes, data);
central.push(
header(46, (v) => {
v.setUint32(0, 0x02014b50, true);
v.setUint16(4, 20, true);
v.setUint16(6, 20, true);
v.setUint16(8, 0x0800, true);
v.setUint16(10, 0, true);
v.setUint16(12, time, true);
v.setUint16(14, date, true);
v.setUint32(16, crc, true);
v.setUint32(20, data.length, true);
v.setUint32(24, data.length, true);
v.setUint16(28, nameBytes.length, true);
v.setUint32(42, offset, true);
}),
nameBytes
);
offset += local.length + nameBytes.length + data.length;
}
const centralSize = central.reduce((sum, part) => sum + part.length, 0);
const end = header(22, (v) => {
v.setUint32(0, 0x06054b50, true);
v.setUint16(8, files.length, true);
v.setUint16(10, files.length, true);
v.setUint32(12, centralSize, true);
v.setUint32(16, offset, true);
});
return new Blob([...parts, ...central, end], { type: "application/zip" });
}