Files
SpineParticlesWeb/src/export/zipArchive.ts
T
2026-10-22 18:13:43 +08:00

94 lines
3.4 KiB
TypeScript

import { translate } from '../i18n'
export interface ZipArchiveFile {
name: string
data: Uint8Array
}
const crcTable = Array.from({ length: 256 }, (_, value) => {
let crc = value
for (let bit = 0; bit < 8; bit++) crc = (crc & 1) ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1
return crc >>> 0
})
function crc32(data: Uint8Array) {
let crc = 0xffffffff
for (const byte of data) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8)
return (crc ^ 0xffffffff) >>> 0
}
function safeArchivePath(value: string) {
const parts = String(value || '').replace(/\\/g, '/').split('/').filter((part) => part && part !== '.')
if (!parts.length || parts.some((part) => part === '..')) throw new Error(translate('errors.invalidZipPath'))
return parts.join('/')
}
function dateTime(date = new Date()) {
const year = Math.max(1980, date.getFullYear())
return {
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
}
}
function header(size: number) {
const bytes = new Uint8Array(size)
return { bytes, view: new DataView(bytes.buffer) }
}
function combine(chunks: Uint8Array[]) {
const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0))
let offset = 0
for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.length }
return output
}
/** 创建 Store 模式 ZIP;图片本身已经压缩,避免引入额外打包依赖。 */
export function createZipArchive(files: ZipArchiveFile[]) {
const encoder = new TextEncoder()
const localChunks: Uint8Array[] = []
const centralChunks: Uint8Array[] = []
const timestamp = dateTime()
let offset = 0
for (const file of files) {
const name = encoder.encode(safeArchivePath(file.name))
const crc = crc32(file.data)
const local = header(30)
local.view.setUint32(0, 0x04034b50, true)
local.view.setUint16(4, 20, true)
local.view.setUint16(6, 0x0800, true)
local.view.setUint16(10, timestamp.time, true)
local.view.setUint16(12, timestamp.date, true)
local.view.setUint32(14, crc, true)
local.view.setUint32(18, file.data.length, true)
local.view.setUint32(22, file.data.length, true)
local.view.setUint16(26, name.length, true)
localChunks.push(local.bytes, name, file.data)
const central = header(46)
central.view.setUint32(0, 0x02014b50, true)
central.view.setUint16(4, 20, true)
central.view.setUint16(6, 20, true)
central.view.setUint16(8, 0x0800, true)
central.view.setUint16(12, timestamp.time, true)
central.view.setUint16(14, timestamp.date, true)
central.view.setUint32(16, crc, true)
central.view.setUint32(20, file.data.length, true)
central.view.setUint32(24, file.data.length, true)
central.view.setUint16(28, name.length, true)
central.view.setUint32(42, offset, true)
centralChunks.push(central.bytes, name)
offset += local.bytes.length + name.length + file.data.length
}
const centralDirectory = combine(centralChunks)
const end = header(22)
end.view.setUint32(0, 0x06054b50, true)
end.view.setUint16(8, files.length, true)
end.view.setUint16(10, files.length, true)
end.view.setUint32(12, centralDirectory.length, true)
end.view.setUint32(16, offset, true)
return new Blob([combine([...localChunks, centralDirectory, end.bytes])], { type: 'application/zip' })
}