ScreenshotNeo

BlogHTML to image & PDF

PDFShift PDF output has broken fonts: troubleshooting guide

Fix missing or inconsistent fonts in PDFShift PDFs by checking font delivery, CSS, readiness, and header and footer embedding.

By the ScreenshotNeo team4 October 20269 min read

If PDFShift PDF output has missing, substituted, or inconsistent fonts, start by checking when and how the font reaches the converter. PDFShift warns that remotely hosted fonts can load intermittently if conversion starts before the font is ready. Use an accessible font URL or embed the font as base64; for remote fonts, wait for document.fonts.ready with PDFShift’s wait_for option. Header and footer fonts need separate handling because those regions cannot make network requests.

This guide separates PDFShift’s documented behavior from practical diagnostic checks. It does not assume that every font defect has the same cause.

1. Confirm that the font is delivered

PDFShift supports custom fonts through a CSS declaration and its css request parameter. If your stylesheet points to a remote font file, first verify that the URL is reachable from the conversion environment and serves the intended file. A font URL that works in your own browser may still be inaccessible to the converter because of authentication, network restrictions, redirects, or a mistyped path.

PDFShift specifically cautions that external font loading can be intermittent when the conversion begins before the font has loaded. Its documentation recommends local or base64 font data for more consistent loading. Choose based on your deployment, document size, and font licensing; there is no universal best option.

2. Check the CSS family, weight, and style

Make sure the family name declared in @font-face exactly matches the family assigned to the page elements. Also check that the supplied file corresponds to the weight and style your CSS requests. If only a regular face is declared but the document requests a bold or italic face, the renderer may synthesize or substitute styling.

PDFShift’s Node guide says the css parameter accepts a CSS string or a URL and applies it to the converted page. Use deliberate selectors in production. The broad * rule below is compact for a minimal example, but may override intentional typography across a real document.

@font-face {
  font-family: "Report Sans";
  src: url("https://example.com/fonts/report-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: "Report Sans";
  src: url("https://example.com/fonts/report-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
}

body {
  font-family: "Report Sans", sans-serif;
  font-weight: 400;
}

strong, h1, h2 {
  font-weight: 700;
}

Replace the example font URLs with URLs accessible to PDFShift. If you pass CSS in a JSON request, encode it as a JSON string; if you pass a URL, ensure the converter can fetch that stylesheet too.

3. Wait for asynchronous font loading

When the font must be fetched externally, use PDFShift’s wait_for parameter to defer conversion until a global function reports readiness. PDFShift’s documented example sets a flag after document.fonts.ready resolves, then polls a function that returns the flag.

<script>
  window.fontsReady = false;
  document.fonts.ready.then(() => {
    window.fontsReady = true;
  });

  window.pdfFontsReady = () => window.fontsReady;
</script>

Configure wait_for to call pdfFontsReady using the syntax supported by your PDFShift integration. The wait is bounded by the remaining conversion timeout. PDFShift’s help article lists 30 seconds for free accounts and 100 seconds for premium accounts at the time described by that documentation; account limits can change, so check the current plan documentation before relying on those values.

This readiness check cannot fix a missing font URL, an invalid font file, or a face with no requested glyphs. It only helps when loading is asynchronous and the font eventually becomes available.

4. Embed fonts in headers and footers separately

PDFShift documents that headers and footers are separate from the main content and cannot make network requests. A remote font reference in those regions therefore cannot be relied on in the same way as a font loaded by the main page.

PDFShift’s prescribed approach is to base64-encode the font, include the encoded font in both the main document and the header or footer, and use the face in the main body so the converter includes it in the generated PDF. Its help article reports successful tests with TTF and WOFF2; that is a vendor-reported result, not an independent test.

/* Put this declaration in the main document and in header/footer CSS. */
@font-face {
  font-family: "Report Sans Embedded";
  src: url(data:font/woff2;base64,REPLACE_WITH_BASE64_FONT_DATA) format("woff2");
  font-weight: 400;
  font-style: normal;
}

/* Use the face in the body as well as in the header/footer. */
body {
  font-family: "Report Sans Embedded", sans-serif;
}

.pdf-header, .pdf-footer {
  font-family: "Report Sans Embedded", sans-serif;
}

The placeholder is not a usable font: replace it with the base64 data for your licensed font file, and use the correct MIME type and format for that file. Follow PDFShift’s current header/footer request format when supplying the separate regions.

5. Runnable request examples

The following examples show how to submit HTML and CSS to PDFShift. Replace YOUR_API_KEY and the example font URLs. The HTML example includes the readiness flag; configure the PDFShift wait_for option in the JSON body as required by the current API syntax for your account.

cURL

curl --request POST \
  --url https://api.pdfshift.io/v3/convert/pdf \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "source": "<!doctype html><html><head><style>@font-face{font-family:ReportSans;src:url(https://example.com/fonts/report.woff2) format(\"woff2\")}body{font-family:ReportSans,sans-serif}</style></head><body><p>PDF font check</p><script>window.ready=false;document.fonts.ready.then(()=>window.ready=true);window.pdfFontsReady=()=>window.ready</script></body></html>",
    "wait_for": "pdfFontsReady"
  }' \
  --output document.pdf

Check PDFShift’s current API reference for the exact endpoint, authentication, output, and wait_for schema used by your account or API version before deployment.

Python

import requests

api_key = "YOUR_API_KEY"
html = """<!doctype html>
<html>
<head>
  <style>
    @font-face {
      font-family: ReportSans;
      src: url('https://example.com/fonts/report.woff2') format('woff2');
      font-weight: 400;
    }
    body { font-family: ReportSans, sans-serif; }
  </style>
</head>
<body>
  <p>PDF font check</p>
  <script>
    window.fontsReady = false;
    document.fonts.ready.then(() => { window.fontsReady = true; });
    window.pdfFontsReady = () => window.fontsReady;
  </script>
</body>
</html>"""

response = requests.post(
    "https://api.pdfshift.io/v3/convert/pdf",
    headers={"X-API-Key": api_key},
    json={"source": html, "wait_for": "pdfFontsReady"},
    timeout=120,
)
response.raise_for_status()
with open("document.pdf", "wb") as pdf_file:
    pdf_file.write(response.content)

Node.js

const apiKey = 'YOUR_API_KEY';
const html = `<!doctype html>
<html>
<head>
  <style>
    @font-face {
      font-family: ReportSans;
      src: url('https://example.com/fonts/report.woff2') format('woff2');
      font-weight: 400;
    }
    body { font-family: ReportSans, sans-serif; }
  </style>
</head>
<body>
  <p>PDF font check</p>
  <script>
    window.fontsReady = false;
    document.fonts.ready.then(() => { window.fontsReady = true; });
    window.pdfFontsReady = () => window.fontsReady;
  </script>
</body>
</html>`;

const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {
  method: 'POST',
  headers: {
    'X-API-Key': apiKey,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ source: html, wait_for: 'pdfFontsReady' }),
});

