Canvas Winding Rules and TextMetrics: The Fingerprint Signals Beyond the Canvas Image
Most discussions of canvas fingerprinting stop at the image: draw something, call toDataURL, hash the result. But a browser's canvas API exposes more than pixels. Two sub-signals in particular, isPointInPath winding behavior and measureText metrics, come from the path engine and the font rasterizer rather than the bitmap.
To see why that matters, call measureText on a 48px Arial string and log actualBoundingBoxAscent on a Windows machine and on a Mac. The values can differ by a fraction of a pixel. Same code, same font name, different operating-system text stack. The canvas image can be identical while the text metrics reveal where the browser is running.
This tutorial explains what those two signals are, who reads them and why, and how to audit your own profiles for them. It is written for site owners, QA engineers and privacy-minded users, and for teams that need each browser profile to behave consistently.
What We're Learning
By the end of this walkthrough, you'll understand:
- How
isPointInPathwinding rules produce a stable signal independent of canvas image hashing - Why
TextMetricsproperties likeactualBoundingBoxAscentandfontBoundingBoxDescentvary by OS - Why per-frame canvas image noise does not touch these signals
- How to test your multi-account browser profiles for them
- What engine-level handling looks like versus JavaScript injection
You'll also have a test snippet you can run against your own profiles. To see the values your browser exposes before you start, you can open the JustBrowser fingerprint checker.
Prerequisites
- A multi-account browser with at least one configured profile
- Basic JavaScript (enough to paste code into devtools)
- Understanding of canvas fingerprinting basics. If you haven't read about WebGL fingerprinting or the wider fingerprinting picture, start there
- 20 minutes
Step 1: Understanding the isPointInPath Winding Problem
Canvas contexts have a method called isPointInPath(x, y, fillRule) that returns true if a point falls inside a drawn path. The fillRule parameter accepts two values: "nonzero" and "evenodd". These are standard SVG winding rules that determine how self-intersecting or overlapping paths get filled.
Here's the thing: rendering engines can implement the edge cases slightly differently. Not the obvious cases — a point clearly inside or outside a simple rectangle returns the same result everywhere. But consider:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Self-intersecting path
ctx.beginPath();
ctx.moveTo(50, 0);
ctx.lineTo(100, 100);
ctx.lineTo(0, 40);
ctx.lineTo(100, 40);
ctx.lineTo(0, 100);
ctx.closePath();
// Check a point right on the intersection boundary
const result1 = ctx.isPointInPath(50, 50, 'evenodd');
const result2 = ctx.isPointInPath(50.001, 50, 'evenodd');
The exact boundary handling — subpixel coordinates, antialiasing interaction, floating-point rounding — varies. And because these are path operations (not pixel operations), canvas noise injection doesn't affect them at all. You could add 100 random pixels to a canvas image and the path math would not change.
A fingerprinting script can use this. It draws a path with known problem areas, probes boundary points with both winding rules, and folds the results into its hash, which is independent of the canvas image.
Step 2: TextMetrics Carry More OS Information
The measureText method returns a TextMetrics object. Most fingerprinting discussions focus on the width property. But modern browsers expose much more:
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '48px Arial';
const metrics = ctx.measureText('Sphinx of black quartz');
console.log({
width: metrics.width,
actualBoundingBoxAscent: metrics.actualBoundingBoxAscent,
actualBoundingBoxDescent: metrics.actualBoundingBoxDescent,
fontBoundingBoxAscent: metrics.fontBoundingBoxAscent,
fontBoundingBoxDescent: metrics.fontBoundingBoxDescent,
actualBoundingBoxLeft: metrics.actualBoundingBoxLeft,
actualBoundingBoxRight: metrics.actualBoundingBoxRight
});
These values come directly from the font rasterizer. Windows uses DirectWrite. macOS uses Core Text. Linux typically uses FreeType with Fontconfig. Each system applies different hinting, different subpixel positioning, different glyph substitution rules. Arial on Windows looks almost like Arial on macOS — but the bounding box measurements differ by fractions of a pixel. Enough to fingerprint.
Here's the key point: measureText never touches the canvas bitmap. No pixels get drawn. No toDataURL gets called. Canvas noise injection operates on the pixel buffer. TextMetrics operates on the font shaper. Completely different pipelines. Changing one has no effect on the other.
Browsers differ in how they treat these signals. Some tools change the canvas image and leave measureText returning real host values. JustBrowser restricts the font list to the profile's platform set at the engine level and adds per-profile noise to canvas text measurement, so the host font list does not show through measureText. It does not claim to reproduce another OS's exact glyph metrics. This happens in the Chromium font stack, not in JavaScript or an extension.
Step 3: Testing Your Profile
Here's a test script. Open your profile, paste this into devtools, and compare the output with the same script run on a regular Chrome install.
function testCanvasSubAPI() {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
const ctx = canvas.getContext('2d');
// TextMetrics test
ctx.font = '48px Arial';
const metrics = ctx.measureText('Mmwxyz');
// Winding rule test - self-intersecting star
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
const x = 100 + 80 * Math.cos(angle);
const y = 100 + 80 * Math.sin(angle);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
const windingResults = [];
const testPoints = [[100, 100], [100, 60], [60, 80], [100, 40]];
for (const [x, y] of testPoints) {
windingResults.push({
point: [x, y],
nonzero: ctx.isPointInPath(x, y, 'nonzero'),
evenodd: ctx.isPointInPath(x, y, 'evenodd')
});
}
return {
textMetrics: {
width: metrics.width,
ascent: metrics.actualBoundingBoxAscent,
descent: metrics.actualBoundingBoxDescent,
fontAscent: metrics.fontBoundingBoxAscent,
fontDescent: metrics.fontBoundingBoxDescent
},
windingResults
};
}
console.log(JSON.stringify(testCanvasSubAPI(), null, 2));
What to look for:
-
TextMetrics ascent/descent values: Compare against a regular browser on each OS you care about, since Windows and macOS can return slightly different values for the same font and size. The point is to know which values your profile reports and that they stay the same from session to session.
-
Winding consistency: The results should match stock Chromium behavior. If a tool modifies path operations differently than stock Chromium, results become harder to reason about.
Run this on your actual host machine first, then inside a profile. Differences between the two are expected and tell you what the profile changes. Identical values tell you the profile is reporting the host's text stack.
Step 4: Why Extension-Based Approaches Reach Only So Far
Extension-based tools work by overriding JavaScript APIs. They intercept calls to canvas.toDataURL or ctx.getImageData and modify the return values. That works for the specific functions they hook.
But measureText returns a TextMetrics object created deep inside the rendering engine. You can override the function to return a stand-in object, but then the values need to be internally consistent for every font, size and string. Measuring 'Test' and 'Testing' and comparing the width difference exposes a hardcoded table quickly, because the relationship between glyph widths has to hold together.
Engine-level handling avoids that problem: the engine produces real measurements from the font stack it is configured with. That is the approach behind JustBrowser's native Chromium design. The layer where a setting is applied determines how consistent it is across APIs.
Step 5: Verification Patterns That Work
For teams managing separate profiles for accounts they are allowed to hold (client accounts with permission, shared team profiles, QA):
- Test every profile template with the script above before deployment
- Compare results against reference values from the OSes you test on
- If a tool reports your host's text stack when you expected otherwise, treat it as an isolation gap in that profile. Check each platform's rules too: where a platform forbids multiple accounts, profiles are not a way around that, and its appeal route is the right path
- Re-run the script after browser updates, since new sub-API checks appear over time
For automation engineers:
- Drive profiles through plain Playwright or Puppeteer connected over CDP, as in the Playwright setup guide
- Consider running headed through JustBrowser's API (
{"headless": false}) rather than a headless session, whose screen and viewport follow headless defaults - Save the script's output for each profile and diff it weekly. If something changes, find out why.
For everyone:
The canvas fingerprint isn't one signal. It's dozens of signals across multiple APIs: the image hash, TextMetrics, winding rules, gradient rendering differences, path stroking precision. Treat canvas the way you would treat TLS fingerprinting and assume it is deeper than any single check suggests.
Common Errors
Error: TextMetrics values don't match any OS you recognize
Cause: The tool is changing text measurement in a way that doesn't correspond to a real system's font stack.
Fix: Compare against a stock browser on each OS you care about, and prefer a tool that works at the engine level.
Error: Winding rule results differ from stock Chromium
Cause: Some tools modify canvas path operations in ways that create differences.
Fix: Test against a clean Chromium install. Winding results should be identical. If they are not, check whether there's a setting to disable path modifications.
Error: Results are stable but your site still treats the test profile differently
Cause: Canvas sub-APIs are one of many signals. IP reputation, behavior, TLS fingerprinting and session state all feed a site's bot-management and fraud rules.
Fix: On a site you run, check those rules and allowlist your test traffic rather than tuning the profile, so the test measures the site and not the rules. For tracking your own test traffic, JustAnalytics can help spot anomalies.
Next Steps
Now that you understand these sub-API signals:
- Audit your existing profiles by running the test script on every template you use
- Check your tool's documentation for what it says about font stack handling, and test whether the claim holds
- Add TextMetrics to your verification workflow alongside your other fingerprint checks
- See a full QA workflow with separate profiles — how to QA in-app review and rating prompts walks through one end to end
The canvas image is a starting point. TextMetrics, winding rules and the other sub-APIs are where the remaining differences live, and most people never look at them.
Frequently Asked Questions
What is canvas winding rule fingerprinting?
When you call isPointInPath on a canvas context, you can specify either 'nonzero' or 'evenodd' as the winding rule. Rendering engines can handle edge cases, such as self-intersecting paths or subpixel boundary conditions, slightly differently. A script that probes those cases gets a result that is independent of the canvas image, because the winding logic is not affected by pixel manipulation.
How do TextMetrics reflect operating system information?
The measureText method returns a TextMetrics object with properties like actualBoundingBoxAscent, actualBoundingBoxDescent, fontBoundingBoxAscent, and width. These values depend on your OS font rendering stack: hinting algorithms, subpixel positioning, glyph substitution tables. Windows and macOS produce measurably different metrics for identical font and size combinations because they use different text shaping engines.
Why does canvas image noise not change TextMetrics?
Canvas noise adds random offsets to rendered pixel data before a hash is computed. TextMetrics values come from the font rasterizer before any drawing happens: they are measurements, not pixels. Changing what toDataURL returns has no effect on what measureText returns. The two APIs are separate.
How does JustBrowser handle these sub-API signals?
JustBrowser restricts the font list to the profile's platform set at the engine level and adds per-profile noise to canvas text measurement, so the host font list does not show through measureText. It does not claim to reproduce another OS's exact glyph metrics. isPointInPath is left as stock Chromium behavior. Because these changes are made in the engine rather than by JavaScript overrides, they stay internally consistent across canvas API methods.
Try JustBrowser
A multi-account browser for teams, built on a custom Chromium engine with native C++ integration (not extension-based). Each profile is its own browser with its own cookies, storage, proxy and 40+ identity parameters. Local REST API and CDP for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.
Get started → · How it differs from Multilogin / GoLogin / AdsPower
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.