ScreenshotNeo

BlogHow-to

How to Fix Playwright Screenshots of Indian Websites with Broken Devanagari Glyphs

Broken Hindi text in Playwright screenshots usually points to missing fonts, failed web-font loading, or environment differences. Diagnose the rendering path and make CI captures reproducible.

By the ScreenshotNeo team4 October 20269 min read

Broken Devanagari glyphs in a Playwright screenshot are usually a font-rendering problem, not an image-encoding problem. Check the exact browser and operating-system environment, confirm a Devanagari-capable font is installed or the site’s web font loaded, and wait for the page’s fonts and application to be ready before capturing. For Ubuntu-based Linux CI, installing fonts-noto-core provides Noto Sans Devanagari and Noto Serif Devanagari. Then pin the capture environment so visual comparisons use the same browser, OS image, and font files.

1. Classify the rendering symptom

Start with the text as rendered in the browser. Missing-glyph boxes often indicate that the selected font lacks the needed characters or was not loaded. Separated, misplaced, or visibly malformed marks can point to font selection, shaping, or a difference in the rendering stack. These are clues, not proof: inspect the actual page and environment before deciding on a cause.

  1. Capture the same text in a normal browser session and in the Playwright runtime.
  2. Check whether the issue affects all Devanagari text or only text using a particular component or font.
  3. Record the exact string and inspect the element’s computed font-family.

If both browsers show the same problem, investigate the page’s CSS and font delivery. If only the Playwright environment differs, focus on its installed fonts, browser build, and headless configuration.

2. Record the Playwright runtime

Before changing fonts, identify the environment that produced the screenshot. Record the OS and container image version, Playwright package version, browser engine and exact browser build or channel, and whether capture is headed or headless. Playwright supports Chromium, Firefox, and WebKit, and they need not render identically. Its visual comparison guidance notes that rendering can vary with the host OS, browser version, settings, hardware, power state, and headless mode. Use the same environment when generating and comparing baselines. Playwright visual comparisons and Playwright browser documentation describe these environment considerations.

3. Install Devanagari fonts in Linux CI

A font installed on a developer’s workstation is not automatically available inside a CI container. For Ubuntu 24.04, the archive’s fonts-noto-core package includes regular and bold Noto Sans Devanagari and Noto Serif Devanagari files. Ubuntu package details list the package, and its file list shows the Devanagari font files.

Add the font package to the image setup before launching the browser. For an Ubuntu-based image where package installation is available, the commands are:

apt-get update
apt-get install -y fonts-noto-core
fc-cache -f -v

To check the installed files and font family names in that environment, use:

fc-list | grep -i 'Noto.*Devanagari'

Package names and available font files vary by distribution and base image. Check the package index and file list for your actual runtime rather than assuming the Ubuntu package name applies everywhere. Rebuild the container or runner image after changing its font packages; installing fonts on a separate machine will not affect the browser process in CI.

4. Check the requested and loaded web font

A page can declare a Devanagari web font but still render with a fallback if the font request fails, is blocked, or has not completed by capture time. Inspect the affected element’s computed font-family, the relevant @font-face rules, and failed network requests for font resources. A Playwright issue report illustrates blocked requested fonts followed by system-font fallback, but that report is one example, not evidence that every broken capture has this cause. Playwright issue example.

Wait for the page’s own application-ready condition and for the browser’s font-loading set before taking the screenshot. Also inspect failed requests so a font delivery failure is not mistaken for a timing issue. The exact readiness condition depends on the application; a generic font wait alone cannot guarantee that every application component or dynamically inserted font is ready.

import { test, expect } from '@playwright/test';

test('capture page after fonts are ready', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.evaluate(() => document.fonts.ready);
  // Replace this with an application-specific ready condition if needed.
  await expect(page.locator('main')).toBeVisible();
  await page.screenshot({ path: 'page.png', fullPage: true });
});

This example assumes the project uses the Playwright Test runner and that the page has a visible main element. Adapt the URL and readiness check to the application. If a font is loaded only after a user action or component render, trigger that action or wait for the specific component before capture.

5. Inspect fonts and requests with a runnable diagnostic

The following Playwright Test example records the computed font stack, whether the browser’s font-loading set is ready, and failed requests. It does not prove which physical font rendered each glyph; use browser developer tools and a controlled comparison to investigate further.

import { test } from '@playwright/test';

test('diagnose Devanagari rendering', async ({ page }) => {
  page.on('requestfailed', request => {
    console.log('REQUEST FAILED', request.url(), request.failure()?.errorText);
  });
  page.on('response', response => {
    if (!response.ok() && /\.(woff2?|ttf|otf)(\?|$)/i.test(response.url())) {
      console.log('FONT RESPONSE', response.status(), response.url());
    }
  });

  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  const details = await page.locator('YOUR_DEVANAGARI_SELECTOR').evaluate(async element => {
    await document.fonts.ready;
    const style = getComputedStyle(element);
    return {
      text: element.textContent,
      fontFamily: style.fontFamily,
      fontWeight: style.fontWeight,
      fontStyle: style.fontStyle,
      fontsStatus: document.fonts.status,
      checkNotoSans: document.fonts.check('16px "Noto Sans Devanagari"'),
      checkNotoSerif: document.fonts.check('16px "Noto Serif Devanagari"')
    };
  });
  console.log(details);
  await page.screenshot({ path: 'diagnostic.png' });
});