if (!response.ok) {
  throw new Error(`PDFShift returned ${response.status}: ${await response.text()}`);
}

const pdf = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('document.pdf', pdf));

These request examples are practical integration templates. Confirm the current PDFShift API details for your account before using them unchanged.

6. Troubleshooting checklist

Symptom Likely area to inspect Action
Every element uses a fallback font Font URL, access, or CSS parsing Check the URL from the conversion environment, confirm it serves a font file, and verify the @font-face declaration is in the CSS actually sent.
Some conversions work and others substitute fonts External font readiness Use local or base64 data where feasible, or wait on a readiness function based on document.fonts.ready.
Body text is correct, header/footer text is not Separate header/footer loading context Embed the font in both regions and use it in the body, following PDFShift’s documented approach.
Regular text works, bold or italic does not Face metadata and weight/style mapping Declare the supplied face’s actual weight and style; verify a matching font file is available.
Only particular symbols or scripts are wrong Font glyph coverage Check whether the font file contains the needed glyphs; a readiness wait cannot supply missing characters.
Layout changes after adding a global font rule Overbroad selector or forced override Replace blanket rules with selectors for the intended content and remove unnecessary !important.
Conversion waits and then times out Readiness callback never returns true or font never loads Confirm the global callback exists, becomes truthy, and the font request succeeds; account for the remaining conversion timeout.

The glyph-coverage and CSS-scope checks are general diagnostic suggestions. PDFShift’s cited help materials focus on font delivery, readiness, and header/footer constraints rather than claiming to diagnose every possible font defect.

7. Performance, reliability, and cost considerations

  • External files: They are easy to update centrally, but PDFShift warns that loading can be intermittent if conversion starts too early. Waiting can improve reliability when the font eventually loads, while adding time to conversion.
  • Local or embedded data: PDFShift describes local or base64 font data as more consistent. Embedded data increases the document or request payload, so consider file size and licensing.
  • Header and footer: Treat these as separate font contexts and include embedded data in both as directed by PDFShift.
  • Timeouts: A readiness wait consumes conversion time. Avoid waiting for unrelated page activity, and verify current plan limits rather than assuming the help article’s listed values remain current.
  • Cost: The supplied documentation does not establish a per-conversion cost comparison for these font strategies. Compare your account’s current plan and usage terms; do not infer that a longer wait changes the price.

8. Or skip the browser setup

PDFShift generates PDFs; ScreenshotNeo is a website screenshot API and MCP server for developers. If your actual deliverable is a page image, it can capture PNG, JPEG, or WebP with one GET request. See the ScreenshotNeo API documentation for parameters and formats.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie and consent banners like a visitor, then removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, or any MCP client. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. It is an alternative for screenshot and PDF capture workflows, not a fix for PDFShift font rendering.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card.

Frequently asked questions

Does document.fonts.ready guarantee the correct font will appear?

No. It helps detect that font loading has settled. The font still needs to be reachable, valid, correctly declared, and contain the required glyphs.

Should I convert every font to base64?

Not necessarily. PDFShift documents local or base64 font data as more consistent than relying on a remote font that may load late. Consider payload size, deployment constraints, and font licensing.

Why does the body font work while the header font does not?

PDFShift treats header and footer content separately, and those regions cannot make network requests. Embed the font in those regions and use it in the main body as well.

Can ScreenshotNeo repair a PDFShift font problem?

No. ScreenshotNeo captures website images or PDFs; it does not change how PDFShift embeds or renders fonts. Use it when a screenshot or a separate capture workflow meets the need.

Sources