Vibecode PDF Expert
track this build5 phases, 15 steps, beginner friendly0%The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For PDF Expert, annotate, fill, merge, and lightly edit PDFs in a polished local desktop app. The hard boundary is native apple polish, sync, reliable rendering, and mobile continuity, plus document fidelity, identity, and compliance.
You are building a lean indie version of PDF Expert. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # PDF Expert · indie build A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | a real desktop application with filesystem access, without shipping a browser | | Rendering | pdf.js for display | the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet | | Manipulation | qpdf for structural operations and mupdf for content | page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's | | Optical character recognition | Tesseract, run locally | scans become searchable without uploading anyone's documents | | Storage | Originals untouched, outputs written alongside with a JSON operation log | the whole promise of the tool is that you can always go back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the native shell and runs the library calls. - Get it: Install with rustup from rustup.rs, plus the platform packages Tauri lists for your operating system. - Verify: A Tauri hello world builds and runs - [ ] **qpdf and mupdf** · free - Why: Structural operations and content extraction. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf, or your package manager. Decide early whether you bundle them or require them, because it affects distribution. - Verify: qpdf --version and mutool both run - [ ] **Tesseract with a language model** (optional) · free - Why: Phase 5 only. Without it, scans stay unsearchable and everything else still works. - Get it: brew install tesseract, or your package manager, plus the language data for the languages you need. - Verify: tesseract --list-langs includes your language - [ ] **Five awkward PDF fixtures** · free - Why: Every phase is tested against them: a text document, a scan, an encrypted file, a form and a corrupted file. Testing against one clean PDF proves nothing. - Get it: Collect them from documents you already have. Corrupt one deliberately by truncating it. Keep them in a fixtures directory. - Verify: All five are present and each opens or fails with a specific message ## Quick start ```sh npm create tauri-app@latest npm install pdfjs-dist ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. - native Apple polish, sync, reliable rendering, and mobile continuity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for PDF Expert, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · PDF Expert The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local PDF toolkit that annotates, fills, merges and lightly edits without ever destroying the original to replace PDF Expert. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · a real desktop application with filesystem access, without shipping a browser - Rendering: pdf.js for display · the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet - Manipulation: qpdf for structural operations and mupdf for content · page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's - Optical character recognition: Tesseract, run locally · scans become searchable without uploading anyone's documents - Storage: Originals untouched, outputs written alongside with a JSON operation log · the whole promise of the tool is that you can always go back ### Data model (create this before Phase 1) - documents(id, path, pages, encrypted, has_text_layer, imported_at, content_hash) - operations(id, document_id, kind, params_json, output_path, at) · kind is annotate, fill, merge, split, redact, ocr or reorder - annotations(id, document_id, page, kind, rect_json, content, color, created_at) - form_fields(document_id, page, name, kind, value, flattened) · read from the file's own form definition - The operation log is what makes an output auditable: every produced file names the input hash and the operations applied. ### Phase 1 · Open anything, and know what you opened Build: Render pages with pdf.js. Detect and report the properties that change what is possible: whether there is a text layer, whether it is encrypted, whether it has a form, and whether the page tree is damaged. Show these before the user tries an operation that depends on them. Trap: A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. Done when: A text PDF, a scanned PDF, an encrypted PDF and a deliberately corrupted one all open or fail with a specific message naming the reason. The properties panel correctly reports the presence of a text layer for each. A hundred page document renders its first page quickly rather than after processing all hundred. Do not build yet: No editing at all. Open, render and describe. ### Phase 2 · Annotation that other readers can see Build: Add highlight, note, freehand and text annotations using the PDF specification's own annotation objects, so they open correctly in other readers. Keep the original file and write annotations to a new one. Trap: Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. Done when: A highlight added in this application is visible in a different PDF reader. A note's text is readable there too. The original file is unchanged, verified by checksum. Annotations survive a round trip through another application that re-saves the file. Do not build yet: No forms, no merging, no redaction. ### Phase 3 · Forms Build: Read the document's form field definitions, present them for filling, write values back, and offer flattening so the filled values become page content rather than editable fields. Keep both the filled and the flattened output. Trap: A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. Done when: A form filled in this application shows the entered values in a different reader. The flattened output has no editable fields, verified by inspecting it with a tool rather than by eye. A form with required fields left empty warns before producing an output. Field values with accented characters survive. Do not build yet: No merging or redaction yet. ### Phase 4 · Pages, merging and redaction that is real Build: Merge, split, reorder, rotate and delete pages with qpdf. Then implement redaction properly: remove the text from the content stream, not just draw a black rectangle over it. Verify by extracting text from the output. Trap: A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. Done when: Merging three documents produces one with the correct page count and order. Extracting text from a redacted output contains none of the redacted string, verified with a text extraction tool. Splitting a document and merging the parts reproduces the original page order. Rotation persists in other readers. Do not build yet: No optical character recognition yet. ### Phase 5 · Optical character recognition for scans Build: Run Tesseract over pages with no text layer and write the recognised text back as an invisible layer positioned over the image. The page must look identical and become searchable. Report confidence so a poor scan is visibly poor. Trap: Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. Done when: A scanned page becomes searchable and looks pixel-identical to before, compared by image diff. Searching for a word visible in the scan finds it. A deliberately poor scan reports low confidence rather than silently producing nonsense. Recognition does not run on pages that already have a text layer. Do not build yet: Batch and automation come next. ### Phase 6 · Batch, audit and distribution Build: A watch folder applying a saved operation chain to new files. An operation log recording the input hash, the operations and the output for every produced file. Signing and notarization for distribution. Trap: A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. Done when: Dropping a file into the watch folder applies the saved chain and produces an output with a log entry naming the input hash. A batch containing a redaction verifies each output by text extraction and fails loudly on any that still contain the target string. The application is signed and opens on another machine without a warning. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering engines. Files that render perfectly in a commercial reader and imperfectly here will exist, and there is no shortcut to closing that gap. - Cross-device sync and mobile continuity, which is a substantial part of what the paid application provides. - Legally recognised digital signatures with identity verification, which is a certificate authority relationship rather than a feature. - Full content editing of arbitrary PDFs. Reflowing text in a document that was never designed to be edited is where every tool in this category struggles. ### README must contain - That originals are never modified and every output carries an operation log entry - That redaction removes content rather than covering it, and how to verify that yourself - That a scanned document has no text layer until recognition runs, and what that means for search and redaction - That flattening a form is what stops a recipient editing your answers - That documents never leave the machine, including during recognition ===== AGENTS.md ===== # Agent instructions · PDF Expert indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, pdf.js for display, qpdf for structural operations and mupdf for content, Tesseract, run locally, Originals untouched, outputs written alongside with a JSON operation log. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ===== BUILD_PLAN.md ===== # Build plan · PDF Expert A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Open anything, and know what you opened Real PDFs render, including the broken ones, and you can describe what each file actually is. ### Steps 1. Render with pdf.js and lazy-load pages Render on demand rather than eagerly. A large document must open immediately and fill in as you scroll. Files: `src/render.ts` ```sh npm create tauri-app@latest npm install pdfjs-dist ``` 2. Detect the properties that matter Text layer, encryption, forms, page count and structural damage. Each one changes which operations are possible. Files: `src/inspect.ts` 3. Collect awkward test files A scan, an encrypted file, a form, a very large document and something corrupt. These are your fixtures for every later phase. Files: `fixtures/` ### Done when - [ ] All five fixture types open or fail with a specific message - [ ] The text layer presence is correctly reported for each - [ ] A hundred page document shows page one quickly - [ ] A corrupted file produces a clear error rather than a crash ### Watch out - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. ## Phase 2 · Annotation that other readers can see Highlights and notes are real PDF annotations, not a private overlay. ### Steps 1. Write real annotation objects Use the specification's annotation types rather than drawing into the page content. This is what makes them portable and editable elsewhere. Files: `src/annotate.ts` 2. Never modify the input Read the original, write a new file, record the operation. Checksum the input before and after as a test. Files: `src/pipeline.ts` 3. Verify in a different reader Open the output somewhere else. If your annotations are invisible there, they are not annotations. ### Done when - [ ] A highlight is visible in a different PDF reader - [ ] A note's text is readable in that reader - [ ] The original file checksum is unchanged - [ ] Annotations survive a re-save by another application ### Watch out - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. ## Phase 3 · Forms You can fill a form and produce a version nobody can un-fill. ### Steps 1. Read the form definition from the file Field names, types and current values come from the document. Never infer a form from visual layout. Files: `src/forms.ts` 2. Write values and flatten Two outputs: filled and editable, or flattened and final. Make the difference explicit in the interface. Files: `src/forms.ts` 3. Verify flattening properly Inspect the output for remaining field objects with qpdf rather than trusting that it looks right. ```sh qpdf --json out.pdf | grep -c AcroForm ``` ### Done when - [ ] Filled values appear in a different reader - [ ] The flattened output contains no editable fields, verified by inspection - [ ] Empty required fields produce a warning before export - [ ] Accented characters survive in field values ### Watch out - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. ## Phase 4 · Pages, merging and redaction that is real You can restructure documents, and a redaction actually removes the text. ### Steps 1. Do structural operations with qpdf Merge, split, rotate and reorder. It handles the page tree correctly, which hand-written manipulation does not. Files: `src/pages.ts` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Remove content, then cover Delete the text objects in the region from the content stream, then draw the black box. Order matters and only one order is correct. Files: `src/redact.ts` 3. Test redaction by extraction Extract text from the output and assert the redacted string is absent. Make this an automated test, because it is the one that protects people. Files: `test/redact.test.ts` ```sh pdftotext redacted.pdf - | grep -c "secret" ``` ### Done when - [ ] A merge produces the correct page count and order - [ ] Text extraction from a redacted output does not contain the redacted string - [ ] Split then merge reproduces the original order - [ ] Rotation persists when opened elsewhere ### Watch out - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. ## Phase 5 · Optical character recognition for scans A scanned document becomes searchable without its appearance changing. ### Steps 1. Detect and deskew before recognising Check effective resolution and correct skew first. This does more for accuracy than any Tesseract setting. Files: `src/ocr.ts` 2. Write an invisible text layer Text positioned over the image in invisible render mode, so appearance is unchanged and selection works. ```sh tesseract page.png out pdf ``` 3. Report confidence Per page. A low score is information the user needs before relying on a search result. Files: `src/ocr.ts` ### Done when - [ ] A processed scan is pixel-identical to the original by image diff - [ ] A word visible in the scan is findable by search - [ ] A poor scan reports low confidence - [ ] Pages with an existing text layer are skipped ### Watch out - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ## Not in this build - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. ## After v1, if you want it - Bates numbering for legal work, which is a small feature with a devoted audience - Comparing two versions of a document and highlighting differences - A command line interface over the same operation chains ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where produced files are written. Deliberately never the source directory. OUTPUT_DIR=~/Documents/PDF Outputs # Optional. Only needed if qpdf is not bundled and not on PATH. QPDF_PATH=/opt/homebrew/bin/qpdf # Optional. Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. TESSERACT_PATH=/opt/homebrew/bin/tesseract # Required. Below this, recognition accuracy falls sharply and a warning is shown. OCR_MIN_DPI=300 # Required. The audit trail linking every output to its input hash and operations. OPERATION_LOG_PATH=~/Documents/PDF Outputs/operations.json
You are building a lean indie version of PDF Expert. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # PDF Expert · indie build A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | a real desktop application with filesystem access, without shipping a browser | | Rendering | pdf.js for display | the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet | | Manipulation | qpdf for structural operations and mupdf for content | page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's | | Optical character recognition | Tesseract, run locally | scans become searchable without uploading anyone's documents | | Storage | Originals untouched, outputs written alongside with a JSON operation log | the whole promise of the tool is that you can always go back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the native shell and runs the library calls. - Get it: Install with rustup from rustup.rs, plus the platform packages Tauri lists for your operating system. - Verify: A Tauri hello world builds and runs - [ ] **qpdf and mupdf** · free - Why: Structural operations and content extraction. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf, or your package manager. Decide early whether you bundle them or require them, because it affects distribution. - Verify: qpdf --version and mutool both run - [ ] **Tesseract with a language model** (optional) · free - Why: Phase 5 only. Without it, scans stay unsearchable and everything else still works. - Get it: brew install tesseract, or your package manager, plus the language data for the languages you need. - Verify: tesseract --list-langs includes your language - [ ] **Five awkward PDF fixtures** · free - Why: Every phase is tested against them: a text document, a scan, an encrypted file, a form and a corrupted file. Testing against one clean PDF proves nothing. - Get it: Collect them from documents you already have. Corrupt one deliberately by truncating it. Keep them in a fixtures directory. - Verify: All five are present and each opens or fails with a specific message ## Quick start ```sh npm create tauri-app@latest npm install pdfjs-dist ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. - native Apple polish, sync, reliable rendering, and mobile continuity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for PDF Expert, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · PDF Expert The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local PDF toolkit that annotates, fills, merges and lightly edits without ever destroying the original to replace PDF Expert. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · a real desktop application with filesystem access, without shipping a browser - Rendering: pdf.js for display · the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet - Manipulation: qpdf for structural operations and mupdf for content · page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's - Optical character recognition: Tesseract, run locally · scans become searchable without uploading anyone's documents - Storage: Originals untouched, outputs written alongside with a JSON operation log · the whole promise of the tool is that you can always go back ### Data model (create this before Phase 1) - documents(id, path, pages, encrypted, has_text_layer, imported_at, content_hash) - operations(id, document_id, kind, params_json, output_path, at) · kind is annotate, fill, merge, split, redact, ocr or reorder - annotations(id, document_id, page, kind, rect_json, content, color, created_at) - form_fields(document_id, page, name, kind, value, flattened) · read from the file's own form definition - The operation log is what makes an output auditable: every produced file names the input hash and the operations applied. ### Phase 1 · Open anything, and know what you opened Build: Render pages with pdf.js. Detect and report the properties that change what is possible: whether there is a text layer, whether it is encrypted, whether it has a form, and whether the page tree is damaged. Show these before the user tries an operation that depends on them. Trap: A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. Done when: A text PDF, a scanned PDF, an encrypted PDF and a deliberately corrupted one all open or fail with a specific message naming the reason. The properties panel correctly reports the presence of a text layer for each. A hundred page document renders its first page quickly rather than after processing all hundred. Do not build yet: No editing at all. Open, render and describe. ### Phase 2 · Annotation that other readers can see Build: Add highlight, note, freehand and text annotations using the PDF specification's own annotation objects, so they open correctly in other readers. Keep the original file and write annotations to a new one. Trap: Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. Done when: A highlight added in this application is visible in a different PDF reader. A note's text is readable there too. The original file is unchanged, verified by checksum. Annotations survive a round trip through another application that re-saves the file. Do not build yet: No forms, no merging, no redaction. ### Phase 3 · Forms Build: Read the document's form field definitions, present them for filling, write values back, and offer flattening so the filled values become page content rather than editable fields. Keep both the filled and the flattened output. Trap: A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. Done when: A form filled in this application shows the entered values in a different reader. The flattened output has no editable fields, verified by inspecting it with a tool rather than by eye. A form with required fields left empty warns before producing an output. Field values with accented characters survive. Do not build yet: No merging or redaction yet. ### Phase 4 · Pages, merging and redaction that is real Build: Merge, split, reorder, rotate and delete pages with qpdf. Then implement redaction properly: remove the text from the content stream, not just draw a black rectangle over it. Verify by extracting text from the output. Trap: A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. Done when: Merging three documents produces one with the correct page count and order. Extracting text from a redacted output contains none of the redacted string, verified with a text extraction tool. Splitting a document and merging the parts reproduces the original page order. Rotation persists in other readers. Do not build yet: No optical character recognition yet. ### Phase 5 · Optical character recognition for scans Build: Run Tesseract over pages with no text layer and write the recognised text back as an invisible layer positioned over the image. The page must look identical and become searchable. Report confidence so a poor scan is visibly poor. Trap: Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. Done when: A scanned page becomes searchable and looks pixel-identical to before, compared by image diff. Searching for a word visible in the scan finds it. A deliberately poor scan reports low confidence rather than silently producing nonsense. Recognition does not run on pages that already have a text layer. Do not build yet: Batch and automation come next. ### Phase 6 · Batch, audit and distribution Build: A watch folder applying a saved operation chain to new files. An operation log recording the input hash, the operations and the output for every produced file. Signing and notarization for distribution. Trap: A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. Done when: Dropping a file into the watch folder applies the saved chain and produces an output with a log entry naming the input hash. A batch containing a redaction verifies each output by text extraction and fails loudly on any that still contain the target string. The application is signed and opens on another machine without a warning. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering engines. Files that render perfectly in a commercial reader and imperfectly here will exist, and there is no shortcut to closing that gap. - Cross-device sync and mobile continuity, which is a substantial part of what the paid application provides. - Legally recognised digital signatures with identity verification, which is a certificate authority relationship rather than a feature. - Full content editing of arbitrary PDFs. Reflowing text in a document that was never designed to be edited is where every tool in this category struggles. ### README must contain - That originals are never modified and every output carries an operation log entry - That redaction removes content rather than covering it, and how to verify that yourself - That a scanned document has no text layer until recognition runs, and what that means for search and redaction - That flattening a form is what stops a recipient editing your answers - That documents never leave the machine, including during recognition ===== AGENTS.md ===== # Agent instructions · PDF Expert indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, pdf.js for display, qpdf for structural operations and mupdf for content, Tesseract, run locally, Originals untouched, outputs written alongside with a JSON operation log. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ===== BUILD_PLAN.md ===== # Build plan · PDF Expert A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Open anything, and know what you opened Real PDFs render, including the broken ones, and you can describe what each file actually is. ### Steps 1. Render with pdf.js and lazy-load pages Render on demand rather than eagerly. A large document must open immediately and fill in as you scroll. Files: `src/render.ts` ```sh npm create tauri-app@latest npm install pdfjs-dist ``` 2. Detect the properties that matter Text layer, encryption, forms, page count and structural damage. Each one changes which operations are possible. Files: `src/inspect.ts` 3. Collect awkward test files A scan, an encrypted file, a form, a very large document and something corrupt. These are your fixtures for every later phase. Files: `fixtures/` ### Done when - [ ] All five fixture types open or fail with a specific message - [ ] The text layer presence is correctly reported for each - [ ] A hundred page document shows page one quickly - [ ] A corrupted file produces a clear error rather than a crash ### Watch out - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. ## Phase 2 · Annotation that other readers can see Highlights and notes are real PDF annotations, not a private overlay. ### Steps 1. Write real annotation objects Use the specification's annotation types rather than drawing into the page content. This is what makes them portable and editable elsewhere. Files: `src/annotate.ts` 2. Never modify the input Read the original, write a new file, record the operation. Checksum the input before and after as a test. Files: `src/pipeline.ts` 3. Verify in a different reader Open the output somewhere else. If your annotations are invisible there, they are not annotations. ### Done when - [ ] A highlight is visible in a different PDF reader - [ ] A note's text is readable in that reader - [ ] The original file checksum is unchanged - [ ] Annotations survive a re-save by another application ### Watch out - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. ## Phase 3 · Forms You can fill a form and produce a version nobody can un-fill. ### Steps 1. Read the form definition from the file Field names, types and current values come from the document. Never infer a form from visual layout. Files: `src/forms.ts` 2. Write values and flatten Two outputs: filled and editable, or flattened and final. Make the difference explicit in the interface. Files: `src/forms.ts` 3. Verify flattening properly Inspect the output for remaining field objects with qpdf rather than trusting that it looks right. ```sh qpdf --json out.pdf | grep -c AcroForm ``` ### Done when - [ ] Filled values appear in a different reader - [ ] The flattened output contains no editable fields, verified by inspection - [ ] Empty required fields produce a warning before export - [ ] Accented characters survive in field values ### Watch out - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. ## Phase 4 · Pages, merging and redaction that is real You can restructure documents, and a redaction actually removes the text. ### Steps 1. Do structural operations with qpdf Merge, split, rotate and reorder. It handles the page tree correctly, which hand-written manipulation does not. Files: `src/pages.ts` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Remove content, then cover Delete the text objects in the region from the content stream, then draw the black box. Order matters and only one order is correct. Files: `src/redact.ts` 3. Test redaction by extraction Extract text from the output and assert the redacted string is absent. Make this an automated test, because it is the one that protects people. Files: `test/redact.test.ts` ```sh pdftotext redacted.pdf - | grep -c "secret" ``` ### Done when - [ ] A merge produces the correct page count and order - [ ] Text extraction from a redacted output does not contain the redacted string - [ ] Split then merge reproduces the original order - [ ] Rotation persists when opened elsewhere ### Watch out - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. ## Phase 5 · Optical character recognition for scans A scanned document becomes searchable without its appearance changing. ### Steps 1. Detect and deskew before recognising Check effective resolution and correct skew first. This does more for accuracy than any Tesseract setting. Files: `src/ocr.ts` 2. Write an invisible text layer Text positioned over the image in invisible render mode, so appearance is unchanged and selection works. ```sh tesseract page.png out pdf ``` 3. Report confidence Per page. A low score is information the user needs before relying on a search result. Files: `src/ocr.ts` ### Done when - [ ] A processed scan is pixel-identical to the original by image diff - [ ] A word visible in the scan is findable by search - [ ] A poor scan reports low confidence - [ ] Pages with an existing text layer are skipped ### Watch out - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ## Not in this build - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. ## After v1, if you want it - Bates numbering for legal work, which is a small feature with a devoted audience - Comparing two versions of a document and highlighting differences - A command line interface over the same operation chains ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where produced files are written. Deliberately never the source directory. OUTPUT_DIR=~/Documents/PDF Outputs # Optional. Only needed if qpdf is not bundled and not on PATH. QPDF_PATH=/opt/homebrew/bin/qpdf # Optional. Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. TESSERACT_PATH=/opt/homebrew/bin/tesseract # Required. Below this, recognition accuracy falls sharply and a warning is shown. OCR_MIN_DPI=300 # Required. The audit trail linking every output to its input hash and operations. OPERATION_LOG_PATH=~/Documents/PDF Outputs/operations.json
You are building a production product version of PDF Expert. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # PDF Expert · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For PDF Expert, annotate, fill, merge, and lightly edit PDFs in a polished local desktop app. The hard boundary is native apple polish, sync, reliable rendering, and mobile continuity, plus document fidelity, identity, and compliance. ## Product outcome A toolkit that never damages an original, produces annotations and forms other readers understand, redacts in a way you can verify by extraction, and logs what produced every file it writes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - LibreOffice headless - local or S3-compatible storage - public HTTPS for signature links ## Explicit non-goals for v1 - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. - native Apple polish, sync, reliable rendering, and mobile continuity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - The redaction extraction test passes on every fixture - Annotations are confirmed visible in a second PDF reader - Flattened forms are confirmed to contain no editable fields - Source checksums are unchanged after every operation - A recognised scan is pixel-identical to the original by image diff ===== BRIEF.md ===== # Build brief · PDF Expert The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local PDF toolkit that annotates, fills, merges and lightly edits without ever destroying the original to replace PDF Expert. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · a real desktop application with filesystem access, without shipping a browser - Rendering: pdf.js for display · the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet - Manipulation: qpdf for structural operations and mupdf for content · page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's - Optical character recognition: Tesseract, run locally · scans become searchable without uploading anyone's documents - Storage: Originals untouched, outputs written alongside with a JSON operation log · the whole promise of the tool is that you can always go back ### Data model (create this before Phase 1) - documents(id, path, pages, encrypted, has_text_layer, imported_at, content_hash) - operations(id, document_id, kind, params_json, output_path, at) · kind is annotate, fill, merge, split, redact, ocr or reorder - annotations(id, document_id, page, kind, rect_json, content, color, created_at) - form_fields(document_id, page, name, kind, value, flattened) · read from the file's own form definition - The operation log is what makes an output auditable: every produced file names the input hash and the operations applied. ### Phase 1 · Open anything, and know what you opened Build: Render pages with pdf.js. Detect and report the properties that change what is possible: whether there is a text layer, whether it is encrypted, whether it has a form, and whether the page tree is damaged. Show these before the user tries an operation that depends on them. Trap: A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. Done when: A text PDF, a scanned PDF, an encrypted PDF and a deliberately corrupted one all open or fail with a specific message naming the reason. The properties panel correctly reports the presence of a text layer for each. A hundred page document renders its first page quickly rather than after processing all hundred. Do not build yet: No editing at all. Open, render and describe. ### Phase 2 · Annotation that other readers can see Build: Add highlight, note, freehand and text annotations using the PDF specification's own annotation objects, so they open correctly in other readers. Keep the original file and write annotations to a new one. Trap: Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. Done when: A highlight added in this application is visible in a different PDF reader. A note's text is readable there too. The original file is unchanged, verified by checksum. Annotations survive a round trip through another application that re-saves the file. Do not build yet: No forms, no merging, no redaction. ### Phase 3 · Forms Build: Read the document's form field definitions, present them for filling, write values back, and offer flattening so the filled values become page content rather than editable fields. Keep both the filled and the flattened output. Trap: A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. Done when: A form filled in this application shows the entered values in a different reader. The flattened output has no editable fields, verified by inspecting it with a tool rather than by eye. A form with required fields left empty warns before producing an output. Field values with accented characters survive. Do not build yet: No merging or redaction yet. ### Phase 4 · Pages, merging and redaction that is real Build: Merge, split, reorder, rotate and delete pages with qpdf. Then implement redaction properly: remove the text from the content stream, not just draw a black rectangle over it. Verify by extracting text from the output. Trap: A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. Done when: Merging three documents produces one with the correct page count and order. Extracting text from a redacted output contains none of the redacted string, verified with a text extraction tool. Splitting a document and merging the parts reproduces the original page order. Rotation persists in other readers. Do not build yet: No optical character recognition yet. ### Phase 5 · Optical character recognition for scans Build: Run Tesseract over pages with no text layer and write the recognised text back as an invisible layer positioned over the image. The page must look identical and become searchable. Report confidence so a poor scan is visibly poor. Trap: Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. Done when: A scanned page becomes searchable and looks pixel-identical to before, compared by image diff. Searching for a word visible in the scan finds it. A deliberately poor scan reports low confidence rather than silently producing nonsense. Recognition does not run on pages that already have a text layer. Do not build yet: Batch and automation come next. ### Phase 6 · Batch, audit and distribution Build: A watch folder applying a saved operation chain to new files. An operation log recording the input hash, the operations and the output for every produced file. Signing and notarization for distribution. Trap: A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. Done when: Dropping a file into the watch folder applies the saved chain and produces an output with a log entry naming the input hash. A batch containing a redaction verifies each output by text extraction and fails loudly on any that still contain the target string. The application is signed and opens on another machine without a warning. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering engines. Files that render perfectly in a commercial reader and imperfectly here will exist, and there is no shortcut to closing that gap. - Cross-device sync and mobile continuity, which is a substantial part of what the paid application provides. - Legally recognised digital signatures with identity verification, which is a certificate authority relationship rather than a feature. - Full content editing of arbitrary PDFs. Reflowing text in a document that was never designed to be edited is where every tool in this category struggles. ### README must contain - That originals are never modified and every output carries an operation log entry - That redaction removes content rather than covering it, and how to verify that yourself - That a scanned document has no text layer until recognition runs, and what that means for search and redaction - That flattening a form is what stops a recipient editing your answers - That documents never leave the machine, including during recognition ===== ARCHITECTURE.md ===== # Architecture · PDF Expert ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | a real desktop application with filesystem access, without shipping a browser | | Rendering | pdf.js for display | the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet | | Manipulation | qpdf for structural operations and mupdf for content | page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's | | Optical character recognition | Tesseract, run locally | scans become searchable without uploading anyone's documents | | Storage | Originals untouched, outputs written alongside with a JSON operation log | the whole promise of the tool is that you can always go back | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Render | Displaying pages and reporting document properties. | pdf.js is the practical choice. What must survive any change is reporting the text layer's presence at import. | | Structure | Page tree operations through qpdf. | Not worth replacing. Hand-written page tree manipulation is how documents get corrupted. | | Content | Annotations, forms and redaction. | The rule that redaction removes rather than covers is a correctness property, not an implementation detail. | | Recognition | Optical character recognition and confidence reporting. | Optional and off the critical path. A hosted service would be more accurate and would mean uploading documents. | | Audit | The operation log linking outputs to inputs. | Append-only and small. It is what makes an output defensible. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `OUTPUT_DIR` · required · Where produced files are written. Deliberately never the source directory. - `QPDF_PATH` · optional · Only needed if qpdf is not bundled and not on PATH. - `TESSERACT_PATH` · optional · Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. - `OCR_MIN_DPI` · required · Below this, recognition accuracy falls sharply and a warning is shown. - `OPERATION_LOG_PATH` · required · The audit trail linking every output to its input hash and operations. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · PDF Expert product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri with a TypeScript front end, pdf.js for display, qpdf for structural operations and mupdf for content, Tesseract, run locally, Originals untouched, outputs written alongside with a JSON operation log. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. - A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. ===== MILESTONES.md ===== # Delivery milestones · PDF Expert Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Open anything, and know what you opened Real PDFs render, including the broken ones, and you can describe what each file actually is. ### Steps 1. Render with pdf.js and lazy-load pages Render on demand rather than eagerly. A large document must open immediately and fill in as you scroll. Files: `src/render.ts` ```sh npm create tauri-app@latest npm install pdfjs-dist ``` 2. Detect the properties that matter Text layer, encryption, forms, page count and structural damage. Each one changes which operations are possible. Files: `src/inspect.ts` 3. Collect awkward test files A scan, an encrypted file, a form, a very large document and something corrupt. These are your fixtures for every later phase. Files: `fixtures/` ### Done when - [ ] All five fixture types open or fail with a specific message - [ ] The text layer presence is correctly reported for each - [ ] A hundred page document shows page one quickly - [ ] A corrupted file produces a clear error rather than a crash ### Watch out - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. ## M2 · Annotation that other readers can see Highlights and notes are real PDF annotations, not a private overlay. ### Steps 1. Write real annotation objects Use the specification's annotation types rather than drawing into the page content. This is what makes them portable and editable elsewhere. Files: `src/annotate.ts` 2. Never modify the input Read the original, write a new file, record the operation. Checksum the input before and after as a test. Files: `src/pipeline.ts` 3. Verify in a different reader Open the output somewhere else. If your annotations are invisible there, they are not annotations. ### Done when - [ ] A highlight is visible in a different PDF reader - [ ] A note's text is readable in that reader - [ ] The original file checksum is unchanged - [ ] Annotations survive a re-save by another application ### Watch out - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. ## M3 · Forms You can fill a form and produce a version nobody can un-fill. ### Steps 1. Read the form definition from the file Field names, types and current values come from the document. Never infer a form from visual layout. Files: `src/forms.ts` 2. Write values and flatten Two outputs: filled and editable, or flattened and final. Make the difference explicit in the interface. Files: `src/forms.ts` 3. Verify flattening properly Inspect the output for remaining field objects with qpdf rather than trusting that it looks right. ```sh qpdf --json out.pdf | grep -c AcroForm ``` ### Done when - [ ] Filled values appear in a different reader - [ ] The flattened output contains no editable fields, verified by inspection - [ ] Empty required fields produce a warning before export - [ ] Accented characters survive in field values ### Watch out - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. ## M4 · Pages, merging and redaction that is real You can restructure documents, and a redaction actually removes the text. ### Steps 1. Do structural operations with qpdf Merge, split, rotate and reorder. It handles the page tree correctly, which hand-written manipulation does not. Files: `src/pages.ts` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Remove content, then cover Delete the text objects in the region from the content stream, then draw the black box. Order matters and only one order is correct. Files: `src/redact.ts` 3. Test redaction by extraction Extract text from the output and assert the redacted string is absent. Make this an automated test, because it is the one that protects people. Files: `test/redact.test.ts` ```sh pdftotext redacted.pdf - | grep -c "secret" ``` ### Done when - [ ] A merge produces the correct page count and order - [ ] Text extraction from a redacted output does not contain the redacted string - [ ] Split then merge reproduces the original order - [ ] Rotation persists when opened elsewhere ### Watch out - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. ## M5 · Optical character recognition for scans A scanned document becomes searchable without its appearance changing. ### Steps 1. Detect and deskew before recognising Check effective resolution and correct skew first. This does more for accuracy than any Tesseract setting. Files: `src/ocr.ts` 2. Write an invisible text layer Text positioned over the image in invisible render mode, so appearance is unchanged and selection works. ```sh tesseract page.png out pdf ``` 3. Report confidence Per page. A low score is information the user needs before relying on a search result. Files: `src/ocr.ts` ### Done when - [ ] A processed scan is pixel-identical to the original by image diff - [ ] A word visible in the scan is findable by search - [ ] A poor scan reports low confidence - [ ] Pages with an existing text layer are skipped ### Watch out - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ## M6 · Batch, audit and distribution (production only) Repeated work runs unattended and every output can be traced to its input. ### Steps 1. Save operation chains and watch a folder A named chain applied to new arrivals, waiting for the file size to settle before processing. Files: `src/batch.ts` 2. Verify redaction on every batch output Extraction test per file. A failure fails the job rather than producing a quiet mistake. Files: `src/batch.ts` 3. Sign and notarize Platform signing for distribution. Document that documents never leave the machine. Files: `README.md` ### Done when - [ ] A watched folder applies the chain and logs the input hash - [ ] Batch redaction outputs are each verified by text extraction - [ ] A failed verification fails the job loudly - [ ] The signed build opens on another machine without a warning ### Watch out - A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. ===== OPERATIONS.md ===== # Operations · PDF Expert ## Backup Source documents are the irreplaceable data and belong in an ordinary backup. Outputs are regenerable from sources plus the operation log, which is the practical reason the log exists. ## Restore Restore sources and the log. Any output can be recreated by replaying its operations against the input hash the log names. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch use, watch the redaction verification failure rate. A non-zero rate is not a warning, it is a stop, because it means files that look redacted are not. ## Incident checklist A file that looks redacted but is not is the incident that matters here, and it has caused real disclosures elsewhere. The extraction test is the control; if it ever fails silently, treat that as the highest priority bug in the application. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The redaction extraction test passes on every fixture - [ ] Annotations are confirmed visible in a second PDF reader - [ ] Flattened forms are confirmed to contain no editable fields - [ ] Source checksums are unchanged after every operation - [ ] A recognised scan is pixel-identical to the original by image diff ## Launch constraint Do not market omitted PDF Expert capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where produced files are written. Deliberately never the source directory. OUTPUT_DIR=~/Documents/PDF Outputs # Optional. Only needed if qpdf is not bundled and not on PATH. QPDF_PATH=/opt/homebrew/bin/qpdf # Optional. Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. TESSERACT_PATH=/opt/homebrew/bin/tesseract # Required. Below this, recognition accuracy falls sharply and a warning is shown. OCR_MIN_DPI=300 # Required. The audit trail linking every output to its input hash and operations. OPERATION_LOG_PATH=~/Documents/PDF Outputs/operations.json
# PDF Expert · indie build A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | a real desktop application with filesystem access, without shipping a browser | | Rendering | pdf.js for display | the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet | | Manipulation | qpdf for structural operations and mupdf for content | page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's | | Optical character recognition | Tesseract, run locally | scans become searchable without uploading anyone's documents | | Storage | Originals untouched, outputs written alongside with a JSON operation log | the whole promise of the tool is that you can always go back | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the front end and the tooling. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **The Rust toolchain** · free - Why: Tauri compiles the native shell and runs the library calls. - Get it: Install with rustup from rustup.rs, plus the platform packages Tauri lists for your operating system. - Verify: A Tauri hello world builds and runs - [ ] **qpdf and mupdf** · free - Why: Structural operations and content extraction. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf, or your package manager. Decide early whether you bundle them or require them, because it affects distribution. - Verify: qpdf --version and mutool both run - [ ] **Tesseract with a language model** (optional) · free - Why: Phase 5 only. Without it, scans stay unsearchable and everything else still works. - Get it: brew install tesseract, or your package manager, plus the language data for the languages you need. - Verify: tesseract --list-langs includes your language - [ ] **Five awkward PDF fixtures** · free - Why: Every phase is tested against them: a text document, a scan, an encrypted file, a form and a corrupted file. Testing against one clean PDF proves nothing. - Get it: Collect them from documents you already have. Corrupt one deliberately by truncating it. Keep them in a fixtures directory. - Verify: All five are present and each opens or fails with a specific message ## Quick start ```sh npm create tauri-app@latest npm install pdfjs-dist ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. - native Apple polish, sync, reliable rendering, and mobile continuity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem If one of those is essential to you, that is the reason to keep paying for PDF Expert, and the README should say so rather than pretend.
# Build brief · PDF Expert The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local PDF toolkit that annotates, fills, merges and lightly edits without ever destroying the original to replace PDF Expert. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · a real desktop application with filesystem access, without shipping a browser - Rendering: pdf.js for display · the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet - Manipulation: qpdf for structural operations and mupdf for content · page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's - Optical character recognition: Tesseract, run locally · scans become searchable without uploading anyone's documents - Storage: Originals untouched, outputs written alongside with a JSON operation log · the whole promise of the tool is that you can always go back ### Data model (create this before Phase 1) - documents(id, path, pages, encrypted, has_text_layer, imported_at, content_hash) - operations(id, document_id, kind, params_json, output_path, at) · kind is annotate, fill, merge, split, redact, ocr or reorder - annotations(id, document_id, page, kind, rect_json, content, color, created_at) - form_fields(document_id, page, name, kind, value, flattened) · read from the file's own form definition - The operation log is what makes an output auditable: every produced file names the input hash and the operations applied. ### Phase 1 · Open anything, and know what you opened Build: Render pages with pdf.js. Detect and report the properties that change what is possible: whether there is a text layer, whether it is encrypted, whether it has a form, and whether the page tree is damaged. Show these before the user tries an operation that depends on them. Trap: A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. Done when: A text PDF, a scanned PDF, an encrypted PDF and a deliberately corrupted one all open or fail with a specific message naming the reason. The properties panel correctly reports the presence of a text layer for each. A hundred page document renders its first page quickly rather than after processing all hundred. Do not build yet: No editing at all. Open, render and describe. ### Phase 2 · Annotation that other readers can see Build: Add highlight, note, freehand and text annotations using the PDF specification's own annotation objects, so they open correctly in other readers. Keep the original file and write annotations to a new one. Trap: Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. Done when: A highlight added in this application is visible in a different PDF reader. A note's text is readable there too. The original file is unchanged, verified by checksum. Annotations survive a round trip through another application that re-saves the file. Do not build yet: No forms, no merging, no redaction. ### Phase 3 · Forms Build: Read the document's form field definitions, present them for filling, write values back, and offer flattening so the filled values become page content rather than editable fields. Keep both the filled and the flattened output. Trap: A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. Done when: A form filled in this application shows the entered values in a different reader. The flattened output has no editable fields, verified by inspecting it with a tool rather than by eye. A form with required fields left empty warns before producing an output. Field values with accented characters survive. Do not build yet: No merging or redaction yet. ### Phase 4 · Pages, merging and redaction that is real Build: Merge, split, reorder, rotate and delete pages with qpdf. Then implement redaction properly: remove the text from the content stream, not just draw a black rectangle over it. Verify by extracting text from the output. Trap: A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. Done when: Merging three documents produces one with the correct page count and order. Extracting text from a redacted output contains none of the redacted string, verified with a text extraction tool. Splitting a document and merging the parts reproduces the original page order. Rotation persists in other readers. Do not build yet: No optical character recognition yet. ### Phase 5 · Optical character recognition for scans Build: Run Tesseract over pages with no text layer and write the recognised text back as an invisible layer positioned over the image. The page must look identical and become searchable. Report confidence so a poor scan is visibly poor. Trap: Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. Done when: A scanned page becomes searchable and looks pixel-identical to before, compared by image diff. Searching for a word visible in the scan finds it. A deliberately poor scan reports low confidence rather than silently producing nonsense. Recognition does not run on pages that already have a text layer. Do not build yet: Batch and automation come next. ### Phase 6 · Batch, audit and distribution Build: A watch folder applying a saved operation chain to new files. An operation log recording the input hash, the operations and the output for every produced file. Signing and notarization for distribution. Trap: A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. Done when: Dropping a file into the watch folder applies the saved chain and produces an output with a log entry naming the input hash. A batch containing a redaction verifies each output by text extraction and fails loudly on any that still contain the target string. The application is signed and opens on another machine without a warning. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering engines. Files that render perfectly in a commercial reader and imperfectly here will exist, and there is no shortcut to closing that gap. - Cross-device sync and mobile continuity, which is a substantial part of what the paid application provides. - Legally recognised digital signatures with identity verification, which is a certificate authority relationship rather than a feature. - Full content editing of arbitrary PDFs. Reflowing text in a document that was never designed to be edited is where every tool in this category struggles. ### README must contain - That originals are never modified and every output carries an operation log entry - That redaction removes content rather than covering it, and how to verify that yourself - That a scanned document has no text layer until recognition runs, and what that means for search and redaction - That flattening a form is what stops a recipient editing your answers - That documents never leave the machine, including during recognition
# Agent instructions · PDF Expert indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri with a TypeScript front end, pdf.js for display, qpdf for structural operations and mupdf for content, Tesseract, run locally, Originals untouched, outputs written alongside with a JSON operation log. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one.
# Build plan · PDF Expert A desktop application over mature PDF libraries: annotate and highlight, fill and flatten forms, merge, split and reorder pages, redact so the underlying text is genuinely gone, run optical character recognition over scans, and keep every original untouched with an operation log describing what produced each output. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Open anything, and know what you opened Real PDFs render, including the broken ones, and you can describe what each file actually is. ### Steps 1. Render with pdf.js and lazy-load pages Render on demand rather than eagerly. A large document must open immediately and fill in as you scroll. Files: `src/render.ts` ```sh npm create tauri-app@latest npm install pdfjs-dist ``` 2. Detect the properties that matter Text layer, encryption, forms, page count and structural damage. Each one changes which operations are possible. Files: `src/inspect.ts` 3. Collect awkward test files A scan, an encrypted file, a form, a very large document and something corrupt. These are your fixtures for every later phase. Files: `fixtures/` ### Done when - [ ] All five fixture types open or fail with a specific message - [ ] The text layer presence is correctly reported for each - [ ] A hundred page document shows page one quickly - [ ] A corrupted file produces a clear error rather than a crash ### Watch out - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. ## Phase 2 · Annotation that other readers can see Highlights and notes are real PDF annotations, not a private overlay. ### Steps 1. Write real annotation objects Use the specification's annotation types rather than drawing into the page content. This is what makes them portable and editable elsewhere. Files: `src/annotate.ts` 2. Never modify the input Read the original, write a new file, record the operation. Checksum the input before and after as a test. Files: `src/pipeline.ts` 3. Verify in a different reader Open the output somewhere else. If your annotations are invisible there, they are not annotations. ### Done when - [ ] A highlight is visible in a different PDF reader - [ ] A note's text is readable in that reader - [ ] The original file checksum is unchanged - [ ] Annotations survive a re-save by another application ### Watch out - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. ## Phase 3 · Forms You can fill a form and produce a version nobody can un-fill. ### Steps 1. Read the form definition from the file Field names, types and current values come from the document. Never infer a form from visual layout. Files: `src/forms.ts` 2. Write values and flatten Two outputs: filled and editable, or flattened and final. Make the difference explicit in the interface. Files: `src/forms.ts` 3. Verify flattening properly Inspect the output for remaining field objects with qpdf rather than trusting that it looks right. ```sh qpdf --json out.pdf | grep -c AcroForm ``` ### Done when - [ ] Filled values appear in a different reader - [ ] The flattened output contains no editable fields, verified by inspection - [ ] Empty required fields produce a warning before export - [ ] Accented characters survive in field values ### Watch out - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. ## Phase 4 · Pages, merging and redaction that is real You can restructure documents, and a redaction actually removes the text. ### Steps 1. Do structural operations with qpdf Merge, split, rotate and reorder. It handles the page tree correctly, which hand-written manipulation does not. Files: `src/pages.ts` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Remove content, then cover Delete the text objects in the region from the content stream, then draw the black box. Order matters and only one order is correct. Files: `src/redact.ts` 3. Test redaction by extraction Extract text from the output and assert the redacted string is absent. Make this an automated test, because it is the one that protects people. Files: `test/redact.test.ts` ```sh pdftotext redacted.pdf - | grep -c "secret" ``` ### Done when - [ ] A merge produces the correct page count and order - [ ] Text extraction from a redacted output does not contain the redacted string - [ ] Split then merge reproduces the original order - [ ] Rotation persists when opened elsewhere ### Watch out - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. ## Phase 5 · Optical character recognition for scans A scanned document becomes searchable without its appearance changing. ### Steps 1. Detect and deskew before recognising Check effective resolution and correct skew first. This does more for accuracy than any Tesseract setting. Files: `src/ocr.ts` 2. Write an invisible text layer Text positioned over the image in invisible render mode, so appearance is unchanged and selection works. ```sh tesseract page.png out pdf ``` 3. Report confidence Per page. A low score is information the user needs before relying on a search result. Files: `src/ocr.ts` ### Done when - [ ] A processed scan is pixel-identical to the original by image diff - [ ] A word visible in the scan is findable by search - [ ] A poor scan reports low confidence - [ ] Pages with an existing text layer are skipped ### Watch out - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ## Not in this build - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. ## After v1, if you want it - Bates numbering for legal work, which is a small feature with a devoted audience - Comparing two versions of a document and highlighting differences - A command line interface over the same operation chains
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where produced files are written. Deliberately never the source directory. OUTPUT_DIR=~/Documents/PDF Outputs # Optional. Only needed if qpdf is not bundled and not on PATH. QPDF_PATH=/opt/homebrew/bin/qpdf # Optional. Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. TESSERACT_PATH=/opt/homebrew/bin/tesseract # Required. Below this, recognition accuracy falls sharply and a warning is shown. OCR_MIN_DPI=300 # Required. The audit trail linking every output to its input hash and operations. OPERATION_LOG_PATH=~/Documents/PDF Outputs/operations.json
# PDF Expert · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For PDF Expert, annotate, fill, merge, and lightly edit PDFs in a polished local desktop app. The hard boundary is native apple polish, sync, reliable rendering, and mobile continuity, plus document fidelity, identity, and compliance. ## Product outcome A toolkit that never damages an original, produces annotations and forms other readers understand, redacts in a way you can verify by extraction, and logs what produced every file it writes. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - LibreOffice headless - local or S3-compatible storage - public HTTPS for signature links ## Explicit non-goals for v1 - The proprietary rendering engines and the files only they handle correctly. - Cross-device sync and mobile continuity. - Legally recognised signatures with identity verification. - Full content editing of arbitrary documents. - native Apple polish, sync, reliable rendering, and mobile continuity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - The redaction extraction test passes on every fixture - Annotations are confirmed visible in a second PDF reader - Flattened forms are confirmed to contain no editable fields - Source checksums are unchanged after every operation - A recognised scan is pixel-identical to the original by image diff
# Build brief · PDF Expert The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local PDF toolkit that annotates, fills, merges and lightly edits without ever destroying the original to replace PDF Expert. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Shell: Tauri with a TypeScript front end · a real desktop application with filesystem access, without shipping a browser - Rendering: pdf.js for display · the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet - Manipulation: qpdf for structural operations and mupdf for content · page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's - Optical character recognition: Tesseract, run locally · scans become searchable without uploading anyone's documents - Storage: Originals untouched, outputs written alongside with a JSON operation log · the whole promise of the tool is that you can always go back ### Data model (create this before Phase 1) - documents(id, path, pages, encrypted, has_text_layer, imported_at, content_hash) - operations(id, document_id, kind, params_json, output_path, at) · kind is annotate, fill, merge, split, redact, ocr or reorder - annotations(id, document_id, page, kind, rect_json, content, color, created_at) - form_fields(document_id, page, name, kind, value, flattened) · read from the file's own form definition - The operation log is what makes an output auditable: every produced file names the input hash and the operations applied. ### Phase 1 · Open anything, and know what you opened Build: Render pages with pdf.js. Detect and report the properties that change what is possible: whether there is a text layer, whether it is encrypted, whether it has a form, and whether the page tree is damaged. Show these before the user tries an operation that depends on them. Trap: A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. Done when: A text PDF, a scanned PDF, an encrypted PDF and a deliberately corrupted one all open or fail with a specific message naming the reason. The properties panel correctly reports the presence of a text layer for each. A hundred page document renders its first page quickly rather than after processing all hundred. Do not build yet: No editing at all. Open, render and describe. ### Phase 2 · Annotation that other readers can see Build: Add highlight, note, freehand and text annotations using the PDF specification's own annotation objects, so they open correctly in other readers. Keep the original file and write annotations to a new one. Trap: Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. Done when: A highlight added in this application is visible in a different PDF reader. A note's text is readable there too. The original file is unchanged, verified by checksum. Annotations survive a round trip through another application that re-saves the file. Do not build yet: No forms, no merging, no redaction. ### Phase 3 · Forms Build: Read the document's form field definitions, present them for filling, write values back, and offer flattening so the filled values become page content rather than editable fields. Keep both the filled and the flattened output. Trap: A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. Done when: A form filled in this application shows the entered values in a different reader. The flattened output has no editable fields, verified by inspecting it with a tool rather than by eye. A form with required fields left empty warns before producing an output. Field values with accented characters survive. Do not build yet: No merging or redaction yet. ### Phase 4 · Pages, merging and redaction that is real Build: Merge, split, reorder, rotate and delete pages with qpdf. Then implement redaction properly: remove the text from the content stream, not just draw a black rectangle over it. Verify by extracting text from the output. Trap: A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. Done when: Merging three documents produces one with the correct page count and order. Extracting text from a redacted output contains none of the redacted string, verified with a text extraction tool. Splitting a document and merging the parts reproduces the original page order. Rotation persists in other readers. Do not build yet: No optical character recognition yet. ### Phase 5 · Optical character recognition for scans Build: Run Tesseract over pages with no text layer and write the recognised text back as an invisible layer positioned over the image. The page must look identical and become searchable. Report confidence so a poor scan is visibly poor. Trap: Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. Done when: A scanned page becomes searchable and looks pixel-identical to before, compared by image diff. Searching for a word visible in the scan finds it. A deliberately poor scan reports low confidence rather than silently producing nonsense. Recognition does not run on pages that already have a text layer. Do not build yet: Batch and automation come next. ### Phase 6 · Batch, audit and distribution Build: A watch folder applying a saved operation chain to new files. An operation log recording the input hash, the operations and the output for every produced file. Signing and notarization for distribution. Trap: A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check. Done when: Dropping a file into the watch folder applies the saved chain and produces an output with a log entry naming the input hash. A batch containing a redaction verifies each output by text extraction and fails loudly on any that still contain the target string. The application is signed and opens on another machine without a warning. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering engines. Files that render perfectly in a commercial reader and imperfectly here will exist, and there is no shortcut to closing that gap. - Cross-device sync and mobile continuity, which is a substantial part of what the paid application provides. - Legally recognised digital signatures with identity verification, which is a certificate authority relationship rather than a feature. - Full content editing of arbitrary PDFs. Reflowing text in a document that was never designed to be edited is where every tool in this category struggles. ### README must contain - That originals are never modified and every output carries an operation log entry - That redaction removes content rather than covering it, and how to verify that yourself - That a scanned document has no text layer until recognition runs, and what that means for search and redaction - That flattening a form is what stops a recipient editing your answers - That documents never leave the machine, including during recognition
# Architecture · PDF Expert ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri with a TypeScript front end | a real desktop application with filesystem access, without shipping a browser | | Rendering | pdf.js for display | the most battle-tested renderer outside the proprietary engines, and it handles the malformed files you will meet | | Manipulation | qpdf for structural operations and mupdf for content | page trees, encryption and linearisation are qpdf's domain; text and image extraction is mupdf's | | Optical character recognition | Tesseract, run locally | scans become searchable without uploading anyone's documents | | Storage | Originals untouched, outputs written alongside with a JSON operation log | the whole promise of the tool is that you can always go back | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Render | Displaying pages and reporting document properties. | pdf.js is the practical choice. What must survive any change is reporting the text layer's presence at import. | | Structure | Page tree operations through qpdf. | Not worth replacing. Hand-written page tree manipulation is how documents get corrupted. | | Content | Annotations, forms and redaction. | The rule that redaction removes rather than covers is a correctness property, not an implementation detail. | | Recognition | Optical character recognition and confidence reporting. | Optional and off the critical path. A hosted service would be more accurate and would mean uploading documents. | | Audit | The operation log linking outputs to inputs. | Append-only and small. It is what makes an output defensible. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `OUTPUT_DIR` · required · Where produced files are written. Deliberately never the source directory. - `QPDF_PATH` · optional · Only needed if qpdf is not bundled and not on PATH. - `TESSERACT_PATH` · optional · Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. - `OCR_MIN_DPI` · required · Below this, recognition accuracy falls sharply and a warning is shown. - `OPERATION_LOG_PATH` · required · The audit trail linking every output to its input hash and operations. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · PDF Expert product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri with a TypeScript front end, pdf.js for display, qpdf for structural operations and mupdf for content, Tesseract, run locally, Originals untouched, outputs written alongside with a JSON operation log. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. - A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check.
# Delivery milestones · PDF Expert Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Open anything, and know what you opened Real PDFs render, including the broken ones, and you can describe what each file actually is. ### Steps 1. Render with pdf.js and lazy-load pages Render on demand rather than eagerly. A large document must open immediately and fill in as you scroll. Files: `src/render.ts` ```sh npm create tauri-app@latest npm install pdfjs-dist ``` 2. Detect the properties that matter Text layer, encryption, forms, page count and structural damage. Each one changes which operations are possible. Files: `src/inspect.ts` 3. Collect awkward test files A scan, an encrypted file, a form, a very large document and something corrupt. These are your fixtures for every later phase. Files: `fixtures/` ### Done when - [ ] All five fixture types open or fail with a specific message - [ ] The text layer presence is correctly reported for each - [ ] A hundred page document shows page one quickly - [ ] A corrupted file produces a clear error rather than a crash ### Watch out - A scanned PDF and a text PDF look identical on screen and behave completely differently: search finds nothing, selection selects nothing, and redaction cannot remove text that is not there. Detect the absence of a text layer at import and say so, rather than letting the user discover it when a redaction quietly does nothing. ## M2 · Annotation that other readers can see Highlights and notes are real PDF annotations, not a private overlay. ### Steps 1. Write real annotation objects Use the specification's annotation types rather than drawing into the page content. This is what makes them portable and editable elsewhere. Files: `src/annotate.ts` 2. Never modify the input Read the original, write a new file, record the operation. Checksum the input before and after as a test. Files: `src/pipeline.ts` 3. Verify in a different reader Open the output somewhere else. If your annotations are invisible there, they are not annotations. ### Done when - [ ] A highlight is visible in a different PDF reader - [ ] A note's text is readable in that reader - [ ] The original file checksum is unchanged - [ ] Annotations survive a re-save by another application ### Watch out - Storing annotations in your own sidecar database and drawing them at render time produces a document that is blank for everyone else. Write them into the PDF as annotation objects, then verify by opening the output in a different reader, which is the only test that matters. ## M3 · Forms You can fill a form and produce a version nobody can un-fill. ### Steps 1. Read the form definition from the file Field names, types and current values come from the document. Never infer a form from visual layout. Files: `src/forms.ts` 2. Write values and flatten Two outputs: filled and editable, or flattened and final. Make the difference explicit in the interface. Files: `src/forms.ts` 3. Verify flattening properly Inspect the output for remaining field objects with qpdf rather than trusting that it looks right. ```sh qpdf --json out.pdf | grep -c AcroForm ``` ### Done when - [ ] Filled values appear in a different reader - [ ] The flattened output contains no editable fields, verified by inspection - [ ] Empty required fields produce a warning before export - [ ] Accented characters survive in field values ### Watch out - A filled but unflattened form still carries editable field values, so a recipient can change what you entered and the change is invisible in most viewers. If the document is going to anyone else, flatten it, and make that the prominent option rather than an advanced one. ## M4 · Pages, merging and redaction that is real You can restructure documents, and a redaction actually removes the text. ### Steps 1. Do structural operations with qpdf Merge, split, rotate and reorder. It handles the page tree correctly, which hand-written manipulation does not. Files: `src/pages.ts` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Remove content, then cover Delete the text objects in the region from the content stream, then draw the black box. Order matters and only one order is correct. Files: `src/redact.ts` 3. Test redaction by extraction Extract text from the output and assert the redacted string is absent. Make this an automated test, because it is the one that protects people. Files: `test/redact.test.ts` ```sh pdftotext redacted.pdf - | grep -c "secret" ``` ### Done when - [ ] A merge produces the correct page count and order - [ ] Text extraction from a redacted output does not contain the redacted string - [ ] Split then merge reproduces the original order - [ ] Rotation persists when opened elsewhere ### Watch out - A black rectangle drawn over text leaves the text in the file, selectable and extractable, and this has caused real disclosures in published legal and government documents. Redaction must remove the underlying content and then rasterise or rewrite the region. The test is text extraction on the output, not a visual check. ## M5 · Optical character recognition for scans A scanned document becomes searchable without its appearance changing. ### Steps 1. Detect and deskew before recognising Check effective resolution and correct skew first. This does more for accuracy than any Tesseract setting. Files: `src/ocr.ts` 2. Write an invisible text layer Text positioned over the image in invisible render mode, so appearance is unchanged and selection works. ```sh tesseract page.png out pdf ``` 3. Report confidence Per page. A low score is information the user needs before relying on a search result. Files: `src/ocr.ts` ### Done when - [ ] A processed scan is pixel-identical to the original by image diff - [ ] A word visible in the scan is findable by search - [ ] A poor scan reports low confidence - [ ] Pages with an existing text layer are skipped ### Watch out - Recognition quality depends heavily on resolution and skew. Below roughly 300 dots per inch, accuracy falls off sharply, and a skewed scan compounds it. Deskew and check the effective resolution before recognising, and report low confidence rather than presenting a bad result as a good one. ## M6 · Batch, audit and distribution (production only) Repeated work runs unattended and every output can be traced to its input. ### Steps 1. Save operation chains and watch a folder A named chain applied to new arrivals, waiting for the file size to settle before processing. Files: `src/batch.ts` 2. Verify redaction on every batch output Extraction test per file. A failure fails the job rather than producing a quiet mistake. Files: `src/batch.ts` 3. Sign and notarize Platform signing for distribution. Document that documents never leave the machine. Files: `README.md` ### Done when - [ ] A watched folder applies the chain and logs the input hash - [ ] Batch redaction outputs are each verified by text extraction - [ ] A failed verification fails the job loudly - [ ] The signed build opens on another machine without a warning ### Watch out - A batch that redacts must never be trusted without verification, because a redaction that silently failed produces a file that looks correct and is not. Run the extraction test on every batch output and fail the job loudly rather than writing a file that passes a visual check.
# Operations · PDF Expert ## Backup Source documents are the irreplaceable data and belong in an ordinary backup. Outputs are regenerable from sources plus the operation log, which is the practical reason the log exists. ## Restore Restore sources and the log. Any output can be recreated by replaying its operations against the input hash the log names. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch use, watch the redaction verification failure rate. A non-zero rate is not a warning, it is a stop, because it means files that look redacted are not. ## Incident checklist A file that looks redacted but is not is the incident that matters here, and it has caused real disclosures elsewhere. The extraction test is the control; if it ever fails silently, treat that as the highest priority bug in the application. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The redaction extraction test passes on every fixture - [ ] Annotations are confirmed visible in a second PDF reader - [ ] Flattened forms are confirmed to contain no editable fields - [ ] Source checksums are unchanged after every operation - [ ] A recognised scan is pixel-identical to the original by image diff ## Launch constraint Do not market omitted PDF Expert capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where produced files are written. Deliberately never the source directory. OUTPUT_DIR=~/Documents/PDF Outputs # Optional. Only needed if qpdf is not bundled and not on PATH. QPDF_PATH=/opt/homebrew/bin/qpdf # Optional. Only needed if Tesseract is not bundled. Recognition is disabled when it cannot be found. TESSERACT_PATH=/opt/homebrew/bin/tesseract # Required. Below this, recognition accuracy falls sharply and a warning is shown. OCR_MIN_DPI=300 # Required. The audit trail linking every output to its input hash and operations. OPERATION_LOG_PATH=~/Documents/PDF Outputs/operations.json
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for PDF Expert because the visible editor is reproducible, but reliable rendering and legally defensible signature workflows are not a casual side project. The recurring cost buys format edge cases, fonts, rendering, encryption, signatures, evidence, retention, storage, backups, and legal requirements, not just the visible interface.
xnative Apple polish, sync, reliable rendering, and mobile continuity
xpixel-perfect proprietary PDF engine
xidentity verification
xqualified trust services
xlarge template and integration ecosystem
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
PDF Expert pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| weekly | custom | — | iPhone, iPad and Mac; Premium features including OCR, conversion, editing and PDF Copilot |
| yearly | — | $6.67 | iPhone, iPad and Mac; major updates/new features, PDF Copilot, OCR, conversion and editing |
| lifetime | custom | — | Mac only; one-time $139.99; ongoing fixes, but future major tools may cost extra |
free tierFree download supports basic reading/annotation/form use; official page does not publish numeric free-use caps
billingweekly + annual ($79.99/year in the US App Store) + $139.99 lifetime Mac-only; annual/lifetime have a 30-day guarantee; storefront pricing varies by region
hidden costsLifetime does not cover iPhone/iPad and future major features may require another fee. Weekly USD pricing was not identifiable from the US App Store SKU list, and the website's localized GBP prices differ materially from the US storefront.
verified 2026-08-13 · source ↗
Vibecode PDF Expert
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal PDF Expert replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does PDF Expert cost?
PDF Expert costs about $6.67/month (Premium, checked 2026-07-31), which is $80.03999999999999 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing PDF Expert?
Honestly: native Apple polish, sync, reliable rendering, and mobile continuity; pixel-perfect proprietary PDF engine; identity verification; qualified trust services; large template and integration ecosystem. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to PDF Expert?
Yes: PDFgear (A polished free local editor for annotation, forms, signatures, merging, conversion, OCR, and light text edits; the most direct answer here.) Preview (Already on every Mac and quietly excellent at viewing, annotating, filling, signing, rearranging, merging, and exporting PDFs; direct text editing is not the deal.) The prompt is for when you want it exactly your way.