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.
Step-by-step solution
1. Decide raster or vector from what the map contains
| Map content | Format | Why |
|---|---|---|
| Vector layers, labels, legend | 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.
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.
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.facecolordefaults 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.
Internal links
- How to build a print-ready map layout in Matplotlib โ the layout this exports
- Fixing missing or substituted fonts in an exported map PDF โ the font failure in detail
- Fixing a map export that is hundreds of megabytes โ when the vertex count got away
- Fixing map text that is too small in the exported file โ the scale factor
- Map scale explained: how much detail a scale can hold โ choosing the simplification tolerance
- How to save a map image with matplotlib โ the basics
- How to reduce GIS file size โ the data-side equivalent
- How to build a reusable map style module โ where the presets live
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.