ScreenshotNeo

BlogHTML to image & PDF

How to Convert HTML to PDF with Embedded Web Fonts

Embed web fonts in HTML-to-PDF output with WeasyPrint or Playwright, including font loading, print styles, troubleshooting, and a one-call API option.

By the ScreenshotNeo team4 October 20268 min read

To convert HTML to PDF with embedded web fonts, make the font available to the renderer before PDF generation. With WeasyPrint, declare it using @font-face, pass a shared FontConfiguration to the stylesheet and PDF call, and generate the PDF with HTML.write_pdf(). With Playwright, wait for the page’s fonts to load before calling page.pdf(); remember that PDF generation uses print CSS by default.

A font declaration in the source HTML is not proof that the PDF used or embedded that font. Verify the generated file in the target PDF reader, including representative glyphs, weights, and page breaks. WeasyPrint documents automatic font embedding and subsetting by default. WeasyPrint first steps · WeasyPrint API reference

1. Choose a rendering approach

Use WeasyPrint when you want its documented HTML/CSS-to-PDF workflow and font configuration. Use Playwright when the document depends on browser rendering or JavaScript-driven page content. The supplied documentation supports these implementation details, but does not provide a head-to-head performance comparison; benchmark your own documents and deployment before choosing based on speed.

Need Practical choice
Declare a web font and generate a PDF with a Python library WeasyPrint with @font-face and FontConfiguration
Render a page in a browser before printing Playwright, with an explicit font-readiness check
Control print versus screen CSS in Playwright Use the default print media or call page.emulate_media(media="screen")

2. Convert HTML with WeasyPrint

Install WeasyPrint and its platform dependencies following the documentation for your operating system. This runnable script takes a local HTML file, applies a stylesheet that fetches a web font, and writes output.pdf. Replace the example font URL with a font file your rendering process can access.

from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration

font_config = FontConfiguration()
css = CSS(
    string="""
    @font-face {
        font-family: ExampleFont;
        src: url("https://example.com/fonts/example.woff2") format("woff2");
        font-weight: 400;
        font-style: normal;
    }

    @page { size: A4; margin: 18mm; }
    body { font-family: ExampleFont, sans-serif; }
    """,
    font_config=font_config,
)

HTML(filename="input.html").write_pdf(
    "output.pdf",
    stylesheets=[css],
    font_config=font_config,
)

The same font_config is passed when constructing CSS and when writing the PDF. The font URL must resolve from the machine or container running the conversion. WeasyPrint also discovers fonts installed on the host; a locally installed font can be useful when remote fetching is unavailable or undesirable. The official guide describes @font-face and this configuration pattern; confirm the selected format and behavior against the WeasyPrint version deployed.

Use inline HTML instead of a file

from weasyprint import HTML, CSS
from weasyprint.text.fonts import FontConfiguration

font_config = FontConfiguration()
html = HTML(string="""
<!doctype html>
<html>
  <body>
    <h1>PDF with a web font</h1>
    <p>The font stylesheet is supplied separately.</p>
  </body>
</html>
""")
css = CSS(string="""
@font-face {
  font-family: ExampleFont;
  src: url("https://example.com/fonts/example.woff2") format("woff2");
}
body { font-family: ExampleFont, sans-serif; }
""", font_config=font_config)
html.write_pdf("output.pdf", stylesheets=[css], font_config=font_config)

Match the face to the requested weight and style

Declare each face you need with its actual weight and style. If the document requests a weight or italic face that you did not provide, the renderer may select a fallback or synthesize styling. Keep family names consistent between @font-face and the document rules.

