fix(reader): use blob URLs for fonts to bypass sandboxed iframe restrictions
The paginator renders inside a sandboxed iframe that blocks @font-face URL fetches. Fonts were never loading — all font-family rules fell back to the generic 'serif' system font, making every reading font identical. Fix: fetch font files on the parent page, create blob: URLs via URL.createObjectURL(), and use those blob URLs in the @font-face rules injected into the iframe via setStyles(). Blob URLs are always same-origin with the creating document, so the sandboxed iframe can access them with allow-same-origin. Also added Playwrite NZ Guides as a test font for verifying font switching works.
This commit is contained in:
+67
-132
@@ -12,142 +12,76 @@ const FONT_MAP: Record<string, string> = {
|
||||
"noto-serif": '"Noto Serif"',
|
||||
"charis-sil": '"Charis SIL"',
|
||||
"ibm-plex": '"IBM Plex Serif"',
|
||||
"playwrite": '"Playwrite NZ Guides"',
|
||||
};
|
||||
|
||||
const FONT_FACE_RULES = `
|
||||
@font-face {
|
||||
font-family: "Literata";
|
||||
src: url("/static/fonts/literata/Literata-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
interface FontFile {
|
||||
family: string;
|
||||
url: string;
|
||||
weight: string;
|
||||
style: string;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Literata";
|
||||
src: url("/static/fonts/literata/Literata-Italic-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
|
||||
const FONT_FILES: FontFile[] = [
|
||||
{ family: '"Literata"', url: "/static/fonts/literata/Literata-Variable.woff2", weight: "100 900", style: "normal" },
|
||||
{ family: '"Literata"', url: "/static/fonts/literata/Literata-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
||||
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Variable.woff2", weight: "100 900", style: "normal" },
|
||||
{ family: '"Crimson Pro"', url: "/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
||||
{ family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Variable.woff2", weight: "100 900", style: "normal" },
|
||||
{ family: '"Source Serif 4"', url: "/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
||||
{ family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Variable.woff2", weight: "100 900", style: "normal" },
|
||||
{ family: '"EB Garamond"', url: "/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Regular.woff2", weight: "400", style: "normal" },
|
||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Bold.woff2", weight: "700", style: "normal" },
|
||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-Italic.woff2", weight: "400", style: "italic" },
|
||||
{ family: '"Libertinus Serif"', url: "/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2", weight: "700", style: "italic" },
|
||||
{ family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Variable.woff2", weight: "100 900", style: "normal" },
|
||||
{ family: '"Noto Serif"', url: "/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2", weight: "100 900", style: "italic" },
|
||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Regular.woff2", weight: "400", style: "normal" },
|
||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Bold.woff2", weight: "700", style: "normal" },
|
||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-Italic.woff2", weight: "400", style: "italic" },
|
||||
{ family: '"Charis SIL"', url: "/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2", weight: "700", style: "italic" },
|
||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2", weight: "400", style: "normal" },
|
||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2", weight: "700", style: "normal" },
|
||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2", weight: "400", style: "italic" },
|
||||
{ family: '"IBM Plex Serif"', url: "/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2", weight: "700", style: "italic" },
|
||||
{ family: '"Playwrite NZ Guides"', url: "/static/fonts/Playwrite_NZ_Guides/PlaywriteNZGuides-Regular.woff2", weight: "400", style: "normal" },
|
||||
];
|
||||
|
||||
let fontBlobUrls: Record<string, string> = {};
|
||||
let fontsLoaded = false;
|
||||
|
||||
async function loadFontBlobUrls(): Promise<void> {
|
||||
if (fontsLoaded) return;
|
||||
const entries = await Promise.all(
|
||||
FONT_FILES.map(async (f) => {
|
||||
try {
|
||||
const resp = await fetch(f.url);
|
||||
const blob = await resp.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return { key: f.url, blobUrl };
|
||||
} catch (_e) {
|
||||
return { key: f.url, blobUrl: f.url };
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const { key, blobUrl } of entries) {
|
||||
fontBlobUrls[key] = blobUrl;
|
||||
}
|
||||
fontsLoaded = true;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Crimson Pro";
|
||||
src: url("/static/fonts/crimson/CrimsonPro-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
|
||||
function buildFontFaceCSS(): string {
|
||||
return FONT_FILES.map((f) => {
|
||||
const src = fontBlobUrls[f.url] ?? f.url;
|
||||
return `@font-face {
|
||||
font-family: ${f.family};
|
||||
src: url("${src}") format("woff2");
|
||||
font-weight: ${f.weight};
|
||||
font-style: ${f.style};
|
||||
}`;
|
||||
}).join("\n");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Crimson Pro";
|
||||
src: url("/static/fonts/crimson/CrimsonPro-Italic-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Serif 4";
|
||||
src: url("/static/fonts/source-serif/SourceSerif4-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Serif 4";
|
||||
src: url("/static/fonts/source-serif/SourceSerif4-Italic-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "EB Garamond";
|
||||
src: url("/static/fonts/eb-garamond/EBGaramond-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "EB Garamond";
|
||||
src: url("/static/fonts/eb-garamond/EBGaramond-Italic-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Libertinus Serif";
|
||||
src: url("/static/fonts/libertinus/LibertinusSerif-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Libertinus Serif";
|
||||
src: url("/static/fonts/libertinus/LibertinusSerif-Bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Libertinus Serif";
|
||||
src: url("/static/fonts/libertinus/LibertinusSerif-Italic.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Libertinus Serif";
|
||||
src: url("/static/fonts/libertinus/LibertinusSerif-BoldItalic.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Noto Serif";
|
||||
src: url("/static/fonts/noto-serif/NotoSerif-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Noto Serif";
|
||||
src: url("/static/fonts/noto-serif/NotoSerif-Italic-Variable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Charis SIL";
|
||||
src: url("/static/fonts/charis-sil/CharisSIL-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Charis SIL";
|
||||
src: url("/static/fonts/charis-sil/CharisSIL-Bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Charis SIL";
|
||||
src: url("/static/fonts/charis-sil/CharisSIL-Italic.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Charis SIL";
|
||||
src: url("/static/fonts/charis-sil/CharisSIL-BoldItalic.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Serif";
|
||||
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Regular.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Serif";
|
||||
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Bold.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Serif";
|
||||
src: url("/static/fonts/ibm-plex/IBMPlexSerif-Italic.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Serif";
|
||||
src: url("/static/fonts/ibm-plex/IBMPlexSerif-BoldItalic.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ThemeColors {
|
||||
fg: string;
|
||||
@@ -259,7 +193,7 @@ const getCSS = ({
|
||||
}` : "";
|
||||
|
||||
return `
|
||||
${FONT_FACE_RULES}
|
||||
${buildFontFaceCSS()}
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
@@ -362,6 +296,7 @@ document.addEventListener("alpine:init", () => {
|
||||
const themeColors = this.readingMode === "dark" ? themeSet.dark : themeSet.light;
|
||||
viewport.style.backgroundColor = themeColors.bg;
|
||||
this.view = document.getElementById("reader-view") as any;
|
||||
await loadFontBlobUrls();
|
||||
const resp = await fetch(config.fileUrl, {
|
||||
headers: { Authorization: `Bearer ${getToken()}` },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user