ScreenshotNeo

BlogHow-to

Fix PHP Browsershot Screenshots of Indian Websites with Missing Rupee Symbols

If ₹ is missing from a PHP Browsershot screenshot, check the character, CSS font selection, and fonts available to headless Chrome—in that order.

By the ScreenshotNeo team4 October 20268 min read

If the Indian rupee symbol is missing in a PHP Browsershot screenshot, check these three things in order: the character is U+20B9, the affected text uses a font with that glyph, and the headless Chrome process launched by Browsershot can access that font. A font installed on your laptop—or a font named in CSS—does not prove the screenshot process can render it.

Browsershot uses Puppeteer and headless Chrome to render pages. Diagnose the browser runtime that actually makes the screenshot, under the same service user and with the same Chrome executable as production. The Indian Rupee Sign is ₹, Unicode U+20B9; it is distinct from U+20A8, the older rupee character. The Unicode Standard identifies U+20B9 as the official Indian rupee currency symbol introduced in 2010.

1. Confirm which character the page contains

First establish whether the input really contains U+20B9. The visible shapes of currency symbols can be easy to confuse, and a font cannot fix text that contains the wrong code point.

<span>₹ 123</span>
<span>&#8377; 123</span>
<span>&#x20B9; 123</span>

All three examples represent the Indian Rupee Sign. If you can inspect the PHP string, check its code points rather than relying on how a terminal or editor displays it:

<?php
$text = '₹ 123';

foreach (mb_str_split($text) as $character) {
    printf("U+%04X\n", mb_ord($character, 'UTF-8'));
}

The rupee sign should report as U+20B9. If it reports U+20A8, replace the character with U+20B9. If the source is correct but the rendered DOM is not, trace the data transformation or template that creates the page before investigating fonts.

2. Reproduce the problem with a minimal Browsershot page

Reduce the page to the affected symbol and a known font stack. Run the capture through the same PHP service, container, operating-system image, browser executable, and service account as the failing screenshot. That removes site layout and application code from the first diagnostic.

<?php

require __DIR__ . '/vendor/autoload.php';

use Spatie\Browsershot\Browsershot;

$html = '<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    body { font-family: Arial, sans-serif; font-size: 48px; }
  </style>
</head>
<body>₹ 123</body>
</html>';

Browsershot::html($html)
    ->windowSize(500, 160)
    ->save(__DIR__ . '/rupee-diagnostic.png');

Use a UTF-8 source file and include the charset declaration. If this minimal image shows a blank, box, or replacement mark, focus on font coverage or browser environment. If it works, compare the failing page’s computed font, web-font loading, CSS, and capture timing.

3. Check the computed font and font loading

In the failing page, inspect the element containing the amount and determine which font Chrome actually selected. Check the computed font-family, whether the intended web font request succeeds, and whether the font has finished loading before the screenshot begins. A CSS font-family name is only a preference; it does not guarantee that the font is installed, loaded, or contains U+20B9.

For a quick CSS experiment, inject a font stack into the capture. Browsershot supports adding styles for image captures; the injection can test whether changing CSS selection helps, but it cannot install a font or add a missing glyph to a font file.

<?php

use Spatie\Browsershot\Browsershot;

