thunderbird2docuware/lib/pdf.js

173 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, "\\$&");
},
// Unicode-Sonderzeichen, die NICHT in Latin-1, aber in WinAnsiEncoding liegen
// (0x800x9F). Da die Schrift /WinAnsiEncoding nutzt und Bytes per charCodeAt&0xFF
// geschrieben werden, rendern diese so als echte Typografie-Zeichen statt als '?'.
_WINANSI: {
"€": 0x80, "": 0x82, "ƒ": 0x83, "„": 0x84, "…": 0x85,
"†": 0x86, "‡": 0x87, "ˆ": 0x88, "‰": 0x89, "Š": 0x8A,
"": 0x8B, "Œ": 0x8C, "Ž": 0x8E, "": 0x91, "": 0x92,
"“": 0x93, "”": 0x94, "•": 0x95, "": 0x96, "—": 0x97,
"˜": 0x98, "™": 0x99, "š": 0x9A, "": 0x9B, "œ": 0x9C,
"ž": 0x9E, "Ÿ": 0x9F,
},
_toLatin1(s) {
let out = "";
for (const ch of (s || "")) {
const cp = ch.codePointAt(0);
if (cp <= 0xff) out += ch; // Latin-1 direkt
else if (this._WINANSI[ch] != null) out += String.fromCharCode(this._WINANSI[ch]);
else out += "?"; // nicht darstellbar
}
return out;
},
_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 };