import { strToU8, zipSync } from "fflate"; export type RecoveryWorkbookImage = { bytes: Uint8Array; contentType: string; width?: number | null; height?: number | null; description: string; }; export type RecoveryWorkbookRow = { cells: Array; images: Array<{ column: number; image: RecoveryWorkbookImage; }>; hyperlinks?: Array<{ column: number; url: string; }>; }; type WorkbookOptions = { sheetName: string; headers: string[]; columnWidths: number[]; rows: RecoveryWorkbookRow[]; }; const XML_HEADER = ''; const IMAGE_ROW_HEIGHT = 126; function cleanXmlText(value: unknown) { return String(value ?? "") .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "") .slice(0, 32767) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function columnName(index: number) { let value = index + 1; let result = ""; while (value > 0) { value -= 1; result = String.fromCharCode(65 + (value % 26)) + result; value = Math.floor(value / 26); } return result; } function safeSheetName(value: string) { const cleaned = value.replace(/[\\/?*\[\]:]/g, " ").trim(); return (cleaned || "数据回收").slice(0, 31); } function safeHyperlink(value: string) { try { const url = new URL(value); return ["http:", "https:"].includes(url.protocol) ? url.toString() : ""; } catch { return ""; } } function imageFormat(contentType: string, bytes: Uint8Array) { const normalized = contentType.toLowerCase(); if (normalized.includes("png") || (bytes[0] === 0x89 && bytes[1] === 0x50)) { return { extension: "png", contentType: "image/png" }; } if ( normalized.includes("gif") || (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) ) { return { extension: "gif", contentType: "image/gif" }; } if ( normalized.includes("webp") || String.fromCharCode(...bytes.slice(8, 12)) === "WEBP" ) { return { extension: "webp", contentType: "image/webp" }; } return { extension: "jpg", contentType: "image/jpeg" }; } function imageDimensions(image: RecoveryWorkbookImage) { if (image.width && image.height) { return { width: image.width, height: image.height }; } const bytes = image.bytes; if (bytes.length >= 24 && bytes[0] === 0x89 && bytes[1] === 0x50) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); return { width: view.getUint32(16), height: view.getUint32(20) }; } if ( bytes.length >= 10 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 ) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); return { width: view.getUint16(6, true), height: view.getUint16(8, true) }; } if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) { let offset = 2; while (offset + 9 < bytes.length) { if (bytes[offset] !== 0xff) { offset += 1; continue; } const marker = bytes[offset + 1]; const length = (bytes[offset + 2] << 8) + bytes[offset + 3]; if ( [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes( marker, ) ) { return { width: (bytes[offset + 7] << 8) + bytes[offset + 8], height: (bytes[offset + 5] << 8) + bytes[offset + 6], }; } if (!length) break; offset += length + 2; } } return { width: 4, height: 3 }; } function imageDisplaySize(image: RecoveryWorkbookImage) { const dimensions = imageDimensions(image); const scale = Math.min(160 / dimensions.width, 150 / dimensions.height, 1); return { width: Math.max(28, Math.round(dimensions.width * scale)), height: Math.max(28, Math.round(dimensions.height * scale)), }; } function inlineCell(reference: string, value: unknown, style: number) { return `${cleanXmlText(value)}`; } function numberCell(reference: string, value: number, style: number) { return `${Number.isFinite(value) ? value : 0}`; } export function buildRecoveryWorkbook(options: WorkbookOptions) { const sheetName = safeSheetName(options.sheetName); const imageEntries = options.rows.flatMap((row, rowIndex) => row.images.map((item) => ({ ...item, row: rowIndex + 1 })), ); const hyperlinkEntries = options.rows.flatMap((row, rowIndex) => (row.hyperlinks ?? []) .map((item) => ({ ...item, row: rowIndex + 2, url: safeHyperlink(item.url), })) .filter((item) => item.url), ); const lastColumn = columnName(Math.max(0, options.headers.length - 1)); const lastRow = Math.max(1, options.rows.length + 1); const headerCells = options.headers .map((value, index) => inlineCell(`${columnName(index)}1`, value, 1)) .join(""); const dataRows = options.rows .map((row, rowIndex) => { const number = rowIndex + 2; const imageColumns = new Set(row.images.map((item) => item.column)); const hyperlinkColumns = new Set( (row.hyperlinks ?? []).map((item) => item.column), ); const cells = options.headers .map((_, columnIndex) => { const reference = `${columnName(columnIndex)}${number}`; const value = row.cells[columnIndex] ?? ""; if (imageColumns.has(columnIndex)) { return inlineCell(reference, value || "见图", 4); } return typeof value === "number" ? numberCell(reference, value, 3) : inlineCell( reference, value, hyperlinkColumns.has(columnIndex) ? 5 : 2, ); }) .join(""); const rowHeight = row.images.length > 0 ? IMAGE_ROW_HEIGHT : 42; return `${cells}`; }) .join(""); const columns = options.headers .map((_, index) => { const width = Math.min(70, Math.max(8, options.columnWidths[index] ?? 14)); return ``; }) .join(""); const drawingXml = imageEntries.length ? `${XML_HEADER}${imageEntries .map((entry, index) => { const size = imageDisplaySize(entry.image); const width = size.width * 9525; const height = size.height * 9525; return `${entry.column}57150${entry.row}57150`; }) .join("")}` : ""; const drawingRelationships = imageEntries.length ? `${XML_HEADER}${imageEntries .map((entry, index) => { const format = imageFormat(entry.image.contentType, entry.image.bytes); return ``; }) .join("")}` : ""; const imageFormats = new Map(); for (const entry of imageEntries) { const format = imageFormat(entry.image.contentType, entry.image.bytes); imageFormats.set(format.extension, format.contentType); } const imageContentTypes = [...imageFormats.entries()] .map(([extension, contentType]) => ``) .join(""); const contentTypes = `${XML_HEADER}${imageContentTypes}${imageEntries.length ? '' : ""}`; const hyperlinkRelationshipOffset = imageEntries.length ? 2 : 1; const hyperlinksXml = hyperlinkEntries.length ? `${hyperlinkEntries .map( (entry, index) => ``, ) .join("")}` : ""; const worksheet = `${XML_HEADER}${columns}${headerCells}${dataRows}${hyperlinksXml}${imageEntries.length ? '' : ""}`; const sheetRelationships = [ ...(imageEntries.length ? [ '', ] : []), ...hyperlinkEntries.map( (entry, index) => ``, ), ]; const files: Record = { "[Content_Types].xml": strToU8(contentTypes), "_rels/.rels": strToU8(`${XML_HEADER}`), "docProps/app.xml": strToU8(`${XML_HEADER}KOC LOOP`), "docProps/core.xml": strToU8(`${XML_HEADER}KOC LOOPKOC LOOP${new Date().toISOString()}`), "xl/workbook.xml": strToU8(`${XML_HEADER}`), "xl/_rels/workbook.xml.rels": strToU8(`${XML_HEADER}`), "xl/styles.xml": strToU8(`${XML_HEADER}`), "xl/worksheets/sheet1.xml": strToU8(worksheet), }; if (sheetRelationships.length) { files["xl/worksheets/_rels/sheet1.xml.rels"] = strToU8(`${XML_HEADER}${sheetRelationships.join("")}`); } if (imageEntries.length) { files["xl/drawings/drawing1.xml"] = strToU8(drawingXml); files["xl/drawings/_rels/drawing1.xml.rels"] = strToU8(drawingRelationships); imageEntries.forEach((entry, index) => { const format = imageFormat(entry.image.contentType, entry.image.bytes); files[`xl/media/image${index + 1}.${format.extension}`] = entry.image.bytes; }); } return zipSync(files, { level: 1 }); }