Browsershot::url('https://example.com/invoice')
    ->setOption('addStyleTag', ['content' => '
        body, body * {
            font-family: "Known Rupee-Capable Font", sans-serif !important;
        }
    '])
    ->save(__DIR__ . '/invoice.png');

Replace the example font with one whose U+20B9 coverage and deployment license you have verified. This experiment is useful only if that font is available to Chrome. For a remote web font, confirm its request succeeds and that capture waits for it to load; for a local font, confirm the Chrome runtime can see the installed or bundled font.

4. Make a rupee-capable font available to headless Chrome

If the selected font lacks U+20B9, choose a font with verified coverage and make it available to the exact runtime used by Browsershot. You can install a suitable font in the operating-system image or bundle and load it as part of the application, depending on your deployment. Check the font’s license for your use and redistribution model.

There is no single installation command that applies to every deployment: the correct package name and font-cache steps depend on the base operating system and container image. Check the target image’s package manager and font configuration rather than copying a command for a different distribution. After changing the image or installed fonts, rebuild or restart the relevant runtime as needed, then repeat the minimal capture as the application’s service user.

5. Verify the browser executable and runtime

Confirm that the executable launched by Browsershot is the one you expect. A system Chrome, a bundled Chromium, and a browser inside a container can have different access to fonts. Browsershot documents ways to select a Chrome or Chromium executable and pass Chromium arguments. Its current v4 requirements page lists Node 22.0 LTS or higher and Puppeteer 23.0 or higher; check the requirements for the version installed in your project because package requirements can change.

Start by checking the installed versions and Browsershot configuration in the application environment. If your deployment uses a custom browser path, confirm it points to the same browser you are inspecting. Then render the diagnostic page with that executable and the same account that serves requests. For package setup and configuration details, see the Browsershot v4 requirements and the image usage documentation.

6. Separate glyph coverage from font rendering quality

A missing character and a poorly rendered character are different problems. If the symbol is absent or replaced by an empty box, first verify U+20B9 coverage and font availability. Spatie documents font-render-hinting: none as an option that may help font-rendering issues on some Linux distributions. That flag adjusts rendering behavior; the documentation does not describe it as supplying a missing glyph.

Troubleshooting common failures

Symptom Likely cause What to check or change
The page shows a box or blank space where ₹ belongs The selected font lacks U+20B9, or Chrome cannot access a font that has it. Verify the character and computed font. Make a font with verified coverage available to the actual Chrome runtime, then repeat the minimal capture.
The HTML source looks correct, but the screenshot is wrong The rendered DOM may differ, a CSS rule may select another font, or a web font may not have loaded. Inspect the rendered element, computed font, and font network requests. Check capture timing and compare against the minimal page.
The test works locally but fails in production The local and production browser environments differ in fonts, executable, user, or container image. Run the diagnostic under the production service user and browser executable. Check the deployment image for the chosen font.
A CSS override has no effect The font may not be installed or loaded, the rule may not match the element, or the font may not contain U+20B9. Inspect the computed style and font requests. Treat CSS injection as a selection test, not a font installation mechanism.
A remote font works in an interactive browser but not in the screenshot The font request may fail or capture may begin before it finishes. Check the request from the headless browser and wait for the page’s font-loading work to complete before capture.
The symbol looks rough or misaligned but is present This may be a font-rendering or layout issue rather than missing glyph coverage. Check font size, line height, and rendering behavior. Consider the documented Linux hinting option only for rendering quality, not missing coverage.

Performance, reliability, and deployment notes

  • Use a minimal capture first. It narrows the issue to character data, CSS selection, font loading, or runtime availability without the noise of a full page.
  • Keep the runtime reproducible. Install or bundle fonts as part of the deployment image or application setup, and run captures with the same browser and account used in production.
  • Account for remote font loading. A page that depends on a remote font also depends on that request completing successfully before capture. A local, deployed font avoids that network dependency when it fits your licensing and packaging needs.
  • Retest after deployment changes. Font installation and browser selection are environment-level changes. Validate the screenshot in the rebuilt or restarted runtime, not only in a developer session.
  • Do not infer a universal cause. Missing-font coverage is a plausible explanation for a missing glyph, but the character, CSS, loading, browser, and runtime each need to be checked.

Or skip the browser setup

If you need a screenshot without maintaining a headless browser environment, ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return an image or PDF; its clean-shot flow accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. AI agents can use its MCP tools to take screenshots, get page information, or capture PDFs.

For a normal page screenshot, request an image from the API. The API supports PNG, JPEG, and WebP output. See the ScreenshotNeo API documentation for available parameters and configuration.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card. Paid plans start at $5 for 3,000 screenshots. Sign up for the free plan.

FAQ

Is the correct Indian rupee symbol U+20B9?

Yes. Use ₹ or its numeric HTML reference &#8377; / &#x20B9;. U+20A8 is a different, older rupee character.

Will adding a font-family rule install a font?

No. It changes the requested font selection. The font must also be available to Chrome and include the needed glyph.

Should I add font-render-hinting: none to fix a blank rupee sign?

Not as the first fix. It is documented as a possible rendering-quality option on some Linux distributions, not a way to provide missing U+20B9 coverage.

Which exact font package should I install?

That depends on your operating-system image and the font’s verified glyph coverage and license. Check those details for the target runtime before choosing a package.