Fix Swedish åäö rendering as mojibake in PDF exports
buildSimplePdf's font declared no /Encoding at all, so PDF viewers fell back to a standard font's built-in encoding, which has no å/ä/ö glyphs - and separately, the content stream was written as UTF-8 instead of a single-byte encoding, so even a correctly-encoded font would receive the wrong bytes (a multi-byte UTF-8 sequence where PDF expects exactly one byte per character): the classic "åäö" mojibake pattern. Fixed by declaring /Encoding /WinAnsiEncoding and writing the content stream through a proper WinAnsi (~CP1252) byte mapper instead of UTF-8. This is the single shared renderer behind every PDF export (race results, start lists, heat sheets, team reports, the results archive), so the fix applies everywhere at once, matching what was reported. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
30
server.js
30
server.js
@@ -662,7 +662,7 @@ function buildSimplePdf(payload) {
|
||||
|
||||
objects.push(createPdfTextObject(1, "<< /Type /Catalog /Pages 2 0 R >>"));
|
||||
objects.push(createPdfTextObject(2, `<< /Type /Pages /Count ${pages.length} /Kids [${pageObjectIds.map((id) => `${id} 0 R`).join(" ")}] >>`));
|
||||
objects.push(createPdfTextObject(fontObjectId, "<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>"));
|
||||
objects.push(createPdfTextObject(fontObjectId, "<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>"));
|
||||
|
||||
if (logo && imageObjectId) {
|
||||
objects.push(
|
||||
@@ -678,7 +678,7 @@ function buildSimplePdf(payload) {
|
||||
const resources = imageObjectId
|
||||
? `<< /Font << /F1 ${fontObjectId} 0 R >> /XObject << /Im1 ${imageObjectId} 0 R >> >>`
|
||||
: `<< /Font << /F1 ${fontObjectId} 0 R >> >>`;
|
||||
const contentStream = Buffer.from(
|
||||
const contentStream = pdfWinAnsiBuffer(
|
||||
buildPdfContentStream(
|
||||
pages[index],
|
||||
marginLeft,
|
||||
@@ -692,8 +692,7 @@ function buildSimplePdf(payload) {
|
||||
height: logo.displayHeight,
|
||||
}
|
||||
: null
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
objects.push(
|
||||
createPdfTextObject(
|
||||
@@ -943,6 +942,29 @@ function escapePdfText(text) {
|
||||
.replaceAll(")", "\\)");
|
||||
}
|
||||
|
||||
// The content stream is declared with /Encoding /WinAnsiEncoding (see buildSimplePdf), so every
|
||||
// character must become exactly one WinAnsi (~CP1252) byte or a PDF viewer renders garbage/boxes
|
||||
// for anything outside plain ASCII - accented letters most of all (å, ä, ö, ...). WinAnsi matches
|
||||
// Latin-1 one-for-one above 0x9F (where å/ä/ö/Å/Ä/Ö all live), so a plain code-point clamp covers
|
||||
// Swedish text; only the narrow 0x80-0x9F "smart punctuation" block needs its own mapping.
|
||||
const PDF_WIN_ANSI_OVERRIDES = {
|
||||
0x20ac: 0x80, 0x201a: 0x82, 0x0192: 0x83, 0x201e: 0x84, 0x2026: 0x85, 0x2020: 0x86, 0x2021: 0x87,
|
||||
0x02c6: 0x88, 0x2030: 0x89, 0x0160: 0x8a, 0x2039: 0x8b, 0x0152: 0x8c, 0x017d: 0x8e,
|
||||
0x2018: 0x91, 0x2019: 0x92, 0x201c: 0x93, 0x201d: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
|
||||
0x02dc: 0x98, 0x2122: 0x99, 0x0161: 0x9a, 0x203a: 0x9b, 0x0153: 0x9c, 0x017e: 0x9e, 0x0178: 0x9f,
|
||||
};
|
||||
|
||||
function pdfWinAnsiBuffer(text) {
|
||||
const bytes = [];
|
||||
for (const char of String(text || "")) {
|
||||
const code = char.codePointAt(0);
|
||||
if (code < 0x80 || (code > 0x9f && code <= 0xff)) bytes.push(code);
|
||||
else if (PDF_WIN_ANSI_OVERRIDES[code] !== undefined) bytes.push(PDF_WIN_ANSI_OVERRIDES[code]);
|
||||
else bytes.push(0x3f); // "?" - outside what a standard PDF font/WinAnsiEncoding can represent
|
||||
}
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
function getLanUrls(port) {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const urls = [];
|
||||
|
||||
Reference in New Issue
Block a user