How-to
Automating Reporting WorkflowsDeep dive

Fit an Excel Sheet to One Page Wide When Exporting to PDF

Stop wide Excel reports from spilling columns onto extra PDF pages — set fit-to-width with openpyxl, predict how small the text will get before converting, and when it gets too small, use landscape, narrower margins, hidden helper columns or a two-page-wide layout with repeated labels. Every layout measured in a LibreOffice PDF export.

A wide monthly report exported to PDF comes out as nine pages: the first three months on page one, the next months on page four, and nobody can match a number to its row. Fit-to-page fixes the split in three lines of openpyxl, and creates a new problem: the text shrinks until everything fits, possibly to a size nobody can read. This guide, part of Exporting Excel Reports to PDF, sets the sheet to one page wide, predicts the printed text size before converting, and uses the layout changes that keep it readable. Each layout below was exported through LibreOffice and measured.

Printed size of 11-point text, 18-column report on A4 Measured in PDF exports of an 18-column, 120-row sheet. Without scaling the text stays at 11 points but the sheet spreads over 9 pages. Fit to one page wide in portrait shrinks it to 4.4 points. Landscape gives 6.7 points, landscape with narrow margins 7.4, and hiding four helper columns 9.2. Two pages wide with the label column repeated gives 8.6 points in portrait. no scaling (9 pages) 11 pt fit width, portrait 4.4 pt fit width, landscape 6.7 pt + narrow margins 7.4 pt + 4 columns hidden 9.2 pt 2 pages wide, portrait 8.6 pt fitting always works; whether the result is readable depends on the layout

Prerequisites

Bash
pip install openpyxl pypdf

The PDF conversion uses LibreOffice on Linux or macOS, as described in Convert Excel to PDF on Linux with LibreOffice; Excel itself honours the same settings. A wide report to work with — twelve months plus five summary columns:

Python
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.page import PageMargins
from openpyxl.worksheet.properties import PageSetupProperties

def make_report():
    wb = Workbook()
    ws = wb.active
    ws.title = "Sales"
    months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
    ws.append(["Region"] + [f"{m} 2026" for m in months] + ["Total", "Target", "Variance", "Share", "Notes"])
    for r in range(120):
        ws.append([f"Store {r + 1:03d}"] + [1000 + r * 13 + c * 7 for c in range(12)] + [0, 0, 0, 0.1, "ok"])
    for c in range(1, 19):
        ws.column_dimensions[get_column_letter(c)].width = 12
    ws.page_setup.paperSize = ws.PAPERSIZE_A4
    return wb

wb = make_report()
ws = wb.active

Step 1 — Fit to one page wide, any number of pages down

Three settings work together. fitToWidth = 1 asks for one page across; fitToHeight = 0 means no limit downwards, so a long report still flows onto as many pages as it needs; and fitToPage = True switches the sheet from percentage scaling to fit mode — without it the other two are ignored:

Python
ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)
ws.page_setup.fitToWidth = 1
ws.page_setup.fitToHeight = 0
ws.print_title_rows = "1:1"                         # repeat the header on every page
wb.save("sales_fit.xlsx")

Leaving fitToHeight at its default of 1 squeezes the entire sheet onto a single page, which for 120 rows makes the text microscopic. Repeating row 1 keeps the month headings above the numbers on every page, since only the first page would otherwise show them. The other page-setup options — print areas, paper size, orientation — are covered in Set Print Area and Page Setup with openpyxl.

Exported to PDF, the unscaled sheet took 9 pages; the fitted one took 2. But the heading text measured 4.4 points: the sheet was scaled to about 40% to get 18 columns across a portrait A4 page.

Step 2 — Predict the scale before converting

The scale is simple arithmetic: printable width divided by the total width of the printed columns. Column widths are in characters of the default font; for 11-point Calibri one character is 7 pixels plus 5 pixels of padding per column, and a pixel is 0.75 points:

