Vibecode iLovePDF Premium
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 ILovePDF Premium, merge, split, compress, convert, and annotate PDFs without uploading them to a vendor. The hard boundary is simple hosted delivery, mobile apps, desktop tools, ocr, and batch capacity, plus document fidelity, identity, and compliance.
You are building a lean indie version of iLovePDF Premium. 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 ===== # iLovePDF Premium · indie build A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 | | --- | --- | --- | | Structure | qpdf for page tree operations | merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files | | Content | mupdf or an equivalent for content inspection | compression needs to know what is actually taking the space | | Conversion | LibreOffice headless for office formats | document conversion fidelity is decades of work and is not the project | | Runtime | Python 3.12 orchestrating them | the work is inspection, decisions and bookkeeping | | Storage | Originals untouched, outputs alongside with an operation log | every output should be traceable to its input and settings | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates the tools and does the analysis. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **qpdf and a content inspection library** · free - Why: Structural operations and byte attribution respectively. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf or your package manager, plus a Python binding for inspection. - Verify: qpdf --version runs and a document's objects can be listed - [ ] **LibreOffice for headless conversion** · free - Why: Office document fidelity is decades of work and is the one part of this genuinely not worth attempting. - Get it: Install LibreOffice and confirm the headless conversion works from the command line. - Verify: soffice converts a test document to PDF - [ ] **Six awkward documents** · free - Why: Every phase is tested against them: a scan, a text document with embedded fonts, an encrypted file, a damaged file, a very large one and an office document. - Get it: Collect from documents you already have. Truncate one deliberately to produce the damaged case. - Verify: All six are present and each behaves as expected - [ ] **A second PDF reader** · free - Why: Validating output in the tool that produced it proves nothing, and a structurally broken file often opens fine in a forgiving viewer. - Get it: Any reader other than the one your library uses. A strict one is more useful than a forgiving one. - Verify: It opens a merged output and reports a corrupt one as corrupt ## Quick start ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. - simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity - 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 iLovePDF Premium, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · iLovePDF Premium 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 local PDF operations: merge, split, compress and convert, without uploading anything to replace iLovePDF Premium. 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. ### Rule zero The reason to build this rather than use a web tool is that the documents never leave your machine. That is not a preference: contracts, medical letters and identity documents are exactly what people put through free web converters, and every one of those uploads is a copy on someone else's server with a retention policy nobody read. So there is no network path in this build at all, and the check at the end of every phase is that the operation completed with networking disabled. If a feature genuinely needs a service, it is a separate tool with its own decision rather than a quiet exception here. ### Stack (fixed, do not substitute) - Structure: qpdf for page tree operations · merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files - Content: mupdf or an equivalent for content inspection · compression needs to know what is actually taking the space - Conversion: LibreOffice headless for office formats · document conversion fidelity is decades of work and is not the project - Runtime: Python 3.12 orchestrating them · the work is inspection, decisions and bookkeeping - Storage: Originals untouched, outputs alongside with an operation log · every output should be traceable to its input and settings ### Data model (create this before Phase 1) - documents(id, path, pages, bytes, encrypted, has_text_layer, content_hash, imported_at) - page_stats(document_id, page, image_bytes, text_bytes, font_bytes, other_bytes) · what is actually large, per page - operations(id, kind, input_hashes_json, params_json, output_path, output_bytes, at) · kind is merge, split, compress, convert or rotate - fonts(document_id, name, embedded, subset, bytes) · because unsubset embedded fonts are a common source of size - The page statistics are what turn compression from guesswork into a decision, and they are worth computing before offering any setting. ### Phase 1 · Structure operations that do not corrupt Build: Merge several documents, split by page range, and rotate pages, using a library that understands the page tree. Preserve bookmarks and metadata where possible. Validate each output opens in a different reader. Trap: Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. Done when: Merging three documents produces one with the correct page count and order that opens in two different readers. Splitting and re-merging the parts reproduces the original page order. Rotation persists when opened elsewhere. Bookmarks survive a merge or their loss is reported rather than silent. Do not build yet: No compression, conversion or batch. ### Phase 2 · Finding out what is actually large Build: Analyse a document into per-page byte contributions: images, text, embedded fonts and everything else. Report the largest contributors. Do this before offering any compression setting. Trap: Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. Done when: Analysing a scanned document attributes most of its size to images. Analysing a text document with embedded fonts attributes size to fonts rather than images. The report names the three largest contributors. A document whose size is mostly structural overhead is identified as such. Do not build yet: No compression applied yet. ### Phase 3 · Compression that keeps documents usable Build: Apply the approach the analysis suggests: downsample and recompress images to a target resolution, subset embedded fonts, and remove unused objects. Never rasterise a text document. Report the before and after with the text layer confirmed intact. Trap: Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. Done when: A scanned document compresses substantially through image downsampling. A text document retains a working text layer after compression, verified by extracting text. Font subsetting reduces size without changing rendered appearance. The report states before and after sizes and confirms the text layer survived. Do not build yet: No conversion or batch yet. ### Phase 4 · Conversion in both directions Build: Convert office documents to PDF through a headless converter, and PDF to images at a chosen resolution. For PDF to editable formats, report honestly what the conversion loses rather than presenting it as lossless. Trap: Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. Done when: An office document converts to PDF with its layout preserved, checked against the original. A PDF converts to images at the requested resolution. A PDF to editable conversion produces a result with a stated warning about what was lost. Conversion works with networking disabled. Do not build yet: No batch or annotation yet. ### Phase 5 · Batch, and the awkward documents Build: Process a directory with one operation chain. Handle encrypted, damaged and enormous documents by reporting them rather than failing the run. Write a manifest naming every input, its settings and its output. Trap: An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. Done when: A folder of fifty documents processes unattended. An encrypted document is reported and skipped rather than stopping the run. A damaged document is reported with the reason. The manifest names every input, its settings and its output, and re-running with the same settings produces identical outputs. Do not build yet: Verification and cleanup come last. ### Phase 6 · Verifying the offline promise Build: Run the full test suite with networking disabled and confirm every operation succeeds. Audit the dependencies for anything that fetches at runtime. Document the promise and how a user can verify it themselves. Trap: A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. Done when: The full test suite passes with networking disabled. A dependency audit found no runtime network access, or the offending dependency was replaced. The documentation states the offline promise and how to verify it. Originals are confirmed unmodified across the whole suite by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering and conversion engines, which handle a long tail of malformed documents better than any open toolchain. - Optical character recognition, which is a separate tool with its own quality questions rather than a compression setting. - Mobile applications, which is how a lot of this category is actually used. - Hosted convenience. A web tool needs no installation, which is genuinely why people use one, and the trade is that your documents go with it. ### README must contain - That nothing leaves the machine and how to verify that yourself - That compression is chosen from what analysis says is actually large - That a document with a text layer keeps one, and how that is checked - That PDF to editable conversion is lossy and what it loses - That originals are never modified and every output has a manifest entry ===== AGENTS.md ===== # Agent instructions · iLovePDF Premium indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: qpdf for page tree operations, mupdf or an equivalent for content inspection, LibreOffice headless for office formats, Python 3.12 orchestrating them, Originals untouched, outputs alongside with an 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 - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ===== BUILD_PLAN.md ===== # Build plan · iLovePDF Premium A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 · Structure operations that do not corrupt Merge, split and rotate produce valid documents that open everywhere. ### Steps 1. Use a structural library Page tree operations through qpdf rather than by hand. This is where files get corrupted. Files: `src/structure.py` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Preserve or report metadata loss Bookmarks and document properties. Losing them silently is the common annoyance. Files: `src/structure.py` 3. Validate every output Open in a second reader, and check structure with a tool rather than by eye. ```sh qpdf --check merged.pdf ``` ### Done when - [ ] A merge produces correct page count and order in two readers - [ ] Split then merge reproduces the original order - [ ] Rotation persists elsewhere - [ ] Bookmark loss is preserved or reported ### Watch out - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. ## Phase 2 · Finding out what is actually large You can say why a document is fifty megabytes before compressing it. ### Steps 1. Attribute bytes per page by kind Images, text, fonts, other. The attribution is what makes the next phase a decision. Files: `src/analyse.py` 2. Detect unsubset embedded fonts A full font embedded for three characters is a common and easily fixed cause of size. Files: `src/fonts.py` 3. Report the largest contributors Three is enough. It tells you which compression approach is worth applying. Files: `src/report.py` ### Done when - [ ] A scanned document attributes size to images - [ ] A text document attributes size to fonts - [ ] The three largest contributors are named - [ ] A structurally bloated document is identified ### Watch out - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. ## Phase 3 · Compression that keeps documents usable A document gets smaller without becoming unreadable or losing its text. ### Steps 1. Downsample images to a target resolution Chosen for the use: screen, print or archive. One setting does not serve all three. Files: `src/compress.py` 2. Subset fonts and drop unused objects Cheap size reductions that change nothing visible. Files: `src/compress.py` 3. Assert the text layer survived Extract text before and after and compare. This is the check that prevents the worst outcome. Files: `tests/test_text_layer.py` ```sh pdftotext before.pdf - | wc -w pdftotext after.pdf - | wc -w ``` ### Done when - [ ] A scanned document compresses substantially - [ ] A text document keeps a working text layer - [ ] Font subsetting reduces size without visible change - [ ] Before and after sizes are reported ### Watch out - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. ## Phase 4 · Conversion in both directions Documents convert to and from common formats with the losses stated. ### Steps 1. Convert office documents headlessly Through an installed converter, entirely locally. Files: `src/convert.py` ```sh soffice --headless --convert-to pdf --outdir out input.docx ``` 2. Rasterise at a chosen resolution Screen, print or archive. State which the user picked in the output name. Files: `src/rasterise.py` 3. State the loss on editable conversion Prominently. A user who does not know will convert back and lose everything. Files: `src/convert.py` ### Done when - [ ] An office document converts with layout preserved - [ ] A PDF rasterises at the requested resolution - [ ] An editable conversion states what it lost - [ ] All conversions work offline ### Watch out - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. ## Phase 5 · Batch, and the awkward documents A folder of documents processes unattended and the difficult ones are reported. ### Steps 1. Detect awkward documents first Encrypted, damaged, enormous. Report before processing rather than failing partway. Files: `src/triage.py` 2. Isolate per-document failures One bad file must not cost an hour of a batch. Files: `src/batch.py` 3. Write a manifest Inputs, settings, outputs. What makes the batch reproducible and auditable. Files: `src/manifest.py` ### Done when - [ ] Fifty documents process unattended - [ ] An encrypted document is reported and skipped - [ ] A damaged document reports its reason - [ ] Re-running with the same settings is identical ### Watch out - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ## Not in this build - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. ## After v1, if you want it - Redaction that removes content rather than covering it, which is a natural neighbour - A watched folder applying a saved chain, since most of this work is repetitive - Page-level operations such as reordering by drag, once the structural work is trustworthy ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where outputs and manifests are written. Never the source directory. OUTPUT_DIR=./out # Required. Target image resolution. Screen, print and archive want different values. COMPRESS_IMAGE_DPI=150 # Required. Never rasterise a document that has a text layer. Effectively always true. COMPRESS_PRESERVE_TEXT=true # Optional. Only needed if LibreOffice is not on the path. Office conversion is disabled without it. SOFFICE_PATH=/usr/bin/soffice # Required. Inputs, settings and outputs per operation. What makes a batch auditable. MANIFEST_PATH=./out/manifest.json
You are building a lean indie version of iLovePDF Premium. 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 ===== # iLovePDF Premium · indie build A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 | | --- | --- | --- | | Structure | qpdf for page tree operations | merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files | | Content | mupdf or an equivalent for content inspection | compression needs to know what is actually taking the space | | Conversion | LibreOffice headless for office formats | document conversion fidelity is decades of work and is not the project | | Runtime | Python 3.12 orchestrating them | the work is inspection, decisions and bookkeeping | | Storage | Originals untouched, outputs alongside with an operation log | every output should be traceable to its input and settings | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates the tools and does the analysis. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **qpdf and a content inspection library** · free - Why: Structural operations and byte attribution respectively. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf or your package manager, plus a Python binding for inspection. - Verify: qpdf --version runs and a document's objects can be listed - [ ] **LibreOffice for headless conversion** · free - Why: Office document fidelity is decades of work and is the one part of this genuinely not worth attempting. - Get it: Install LibreOffice and confirm the headless conversion works from the command line. - Verify: soffice converts a test document to PDF - [ ] **Six awkward documents** · free - Why: Every phase is tested against them: a scan, a text document with embedded fonts, an encrypted file, a damaged file, a very large one and an office document. - Get it: Collect from documents you already have. Truncate one deliberately to produce the damaged case. - Verify: All six are present and each behaves as expected - [ ] **A second PDF reader** · free - Why: Validating output in the tool that produced it proves nothing, and a structurally broken file often opens fine in a forgiving viewer. - Get it: Any reader other than the one your library uses. A strict one is more useful than a forgiving one. - Verify: It opens a merged output and reports a corrupt one as corrupt ## Quick start ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. - simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity - 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 iLovePDF Premium, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · iLovePDF Premium 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 local PDF operations: merge, split, compress and convert, without uploading anything to replace iLovePDF Premium. 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. ### Rule zero The reason to build this rather than use a web tool is that the documents never leave your machine. That is not a preference: contracts, medical letters and identity documents are exactly what people put through free web converters, and every one of those uploads is a copy on someone else's server with a retention policy nobody read. So there is no network path in this build at all, and the check at the end of every phase is that the operation completed with networking disabled. If a feature genuinely needs a service, it is a separate tool with its own decision rather than a quiet exception here. ### Stack (fixed, do not substitute) - Structure: qpdf for page tree operations · merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files - Content: mupdf or an equivalent for content inspection · compression needs to know what is actually taking the space - Conversion: LibreOffice headless for office formats · document conversion fidelity is decades of work and is not the project - Runtime: Python 3.12 orchestrating them · the work is inspection, decisions and bookkeeping - Storage: Originals untouched, outputs alongside with an operation log · every output should be traceable to its input and settings ### Data model (create this before Phase 1) - documents(id, path, pages, bytes, encrypted, has_text_layer, content_hash, imported_at) - page_stats(document_id, page, image_bytes, text_bytes, font_bytes, other_bytes) · what is actually large, per page - operations(id, kind, input_hashes_json, params_json, output_path, output_bytes, at) · kind is merge, split, compress, convert or rotate - fonts(document_id, name, embedded, subset, bytes) · because unsubset embedded fonts are a common source of size - The page statistics are what turn compression from guesswork into a decision, and they are worth computing before offering any setting. ### Phase 1 · Structure operations that do not corrupt Build: Merge several documents, split by page range, and rotate pages, using a library that understands the page tree. Preserve bookmarks and metadata where possible. Validate each output opens in a different reader. Trap: Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. Done when: Merging three documents produces one with the correct page count and order that opens in two different readers. Splitting and re-merging the parts reproduces the original page order. Rotation persists when opened elsewhere. Bookmarks survive a merge or their loss is reported rather than silent. Do not build yet: No compression, conversion or batch. ### Phase 2 · Finding out what is actually large Build: Analyse a document into per-page byte contributions: images, text, embedded fonts and everything else. Report the largest contributors. Do this before offering any compression setting. Trap: Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. Done when: Analysing a scanned document attributes most of its size to images. Analysing a text document with embedded fonts attributes size to fonts rather than images. The report names the three largest contributors. A document whose size is mostly structural overhead is identified as such. Do not build yet: No compression applied yet. ### Phase 3 · Compression that keeps documents usable Build: Apply the approach the analysis suggests: downsample and recompress images to a target resolution, subset embedded fonts, and remove unused objects. Never rasterise a text document. Report the before and after with the text layer confirmed intact. Trap: Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. Done when: A scanned document compresses substantially through image downsampling. A text document retains a working text layer after compression, verified by extracting text. Font subsetting reduces size without changing rendered appearance. The report states before and after sizes and confirms the text layer survived. Do not build yet: No conversion or batch yet. ### Phase 4 · Conversion in both directions Build: Convert office documents to PDF through a headless converter, and PDF to images at a chosen resolution. For PDF to editable formats, report honestly what the conversion loses rather than presenting it as lossless. Trap: Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. Done when: An office document converts to PDF with its layout preserved, checked against the original. A PDF converts to images at the requested resolution. A PDF to editable conversion produces a result with a stated warning about what was lost. Conversion works with networking disabled. Do not build yet: No batch or annotation yet. ### Phase 5 · Batch, and the awkward documents Build: Process a directory with one operation chain. Handle encrypted, damaged and enormous documents by reporting them rather than failing the run. Write a manifest naming every input, its settings and its output. Trap: An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. Done when: A folder of fifty documents processes unattended. An encrypted document is reported and skipped rather than stopping the run. A damaged document is reported with the reason. The manifest names every input, its settings and its output, and re-running with the same settings produces identical outputs. Do not build yet: Verification and cleanup come last. ### Phase 6 · Verifying the offline promise Build: Run the full test suite with networking disabled and confirm every operation succeeds. Audit the dependencies for anything that fetches at runtime. Document the promise and how a user can verify it themselves. Trap: A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. Done when: The full test suite passes with networking disabled. A dependency audit found no runtime network access, or the offending dependency was replaced. The documentation states the offline promise and how to verify it. Originals are confirmed unmodified across the whole suite by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering and conversion engines, which handle a long tail of malformed documents better than any open toolchain. - Optical character recognition, which is a separate tool with its own quality questions rather than a compression setting. - Mobile applications, which is how a lot of this category is actually used. - Hosted convenience. A web tool needs no installation, which is genuinely why people use one, and the trade is that your documents go with it. ### README must contain - That nothing leaves the machine and how to verify that yourself - That compression is chosen from what analysis says is actually large - That a document with a text layer keeps one, and how that is checked - That PDF to editable conversion is lossy and what it loses - That originals are never modified and every output has a manifest entry ===== AGENTS.md ===== # Agent instructions · iLovePDF Premium indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: qpdf for page tree operations, mupdf or an equivalent for content inspection, LibreOffice headless for office formats, Python 3.12 orchestrating them, Originals untouched, outputs alongside with an 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 - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ===== BUILD_PLAN.md ===== # Build plan · iLovePDF Premium A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 · Structure operations that do not corrupt Merge, split and rotate produce valid documents that open everywhere. ### Steps 1. Use a structural library Page tree operations through qpdf rather than by hand. This is where files get corrupted. Files: `src/structure.py` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Preserve or report metadata loss Bookmarks and document properties. Losing them silently is the common annoyance. Files: `src/structure.py` 3. Validate every output Open in a second reader, and check structure with a tool rather than by eye. ```sh qpdf --check merged.pdf ``` ### Done when - [ ] A merge produces correct page count and order in two readers - [ ] Split then merge reproduces the original order - [ ] Rotation persists elsewhere - [ ] Bookmark loss is preserved or reported ### Watch out - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. ## Phase 2 · Finding out what is actually large You can say why a document is fifty megabytes before compressing it. ### Steps 1. Attribute bytes per page by kind Images, text, fonts, other. The attribution is what makes the next phase a decision. Files: `src/analyse.py` 2. Detect unsubset embedded fonts A full font embedded for three characters is a common and easily fixed cause of size. Files: `src/fonts.py` 3. Report the largest contributors Three is enough. It tells you which compression approach is worth applying. Files: `src/report.py` ### Done when - [ ] A scanned document attributes size to images - [ ] A text document attributes size to fonts - [ ] The three largest contributors are named - [ ] A structurally bloated document is identified ### Watch out - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. ## Phase 3 · Compression that keeps documents usable A document gets smaller without becoming unreadable or losing its text. ### Steps 1. Downsample images to a target resolution Chosen for the use: screen, print or archive. One setting does not serve all three. Files: `src/compress.py` 2. Subset fonts and drop unused objects Cheap size reductions that change nothing visible. Files: `src/compress.py` 3. Assert the text layer survived Extract text before and after and compare. This is the check that prevents the worst outcome. Files: `tests/test_text_layer.py` ```sh pdftotext before.pdf - | wc -w pdftotext after.pdf - | wc -w ``` ### Done when - [ ] A scanned document compresses substantially - [ ] A text document keeps a working text layer - [ ] Font subsetting reduces size without visible change - [ ] Before and after sizes are reported ### Watch out - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. ## Phase 4 · Conversion in both directions Documents convert to and from common formats with the losses stated. ### Steps 1. Convert office documents headlessly Through an installed converter, entirely locally. Files: `src/convert.py` ```sh soffice --headless --convert-to pdf --outdir out input.docx ``` 2. Rasterise at a chosen resolution Screen, print or archive. State which the user picked in the output name. Files: `src/rasterise.py` 3. State the loss on editable conversion Prominently. A user who does not know will convert back and lose everything. Files: `src/convert.py` ### Done when - [ ] An office document converts with layout preserved - [ ] A PDF rasterises at the requested resolution - [ ] An editable conversion states what it lost - [ ] All conversions work offline ### Watch out - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. ## Phase 5 · Batch, and the awkward documents A folder of documents processes unattended and the difficult ones are reported. ### Steps 1. Detect awkward documents first Encrypted, damaged, enormous. Report before processing rather than failing partway. Files: `src/triage.py` 2. Isolate per-document failures One bad file must not cost an hour of a batch. Files: `src/batch.py` 3. Write a manifest Inputs, settings, outputs. What makes the batch reproducible and auditable. Files: `src/manifest.py` ### Done when - [ ] Fifty documents process unattended - [ ] An encrypted document is reported and skipped - [ ] A damaged document reports its reason - [ ] Re-running with the same settings is identical ### Watch out - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ## Not in this build - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. ## After v1, if you want it - Redaction that removes content rather than covering it, which is a natural neighbour - A watched folder applying a saved chain, since most of this work is repetitive - Page-level operations such as reordering by drag, once the structural work is trustworthy ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where outputs and manifests are written. Never the source directory. OUTPUT_DIR=./out # Required. Target image resolution. Screen, print and archive want different values. COMPRESS_IMAGE_DPI=150 # Required. Never rasterise a document that has a text layer. Effectively always true. COMPRESS_PRESERVE_TEXT=true # Optional. Only needed if LibreOffice is not on the path. Office conversion is disabled without it. SOFFICE_PATH=/usr/bin/soffice # Required. Inputs, settings and outputs per operation. What makes a batch auditable. MANIFEST_PATH=./out/manifest.json
You are building a production product version of iLovePDF Premium. 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 ===== # iLovePDF Premium · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For ILovePDF Premium, merge, split, compress, convert, and annotate PDFs without uploading them to a vendor. The hard boundary is simple hosted delivery, mobile apps, desktop tools, ocr, and batch capacity, plus document fidelity, identity, and compliance. ## Product outcome A toolbox that never uploads a document, compresses by fixing what is actually large, never destroys a text layer, and can prove its offline promise by running with the network off. ## 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 - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. - simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - The full suite passes with networking disabled - A compressed text document is confirmed to keep its text layer - Merged output is confirmed valid in a second reader - An encrypted document is confirmed reported rather than fatal - Originals are confirmed unmodified by checksum ===== BRIEF.md ===== # Build brief · iLovePDF Premium 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 local PDF operations: merge, split, compress and convert, without uploading anything to replace iLovePDF Premium. 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. ### Rule zero The reason to build this rather than use a web tool is that the documents never leave your machine. That is not a preference: contracts, medical letters and identity documents are exactly what people put through free web converters, and every one of those uploads is a copy on someone else's server with a retention policy nobody read. So there is no network path in this build at all, and the check at the end of every phase is that the operation completed with networking disabled. If a feature genuinely needs a service, it is a separate tool with its own decision rather than a quiet exception here. ### Stack (fixed, do not substitute) - Structure: qpdf for page tree operations · merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files - Content: mupdf or an equivalent for content inspection · compression needs to know what is actually taking the space - Conversion: LibreOffice headless for office formats · document conversion fidelity is decades of work and is not the project - Runtime: Python 3.12 orchestrating them · the work is inspection, decisions and bookkeeping - Storage: Originals untouched, outputs alongside with an operation log · every output should be traceable to its input and settings ### Data model (create this before Phase 1) - documents(id, path, pages, bytes, encrypted, has_text_layer, content_hash, imported_at) - page_stats(document_id, page, image_bytes, text_bytes, font_bytes, other_bytes) · what is actually large, per page - operations(id, kind, input_hashes_json, params_json, output_path, output_bytes, at) · kind is merge, split, compress, convert or rotate - fonts(document_id, name, embedded, subset, bytes) · because unsubset embedded fonts are a common source of size - The page statistics are what turn compression from guesswork into a decision, and they are worth computing before offering any setting. ### Phase 1 · Structure operations that do not corrupt Build: Merge several documents, split by page range, and rotate pages, using a library that understands the page tree. Preserve bookmarks and metadata where possible. Validate each output opens in a different reader. Trap: Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. Done when: Merging three documents produces one with the correct page count and order that opens in two different readers. Splitting and re-merging the parts reproduces the original page order. Rotation persists when opened elsewhere. Bookmarks survive a merge or their loss is reported rather than silent. Do not build yet: No compression, conversion or batch. ### Phase 2 · Finding out what is actually large Build: Analyse a document into per-page byte contributions: images, text, embedded fonts and everything else. Report the largest contributors. Do this before offering any compression setting. Trap: Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. Done when: Analysing a scanned document attributes most of its size to images. Analysing a text document with embedded fonts attributes size to fonts rather than images. The report names the three largest contributors. A document whose size is mostly structural overhead is identified as such. Do not build yet: No compression applied yet. ### Phase 3 · Compression that keeps documents usable Build: Apply the approach the analysis suggests: downsample and recompress images to a target resolution, subset embedded fonts, and remove unused objects. Never rasterise a text document. Report the before and after with the text layer confirmed intact. Trap: Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. Done when: A scanned document compresses substantially through image downsampling. A text document retains a working text layer after compression, verified by extracting text. Font subsetting reduces size without changing rendered appearance. The report states before and after sizes and confirms the text layer survived. Do not build yet: No conversion or batch yet. ### Phase 4 · Conversion in both directions Build: Convert office documents to PDF through a headless converter, and PDF to images at a chosen resolution. For PDF to editable formats, report honestly what the conversion loses rather than presenting it as lossless. Trap: Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. Done when: An office document converts to PDF with its layout preserved, checked against the original. A PDF converts to images at the requested resolution. A PDF to editable conversion produces a result with a stated warning about what was lost. Conversion works with networking disabled. Do not build yet: No batch or annotation yet. ### Phase 5 · Batch, and the awkward documents Build: Process a directory with one operation chain. Handle encrypted, damaged and enormous documents by reporting them rather than failing the run. Write a manifest naming every input, its settings and its output. Trap: An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. Done when: A folder of fifty documents processes unattended. An encrypted document is reported and skipped rather than stopping the run. A damaged document is reported with the reason. The manifest names every input, its settings and its output, and re-running with the same settings produces identical outputs. Do not build yet: Verification and cleanup come last. ### Phase 6 · Verifying the offline promise Build: Run the full test suite with networking disabled and confirm every operation succeeds. Audit the dependencies for anything that fetches at runtime. Document the promise and how a user can verify it themselves. Trap: A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. Done when: The full test suite passes with networking disabled. A dependency audit found no runtime network access, or the offending dependency was replaced. The documentation states the offline promise and how to verify it. Originals are confirmed unmodified across the whole suite by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering and conversion engines, which handle a long tail of malformed documents better than any open toolchain. - Optical character recognition, which is a separate tool with its own quality questions rather than a compression setting. - Mobile applications, which is how a lot of this category is actually used. - Hosted convenience. A web tool needs no installation, which is genuinely why people use one, and the trade is that your documents go with it. ### README must contain - That nothing leaves the machine and how to verify that yourself - That compression is chosen from what analysis says is actually large - That a document with a text layer keeps one, and how that is checked - That PDF to editable conversion is lossy and what it loses - That originals are never modified and every output has a manifest entry ===== ARCHITECTURE.md ===== # Architecture · iLovePDF Premium ## Stack | Part | Choice | Why | | --- | --- | --- | | Structure | qpdf for page tree operations | merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files | | Content | mupdf or an equivalent for content inspection | compression needs to know what is actually taking the space | | Conversion | LibreOffice headless for office formats | document conversion fidelity is decades of work and is not the project | | Runtime | Python 3.12 orchestrating them | the work is inspection, decisions and bookkeeping | | Storage | Originals untouched, outputs alongside with an operation log | every output should be traceable to its input and settings | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Structure | Page tree operations. | Not worth replacing. Hand-written page tree manipulation is how documents get corrupted. | | Analysis | Byte attribution per page. | Any inspection library. The attribution is what turns compression into a decision. | | Compression | Reducing size without losing usability. | Any implementation, provided the text layer assertion survives. | | Conversion | Format changes and honest loss reporting. | Converters vary in fidelity. Stating the loss is the part that does not change. | | Offline | The promise and its verification. | Not a module so much as a constraint on every other one, checked by running with the network off. | ## 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 outputs and manifests are written. Never the source directory. - `COMPRESS_IMAGE_DPI` · required · Target image resolution. Screen, print and archive want different values. - `COMPRESS_PRESERVE_TEXT` · required · Never rasterise a document that has a text layer. Effectively always true. - `SOFFICE_PATH` · optional · Only needed if LibreOffice is not on the path. Office conversion is disabled without it. - `MANIFEST_PATH` · required · Inputs, settings and outputs per operation. What makes a batch auditable. ## 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 · iLovePDF Premium product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: qpdf for page tree operations, mupdf or an equivalent for content inspection, LibreOffice headless for office formats, Python 3.12 orchestrating them, Originals untouched, outputs alongside with an 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 - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. - A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. ===== MILESTONES.md ===== # Delivery milestones · iLovePDF Premium Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Structure operations that do not corrupt Merge, split and rotate produce valid documents that open everywhere. ### Steps 1. Use a structural library Page tree operations through qpdf rather than by hand. This is where files get corrupted. Files: `src/structure.py` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Preserve or report metadata loss Bookmarks and document properties. Losing them silently is the common annoyance. Files: `src/structure.py` 3. Validate every output Open in a second reader, and check structure with a tool rather than by eye. ```sh qpdf --check merged.pdf ``` ### Done when - [ ] A merge produces correct page count and order in two readers - [ ] Split then merge reproduces the original order - [ ] Rotation persists elsewhere - [ ] Bookmark loss is preserved or reported ### Watch out - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. ## M2 · Finding out what is actually large You can say why a document is fifty megabytes before compressing it. ### Steps 1. Attribute bytes per page by kind Images, text, fonts, other. The attribution is what makes the next phase a decision. Files: `src/analyse.py` 2. Detect unsubset embedded fonts A full font embedded for three characters is a common and easily fixed cause of size. Files: `src/fonts.py` 3. Report the largest contributors Three is enough. It tells you which compression approach is worth applying. Files: `src/report.py` ### Done when - [ ] A scanned document attributes size to images - [ ] A text document attributes size to fonts - [ ] The three largest contributors are named - [ ] A structurally bloated document is identified ### Watch out - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. ## M3 · Compression that keeps documents usable A document gets smaller without becoming unreadable or losing its text. ### Steps 1. Downsample images to a target resolution Chosen for the use: screen, print or archive. One setting does not serve all three. Files: `src/compress.py` 2. Subset fonts and drop unused objects Cheap size reductions that change nothing visible. Files: `src/compress.py` 3. Assert the text layer survived Extract text before and after and compare. This is the check that prevents the worst outcome. Files: `tests/test_text_layer.py` ```sh pdftotext before.pdf - | wc -w pdftotext after.pdf - | wc -w ``` ### Done when - [ ] A scanned document compresses substantially - [ ] A text document keeps a working text layer - [ ] Font subsetting reduces size without visible change - [ ] Before and after sizes are reported ### Watch out - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. ## M4 · Conversion in both directions Documents convert to and from common formats with the losses stated. ### Steps 1. Convert office documents headlessly Through an installed converter, entirely locally. Files: `src/convert.py` ```sh soffice --headless --convert-to pdf --outdir out input.docx ``` 2. Rasterise at a chosen resolution Screen, print or archive. State which the user picked in the output name. Files: `src/rasterise.py` 3. State the loss on editable conversion Prominently. A user who does not know will convert back and lose everything. Files: `src/convert.py` ### Done when - [ ] An office document converts with layout preserved - [ ] A PDF rasterises at the requested resolution - [ ] An editable conversion states what it lost - [ ] All conversions work offline ### Watch out - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. ## M5 · Batch, and the awkward documents A folder of documents processes unattended and the difficult ones are reported. ### Steps 1. Detect awkward documents first Encrypted, damaged, enormous. Report before processing rather than failing partway. Files: `src/triage.py` 2. Isolate per-document failures One bad file must not cost an hour of a batch. Files: `src/batch.py` 3. Write a manifest Inputs, settings, outputs. What makes the batch reproducible and auditable. Files: `src/manifest.py` ### Done when - [ ] Fifty documents process unattended - [ ] An encrypted document is reported and skipped - [ ] A damaged document reports its reason - [ ] Re-running with the same settings is identical ### Watch out - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ## M6 · Verifying the offline promise (production only) You can prove nothing left the machine. ### Steps 1. Run everything offline The whole suite with networking disabled. Any failure is a dependency problem. Files: `scripts/test-offline.sh` 2. Audit dependencies for network access Fonts, language packs, licence checks. All three are common and all three break the promise. Files: `docs/offline-audit.md` 3. Document how to verify So a user can check the claim rather than believing it. Files: `README.md` ### Done when - [ ] The full suite passes with networking disabled - [ ] A dependency audit found no runtime network access - [ ] The documentation explains how to verify the promise - [ ] Originals are unmodified across the suite ### Watch out - A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. ===== OPERATIONS.md ===== # Operations · iLovePDF Premium ## Backup Source documents are the irreplaceable data. Outputs regenerate from sources plus the manifest, which is why the manifest records settings rather than just paths. ## Restore Restore sources and manifests, then re-run and compare against a stored output. Byte equality proves it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch use, the triage report is the thing to watch: a rising count of encrypted or damaged documents usually means the source of the documents changed rather than the tool breaking. ## Incident checklist Producing a compressed document that lost its text layer is the failure that matters, since it is invisible until someone needs to search or copy from it. The extraction assertion is the control and belongs in every release check. 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 full suite passes with networking disabled - [ ] A compressed text document is confirmed to keep its text layer - [ ] Merged output is confirmed valid in a second reader - [ ] An encrypted document is confirmed reported rather than fatal - [ ] Originals are confirmed unmodified by checksum ## Launch constraint Do not market omitted iLovePDF Premium 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 outputs and manifests are written. Never the source directory. OUTPUT_DIR=./out # Required. Target image resolution. Screen, print and archive want different values. COMPRESS_IMAGE_DPI=150 # Required. Never rasterise a document that has a text layer. Effectively always true. COMPRESS_PRESERVE_TEXT=true # Optional. Only needed if LibreOffice is not on the path. Office conversion is disabled without it. SOFFICE_PATH=/usr/bin/soffice # Required. Inputs, settings and outputs per operation. What makes a batch auditable. MANIFEST_PATH=./out/manifest.json
# iLovePDF Premium · indie build A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 | | --- | --- | --- | | Structure | qpdf for page tree operations | merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files | | Content | mupdf or an equivalent for content inspection | compression needs to know what is actually taking the space | | Conversion | LibreOffice headless for office formats | document conversion fidelity is decades of work and is not the project | | Runtime | Python 3.12 orchestrating them | the work is inspection, decisions and bookkeeping | | Storage | Originals untouched, outputs alongside with an operation log | every output should be traceable to its input and settings | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Orchestrates the tools and does the analysis. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **qpdf and a content inspection library** · free - Why: Structural operations and byte attribution respectively. Writing either yourself means implementing parts of the PDF specification. - Get it: brew install qpdf mupdf or your package manager, plus a Python binding for inspection. - Verify: qpdf --version runs and a document's objects can be listed - [ ] **LibreOffice for headless conversion** · free - Why: Office document fidelity is decades of work and is the one part of this genuinely not worth attempting. - Get it: Install LibreOffice and confirm the headless conversion works from the command line. - Verify: soffice converts a test document to PDF - [ ] **Six awkward documents** · free - Why: Every phase is tested against them: a scan, a text document with embedded fonts, an encrypted file, a damaged file, a very large one and an office document. - Get it: Collect from documents you already have. Truncate one deliberately to produce the damaged case. - Verify: All six are present and each behaves as expected - [ ] **A second PDF reader** · free - Why: Validating output in the tool that produced it proves nothing, and a structurally broken file often opens fine in a forgiving viewer. - Get it: Any reader other than the one your library uses. A strict one is more useful than a forgiving one. - Verify: It opens a merged output and reports a corrupt one as corrupt ## Quick start ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. - simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity - 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 iLovePDF Premium, and the README should say so rather than pretend.
# Build brief · iLovePDF Premium 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 local PDF operations: merge, split, compress and convert, without uploading anything to replace iLovePDF Premium. 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. ### Rule zero The reason to build this rather than use a web tool is that the documents never leave your machine. That is not a preference: contracts, medical letters and identity documents are exactly what people put through free web converters, and every one of those uploads is a copy on someone else's server with a retention policy nobody read. So there is no network path in this build at all, and the check at the end of every phase is that the operation completed with networking disabled. If a feature genuinely needs a service, it is a separate tool with its own decision rather than a quiet exception here. ### Stack (fixed, do not substitute) - Structure: qpdf for page tree operations · merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files - Content: mupdf or an equivalent for content inspection · compression needs to know what is actually taking the space - Conversion: LibreOffice headless for office formats · document conversion fidelity is decades of work and is not the project - Runtime: Python 3.12 orchestrating them · the work is inspection, decisions and bookkeeping - Storage: Originals untouched, outputs alongside with an operation log · every output should be traceable to its input and settings ### Data model (create this before Phase 1) - documents(id, path, pages, bytes, encrypted, has_text_layer, content_hash, imported_at) - page_stats(document_id, page, image_bytes, text_bytes, font_bytes, other_bytes) · what is actually large, per page - operations(id, kind, input_hashes_json, params_json, output_path, output_bytes, at) · kind is merge, split, compress, convert or rotate - fonts(document_id, name, embedded, subset, bytes) · because unsubset embedded fonts are a common source of size - The page statistics are what turn compression from guesswork into a decision, and they are worth computing before offering any setting. ### Phase 1 · Structure operations that do not corrupt Build: Merge several documents, split by page range, and rotate pages, using a library that understands the page tree. Preserve bookmarks and metadata where possible. Validate each output opens in a different reader. Trap: Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. Done when: Merging three documents produces one with the correct page count and order that opens in two different readers. Splitting and re-merging the parts reproduces the original page order. Rotation persists when opened elsewhere. Bookmarks survive a merge or their loss is reported rather than silent. Do not build yet: No compression, conversion or batch. ### Phase 2 · Finding out what is actually large Build: Analyse a document into per-page byte contributions: images, text, embedded fonts and everything else. Report the largest contributors. Do this before offering any compression setting. Trap: Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. Done when: Analysing a scanned document attributes most of its size to images. Analysing a text document with embedded fonts attributes size to fonts rather than images. The report names the three largest contributors. A document whose size is mostly structural overhead is identified as such. Do not build yet: No compression applied yet. ### Phase 3 · Compression that keeps documents usable Build: Apply the approach the analysis suggests: downsample and recompress images to a target resolution, subset embedded fonts, and remove unused objects. Never rasterise a text document. Report the before and after with the text layer confirmed intact. Trap: Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. Done when: A scanned document compresses substantially through image downsampling. A text document retains a working text layer after compression, verified by extracting text. Font subsetting reduces size without changing rendered appearance. The report states before and after sizes and confirms the text layer survived. Do not build yet: No conversion or batch yet. ### Phase 4 · Conversion in both directions Build: Convert office documents to PDF through a headless converter, and PDF to images at a chosen resolution. For PDF to editable formats, report honestly what the conversion loses rather than presenting it as lossless. Trap: Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. Done when: An office document converts to PDF with its layout preserved, checked against the original. A PDF converts to images at the requested resolution. A PDF to editable conversion produces a result with a stated warning about what was lost. Conversion works with networking disabled. Do not build yet: No batch or annotation yet. ### Phase 5 · Batch, and the awkward documents Build: Process a directory with one operation chain. Handle encrypted, damaged and enormous documents by reporting them rather than failing the run. Write a manifest naming every input, its settings and its output. Trap: An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. Done when: A folder of fifty documents processes unattended. An encrypted document is reported and skipped rather than stopping the run. A damaged document is reported with the reason. The manifest names every input, its settings and its output, and re-running with the same settings produces identical outputs. Do not build yet: Verification and cleanup come last. ### Phase 6 · Verifying the offline promise Build: Run the full test suite with networking disabled and confirm every operation succeeds. Audit the dependencies for anything that fetches at runtime. Document the promise and how a user can verify it themselves. Trap: A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. Done when: The full test suite passes with networking disabled. A dependency audit found no runtime network access, or the offending dependency was replaced. The documentation states the offline promise and how to verify it. Originals are confirmed unmodified across the whole suite by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering and conversion engines, which handle a long tail of malformed documents better than any open toolchain. - Optical character recognition, which is a separate tool with its own quality questions rather than a compression setting. - Mobile applications, which is how a lot of this category is actually used. - Hosted convenience. A web tool needs no installation, which is genuinely why people use one, and the trade is that your documents go with it. ### README must contain - That nothing leaves the machine and how to verify that yourself - That compression is chosen from what analysis says is actually large - That a document with a text layer keeps one, and how that is checked - That PDF to editable conversion is lossy and what it loses - That originals are never modified and every output has a manifest entry
# Agent instructions · iLovePDF Premium indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: qpdf for page tree operations, mupdf or an equivalent for content inspection, LibreOffice headless for office formats, Python 3.12 orchestrating them, Originals untouched, outputs alongside with an 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 - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest.
# Build plan · iLovePDF Premium A local toolbox for the operations people actually need: merging and splitting with the page tree handled properly, compression that reduces size by fixing what is actually large, conversion to and from common formats, and every operation leaving the original untouched with a record of 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 · Structure operations that do not corrupt Merge, split and rotate produce valid documents that open everywhere. ### Steps 1. Use a structural library Page tree operations through qpdf rather than by hand. This is where files get corrupted. Files: `src/structure.py` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Preserve or report metadata loss Bookmarks and document properties. Losing them silently is the common annoyance. Files: `src/structure.py` 3. Validate every output Open in a second reader, and check structure with a tool rather than by eye. ```sh qpdf --check merged.pdf ``` ### Done when - [ ] A merge produces correct page count and order in two readers - [ ] Split then merge reproduces the original order - [ ] Rotation persists elsewhere - [ ] Bookmark loss is preserved or reported ### Watch out - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. ## Phase 2 · Finding out what is actually large You can say why a document is fifty megabytes before compressing it. ### Steps 1. Attribute bytes per page by kind Images, text, fonts, other. The attribution is what makes the next phase a decision. Files: `src/analyse.py` 2. Detect unsubset embedded fonts A full font embedded for three characters is a common and easily fixed cause of size. Files: `src/fonts.py` 3. Report the largest contributors Three is enough. It tells you which compression approach is worth applying. Files: `src/report.py` ### Done when - [ ] A scanned document attributes size to images - [ ] A text document attributes size to fonts - [ ] The three largest contributors are named - [ ] A structurally bloated document is identified ### Watch out - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. ## Phase 3 · Compression that keeps documents usable A document gets smaller without becoming unreadable or losing its text. ### Steps 1. Downsample images to a target resolution Chosen for the use: screen, print or archive. One setting does not serve all three. Files: `src/compress.py` 2. Subset fonts and drop unused objects Cheap size reductions that change nothing visible. Files: `src/compress.py` 3. Assert the text layer survived Extract text before and after and compare. This is the check that prevents the worst outcome. Files: `tests/test_text_layer.py` ```sh pdftotext before.pdf - | wc -w pdftotext after.pdf - | wc -w ``` ### Done when - [ ] A scanned document compresses substantially - [ ] A text document keeps a working text layer - [ ] Font subsetting reduces size without visible change - [ ] Before and after sizes are reported ### Watch out - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. ## Phase 4 · Conversion in both directions Documents convert to and from common formats with the losses stated. ### Steps 1. Convert office documents headlessly Through an installed converter, entirely locally. Files: `src/convert.py` ```sh soffice --headless --convert-to pdf --outdir out input.docx ``` 2. Rasterise at a chosen resolution Screen, print or archive. State which the user picked in the output name. Files: `src/rasterise.py` 3. State the loss on editable conversion Prominently. A user who does not know will convert back and lose everything. Files: `src/convert.py` ### Done when - [ ] An office document converts with layout preserved - [ ] A PDF rasterises at the requested resolution - [ ] An editable conversion states what it lost - [ ] All conversions work offline ### Watch out - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. ## Phase 5 · Batch, and the awkward documents A folder of documents processes unattended and the difficult ones are reported. ### Steps 1. Detect awkward documents first Encrypted, damaged, enormous. Report before processing rather than failing partway. Files: `src/triage.py` 2. Isolate per-document failures One bad file must not cost an hour of a batch. Files: `src/batch.py` 3. Write a manifest Inputs, settings, outputs. What makes the batch reproducible and auditable. Files: `src/manifest.py` ### Done when - [ ] Fifty documents process unattended - [ ] An encrypted document is reported and skipped - [ ] A damaged document reports its reason - [ ] Re-running with the same settings is identical ### Watch out - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ## Not in this build - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. ## After v1, if you want it - Redaction that removes content rather than covering it, which is a natural neighbour - A watched folder applying a saved chain, since most of this work is repetitive - Page-level operations such as reordering by drag, once the structural work is trustworthy
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Where outputs and manifests are written. Never the source directory. OUTPUT_DIR=./out # Required. Target image resolution. Screen, print and archive want different values. COMPRESS_IMAGE_DPI=150 # Required. Never rasterise a document that has a text layer. Effectively always true. COMPRESS_PRESERVE_TEXT=true # Optional. Only needed if LibreOffice is not on the path. Office conversion is disabled without it. SOFFICE_PATH=/usr/bin/soffice # Required. Inputs, settings and outputs per operation. What makes a batch auditable. MANIFEST_PATH=./out/manifest.json
# iLovePDF Premium · product brief ## Problem The core loop is small enough for a capable coding agent to produce a useful local version in one sitting. For ILovePDF Premium, merge, split, compress, convert, and annotate PDFs without uploading them to a vendor. The hard boundary is simple hosted delivery, mobile apps, desktop tools, ocr, and batch capacity, plus document fidelity, identity, and compliance. ## Product outcome A toolbox that never uploads a document, compresses by fixing what is actually large, never destroys a text layer, and can prove its offline promise by running with the network off. ## 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 - Proprietary rendering and conversion engines. - Optical character recognition. - Mobile applications. - The convenience of a web tool needing no installation. - simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity - pixel-perfect proprietary PDF engine - identity verification - qualified trust services - large template and integration ecosystem ## Success criteria - The full suite passes with networking disabled - A compressed text document is confirmed to keep its text layer - Merged output is confirmed valid in a second reader - An encrypted document is confirmed reported rather than fatal - Originals are confirmed unmodified by checksum
# Build brief · iLovePDF Premium 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 local PDF operations: merge, split, compress and convert, without uploading anything to replace iLovePDF Premium. 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. ### Rule zero The reason to build this rather than use a web tool is that the documents never leave your machine. That is not a preference: contracts, medical letters and identity documents are exactly what people put through free web converters, and every one of those uploads is a copy on someone else's server with a retention policy nobody read. So there is no network path in this build at all, and the check at the end of every phase is that the operation completed with networking disabled. If a feature genuinely needs a service, it is a separate tool with its own decision rather than a quiet exception here. ### Stack (fixed, do not substitute) - Structure: qpdf for page tree operations · merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files - Content: mupdf or an equivalent for content inspection · compression needs to know what is actually taking the space - Conversion: LibreOffice headless for office formats · document conversion fidelity is decades of work and is not the project - Runtime: Python 3.12 orchestrating them · the work is inspection, decisions and bookkeeping - Storage: Originals untouched, outputs alongside with an operation log · every output should be traceable to its input and settings ### Data model (create this before Phase 1) - documents(id, path, pages, bytes, encrypted, has_text_layer, content_hash, imported_at) - page_stats(document_id, page, image_bytes, text_bytes, font_bytes, other_bytes) · what is actually large, per page - operations(id, kind, input_hashes_json, params_json, output_path, output_bytes, at) · kind is merge, split, compress, convert or rotate - fonts(document_id, name, embedded, subset, bytes) · because unsubset embedded fonts are a common source of size - The page statistics are what turn compression from guesswork into a decision, and they are worth computing before offering any setting. ### Phase 1 · Structure operations that do not corrupt Build: Merge several documents, split by page range, and rotate pages, using a library that understands the page tree. Preserve bookmarks and metadata where possible. Validate each output opens in a different reader. Trap: Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. Done when: Merging three documents produces one with the correct page count and order that opens in two different readers. Splitting and re-merging the parts reproduces the original page order. Rotation persists when opened elsewhere. Bookmarks survive a merge or their loss is reported rather than silent. Do not build yet: No compression, conversion or batch. ### Phase 2 · Finding out what is actually large Build: Analyse a document into per-page byte contributions: images, text, embedded fonts and everything else. Report the largest contributors. Do this before offering any compression setting. Trap: Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. Done when: Analysing a scanned document attributes most of its size to images. Analysing a text document with embedded fonts attributes size to fonts rather than images. The report names the three largest contributors. A document whose size is mostly structural overhead is identified as such. Do not build yet: No compression applied yet. ### Phase 3 · Compression that keeps documents usable Build: Apply the approach the analysis suggests: downsample and recompress images to a target resolution, subset embedded fonts, and remove unused objects. Never rasterise a text document. Report the before and after with the text layer confirmed intact. Trap: Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. Done when: A scanned document compresses substantially through image downsampling. A text document retains a working text layer after compression, verified by extracting text. Font subsetting reduces size without changing rendered appearance. The report states before and after sizes and confirms the text layer survived. Do not build yet: No conversion or batch yet. ### Phase 4 · Conversion in both directions Build: Convert office documents to PDF through a headless converter, and PDF to images at a chosen resolution. For PDF to editable formats, report honestly what the conversion loses rather than presenting it as lossless. Trap: Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. Done when: An office document converts to PDF with its layout preserved, checked against the original. A PDF converts to images at the requested resolution. A PDF to editable conversion produces a result with a stated warning about what was lost. Conversion works with networking disabled. Do not build yet: No batch or annotation yet. ### Phase 5 · Batch, and the awkward documents Build: Process a directory with one operation chain. Handle encrypted, damaged and enormous documents by reporting them rather than failing the run. Write a manifest naming every input, its settings and its output. Trap: An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. Done when: A folder of fifty documents processes unattended. An encrypted document is reported and skipped rather than stopping the run. A damaged document is reported with the reason. The manifest names every input, its settings and its output, and re-running with the same settings produces identical outputs. Do not build yet: Verification and cleanup come last. ### Phase 6 · Verifying the offline promise Build: Run the full test suite with networking disabled and confirm every operation succeeds. Audit the dependencies for anything that fetches at runtime. Document the promise and how a user can verify it themselves. Trap: A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip. Done when: The full test suite passes with networking disabled. A dependency audit found no runtime network access, or the offending dependency was replaced. The documentation states the offline promise and how to verify it. Originals are confirmed unmodified across the whole suite by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - The proprietary rendering and conversion engines, which handle a long tail of malformed documents better than any open toolchain. - Optical character recognition, which is a separate tool with its own quality questions rather than a compression setting. - Mobile applications, which is how a lot of this category is actually used. - Hosted convenience. A web tool needs no installation, which is genuinely why people use one, and the trade is that your documents go with it. ### README must contain - That nothing leaves the machine and how to verify that yourself - That compression is chosen from what analysis says is actually large - That a document with a text layer keeps one, and how that is checked - That PDF to editable conversion is lossy and what it loses - That originals are never modified and every output has a manifest entry
# Architecture · iLovePDF Premium ## Stack | Part | Choice | Why | | --- | --- | --- | | Structure | qpdf for page tree operations | merging, splitting and rotating touch the document structure and hand-written manipulation corrupts files | | Content | mupdf or an equivalent for content inspection | compression needs to know what is actually taking the space | | Conversion | LibreOffice headless for office formats | document conversion fidelity is decades of work and is not the project | | Runtime | Python 3.12 orchestrating them | the work is inspection, decisions and bookkeeping | | Storage | Originals untouched, outputs alongside with an operation log | every output should be traceable to its input and settings | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Structure | Page tree operations. | Not worth replacing. Hand-written page tree manipulation is how documents get corrupted. | | Analysis | Byte attribution per page. | Any inspection library. The attribution is what turns compression into a decision. | | Compression | Reducing size without losing usability. | Any implementation, provided the text layer assertion survives. | | Conversion | Format changes and honest loss reporting. | Converters vary in fidelity. Stating the loss is the part that does not change. | | Offline | The promise and its verification. | Not a module so much as a constraint on every other one, checked by running with the network off. | ## 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 outputs and manifests are written. Never the source directory. - `COMPRESS_IMAGE_DPI` · required · Target image resolution. Screen, print and archive want different values. - `COMPRESS_PRESERVE_TEXT` · required · Never rasterise a document that has a text layer. Effectively always true. - `SOFFICE_PATH` · optional · Only needed if LibreOffice is not on the path. Office conversion is disabled without it. - `MANIFEST_PATH` · required · Inputs, settings and outputs per operation. What makes a batch auditable. ## 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 · iLovePDF Premium product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: qpdf for page tree operations, mupdf or an equivalent for content inspection, LibreOffice headless for office formats, Python 3.12 orchestrating them, Originals untouched, outputs alongside with an 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 - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. - A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip.
# Delivery milestones · iLovePDF Premium Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Structure operations that do not corrupt Merge, split and rotate produce valid documents that open everywhere. ### Steps 1. Use a structural library Page tree operations through qpdf rather than by hand. This is where files get corrupted. Files: `src/structure.py` ```sh qpdf --empty --pages a.pdf b.pdf -- merged.pdf ``` 2. Preserve or report metadata loss Bookmarks and document properties. Losing them silently is the common annoyance. Files: `src/structure.py` 3. Validate every output Open in a second reader, and check structure with a tool rather than by eye. ```sh qpdf --check merged.pdf ``` ### Done when - [ ] A merge produces correct page count and order in two readers - [ ] Split then merge reproduces the original order - [ ] Rotation persists elsewhere - [ ] Bookmark loss is preserved or reported ### Watch out - Merging by concatenating page objects without rebuilding the page tree produces a file that opens in a forgiving reader and fails in a strict one, and the failure appears at the worst moment on someone else's machine. Use a library that rebuilds the structure, and validate the output rather than trusting that it looked fine in one viewer. ## M2 · Finding out what is actually large You can say why a document is fifty megabytes before compressing it. ### Steps 1. Attribute bytes per page by kind Images, text, fonts, other. The attribution is what makes the next phase a decision. Files: `src/analyse.py` 2. Detect unsubset embedded fonts A full font embedded for three characters is a common and easily fixed cause of size. Files: `src/fonts.py` 3. Report the largest contributors Three is enough. It tells you which compression approach is worth applying. Files: `src/report.py` ### Done when - [ ] A scanned document attributes size to images - [ ] A text document attributes size to fonts - [ ] The three largest contributors are named - [ ] A structurally bloated document is identified ### Watch out - Applying a generic compression setting to every document produces poor results in both directions: a scan-heavy document stays large because the setting was gentle, and a text document is degraded for nothing because it was never image-heavy in the first place. Measure first, then choose an approach per document. ## M3 · Compression that keeps documents usable A document gets smaller without becoming unreadable or losing its text. ### Steps 1. Downsample images to a target resolution Chosen for the use: screen, print or archive. One setting does not serve all three. Files: `src/compress.py` 2. Subset fonts and drop unused objects Cheap size reductions that change nothing visible. Files: `src/compress.py` 3. Assert the text layer survived Extract text before and after and compare. This is the check that prevents the worst outcome. Files: `tests/test_text_layer.py` ```sh pdftotext before.pdf - | wc -w pdftotext after.pdf - | wc -w ``` ### Done when - [ ] A scanned document compresses substantially - [ ] A text document keeps a working text layer - [ ] Font subsetting reduces size without visible change - [ ] Before and after sizes are reported ### Watch out - Rasterising pages to reach a size target destroys the text layer, so the document stops being searchable, selectable and accessible, and that loss is invisible in a viewer until someone tries to copy a line. If a document has a text layer, it must still have one afterwards. ## M4 · Conversion in both directions Documents convert to and from common formats with the losses stated. ### Steps 1. Convert office documents headlessly Through an installed converter, entirely locally. Files: `src/convert.py` ```sh soffice --headless --convert-to pdf --outdir out input.docx ``` 2. Rasterise at a chosen resolution Screen, print or archive. State which the user picked in the output name. Files: `src/rasterise.py` 3. State the loss on editable conversion Prominently. A user who does not know will convert back and lose everything. Files: `src/convert.py` ### Done when - [ ] An office document converts with layout preserved - [ ] A PDF rasterises at the requested resolution - [ ] An editable conversion states what it lost - [ ] All conversions work offline ### Watch out - Converting a PDF back to an editable document is inherently lossy, because a PDF describes positioned glyphs rather than a document structure, and the result is frequently a mess of text boxes. Say so rather than presenting it as a round trip, since a user who edits the result and converts back will lose the layout entirely. ## M5 · Batch, and the awkward documents A folder of documents processes unattended and the difficult ones are reported. ### Steps 1. Detect awkward documents first Encrypted, damaged, enormous. Report before processing rather than failing partway. Files: `src/triage.py` 2. Isolate per-document failures One bad file must not cost an hour of a batch. Files: `src/batch.py` 3. Write a manifest Inputs, settings, outputs. What makes the batch reproducible and auditable. Files: `src/manifest.py` ### Done when - [ ] Fifty documents process unattended - [ ] An encrypted document is reported and skipped - [ ] A damaged document reports its reason - [ ] Re-running with the same settings is identical ### Watch out - An encrypted document in a batch will stop a naive run partway through, usually after an hour, and restarting reprocesses everything already done. Detect the awkward cases at the start of the run, report them, and process the rest. ## M6 · Verifying the offline promise (production only) You can prove nothing left the machine. ### Steps 1. Run everything offline The whole suite with networking disabled. Any failure is a dependency problem. Files: `scripts/test-offline.sh` 2. Audit dependencies for network access Fonts, language packs, licence checks. All three are common and all three break the promise. Files: `docs/offline-audit.md` 3. Document how to verify So a user can check the claim rather than believing it. Files: `README.md` ### Done when - [ ] The full suite passes with networking disabled - [ ] A dependency audit found no runtime network access - [ ] The documentation explains how to verify the promise - [ ] Originals are unmodified across the suite ### Watch out - A dependency that fetches a font, a language pack or a licence check at runtime breaks the offline promise without any code of yours doing so, and it will do it silently on a machine that happens to be online. Run the tests with networking off and treat any failure as a dependency to replace rather than a test to skip.
# Operations · iLovePDF Premium ## Backup Source documents are the irreplaceable data. Outputs regenerate from sources plus the manifest, which is why the manifest records settings rather than just paths. ## Restore Restore sources and manifests, then re-run and compare against a stored output. Byte equality proves it. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch use, the triage report is the thing to watch: a rising count of encrypted or damaged documents usually means the source of the documents changed rather than the tool breaking. ## Incident checklist Producing a compressed document that lost its text layer is the failure that matters, since it is invisible until someone needs to search or copy from it. The extraction assertion is the control and belongs in every release check. 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 full suite passes with networking disabled - [ ] A compressed text document is confirmed to keep its text layer - [ ] Merged output is confirmed valid in a second reader - [ ] An encrypted document is confirmed reported rather than fatal - [ ] Originals are confirmed unmodified by checksum ## Launch constraint Do not market omitted iLovePDF Premium 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 outputs and manifests are written. Never the source directory. OUTPUT_DIR=./out # Required. Target image resolution. Screen, print and archive want different values. COMPRESS_IMAGE_DPI=150 # Required. Never rasterise a document that has a text layer. Effectively always true. COMPRESS_PRESERVE_TEXT=true # Optional. Only needed if LibreOffice is not on the path. Office conversion is disabled without it. SOFFICE_PATH=/usr/bin/soffice # Required. Inputs, settings and outputs per operation. What makes a batch auditable. MANIFEST_PATH=./out/manifest.json
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for ILovePDF Premium 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.
xsimple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity
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.
all 3 free alternatives to iLovePDF Premium →· no votes, no pay-to-list · just what's real
iLovePDF Premium pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| basic | $0/user | $0/user | Examples per task: merge 25 files/100 MB; compress 2 files/200 MB; most Office conversions 1 file/15 MB; image-to-PDF 20 files/40 MB; sign 3 files/50 MB; redact/compare up to 400 MB |
| premium | $9/user | $5/user | Unlimited document processing overall; task caps commonly 10 files or 4 GB (tool-specific); 2,000 AI credits; 5 advanced eSignatures; web, desktop and mobile |
| business | custom | — | 25+ users; custom AI credits and advanced-signature allowance; SSO, dedicated manager, custom contract |
free tierTool-specific: merge 25 files/100 MB; compress 2/200 MB; most conversions 1/15 MB; image-to-PDF 20/40 MB; sign 3/50 MB; redact and compare 2/400 MB; no AI credits
billingmonthly + annual (Premium $9 monthly or $60/year = $5/month; annual is 42% lower); Business custom
hidden costsPremium includes 2,000 AI credits and only 5 advanced eSignatures; Business pricing is quote-only; adding/removing licenses adjusts the subscription
verified 2026-08-13 · source ↗
Vibecode iLovePDF Premium
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal iLovePDF Premium replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does iLovePDF Premium cost?
iLovePDF Premium costs about $9/month (Premium, checked 2026-07-31), which is $108 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing iLovePDF Premium?
Honestly: simple hosted delivery, mobile apps, desktop tools, OCR, and batch capacity; 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 iLovePDF Premium?
Yes: Stirling PDF (The private iLovePDF answer: merge, split, compress, convert, OCR, annotate, redact, and batch-process without feeding documents to somebody else's cloud.) PDF24 (Nearly every common PDF transformation is free and unmetered; Windows users also get an offline desktop suite.) PDFgear (A polished free editor and converter with local desktop and mobile apps; fewer batch and server workflows than Stirling PDF.) All 3 curated free alternatives are at vibecodeit.com/ilovepdf-premium/alternatives. The prompt is for when you want it exactly your way.