Skip to content

fix(document-extract): set standardFontDataUrl for standard-font PDFs#74355

Closed
solomonneas wants to merge 3 commits into
openclaw:mainfrom
solomonneas:fix/pdf-standard-font-data-url-plugin
Closed

fix(document-extract): set standardFontDataUrl for standard-font PDFs#74355
solomonneas wants to merge 3 commits into
openclaw:mainfrom
solomonneas:fix/pdf-standard-font-data-url-plugin

Conversation

@solomonneas

Copy link
Copy Markdown
Contributor

Summary

  • Problem: PDFs that reference any of the 14 standard fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats — i.e. nearly every real-world PDF) extract to empty text and emit UnknownErrorException: Ensure that the standardFontDataUrl API parameter is provided per page. Downstream callers then send the model an empty document, the model rejects, and the agent run times out.
  • Why it matters: Out-of-the-box PDF extraction is broken for the common case. PR refactor(pdf): move document extraction to plugin #71278 moved PDF extraction to the bundled document-extract plugin and noted "preserving default PDF extraction behavior", but the plugin's getDocument call and its PdfJsModule type both omit standardFontDataUrl, so the standard fonts pdf.js ships under pdfjs-dist/standard_fonts/ are never wired up.
  • What changed: extensions/document-extract/document-extractor.ts now resolves the standard-font directory via createRequire(import.meta.url).resolve("pdfjs-dist/package.json")dirname + "/standard_fonts/" + sep, caches it across calls, types it on PdfJsModule.getDocument, and passes it on every call. A real-PDF integration test using a 518-byte Helvetica fixture proves text extraction now succeeds and that pdf.js no longer warns about the missing parameter.
  • What did NOT change (scope boundary): no other plugin files touched; existing mock-based unit tests are untouched; the SDK contract, manifest, and exported surface are unchanged; canvas/image extraction path is unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/document-extract/document-extractor.standard-fonts.test.ts
  • Scenario the test should lock in: extract from a real PDF that uses Helvetica (one of the standard fonts) using createPdfDocumentExtractor() end-to-end with the actual pdfjs-dist module — assert non-empty extracted text and no standardFontDataUrl-related warnings on stderr.
  • Why this is the smallest reliable guardrail: A pure unit test can stub pdfjs-dist and pass even if the real call site is wrong (this is how the regression was missed). The integration test loads pdf.js for real and exercises both the type signature and the resolved path, including the file:// vs filesystem-path footgun. The fixture is a 518-byte inline base64 PDF, so the test stays fast (~120 ms) and self-contained.
  • Existing test that already covers this (if any): None. document-extractor.test.ts mocks pdfjs-dist end-to-end, which cannot detect a missing param on the real module.
  • If no new test is added, why not: N/A — test is added.

User-visible / Behavior Changes

PDF text extraction now works for PDFs that use any of the 14 standard PDF fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats) — i.e. the common case. Previously these PDFs returned empty text plus per-page warnings; afterwards they return the actual page text. No config or default changes.

Diagram (if applicable)

Before:
[user uploads PDF using Helvetica]
  -> extractor.extract()
    -> pdfjs.getDocument({ data, disableWorker: true })
      -> pdf.js: "Ensure that the standardFontDataUrl API parameter is provided"
    -> page.getTextContent() returns items: []
  -> result.text === ""
  -> downstream: "Instructions are required" + agent timeout

After:
[user uploads PDF using Helvetica]
  -> extractor.extract()
    -> pdfjs.getDocument({ data, disableWorker: true,
                           standardFontDataUrl: <pdfjs-dist>/standard_fonts/ })
    -> page.getTextContent() returns real items
  -> result.text === "Hello PDF World"

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? NocreateRequire(import.meta.url).resolve("pdfjs-dist/package.json") only resolves a path inside the plugin's already-bundled optional dependency. The resolved path is read by pdf.js at extraction time, identical to how it loads any other shipped resource.

Repro + Verification

Environment

  • OS: Ubuntu 24.04 (LXC), Node 22.22.2, pnpm 10.33.0
  • Runtime/container: openclaw gateway, plugin loader runs the document-extract bundled plugin
  • Model/provider: any (the bug is local to PDF extraction; it was originally surfaced when the model received empty text)
  • Integration/channel (if any): any channel that accepts file uploads
  • Relevant config (redacted): default; no plugin config required

