Skip to content

feat: add local PDF OCR with PDFOxide#658

Open
AnthonyRonning wants to merge 4 commits into
masterfrom
codex-maple-pdf-oxide-ocr-maple
Open

feat: add local PDF OCR with PDFOxide#658
AnthonyRonning wants to merge 4 commits into
masterfrom
codex-maple-pdf-oxide-ocr-maple

Conversation

@AnthonyRonning

@AnthonyRonning AnthonyRonning commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace pdf-extract with PDFOxide 0.3.74 for native, scanned, and hybrid PDF handling
  • pin the parser through the narrow OpenSecretCloud/pdf_oxide loader-policy PR; the fork only separates caller-controlled ORT loading from the legacy dynamic-loader feature and does not change parsing, rendering, or OCR algorithms
  • add local English PaddleOCR with immutable model revisions, size/SHA-256 verification, on-demand caching, and progress events
  • use one ort = 2.0.0-rc.11 / ONNX Runtime 1.23.2 stack for PDF OCR and TTS; remove tract; statically link iOS and package dynamically loaded runtimes for macOS, Linux, Windows, and Android
  • contain ordinary parser/OCR Rust panics in a dedicated worker and surface normal attachment errors; required scanned-page OCR failures now reject the upload instead of attaching partial text
  • improve attachment state handling for unsupported selections, removal during processing, backend errors, and stale upload results

End-to-end proof

  • built and launched this workspace's uniquely identified macOS app against its managed OpenSecret/Billing services and a local Pro account
  • selected a genuine 10-page, image-only NASA/NACA archival scan through the native macOS picker; Maple OCR extracted all 10 pages (7,536 characters)
  • with web search disabled, asked Maple for the title/author, aerodynamic problem, assumptions, and mathematical method; its answer correctly identified L. Prandtl, Theory of Lifting Surfaces. Part II, induced resistance of multiplanes, the same-plane assumption, and the direct-image construction
  • independently checked those claims against the OCR output and rendered source pages, so this validates document understanding rather than filename recognition
  • release-profile OCR of the same scan passes in 4.72 seconds on the test Mac
  • a genuine over-budget 600-DPI scan returns the specific first-page error without attaching a misleading partial excerpt

Validation

  • frontend production build/typecheck and 143 tests pass
  • Rust all-target suite: 139 passed, 1 ignored model-backed test; that ignored test passes separately with the real scan and production model pack
  • cargo fmt --check, changed-shell-script bash -n/ShellCheck, git diff --check, and nix flake check --no-build pass after rebasing onto current master
  • full iOS simulator app build and link pass with the static runtime
  • universal macOS package passes for arm64 and x86_64; the packaged app loads the exact pinned universal ONNX Runtime dylib
  • Android's official four-ABI AAR, API 24 floor, exact artifact hashes, 16 KiB ELF alignment, and APK ZIP alignment pass in fresh native GitHub CI
  • focused independent reviews found no major or critical issue; CodeRabbit and Devin also completed cleanly on the final head

Deliberately deferred

  • the model pack is English-only and downloads on first OCR use (12,577,821 bytes); dedicated download UI and more languages are separate product work
  • the existing 24-million-source-pixel safety cap remains, so some 600-DPI archival pages are rejected rather than downsampled; budget/performance tuning needs separate desktop/mobile measurements
  • PDFOxide issue #860 can blank-render some one-bit FlateDecode scans; this is documented and intentionally not patched in the loader-policy fork
  • no parser edge-case patches or speculative OCR optimizations are included

Summary by CodeRabbit

  • New Features

    • Added end-to-end, on-device OCR for scanned/image-based PDFs, using native text when available (hybrid pages supported).
    • Added document uploads for PDF, TXT, and Markdown.
    • Added OCR model download on first use with visible progress.
  • Improvements

    • Prevent sending messages while a document is processing; attachment UI now shows a loading spinner and disables send/submit actions.
    • More reliable uploads during quick clears/updates, with clearer page-specific OCR error reporting and safer handling for oversized/invalid/locked PDFs.
  • Documentation

    • Expanded guidance on PDF extraction/OCR behavior, limitations, and troubleshooting.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3097225
Status: ✅  Deploy successful!
Preview URL: https://fba80549.maple-ca8.pages.dev
Branch Preview URL: https://codex-maple-pdf-oxide-ocr-ma.maple-ca8.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Maple adds PDFOxide extraction with local PaddleOCR, pinned model caching, shared ONNX Runtime 1.23.2 provisioning, Android artifact verification, frontend upload lifecycle guards, and updated desktop/iOS build configuration and documentation.

