The reliable fix is to install an emoji-capable font in the same Ubuntu host or container that runs Chromium, then wait for the page’s fonts before capturing. On Debian or Ubuntu, install fonts-noto-color-emoji, rebuild any Docker image, verify that fontconfig can see /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf, and test the exact emoji sequences your application uses.
If a custom @font-face is involved, a file existing on disk is not enough. The page must be able to fetch the font through an accessible file:// or HTTP(S) URL, and Puppeteer must await document.fonts.ready before taking a screenshot or generating a PDF.
Why Puppeteer shows blank emoji boxes
Puppeteer delegates text shaping and painting to Chromium. Chromium can only render an emoji when the browser process can find a font containing the required glyphs and sequences. A minimal host installation, a slim container, or a font request blocked by the page origin can therefore produce tofu (empty square boxes), missing characters, or monochrome fallback symbols.
There are several independent failure points:
- The emoji font is not installed in the runtime where Chromium actually runs.
- Fontconfig has not refreshed its cache, or the browser runs as a different user or in a different container.
- An
@font-faceURL points at a filesystem path that the document is not allowed to fetch. - The capture starts before web fonts finish loading.
- The font contains some characters but not the variation selectors, skin-tone modifiers, flags, or zero-width-joiner sequences used by the page.
- The Chromium build handles bitmap or COLRv1 color fonts differently for screenshots and PDFs.
Fix these in order rather than changing CSS at random.
#1 Best Overall
Install Noto Color Emoji in Ubuntu
Ubuntu or Debian host
Ubuntu’s fonts-noto-color-emoji package is the standard starting point. Install it and refresh fontconfig:
sudo apt-get update
sudo apt-get install -y fonts-noto-color-emoji fontconfig
sudo fc-cache -f -v
fc-list | grep -i 'Noto Color Emoji'
A successful installation should expose the font at /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf. The fc-list command is more useful than checking the package database alone because it confirms that fontconfig, which Chromium consults, can see the font.
Docker image
Install the packages in the image that launches Puppeteer, not merely on your development workstation. Rebuild the image after changing the Dockerfile:
FROM node:22-bookworm
RUN apt-get update
&& apt-get install -y --no-install-recommends
fonts-noto-color-emoji fontconfig
&& fc-cache -f -v
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "capture.js"]
Run fc-list inside the finished image and as the same runtime user used by your service. If a multi-stage build, Kubernetes sidecar, or remote browser is involved, identify the process that owns Chromium and install the font there. Puppeteer’s Debian/Ubuntu browser dependencies are separate from the emoji font; a browser that cannot start still needs the normal shared libraries and sandbox configuration.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsVerify with a minimal page before testing your application
Use a page that exercises ordinary emoji, a variation selector, a flag, and a zero-width-joiner sequence:
Rank #2
<!doctype html>
<meta charset="utf-8">
<style>
.emoji {
font-family: "Noto Color Emoji", sans-serif;
font-size: 48px;
}
</style>
<p class="emoji">😀 😍 🚀 ❤️ 🏳️🌈 👍🏽 🇺🇳 👨👩👧👦</p>
Save it as emoji-test.html. If this page fails, stay at the operating-system and Chromium layer. If it succeeds while your application fails, inspect application CSS, font requests, and the exact code points in the failing content.
Capture only after Chromium has loaded fonts
This complete Puppeteer example uses a local document, logs browser and network errors, waits for the font set, and writes both a PNG and a PDF:
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
try {
const page = await browser.newPage();
page.on('console', message => console.log('[browser]', message.text()));
page.on('requestfailed', request => {
console.error('[request failed]', request.url(), request.failure()?.errorText);
});
const htmlPath = path.resolve('emoji-test.html');
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'emoji.png', fullPage: true });
await page.pdf({
path: 'emoji.pdf',
printBackground: true,
format: 'A4'
});
} finally {
await browser.close();
}
})();
Run it with a Puppeteer installation compatible with the Chromium binary you launch. The same document.fonts.ready wait belongs immediately before the real screenshot or PDF call, after any route changes or DOM updates that introduce new font faces.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Make custom @font-face URLs fetchable
A TTF can be present on disk and still be invisible to the page. CSS does not turn an arbitrary server filesystem path into a fetchable resource. The document origin must be allowed to request the URL.
Use a file origin consistently
When both the HTML and font are local, navigate to the HTML with a file:// URL and reference the font with a matching accessible URL. The example above uses pathToFileURL instead of manually concatenating slashes, which avoids malformed paths.
Rank #3
Prefer HTTP(S) for application pages
Serve the HTML and font from the same local HTTP(S) origin during capture, or configure the server’s cross-origin policy deliberately when they are on different origins. Then inspect the browser console and failed requests for blocked, malformed, or cross-origin font URLs.
Be careful with page.setContent()
page.setContent() creates an in-memory document whose origin may be about:blank. A relative or local filesystem font URL can consequently be blocked even though the file exists. Navigate to a served URL instead, or embed the font as a data URL when your policy permits it, and still await document.fonts.ready.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Diagnose partial emoji coverage
“Some emoji work” does not prove the installation is complete. Test categories separately so you can distinguish missing glyphs from shaping or color-font behavior:
| Test | What it exercises | Typical implication when it fails |
|---|---|---|
| 😀 or 🚀 | Single Unicode code point | Basic font discovery or package problem |
| ❤️ | Variation-selector presentation | Variation-selector coverage or shaping difference |
| 👍🏽 | Emoji plus skin-tone modifier | Modifier coverage or sequence shaping |
| 🇺🇳 | Regional-indicator pair | Flag sequence support |
| 👨👩👧👦 or 🏳️🌈 | Zero-width-joiner sequence | Complex shaping or incomplete sequence coverage |
Rendering can vary with Ubuntu release, fontconfig rules, and Chromium version. Noto Color Emoji uses the CBDT/CBLC bitmap color-font format, while Chromium also has separate handling for other color-font formats. Validate the exact browser build and artifact you ship instead of assuming that a successful browser tab guarantees an identical PDF.
Troubleshoot in a fixed order
- Identify the runtime. Confirm the Puppeteer process, Chromium executable, container, and runtime user. Installing a font on the host does nothing for a browser in another image.
- Inspect fontconfig there. Run
fc-list | grep -i 'Noto Color Emoji'inside that runtime and verify the expected TTF path. - Confirm Chromium dependencies. If the browser crashes, exits immediately, or never creates a page, resolve the normal Debian/Ubuntu shared-library and sandbox requirements before diagnosing glyphs.
- Run the minimal system-font page. A failure here points to installation, cache, fontconfig, or color-font support rather than your application.
- Check requests for custom fonts. Log
requestfailed, browser console messages, and network responses. A blocked local or cross-origin URL must be made accessible to the document. - Wait at the last possible moment. After navigation and any JavaScript that changes classes or content, await
document.fonts.readyimmediately before capture. - Compare exact sequences. Record which code points fail and test single characters, modifiers, flags, and ZWJ sequences independently.
- Compare output types. Open the PNG and PDF produced from the same page. If only one fails, investigate that artifact’s Chromium color-font path and print settings.
- Rebuild and retest CI. Pin the container image and browser version used in production, rebuild after package changes, and keep the diagnostic page as a smoke test.
Performance and reliability considerations
Font discovery and shaping happen inside each browser runtime. Keeping a warm browser can avoid repeated startup cost, but it does not replace waiting for fonts after a new page or navigation. A deterministic image should use the same viewport, device scale factor, Chromium build, Ubuntu image, and font package in development and CI.
Rank #4
Full-page screenshots may trigger lazy content while the page is laid out; PDFs can apply print media and pagination, so validate both if you deliver both. Do not treat a cached screenshot as proof that a newly deployed font is active: rebuild the image, clear any relevant browser cache, and rerun the diagnostic page.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Or skip the browser setup
For a public URL where you need a production screenshot rather than a local Puppeteer debugging session, ScreenshotNeo makes one GET request and returns PNG, JPEG, WebP, or PDF. It accepts the consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and whether the request was billed.
Use the API documentation at https://screenshotneo.com/docs/. The basic call is:
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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
require('node:fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also supports full-page and element captures, dark mode, 12 device presets or custom viewports, retina scale, PDF paper settings and page ranges, custom CSS and JavaScript, click and wait actions, selector hiding, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous webhooks, up to 100 URLs per bulk call, a usage API, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing gives two months free, and every feature is included on every plan. This service is not a substitute for fixing a local custom-font or PDF-rendering bug, but it removes the Ubuntu browser setup when the target is an accessible web URL. Sign up free for ScreenshotNeo.
FAQ
Does the font need to be installed on my laptop?
No. It must be installed and visible to the operating system and fontconfig used by the Chromium process that performs the capture.
Best Value
Why can a browser screenshot pass while my PDF fails?
Screenshot and PDF output can exercise different Chromium color-font and print paths. Validate the artifact your application actually distributes, not only an interactive tab.
Can a successful fc-list result prove that every emoji is supported?
No. It proves discovery of the font, not complete coverage of variation selectors, modifiers, flags, or joined sequences. Keep representative tests for the emoji set your product uses.
Frequently Asked Questions
Does the font need to be installed on my laptop?
No. It must be installed and visible to the operating system and fontconfig used by the Chromium process that performs the capture.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallWhy can a browser screenshot pass while my PDF fails?
Screenshot and PDF output can exercise different Chromium color-font and print paths. Validate the artifact your application actually distributes, not only an interactive tab.
Can a successful fc-list result prove that every emoji is supported?
No. It proves discovery of the font, not complete coverage of variation selectors, modifiers, flags, or joined sequences. Keep representative tests for the emoji set your product uses.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




