Back to Knowledge Hub
Fingerprint

Text Measurement Fingerprinting: Sub-Pixel Tracking

Canvas measureText() exposes sub-pixel metrics that vary with fonts and browser setups. Compare this signal with Canvas and understand BotBrowser profile limits.

BotBrowser Team

Documentation

Want the structured docs for Fingerprint?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

BotBrowser can align supported text and Canvas metrics with a loaded profile, but it cannot control site collection or promise identical values across every browser, font, and graphics setup.

Text measurement inputs and controlled comparison boundaries

TL;DR

Text measurement exposes numeric width and bounding-box values that depend on the active browser, fonts, locale, and graphics path. A loaded BotBrowser profile can make supported measurements more repeatable within documented deployment conditions, but it cannot control site collection or promise identical output in every environment.

Contents

  • How text measurement becomes a fingerprint signal
  • Profile-aligned configuration and verification
  • Practical limits, FAQ, and public references

Introduction

When a website measures the width of a text string using the Canvas API's measureText() method, the result is a floating-point value with sub-pixel precision. These values vary across operating systems because each platform uses a different built-in rendering engine with its own internal arithmetic model. The differences are tiny, invisible to the human eye, but they are consistent, reproducible, and measurable through JavaScript.

This sub-pixel precision makes measureText() a useful browser-observable signal. Unlike Canvas pixel rendering, it returns numeric metrics directly, so developers can compare widths, ascent, and descent values without reading pixels. Results are not a universal operating-system identifier: fonts, browser versions, locale, graphics settings, and execution mode all matter.

This makes text measurement a browser-observable signal that fingerprinting scripts may collect across many kinds of sites. It requires no special permissions and generates no user-visible behavior, but its stability depends on the browser session and deployment. As browsers improve protections for other fingerprinting vectors, text measurement receives attention because API-level changes can affect web application behavior.

BotBrowser's Profile-Aligned Approach

BotBrowser uses the loaded fingerprint profile to align supported text and Canvas metrics with the profile's declared environment. The browser still exposes the standard APIs; exact behavior depends on the release, font set, graphics stack, and run mode.

Unified Font Rendering Path

Profile settings are applied to the browser's text-rendering path so that measurements can be compared against the declared environment. Treat the result as a documented comparison target, not identical output on every host or release.

When a profile specifies a device configuration, use measureText() and Canvas drawing together to check whether the deployment is within the profile's documented behavior. Host fonts and the browser build remain part of that result.

Profile-Driven Consistency

Each BotBrowser fingerprint profile describes supported text-metric behavior for a declared environment. When the profile is loaded, measurements can be compared against that declaration:

  • The same text and font settings can be compared across runs when the profile, browser build, fonts, locale, and execution mode are held constant.
  • The same profile can keep measureText() values within the documented profile policy across macOS, Windows, and Linux; verify exact values for the release and font set you deploy.
  • The results are internally consistent with other font-related signals (font list, Canvas text rendering, CSS layout metrics).

Check each TextMetrics property used by your application. Support and values for actualBoundingBoxAscent, actualBoundingBoxDescent, fontBoundingBoxAscent, fontBoundingBoxDescent, and left/right bounds can vary with browser release, fonts, and graphics mode.

Cross-platform consistency and its limits

The goal is stable, profile-aligned results across supported platforms. Exact floating-point values can still depend on the browser release, fonts, locale, graphics stack, and execution mode, so treat a cross-platform comparison as a validation result rather than a universal guarantee.

This comparison is meaningful only when the deployment variables are controlled. Extensions, API proxies, and JavaScript wrappers cannot determine the exact output of the site's browser, so validate the complete setup instead.

BotBrowser uses the profile as a declared reference for supported behavior. Host fonts, browser release, graphics path, and execution mode can still affect the output, so a controlled comparison is required.

Observable behavior and privacy boundaries

The relevant observable behavior is the standard Canvas API:

  • measureText() returns native JavaScript Number values with full double-precision floating-point precision through the standard API.
  • Values should be checked against Canvas rendering and CSS layout in the same deployment; small differences can be legitimate.
  • BotBrowser does not stop a site from collecting or transmitting these values, and it does not decide how a site stores or interprets them.

Internal Consistency with Other Signals

Text metrics should be reviewed with other observable signals such as Canvas output, font availability, and CSS layout. A profile can improve consistency within its documented scope, but it cannot make unrelated site signals agree or guarantee a site's interpretation.

Configuration and Usage

CLI Usage