Steps

  1. Build a 1-page PDF that uses Helvetica (or use the inline fixture in the new test). The repro fixture is JVBERi0xLjQK... (518 bytes, deterministic).
  2. Without this PR: call createPdfDocumentExtractor().extract({ buffer, mimeType: "application/pdf", maxPages: 1, maxPixels: 2_000_000, minTextChars: 5 }).
  3. With this PR: same call.

Expected

  • Result text contains the page's text content (e.g. "Hello PDF World").
  • No Warning: UnknownErrorException: Ensure that the standardFontDataUrl API parameter is provided on stderr.

Actual

  • Without this PR: text === "", stderr fills with standardFontDataUrl warnings (one per page).
  • With this PR: text === "Hello PDF World", stderr clean. Verified by the new integration test which captures console.warn and asserts no standardFontDataUrl-mentioning warnings.

Evidence

  • Failing test/log before + passing after

The new test file (extensions/document-extract/document-extractor.standard-fonts.test.ts) is the proof. Without the production code change, the assertion expect(result?.text).toContain("Hello PDF World") fails because result.text === "", and the fontWarnings array picks up the standardFontDataUrl warning. With the fix, the test passes in 89 ms.

Test run output (clean dev container, this branch):

✓ extensions extensions/document-extract/document-extractor.test.ts (2 tests) 14ms
✓ extensions extensions/document-extract/document-extractor.standard-fonts.test.ts (2 tests) 89ms
Test Files  2 passed (2)
     Tests  4 passed (4)

Targeted typecheck output:

pnpm tsgo:extensions          → clean
pnpm tsgo:extensions:test     → clean

Full build output:

pnpm build  → 14 sequential steps green in 29 s

