DevelopmentMarch 17, 20254 min read

PDF Merger using Python and Streamlit

Merge multiple PDFs into one with Python and Streamlit — drag, reorder, and combine documents with a live page count and download.

Galvan

Galvan

Founder & Creator

Introduction

Everyone has merged PDFs with sketchy free websites that upload your documents to who-knows-where. This tutorial builds the same tool locally: upload several PDFs, see their page counts, reorder them, and download one combined file — with nothing ever leaving your machine. It uses pypdf, the standard pure-Python PDF library, and pairs nicely with the file organizer for keeping your documents tidy afterwards.

The core API is tiny — a PdfWriter that accepts pages from PdfReader objects — so the interesting engineering is in the UX: multi-upload ordering and streaming a proper download.

Features

  • Multi-upload — select any number of PDFs at once.
  • Page counts — per-file page totals shown before merging.
  • Reorder files — move files up/down before combining.
  • One-click merge — single writer appends every file in order.
  • Instant download — merged PDF served via st.download_button.
  • Prerequisites

  • Python 3.8+ — from python.org.
  • Dependencies — install with pip:
  • code
    pip install streamlit pypdf

    Step 1: Create the Script

    Save as pdf_merger.py:

    code
    import streamlit as st
    from pypdf import PdfReader, PdfWriter
    from io import BytesIO
    
    st.set_page_config(page_title="PDF Merger", page_icon="📄")
    st.title("📄 PDF Merger")
    
    uploads = st.file_uploader(
        "Upload PDFs (in any order — reorder below)",
        type=["pdf"],
        accept_multiple_files=True,
    )
    
    if uploads:
        # Show page counts and allow reordering
        st.subheader("Files & order")
        for i, f in enumerate(uploads):
            pages = len(PdfReader(BytesIO(f.getvalue())).pages)
            c1, c2, c3, c4 = st.columns([0.6, 0.13, 0.13, 0.14])
            c1.markdown(f"**{i + 1}. {f.name}** — {pages} pages")
            if c2.button("↑", key=f"up{i}", disabled=i == 0):
                uploads[i - 1], uploads[i] = uploads[i], uploads[i - 1]
                st.rerun()
            if c3.button("↓", key=f"down{i}", disabled=i == len(uploads) - 1):
                uploads[i + 1], uploads[i] = uploads[i], uploads[i + 1]
                st.rerun()
            c4.button("✕", key=f"del{i}")
    
        if st.button("🔗 Merge PDFs", type="primary"):
            writer = PdfWriter()
            total = 0
            for f in uploads:
                reader = PdfReader(BytesIO(f.getvalue()))
                for page in reader.pages:
                    writer.add_page(page)
                    total += 1
    
            buffer = BytesIO()
            writer.write(buffer)
            st.success(f"Merged {len(uploads)} files ({total} pages)")
            st.download_button(
                "⬇️ Download merged PDF",
                data=buffer.getvalue(),
                file_name="merged.pdf",
                mime="application/pdf",
            )

    Step 2: Run the App

    code
    streamlit run pdf_merger.py

    Upload three PDFs, reorder one, merge, and open the result — pages appear exactly in your chosen order.

    How It Works

    pypdf treats a PDF as a container of page objects. PdfReader parses a file and exposes .pages; PdfWriter collects pages from any number of readers and writes them into a fresh PDF. Because merging happens page-by-page rather than file-by-file, the same loop is where you would later insert page ranges, rotations, or watermarks.

    The BytesIO buffer appears twice, doing two jobs: wrapping each upload's bytes so pypdf can read them (it expects a file-like object, not raw bytes), and holding the output so st.download_button can serve it. This in-memory pattern — no temp files anywhere — is the same one used in the QR generator.

    Reordering is plain Python: swap adjacent items in the upload list and call st.rerun() so the UI reflects the new order. Note the key on every button — without unique keys Streamlit cannot tell the ten reorder buttons apart.

    Common Errors & Fixes

  • `PdfReadError: file has not been decrypted` — the PDF is password-protected. Detect with reader.is_encrypted and skip with a warning (or call reader.decrypt("") for owner-password-only files).
  • Merged PDF is corrupt/empty — you wrote the writer to a buffer but downloaded the wrong object; pass buffer.getvalue(), not buffer.
  • `StreamlitAPIException` on rerun after reorder — buttons recreated in a new order need stable keys; keep keys tied to the file, not the index.
  • Scanned PDFs balloon in size — pages are images; that is expected. Run the output through a compress step (pypdf can't recompress images — use Ghostscript if size matters).
  • Key Concepts

  • Reader/Writer split — parse and collect pages independently of source files.
  • BytesIO as file-like — bridges in-memory bytes to libraries expecting files.
  • `st.download_button` — streams generated bytes straight to the user.
  • List reordering UI — swap + rerun is all a reorder control needs.
  • What to Try Next

  • Add a page-range selector per file (merge only pages 2–5) using reader page indices.
  • Add a splitter tab — one PDF in, a ZIP of single-page PDFs out.
  • Rotate pages before merging with page.rotate(90).
  • Batch-rename the output with a date prefix — the file organizer logic plugs in directly.
  • FAQ

    Do my files get uploaded anywhere?

    No — everything happens in the browser session and your Python process's memory. That is the entire point of running this locally instead of using an online merger.

    Internal links and forms mostly survive; document-level outlines (bookmarks) do not merge automatically. pypdf's append() (instead of per-page add_page) preserves more structure when you don't need per-page control.

    How large can the files be?

    Streamlit caps uploads (default 200 MB, configurable via server.maxUploadSize). Memory is the real limit — each PDF is held in RAM during the merge.