Python
PAPER_WIDTH_PT = {9: (595.3, 841.9), 1: (612.0, 792.0)}   # A4, Letter (portrait w, h)

def column_points(ws, letter, default=8.43):
    dim = ws.column_dimensions[letter] if letter in ws.column_dimensions else None
    if dim is not None and dim.hidden:
        return 0.0
    width = dim.width if dim is not None and dim.width else default
    return (int(width * 7) + 5) * 0.75

def estimated_scale(ws, pages_wide=1, repeat_cols=0):
    paper_w, paper_h = PAPER_WIDTH_PT[ws.page_setup.paperSize or 9]
    if ws.page_setup.orientation == "landscape":
        paper_w = paper_h
    printable = paper_w - (ws.page_margins.left + ws.page_margins.right) * 72
    letters = [get_column_letter(c) for c in range(1, ws.max_column + 1)]
    total = sum(column_points(ws, l) for l in letters)
    total += (pages_wide - 1) * sum(column_points(ws, l) for l in letters[:repeat_cols])
    return min(1.0, printable * pages_wide / total)

scale = estimated_scale(ws)
print(f"scale {scale:.0%}, 11 pt text prints at {11 * scale:.1f} pt")
# scale 41%, 11 pt text prints at 4.5 pt

The estimate of 4.5 points matched the measured 4.4 in the PDF. For every layout below it came within half a point, and always on the high side, so set the threshold with a small margin — treat an estimate of 7.5 points as the floor if 7 is the real limit. That makes it a cheap gate: run it while building the workbook and change the layout automatically instead of discovering unreadable PDFs later.

Where the printed width goes The paper width minus the left and right margins gives the printable width. The columns' total width is compared with it; if the columns are wider, the whole sheet is scaled down by the ratio. Reducing the columns' width or increasing the printable width both raise the scale. Paper width A4 portrait 595 pt, landscape 842 pt − left and right margins 0.75 in each by default = 108 pt = printable width 487 pt portrait ÷ visible column width 18 × 12 chars ≈ 1,202 pt

Step 3 — Buy width: landscape, then margins

Landscape is the first and biggest gain, turning 487 points of printable width into 734. Narrower margins add more; a quarter inch on each side is safe for office printers and irrelevant for PDFs read on screen:

Python
ws.page_setup.orientation = "landscape"
print(f"landscape: {11 * estimated_scale(ws):.1f} pt")
ws.page_margins = PageMargins(left=0.25, right=0.25, top=0.5, bottom=0.5)
print(f"narrow margins: {11 * estimated_scale(ws):.1f} pt")   # 6.7, then 7.4
wb.save("sales_landscape.xlsx")

Measured, those were 6.7 and 7.4 points — better, still small. Past this point the paper has no more width to give; the remaining gains come from the columns.

Step 4 — Print fewer columns

Hidden columns are not printed. Working columns that help build the report — targets, shares, notes — can stay in the workbook for anyone who opens it and still drop out of the PDF:

Python
for letter in "OPQR":                               # Target, Variance, Share, Notes
    ws.column_dimensions[letter].hidden = True
print(f"helpers hidden: {11 * estimated_scale(ws):.1f} pt")   # 9.5 estimated, 9.2 measured
wb.save("sales_hidden.xlsx")

With four columns hidden the measured size was 9.2 points: comfortably readable, still one page wide. When hiding is not appropriate, narrow the columns instead — numbers in a #,##0 format rarely need twelve characters — or set a print area that covers only the range the PDF should show.

Step 5 — When every column must stay, go two pages wide

Some reports cannot lose a column. Then print them two pages wide on purpose, and repeat the label column so each page stands alone:

Python
wb = make_report()
ws = wb.active
ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)
ws.page_setup.fitToWidth = 2
ws.page_setup.fitToHeight = 0
ws.page_setup.pageOrder = "overThenDown"            # left half, right half, next rows
ws.print_title_rows = "1:1"
ws.print_title_cols = "A:A"                         # store names on every page
ws.page_margins = PageMargins(left=0.5, right=0.5, top=0.5, bottom=0.5)
print(f"two pages wide: {11 * estimated_scale(ws, pages_wide=2, repeat_cols=1):.1f} pt")   # 9.1 estimated, 8.6 measured
wb.save("sales_two_wide.xlsx")

The export measured 8.6 points in portrait, on 4 pages, and the right-hand pages opened with the "Region" heading and the store names. overThenDown puts the two halves of the same rows next to each other in the PDF; the default, downThenOver, prints all the left halves first, which is awkward to read on screen.

Choosing a layout for a wide sheet Estimate the scale first. If it is 65% or more, fit to one page wide as is. If it is lower, switch to landscape and narrower margins. If that is still too small and some columns are helpers, hide them. If every column must stay, print two pages wide with the label column repeated. Estimated scale at one page wide? 65% or more fit as is fitToWidth = 1 below 65% landscape and margins +50% width still too small hide helper columns not printed every column needed two pages wide repeat column A

Step 6 — Check the PDF, not the settings

Settings can be right and the export still surprising: a column width set later in the code, a print area left over from a template. Convert, then check the page count and the size of the heading text with pypdf:

Python
import subprocess
from pypdf import PdfReader

subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "sales_hidden.xlsx"],
               check=True, timeout=120)

def heading_size(page, heading="Region"):
    sizes = []
    def visit(text, cm, tm, font, size):
        if text.strip() == heading:
            sizes.append(size * abs(tm[0]))
    page.extract_text(visitor_text=visit)
    return round(sizes[0], 1) if sizes else None

pdf = PdfReader("sales_hidden.pdf")
print(len(pdf.pages), "pages, heading at", heading_size(pdf.pages[0]), "pt")

pypdf reports the font size and the text matrix separately, and the matrix carries the scaling; their product is the printed size. Failing the job when the size drops below a threshold turns "the PDF is unreadable" from a complaint into a test.

Common pitfalls

  • fitToPage missing. fitToWidth without pageSetUpPr.fitToPage = True changes nothing. It is the most common reason the setting "does not work".
  • fitToHeight left at 1. The default fits the whole sheet on one page. Set it to 0 for long reports.
  • Unset column widths. Columns without an explicit width use the default of about 8.43 characters, but openpyxl reports 13 for a column it has not seen. The estimate above checks in ws.column_dimensions to avoid creating one.
  • Assuming the default font. The estimate assumes 11-point Calibri. A report in a wider font, or with larger header text, prints smaller than predicted; check the PDF.

Conclusion

fitToWidth = 1, fitToHeight = 0 and fitToPage = True put a wide sheet on one page across. Estimate the resulting scale from the column widths before converting; when text would print below about 7 points, use landscape and narrower margins, hide helper columns, or print two pages wide with the label column repeated, and confirm the result by measuring the PDF.

Frequently asked questions

How do I make an Excel sheet print one page wide from Python? With openpyxl, set ws.page_setup.fitToWidth = 1, ws.page_setup.fitToHeight = 0 and ws.sheet_properties.pageSetUpPr.fitToPage = True. The zero height means as many pages down as needed. Without fitToPage the two numbers are ignored.

Why is the text tiny after fitting the sheet to one page? Fitting scales the whole sheet down until every column fits. An 18-column report on portrait A4 printed at 40%, turning 11-point text into 4.4 points. Switch to landscape, narrow the margins or remove columns before fitting.

How small can the scale go before a PDF report is hard to read? Body text below about 7 points is hard to read on paper and needs zooming on screen. For 11-point cells that means a scale of roughly 65% or more; below that, change the layout instead of fitting harder.

Do hidden columns appear in the PDF? No. Hidden columns are not printed, so hiding helper or working columns is a quick way to make a sheet narrower for export without deleting data from the workbook.

More on pdf export

More on pdf export →