feat(viewer): add self-hosted PDFium font fallback and eager WASM streaming fallback

This commit is contained in:
Balázs Szücs
2026-08-27 22:48:51 +02:00
parent be13028209
commit 9ab43675de
5 changed files with 101 additions and 14 deletions
@@ -100,6 +100,7 @@ import { DocumentPermissionsAPIBridge } from "@app/components/viewer/DocumentPer
import { DocumentReadyWrapper } from "@app/components/viewer/DocumentReadyWrapper";
import { ActiveDocumentProvider } from "@app/components/viewer/ActiveDocumentContext";
import { pdfiumWasmUrl } from "@app/services/wasmPrecompiler";
import { getLocalFontFallbackConfig } from "@app/services/pdfiumFontFallback";
import { FormFieldOverlay } from "@app/tools/formFill/FormFieldOverlay";
import { FormCreationInteractionLock } from "@app/tools/formFill/FormCreationInteractionLock";
import { FormFieldCreationOverlay } from "@app/tools/formFill/FormFieldCreationOverlay";
@@ -437,9 +438,12 @@ export function LocalEmbedPDF({
];
}, [pdfUrl, enableAnnotations, exportFileName]);
const fontFallbackConfig = useMemo(() => getLocalFontFallbackConfig(), []);
// Initialize the engine with the React hook - use local WASM for offline support
const { engine, isLoading, error } = usePdfiumEngine({
wasmUrl: pdfiumWasmUrl,
fontFallback: fontFallbackConfig,
});
// Early return if no file or URL provided
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { FontCharset } from "@embedpdf/models";
import { getLocalFontFallbackConfig } from "@app/services/pdfiumFontFallback";
describe("pdfiumFontFallback", () => {
it("generates a self-hosted font fallback configuration without external CDN URLs", () => {
const config = getLocalFontFallbackConfig();
expect(config.baseUrl).toContain("/fonts");
expect(config.defaultFont).toBe("NotoSans-Regular.ttf");
expect(config.fonts[FontCharset.ANSI]).toBe("NotoSans-Regular.ttf");
expect(config.fonts[FontCharset.DEFAULT]).toBe("NotoSans-Regular.ttf");
expect(config.fonts[FontCharset.SHIFTJIS]).toBe("NotoSansJP-Regular.ttf");
expect(config.fonts[FontCharset.HANGEUL]).toBe("NotoSansKR-Regular.ttf");
expect(config.fonts[FontCharset.GB2312]).toBe("NotoSansSC-Regular.ttf");
expect(config.fonts[FontCharset.CHINESEBIG5]).toBe(
"NotoSansTC-Regular.ttf",
);
expect(config.fonts[FontCharset.ARABIC]).toBe("NotoSansArabic-Regular.ttf");
expect(config.fonts[FontCharset.THAI]).toBe("NotoSansThai-Regular.ttf");
expect(config.baseUrl).not.toContain("jsdelivr");
for (const fontVal of Object.values(config.fonts)) {
expect(String(fontVal)).not.toContain("http://");
expect(String(fontVal)).not.toContain("https://");
expect(String(fontVal)).not.toContain("jsdelivr");
}
});
});
@@ -0,0 +1,27 @@
import { BASE_PATH } from "@app/constants/app";
import type { FontFallbackConfig } from "@embedpdf/engines";
import { FontCharset } from "@embedpdf/models";
export function getLocalFontFallbackConfig(): FontFallbackConfig {
const origin = typeof window !== "undefined" ? window.location.origin : "";
const baseUrl = `${origin}${BASE_PATH}/fonts`;
return {
baseUrl,
defaultFont: "NotoSans-Regular.ttf",
fonts: {
[FontCharset.ANSI]: "NotoSans-Regular.ttf",
[FontCharset.DEFAULT]: "NotoSans-Regular.ttf",
[FontCharset.CYRILLIC]: "NotoSans-Regular.ttf",
[FontCharset.GREEK]: "NotoSans-Regular.ttf",
[FontCharset.VIETNAMESE]: "NotoSans-Regular.ttf",
[FontCharset.EASTERNEUROPEAN]: "NotoSans-Regular.ttf",
[FontCharset.ARABIC]: "NotoSansArabic-Regular.ttf",
[FontCharset.THAI]: "NotoSansThai-Regular.ttf",
[FontCharset.SHIFTJIS]: "NotoSansJP-Regular.ttf",
[FontCharset.HANGEUL]: "NotoSansKR-Regular.ttf",
[FontCharset.GB2312]: "NotoSansSC-Regular.ttf",
[FontCharset.CHINESEBIG5]: "NotoSansTC-Regular.ttf",
},
};
}
@@ -32,20 +32,40 @@ export function startEagerWasmCompilation(): void {
if (compilationStarted) return;
compilationStarted = true;
if (
typeof WebAssembly === "object" &&
typeof WebAssembly.compileStreaming === "function"
) {
WebAssembly.compileStreaming(fetch(pdfiumWasmUrl))
.then(resolvePromise)
.catch((err) => {
console.warn(
"Eager WASM compilation failed or not supported in this environment:",
err,
);
resolvePromise(null);
});
} else {
if (typeof WebAssembly !== "object") {
resolvePromise(null);
return;
}
const compileWithFallback = async (): Promise<WebAssembly.Module | null> => {
try {
if (typeof WebAssembly.compileStreaming === "function") {
try {
return await WebAssembly.compileStreaming(fetch(pdfiumWasmUrl));
} catch (streamingErr) {
console.warn(
"WASM compileStreaming failed, falling back to ArrayBuffer:",
streamingErr,
);
}
}
// compileStreaming requires application/wasm MIME; fall back to ArrayBuffer if the server or proxy serves octet-stream.
const res = await fetch(pdfiumWasmUrl);
if (!res.ok) {
throw new Error(
`Failed to fetch WASM: ${res.status} ${res.statusText}`,
);
}
const buffer = await res.arrayBuffer();
return await WebAssembly.compile(buffer);
} catch (err) {
console.warn("WASM compilation failed:", err);
return null;
}
};
compileWithFallback()
.then(resolvePromise)
.catch(() => resolvePromise(null));
}
+6
View File
@@ -256,6 +256,7 @@ export default defineConfig(async ({ mode, command }) => {
"/login/saml2": backendProxy,
"/swagger-ui": backendProxy,
"/v1/api-docs": backendProxy,
"/fonts": backendProxy,
};
return {
@@ -340,6 +341,11 @@ export default defineConfig(async ({ mode, command }) => {
src: "src/core/assets/brand/modern-logo/*",
dest: "modern-logo",
},
{
// Fallback TrueType fonts for PDFium (Noto Sans, CJK, Arabic, etc.)
src: "../../app/core/src/main/resources/static/fonts/*.ttf",
dest: "fonts",
},
],
}),
compressStaticCopyPlugin(),