@font-face {
  font-family: ExampleFont;
  src: url("https://example.com/fonts/example-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
}
@font-face {
  font-family: ExampleFont;
  src: url("https://example.com/fonts/example-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
}
@font-face {
  font-family: ExampleFont;
  src: url("https://example.com/fonts/example-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
}

3. Convert a page with Playwright

Playwright’s page.pdf() uses print CSS media. The example below navigates to a page, waits for the browser’s font set to report readiness, and writes a PDF. The font-readiness wait is an implementation step; the supplied Playwright PDF documentation does not define a dedicated PDF font-loading option.

import asyncio
from pathlib import Path
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com/report", wait_until="networkidle")
        await page.evaluate("document.fonts.ready")
        await page.pdf(
            path="output.pdf",
            format="A4",
            print_background=True,
            prefer_css_page_size=True,
        )
        await browser.close()

asyncio.run(main())

Remove the unused Path import if copying this as-is; it is not required. For screen styling rather than print styling, call await page.emulate_media(media="screen") before page.pdf(). Playwright documents PDF controls including page format or dimensions, margins, preference for CSS page size, background printing, and color handling. Set options to match your output requirements and consult the Playwright Page PDF API for the installed version.

Check specific font families in the page

Waiting for document.fonts.ready lets the page finish its font loading work before capture. You can also check whether a required face is available to the page’s font set:

await page.evaluate("document.fonts.ready")
font_ok = await page.evaluate("document.fonts.check('16px ExampleFont')")
if not font_ok:
    raise RuntimeError("Required font did not become available")

This checks the browser page’s font set; it does not by itself prove which font was embedded in the final PDF. Inspect the output PDF and verify the result in the PDF reader or tooling used by your workflow.

4. Set page layout and print behavior

For stable pagination, define page size and margins deliberately. In CSS, use @page; in Playwright, use the documented PDF options or prefer CSS page size. Print and screen media can apply different CSS, so inspect the layout under the same media mode used to generate the PDF.

@page {
  size: A4;
  margin: 18mm 16mm;
}

@media print {
  .screen-only { display: none; }
  h1, h2 { break-after: avoid; }
  .keep-together { break-inside: avoid; }
}

For Playwright, format, width/height, margin, prefer_css_page_size, and background/color controls affect output. Choose one source of page sizing intentionally: CSS page rules or explicit PDF dimensions. Test long tables, headings near page boundaries, and content with different font metrics.

5. Verify the PDF

  1. Confirm the conversion completed and the output file is non-empty.
  2. Open it in the PDF reader used by your recipients or downstream system.
  3. Check that body text, headings, bold and italic faces, and non-Latin or special glyphs render correctly.
  4. Inspect page breaks, line wrapping, margins, and backgrounds under the intended media mode.
  5. For important documents, inspect PDF font properties with your PDF tooling to confirm the expected font is embedded and that required glyphs are covered.

WeasyPrint states that fonts are automatically embedded and subset by default to glyphs used in the PDF. Subsetting keeps only used glyphs; it does not compensate for a missing font, unavailable font URL, or missing glyph in the source font. Playwright’s browser rendering should also be checked in the resulting artifact rather than inferred from the page’s CSS alone.

6. Troubleshooting

Symptom Likely cause What to check or change
Text appears as squares or missing glyphs The intended font is unavailable, or its character coverage is incomplete. Confirm the font is installed or the @font-face URL is reachable by the renderer. Check whether the font includes the required glyphs and inspect fallback behavior.
PDF uses a fallback font The face declaration, family name, weight, or style does not match the requested CSS, or loading failed. Align family, weight, and style declarations; verify the resource URL and inspect the generated PDF.
Remote font works in a browser but not on the server The renderer’s environment cannot fetch the font because of network policy, TLS, authentication, or URL access. Test access from the conversion environment, check certificates and network rules, and use an allowed host-installed font or reachable asset URL.
Playwright PDF looks unlike the page page.pdf() uses print media by default. Adjust print CSS or call page.emulate_media(media="screen") when screen styling is intended.
Different line breaks or page count Font metrics or print styles differ from the browser view; page size and margins may also differ. Wait for fonts before capture, set page sizing explicitly, and review the output at the required paper size.
Conversion fails on HTML with local assets Resource paths may be invalid or blocked by the renderer’s resource access policy. Resolve paths deliberately and configure resource access for the intended assets. For untrusted input, do not grant unrestricted local-file access.

7. Security, reliability, and cost considerations

Server-side conversion fetches resources referenced by HTML and CSS. WeasyPrint’s security guidance notes that documents can access local files through file:// URLs; for untrusted content, sandbox access to trusted files and use a custom URL fetcher that blocks or filters protocols and paths. Apply network and file access rules to the document, stylesheets, and font resources.

Font loading adds an external dependency when the font is fetched remotely. For reliable repeatable output, keep the font URL stable and accessible, pin your renderer version, and validate representative documents after deployment changes. The reviewed sources provide no benchmark or universal cost estimate: measure conversion time, resource-fetch behavior, and output size using your actual documents and runtime.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For PDF output, its one-request API can capture a page as a PDF; see the API documentation for parameters and configuration. This is a rendered-page capture option when you want a hosted capture instead of maintaining browser setup.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -d format=pdf \
  -o page.pdf
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={
        "access_key": "YOUR_API_KEY",
        "url": "https://stripe.com",
        "format": "pdf",
    },
    timeout=90,
)
r.raise_for_status()
open("page.pdf", "wb").write(r.content)
const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
  format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await (await import('node:fs/promises')).writeFile('page.pdf', bytes);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 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 response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free account for 1,000 screenshots a month, with no card.

FAQ

Does a PDF always contain an embedded font when the source page uses a web font?

No. The renderer must load and use the font during PDF generation. Check the generated PDF rather than assuming the HTML declaration guarantees embedding.

Will a PDF preserve every character in my source text?

Only if the selected font contains the needed glyphs or a suitable fallback supplies them. Verify the characters your documents require.

Should I use print or screen styles for a PDF?

Use the stylesheet mode that matches the intended document. Playwright defaults to print media; explicitly select screen media when that is the desired appearance.

Can I safely convert arbitrary user-submitted HTML on a server?

Only with deliberate isolation and resource restrictions. HTML and CSS can reference local or remote resources, so constrain what the renderer can access.