Fix Images Too Large in python-docx

Calling doc.add_picture("logo.png") without a width or height argument inserts the image at its native physical size derived from the file's DPI metadata. A 2000 x 1500 px image saved at 72 DPI renders as a 27 x 20 inch shape — it overflows the page margins, pushes all subsequent content down, and frequently causes Word to flag the document as corrupt on open.

This is the single most common failure when inserting images into Word documents, and it has one root cause and one reliable fix: never call add_picture without an explicit size.

Root Cause

python-docx converts pixel dimensions to EMU (English Metric Units) using the image's embedded DPI:

width_emu = pixel_width / dpi * 914400

A 2000 px wide image at 72 DPI produces 2000 / 72 * 914400 = 25,400,000 EMU = 27.8 inches. A standard US Letter page is only 8.5 inches wide, so the image blows past both margins.

The same logic applies in reverse for high-DPI images: a 600 x 450 px image at 300 DPI renders at 2 x 1.5 inches, which may be smaller than expected.

Minimal Reproducible Diagnostic

Run this with Pillow to confirm your image's native physical size before inserting it:

# pip install Pillow
from PIL import Image
from pathlib import Path

img_path = Path("assets/logo.png")
try:
    with Image.open(img_path) as img:
        w, h = img.size
        dpi = img.info.get("dpi", (72, 72))
        print(f"Pixels: {w} x {h}")
        print(f"DPI: {dpi[0]} x {dpi[1]}")
        print(f"Native size: {w/dpi[0]:.2f} x {h/dpi[1]:.2f} inches")
except FileNotFoundError:
    print(f"File not found: {img_path}")
except Exception as e:
    print(f"Could not read image: {e}")

If "Native size" is larger than your usable page area (typically 6–7 inches for a standard letter/A4 page with 1-inch margins), the image will overflow without an explicit size argument.

Fix: Always Pass an Explicit Width

The primary fix is to always supply width (or height) to add_picture. python-docx preserves the aspect ratio when only one dimension is given.

# pip install python-docx
from pathlib import Path
from docx import Document
from docx.shared import Inches

IMAGE = Path("assets/logo.png")
OUTPUT = Path("output/fixed_image.docx")

doc = Document()
doc.add_heading("Fixed Report", level=1)

try:
    # Pass explicit width — aspect ratio is preserved automatically
    doc.add_picture(str(IMAGE), width=Inches(3.0))  # never omit this argument
    doc.save(str(OUTPUT))
    print(f"Saved: {OUTPUT}")
except FileNotFoundError:
    print(f"Image not found: {IMAGE}")
except Exception as e:
    print(f"Insertion failed: {e}")

Use Cm(n) instead of Inches(n) if your document uses metric measurements. Both are valid arguments; they are just different units for the same EMU value.

Fix: Compute Width to Fit the Usable Page Area

Hard-coding Inches(3.0) works for a single document but breaks when the template uses non-standard margins. Derive the usable width from the section geometry — the same section object you configure when automating Word document creation:

# pip install python-docx
from pathlib import Path
from docx import Document

IMAGE = Path("assets/logo.png")
OUTPUT = Path("output/auto_width_image.docx")

doc = Document()
section = doc.sections[0]

# Compute usable width in EMU — works for any margin configuration
usable_width = (
    section.page_width        # total page width in EMU
    - section.left_margin     # subtract left margin
    - section.right_margin    # subtract right margin
)

doc.add_heading("Auto-Fitted Image", level=1)
try:
    doc.add_picture(str(IMAGE), width=usable_width)  # fills the text column exactly
    doc.save(str(OUTPUT))
    print(f"Usable width: {usable_width / 914400:.2f} in — saved {OUTPUT}")
except FileNotFoundError:
    print(f"Image not found: {IMAGE}")
except Exception as e:
    print(f"Error: {e}")

All three values (page_width, left_margin, right_margin) are already in EMU, so arithmetic is exact. Pass the result directly to width= — no unit conversion needed.

Variant: High-DPI Images Come Out Too Small