Launch BotBrowser with a fingerprint profile to enable text measurement protection:

chrome --bot-profile="path/to/profile.enc" \
       --user-data-dir="$(mktemp -d)"

For deterministic results across runs, add a noise seed:

chrome --bot-profile="path/to/profile.enc" \
       --bot-noise-seed=12345 \
       --user-data-dir="$(mktemp -d)"

Combine with timezone, locale, and proxy settings for comprehensive identity consistency:

chrome --bot-profile="path/to/profile.enc" \
       --bot-noise-seed=12345 \
       --proxy-server="socks5://user:pass@proxy:1080" \
       --bot-timezone="America/New_York" \
       --bot-locale="en-US" \
       --bot-languages="en-US,en"

Playwright Integration

const { chromium } = require('playwright-core');

(async () => {
  const browser = await chromium.launch({
    executablePath: 'path/to/botbrowser/chrome',
    args: ['--bot-profile=path/to/profile.enc', '--bot-noise-seed=42'],
    headless: true,
  });

  const context = await browser.newContext({ viewport: null });
  const page = await context.newPage();

  await page.goto('https://example.com');

  // measureText() results will be consistent with the loaded profile
  const metrics = await page.evaluate(() => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.font = '16px Arial';
    const measurement = ctx.measureText('Sample text for measurement');
    return {
      width: measurement.width,
      actualBoundingBoxAscent: measurement.actualBoundingBoxAscent,
      actualBoundingBoxDescent: measurement.actualBoundingBoxDescent,
    };
  });

  console.log('Text metrics:', metrics);

  await browser.close();
})();

Puppeteer Integration

const puppeteer = require('puppeteer-core');

(async () => {
  const browser = await puppeteer.launch({
    executablePath: 'path/to/botbrowser/chrome',
    args: ['--bot-profile=path/to/profile.enc', '--bot-noise-seed=42'],
    headless: true,
    defaultViewport: null,
  });

  const page = await browser.newPage();
  await page.goto('https://example.com');

  // Verify text measurement from the loaded profile
  const metrics = await page.evaluate(() => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.font = '16px Arial';
    const m = ctx.measureText('Sample text for measurement');
    return { width: m.width };
  });

  console.log('Text width:', metrics.width);

  await browser.close();
})();

Practical fixture

Keep a small, local fixture so a change in a metric has an explainable cause. Wait for web fonts, set every relevant Canvas state explicitly, and record raw values rather than rounding them:

await page.evaluate(async () => {
  await document.fonts.ready;
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d', { alpha: false });
  ctx.font = '16px Arial';
  ctx.direction = 'ltr';
  ctx.textAlign = 'start';
  ctx.textBaseline = 'alphabetic';
  const sample = 'Aa 123, مرحبا, 世界';
  const metrics = ctx.measureText(sample);
  return {
    sample,
    font: ctx.font,
    width: metrics.width,
    actualBoundingBoxAscent: metrics.actualBoundingBoxAscent,
    actualBoundingBoxDescent: metrics.actualBoundingBoxDescent,
    devicePixelRatio: window.devicePixelRatio,
    visualViewportScale: window.visualViewport?.scale ?? 1,
  };
});

Run the fixture on a control browser and on the profiled deployment with the same browser build, font files, locale, zoom, and headed/headless mode. A changed value is evidence to investigate, not proof that a site can or cannot identify a user.

Verification

After launching BotBrowser with a profile, verify that text measurement protection is active:

const width = await page.evaluate(() => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = '16px Arial';
  return ctx.measureText('Verification string').width;
});
console.log('Text width:', width);

What to check:

  1. The width value is a full-precision floating-point number; compare it with the recorded fixture and deployment baseline.
  2. Reloading the page and repeating the measurement produces the same value.
  3. Restarting the browser with the same profile and noise seed produces the same value.
  4. Compare the result with your own controlled Canvas and CSS checks; third-party test pages may change independently.

Best Practices

Always Use a Complete Profile

Text measurement protection works as part of BotBrowser's complete fingerprint profile. The profile can align supported text metrics with related browser signals within documented conditions; exact values still depend on the browser release, fonts, graphics path, and execution mode. Loading a profile is required for supported text-measurement behavior to be active.

Combine with Network Identity

Text metrics identify the platform. Network signals identify the location. For consistent identity, combine the fingerprint profile with appropriate proxy and locale settings:

chrome --bot-profile="path/to/profile.enc" \
       --proxy-server="socks5://user:pass@proxy:1080" \
       --bot-timezone="Europe/London" \
       --bot-locale="en-GB" \
       --bot-languages="en-GB,en"

Use Deterministic Mode for Testing

When running automated tests or research experiments, use --bot-noise-seed to make results reproducible. This lets you check whether text metrics remain stable across test runs and detect regressions in metric consistency.

Test with Multiple Font Configurations

When verifying protection, test with a variety of font families and sizes. Include both common web fonts and system defaults in your test set. This helps cover the profile's text-metric behavior across the fonts that tracking scripts may probe.

Keep Profiles and BotBrowser Updated

Text measurement fingerprinting continues to evolve. New TextMetrics properties have been added in recent browser versions, and tracking scripts update their collection to include these new signals. Keep your BotBrowser installation and profiles updated to ensure coverage of newly exploitable properties.

A useful measurement check is a small, local fixture rather than a one-line digest. The fixture should record the input and the conditions that produced it, so a change can be investigated without guessing. Wait for document.fonts.ready before measuring web-font text. Record the complete CSS font shorthand, the language and direction, the browser build, the profile identifier, and whether the page is headed or headless. Keep the raw decimal values for every TextMetrics field that the application uses. Rounding a value for display is fine, but rounding before comparison can hide a meaningful difference.

The font family is an input, not decoration. If a requested family is unavailable, fallback selection changes glyph advances and can change every downstream metric. The same family name can resolve to different files on two machines. A remote web font can also be blocked by permissions, content-security policy, network policy, or a transient failure. A failed load and a successful fallback are different test outcomes, so record the font-loading state with the measurement rather than treating both as the same browser identity.

Language-sensitive shaping is another input. Compare Latin, Cyrillic, Arabic, and CJK samples separately when those scripts matter to the product. Include punctuation, combining marks, emoji, and bidirectional text only when they represent real application content. A short, versioned corpus is easier to review than one opaque digest. It also makes a regression explainable: reviewers can see which sample changed and whether the change is related to shaping, fallback, or a browser update.

Page zoom and device pixel ratio affect the surrounding rendering context. measureText() reports coordinates in the current Canvas coordinate system, while a pixel comparison also observes the backing-store dimensions and rasterization scale. Record window.devicePixelRatio, visualViewport.scale, Canvas width and height, and the page zoom used by the fixture. Keep page zoom and operating-system scaling in separate fields because they are not interchangeable. A changed scale should not be mistaken for a changed font or a profile failure.

Reset Canvas state between probes. Set ctx.font, ctx.direction, ctx.textAlign, and ctx.textBaseline explicitly, and use the same Canvas dimensions for each run. Read the fields that are available in the target browser, including width and bounding-box values, without assuming that every release exposes every field. Run a control page and the profiled page with the same browser build, font files, locale, scale, and execution mode. This isolates the profile comparison from unrelated host changes.

Use four public layers for a practical review: the TextMetrics object, a Canvas raster of the same text, a DOM element using the same CSS font, and the browser's font-loading events. Agreement across these layers is stronger evidence than any single number. When they disagree, inspect font loading, scale, graphics mode, and locale before attributing the difference to fingerprint protection. This method uses public web APIs and works with Playwright or Puppeteer; it does not require private browser hooks or internal traces.

Keep the fixture local and minimize collected data. Do not upload raw text samples or metric vectors to a third-party dashboard unless the data is approved for that purpose. A small JSON record containing the sample label, browser build, profile version, font state, scale, and raw metrics is enough for a regression review. Retain it according to the project's privacy policy and remove captures that are no longer needed.

Build an auditable measurement check

Treat a text-metric comparison as a compatibility test, not a single pass/fail fingerprint verdict. The Canvas 2D specification defines measureText() as a measurement of shaped text in the current drawing context. Record the browser build with every fixture result because support and implementation details can change between releases.

The font shorthand is part of the input. Record style, variant, weight, stretch, size, line height, and the complete family list. Wait for document.fonts.ready before measuring web fonts, and record whether each font came from a local file, a network response, or a system fallback. Locale and text corpus matter too: keep Latin, Cyrillic, Arabic, and CJK samples separate when those scripts matter, and version the corpus.

Page scale changes the rendering context. Record devicePixelRatio, visualViewport.scale, canvas dimensions, page zoom, browser zoom, and operating-system scaling before comparing Canvas pixels. Reset Canvas state between probes, set font, direction, alignment, baseline, and supported kerning controls explicitly, then preserve raw decimal values. Run control and profiled pages with the same browser build, fonts, locale, and headed or headless mode.

