ScreenshotNeo

BlogHTML to image & PDF

HTML to PDF in Python on Ubuntu with Indian Language Fonts

Generate PDFs from HTML with Python on Ubuntu and troubleshoot missing Indian-language glyphs using WeasyPrint, Fontconfig, and practical checks.

By the ScreenshotNeo team4 October 20267 min read

Use WeasyPrint to generate a PDF from HTML in Python on Ubuntu. The essential fix for blank boxes or missing Indian-language characters is to install a font that covers the exact scripts you need, make it visible to Fontconfig in the rendering environment, and request it in CSS. Confirm the match with fc-match and render representative text under the same user and environment that will run your application.

This guide uses WeasyPrint because its official documentation provides both a Python API and a font troubleshooting path. Font coverage varies: do not assume a generic font or fallback handles every Indian script, conjunct, or diacritic.

1. Install WeasyPrint on Ubuntu

WeasyPrint depends on native libraries as well as Python packages. Use the current stable installation instructions for your Ubuntu release. They document the distribution package route and a virtual-environment route with the required native dependencies. Avoid copying native package commands from a manual for a different Ubuntu release.

  1. Install WeasyPrint using one of the current documented installation routes.
  2. Check the command-line installation information with weasyprint --info.
  3. Install a font with coverage for your target script using the package manager or make an appropriately licensed font file available to Fontconfig.
  4. Run font checks as the same service account and in the same container or host environment that will generate PDFs.

Font package names and availability depend on the Ubuntu release. The sources for this guide do not verify a specific package or font family for every Indian language, so select based on required script coverage and verify it locally.

2. Check that Ubuntu can find the font

WeasyPrint accesses fonts through Pango and Fontconfig. Use fc-list to inspect fonts Fontconfig knows about, and fc-match to see which installed font is selected for a family or pattern.

fc-list : family | sort -u
fc-match "Your Script Font Family"
weasyprint --info

Replace Your Script Font Family with the family you intend to use. A successful match only shows what Fontconfig selected; it does not prove that the font contains every character in your content. Include actual words, conjuncts, and combining marks from your documents in the validation sample.

If you install a font file manually, make sure it is readable by the rendering process and discoverable by Fontconfig. If the service runs in a container, the font must be present in that container, not just on the host.

3. Create a PDF from HTML in Python

This minimal example writes a PDF from an HTML string. Substitute text in the target script and a family installed on your system. The fallback list is a preference list, not a guarantee of script coverage.

from weasyprint import HTML

html = """


  
  


  <h1>Example document</h1>
  <p>Replace this sentence with representative text in your target Indian language.</p>

"""

HTML(string=html, base_url=".").write_pdf("output.pdf")

base_url gives relative resources, such as stylesheets or images, a base location. Set it deliberately when the HTML references local resources; for remote or generated content, choose an appropriate resource-loading policy as well. WeasyPrint’s Python API accepts HTML input and writes a PDF with write_pdf(). See the official first-steps documentation for API details and installation guidance.

4. Load a font with CSS @font-face

If a system-installed family is not being selected reliably, provide a font file explicitly through CSS @font-face. The official example uses FontConfiguration for both the stylesheet and the PDF write call:

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

font_config = FontConfiguration()
font_path = Path("fonts/your-script-font.ttf").resolve()

html = """


  
  


  <p>Replace with representative text from the target script.</p>

"""

css = CSS(
    string="body { font-family: DocumentScript, sans-serif; }",
    base_url=str(font_path.parent),
    font_config=font_config,
)
HTML(string=html, base_url=str(font_path.parent)).write_pdf(
    "output.pdf",
    stylesheets=[css],
    font_config=font_config,
)

Keep the font file deployed with the application and use a license that permits your intended use and distribution. Do not assume a font referenced on a developer workstation will also exist on a server.