Human Verification (required)

  • Verified scenarios:
    • Real-PDF integration test runs against the actual pdfjs-dist module, not a mock; asserts both content extraction and absence of the standardFontDataUrl warning.
    • Confirmed the resolved path is a plain filesystem path (with sep trailing separator), not a file:// URL — pdf.js silently rejects the URL form on Node, which was the trap that surfaced during PR Media: fix PDF text extraction by setting standardFontDataUrl #62175 review.
    • Confirmed mock-based unit tests in the existing document-extractor.test.ts still pass unchanged (the new param is optional in the type, so mock fixtures don't need updating).
    • Second test in the new file extracts twice and asserts the result text is stable across repeated calls. (Note: this exercises the no-leak path but does not by itself prove the resolver cache is hit on the second call — the resolver's string | false | null cache state is exercised in the same process for both calls but the cache hit is internal.)
  • Edge cases checked:
    • pdfjs-dist not installed → createRequire resolution throws, cache becomes "", function returns undefined, behavior matches the pre-PR call shape (no regression for the optional-dep path).
    • PDF with no embedded text but a standard font in the resources → still falls through to canvas image extraction; canvas path untouched by this PR.
  • What you did not verify:
    • Did not exercise non-Helvetica standard fonts (Times, Courier, Symbol, ZapfDingbats) individually — pdf.js wires them all through the same standardFontDataUrl mechanism, so Helvetica is a representative smoke; happy to add fixtures if reviewers want them.
    • Did not run OPENCLAW_LIVE_TEST=1 pnpm test:live against external providers — this PR is local to PDF parsing and does not change provider plumbing.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? YesstandardFontDataUrl is optional on the getDocument param type (consumers' existing code shape continues to compile); the call site adds it unconditionally so default extraction behavior just gets better.
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: createRequire(import.meta.url).resolve("pdfjs-dist/package.json") could fail in environments where the optional dependency is unavailable.
    • Mitigation: wrapped in try/catch; on failure the cache becomes "" and the function returns undefined. Calling getDocument with standardFontDataUrl: undefined is the pre-PR shape, so we degrade to current behavior rather than crash. The existing optional-dependency error path (Optional dependency pdfjs-dist is required for PDF extraction) still owns the actually-missing case.
  • Risk: cross-platform path correctness. pdf.js's getFactoryUrlProp (pdfjs-dist/legacy/build/pdf.mjs:15024) hard-checks val.endsWith("/") and throws Invalid factory url ... must include trailing slash otherwise — it's specifically a forward slash, not platform-sep.
    • Mitigation: using node:path's join(dirname(pkgPath), "standard_fonts") to compose the directory portion (so the path itself is platform-correct), then unconditionally appending / for the trailing separator pdf.js requires. On Windows this yields C:\...\standard_fonts/; Node's fs accepts mixed separators on Windows, and pdf.js concatenates ${standardFontDataUrl}${filename} (pdfjs-dist/legacy/build/pdf.worker.mjs:40585) so the final fetch path is well-formed on both platforms.

@greptile-apps

greptile-apps Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes PDF text extraction for the common case of PDFs that use any of the 14 standard fonts (Helvetica, Times, Courier, etc.) by resolving and passing standardFontDataUrl to pdf.js's getDocument call. The resolution is cached module-level with a string | false | null sentinel and degrades gracefully (returns undefined, preserving the pre-PR call shape) when pdfjs-dist is absent. An inline-fixture integration test locks in the regression.

Confidence Score: 4/5

Safe to merge — fix is minimal, well-tested, and degrades gracefully when the optional dependency is absent.

Only P2 style observations; the production logic is correct. Score held at 4 (P2s only).

No files require special attention; the one style note is on the test's console.warn monkey-patching pattern.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/document-extract/document-extractor.standard-fonts.test.ts
Line: 27-29

Comment:
**Prefer `vi.spyOn` over manual `console.warn` reassignment**

Directly overwriting `console.warn` works but can suppress warnings from other concurrent test files if Vitest runs them in the same worker. `vi.spyOn(console, 'warn')` is the idiomatic alternative — it wraps the original, captures calls, and is automatically restored after the test (no `finally` needed).

```suggestion
    const warnSpy = vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
      warnings.push(args.map((arg) => String(arg)).join(" "));
    });

    try {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/document-extract/document-extractor.standard-fonts.test.ts
Line: 44-46

Comment:
**Restore spy in `finally` with `vi.spyOn` approach**

If you adopt `vi.spyOn`, remove the manual restore below — vitest restores it automatically after the test. If keeping the manual approach, the existing `finally { console.warn = originalWarn; }` is the correct safety net.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Document-extract: address codex review (..." | Re-trigger Greptile

Comment on lines +27 to +29
maxPages: 1,
maxPixels: 2_000_000,
minTextChars: 5,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer vi.spyOn over manual console.warn reassignment

Directly overwriting console.warn works but can suppress warnings from other concurrent test files if Vitest runs them in the same worker. vi.spyOn(console, 'warn') is the idiomatic alternative — it wraps the original, captures calls, and is automatically restored after the test (no finally needed).

Suggested change
maxPages: 1,
maxPixels: 2_000_000,
minTextChars: 5,
const warnSpy = vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
warnings.push(args.map((arg) => String(arg)).join(" "));
});
try {
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/document-extract/document-extractor.standard-fonts.test.ts
Line: 27-29

Comment:
**Prefer `vi.spyOn` over manual `console.warn` reassignment**

Directly overwriting `console.warn` works but can suppress warnings from other concurrent test files if Vitest runs them in the same worker. `vi.spyOn(console, 'warn')` is the idiomatic alternative — it wraps the original, captures calls, and is automatically restored after the test (no `finally` needed).

```suggestion
    const warnSpy = vi.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
      warnings.push(args.map((arg) => String(arg)).join(" "));
    });

    try {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +44 to +46
const first = await extractor.extract({
buffer: helloPdfBuffer,
mimeType: "application/pdf",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Restore spy in finally with vi.spyOn approach

If you adopt vi.spyOn, remove the manual restore below — vitest restores it automatically after the test. If keeping the manual approach, the existing finally { console.warn = originalWarn; } is the correct safety net.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/document-extract/document-extractor.standard-fonts.test.ts
Line: 44-46

Comment:
**Restore spy in `finally` with `vi.spyOn` approach**

If you adopt `vi.spyOn`, remove the manual restore below — vitest restores it automatically after the test. If keeping the manual approach, the existing `finally { console.warn = originalWarn; }` is the correct safety net.

How can I resolve this? If you propose a fix, please make it concise.

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes:

The PR updates the bundled document-extract PDF extractor to resolve pdfjs-dist/standard_fonts/, pass it as standardFontDataUrl to pdf.js, and add a Helvetica PDF integration test.

Maintainer follow-up before merge:

This is already an open implementation PR with a narrow, evidence-backed fix; the remaining action is maintainer review and merge decision, not a separate automated repair PR.

Best possible solution:

Land this PR or an equivalent narrow plugin-owned fix that resolves the local pdfjs-dist/standard_fonts/ directory, passes it to pdf.js for PDF extraction, and keeps the real standard-font PDF regression test without expanding core or SDK contracts.

Acceptance criteria:

  • pnpm test extensions/document-extract/document-extractor.test.ts extensions/document-extract/document-extractor.standard-fonts.test.ts
  • pnpm tsgo:extensions
  • pnpm tsgo:extensions:test
  • pnpm build

What I checked:

Likely related people:

  • vincentkoc: Related PR refactor(pdf): move document extraction to plugin #71278 moved local PDF extraction into the bundled document-extract plugin and established this plugin-owned pdf.js path. (role: introduced behavior; confidence: high; commits: 767dd0d4b4c4, 22737379cd51; files: extensions/document-extract/document-extractor.ts, extensions/document-extract/package.json, src/media/pdf-extract.ts)
  • steipete: The current checkout history attributes the present document-extract files and PDF docs to recent consolidation/release-maintenance commits, making this a likely routing point for current main. (role: recent maintainer; confidence: medium; commits: 81551ac24b69, be8c24633aaa; files: extensions/document-extract/document-extractor.ts, extensions/document-extract/document-extractor.test.ts, src/media/pdf-extract.ts)

Remaining risk / open question:

  • The PR still needs normal maintainer review and CI validation before merge.
  • Greptile noted a low-severity test-style concern around direct console.warn reassignment; it is not a production blocker but may be worth addressing before landing.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 542821cd1e60.

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

What this changes:

The branch resolves pdfjs-dist/standard_fonts/ from the bundled document-extract plugin, passes it as standardFontDataUrl to pdf.js, and adds a Helvetica PDF integration test.

Maintainer follow-up before merge:

This is a valid implementation PR but overlaps with #70936, so the remaining action is maintainer branch selection and possible test/fix transplant rather than a new automated repair job.

Review details

Best possible solution:

Land one narrow plugin-owned fix that resolves the installed pdfjs-dist/standard_fonts/ directory as a slash-terminated filesystem path, passes it to pdf.js, and keeps real regression coverage; then close #51455 and supersede the duplicate PR branch that is not used.

Acceptance criteria:

  • pnpm test extensions/document-extract/document-extractor.test.ts extensions/document-extract/document-extractor.standard-fonts.test.ts
  • pnpm tsgo:extensions
  • pnpm tsgo:extensions:test
  • pnpm build

What I checked:

Likely related people:

  • vincentkoc: PR refactor(pdf): move document extraction to plugin #71278 moved local PDF extraction into the bundled document-extract plugin and established the current plugin-owned pdf.js path; public commit metadata attributes the central migration commits to this maintainer. (role: introduced behavior / recent maintainer; confidence: high; commits: 767dd0d4b4c4, 22737379cd51; files: extensions/document-extract/document-extractor.ts, extensions/document-extract/package.json, src/media/pdf-extract.ts)
  • tyler6204: Prior ClawSweeper history for bug: pdf extraction fallback renderer omits standardFontDataUrl and emits PDF.js warnings in Node #51455 attributes the original PDF analysis fallback renderer and getDocument({ data, disableWorker: true }) shape to this earlier PDF tool work, before it moved into the plugin. (role: original feature introducer; confidence: medium; commits: d0ac1b019517; files: src/media/pdf-extract.ts, src/agents/tools/pdf-tool.ts)
  • steipete: Local history and release provenance show recent release/changelog maintenance around the same PDF extraction files and latest release tag containing the unfixed behavior. (role: recent adjacent maintainer; confidence: medium; commits: be8c24633aaa; files: extensions/document-extract/document-extractor.ts, extensions/document-extract/document-extractor.test.ts, src/media/pdf-extract.ts)

Remaining risk / open question:

Codex review notes: model gpt-5.5, reasoning high; reviewed against 8cf724a381a3.

@solomonneas

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #70936, which landed on main as 83753535eb and applies the same pdfjs-dist/package.json resolution approach plus a regression test asserting the forwarded path matches the installed package root. The release changelog entry for that PR carries forward credit for #62175 and the prior reports.

Sorry for the duplicate filing — I missed #70936 sitting open while focusing on the post-document-extract-refactor regression. Thanks for the carry-forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant