/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ function createPdfDocument() { const objects = []; let nextObjId = 1; const pageDefs = []; let fontId, catalogId, pagesId; function newObj(content) { const id = nextObjId++; objects.push({ id, content }); return id; } function fmt(n) { return Math.round(n * 100) / 100; } function escapePdfText(str) { return str.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)'); } function toLatin1(str) { let out = ''; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); out += code <= 255 ? str[i] : '?'; } return out; } function estimateTextWidth(str, size) { return str.length * size * 0.55; } fontId = newObj('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>'); function addPage(widthPt, heightPt) { const ops = []; let strokeKey = null, fillKey = null, widthKey = null; function setStroke(r, g, b) { const key = r + ',' + g + ',' + b; if (key === strokeKey) return; strokeKey = key; ops.push(`${fmt(r)} ${fmt(g)} ${fmt(b)} RG`); } function setFill(r, g, b) { const key = r + ',' + g + ',' + b; if (key === fillKey) return; fillKey = key; ops.push(`${fmt(r)} ${fmt(g)} ${fmt(b)} rg`); } function setLineWidth(w) { if (w === widthKey) return; widthKey = w; ops.push(`${fmt(w)} w`); } function line(x1, y1, x2, y2) { ops.push(`${fmt(x1)} ${fmt(y1)} m ${fmt(x2)} ${fmt(y2)} l S`); } function rect(x, y, w, h, mode) { ops.push(`${fmt(x)} ${fmt(y)} ${fmt(w)} ${fmt(h)} re ${mode || 'S'}`); } function polyline(points, close) { if (!points.length) return; ops.push(`${fmt(points[0][0])} ${fmt(points[0][1])} m`); for (let i = 1; i < points.length; i++) ops.push(`${fmt(points[i][0])} ${fmt(points[i][1])} l`); ops.push(close ? 's' : 'S'); } function text(x, y, size, str, align) { const s = escapePdfText(toLatin1(str)); let tx = x; if (align === 'center') tx = x - estimateTextWidth(str, size) / 2; else if (align === 'right') tx = x - estimateTextWidth(str, size); ops.push(`BT /F1 ${fmt(size)} Tf ${fmt(tx)} ${fmt(y)} Td (${s}) Tj ET`); } function textRotated(x, y, size, str, degrees) { const rad = (degrees * Math.PI) / 180; const cos = Math.cos(rad), sin = Math.sin(rad); const s = escapePdfText(toLatin1(str)); ops.push(`BT /F1 ${fmt(size)} Tf ${fmt(cos)} ${fmt(sin)} ${fmt(-sin)} ${fmt(cos)} ${fmt(x)} ${fmt(y)} Tm (${s}) Tj ET`); } const contentId = newObj(''); const pageId = newObj(''); pageDefs.push({ pageId, contentId, ops, widthPt, heightPt }); return { setStroke, setFill, setLineWidth, line, rect, polyline, text, textRotated, estimateTextWidth }; } function build() { pagesId = newObj(''); catalogId = newObj(`<< /Type /Catalog /Pages ${pagesId} 0 R >>`); pageDefs.forEach(p => { const streamBody = p.ops.join('\n'); objects.find(o => o.id === p.contentId).content = `<< /Length ${streamBody.length} >>\nstream\n${streamBody}\nendstream`; objects.find(o => o.id === p.pageId).content = `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${fmt(p.widthPt)} ${fmt(p.heightPt)}] ` + `/Resources << /Font << /F1 ${fontId} 0 R >> >> /Contents ${p.contentId} 0 R >>`; }); objects.find(o => o.id === pagesId).content = `<< /Type /Pages /Kids [${pageDefs.map(p => p.pageId + ' 0 R').join(' ')}] /Count ${pageDefs.length} >>`; objects.sort((a, b) => a.id - b.id); let out = '%PDF-1.4\n%\xE2\xE3\xCF\xD3\n'; const offsets = [0]; objects.forEach(obj => { offsets[obj.id] = out.length; out += `${obj.id} 0 obj\n${obj.content}\nendobj\n`; }); const xrefOffset = out.length; out += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`; for (let i = 1; i <= objects.length; i++) { out += String(offsets[i]).padStart(10, '0') + ' 00000 n \n'; } out += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`; return out; } return { addPage, build }; } function savePdfBlob(latin1String, filename) { const bytes = new Uint8Array(latin1String.length); for (let i = 0; i < latin1String.length; i++) bytes[i] = latin1String.charCodeAt(i) & 0xff; const blob = new Blob([bytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }