How to Export a Map at Print Quality: DPI, Vector and Fonts

Problem statement

The map is finished. Now it has to leave Python, and the export decision has three parts that are usually made by accident:

  • Raster or vector. A PNG at the wrong DPI is either blurry or enormous; a PDF keeps text as text and scales perfectly.
  • DPI. 72 for screen, 300 for print, 600 for fine linework โ€” and the file size roughly follows the square.
  • Fonts. Matplotlib's default PDF font type is refused or substituted by some publishing workflows, and the SVG default converts every glyph to outlines.

Measured on one map โ€” 51 polygons, 53,352 vertices, an 8 ร— 5 inch figure:

format          size      text        scales?
PNG   72 dpi     22 kB    pixels      no
PNG  150 dpi     57 kB    pixels      no
PNG  300 dpi    128 kB    pixels      no
PNG  600 dpi    287 kB    pixels      no
PDF vector      457 kB    selectable  yes
SVG vector    1,309 kB    selectable  yes

Quick answer

Vector for maps with text and linework, raster only for imagery โ€” and set the font types explicitly:

import matplotlib.pyplot as plt

plt.rcParams.update({
    "pdf.fonttype": 42,       # TrueType subset, not Type 3
    "ps.fonttype": 42,
    "svg.fonttype": "none",   # keep <text> as text
})

fig.savefig("map.pdf")                       # print: vector, any size
fig.savefig("map.png", dpi=300)              # print: raster, if imagery
fig.savefig("map.svg")                       # editable, text preserved

Three lines of rcParams remove the two most common publisher complaints before they happen.

Decision diagram routing map content to PDF, PNG, a mixed export or SVG.
The exception is genuine imagery, which has no vector description to preserve.

Step-by-step solution

1. Decide raster or vector from what the map contains

Map content Format Why
Vector layers, labels, legend PDF text stays text; sharp at any size
Satellite imagery, hillshade, big rasters PNG or TIFF at 300 dpi vector wrapping a raster gains nothing
Mixed โ€” vector over a hillshade PDF, with the raster at explicit DPI best of both
Going into a design tool SVG with svg.fonttype="none" editable

The measurement above shows why the default should be vector: the 457 kB PDF is smaller than a 600 dpi PNG of the same map and reproduces at any size.

2. Choose the DPI from the output, not from habit

DPI only matters for raster output. The number depends on the destination:

  • 72โ€“96 โ€” screen only. A draft.
  • 150 โ€” acceptable for internal documents and slides.
  • 300 โ€” the standard print minimum, and what most journals require.
  • 600 โ€” fine linework, large-format printing, or maps with hairline boundaries.

File size scales roughly with the square of DPI: 22 kB at 72, 128 kB at 300, 287 kB at 600 for the same figure. That is cheap; the reason not to default to 600 is render time on complex maps, not storage.

3. Set the font type for PDF and PostScript

Matplotlib writes Type 3 fonts by default. They are compact โ€” a test figure was 7.3 kB with Type 3 and 11.6 kB with TrueType โ€” and they are the reason journals send figures back.

plt.rcParams["pdf.fonttype"] = 42     # TrueType
plt.rcParams["ps.fonttype"] = 42

The cost is four kilobytes. The benefit is text that is selectable, searchable and rendered in the font you chose.

4. Decide whether SVG keeps text or outlines

plt.rcParams["svg.fonttype"] = "none"   # <text> elements, 1.4 kB in a test figure
plt.rcParams["svg.fonttype"] = "path"   # glyph outlines, 7.2 kB, 15 <path> elements

"none" keeps the file small and editable but depends on the viewer having the font. "path" guarantees identical rendering anywhere and makes the text uneditable and unsearchable.

Use "none" when you control the environment โ€” a design tool with your fonts installed, or a website that ships them. Use "path" when the SVG goes somewhere unknown.

5. Reduce the geometry before exporting a vector file

A vector file stores every vertex. The same map simplified to the tolerance its scale can show:

tolerance   vertices   PDF size
   none       53,352     457 kB
   200 m      34,484     298 kB
   500 m      22,590     198 kB
 1,000 m      14,943     133 kB

At 1:1,000,000 the resolvable ground distance is 200 m, so the 298 kB file is visually identical to the 457 kB one. Simplification is the correct fix for a large vector map; raising DPI or switching to raster is not.

6. Keep the figure at its final size

Every point-based size โ€” type, line width, marker size โ€” is physical. Export at the size you designed and place at 100%, or the scale factor multiplies all of them at once: an 8-inch figure placed in a 90 mm column scales by 0.443, turning 8 pt text into 3.54 pt.

7. Open the exported file and check it

Zoom the PDF to 100% and read the smallest text. Check that the fonts are the ones you chose, that no element is clipped, and that the raster components are sharp. Five seconds, and it catches everything above.

Grid of four font settings with their measured file sizes and outcomes.
Measured on a one-line test figure, so the absolute sizes are small and the ratios hold.

Code examples

Example 1 โ€” export presets, with the settings that matter baked in

import os
import matplotlib.pyplot as plt

PRINT_RC = {
    "pdf.fonttype": 42, "ps.fonttype": 42, "svg.fonttype": "none",
    "savefig.transparent": False, "savefig.facecolor": "white",
}

PRESETS = {
    "journal_pdf":  dict(format="pdf", dpi=300),
    "journal_tiff": dict(format="tiff", dpi=600, pil_kwargs={"compression": "tiff_lzw"}),
    "report_png":   dict(format="png", dpi=300),
    "slide_png":    dict(format="png", dpi=150),
    "web_png":      dict(format="png", dpi=144),
    "editable_svg": dict(format="svg"),
}


def export_map(fig, stem, preset="journal_pdf", verbose=True):
    with plt.rc_context(PRINT_RC):
        options = dict(PRESETS[preset])
        fmt = options.pop("format")
        path = f"{stem}.{fmt}"
        fig.savefig(path, **options)
    if verbose:
        w, h = fig.get_size_inches()
        print(f"{path:28} {os.path.getsize(path) / 1024:8,.0f} kB   "
              f"{w * 25.4:.0f} ร— {h * 25.4:.0f} mm   [{preset}]")
    return path

Example 2 โ€” auditing what an export will contain

def export_audit(fig, target_width_mm=None, min_pt=6.0):
    """Vertices, text sizes and raster elements โ€” before you export."""
    import matplotlib.text as mtext
    import matplotlib.image as mimage

    width_mm = fig.get_size_inches()[0] * 25.4
    scale = (target_width_mm / width_mm) if target_width_mm else 1.0

    vertices = 0
    for ax in fig.axes:
        for collection in ax.collections:
            for path in collection.get_paths():
                vertices += len(path.vertices)
        for line in ax.lines:
            vertices += len(line.get_xydata())

    texts = [t for t in fig.findobj(mtext.Text) if t.get_text().strip()]
    small = [(t.get_text()[:24], t.get_fontsize() * scale)
             for t in texts if t.get_fontsize() * scale < min_pt]
    rasters = fig.findobj(mimage.AxesImage)

    print(f"figure         {width_mm:.0f} mm wide"
          + (f", placed at {target_width_mm:.0f} mm (ร—{scale:.3f})" if target_width_mm else ""))
    print(f"vertices       {vertices:,}"
          + ("   <- simplify before a vector export" if vertices > 30_000 else ""))
    print(f"text objects   {len(texts)}, {len(small)} below {min_pt} pt after placement")
    for label, size in small[:6]:
        print(f"                 {label:26} {size:4.2f} pt")
    print(f"raster layers  {len(rasters)}"
          + ("   <- set their DPI explicitly" if rasters else ""))
    return {"vertices": vertices, "small_text": small, "rasters": len(rasters)}

Example 3 โ€” checking the fonts actually embedded

import subprocess


