From 9ab43675deb6429083ede7a5dfda2d51e3dff681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= Date: Thu, 27 Aug 2026 22:48:51 +0200 Subject: [PATCH] feat(viewer): add self-hosted PDFium font fallback and eager WASM streaming fallback --- .../core/components/viewer/LocalEmbedPDF.tsx | 4 ++ .../core/services/pdfiumFontFallback.test.ts | 30 ++++++++++++ .../src/core/services/pdfiumFontFallback.ts | 27 +++++++++++ .../src/core/services/wasmPrecompiler.ts | 48 +++++++++++++------ frontend/editor/vite.config.ts | 6 +++ 5 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 frontend/editor/src/core/services/pdfiumFontFallback.test.ts create mode 100644 frontend/editor/src/core/services/pdfiumFontFallback.ts diff --git a/frontend/editor/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/editor/src/core/components/viewer/LocalEmbedPDF.tsx index 07fe867ed2..7838503f76 100644 --- a/frontend/editor/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/editor/src/core/components/viewer/LocalEmbedPDF.tsx @@ -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 diff --git a/frontend/editor/src/core/services/pdfiumFontFallback.test.ts b/frontend/editor/src/core/services/pdfiumFontFallback.test.ts new file mode 100644 index 0000000000..a0172b8bb9 --- /dev/null +++ b/frontend/editor/src/core/services/pdfiumFontFallback.test.ts @@ -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"); + } + }); +}); diff --git a/frontend/editor/src/core/services/pdfiumFontFallback.ts b/frontend/editor/src/core/services/pdfiumFontFallback.ts new file mode 100644 index 0000000000..0af0638935 --- /dev/null +++ b/frontend/editor/src/core/services/pdfiumFontFallback.ts @@ -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", + }, + }; +} diff --git a/frontend/editor/src/core/services/wasmPrecompiler.ts b/frontend/editor/src/core/services/wasmPrecompiler.ts index dccd0f12e2..085c9a952e 100644 --- a/frontend/editor/src/core/services/wasmPrecompiler.ts +++ b/frontend/editor/src/core/services/wasmPrecompiler.ts @@ -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 => { + 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)); } diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts index 22b8ce4e24..d6342a0b74 100644 --- a/frontend/editor/vite.config.ts +++ b/frontend/editor/vite.config.ts @@ -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(),