109 lines
4.3 KiB
JavaScript
109 lines
4.3 KiB
JavaScript
/* =========================================================================
|
|
BiRefNet WebUI · 极简 ZIP 打包器(无第三方依赖)
|
|
用途:结果列表里的记录可能来自多个不同任务,服务端的「按任务打包」不再适用,
|
|
所以在前端把所有可见结果打成一个 ZIP。
|
|
实现:ZIP 规范里最朴素的 STORE(不压缩)方式 —— 结果本来就是 PNG,
|
|
已经过 deflate,再压一次收益几乎为零,直接存更快更简单。
|
|
========================================================================= */
|
|
(function (root) {
|
|
'use strict';
|
|
|
|
const CRC_TABLE = (() => {
|
|
const table = new Uint32Array(256);
|
|
for (let i = 0; i < 256; i++) {
|
|
let c = i;
|
|
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
table[i] = c >>> 0;
|
|
}
|
|
return table;
|
|
})();
|
|
|
|
function crc32(bytes) {
|
|
let c = 0xffffffff;
|
|
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
/** JS Date -> MS-DOS 时间/日期(ZIP 头里的老格式)。 */
|
|
function dosStamp(date) {
|
|
const time = ((date.getHours() & 0x1f) << 11) | ((date.getMinutes() & 0x3f) << 5) | ((date.getSeconds() >> 1) & 0x1f);
|
|
const day = (((date.getFullYear() - 1980) & 0x7f) << 9) | (((date.getMonth() + 1) & 0x0f) << 5) | (date.getDate() & 0x1f);
|
|
return { time, day };
|
|
}
|
|
|
|
/**
|
|
* 生成一个 ZIP 文件。
|
|
* @param {Array<{name: string, data: Uint8Array}>} entries
|
|
* @returns {Uint8Array}
|
|
*/
|
|
function buildZip(entries) {
|
|
const list = entries || [];
|
|
if (!list.length) throw new Error('没有可打包的文件');
|
|
const encoder = new TextEncoder();
|
|
const { time, day } = dosStamp(new Date());
|
|
|
|
const body = []; // 本地文件头 + 数据
|
|
const central = []; // 中央目录
|
|
let offset = 0;
|
|
|
|
for (const entry of list) {
|
|
const name = encoder.encode(String(entry.name));
|
|
const data = entry.data instanceof Uint8Array ? entry.data : new Uint8Array(entry.data);
|
|
const crc = crc32(data);
|
|
|
|
const local = new Uint8Array(30 + name.length);
|
|
const lv = new DataView(local.buffer);
|
|
lv.setUint32(0, 0x04034b50, true); // 本地文件头签名
|
|
lv.setUint16(4, 20, true); // 解压所需版本
|
|
lv.setUint16(6, 0x0800, true); // 标志位:文件名为 UTF-8
|
|
lv.setUint16(8, 0, true); // 压缩方式 0 = STORE
|
|
lv.setUint16(10, time, true);
|
|
lv.setUint16(12, day, true);
|
|
lv.setUint32(14, crc, true);
|
|
lv.setUint32(18, data.length, true); // 压缩后大小
|
|
lv.setUint32(22, data.length, true); // 原始大小
|
|
lv.setUint16(26, name.length, true);
|
|
lv.setUint16(28, 0, true); // 扩展字段长度
|
|
local.set(name, 30);
|
|
body.push(local, data);
|
|
|
|
const cd = new Uint8Array(46 + name.length);
|
|
const cv = new DataView(cd.buffer);
|
|
cv.setUint32(0, 0x02014b50, true); // 中央目录签名
|
|
cv.setUint16(4, 20, true); // 生成程序版本
|
|
cv.setUint16(6, 20, true); // 解压所需版本
|
|
cv.setUint16(8, 0x0800, true);
|
|
cv.setUint16(10, 0, true); // STORE
|
|
cv.setUint16(12, time, true);
|
|
cv.setUint16(14, day, true);
|
|
cv.setUint32(16, crc, true);
|
|
cv.setUint32(20, data.length, true);
|
|
cv.setUint32(24, data.length, true);
|
|
cv.setUint16(28, name.length, true);
|
|
cv.setUint32(42, offset, true); // 本地头偏移
|
|
cd.set(name, 46);
|
|
central.push(cd);
|
|
|
|
offset += local.length + data.length;
|
|
}
|
|
|
|
const centralSize = central.reduce((n, c) => n + c.length, 0);
|
|
const eocd = new Uint8Array(22);
|
|
const ev = new DataView(eocd.buffer);
|
|
ev.setUint32(0, 0x06054b50, true); // 中央目录结束标记
|
|
ev.setUint16(8, list.length, true); // 本盘条目数
|
|
ev.setUint16(10, list.length, true); // 总条目数
|
|
ev.setUint32(12, centralSize, true);
|
|
ev.setUint32(16, offset, true);
|
|
|
|
const out = new Uint8Array(offset + centralSize + 22);
|
|
let p = 0;
|
|
for (const part of body) { out.set(part, p); p += part.length; }
|
|
for (const c of central) { out.set(c, p); p += c.length; }
|
|
out.set(eocd, p);
|
|
return out;
|
|
}
|
|
|
|
root.BiRefNetZip = { buildZip, crc32 };
|
|
})(typeof window !== 'undefined' ? window : globalThis);
|