Configurator voor Van Emous Mantelzorgwoningen: wanden, vloeren, units, woningen (L-vorm), plat dak met dakrand, buitenverlichting, latwerk op vochtdoek, trasraam, sandwichpanelen en PDF-tekenbladen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
5.7 KiB
JavaScript
164 lines
5.7 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// ============================================================
|
|
// js/core/pdf-writer.js — minimale, dependency-vrije PDF-generator.
|
|
// Schrijft geldige PDF 1.4 bytes rechtstreeks (geen jsPDF: geen
|
|
// netwerktoegang om dat te vendoren). Ondersteunt lijnen, rechthoeken,
|
|
// tekst (standaard Helvetica, geen embedding nodig) en kleuren, over
|
|
// meerdere pagina's. Coordinaten in PDF-punten (1pt = 1/72 inch),
|
|
// oorsprong linksonder — precies zoals PDF dat zelf verwacht.
|
|
//
|
|
// Puur string/byte-opbouw, geen browser-specifieke API's nodig om het
|
|
// DOCUMENT te bouwen (alleen het downloaden ervan is browser-specifiek,
|
|
// zie savePdfBlob hieronder).
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
// Ruwe schatting van Helvetica-tekstbreedte (geen AFM-metrics embedded).
|
|
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 };
|
|
}
|
|
|
|
// Zet een Latin1-string (elk char = 1 byte, zie hierboven) om naar een
|
|
// Blob en start een download. Browser-specifiek, puur I/O — de opbouw
|
|
// hierboven is dat niet.
|
|
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);
|
|
}
|