Why your web-map PDF export looks blocky — and three ways to fix it
You add a “Download PDF” button, screenshot the map, drop the image into a branded page, and ship it. On screen it looks fine. Printed — or just zoomed in the PDF viewer — the map is soft, the labels are fuzzy, and a stakeholder asks why the report looks low-budget. The problem isn’t your PDF library. It’s where the pixels came from.
We hit this building a map-export feature and spent six iterations fighting it before naming the real constraint. Here is why client-side capture is structurally incapable of a crisp export, the two requirements that any good export has to satisfy at once, and a decision tree for picking the right of three approaches for your app — because the right answer depends entirely on who can reach your data.
Why it’s blocky
A browser-side screenshot — view.takeScreenshot(),
html2canvas, a canvas grab — reads the map exactly as it sits in the
on-screen WebGL framebuffer: at screen resolution and the level of detail the screen
asked for. A typical map view is ~96 DPI. Print wants 200–300. When you scale that screenshot
up to fill a letter-size page, you are enlarging pixels that were never rendered — there is no extra
detail to recover.
// The blocky path: capture what's already on screen const { dataUrl } = await view.takeScreenshot({ format: "png" }); pdf.addImage(dataUrl, "PNG", x, y, w, h); // reads the on-screen WebGL framebuffer: screen resolution + screen LOD. // Scaling it up in the PDF cannot add pixels that were never drawn.
No PDF setting fixes this, because the deficiency is upstream of the PDF. To get a crisp export you have to render the map again, at print DPI — and that is where the real architecture question starts.
The two requirements that pull against each other
Every high-quality map export has to satisfy two independent things at once:
1. Resolution — render at print DPI, not the screen’s pixel size.
2. Data access — whatever does that rendering must be able to fetch every layer
on the map.
These fight. The way to get resolution for free is to render server-side — but a server can only render data it can authenticate to. Client-side capture has the opposite profile: it can draw anything the signed-in user can already see, but it’s stuck at screen resolution. That single trade-off decides which of the three approaches you can actually use.
Three ways to fix it
1. Esri’s ExportWebMap print service — best quality, lowest build cost
The ArcGIS-hosted Export Web Map task takes a JSON description of your map and
re-fetches every layer server-side at the DPI you request, then composites a crisp PDF or
image
(ExportWebMap specification).
In the Maps SDK it’s a few lines — the Print widget for a turnkey UI, or
rest/print for a custom button:
import * as print from "@arcgis/core/rest/print.js"; import PrintParameters from "@arcgis/core/rest/support/PrintParameters.js"; import PrintTemplate from "@arcgis/core/rest/support/PrintTemplate.js"; const template = new PrintTemplate({ format: "pdf", layout: "a4-portrait", exportOptions: { dpi: 300 }, // print DPI, not screen pixels layoutOptions: { titleText: "Site Report" }, }); const { url } = await print.execute( "https://utility.arcgisonline.com/arcgis/rest/services/Utilities/PrintingTools/GPServer/Export%20Web%20Map%20Task", new PrintParameters({ view, template }), ); // url -> a PDF the SERVER composited by re-fetching each layer at 300 DPI
It’s crisp, keyless to the end-user, and printing is not a credit-charged operation (credits documentation). The catch is data access. The print server can only render what it can fetch:
- Can render: public ArcGIS services; ArcGIS-token-secured layers when the client injects the token (printing secured services); and any client-side graphics — a route line, a highlighted parcel, points you drew in the browser — because the SDK serializes those inline into the request.
- Cannot render: a basemap that needs your API key or referrer (the server doesn’t have them) — so swap in a public, tokenless basemap just for the print render; and any non-ArcGIS secured endpoint (a PostGIS/REST API behind row-level security), which the server can’t authenticate to at all.
Use it when your printable content is public ArcGIS plus a few graphics. For a lot of apps, that’s the whole job — done in an afternoon.
2. Client-side capture — works with any data, fails on quality
The screenshot approach you started with. Because it renders under the user’s own session, it trivially handles private data, key-locked basemaps, anything the user can see. It just can’t exceed screen resolution. Keep it only as a low-stakes fallback, or when nothing else can reach the data and the output doesn’t need to print well.
3. A custom serverless render service — the premium, paid-app answer
When an app has private, mixed, or non-ArcGIS data and still needs premium, fully-branded exports, the durable answer is a small render service you own (a serverless function or container). It holds the credentials — the API keys, the database service role — server-side, never exposed to the browser, so as a trusted principal it can reach everything: public, key-secured, and row-level-secured data alike. It renders at any DPI, fully branded, keyless to the end-user, behind your existing paywall. One service serves every app in a portfolio. The cost is real engineering plus negligible per-render compute — worth it precisely when exports are a paid feature.
Who can reach what
This table is the whole decision, on one page — match your map’s data to the column that can render it:
| Data on the map | Browser (user’s session) | Esri print server | Your render service |
|---|---|---|---|
| Public ArcGIS layers | Yes | Yes | Yes |
| API-key / referrer-secured basemap | Yes (has the key) | No — swap a public basemap | Yes (holds the key) |
| ArcGIS token-secured layers | Yes (signed in) | Yes (inject the token) | Yes |
| Non-ArcGIS secured (PostGIS / RLS) | Yes (its own auth) | No — but drawn graphics serialize | Yes (service credentials) |
| Resolution at print size | Screen-capped (blocky) | Any DPI | Any DPI |
“Can’t we just build our own Esri print service?”
You can — with caveats most teams don’t expect. Esri’s own custom print service (design
.pagx layout and .rptx report templates in ArcGIS Pro,
publish a synchronous web tool) is genuinely excellent for branded, multi-section report PDFs — but it
is powered by a geoprocessing service on a federated ArcGIS Server and therefore requires
ArcGIS Enterprise; you cannot host a custom print service in ArcGIS Online
(Printing in web applications).
And even with Enterprise, it still reaches only public and ArcGIS-secured layers — it still
can’t read your PostGIS-behind-RLS data or your key-locked basemap. So if your blocker is non-ArcGIS
private data, Enterprise doesn’t solve it; the serverless render service (option 3) is the one that
does. Stand up Enterprise as a platform decision, never just for printing.
The honest ceiling
The decision tree
- Is all printable content public ArcGIS (plus simple graphics)? Use the Esri ExportWebMap print service, anonymously. Crisp, cheap, fast to build.
- Paid or private data, premium quality required? Build the serverless render service. Interim stopgap: the print service with a public-basemap swap and graphics serialization, accepting its fragility.
- Low-stakes or throwaway, or nothing else can reach the data? Client-side capture — and accept the resolution ceiling.
References
- ExportWebMap specification — ArcGIS REST APIs
- Print widget — ArcGIS Maps SDK for JavaScript
- Print maps that contain secured services — ArcGIS Server
- Printing in web applications (custom print service) — ArcGIS Enterprise
Need export-quality maps out of your web app?
We build premium map exports the right way for your data — print-DPI rendering, branded report layouts, and a render path that can actually reach your secured and non-ArcGIS layers. If your PDF looks blocky, we can fix it.
Book a free intro call