The opposite problem occurs with 300 DPI assets: 1200 / 300 * 914400 = 3,657,600 EMU = 4 inches. That is a reasonable size but it may be too small for a full-width chart or too large for a logo corner. The fix is identical — always pass an explicit width.

For a helper that reads DPI and returns a safe EMU width:

# pip install python-docx Pillow
from pathlib import Path
from docx.shared import Inches
from PIL import Image

def safe_image_width(img_path: Path, max_inches: float = 4.0) -> int:
    """Return an EMU width that never exceeds max_inches, preserving aspect ratio."""
    try:
        with Image.open(img_path) as img:
            w, h = img.size
            dpi = img.info.get("dpi", (96, 96))
            native_inches = w / dpi[0]
            # Clamp to max_inches
            target_inches = min(native_inches, max_inches)
            return int(target_inches * 914400)  # convert to EMU
    except Exception:
        return int(Inches(max_inches))  # fallback to max

# Usage:
# doc.add_picture(str(img_path), width=safe_image_width(img_path, max_inches=3.0))

Variant: Oversized Image Inside a Table Cell

Inside a table cell, the image must fit within the cell width. Cell width is accessible via the XML, but the simplest reliable approach is to compute a target width based on the number of columns and the usable page width:

# pip install python-docx
from pathlib import Path
from docx import Document
from docx.shared import Inches

LOGO = Path("assets/logo.png")
OUTPUT = Path("output/table_constrained.docx")

doc = Document()
section = doc.sections[0]
usable = section.page_width - section.left_margin - section.right_margin

num_cols = 3
# Leave a small gutter; divide evenly across columns
cell_image_width = int((usable / num_cols) * 0.85)  # 85% of the cell share

table = doc.add_table(rows=1, cols=num_cols)
for i in range(num_cols):
    cell = table.cell(0, i)
    para = cell.paragraphs[0]
    run = para.add_run()
    try:
        run.add_picture(str(LOGO), width=cell_image_width)  # constrained to cell share
    except FileNotFoundError:
        cell.text = "[logo missing]"

try:
    doc.save(str(OUTPUT))
    print(f"Saved: {OUTPUT}")
except Exception as e:
    print(f"Save failed: {e}")

The 0.85 factor gives breathing room for cell padding. Adjust to taste; the important constraint is that cell_image_width never exceeds usable / num_cols.

Variant: Image with No DPI Metadata

Some images — especially those generated programmatically, captured with screen-recording tools, or exported from vector software — carry no DPI tag at all. img.info.get("dpi", ...) returns the fallback tuple. python-docx falls back to 72 DPI internally, which is why screen captures almost always overflow.

You have two options:

Option A — assume a sane DPI and scale accordingly. If you know the image was a screen capture at 96 DPI, pass that assumption explicitly:

# pip install python-docx Pillow
from pathlib import Path
from docx import Document
from docx.shared import Inches
from PIL import Image

ASSUMED_DPI = 96  # override for images with no DPI metadata
MAX_WIDTH_INCHES = 5.0

img_path = Path("assets/screenshot.png")
OUTPUT = Path("output/screenshot_report.docx")

doc = Document()
try:
    with Image.open(img_path) as img:
        w, _ = img.size
        native_inches = w / ASSUMED_DPI
        target_width = min(native_inches, MAX_WIDTH_INCHES)
        target_emu = int(target_width * 914400)

    doc.add_picture(str(img_path), width=target_emu)  # use computed EMU width
    doc.save(str(OUTPUT))
    print(f"Saved: {OUTPUT}")
except FileNotFoundError:
    print(f"Image not found: {img_path}")
except Exception as e:
    print(f"Error: {e}")

Option B — ignore pixel count entirely and always pass a fixed display size. This is the safest approach for the batch pipelines behind dynamic mail merge with Python, where per-recipient logos and signatures come from varied sources and you want a consistent thumbnail size regardless of file characteristics:

# pip install python-docx
from pathlib import Path
from docx import Document
from docx.shared import Inches

# Fixed display width regardless of source DPI or pixel dimensions
THUMBNAIL_WIDTH = Inches(2.5)