5. Validate real script rendering

  1. Build a small HTML fixture containing the actual target script, including representative conjuncts, vowel signs, and combining marks where applicable.
  2. Check the requested family with fc-match in the runtime environment.
  3. Generate a PDF using the same code path, service user, container image, and resource configuration as production.
  4. Open the PDF in a viewer and inspect the glyphs and shaping. Where useful, also check extracted text, but do not treat text extraction alone as proof that visual shaping is correct.
  5. Repeat for each script your application supports. A font that works for one language is not automatically adequate for another.

This validation procedure follows from the documented font-matching workflow; the research behind this guide did not test or certify any particular Indian-language font or Ubuntu package.

6. Troubleshoot missing glyphs and failed renders

Symptom Likely cause What to check or change
Blank glyphs or squares The selected font is missing or lacks the needed characters. Check fc-match, install a font with verified script coverage, or load the font with @font-face. WeasyPrint’s documentation identifies font installation and availability as the remedy for missing characters.
It works locally but not in production The server, container, or service account has a different font set or cannot read the font file. Run fc-list and fc-match in the production runtime as its service user; package or deploy the font there.
fc-match returns an unexpected family The requested family is unavailable or Fontconfig resolves the pattern to a fallback. Verify the installed family name with fc-list, correct the CSS family, and consider explicit @font-face loading.
Latin text renders but an Indian script does not The chosen font may cover Latin without covering the required script or character sequence. Use a font with verified coverage for the target script and test representative text rather than relying on a generic fallback.
Relative images or stylesheets are absent The HTML has no suitable base URL, or its resources are unavailable to the renderer. Set base_url for local resources and confirm that the files are readable from the process environment.
Installation fails or the import cannot load native components Python package and native-library requirements may not match the Ubuntu release or installation route. Follow the current stable installation page for that release, then inspect weasyprint --info.

7. Security, performance, and reliability

Security

Do not render untrusted HTML or CSS without addressing WeasyPrint’s documented security risks. HTML and CSS can cause resource access, so define what files and network resources the renderer is allowed to load. Treat uploaded documents and remote URLs as untrusted input, and isolate rendering according to your application’s security requirements.

Performance

Measure rendering with your own representative documents, fonts, images, and page counts. This research does not establish a speed benchmark. For repeated generation, keep the runtime and required fonts installed and avoid unnecessary resource downloads; ensure that any caching or resource reuse preserves correctness and access controls.

Reliability

Pin and deploy a known Python and Ubuntu environment, include the same fonts in every worker image, and run a small script-rendering fixture during deployment checks. Log rendering failures and resource-loading errors without logging sensitive document contents. Recheck font behavior when changing the base image, font package, or WeasyPrint version.

Cost

WeasyPrint is an open-source rendering library, but operating it still uses compute, storage, deployment, and maintenance time. The font sources reviewed here do not establish licensing terms for a particular font; check the license for the font you choose, especially if redistributing it in an application or container.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It can return a screenshot or PDF from one GET request. This is useful when you need a rendered web page or PDF without installing and maintaining a browser capture stack; it is not a replacement for a Python HTML-to-PDF library when you need to generate a PDF directly from your own HTML string.

For example, the following cURL request captures a page as WebP:

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

Python:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options. Before capture, ScreenshotNeo removes known cookie and consent banners, newsletter popups, and chat widgets; each of these cleanup steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response reports page verdict and billing status in headers. Its MCP server provides screenshot tools for AI agents. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.

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

Frequently asked questions

Does this workflow guarantee correct rendering for every Indian language?

No. Font coverage and shaping must be verified for each script and representative text in your actual runtime.

Can I use a system font and @font-face together?

Yes. CSS can specify a preferred family and fallbacks, while an explicit font resource can make a chosen family available to the renderer. Confirm which font is selected and inspect the resulting PDF.

Does ScreenshotNeo convert arbitrary Python HTML strings?

The API captures a URL and can return a PDF. For direct conversion of an HTML string generated inside Python, use a library workflow such as the WeasyPrint example above.