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:
1 parent
28a8151cc0
commit
1a9b726354
5 files changed
+421
-11
No files matched your search
@@ -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" });
|
||||
}
|
||||
Reference in new issue
Block a user