// Shared plumbing for the zero-dependency OOXML writers (pptx.ts, docx.ts). // An Office file is an OPC package: a zip of XML parts plus a relationship graph. Everything here is // format-agnostic — the per-format part builders live in their own modules. // --------------------------------------------------------------------------- // Minimal ZIP writer (deflate, falls back to store) — enough for OPC packages. import { deflateRawSync } from "node:zlib"; const CRC_TABLE = new Uint32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; }); export function crc32(buf: Buffer): number { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]!) & 0xff]! ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } export function zip(entries: { name: string; data: Buffer }[]): Buffer { const locals: Buffer[] = []; const centrals: Buffer[] = []; let offset = 0; for (const e of entries) { const name = Buffer.from(e.name, "utf8"); const crc = crc32(e.data); let method = 8; let body: Buffer = deflateRawSync(e.data); if (body.length >= e.data.length) { method = 0; // deflate made it bigger — store instead body = e.data; } const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0, 6); local.writeUInt16LE(method, 8); local.writeUInt16LE(0, 10); // time local.writeUInt16LE(0x21, 12); // date (1996-01-01 — deterministic output) local.writeUInt32LE(crc, 14); local.writeUInt32LE(body.length, 18); local.writeUInt32LE(e.data.length, 22); local.writeUInt16LE(name.length, 26); local.writeUInt16LE(0, 28); locals.push(local, name, body); const central = Buffer.alloc(46); central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6); central.writeUInt16LE(0, 8); central.writeUInt16LE(method, 10); central.writeUInt16LE(0, 12); central.writeUInt16LE(0x21, 14); central.writeUInt32LE(crc, 16); central.writeUInt32LE(body.length, 20); central.writeUInt32LE(e.data.length, 24); central.writeUInt16LE(name.length, 28); central.writeUInt16LE(0, 30); central.writeUInt16LE(0, 32); central.writeUInt16LE(0, 34); central.writeUInt16LE(0, 36); central.writeUInt32LE(0, 38); central.writeUInt32LE(offset, 42); centrals.push(central, name); offset += local.length + name.length + body.length; } const centralSize = centrals.reduce((n, b) => n + b.length, 0); const eocd = Buffer.alloc(22); eocd.writeUInt32LE(0x06054b50, 0); eocd.writeUInt16LE(0, 4); eocd.writeUInt16LE(0, 6); eocd.writeUInt16LE(entries.length, 8); eocd.writeUInt16LE(entries.length, 10); eocd.writeUInt32LE(centralSize, 12); eocd.writeUInt32LE(offset, 16); eocd.writeUInt16LE(0, 20); return Buffer.concat([...locals, ...centrals, eocd]); } // --------------------------------------------------------------------------- // XML + relationship helpers. export const XML_DECL = ``; export const REL_NS = `xmlns="http://schemas.openxmlformats.org/package/2006/relationships"`; export const RT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; export const EMU = 914400; // English Metric Units per inch export function esc(s: string): string { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } export function relsXml(rels: { id: string; type: string; target: string }[]): string { return `${XML_DECL}${rels.map((r) => ``).join("")}`; } // --------------------------------------------------------------------------- // Image handling: sniff dimensions so pictures keep their aspect ratio. export const IMAGE_TYPES: Record = { ".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif" }; export function imageSize(buf: Buffer, kind: string): { w: number; h: number } | null { try { if (kind === "png" && buf.readUInt32BE(12) === 0x49484452) return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) }; if (kind === "gif") return { w: buf.readUInt16LE(6), h: buf.readUInt16LE(8) }; if (kind === "jpeg") { let i = 2; while (i + 9 < buf.length) { if (buf[i] !== 0xff) break; const marker = buf[i + 1]!; if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) return { w: buf.readUInt16BE(i + 7), h: buf.readUInt16BE(i + 5) }; i += 2 + buf.readUInt16BE(i + 2); } } } catch { /* fall through — caller uses the full bounding box */ } return null; }