Guide

Merging a Letter invoice with an A4 attachment in pypdf, and the page size nobody normalizes

pypdf's writer.append() concatenates PDFs page by page and copies each page's own /MediaBox along with it. Merge a US Letter invoice (612x792pt) with an A4 attachment (595x842pt) and the output has two page sizes, not one. Most viewers render that fine, each page at its own size. A print queue set to "same size as first page," a thumbnail generator, or a batch rasterizer that assumes one page size for the whole document can scale or crop the second page as if it were the first.

What /MediaBox actually is

/MediaBox is a rectangle stored per page inside a PDF's page tree, not once for the whole document. A PDF has no page-size property to read off the file as a whole; every page carries its own, and two pages in the same file are free to disagree. pypdf mirrors that structure. page.mediabox reads the page's own rectangle, and nothing in the library reconciles it against any other page already sitting in the writer.

Reproducing the mismatch

Build two single-page PDFs at different sizes and merge them with append(). Reading page.mediabox back off each page in the result shows exactly what went in: page 0 at 612x792, page 1 at 595.32x841.92. Nothing warns about the mismatch and nothing normalizes it, because normalizing was never part of the job append() does.

from pypdf import PdfReader, PdfWriter

invoice = PdfWriter()
invoice.add_blank_page(width=612, height=792)       # US Letter
invoice.write("invoice.pdf")

attachment = PdfWriter()
attachment.add_blank_page(width=595.32, height=841.92)  # A4
attachment.write("attachment.pdf")

merged = PdfWriter()
for path in ("invoice.pdf", "attachment.pdf"):
    merged.append(PdfReader(path))
merged.write("merged.pdf")

check = PdfReader("merged.pdf")
for i, page in enumerate(check.pages):
    print(i, page.mediabox.width, page.mediabox.height)
# 0 612.0 792.0
# 1 595.32 841.92

Why a viewer hides it and a rasterizer doesn't

A human scrolling through the merged file in Preview or Acrobat will not notice anything wrong. Each page renders at its own size, the way a PDF is supposed to. The trouble shows up one step downstream, in anything built around the assumption that a document has a single page size: a print dialog's "fit to same size as first page" option, a thumbnail grid sized off page one, a rasterizer that renders every page at one fixed DPI-and-dimension pair for a PNG export. Those tools scale or crop the second page to match the first, and an A4 attachment merged after a Letter invoice comes out stretched or clipped in exactly the places that matter.

Normalizing to one page size before merging

The fix is to normalize page size before merging, not after. Create a blank page at the target size, then merge the mismatched page's content onto it through a scale-and-center transform. pypdf's Transformation and PageObject.merge_transformed_page do the actual work; the function below just computes the scale factor that fits the source page inside the target rectangle without distorting its aspect ratio, and the translation that centers it.

from pypdf import PageObject, PdfReader, PdfWriter, Transformation

LETTER = (612, 792)

def fit_to_letter(page):
    src_w = float(page.mediabox.width)
    src_h = float(page.mediabox.height)
    scale = min(LETTER[0] / src_w, LETTER[1] / src_h)
    tx = (LETTER[0] - src_w * scale) / 2
    ty = (LETTER[1] - src_h * scale) / 2

    blank = PageObject.create_blank_page(width=LETTER[0], height=LETTER[1])
    transform = Transformation().scale(scale, scale).translate(tx, ty)
    blank.merge_transformed_page(page, transform)
    return blank

reader = PdfReader("attachment.pdf")
fixed_attachment = fit_to_letter(reader.pages[0])

writer = PdfWriter()
writer.add_page(PdfReader("invoice.pdf").pages[0])
writer.add_page(fixed_attachment)
writer.write("merged.pdf")

Run fit_to_letter on the A4 page before appending it and both pages in the merged output report 612x792. The attachment's content is scaled down slightly and letterboxed rather than stretched to fill a rectangle it was never drawn for, which is the difference between "smaller but legible" and "the wrong aspect ratio."

Try it

/api/merge concatenates pages the same way pypdf's append() does: each input page keeps whatever size it already had, and the endpoint does not resize anything for you. That is the correct default for most callers, since resizing without being asked would silently alter content nobody meant to touch. If you need one uniform page size in the output, normalize each page first with the transform above, then send the normalized files to /api/merge. A free key from /docs/signup covers 250 calls a month, and /api/inspect reads back page dimensions along with field names if you want to check sizes before merging rather than after.