${escapeHtml(t.issued_by)} ${escapeHtml(issuerName)}
${pageWord} ${ofWord}
@@ -655,7 +655,7 @@ export function renderIssuedOrderHtml(
}
.section-content,
.section-content * {
- font-family: Tahoma, sans-serif !important;
+ font-family: "Segoe UI", Tahoma, Arial, sans-serif !important;
}
.section-content { font-size: 14px; }
.section-content h1 { font-size: 20px; }
@@ -667,7 +667,7 @@ export function renderIssuedOrderHtml(
.section-content li { margin-bottom: 0.2em; }
/* Quill fonty – v PDF vynuceno Tahoma */
- [class*="ql-font-"] { font-family: Tahoma, sans-serif !important; }
+ [class*="ql-font-"] { font-family: "Segoe UI", Tahoma, Arial, sans-serif !important; }
.ql-align-center { text-align: center; }
.ql-align-right { text-align: right; }
.ql-align-justify { text-align: justify; }
diff --git a/src/routes/admin/offers-pdf.ts b/src/routes/admin/offers-pdf.ts
index f076a78..73a0b51 100644
--- a/src/routes/admin/offers-pdf.ts
+++ b/src/routes/admin/offers-pdf.ts
@@ -293,7 +293,7 @@ export function renderOfferHtml(
img, table, pre, code { max-width: 100%; }
/* ---- Quill font classes – v PDF vynuceno Tahoma ---- */
- [class*="ql-font-"] { font-family: Tahoma, sans-serif !important; }
+ [class*="ql-font-"] { font-family: "Segoe UI", Tahoma, Arial, sans-serif !important; }
/* ---- Quill alignment ---- */
.ql-align-center { text-align: center; }
@@ -502,7 +502,7 @@ ${indentCSS}
}
.section-content,
.section-content * {
- font-family: Tahoma, sans-serif !important;
+ font-family: "Segoe UI", Tahoma, Arial, sans-serif !important;
}
.section-content { font-size: 14px; }
.section-content h1 { font-size: 20px; }
diff --git a/src/routes/admin/orders-pdf.ts b/src/routes/admin/orders-pdf.ts
index 5769130..08cfe35 100644
--- a/src/routes/admin/orders-pdf.ts
+++ b/src/routes/admin/orders-pdf.ts
@@ -570,7 +570,7 @@ export function renderOrderConfirmationHtml(
.invoice-notes-content li { margin-bottom: 0.2em; }
.invoice-notes-content,
.invoice-notes-content * {
- font-family: Tahoma, sans-serif !important;
+ font-family: "Segoe UI", Tahoma, Arial, sans-serif !important;
}
.invoice-notes-content { font-size: 14px; }
.invoice-notes-content h1 { font-size: 20px; }
@@ -579,7 +579,7 @@ export function renderOrderConfirmationHtml(
.invoice-notes-content h4 { font-size: 15px; }
/* Quill fonty – v PDF vynuceno Tahoma */
- [class*="ql-font-"] { font-family: Tahoma, sans-serif !important; }
+ [class*="ql-font-"] { font-family: "Segoe UI", Tahoma, Arial, sans-serif !important; }
.ql-align-center { text-align: center; }
.ql-align-right { text-align: right; }
.ql-align-justify { text-align: justify; }
diff --git a/src/utils/pdf-shared.ts b/src/utils/pdf-shared.ts
index 4358030..d325f76 100644
--- a/src/utils/pdf-shared.ts
+++ b/src/utils/pdf-shared.ts
@@ -94,7 +94,7 @@ export function buildPdfHeaderTemplate(
logoDataUri: string,
heading: string,
): string {
- return `
+ return `
${logoDataUri ? `

` : ""}
${escapeHtml(heading)}
@@ -108,6 +108,13 @@ export function buildPdfHeaderTemplate(
* and merge adjacent identical spans. Runs AFTER DOMPurify at every call site
* (regex pass = defense-in-depth, not the primary sanitizer).
*/
+/**
+ * Literal CSS color values only: #hex, rgb()/rgba() with numeric components,
+ * or a bare keyword. No quotes, parentheses beyond rgb(a), url(), variables.
+ */
+const SAFE_CSS_COLOR =
+ /^(#[0-9a-f]{3,8}|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*(0|1|0?\.\d+)\s*)?\)|[a-z]{3,20})$/i;
+
export function cleanQuillHtml(html: string | null | undefined): string {
if (!html) return "";
let s = html;
@@ -134,7 +141,28 @@ export function cleanQuillHtml(html: string | null | undefined): string {
);
// Replace with regular space (outside of tags)
s = s.replace(/( )/g, " ");
- s = s.replace(/\s+style\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, "");
+ // Inline styles: Quill writes text/background colors as style attributes.
+ // Keep ONLY color/background-color with literal color values — everything
+ // else (url(), expression(), positioning…) must never reach Puppeteer.
+ s = s.replace(
+ /\s+style\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi,
+ (_m, dq, sq, bare) => {
+ const kept = String(dq ?? sq ?? bare ?? "")
+ .split(";")
+ .map((decl) => {
+ const i = decl.indexOf(":");
+ if (i < 0) return null;
+ const prop = decl.slice(0, i).trim().toLowerCase();
+ const val = decl.slice(i + 1).trim();
+ return (prop === "color" || prop === "background-color") &&
+ SAFE_CSS_COLOR.test(val)
+ ? `${prop}: ${val}`
+ : null;
+ })
+ .filter((d): d is string => d !== null);
+ return kept.length ? ` style="${kept.join("; ")}"` : "";
+ },
+ );
// Merge adjacent spans with same attributes
let prev = "";
while (prev !== s) {