Replace YOUR_DEVANAGARI_SELECTOR with a selector for the affected text. document.fonts.check() reports whether text can be rendered without pending font loads for that font specification; it is not a universal guarantee that every character was drawn from the intended face. Confirm the result visually and check the font’s network response and the runtime’s installed files.

6. Check Chromium defaults without relying on them

Chromium’s Linux default font mappings can depend on the browser build. A Chromium source change dated 2026-03-04 added explicit Devanagari mappings to Noto Sans Devanagari for standard and sans-serif, and Noto Serif Devanagari for serif. The change description says that before the change Chromium had no configured Devanagari defaults and relied on generic system font resolution. This does not mean every installed Playwright Chromium includes the change or that the fonts are present in its runtime image. Check the exact browser build and font files used by CI. Chromium Linux font fallback source.

Where you control the page CSS, specify an intentional stack with a Devanagari-capable face and suitable fallbacks. For example:

body {
  font-family: "Noto Sans Devanagari", "Noto Sans", sans-serif;
}
.serif-content {
  font-family: "Noto Serif Devanagari", "Noto Serif", serif;
}

This only helps if the named fonts are available or successfully delivered as web fonts. A CSS family name does not install a font.

7. Isolate the difference with controlled comparisons

Change one variable at a time. Compare the same page and text while holding the other conditions fixed:

Comparison What to keep fixed What it can reveal
System font vs. site web font Browser build, OS image, page state Whether the issue follows the font asset or system fallback
Chromium vs. Firefox or WebKit OS image, page, font files, capture state Whether the behavior differs by engine
Headed vs. headless Browser build, OS image, page, fonts Whether capture mode is part of the difference
Local workstation vs. CI As many versions and assets as possible Which runtime inputs differ

These comparisons help narrow the cause; they do not guarantee a single diagnostic outcome. If all engines fail, check font assets and CSS first. If one engine differs, investigate its build, font availability, and fallback behavior.

8. Make visual baselines reproducible

  • Pin the container or OS image used for screenshot generation.
  • Pin the Playwright package and browser version used by the project.
  • Install the same font packages in every screenshot runner.
  • Wait for fonts and application-specific readiness before capture.
  • Store the exact text, computed font stack, failed font requests, browser build, and OS image with a failure report.

Playwright’s screenshot comparison waits until two consecutive screenshots match before capturing a stable result. This can reduce timing-related variation within a run, but it cannot make different operating systems or font stacks equivalent. Include browser and platform information in snapshot naming when maintaining multiple environments. Playwright snapshot guidance.

9. Troubleshooting common failures

Symptom Likely cause to investigate Next step
Glyphs appear as empty boxes Selected font lacks coverage, or a required font did not load Check computed font family, font files in the runtime, and font network requests; install or serve a Devanagari-capable face.
Text looks different in CI than locally Different OS image, browser build, installed fonts, or capture mode Record and pin those inputs, then compare in one controlled environment.
Site font is declared but system-looking text appears Font request failed, was blocked, or capture happened before it was ready Inspect failed requests and response status; await font and app readiness.
fc-list shows no Devanagari face Font package is absent from the browser’s runtime image Install the distribution’s package containing Devanagari fonts and rebuild the image.
Only some weights look wrong The requested weight may not be available, or the matching web-font file did not load Check font-weight, @font-face declarations, and network responses for each font file.
Waiting for document.fonts.ready changes nothing The asset may have failed, the requested family may be wrong, or the app has not rendered the affected text Check font errors, computed styles, and the application’s readiness condition; verify the installed fallback.
One browser engine differs from another Engine build, platform font fallback, or font availability differs Compare exact builds and font files on the same OS image; avoid treating cross-engine pixels as interchangeable.

For a useful bug report, attach the screenshot and exact text string, computed font-family, font request results, OS or container image, Playwright version, browser engine and build, and headed or headless mode. Without these details, the screenshot alone usually cannot identify the root cause.

10. Performance, reliability, and cost considerations

Installing fonts in the runner image adds setup and image contents, but putting them in a reusable image avoids repeating package installation for each capture. Waiting for font readiness can add time when fonts are slow; diagnose failed delivery instead of masking it with an arbitrary long delay. Pinning the image and browser improves consistency, while upgrades should be treated as baseline changes and reviewed deliberately. These steps improve reproducibility; they do not guarantee identical rasterization across different operating systems or browser engines.

Or skip the browser setup

If you need a screenshot without maintaining the browser and font setup, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns an image or PDF. Its clean-shot flow accepts cookie or 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 and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server includes take_screenshot, get_page_info, and capture_pdf tools for MCP clients such as Claude and Cursor. The service provides 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000 screenshots. See the ScreenshotNeo API documentation.

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));

Replace the example URL and provide your API key. For browser-controlled rendering and font diagnosis, keep the Playwright workflow above; for a direct screenshot request, use ScreenshotNeo’s API.

Sign up for 1,000 free screenshots a month with no card.

Frequently asked questions

Does this issue affect only Hindi?

No. The same font-coverage and loading checks apply to other Devanagari text, including Marathi and Nepali.

Will installing Noto fonts fix every malformed glyph?

No. It addresses missing system font coverage when that is the cause. Incorrect CSS, failed web fonts, text content, browser differences, or capture timing can still produce problems.

Should I change the screenshot image format?

Usually not as a first fix. Diagnose the browser’s rendered text and effective fonts before changing PNG, JPEG, or WebP output settings.