Changes

Document processing pipeline

Layer / File(s) Summary
OCR dependencies and extraction contracts
frontend/src-tauri/Cargo.toml, frontend/src-tauri/src/pdf_extractor.rs, frontend/src-tauri/src/pdf_ocr.rs, frontend/src/utils/documentUpload.ts
PDFOxide, OCR wiring, extraction limits, and document-upload helpers support PDF, text, and Markdown processing.
Pinned OCR model preparation
frontend/src-tauri/src/pdf_ocr.rs
Pinned model files are cached, verified by size and SHA-256, downloaded atomically with progress events, and initialized once.
Serialized PDF extraction and OCR flow
frontend/src-tauri/src/pdf_extractor.rs
PDFs are preflighted, processed in isolated blocking jobs, OCR-rendered within resource budgets, and merged with native text.
Frontend upload lifecycle and controls
frontend/src/components/UnifiedChat.tsx, frontend/src/utils/documentUpload.ts, frontend/src/utils/documentUpload.test.ts
Uploads use shared helpers, stale results are ignored, errors are normalized, and composer actions reflect processing state.

Shared ONNX Runtime provisioning

Layer / File(s) Summary
Runtime pins and platform contracts
frontend/src-tauri/Cargo.toml, frontend/src-tauri/scripts/onnxruntime-*, frontend/src-tauri/tauri.conf.json
ONNX Runtime 1.23.2 artifacts, hashes, Android mappings, iOS linker flags, dependencies, and bundle paths are updated.
Shared runtime initialization
frontend/src-tauri/src/onnxruntime.rs, frontend/src-tauri/src/tts.rs
A shared lazy initializer resolves platform runtime libraries or uses iOS static linkage, and TTS calls it before loading models.
Desktop and Android provisioning
frontend/src-tauri/scripts/provide-*.sh, frontend/src-tauri/scripts/run-with-desktop-onnxruntime.sh
Verified desktop libraries and Android AAR/JNI libraries are provisioned and exposed to build consumers.
Build and CI integration
justfile, scripts/ci/*, .github/workflows/*, frontend/src-tauri/scripts/build-ios-onnxruntime*.sh
Local recipes and CI prepare, cache, build, and verify ONNX Runtime artifacts across platforms.

Operational documentation

Layer / File(s) Summary
Runtime and OCR documentation
docs/pdf-ocr.md, docs/ios-tts-local-development.md, frontend/src-tauri/resources/windows/README.md, README.md
Documentation describes OCR behavior, model testing, platform loading, Windows DLL selection, and Linux provisioning.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UnifiedChat
  participant Tauri
  participant PDFOxide
  participant OCRModelCache
  participant OcrEngine
  UnifiedChat->>Tauri: submit PDF attachment
  Tauri->>PDFOxide: classify pages and extract native text
  PDFOxide-->>Tauri: native text and OCR requirements
  Tauri->>OCRModelCache: verify or download pinned models
  OCRModelCache-->>Tauri: verified model files
  Tauri->>OcrEngine: process bounded rendered pages
  OcrEngine-->>Tauri: OCR spans
  Tauri-->>UnifiedChat: merged extracted text
Loading

Possibly related PRs

Poem

A bunny scanned a page,
OCR hopped onto the stage.
Models cached with care,
Runtime flies everywhere.
Uploads wait, then send—
Clean words greet their friend!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding local PDF OCR with PDFOxide.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex-maple-pdf-oxide-ocr-maple

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/UnifiedChat.tsx (2)

2359-2438: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cancel parsing when the document is removed.

clearAllAttachments invalidates pending uploads, but removeDocument does not. If a user removes an existing attachment while a replacement PDF is processing, the pending result can still be attached afterward. Invalidate the generation and clear the processing state from removeDocument.

Proposed fix
 const removeDocument = useCallback(() => {
+  documentUploadGenerationRef.current += 1;
+  setIsProcessingDocument(false);
   setDocumentText("");
   setDocumentName("");
 }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/UnifiedChat.tsx` around lines 2359 - 2438, Update
removeDocument to increment documentUploadGenerationRef.current and clear the
document-processing state via setIsProcessingDocument(false), matching
clearAllAttachments. Ensure any pending replacement upload fails its generation
check and cannot attach results after the document is removed.

2362-2428: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report unsupported document selections.

When getSupportedDocumentType() returns null, this handler clears the processing state without attaching anything or showing an error. accept is only a picker hint, so reject this path explicitly.

Proposed fix
         } else if (documentType === "pdf") {
           setAttachmentError("PDF files can only be processed in the Maple app");
           setTimeout(() => setAttachmentError(null), 5000);
+        } else {
+          setAttachmentError("Only PDF, TXT, and Markdown files are supported");
+          setTimeout(() => setAttachmentError(null), 5000);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/UnifiedChat.tsx` around lines 2362 - 2428, Update the
document upload handler around getSupportedDocumentType so a null
unsupported-document result explicitly sets an attachment error and exits
without attaching content, rather than silently clearing processing state.
Preserve the existing text and PDF handling for supported document types, and
use the established attachment-error cleanup behavior.
🧹 Nitpick comments (1)
frontend/src-tauri/src/pdf_extractor.rs (1)

408-419: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Optional: precompute normalized extras to avoid O(n²) re-normalization.

extras.iter().any(|extra| normalize(extra) == normalized) re-runs normalize (a split_whitespace + join + to_lowercase allocation) over every previously kept fragment on each iteration. For image-dense pages with many OCR spans this is quadratic. Store the normalized form alongside the kept fragment.

♻️ Keep normalized alongside each extra
-    let native_normalized = normalize(native_trimmed);
-    let mut extras: Vec<&str> = Vec::new();
+    let native_normalized = normalize(native_trimmed);
+    let mut extras: Vec<&str> = Vec::new();
+    let mut extras_normalized: Vec<String> = Vec::new();
     for fragment in ocr_fragments {
         let fragment = fragment.trim();
         let normalized = normalize(fragment);
         if normalized.is_empty()
             || native_normalized.contains(&normalized)
-            || extras.iter().any(|extra| normalize(extra) == normalized)
+            || extras_normalized.iter().any(|extra| *extra == normalized)
         {
             continue;
         }
+        extras_normalized.push(normalized);
         extras.push(fragment);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src-tauri/src/pdf_extractor.rs` around lines 408 - 419, Update the
extras collection in the OCR fragment processing flow to store each kept
fragment together with its normalized form, rather than retaining only &str
values. Use the stored normalized values for duplicate checks and preserve the
existing fragment text when pushing output, eliminating repeated normalize calls
in extras.iter().any.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/components/UnifiedChat.tsx`:
- Around line 2359-2438: Update removeDocument to increment
documentUploadGenerationRef.current and clear the document-processing state via
setIsProcessingDocument(false), matching clearAllAttachments. Ensure any pending
replacement upload fails its generation check and cannot attach results after
the document is removed.
- Around line 2362-2428: Update the document upload handler around
getSupportedDocumentType so a null unsupported-document result explicitly sets
an attachment error and exits without attaching content, rather than silently
clearing processing state. Preserve the existing text and PDF handling for
supported document types, and use the established attachment-error cleanup
behavior.

---

Nitpick comments:
In `@frontend/src-tauri/src/pdf_extractor.rs`:
- Around line 408-419: Update the extras collection in the OCR fragment
processing flow to store each kept fragment together with its normalized form,
rather than retaining only &str values. Use the stored normalized values for
duplicate checks and preserve the existing fragment text when pushing output,
eliminating repeated normalize calls in extras.iter().any.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76a489bf-e0c0-4aed-a9da-09540064a763

📥 Commits

Reviewing files that changed from the base of the PR and between 3e2f3ed and 5fc9ba1.

⛔ Files ignored due to path filters (1)
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • docs/pdf-ocr.md
  • frontend/src-tauri/Cargo.toml
  • frontend/src-tauri/src/lib.rs
  • frontend/src-tauri/src/pdf_extractor.rs
  • frontend/src-tauri/src/pdf_ocr.rs
  • frontend/src/components/UnifiedChat.tsx
  • frontend/src/utils/documentUpload.test.ts
  • frontend/src/utils/documentUpload.ts
  • scripts/ci/ios-pr.sh
  • scripts/ci/ios-release.sh

@AnthonyRonning
AnthonyRonning force-pushed the codex-maple-pdf-oxide-ocr-maple branch from 5fc9ba1 to a16cd86 Compare July 21, 2026 07:48
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant