Skip to content

Commit 8375353

Browse files
anyechDr JCaivincentkoc
authored
fix(pdf): resolve standard fonts from pdfjs package root (#70936)
* fix(pdf): resolve standard fonts from pdfjs package root Resolve PDF.js standard fonts via pdfjs-dist/package.json instead of a relative ../../node_modules path so the fallback renderer does not depend on emitted dist chunk layout. Add focused regression coverage that asserts the forwarded standardFontDataUrl matches the installed pdfjs-dist package root and exists on disk. * fix(pdf): resolve pdfjs standard fonts from package root * fix(pdf): use PDF.js font URL separator --------- Co-authored-by: Dr JCai <[email protected]> Co-authored-by: vincentkoc <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
1 parent 2d748e4 commit 8375353

4 files changed

Lines changed: 85 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- Security/QQBot: sanitize debug log arguments before writing to `console.*`, so gateway payload fields cannot forge extra log lines when debug logging is enabled. Thanks @vincentkoc.
4646
- CLI/agents/status: keep `openclaw agents`, text `agents list`, and plain text `status` on read-only metadata paths so human output no longer preloads plugin runtimes or live channel scans before printing. Fixes #74195. Thanks @NianJiuZst.
4747
- Agents/local models: derive context-window guard thresholds from the effective model window with 4k/8k safety floors, so small local models are no longer rejected by fixed 16k/32k preflight cutoffs. Fixes #42999. Thanks @chengjialu8888.
48+
- PDF extraction: resolve PDF.js standard fonts from the installed package root and pass a filesystem path to the Node fallback extractor, so built-in font PDFs render without `file://` URL lookup failures. Fixes #51455; carries forward #70936, #54447, and #62175. Thanks @anyech, @JuanRdBO, and @solomonneas.
4849
- Media: treat legacy Word/OLE attachments with `application/msword` or `application/x-cfb` MIME as binary so printable-looking `.doc` files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.
4950
- Config: accept documented `browser.tabCleanup` keys in strict root config validation, so configured tab cleanup no longer fails before runtime reads it. Fixes #74577. Thanks @lonexreb and @ezdlp.
5051
- Cron: validate disabled job schedule edits before persisting updates, so invalid cron changes no longer partially mutate stored jobs. Fixes #74459. Thanks @yfge.

extensions/document-extract/document-extractor.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import { existsSync } from "node:fs";
2+
import { createRequire } from "node:module";
3+
import path from "node:path";
14
import { beforeEach, describe, expect, it, vi } from "vitest";
25

3-
const { canvasSizes, pdfDocument } = vi.hoisted(() => ({
6+
const { canvasSizes, getDocumentMock, pdfDocument } = vi.hoisted(() => ({
47
canvasSizes: [] as Array<{ width: number; height: number }>,
8+
getDocumentMock: vi.fn(),
59
pdfDocument: {
610
numPages: 2,
711
getPage: vi.fn(async () => ({
@@ -16,7 +20,7 @@ const { canvasSizes, pdfDocument } = vi.hoisted(() => ({
1620
}));
1721

1822
vi.mock("pdfjs-dist/legacy/build/pdf.mjs", () => ({
19-
getDocument: vi.fn(() => ({ promise: Promise.resolve(pdfDocument) })),
23+
getDocument: getDocumentMock,
2024
}));
2125

2226
vi.mock("@napi-rs/canvas", () => ({
@@ -30,9 +34,13 @@ vi.mock("@napi-rs/canvas", () => ({
3034

3135
import { createPdfDocumentExtractor } from "./document-extractor.js";
3236

37+
const require = createRequire(import.meta.url);
38+
3339
describe("PDF document extractor", () => {
3440
beforeEach(() => {
3541
canvasSizes.length = 0;
42+
getDocumentMock.mockReset();
43+
getDocumentMock.mockReturnValue({ promise: Promise.resolve(pdfDocument) });
3644
pdfDocument.getPage.mockClear();
3745
});
3846

@@ -59,4 +67,34 @@ describe("PDF document extractor", () => {
5967
expect(result?.images).toHaveLength(1);
6068
expect(canvasSizes).toEqual([{ width: 10, height: 10 }]);
6169
});
70+
71+
it("passes standardFontDataUrl to pdfjs getDocument as a package-root filesystem path", async () => {
72+
const extractor = createPdfDocumentExtractor();
73+
74+
await extractor.extract({
75+
buffer: Buffer.from("%PDF-1.4"),
76+
mimeType: "application/pdf",
77+
maxPages: 1,
78+
maxPixels: 4_000_000,
79+
minTextChars: 200,
80+
});
81+
82+
expect(getDocumentMock).toHaveBeenCalledTimes(1);
83+
const [params] = getDocumentMock.mock.calls[0] ?? [];
84+
expect(params).toMatchObject({
85+
disableWorker: true,
86+
});
87+
expect(typeof params.standardFontDataUrl).toBe("string");
88+
89+
const expectedStandardFontDataUrl =
90+
path.join(path.dirname(require.resolve("pdfjs-dist/package.json")), "standard_fonts") + "/";
91+
expect(params.standardFontDataUrl).toBe(expectedStandardFontDataUrl);
92+
expect(path.isAbsolute(params.standardFontDataUrl)).toBe(true);
93+
expect(params.standardFontDataUrl.endsWith("/")).toBe(true);
94+
expect(params.standardFontDataUrl.startsWith("file://")).toBe(false);
95+
expect(existsSync(params.standardFontDataUrl)).toBe(true);
96+
expect(existsSync(path.join(params.standardFontDataUrl, "LiberationSans-Regular.ttf"))).toBe(
97+
true,
98+
);
99+
});
62100
});

extensions/document-extract/document-extractor.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { createRequire } from "node:module";
2+
import path from "node:path";
13
import type {
24
DocumentExtractedImage,
35
DocumentExtractionRequest,
46
DocumentExtractionResult,
57
DocumentExtractorPlugin,
68
} from "openclaw/plugin-sdk/document-extractor";
9+
import type * as PdfJsLegacy from "pdfjs-dist/legacy/build/pdf.mjs";
710

811
type CanvasLike = {
912
toBuffer(type: "image/png"): Buffer;
@@ -37,19 +40,17 @@ type PdfDocument = {
3740
getPage(pageNumber: number): Promise<PdfPage>;
3841
};
3942

40-
type PdfJsModule = {
41-
getDocument(params: { data: Uint8Array; disableWorker?: boolean }): {
42-
promise: Promise<PdfDocument>;
43-
};
44-
};
43+
type PdfJsModule = typeof PdfJsLegacy;
4544

4645
const CANVAS_MODULE = "@napi-rs/canvas";
4746
const PDFJS_MODULE = "pdfjs-dist/legacy/build/pdf.mjs";
4847
const MAX_EXTRACTED_TEXT_CHARS = 200_000;
4948
const MAX_RENDER_DIMENSION = 10_000;
49+
const require = createRequire(import.meta.url);
5050

5151
let canvasModulePromise: Promise<CanvasModule> | null = null;
5252
let pdfJsModulePromise: Promise<PdfJsModule> | null = null;
53+
let pdfJsStandardFontDataPath: string | null = null;
5354

5455
async function loadCanvasModule(): Promise<CanvasModule> {
5556
if (!canvasModulePromise) {
@@ -75,6 +76,15 @@ async function loadPdfJsModule(): Promise<PdfJsModule> {
7576
return pdfJsModulePromise;
7677
}
7778

79+
function resolvePdfJsStandardFontDataPath(): string {
80+
if (!pdfJsStandardFontDataPath) {
81+
const pdfJsPackageJsonPath = require.resolve("pdfjs-dist/package.json");
82+
pdfJsStandardFontDataPath =
83+
path.join(path.dirname(pdfJsPackageJsonPath), "standard_fonts") + "/";
84+
}
85+
return pdfJsStandardFontDataPath;
86+
}
87+
7888
function appendTextWithinLimit(parts: string[], pageText: string, currentLength: number): number {
7989
if (!pageText) {
8090
return currentLength;
@@ -139,10 +149,11 @@ async function extractPdfContent(
139149
request: DocumentExtractionRequest,
140150
): Promise<DocumentExtractionResult> {
141151
const pdfJsModule = await loadPdfJsModule();
142-
const pdf = await pdfJsModule.getDocument({
152+
const pdf = (await pdfJsModule.getDocument({
143153
data: new Uint8Array(request.buffer),
144154
disableWorker: true,
145-
}).promise;
155+
standardFontDataUrl: resolvePdfJsStandardFontDataPath(),
156+
}).promise) as PdfDocument;
146157

147158
const effectivePages: number[] = request.pageNumbers
148159
? request.pageNumbers.filter((p) => p >= 1 && p <= pdf.numPages).slice(0, request.maxPages)

src/types/pdfjs-dist-legacy.d.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
declare module "pdfjs-dist/legacy/build/pdf.mjs" {
2+
import type {
3+
DocumentInitParameters,
4+
PDFDocumentLoadingTask,
5+
TypedArray,
6+
} from "pdfjs-dist/types/src/display/api.js";
7+
8+
export type LegacyDocumentInitParameters = DocumentInitParameters & {
9+
disableWorker?: boolean;
10+
};
11+
12+
export function getDocument(
13+
src?: string | URL | TypedArray | ArrayBuffer | LegacyDocumentInitParameters,
14+
): PDFDocumentLoadingTask;
15+
16+
export type {
17+
DocumentInitParameters,
18+
PDFDocumentLoadingTask,
19+
PDFDocumentProxy,
20+
PDFPageProxy,
21+
TextContent,
22+
TextItem,
23+
TypedArray,
24+
} from "pdfjs-dist/types/src/display/api.js";
25+
export type { PageViewport } from "pdfjs-dist/types/src/display/display_utils.js";
26+
}

0 commit comments

Comments
 (0)