thunderbird2docuware/lib/pdf.js

155 lines
5.0 KiB
JavaScript
Raw Normal View History

// Minimaler, abhängigkeitsfreier PDF-Generator.
// Rendert die E-Mail (Kopf + Klartext-Body) als einfaches Text-PDF.
// Bewusst schlicht (v1): kein HTML-Rendering, keine Bilder — dafür keine
// externen Libraries. Reicht für eine lesbare Aktenkopie der Mail.
const Pdf = {
/**
* Erzeugt ein PDF-File aus den Mail-Metadaten.
* @param {object} meta Ergebnis von Mail.getMeta()
* @returns {File}
*/
fromMail(meta) {
const header = [
`Von: ${meta.senderName ? meta.senderName + " <" + meta.senderEmail + ">" : meta.senderEmail}`,
`An: ${meta.to}`,
meta.cc ? `CC: ${meta.cc}` : null,
`Datum: ${this._fmtDate(meta.date)}`,
`Betreff: ${meta.subject}`,
"",
"----------------------------------------------------------------------",
"",
].filter((l) => l !== null);
const lines = header.concat(this._wrap(meta.bodyText || "", 95));
const bytes = this._build(lines);
const name = `${(meta.subject || "email").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80) || "email"}.pdf`;
return new File([bytes], name, { type: "application/pdf" });
},
// --- PDF-Aufbau -----------------------------------------------------------
_build(allLines) {
const PAGE_W = 595, PAGE_H = 842; // A4 in pt
const MARGIN = 50, FONT = 10, LEADING = 14;
const linesPerPage = Math.floor((PAGE_H - 2 * MARGIN) / LEADING);
// Seiten aufteilen
const pages = [];
for (let i = 0; i < allLines.length; i += linesPerPage) {
pages.push(allLines.slice(i, i + linesPerPage));
}
if (pages.length === 0) pages.push([""]);
const objects = []; // {id, body} body als latin1-String
const addObj = (body) => { objects.push(body); return objects.length; };
// Platzhalter-IDs vorab festlegen
// 1 Catalog, 2 Pages, 3 Font, dann je Seite: PageObj + ContentObj
const catalogId = 1, pagesId = 2, fontId = 3;
objects.length = 0;
objects.push("", "", ""); // reserviere 1..3
const pageIds = [];
const contentIds = [];
pages.forEach((pageLines) => {
const stream = this._contentStream(pageLines, MARGIN, PAGE_H - MARGIN, FONT, LEADING);
const contentId = addObj(
`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`
);
const pageId = addObj(
`<< /Type /Page /Parent ${pagesId} 0 R ` +
`/MediaBox [0 0 ${PAGE_W} ${PAGE_H}] ` +
`/Resources << /Font << /F1 ${fontId} 0 R >> >> ` +
`/Contents ${contentId} 0 R >>`
);
pageIds.push(pageId);
contentIds.push(contentId);
});
objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`;
objects[pagesId - 1] =
`<< /Type /Pages /Count ${pageIds.length} ` +
`/Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] >>`;
objects[fontId - 1] =
`<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>`;
// Serialisieren mit xref
let pdf = "%PDF-1.4\n";
const offsets = [];
objects.forEach((body, idx) => {
offsets[idx] = pdf.length;
pdf += `${idx + 1} 0 obj\n${body}\nendobj\n`;
});
const xrefOffset = pdf.length;
pdf += `xref\n0 ${objects.length + 1}\n`;
pdf += "0000000000 65535 f \n";
offsets.forEach((off) => {
pdf += `${String(off).padStart(10, "0")} 00000 n \n`;
});
pdf +=
`trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\n` +
`startxref\n${xrefOffset}\n%%EOF`;
return this._latin1Bytes(pdf);
},
_contentStream(lines, x, yTop, font, leading) {
let s = `BT\n/F1 ${font} Tf\n${leading} TL\n${x} ${yTop} Td\n`;
lines.forEach((line, i) => {
if (i > 0) s += "T*\n";
s += `(${this._escape(line)}) Tj\n`;
});
s += "ET";
return s;
},
_wrap(text, width) {
const out = [];
(text || "").split(/\r?\n/).forEach((para) => {
if (para.length <= width) { out.push(para); return; }
let line = "";
para.split(/\s+/).forEach((word) => {
if ((line + " " + word).trim().length > width) {
if (line) out.push(line);
// sehr lange Wörter hart umbrechen
while (word.length > width) { out.push(word.slice(0, width)); word = word.slice(width); }
line = word;
} else {
line = line ? `${line} ${word}` : word;
}
});
if (line) out.push(line);
});
return out;
},
_escape(s) {
return this._toLatin1(s).replace(/[\\()]/g, "\\$&");
},
_toLatin1(s) {
// Zeichen außerhalb Latin-1 durch '?' ersetzen.
return (s || "").replace(/[^\x00-\xFF]/g, "?");
},
_latin1Bytes(str) {
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 0xff;
return bytes;
},
_fmtDate(d) {
try {
return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium", timeStyle: "short",
}).format(d);
} catch (_) {
return String(d);
}
},
};
if (typeof module !== "undefined") module.exports = { Pdf };