PDF is a presentation format, not a document model: it records where glyphs are placed, not what paragraph they belong to. Editing a sentence therefore has no defined meaning, and most tools either decline to try or produce a visually plausible result that has quietly destroyed something. PdfRomeo is a native macOS workspace for Apple Silicon combining a document viewer with forty-three batch tools, and its principal engineering contribution is a paragraph editor that re-wraps text in place and moves the surrounding content to accommodate it. This paper describes that mechanism and the architecture around it. Three design decisions carry the work. First, the engine layer contains no user-interface imports whatsoever, so every operation is drivable from a script, a server, or a test harness without a window. Second, reflow does not use the high-level text-drawing routines of the underlying PDF library: those routines were measured to be structurally unfit — unable to render mixed bold and italic in one call, silently ignoring justification for embedded fonts, and shattering words when chained — so the system measures with the document's own embedded font widths, breaks lines itself, and emits a content-stream fragment that references the page's existing font resources without re-embedding anything. Third, the phase that moves content down the page is treated as the only irreversible operation in the system: because shifted content that passes the page edge is lost silently rather than clipped, the implementation measures the ink in every band before writing, re-stamps the entire page rather than the affected region, and edits the page in place so that links, form widgets, annotations and bookmarks survive. Where a correct result cannot be guaranteed — tables, multi-column or rotated pages, contents pages, OCR layers — the operation refuses with a stated reason rather than producing damage that looks like success.
A word processor stores a document as structure: paragraphs made of runs, laid out at display time. A PDF stores the result of that layout — positioned glyphs, drawing operators, resource dictionaries — and discards the structure that produced it. There is no paragraph object in a PDF to edit.
This gap explains the state of PDF editors. Most either refuse to edit text, or convert to another format and back (losing fidelity), or overlay a white rectangle and draw new text on top (leaving the original text extractable underneath and the layout unchanged). Editors that genuinely re-wrap a paragraph must reconstruct structure that was never recorded, and must then modify a page whose remaining content has no idea the paragraph grew.
PdfRomeo attempts the honest version of this operation. Selecting Edit Text outlines every paragraph the engine believes it can safely re-wrap; double-clicking one and retyping re-wraps the paragraph in the document's own font with its own justification and inline emphasis, and moves the content below it to make room — or refuses, with a reason, where doing so would damage the page. Sections 3 through 6 describe how that is implemented. Section 2 describes the architecture it sits in, and Section 7 the refusal policy that bounds it.
The application separates into three layers, and the separation is enforced rather than aspirational: no module in the engine layer imports the UI toolkit.
| Layer | Contents | Depends on |
|---|---|---|
| Interface | Workspace, viewer, panels, tool forms, commenting | Qt (PySide6), engine |
| Workers | Background execution, progress, cancellation | Engine |
| Engine | Document session, page geometry, reflow, font metrics, text blocks, conversion, tool operations | pikepdf, PyMuPDF — no UI toolkit |
Three consequences follow. Every one of the forty-three tools is callable headlessly, which makes batch and server use possible without the application. The engine is testable without instantiating a window or an event loop, which matters because the behaviours that need testing are geometric and byte-level, not visual. And the operations that can destroy a document are confined to a layer with no user-interface concerns in it, so their correctness can be reasoned about in isolation.
Above the engine, the workspace presents documents as tabs on a threaded, continuously scrolling canvas with page thumbnails, bookmarks, search and comments in side panels, and the tool grid always one click away. Editing operations — annotations, page manipulation, bookmarks — pass through an undo stack; saving is atomic in place, and encrypted documents that open with a password stay encrypted when saved.
The obvious implementation of reflow is to call the PDF library's text-box routine with the new paragraph. That approach was evaluated and rejected on three measured grounds, each structural rather than a matter of tuning.
One style per call. The high-level insert routine accepts a single font, size, and colour for the whole call. A paragraph reading normal bold normal cannot be expressed by it at all — not imperfectly, but not at all.
Justification is silently inert. The justify flag emits the PDF word-spacing operator, which by specification applies only to single-byte character code 32. Because the library embeds TrueType fonts as composite Type0 with Identity-H encoding, the operator matches nothing. The call succeeds, reports no error, and produces ragged text where justified text was requested — the worst class of failure, because it is invisible to the caller.
Chained writers shatter words. Composing mixed runs by chaining the lower-level text writer causes each run to be wrapped against the first line's remaining width, breaking words mid-token — despit / e / head / winds. This is a consequence of how the chain computes available width, not a parameter that can be corrected.
The conclusion drawn is that mixed-run justified reflow requires measuring and breaking lines directly, and emitting the drawing operators without an intermediary.
Line breaking requires knowing the advance width of every glyph in the exact font the page uses. That information is already present in the PDF's font dictionaries, and the engine reads it from there rather than consulting a system font that merely shares a name. Measuring with the document's own widths means the re-wrapped paragraph occupies the same horizontal space as the original, and the emitted text can reference the page's existing font resources — nothing is re-embedded.
Avoiding re-embedding is not only elegance. Identical font streams added by separate operations are not deduplicated by the library's garbage-collection modes, so an editor that embeds on every edit grows the file on every edit. Referencing the existing resource keeps repeated editing size-neutral.
Appending drawing operators to a page content stream appears simple and is the second trap. The save and restore operators preserve and restore graphics state; they do not reset it. A fragment appended to a page whose stream left state modified inherits that state. Measured on a single test page, four distinct leaks each produced a different wrong result:
| Leaked state | Observed effect |
|---|---|
| Unbalanced save with a transform | Text drawn at the wrong coordinates entirely |
| Open clipping path | Text absent from the rendered page |
| Horizontal scaling left at 50% | Every advance halved; justification arithmetic twice wrong |
| Character spacing left non-zero | Span visibly shattered |
Two defences are applied together because neither subsumes the other. The page's existing content is first wrapped so that unbalanced state cannot escape it; the fragment then explicitly pins rendering mode, character spacing, word spacing, horizontal scale, and text rise to their defaults. Wrapping addresses transforms and clipping paths but is a no-op on a page judged already balanced; pinning addresses every text-state leak but nothing about a transform or a clip. Applying only one leaves a reproducible failure.
The third trap is the coordinate conversion. Subtracting the page height from a y-coordinate is the familiar idiom, and it is correct only for an unrotated page whose crop box equals its media box and begins at the origin. It was measured wrong by 200 points on a page rotated 90 degrees, and entirely off-page on a shifted media box. The engine instead applies the inverse of the page's own transformation matrix, which was verified correct for unrotated pages, 90- and 270-degree rotations, an inset crop box, and a shifted media box. The one combination still unhandled — rotation together with a differing crop box — is excluded by the eligibility gate rather than approximated.
A re-wrapped paragraph rarely occupies its original height. Growing it requires moving everything below it down, and this phase is treated as the most dangerous code in the system, because it is the only part whose failure mode is irreversible mangling rather than a refusal.
Content pushed past the page edge is lost, not clipped. In a measured case, shifting a band by 80 points dropped a footer entirely: extracted text fell from 653 to 613 characters, with no exception raised and no error returned. The implementation therefore measures the ink inside every band itself and raises before writing anything, rather than trusting the shift to fail loudly.
Uncovered ranges vanish identically. The shift blanks the page's content stream and re-stamps it from an in-memory clone, so any vertical range not covered by a re-stamped tile never returns. The routine consequently does not stamp only the caller's bands: it tiles the whole page, filling every gap and both horizontal margins of a column-local band with pass-through tiles at zero displacement.
Rebuilding into a new document destroys interactive objects. Constructing a fresh page and showing the old one into it returned empty link and widget lists — annotations, form fields and the table of contents did not survive. The engine edits the same page object in place instead. The in-place variant was measured lossless to the pixel against an ideally typeset reference at 150 dpi, with byte-identical extracted text.
Annotations are not in the content stream and are therefore unaffected by a band shift; they must be moved separately. Setting an annotation rectangle directly fails silently for precisely the markup types the commenting layer produces, so those are handled by their own path rather than the generic one.
The design treats a refusal as a correct outcome and a plausible-looking corruption as a defect. Before any paragraph is offered as editable, an eligibility gate excludes constructions where re-wrapping cannot be guaranteed: tables, where reflow would break column alignment; contents pages, where leader dots and page numbers are positionally meaningful; rotated pages and multi-column layouts outside the verified coordinate cases; and OCR text layers, where the visible content is an image and the text is an invisible overlay. If growth cannot be accommodated without pushing content off the page, the operation declines rather than proceeding.
The user-facing consequence is that some paragraphs are simply not outlined as editable, with a stated reason. That is a smaller product surface than a competitor advertising unrestricted PDF text editing — and a considerably more trustworthy one, because the operations that are offered are the operations that were verified.
Around the workspace sit forty-three batch tools in six families, all implemented in the headless engine.
| Family | Representative operations |
|---|---|
| Organize | Merge; interleaved merge; split by range, page, bookmark, size or text; extract; reorder; crop; rotate; resize; N-up; flip |
| Edit & Sign | Place or rewrite page text; fill and sign; make existing PDFs fillable; watermark; header and footer; page numbers; Bates numbering continuous across files; bookmarks; metadata |
| Convert | PDF to Word, table-aware Excel, PowerPoint, text, and JPG/PNG/TIFF at any DPI; HTML to PDF; images to PDF; Word to PDF |
| Security | AES-128 protection with granular permissions; unlock; flatten to read-only |
| Compress & Scans | Compression by real image downscaling; auto-deskew; OCR to searchable text; grayscale; repair of damaged files |
| Others | Extract embedded images; rename files from page text |
Two entries deserve comment. Redaction removes content from the file rather than drawing over it, so redacted text is not recoverable by extraction — the failure mode that has repeatedly exposed supposedly redacted documents in practice. Compression downscales embedded images rather than only re-writing object streams, which is where the size actually resides in scanned documents.
The application builds to an arm64-native bundle and disk image for Apple Silicon, and runs from source on a virtual environment for development. Documents are processed locally: there is no upload, no account, and no network round-trip in any operation.
Four limitations are worth stating plainly. The reflow gate excludes real and common constructions, most notably tables and multi-column pages, and the combination of page rotation with a differing crop box remains unhandled by design. Some conversions delegate to platform facilities and inherit their availability. The measured guarantees in Sections 4 and 5 were established against specific library versions, and are properties of this implementation paired with those versions rather than of the format. And the application targets Apple Silicon only; while the engine layer carries no platform-specific code and could support other targets, that has not been done.
The interesting problem in PDF editing is not adding text to a page — it is changing text that is already there without silently damaging the page around it. PdfRomeo addresses it by declining the convenient abstractions: measuring with the document's own embedded metrics rather than a system font, breaking lines itself rather than calling a text box that cannot justify or mix styles, emitting operators with inherited state explicitly pinned, and treating the content-shifting phase as dangerous enough to verify byte-for-byte and pixel-for-pixel. The refusal policy is part of the same argument. An editor that declines a table is less capable than one that claims to handle everything, and considerably more useful than one that mangles a table while reporting success.