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 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_p