img_path = Path("assets/screenshot.png")
OUTPUT = Path("output/thumbnail_report.docx")

doc = Document()
try:
    doc.add_picture(str(img_path), width=THUMBNAIL_WIDTH)  # fixed size, DPI irrelevant
    doc.save(str(OUTPUT))
    print(f"Saved: {OUTPUT}")
except FileNotFoundError:
    print(f"Image not found: {img_path}")
except Exception as e:
    print(f"Error: {e}")

Option B is idiomatic for logo slots, watermarks, and icon-sized images where the display size is a design constraint rather than a function of the source file.

Understanding the EMU Scale

The diagram below shows the relationship between pixel count, DPI, and the resulting physical size in inches, and how passing an explicit width short-circuits the DPI dependency entirely.

EMU size calculation path A flow starting from an image file that carries pixel dimensions and a DPI tag, splitting into two paths. The upper, dashed path is taken when no width argument is passed: pixel count divided by DPI times 914400 yields a native EMU size that may overflow the page, for example 27 inches at 72 DPI. The lower, solid path is taken when an explicit width such as Inches(n) is passed: DPI is ignored and the image is placed at the correct size that fits the page. Image file pixels + DPI tag no width argument px ÷ DPI × 914400 native EMU size May overflow 27 in at 72 DPI width= Inches(n) explicit EMU DPI ignored Correct size fits the page

Troubleshooting Table

SymptomLikely causeFix
Image pushes content off pageNo width argument; native size > page widthAlways pass width=Inches(n) or computed usable_width
Image is a tiny thumbnailHigh-DPI source (300 DPI); native size too smallPass larger explicit width; use safe_image_width() helper
Image correct in body, huge in headerHeader paragraph uses a different context; no width passed to header runCall header_run.add_picture(str(img), width=Inches(n))
Word opens file with "repaired content" warningImage XML is malformed; often from a BytesIO with wrong seek positionCall buf.seek(0) before each add_picture(buf, ...) call
Image in table cell overflows the cell borderCell width not accounted for; passed page-wide usable_width to cellDivide usable_width by column count and apply an 85% gutter factor
Pass one dimension, not two Passing only a width lets python-docx scale the height proportionally, preserving the aspect ratio. Passing both width and height forces the image into that box and distorts it unless the numbers happen to match the source ratio. Passing neither inserts the image at its native pixel size, which usually overflows the page. What add_picture does with each argument set width only height scales — ratio kept width and height forced into the box — distorted neither native pixel size — overflows

Verification

Confirm the inserted image is within page bounds by reading back the saved document:

# pip install python-docx
from pathlib import Path
from docx import Document

OUTPUT = Path("output/fixed_image.docx")
MAX_INCHES = 8.5  # letter page width; adjust for A4 (8.27 in)

try:
    doc = Document(str(OUTPUT))
    section = doc.sections[0]
    page_inches = section.page_width / 914400

    for i, shape in enumerate(doc.inline_shapes):
        w_in = shape.width / 914400
        h_in = shape.height / 914400
        status = "OK" if w_in <= page_inches else "OVERFLOW"
        print(f"Shape {i}: {w_in:.2f} x {h_in:.2f} in — {status}")

    print(f"Page width: {page_inches:.2f} in")
except FileNotFoundError:
    print(f"File not found: {OUTPUT}")
except Exception as e:
    print(f"Verification error: {e}")

All shapes should print OK. Any shape where width exceeds the page width will show OVERFLOW — trace it back to the add_picture call and add an explicit width argument. Run this check before the document goes downstream: an oversized inline shape is baked into the layout and carries straight through when you are converting DOCX to PDF with Python, where it is far harder to spot and fix.

The width an image actually has to fit into An A4 page is twenty one centimetres wide. With the default two and a half centimetre margins on each side, the usable content width is sixteen centimetres. Any image wider than that overflows regardless of what the source resolution is, so sixteen centimetres is the number to compute against rather than the page width. A4 width against usable content width 21.0 cm page 16.0 cm usable — the ceiling for any image 2.5 cm 2.5 cm Compute against the content width, not the page width

Part of Inserting Images into Word Documents.