DEV Community

Cover image for What Nobody Tells You About Building "Simple" PDF Tools
Talha Ramzan
Talha Ramzan

Posted on

What Nobody Tells You About Building "Simple" PDF Tools

PDF merge, split, and compress sound like the most boring possible features to build. Take some files, do an operation, return a file. I believed that too, until real user files started hitting the backend and every one of these tools broke in a different, specific way.

Here's what actually went wrong, and what fixed it.

The PDF that wasn't actually a PDF

The first crash report was a "corrupted file" error on a PDF that opened fine in every desktop viewer. Turns out plenty of real-world PDFs are technically malformed, a missing xref table, a truncated stream, an object reference pointing at nothing but viewers like Chrome and Acrobat are extremely forgiving about it. Most Python PDF libraries are not.

try:
    reader = PdfReader(file_path, strict=False)
except PdfReadError:
    # strict=False alone doesn't save you from everything —
    # some files need the xref table rebuilt from scratch
    reader = PdfReader(file_path, strict=False)
    reader._override_encryption = True
Enter fullscreen mode Exit fullscreen mode

strict=False fixed maybe 70% of the "corrupted" reports. The rest needed a repair pass first — scanning the raw byte stream for object markers and reconstructing a valid cross-reference table before the normal parser ever touches it. Painful to write, but it turned "please fix your PDF" into "it just works," which matters a lot when the whole pitch of the tool is "no signup, just upload and go."

Merging PDFs is not free, memory-wise

The naive merge implementation loads every input PDF fully into memory, concatenates pages, writes the output. Fine for two 200KB files. Not fine when someone merges fifteen scanned documents at 40MB each, because now you're holding the equivalent of 600MB of parsed PDF objects in memory at once on a backend container that doesn't have unlimited RAM.

The fix was switching to incremental writes process one input file at a time, write its pages to the output stream, then explicitly drop the reference before moving to the next file:

writer = PdfWriter()
for path in input_paths:
    reader = PdfReader(path, strict=False)
    for page in reader.pages:
        writer.add_page(page)
    del reader  # don't let the parsed object tree accumulate
    gc.collect()
Enter fullscreen mode Exit fullscreen mode

The gc.collect() call looks paranoid, and normally I'd agree it's unnecessary, but with large parsed PDF object trees holding circular references (pages referencing parent objects referencing the page tree back), Python's reference counting alone doesn't reliably free them fast enough between iterations under real memory pressure.

Decompression bombs are a real concern for PDF compression

A PDF compression tool that accepts arbitrary uploads and re-encodes images inside them has to worry about the same class of attack as a zip bomb: a small, legitimately-sized PDF that contains an image with an absurd decompressed resolution. A 500KB PDF containing a "photo" that decompresses to 40,000 × 40,000 pixels will happily consume gigabytes of memory the moment you try to re-encode it.

MAX_PIXELS = 50_000_000  # ~50 megapixels, generous for real photos

if image.width * image.height > MAX_PIXELS:
    raise ValueError("Image dimensions exceed safe processing limit")
Enter fullscreen mode Exit fullscreen mode

This check has to happen before decompression, based on header metadata alone, checking after you've already decoded the full image into memory defeats the entire point.

PDF-to-Word is where font and layout fidelity actually breaks

Converting a text-heavy PDF to editable Word is mostly solved. Converting a PDF with multi-column layouts, embedded custom fonts, and tables is a different problem entirely. The PDF format doesn't really have a concept of "this text belongs to this table cell" it just has text positioned at specific x/y coordinates on a page. Reconstructing table structure means inferring it from whether text blocks are roughly aligned in a grid, which is a heuristic, not a guarantee.

The honest fix here wasn't a clever algorithm — it was setting expectations correctly. Simple, single-column PDFs convert close to perfectly. Complex multi-column academic papers or financial statements with dense tables convert "usably editable, expect to fix formatting," and the tool now says exactly that instead of silently producing a mangled table and letting the user discover it themselves.

Cleanup matters more than it sounds like it should

Backend hosting on a platform like Railway means the filesystem is ephemeral, but "ephemeral" doesn't mean "instantly cleaned between requests" temp files from a crashed or abandoned conversion can sit around until the container itself restarts. Under real traffic, that adds up to disk pressure from files nobody's coming back for.

try:
    result = process_pdf(temp_input_path)
    return result
finally:
    if os.path.exists(temp_input_path):
        os.remove(temp_input_path)
Enter fullscreen mode Exit fullscreen mode

The finally block matters more than it looks like it should an exception partway through processing shouldn't leave an orphaned file, and it's an easy thing to skip when you're focused on the happy path.

What I'd tell someone building similar tools

  • Real-world files are messier than test files. Malformed PDFs are common enough that a repair/recovery path isn't optional if you're accepting arbitrary uploads.
  • Any tool that decodes user-supplied compressed data needs a size check before decompression, not after the same principle as zip bombs applies directly to embedded images.
  • Memory accumulation across a loop is easy to miss when each individual file seems small it's the cumulative footprint across an entire batch operation that actually causes problems in production.
  • Being honest about fidelity limits (this converts cleanly, this one needs manual cleanup) builds more trust than silently producing a degraded result and letting the user find out.

The tools are live and free if you want to see them in action: PDF merger, splitter, and compressor are all part of DukoTools no signup required.

Happy to go deeper on any part of this in the comments, especially the PDF repair/recovery logic if anyone's dealt with similarly malformed real-world files.

Top comments (0)