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.