← Back to Blog

Why your web-map PDF export looks blocky — and three ways to fix it

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:

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 mapBrowser (user’s session)Esri print serverYour render service
Public ArcGIS layersYesYesYes
API-key / referrer-secured basemapYes (has the key)No — swap a public basemapYes (holds the key)
ArcGIS token-secured layersYes (signed in)Yes (inject the token)Yes
Non-ArcGIS secured (PostGIS / RLS)Yes (its own auth)No — but drawn graphics serializeYes (service credentials)
Resolution at print sizeScreen-capped (blocky)Any DPIAny 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

No method beats the source imagery. Server rendering removes the screen ceiling — a big win at normal zooms — but a tiny feature at maximum zoom is still bounded by the basemap’s deepest level of detail. If you need more than that, the only lever left is higher-fidelity source imagery (county or paid ortho), fed through whichever render strategy you chose.

The decision tree

  1. Is all printable content public ArcGIS (plus simple graphics)? Use the Esri ExportWebMap print service, anonymously. Crisp, cheap, fast to build.
  2. 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.
  3. Low-stakes or throwaway, or nothing else can reach the data? Client-side capture — and accept the resolution ceiling.

References

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