Privacy controls can change whether a probe completes. Permissions, storage partitioning, anti-fingerprinting modes, blocked web-font requests, and content-security policy may prevent a font probe from completing. Report network and permission state with the metrics, keep raw samples local, and avoid transmitting metric vectors to third parties during testing.

For an operational review, compare the TextMetrics object, a Canvas raster, a DOM element using the same CSS font, and browser font-loading events. When they disagree, inspect the font resource, scale, graphics path, and locale in that order. Store observations with timestamps, profile identifiers, and browser build information so later comparisons remain auditable.

FAQ

What is text measurement fingerprinting?

Text measurement fingerprinting uses the Canvas measureText() API to compare rendering behavior. The API returns text width values with sub-pixel precision, and those values can differ between operating systems, fonts, and browser releases. A tracking script may use a collection of such signals to infer deployment characteristics, but one measurement is not a universal operating-system identifier or proof of an individual device.

How is this different from Canvas fingerprinting?

Canvas fingerprinting renders visual content (text, shapes, gradients) to a Canvas element and compares the resulting pixel data. Text measurement fingerprinting uses measureText() to get numerical metrics without requiring a pixel readback. Both techniques rely on platform-specific rendering differences, but they operate on different Canvas outputs. You can learn more about Canvas fingerprinting in our Canvas Fingerprinting guide.

Does BotBrowser produce the same values on all platforms?

BotBrowser aims to keep measureText() consistent with the loaded profile across supported platforms. The result is still bounded by the browser release, available fonts, locale, and headless or headed mode, so compare the exact deployment you plan to use instead of assuming byte-for-byte identity.

Does text measurement protection break websites?

No. BotBrowser does not block or modify the measureText() API. The API works normally, returning accurate text width values that web applications can use for layout, text truncation, chart rendering, and all other legitimate purposes. The values are simply consistent with the loaded profile rather than with the host system's native rendering.

How does this relate to font fingerprinting?

Text measurement fingerprinting is closely related to font fingerprinting but targets a different signal. Font fingerprinting checks whether specific font families are available; text measurement examines the resulting sub-pixel metrics. Both signals depend on the font and rendering environment. For a broader overview, see our article on browser fingerprinting.

Can I use BotBrowser's text measurement protection with existing automation frameworks?

Yes. BotBrowser integrates with Playwright, Puppeteer, Selenium, and other automation frameworks. Text measurement protection is automatically active when a fingerprint profile is loaded. No additional configuration or code changes are needed beyond passing the --bot-profile flag. See the Playwright getting started guide and Puppeteer getting started guide for setup instructions.

Does this protection cover all TextMetrics properties?

BotBrowser supports the documented TextMetrics properties, including width, bounding-box fields, and ascent/descent fields. Exact availability and values depend on the browser release, fonts, graphics stack, locale, and execution mode; validate the fields you use in the deployment you plan to run.

Summary

Text measurement fingerprinting uses the sub-pixel precision of the Canvas measureText() API to compare browsers by their rendering engine. Because each operating system uses a different built-in rendering engine with its own internal arithmetic model, the same text at the same font size can produce measurably different width values on each platform. Scripts may collect these differences, but their stability depends on the deployment.

BotBrowser addresses this signal by applying the loaded profile to supported text and Canvas behavior. Results should be validated with the exact browser release, fonts, locale, graphics stack, and execution mode used in production; byte-for-byte equality across hosts is not guaranteed.

Combined with BotBrowser's protection for Canvas rendering, font enumeration, and cross-platform profile consistency, text measurement protection addresses one fingerprinting signal used by tracking systems. Explore the full feature set for rendering protections, or test text measurement consistency in the Proof Center.

BotBrowser Capability and Limitations

BotBrowser can align supported Canvas measureText() behavior with a loaded profile, subject to the documented browser, font, graphics, and execution constraints. It cannot prevent sites from collecting or transmitting these values, control how they store or interpret them, or cover signals outside the profile. Repeatable comparisons require the same profile, flags, fonts, and test setup.

Practical conclusion

Before relying on text measurements, pin the browser release, profile, fonts, locale, graphics mode, and execution mode, then compare the full TextMetrics result with Canvas and CSS checks. Treat the result as one signal in a broader privacy and compatibility review.

Public Sources

#Text Measurement#measureText#Fingerprinting#Font Metrics#Canvas#Sub-Pixel#Privacy#Cross-Platform

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.