def check_pdf_fonts(path):
    """Requires poppler-utils. Type 3 in the output is the thing to avoid."""
    try:
        out = subprocess.run(["pdffonts", path], capture_output=True,
                             text=True, check=True).stdout
    except (FileNotFoundError, subprocess.CalledProcessError):
        print("pdffonts not available โ€” check in a PDF viewer's document properties")
        return None

    print(out)
    if "Type 3" in out:
        print("! Type 3 fonts found โ€” set plt.rcParams['pdf.fonttype'] = 42")
    if "no" in out.split("\n")[2:] and "emb" in out:
        print("! a font is not embedded โ€” the printer will substitute it")
    return out

Explanation

Why vector beats raster for most maps

A map is mostly lines, fills and text โ€” all of which have exact mathematical descriptions. A raster export samples that onto a grid and throws the descriptions away, so any later enlargement re-samples pixels rather than redrawing shapes.

Vector output also keeps text as text, which means it can be searched, selected, and rendered at the printer's resolution rather than the file's. The measured PDF was 457 kB against 287 kB for a 600 dpi PNG โ€” comparable in size, and unlimited in resolution.

The exception is genuine imagery. A satellite scene has no vector description; wrapping it in a PDF stores the same pixels with extra overhead.

Why Type 3 fonts cause trouble

Type 3 PDF fonts embed each glyph as a general PostScript drawing procedure rather than as a standard outline font. That makes them flexible and compact, and it means some RIPs, viewers and publisher pipelines handle them badly โ€” substituting a different font, rendering them at the wrong weight, or refusing the file.

Type 42 embeds a TrueType subset, which every part of the chain understands. The measured cost was 4.3 kB on a small figure.

Why DPI does nothing for a vector file

dpi in savefig sets the resolution of raster output, and of any raster elements inside a vector file. For pure vector content it has no effect โ€” the geometry is stored as coordinates and rendered at the output device's resolution.

Passing dpi=300 to a PDF export is harmless and often useful, because it sets the resolution of embedded rasters such as a basemap or a hillshade.

Why simplification is the right fix for a huge vector map

A vector file's size is dominated by vertex count. The measured ladder shows the whole relationship: 53,352 vertices for 457 kB, and 14,943 for 133 kB.

Switching to raster to avoid a large PDF trades a solvable problem for a permanent one โ€” the map stops scaling. Simplifying to the tolerance the map's scale can actually show removes vertices that were never going to be visible.

Bar chart of PNG file size at four DPI settings for the same map.
The reason not to default to 600 is render time on complex maps, not storage.

Edge cases or notes

  • bbox_inches="tight" changes the physical size of the export, which breaks a print layout.
  • Transparency in PDF is handled inconsistently by some print workflows; flatten if the printer asks.
  • TIFF with LZW compression is what many journals want for raster figures.
  • Set the DPI of embedded rasters explicitly โ€” a basemap at the default can be the blurry part of a sharp map.
  • savefig.facecolor defaults to white, but a transparent export over a coloured page can surprise you.
  • Check hairlines. Lines below about 0.25 pt can disappear on press.
  • Large SVGs slow browsers. Simplify, or export PNG for the web.
  • Keep the export function in a module so every figure in a report is exported identically.

FAQ

Should I export maps as PNG or PDF?

PDF for anything with vector layers and text โ€” it keeps text as text and scales perfectly. PNG only when the map is mostly imagery.

What DPI do I need for print?

300 is the standard minimum, 600 for fine linework. Measured on one map: 128 kB at 300 dpi and 287 kB at 600.

Why do journals reject my matplotlib PDFs?

Usually the Type 3 fonts matplotlib writes by default. Set pdf.fonttype = 42 to embed TrueType subsets instead; the file grows by a few kilobytes.

Should SVG keep text or convert it to outlines?

svg.fonttype="none" keeps <text> and stays small and editable but needs the font available. "path" converts to outlines โ€” measured, 1.4 kB became 7.2 kB โ€” and renders identically anywhere.

My PDF is enormous. What do I do?

Simplify the geometry. Vertex count dominates vector file size: 53,352 vertices produced 457 kB and 14,943 produced 133 kB of the same map.

Does dpi matter for a PDF?

Only for raster elements inside it, such as a basemap. Pure vector content is stored as coordinates and rendered at the output device's resolution.