mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-25 09:28:05 +02:00
Geo-aware locale, audio noise off, font prefs for the bundled-font binary
locale defaults to "auto" and resolves from the proxy egress country the same way timezone does - it reuses the egress IP, maps the country to a BCP-47 locale with the offline mmdb, and falls back to en-US. prefs emit juggler.locale.override (the full Accept-Language list) so the binary keeps navigator.languages and the Intl default locale in sync. The audio fingerprint noise is off in the baseline. Font prefs match the new binary: the sampled whitelist drives font.system.whitelist, system-ui is Segoe UI, the bundled fonts are activated, and the CSS generics are pinned to Windows defaults so they resolve on a non-Windows host too.
This commit is contained in:
parent
8f4b20a19d
commit
3c0efa2d4f
13 changed files with 353 additions and 1015 deletions
|
|
@ -257,41 +257,31 @@ _NETWORK = Network([
|
|||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# FONT WHITELIST (Bayesian: core ∪ sampled_optional | gpu_class)
|
||||
# FONT LIST (Bayesian: core ∪ sampled_optional | gpu_class)
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Semantic flip: previously exclude-list (block N probed fonts per seed).
|
||||
# Now whitelist (browser sees ONLY these fonts, everything else hidden).
|
||||
# The browser sees ONLY these families (everything else hidden) and renders
|
||||
# them from the REAL Windows font files the binary bundles in <GRE>/fonts
|
||||
# (MOZ_BUNDLED_FONTS). No fabricated widths: per-session metric uniqueness
|
||||
# comes from the HarfBuzz per-glyph jitter (shared fpp.hw_seed), not here.
|
||||
# Core (~112): always included — fresh Win11 + Office 2021 English.
|
||||
# Optional (~40): sampled per-session with P(present | gpu_class). Gives
|
||||
# small realistic variance (~3-8 optional fonts differ per session) while
|
||||
# keeping the profile strongly centered on 'typical Windows user'.
|
||||
# Optional (~40): one realistic Windows profile sampled per seed (weighted,
|
||||
# deterministic) → ~3-8 optional families differ per session while staying
|
||||
# centered on 'typical Windows user'.
|
||||
|
||||
|
||||
def derive_font_prefs(gpu_class: str, rng) -> Dict[str, str]:
|
||||
"""Build COHERENT whitelist + metrics strings for the session.
|
||||
"""Build the session's font family list.
|
||||
|
||||
Profile-based (not per-font random):
|
||||
- Core fonts always included (OS defaults + CSS-generic backers).
|
||||
- Optional fonts come from ONE realistic Windows profile picked per seed
|
||||
(weighted, deterministic). Metrics carry REAL per-family widths.
|
||||
- Core families always included (OS defaults + CSS-generic backers).
|
||||
- Optional families come from ONE realistic Windows profile picked per
|
||||
seed (weighted, deterministic).
|
||||
|
||||
Returns:
|
||||
{
|
||||
"whitelist": "arial,calibri,marlett,...",
|
||||
"metrics": "arial|0.978,calibri|0.934,marlett|0.855,..."
|
||||
}
|
||||
|
||||
The whitelist is the list of font families to advertise. The metrics
|
||||
string encodes per-family width scale factors that the consumer can
|
||||
use to make each family detectable by width-diff font probes.
|
||||
|
||||
Each entry in font_pool.json carries its own {name, factor} pair so the
|
||||
two pref strings are GUARANTEED coherent — no chance of a fabricated
|
||||
font with factor 1.0 (undetectable) or a metrics entry for a font not
|
||||
in the whitelist (useless).
|
||||
|
||||
Markers & add-new-font: simply add an entry to font_pool.json:core (with
|
||||
a factor at least 4% away from 1.0) — no special-case code needed.
|
||||
Returns ``{"whitelist": "arial,calibri,marlett,..."}`` — the comma-joined
|
||||
family list to advertise. The binary applies it to the native system font
|
||||
allow-list AT CONSTRUCTION and renders each family from the bundled real
|
||||
Windows file, so glyphs and widths are genuine. To add a family, just add
|
||||
an entry to font_pool.json:core/optional — no special-case code needed.
|
||||
"""
|
||||
# Profile-based (2026-06-18): pick ONE realistic Windows font profile (weighted,
|
||||
# deterministic per seed). Per-font random sampling is superseded — it produced
|
||||
|
|
@ -319,8 +309,8 @@ def derive_font_prefs(gpu_class: str, rng) -> Dict[str, str]:
|
|||
else:
|
||||
included.extend(_FONT_OPTIONAL) # fallback (no profiles defined): all optional
|
||||
# Dedup by name (a profile may list a font that is also in core, e.g. after a
|
||||
# standard font is promoted core→always-present) so the whitelist/metrics never
|
||||
# carry a duplicate family.
|
||||
# standard font is promoted core→always-present) so the list never carries a
|
||||
# duplicate family.
|
||||
_seen: set = set()
|
||||
_uniq: list = []
|
||||
for e in included:
|
||||
|
|
@ -331,13 +321,7 @@ def derive_font_prefs(gpu_class: str, rng) -> Dict[str, str]:
|
|||
# Deterministic ordering: sort by name
|
||||
included.sort(key=lambda e: e["name"])
|
||||
whitelist = ",".join(e["name"] for e in included)
|
||||
# Emit the UNIVERSAL real Windows width per font (host-independent value, same everywhere).
|
||||
# prefs._font_metrics_for_platform divides by the per-platform collapse base to get the C++
|
||||
# factor (measureText = base * factor = the exact Windows width on Windows/Linux/mac).
|
||||
metrics = ",".join(
|
||||
f'{e["name"]}|{e["width"]:.1f}' for e in included
|
||||
)
|
||||
return {"whitelist": whitelist, "metrics": metrics}
|
||||
return {"whitelist": whitelist}
|
||||
|
||||
|
||||
# Back-compat shim: legacy callers still import derive_font_whitelist.
|
||||
|
|
|
|||
|
|
@ -1,723 +1,173 @@
|
|||
{
|
||||
"core": [
|
||||
{
|
||||
"name": "arial",
|
||||
"width": 2256.7
|
||||
"name": "arial"
|
||||
},
|
||||
{
|
||||
"name": "arial black",
|
||||
"width": 2256.7
|
||||
"name": "calibri"
|
||||
},
|
||||
{
|
||||
"name": "arial narrow",
|
||||
"width": 2216.7
|
||||
"name": "cambria"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift",
|
||||
"width": 2231.9
|
||||
"name": "cambria math"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift condensed",
|
||||
"width": 2216.7
|
||||
"name": "candara"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift light",
|
||||
"width": 2216.7
|
||||
"name": "comic sans ms"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift light condensed",
|
||||
"width": 2216.7
|
||||
"name": "consolas"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift light semicondensed",
|
||||
"width": 2216.7
|
||||
"name": "constantia"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semibold",
|
||||
"width": 2216.7
|
||||
"name": "corbel"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semibold condensed",
|
||||
"width": 2216.7
|
||||
"name": "courier new"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semibold semicondensed",
|
||||
"width": 2216.7
|
||||
"name": "ebrima"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semicondensed",
|
||||
"width": 2216.7
|
||||
"name": "franklin gothic"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semilight",
|
||||
"width": 2216.7
|
||||
"name": "gabriola"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semilight condensed",
|
||||
"width": 2216.7
|
||||
"name": "gadugi"
|
||||
},
|
||||
{
|
||||
"name": "bahnschrift semilight semicondensed",
|
||||
"width": 2216.7
|
||||
"name": "georgia"
|
||||
},
|
||||
{
|
||||
"name": "calibri",
|
||||
"width": 2139.5
|
||||
"name": "impact"
|
||||
},
|
||||
{
|
||||
"name": "calibri light",
|
||||
"width": 2139.5
|
||||
"name": "javanese text"
|
||||
},
|
||||
{
|
||||
"name": "cambria",
|
||||
"width": 2258.2
|
||||
"name": "lucida console"
|
||||
},
|
||||
{
|
||||
"name": "cambria math",
|
||||
"width": 2258.2
|
||||
"name": "lucida sans unicode"
|
||||
},
|
||||
{
|
||||
"name": "candara",
|
||||
"width": 2187.8
|
||||
"name": "ms gothic"
|
||||
},
|
||||
{
|
||||
"name": "candara light",
|
||||
"width": 2187.8
|
||||
"name": "ms pgothic"
|
||||
},
|
||||
{
|
||||
"name": "cascadia code",
|
||||
"width": 2362.3
|
||||
"name": "mv boli"
|
||||
},
|
||||
{
|
||||
"name": "cascadia mono",
|
||||
"width": 2362.3
|
||||
"name": "malgun gothic"
|
||||
},
|
||||
{
|
||||
"name": "comic sans ms",
|
||||
"width": 2361.9
|
||||
"name": "marlett"
|
||||
},
|
||||
{
|
||||
"name": "consolas",
|
||||
"width": 2216.7
|
||||
"name": "microsoft himalaya"
|
||||
},
|
||||
{
|
||||
"name": "constantia",
|
||||
"width": 2283.2
|
||||
"name": "microsoft jhenghei"
|
||||
},
|
||||
{
|
||||
"name": "corbel",
|
||||
"width": 2129.9
|
||||
"name": "microsoft jhenghei ui"
|
||||
},
|
||||
{
|
||||
"name": "corbel light",
|
||||
"width": 2129.9
|
||||
"name": "microsoft new tai lue"
|
||||
},
|
||||
{
|
||||
"name": "courier new",
|
||||
"width": 2419.2
|
||||
"name": "microsoft phagspa"
|
||||
},
|
||||
{
|
||||
"name": "ebrima",
|
||||
"width": 2285.2
|
||||
"name": "microsoft sans serif"
|
||||
},
|
||||
{
|
||||
"name": "franklin gothic",
|
||||
"width": 2225.4
|
||||
"name": "microsoft yahei"
|
||||
},
|
||||
{
|
||||
"name": "franklin gothic medium",
|
||||
"width": 2225.4
|
||||
"name": "microsoft yi baiti"
|
||||
},
|
||||
{
|
||||
"name": "gabriola",
|
||||
"width": 1598.5
|
||||
"name": "mingliu-extb"
|
||||
},
|
||||
{
|
||||
"name": "gadugi",
|
||||
"width": 2285.2
|
||||
"name": "mongolian baiti"
|
||||
},
|
||||
{
|
||||
"name": "georgia",
|
||||
"width": 2338.7
|
||||
"name": "myanmar text"
|
||||
},
|
||||
{
|
||||
"name": "hololens mdl2 assets",
|
||||
"width": 2216.7
|
||||
"name": "pmingliu-extb"
|
||||
},
|
||||
{
|
||||
"name": "impact",
|
||||
"width": 2060.8
|
||||
"name": "palatino linotype"
|
||||
},
|
||||
{
|
||||
"name": "ink free",
|
||||
"width": 2122.5
|
||||
"name": "segoe print"
|
||||
},
|
||||
{
|
||||
"name": "javanese text",
|
||||
"width": 2311.9
|
||||
"name": "segoe script"
|
||||
},
|
||||
{
|
||||
"name": "leelawadee ui semilight",
|
||||
"width": 2283.4
|
||||
"name": "segoe ui"
|
||||
},
|
||||
{
|
||||
"name": "lucida console",
|
||||
"width": 2429.5
|
||||
"name": "segoe ui symbol"
|
||||
},
|
||||
{
|
||||
"name": "lucida sans unicode",
|
||||
"width": 2464.3
|
||||
"name": "simsun"
|
||||
},
|
||||
{
|
||||
"name": "malgun gothic",
|
||||
"width": 2351.1
|
||||
"name": "simsun-extb"
|
||||
},
|
||||
{
|
||||
"name": "malgun gothic semilight",
|
||||
"width": 2351.1
|
||||
"name": "sitka small"
|
||||
},
|
||||
{
|
||||
"name": "marlett",
|
||||
"width": 3740.2
|
||||
"name": "sylfaen"
|
||||
},
|
||||
{
|
||||
"name": "microsoft himalaya",
|
||||
"width": 1468.4
|
||||
"name": "trebuchet ms"
|
||||
},
|
||||
{
|
||||
"name": "microsoft jhenghei",
|
||||
"width": 2440.4
|
||||
"name": "verdana"
|
||||
},
|
||||
{
|
||||
"name": "microsoft jhenghei light",
|
||||
"width": 2440.4
|
||||
"name": "webdings"
|
||||
},
|
||||
{
|
||||
"name": "microsoft jhenghei ui",
|
||||
"width": 2440.4
|
||||
"name": "yu gothic"
|
||||
},
|
||||
{
|
||||
"name": "microsoft jhenghei ui light",
|
||||
"width": 2440.4
|
||||
"name": "courier"
|
||||
},
|
||||
{
|
||||
"name": "microsoft new tai lue",
|
||||
"width": 2285.2
|
||||
"name": "helvetica"
|
||||
},
|
||||
{
|
||||
"name": "microsoft phagspa",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "microsoft sans serif",
|
||||
"width": 2256.6
|
||||
},
|
||||
{
|
||||
"name": "microsoft tai le",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "microsoft yahei",
|
||||
"width": 2487.6
|
||||
},
|
||||
{
|
||||
"name": "microsoft yahei light",
|
||||
"width": 2487.6
|
||||
},
|
||||
{
|
||||
"name": "microsoft yahei ui",
|
||||
"width": 2487.6
|
||||
},
|
||||
{
|
||||
"name": "microsoft yahei ui light",
|
||||
"width": 2487.6
|
||||
},
|
||||
{
|
||||
"name": "microsoft yi baiti",
|
||||
"width": 1830.7
|
||||
},
|
||||
{
|
||||
"name": "mingliu-extb",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "mingliu_hkscs-extb",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "mongolian baiti",
|
||||
"width": 2191.7
|
||||
},
|
||||
{
|
||||
"name": "ms gothic",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "ms pgothic",
|
||||
"width": 2031.7
|
||||
},
|
||||
{
|
||||
"name": "mv boli",
|
||||
"width": 2417.4
|
||||
},
|
||||
{
|
||||
"name": "myanmar text",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "nirmala ui",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "nirmala ui semilight",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "nsimsun",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "palatino linotype",
|
||||
"width": 2358.6
|
||||
},
|
||||
{
|
||||
"name": "pmingliu-extb",
|
||||
"width": 2065.9
|
||||
},
|
||||
{
|
||||
"name": "sans serif collection",
|
||||
"width": 2361.6
|
||||
},
|
||||
{
|
||||
"name": "segoe mdl2 assets",
|
||||
"width": 2137.2
|
||||
},
|
||||
{
|
||||
"name": "segoe print",
|
||||
"width": 2675.2
|
||||
},
|
||||
{
|
||||
"name": "segoe script",
|
||||
"width": 2830.3
|
||||
},
|
||||
{
|
||||
"name": "segoe ui",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "segoe ui black",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "segoe ui emoji",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "segoe ui historic",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "segoe ui semibold",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "segoe ui semilight",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "segoe ui symbol",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "segoe ui variable",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "simsun",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "simsun-extb",
|
||||
"width": 2016
|
||||
},
|
||||
{
|
||||
"name": "sitka banner",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sitka display",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sitka heading",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sitka small",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sitka subheading",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sitka text",
|
||||
"width": 2007.8
|
||||
},
|
||||
{
|
||||
"name": "sylfaen",
|
||||
"width": 2279.6
|
||||
},
|
||||
{
|
||||
"name": "symbol",
|
||||
"width": 2223.4
|
||||
},
|
||||
{
|
||||
"name": "tahoma",
|
||||
"width": 2233.1
|
||||
},
|
||||
{
|
||||
"name": "times new roman",
|
||||
"width": 2191.7
|
||||
},
|
||||
{
|
||||
"name": "trebuchet ms",
|
||||
"width": 2222.4
|
||||
},
|
||||
{
|
||||
"name": "verdana",
|
||||
"width": 2529.1
|
||||
},
|
||||
{
|
||||
"name": "webdings",
|
||||
"width": 3627.5
|
||||
},
|
||||
{
|
||||
"name": "wingdings",
|
||||
"width": 3539.7
|
||||
},
|
||||
{
|
||||
"name": "wingdings 2",
|
||||
"width": 3582.1
|
||||
},
|
||||
{
|
||||
"name": "wingdings 3",
|
||||
"width": 2963.4
|
||||
},
|
||||
{
|
||||
"name": "yu gothic",
|
||||
"width": 2351.3
|
||||
},
|
||||
{
|
||||
"name": "yu gothic light",
|
||||
"width": 2351.3
|
||||
},
|
||||
{
|
||||
"name": "yu gothic medium",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "yu gothic ui",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "yu gothic ui light",
|
||||
"width": 2285.2
|
||||
},
|
||||
{
|
||||
"name": "yu gothic ui semibold",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "yu gothic ui semilight",
|
||||
"width": 2216.7
|
||||
}
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"name": "aparajita",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "arabic typesetting",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "arial unicode ms",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "batang",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "century",
|
||||
"width": 2397.7
|
||||
},
|
||||
{
|
||||
"name": "century gothic",
|
||||
"width": 2409.1
|
||||
},
|
||||
{
|
||||
"name": "dengxian",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "dfkai-sb",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "dokchampa",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "estrangelo edessa",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "euphemia",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "fangsong",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "gautami",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "haettenschweiler",
|
||||
"width": 1567.6
|
||||
},
|
||||
{
|
||||
"name": "helv",
|
||||
"width": 2256.6
|
||||
},
|
||||
{
|
||||
"name": "iskoola pota",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "kaiti",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "kalinga",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "kartika",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "khmer ui",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "kokila",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "lao ui",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "latha",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "leelawadee",
|
||||
"width": 2267.4
|
||||
},
|
||||
{
|
||||
"name": "leelawadee ui",
|
||||
"width": 2283.4
|
||||
},
|
||||
{
|
||||
"name": "levenim mt",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "mangal",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "meiryo",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "meiryo ui",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "microsoft uighur",
|
||||
"width": 1498.1
|
||||
},
|
||||
{
|
||||
"name": "monotype corsiva",
|
||||
"width": 1929.3
|
||||
},
|
||||
{
|
||||
"name": "ms mincho",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "ms outlook",
|
||||
"width": 2361.9
|
||||
},
|
||||
{
|
||||
"name": "ms pmincho",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "ms reference sans serif",
|
||||
"width": 2529.0
|
||||
},
|
||||
{
|
||||
"name": "ms reference specialty",
|
||||
"width": 2976.2
|
||||
},
|
||||
{
|
||||
"name": "ms ui gothic",
|
||||
"width": 2031.7
|
||||
},
|
||||
{
|
||||
"name": "mt extra",
|
||||
"width": 2183.8
|
||||
},
|
||||
{
|
||||
"name": "nyala",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "plantagenet cherokee",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "pmingliu",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "pristina",
|
||||
"width": 1829.1
|
||||
},
|
||||
{
|
||||
"name": "raavi",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "segoe fluent icons",
|
||||
"width": 2142.7
|
||||
},
|
||||
{
|
||||
"name": "segoe ui light",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "shonar bangla",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "shruti",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "simhei",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "simkai",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "small fonts",
|
||||
"width": 2256.7
|
||||
},
|
||||
{
|
||||
"name": "traditional arabic",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "tunga",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "urdu typesetting",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "utsaah",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "vani",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "vijaya",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "vrinda",
|
||||
"width": 2216.7
|
||||
},
|
||||
{
|
||||
"name": "yu mincho",
|
||||
"width": 2216.7
|
||||
"name": "segoe ui light"
|
||||
}
|
||||
],
|
||||
"optional": [],
|
||||
"profiles": [
|
||||
{
|
||||
"name": "win_office",
|
||||
"weight": 40,
|
||||
"optional": [
|
||||
"century",
|
||||
"century gothic",
|
||||
"helv",
|
||||
"haettenschweiler",
|
||||
"leelawadee",
|
||||
"ms outlook",
|
||||
"ms reference specialty",
|
||||
"ms ui gothic",
|
||||
"mt extra",
|
||||
"microsoft uighur",
|
||||
"monotype corsiva",
|
||||
"pristina",
|
||||
"segoe ui light",
|
||||
"small fonts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "win_base",
|
||||
"name": "win_clean",
|
||||
"weight": 35,
|
||||
"optional": [
|
||||
"century",
|
||||
"century gothic",
|
||||
"helv",
|
||||
"haettenschweiler",
|
||||
"leelawadee",
|
||||
"ms ui gothic",
|
||||
"microsoft uighur",
|
||||
"monotype corsiva",
|
||||
"pristina",
|
||||
"segoe ui light",
|
||||
"small fonts"
|
||||
]
|
||||
"optional": []
|
||||
},
|
||||
{
|
||||
"name": "win_lean",
|
||||
"weight": 25,
|
||||
"optional": [
|
||||
"century",
|
||||
"century gothic",
|
||||
"helv",
|
||||
"haettenschweiler",
|
||||
"leelawadee",
|
||||
"ms ui gothic",
|
||||
"segoe ui light",
|
||||
"small fonts"
|
||||
]
|
||||
"name": "win_office",
|
||||
"weight": 65,
|
||||
"optional": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (NVIDIA)",
|
||||
"renderer_out": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.462396,
|
||||
"prob": 0.470921,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (NVIDIA)",
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.192201,
|
||||
"prob": 0.195745,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.157382,
|
||||
"prob": 0.160284,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (AMD)",
|
||||
"renderer_out": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.084958,
|
||||
"prob": 0.086524,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (AMD, Radeon HD 3200 Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (AMD)",
|
||||
|
|
@ -67,7 +67,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (AMD)",
|
||||
"renderer_out": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.052925,
|
||||
"prob": 0.053901,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (AMD)",
|
||||
|
|
@ -80,26 +80,10 @@
|
|||
"zoom.stealth.webgl2.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"vendor": "Google Inc. (NVIDIA)",
|
||||
"renderer_out": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.013928,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (NVIDIA)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl.int_params": "2849|1,2885|1029,2886|2305,2931|1,2932|513,2961|0,2962|519,2963|2147483647,2964|7680,2965|7680,2966|7680,2967|0,2968|2147483647,3074|1029,3314|0,3315|0,3316|0,3317|4,3330|0,3331|0,3332|0,3333|4,3379|16384,3408|4,3410|8,3411|8,3412|8,3413|8,3414|24,3415|0,10752|0,32777|32774,32824|0,32877|0,32878|0,32883|2048,32936|1,32937|4,32938|1,32968|0,32969|1,32970|0,32971|1,33000|2147483647,33001|2147483647,33170|4352,34016|33984,34024|16384,34045|2,34076|16384,34816|519,34817|7680,34818|7680,34819|7680,34852|8,34853|1029,34854|0,34855|0,34856|0,34857|0,34858|0,34859|0,34860|0,34877|32774,34921|16,34930|16,35071|2048,35076|-8,35077|7,35371|12,35373|12,35374|24,35375|24,35376|65536,35377|212988,35379|200704,35380|256,35657|4096,35658|16380,35659|120,35660|16,35661|32,35723|4352,35738|5121,35739|6408,35968|4,35978|120,35979|4,36003|0,36004|2147483647,36005|2147483647,36063|8,36183|8,36203|4294967294,36347|4095,36348|30,36349|1024,37137|0,37154|120,37157|120,37443|37444,37447|1000000000",
|
||||
"zoom.stealth.webgl.int2_params": "2928|0:1,3386|32767:32767,33901|1:1024,33902|1:1",
|
||||
"zoom.stealth.webgl.float_params": "",
|
||||
"zoom.stealth.webgl.shader_precisions": "35633*36336|127:127:23,35633*36337|127:127:23,35633*36338|127:127:23,35633*36339|31:30:0,35633*36340|31:30:0,35633*36341|31:30:0,35632*36336|127:127:23,35632*36337|127:127:23,35632*36338|127:127:23,35632*36339|31:30:0,35632*36340|31:30:0,35632*36341|31:30:0",
|
||||
"zoom.stealth.webgl2.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"vendor": "Google Inc. (Microsoft)",
|
||||
"renderer_out": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0), or similar",
|
||||
"prob": 0.012535,
|
||||
"prob": 0.012766,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Microsoft, Microsoft Basic Render Driver Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Microsoft)",
|
||||
|
|
@ -115,9 +99,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0)",
|
||||
"prob": 0.006964,
|
||||
"prob": 0.007092,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
|
|
@ -131,7 +115,7 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1), or similar",
|
||||
"prob": 0.004178,
|
||||
"prob": 0.004255,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
|
|
@ -144,28 +128,12 @@
|
|||
"zoom.stealth.webgl2.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"vendor": "Google Inc. (Google)",
|
||||
"renderer_out": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
|
||||
"prob": 0.002786,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Google)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,EXT_sRGB,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_astc,WEBGL_compressed_texture_etc,WEBGL_compressed_texture_etc1,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_multi_draw",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_color_buffer_half_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,EXT_texture_norm16,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_clip_cull_distance,WEBGL_compressed_texture_astc,WEBGL_compressed_texture_etc,WEBGL_compressed_texture_etc1,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_lose_context,WEBGL_multi_draw",
|
||||
"zoom.stealth.webgl.int_params": "2849|1,2885|1029,2886|2305,2931|1,2932|513,2961|0,2962|519,2963|2147483647,2964|7680,2965|7680,2966|7680,2967|0,2968|2147483647,3074|1029,3314|0,3315|0,3316|0,3317|4,3330|0,3331|0,3332|0,3333|4,3379|8192,3408|4,3410|8,3411|8,3412|8,3413|8,3414|24,3415|0,10752|0,32777|32774,32824|0,32877|0,32878|0,32883|2048,32936|1,32937|4,32938|1,32968|0,32969|1,32970|0,32971|1,33000|2147483647,33001|2147483647,33170|4352,34016|33984,34024|8192,34045|15,34076|16384,34816|519,34817|7680,34818|7680,34819|7680,34852|8,34853|1029,34854|1029,34855|1029,34856|1029,34857|1029,34858|1029,34859|1029,34860|1029,34877|32774,34921|16,34930|32,35071|2048,35076|-8,35077|7,35371|14,35373|14,35374|60,35375|72,35376|65536,35377|245760,35379|245760,35380|256,35657|16384,35658|16384,35659|124,35660|32,35661|64,35723|4352,35738|5121,35739|6408,35968|4,35978|128,35979|4,36003|0,36004|2147483647,36005|2147483647,36063|8,36183|4,36203|4294967294,36345|1,36347|4096,36348|31,36349|4096,37137|0,37154|128,37157|128,37443|37444,37447|0",
|
||||
"zoom.stealth.webgl.int2_params": "2928|0:1,3386|8192:8192,33901|1:1023,33902|1:1",
|
||||
"zoom.stealth.webgl.float_params": "",
|
||||
"zoom.stealth.webgl.shader_precisions": "35633*36336|15:15:10,35633*36337|15:15:10,35633*36338|127:127:23,35633*36339|15:14:0,35633*36340|15:14:0,35633*36341|31:30:0,35632*36336|15:15:10,35632*36337|15:15:10,35632*36338|127:127:23,35632*36339|15:14:0,35632*36340|15:14:0,35632*36341|31:30:0",
|
||||
"zoom.stealth.webgl2.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"vendor": "Google Inc. (AMD)",
|
||||
"renderer_out": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (AMD)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
|
|
@ -179,9 +147,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_0 ps_4_0)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_0 ps_4_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_0 ps_4_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "",
|
||||
|
|
@ -195,9 +163,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (NVIDIA)",
|
||||
"renderer_out": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_4_0 ps_4_0)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_4_0 ps_4_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_4_0 ps_4_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (NVIDIA)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "",
|
||||
|
|
@ -211,9 +179,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel 945GM Direct3D11 vs_4_0 ps_4_0)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel 945GM Direct3D11 vs_4_0 ps_4_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel 945GM Direct3D11 vs_4_0 ps_4_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "",
|
||||
|
|
@ -227,9 +195,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (Intel)",
|
||||
"renderer_out": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_4_1 ps_4_1, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (Intel)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
|
|
@ -243,25 +211,9 @@
|
|||
{
|
||||
"vendor": "Google Inc. (NVIDIA)",
|
||||
"renderer_out": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"prob": 0.001393,
|
||||
"prob": 0.001419,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (NVIDIA)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl.int_params": "2849|1,2885|1029,2886|2305,2931|1,2932|513,2961|0,2962|519,2963|2147483647,2964|7680,2965|7680,2966|7680,2967|0,2968|2147483647,3074|1029,3314|0,3315|0,3316|0,3317|4,3330|0,3331|0,3332|0,3333|4,3379|16384,3408|4,3410|8,3411|8,3412|8,3413|8,3414|24,3415|0,10752|0,32777|32774,32824|0,32877|0,32878|0,32883|2048,32936|1,32937|4,32938|1,32968|0,32969|1,32970|0,32971|1,33000|2147483647,33001|2147483647,33170|4352,34016|33984,34024|16384,34045|2,34076|16384,34816|519,34817|7680,34818|7680,34819|7680,34852|8,34853|1029,34854|0,34855|0,34856|0,34857|0,34858|0,34859|0,34860|0,34877|32774,34921|16,34930|16,35071|2048,35076|-8,35077|7,35371|12,35373|12,35374|24,35375|24,35376|65536,35377|212988,35379|200704,35380|256,35657|4096,35658|16380,35659|120,35660|16,35661|32,35723|4352,35738|5121,35739|6408,35968|4,35978|120,35979|4,36003|0,36004|2147483647,36005|2147483647,36063|8,36183|8,36203|4294967294,36347|4095,36348|30,36349|1024,37137|0,37154|120,37157|120,37443|37444,37447|1000000000",
|
||||
"zoom.stealth.webgl.int2_params": "2928|0:1,3386|32767:32767,33901|1:1024,33902|1:1",
|
||||
"zoom.stealth.webgl.float_params": "",
|
||||
"zoom.stealth.webgl.shader_precisions": "35633*36336|127:127:23,35633*36337|127:127:23,35633*36338|127:127:23,35633*36339|31:30:0,35633*36340|31:30:0,35633*36341|31:30:0,35632*36336|127:127:23,35632*36337|127:127:23,35632*36338|127:127:23,35632*36339|31:30:0,35632*36340|31:30:0,35632*36341|31:30:0",
|
||||
"zoom.stealth.webgl2.enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"vendor": "Google Inc. (NVIDIA)",
|
||||
"renderer_out": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"prob": 0.001393,
|
||||
"prefs": {
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0)",
|
||||
"zoom.stealth.webgl.renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
"zoom.stealth.webgl.vendor": "Google Inc. (NVIDIA)",
|
||||
"zoom.stealth.webgl.extensions": "ANGLE_instanced_arrays,EXT_blend_minmax,EXT_color_buffer_half_float,EXT_float_blend,EXT_frag_depth,EXT_shader_texture_lod,EXT_sRGB,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_element_index_uint,OES_fbo_render_mipmap,OES_standard_derivatives,OES_texture_float,OES_texture_float_linear,OES_texture_half_float,OES_texture_half_float_linear,OES_vertex_array_object,WEBGL_color_buffer_float,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_depth_texture,WEBGL_draw_buffers,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
"zoom.stealth.webgl2.extensions": "EXT_color_buffer_float,EXT_float_blend,EXT_texture_compression_bptc,EXT_texture_compression_rgtc,EXT_texture_filter_anisotropic,OES_draw_buffers_indexed,OES_texture_float_linear,OVR_multiview2,WEBGL_compressed_texture_s3tc,WEBGL_compressed_texture_s3tc_srgb,WEBGL_debug_renderer_info,WEBGL_debug_shaders,WEBGL_lose_context,WEBGL_provoking_vertex",
|
||||
|
|
|
|||
|
|
@ -136,6 +136,56 @@ def ip_to_timezone(ip: str, mmdb_path: Any) -> str:
|
|||
return tz
|
||||
|
||||
|
||||
# ISO 3166 country code -> the primary BCP-47 locale a real Windows machine in that
|
||||
# country most commonly runs. Multi-language countries use the majority language; the
|
||||
# user can always force a specific locale instead of "auto". Unknown -> en-US.
|
||||
_COUNTRY_LOCALE = {
|
||||
"US": "en-US", "GB": "en-GB", "CA": "en-CA", "AU": "en-AU", "NZ": "en-NZ", "IE": "en-IE",
|
||||
"ZA": "en-ZA", "IN": "en-IN", "SG": "en-SG", "PH": "en-PH",
|
||||
"FR": "fr-FR", "BE": "fr-BE", "LU": "fr-LU",
|
||||
"DE": "de-DE", "AT": "de-AT", "CH": "de-CH",
|
||||
"IT": "it-IT", "ES": "es-ES", "PT": "pt-PT", "NL": "nl-NL",
|
||||
"SE": "sv-SE", "NO": "nb-NO", "DK": "da-DK", "FI": "fi-FI", "IS": "is-IS",
|
||||
"PL": "pl-PL", "CZ": "cs-CZ", "SK": "sk-SK", "HU": "hu-HU", "RO": "ro-RO",
|
||||
"GR": "el-GR", "BG": "bg-BG", "HR": "hr-HR", "RS": "sr-RS", "SI": "sl-SI",
|
||||
"RU": "ru-RU", "UA": "uk-UA", "TR": "tr-TR", "IL": "he-IL",
|
||||
"BR": "pt-BR", "MX": "es-MX", "AR": "es-AR", "CL": "es-CL", "CO": "es-CO", "PE": "es-PE",
|
||||
"JP": "ja-JP", "KR": "ko-KR", "CN": "zh-CN", "TW": "zh-TW", "HK": "zh-HK",
|
||||
"ID": "id-ID", "TH": "th-TH", "VN": "vi-VN", "MY": "ms-MY",
|
||||
"SA": "ar-SA", "AE": "ar-AE", "EG": "ar-EG",
|
||||
}
|
||||
|
||||
|
||||
def ip_to_locale(ip: str, mmdb_path: Any) -> str:
|
||||
"""Map ``ip`` -> a BCP-47 locale via the MaxMind ``country.iso_code`` field, so the
|
||||
browser language stays consistent with the proxy egress country. Falls back to
|
||||
``en-US`` for IPs absent from the DB or countries we don't map."""
|
||||
import maxminddb
|
||||
|
||||
with maxminddb.open_database(str(mmdb_path)) as reader:
|
||||
record = reader.get(ip)
|
||||
cc = ""
|
||||
if isinstance(record, dict):
|
||||
cc = ((record.get("country") or {}).get("iso_code") or "")
|
||||
return _COUNTRY_LOCALE.get(cc.upper(), "en-US")
|
||||
|
||||
|
||||
def resolve_session_locale(egress_ip: Optional[str], proxy: Optional[Dict[str, str]]) -> str:
|
||||
"""Resolve ``locale="auto"`` to a BCP-47 locale from the egress country. Behind a proxy
|
||||
it reuses the already-discovered ``egress_ip`` (no extra round-trip); without a proxy it
|
||||
discovers the host's public IP. On any failure it returns ``en-US`` (never breaks launch
|
||||
— locale is cosmetic, unlike timezone which traps a foreign-proxy mismatch)."""
|
||||
from .download import ensure_geoip_mmdb
|
||||
|
||||
try:
|
||||
ip = egress_ip if _proxy_is_set(proxy) else discover_egress_ip(None)
|
||||
if ip is None:
|
||||
return "en-US"
|
||||
return ip_to_locale(ip, ensure_geoip_mmdb())
|
||||
except Exception: # noqa: BLE001
|
||||
return "en-US"
|
||||
|
||||
|
||||
class SessionGeo(NamedTuple):
|
||||
"""Geo facts resolved once per session from a single egress round-trip.
|
||||
|
||||
|
|
|
|||
|
|
@ -185,6 +185,12 @@ def forced_gpu_class(seed: int) -> Optional[str]:
|
|||
# only on the retired amd/arc mix) → dropped. NVIDIA is the worst case, so these are clean on
|
||||
# amd/intel too. hw_seed = the canvas/WebGL gamma render hash (the dominant consistency-score
|
||||
# driver); host-calibrated.
|
||||
# 2026-06-21: with WebGL Option B (zoom.stealth.webgl.substitute_pixels, ON in prefs.py) the WebGL
|
||||
# render hash is hash(seed,idx) = HOST-INDEPENDENT, so this list NO LONGER needs per-host calibration
|
||||
# — it only supplies per-session diversity. A 2026-06-21 attempt to re-calibrate it per-host FAILED
|
||||
# cross-OS: hw_seed clean on Windows went dirty on the Linux GL backend (b008 0.034->0.839; Win-dirty
|
||||
# {7,11,20,27} = Linux-clean and vice-versa; + identity×hw_seed interaction on Linux). That proved
|
||||
# calibration can't work cross-host → substitution replaces it. Kept the original diverse 9-set.
|
||||
CLEAN_RENDER_SEEDS = [0, 5, 6, 9, 11, 16, 19, 20, 28]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class InvisiblePlaywright:
|
|||
proxy: Optional[Dict[str, str]] = None,
|
||||
extra_args: Optional[list[str]] = None,
|
||||
humanize: Union[bool, float] = True,
|
||||
locale: str = "en-US",
|
||||
locale: str = "auto",
|
||||
timezone: str = "",
|
||||
extra_prefs: Optional[Dict[str, Any]] = None,
|
||||
binary_path: Optional[str] = None,
|
||||
|
|
@ -90,6 +90,14 @@ class InvisiblePlaywright:
|
|||
)
|
||||
self._timezone = _geo.timezone
|
||||
self._webrtc_egress_ip = _geo.egress_ip
|
||||
# Geo-aware locale: "auto" derives the language from the egress country (reusing
|
||||
# the egress IP just discovered), like timezone="auto". Keeps the browser language
|
||||
# consistent with the proxy's country instead of a fixed en-US.
|
||||
if (self._locale or "").strip().lower() == "auto":
|
||||
from ._geo import resolve_session_locale
|
||||
self._locale = await asyncio.to_thread(
|
||||
resolve_session_locale, _geo.egress_ip, self._proxy
|
||||
)
|
||||
executable = self._binary_path or ensure_binary()
|
||||
prefs = translate_profile_to_prefs(
|
||||
self._profile,
|
||||
|
|
@ -113,7 +121,7 @@ class InvisiblePlaywright:
|
|||
prefs["stealthfox.humanize.maxTime"] = str(cap)
|
||||
playwright_proxy = _configure_proxy_shared(self._proxy, prefs)
|
||||
pw_headless = self._resolve_headless()
|
||||
env = self._build_env()
|
||||
env = self._build_env(prefs)
|
||||
try:
|
||||
self._pw = await async_playwright().start()
|
||||
if self._profile_dir is not None:
|
||||
|
|
@ -214,11 +222,21 @@ class InvisiblePlaywright:
|
|||
pass
|
||||
self._virtual_display = None
|
||||
|
||||
def _build_env(self) -> Dict[str, str]:
|
||||
def _build_env(self, prefs: Dict[str, Any]) -> Dict[str, str]:
|
||||
import os as _os
|
||||
env = _os.environ.copy()
|
||||
if self._timezone:
|
||||
env["TZ"] = _tz_env(self._timezone)
|
||||
# Font allow-list + system-ui via env (read at the gfxPlatformFontList
|
||||
# constructor, process start) — Playwright delivers firefox_user_prefs
|
||||
# over the juggler protocol after start, too late for the font list ctor,
|
||||
# so without this host fonts leak on Linux/macOS. See sync launcher.
|
||||
fontlist = prefs.get("zoom.stealth.font.fontlist")
|
||||
if fontlist:
|
||||
env["STEALTHFOX_FONTLIST"] = fontlist
|
||||
system_ui = prefs.get("zoom.stealth.font.system_ui")
|
||||
if system_ui:
|
||||
env["STEALTHFOX_SYSTEMUI"] = system_ui
|
||||
# WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress
|
||||
# IP (caller's explicit env var wins, else the IP auto-discovered in
|
||||
# __aenter__) and drop IPv6 from gathering behind a proxy.
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class InvisiblePlaywright:
|
|||
proxy: Optional[Dict[str, str]] = None,
|
||||
extra_args: Optional[list[str]] = None,
|
||||
humanize: Union[bool, float] = True,
|
||||
locale: str = "en-US",
|
||||
locale: str = "auto",
|
||||
timezone: str = "",
|
||||
extra_prefs: Optional[Dict[str, Any]] = None,
|
||||
binary_path: Optional[str] = None,
|
||||
|
|
@ -135,8 +135,15 @@ class InvisiblePlaywright:
|
|||
into a Bezier trajectory with ~10 ms between waypoints.
|
||||
Default ``True`` (~1.5 s max motion). ``False`` disables;
|
||||
a float caps the motion in seconds.
|
||||
locale: BCP-47 tag (e.g. ``"en-US"``). Drives the
|
||||
``Accept-Language`` header and ``navigator.language``.
|
||||
locale: BCP-47 tag (e.g. ``"en-US"``) or ``"auto"`` (default).
|
||||
``"auto"`` derives the locale from the egress country — the proxy
|
||||
egress IP, or the host's public IP without a proxy — exactly like
|
||||
``timezone="auto"``, keeping the browser language consistent with the
|
||||
exit country (a French proxy → ``fr-FR``). Drives
|
||||
``intl.accept_languages`` → both ``navigator.language``/``languages``
|
||||
AND the q-valued ``Accept-Language`` header (the patched binary builds
|
||||
the header from the pref, never from the raw Playwright locale override,
|
||||
so the two never diverge — see nsHttpHandler STEALTHFOX note).
|
||||
timezone: IANA zone (e.g. ``"America/New_York"``) — used as-is
|
||||
when set, the only way to force a specific zone. ``""``
|
||||
(default) or ``"auto"`` ALWAYS resolves from the egress IP:
|
||||
|
|
@ -199,11 +206,17 @@ class InvisiblePlaywright:
|
|||
_geo = prepare_session_geo(self._timezone, self._proxy)
|
||||
self._timezone = _geo.timezone
|
||||
self._webrtc_egress_ip = _geo.egress_ip
|
||||
# Geo-aware locale: "auto" derives the language from the egress country (reusing
|
||||
# the egress IP already discovered above), like timezone="auto". Keeps the browser
|
||||
# language consistent with the proxy's country instead of a fixed en-US.
|
||||
if (self._locale or "").strip().lower() == "auto":
|
||||
from ._geo import resolve_session_locale
|
||||
self._locale = resolve_session_locale(_geo.egress_ip, self._proxy)
|
||||
executable = self._binary_path or ensure_binary()
|
||||
prefs = self._build_prefs()
|
||||
playwright_proxy = _configure_proxy_shared(self._proxy, prefs)
|
||||
pw_headless = self._resolve_headless()
|
||||
env = self._build_env()
|
||||
env = self._build_env(prefs)
|
||||
|
||||
try:
|
||||
self._pw = sync_playwright().start()
|
||||
|
|
@ -358,7 +371,7 @@ class InvisiblePlaywright:
|
|||
prefs["stealthfox.humanize.maxTime"] = str(self._humanize_max_seconds())
|
||||
return prefs
|
||||
|
||||
def _build_env(self) -> Dict[str, str]:
|
||||
def _build_env(self, prefs: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Env vars passed to the Firefox subprocess.
|
||||
|
||||
``TZ`` tunes the libc clock the content process reads for
|
||||
|
|
@ -369,11 +382,24 @@ class InvisiblePlaywright:
|
|||
a synthetic srflx candidate matching the proxy egress IP, avoiding
|
||||
the StaticPref IPC propagation timing issue between parent and
|
||||
socket processes.
|
||||
``STEALTHFOX_FONTLIST`` / ``STEALTHFOX_SYSTEMUI`` carry the font
|
||||
allow-list + system-ui family for the SAME reason: the binary reads
|
||||
them at the gfxPlatformFontList constructor (process start), but
|
||||
Playwright delivers firefox_user_prefs over the juggler protocol
|
||||
AFTER start — too late for the font list ctor. The env var is present
|
||||
at start and inherited by content processes, so the allow-list is
|
||||
applied on every host (without it, host fonts leak on Linux/macOS).
|
||||
"""
|
||||
import os as _os
|
||||
env = _os.environ.copy()
|
||||
if self._timezone:
|
||||
env["TZ"] = _tz_env(self._timezone)
|
||||
fontlist = prefs.get("zoom.stealth.font.fontlist")
|
||||
if fontlist:
|
||||
env["STEALTHFOX_FONTLIST"] = fontlist
|
||||
system_ui = prefs.get("zoom.stealth.font.system_ui")
|
||||
if system_ui:
|
||||
env["STEALTHFOX_SYSTEMUI"] = system_ui
|
||||
# WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress
|
||||
# IP so the srflx candidate matches the proxy (not the real host the
|
||||
# UDP STUN would otherwise leak). An explicit env var set by the caller
|
||||
|
|
|
|||
|
|
@ -170,39 +170,6 @@ _WIN_VOICES = ",".join([
|
|||
])
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Linux font compensation — Linux Firefox uses DejaVu / Liberation
|
||||
# fonts which have wider/narrower glyphs than Windows Arial / Segoe.
|
||||
# These per-generic factors are prepended to ``zoom.stealth.font.metrics``
|
||||
# on Linux only; Windows-native rendering already matches the canonical
|
||||
# widths so we pass an empty string (any factor !=1 would distort real
|
||||
# metrics).
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
_LINUX_GENERIC_FONT_FACTORS = (
|
||||
# GENERIC factors are calibrated to FP Pro's font_preferences probe (NOT the 72px named-probe
|
||||
# scale — verified 2026-06-18: recalibrating to the named-probe scale broke them, mono 121.55
|
||||
# -> 112.4; the original values are correct for font_preferences). DejaVu/Liberation generics
|
||||
# vs Windows targets: serif 0.920, sans 0.889, monospace 1.000, system-ui 0.910.
|
||||
"serif|0.920,sans-serif|0.889,monospace|1.000,"
|
||||
"system-ui|0.910,cursive|0.932,fantasy|0.812,"
|
||||
)
|
||||
|
||||
# Calibration reference string for the C++ self-calibrating absolute-width path.
|
||||
# MUST be byte-identical to the probe the font_pool widths were measured with
|
||||
# (72px canvas measureText) and to the binary's zoom.stealth.font.calib_ref
|
||||
# default. Set explicitly so correctness never depends on the binary default.
|
||||
_FONT_CALIB_REF = "mmmmwwwwiiiillloooMMMMWWWW0123456789 The quick brown fox"
|
||||
# NOTE: there is intentionally NO per-OS collapse-base table here anymore.
|
||||
# The C++ font hook self-calibrates: `font_pool` stores the UNIVERSAL real
|
||||
# Windows measureText width per font and the binary divides it by the host's
|
||||
# own collapse base (gfxTextRun::StealthCollapseBase, summed from the list-head
|
||||
# font's per-glyph advances over zoom.stealth.font.calib_ref). So the SAME
|
||||
# stored value yields the exact Windows width on Windows, Linux AND macOS with
|
||||
# nothing to measure per platform. (Was: _COLLAPSE_BASE = {win32:1530, linux:2400}
|
||||
# + a darwin TODO — removed 2026-06-18 when the self-calibrating C++ landed.)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Baseline — applied to every session regardless of Profile.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -374,9 +341,37 @@ _BASELINE: Dict[str, Any] = {
|
|||
# DevTools anti-detection.
|
||||
"zoom.stealth.debugger.force_detach": True,
|
||||
|
||||
# Canvas substitution — additive ±1 noise over the OS base pattern;
|
||||
# set to True to replace pixels with hash(seed, idx) instead.
|
||||
"zoom.stealth.canvas.substitute_pixels": False,
|
||||
# Canvas substitution (Option B for canvas) — replace pixels with hash(seed,idx),
|
||||
# uniform-skip (red-box exact, masking-safe) + full overwrite. Makes the canvas
|
||||
# render a pure function of (seed) = HOST-INDEPENDENT (kills the DWrite-vs-FreeType
|
||||
# text-raster leak: Canvas Hash + Font hash were the residual Win!=Linux signals).
|
||||
# ON by default (paired with webgl.substitute_pixels).
|
||||
"zoom.stealth.canvas.substitute_pixels": True,
|
||||
|
||||
# WebGL substitution (Option B) — replace readback/snapshot RGB with
|
||||
# hash(seed,idx), endpoint-preserving. Makes the WebGL render hash a pure
|
||||
# function of (seed, dims) = HOST-INDEPENDENT, so no per-host hw_seed
|
||||
# calibration is needed (the gamma path was per-host: NVIDIA/Arc-on-Win clean
|
||||
# seeds went dirty on the Linux GL backend). ON by default.
|
||||
"zoom.stealth.webgl.substitute_pixels": True,
|
||||
|
||||
# WebGPU presence consistency. Firefox enables dom.webgpu.enabled by default on
|
||||
# Windows/Mac-ARM but NOT on Linux/Mac-x64. We ALWAYS claim Windows, so force it ON
|
||||
# on every host: a Windows FF MUST expose navigator.gpu (object); a Linux host leaving
|
||||
# it undefined while the UA says Windows is an inconsistency tell (RE 2026-06-22:
|
||||
# has_gpu was object on Win, undefined on WSL). adapter.info is empty (FF privacy
|
||||
# default) so no GPU-name leak; requestAdapter may be null on a GPU-less host, which
|
||||
# is itself plausible for a real Windows machine.
|
||||
"dom.webgpu.enabled": True,
|
||||
|
||||
# Audio fingerprint noise OFF. RE 2026-06-22: the per-session OfflineAudioContext
|
||||
# noise (gated by hw_seed) was THE dominant driver of FP Pro tampering_ml on Windows
|
||||
# — b005 Win dropped 0.4349 -> 0.0564 with audio noise alone disabled (canvas_text/
|
||||
# emoji unchanged, so they were a red-herring). The audio value is already host-indep
|
||||
# AND identical to a real FF's canonical OfflineAudioContext sum, so a fixed (un-noised)
|
||||
# audio is NOT a linking signal (every real FF has the same value) — removing the noise
|
||||
# matches real Firefox and clears the tampering flag.
|
||||
"zoom.stealth.audio.fp_noise": False,
|
||||
|
||||
# Navigator identity (locked to Windows Firefox 150).
|
||||
**_NAVIGATOR_OVERRIDES,
|
||||
|
|
@ -452,59 +447,17 @@ _WIN_VIRT_DESKTOP_WORKAROUNDS: Dict[str, Any] = {
|
|||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _accept_language(locale: str) -> str:
|
||||
# "<locale>, <base>" — the desktop-default shape (e.g. "en-US, en"). Firefox expands it
|
||||
# to navigator.languages=["en-US","en"] AND (via the patched binary) the q-valued header
|
||||
# "en-US,en;q=0.5". The patched nsHttpHandler (STEALTHFOX, RE 2026-06-23) builds the
|
||||
# Accept-Language header from THIS pref even when juggler sets a per-context locale
|
||||
# override, so header and navigator.languages stay consistent 2/2 — the most authentic
|
||||
# (real desktop) form. Supersedes the 2026-06-22 single-tag workaround.
|
||||
lang = locale.replace("_", "-")
|
||||
base = lang.split("-")[0]
|
||||
return f"{lang}, {base}" if base != lang else lang
|
||||
|
||||
|
||||
def _font_metrics_for_platform(profile_metrics: str) -> str:
|
||||
"""Return ``zoom.stealth.font.metrics`` value.
|
||||
|
||||
The C++ whitelist hook (``gfxPlatformFontList::FindAndAddFamiliesLocked``)
|
||||
backs EVERY whitelisted *named* family with the list-head family on every
|
||||
platform. Without per-font width factors, that means each named font
|
||||
(Arial, Times New Roman, Courier New, …) renders with identical glyphs and
|
||||
collapses to a SINGLE canvas ``measureText`` width — a non-physical
|
||||
1-distinct-width result that strict JS-sensor anti-bots flag via their
|
||||
font probe. The per-font factors in ``profile_metrics``
|
||||
(``arial|0.978,arial black|1.168,…``) spread the fabricated families back
|
||||
to distinct, realistic, deterministic-per-seed widths, so we apply them on
|
||||
EVERY platform (previously suppressed on Windows/mac, which left the
|
||||
collapse in place — only the CSS-generic vector, which FP Pro probes, was
|
||||
ever correct there).
|
||||
|
||||
These factors only key *named* families. CSS generics
|
||||
(serif/sans-serif/monospace/system-ui) bypass the whitelist entirely and
|
||||
render at the host's native widths, so they are never present in
|
||||
``profile_metrics`` and stay unfactored — FP Pro's ``font_preferences``
|
||||
probe (which measures the generics) is unaffected. That is also why
|
||||
applying named-font factors here does NOT distort the canonical generic
|
||||
widths.
|
||||
|
||||
Linux ADDITIONALLY needs generic-family compensation
|
||||
(``_LINUX_GENERIC_FONT_FACTORS``) because DejaVu/Liberation generics render
|
||||
wider/narrower than the Windows widths the spoofed profile claims; on
|
||||
Windows/mac the generics already render native, so no generic compensation
|
||||
is applied — only the named-font factors.
|
||||
"""
|
||||
if not profile_metrics:
|
||||
return ""
|
||||
# profile_metrics arrives as "name|<universal_real_width>,..." (host-independent
|
||||
# absolute widths, e.g. "arial|2256.7"). We pass them through UNCHANGED: the C++
|
||||
# hook treats a value >= 10 as an ABSOLUTE target measureText width and divides it
|
||||
# by the host's own collapse base (self-calibrating), so the SAME value yields the
|
||||
# exact Windows width on Windows, Linux AND macOS. No per-OS division here.
|
||||
metrics = profile_metrics
|
||||
# Linux ADDITIONALLY needs CSS-generic compensation (DejaVu/Liberation generics
|
||||
# render wider/narrower than Windows). These are multiplicative FACTORS (< 10),
|
||||
# calibrated to FP Pro's font_preferences probe; the C++ hook treats values < 10 as
|
||||
# factors. Generics bypass the whitelist/collapse so they are NOT self-calibrated.
|
||||
# Windows/mac generics render native -> no compensation.
|
||||
if sys.platform.startswith("linux"):
|
||||
return _LINUX_GENERIC_FONT_FACTORS + metrics
|
||||
return metrics
|
||||
|
||||
|
||||
def translate_profile_to_prefs(
|
||||
profile: Profile,
|
||||
*,
|
||||
|
|
@ -606,15 +559,37 @@ def translate_profile_to_prefs(
|
|||
prefs["media.mediasource.webm.enabled"] = profile.codec.mediasource_webm
|
||||
prefs["media.mediasource.mp4.enabled"] = profile.codec.mediasource_mp4
|
||||
|
||||
# Fonts
|
||||
prefs["zoom.stealth.font.whitelist"] = ",".join(profile.fonts)
|
||||
prefs["zoom.stealth.font.metrics"] = _font_metrics_for_platform(
|
||||
profile._raw.get("font_metrics", "") or ""
|
||||
)
|
||||
# Reference string the binary sums the collapsed font's advances over to
|
||||
# self-calibrate the per-host collapse base (turns the absolute widths in
|
||||
# `metrics` into the right factor on any OS). See _font_metrics_for_platform.
|
||||
prefs["zoom.stealth.font.calib_ref"] = _FONT_CALIB_REF
|
||||
# Fonts — real bundled fonts (no collapse). The binary ships the real
|
||||
# Windows font files in <GRE>/fonts and loads them via MOZ_BUNDLED_FONTS, so
|
||||
# glyphs are genuine on every host. We expose this profile's family set via
|
||||
# the per-profile fontlist, which the binary applies to the native system
|
||||
# font allow-list AT CONSTRUCTION (no runtime rebuild → no scan stall), and
|
||||
# force the system-ui generic to Segoe UI. profile.fonts is the _fpforge
|
||||
# sample (core always + a conditioned optional subset). Per-profile metric
|
||||
# uniqueness comes from the shared fpp.hw_seed jitter in the HarfBuzz shaper
|
||||
# (set with the other fpp prefs), not from fabricated widths.
|
||||
prefs["zoom.stealth.font.fontlist"] = ",".join(profile.fonts)
|
||||
prefs["zoom.stealth.font.system_ui"] = "Segoe UI"
|
||||
|
||||
# Activate the bundled real-Windows fonts (MOZ_BUNDLED_FONTS / <GRE>/fonts).
|
||||
prefs["gfx.bundled-fonts.activate"] = 1
|
||||
# Point the CSS generics at Windows defaults. On a Windows HOST Firefox's
|
||||
# built-in name-lists already resolve to these (and they're in the fontlist
|
||||
# so they survive the allow-list); but on a non-Windows host (Linux/Mac) the
|
||||
# built-in defaults name host fonts (DejaVu/Liberation/…) which the allow-list
|
||||
# hides — so the generics would collapse. Setting them explicitly keeps the
|
||||
# generics resolving to the bundled Windows families on EVERY host (system-ui
|
||||
# is forced to Segoe UI by the C++ hook above, so it is not listed here).
|
||||
prefs["font.name-list.serif.x-western"] = "Times New Roman"
|
||||
prefs["font.name-list.sans-serif.x-western"] = "Arial"
|
||||
prefs["font.name-list.monospace.x-western"] = "Consolas"
|
||||
prefs["font.name-list.sans-serif.ja"] = "Yu Gothic UI"
|
||||
prefs["font.name-list.serif.ja"] = "Yu Gothic UI"
|
||||
prefs["font.name-list.sans-serif.ko"] = "Malgun Gothic"
|
||||
prefs["font.name-list.serif.ko"] = "Malgun Gothic"
|
||||
prefs["font.name-list.sans-serif.zh-CN"] = "Microsoft YaHei UI"
|
||||
prefs["font.name-list.sans-serif.zh-TW"] = "Microsoft JhengHei UI"
|
||||
prefs["font.name-list.sans-serif.zh-HK"] = "Microsoft JhengHei UI"
|
||||
|
||||
# UI / dark mode + Windows colors palette (only when light theme).
|
||||
prefs["ui.systemUsesDarkTheme"] = int(profile.dark_theme)
|
||||
|
|
@ -628,6 +603,15 @@ def translate_profile_to_prefs(
|
|||
prefs["general.useragent.locale"] = lang
|
||||
prefs["intl.locale.requested"] = lang
|
||||
prefs["privacy.spoof_english"] = 0
|
||||
# juggler.locale.override seeds the BrowsingContext LanguageOverride FIELD in
|
||||
# the parent process (BrowsingContext::Attach), whose DidSet drives BOTH
|
||||
# navigator.languages (the full list) AND the realm Intl default locale (the
|
||||
# primary tag it extracts) — so Intl.DateTimeFormat / NumberFormat /
|
||||
# toLocaleString follow the locale, not just the Accept-Language header. Seed
|
||||
# it with the full Accept-Language list so navigator.languages stays the
|
||||
# desktop-default 2 elements (["fr-FR","fr"]); the C++ DidSet takes "fr-FR"
|
||||
# for Intl. Mirrors juggler.timezone.override; the SOLE source of truth.
|
||||
prefs["juggler.locale.override"] = _accept_language(locale)
|
||||
|
||||
if timezone:
|
||||
# juggler.timezone.override is the SOLE source of truth read by the C++
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
"""Canvas / WebGL render-stealth regression tests (binary-level, 2026-06-18).
|
||||
"""Canvas / WebGL render-stealth regression test (binary-level, 2026-06-18).
|
||||
|
||||
Two patched-binary behaviours that must never regress, both needed for the
|
||||
Guards a patched-binary behaviour that must never regress, needed for the
|
||||
fingerprint to look like a real Windows browser to FOSS detectors (CreepJS,
|
||||
FingerprintJS, BrowserLeaks) and to image-dedup font probes / fixed-hash
|
||||
reference checks:
|
||||
FingerprintJS, BrowserLeaks) and fixed-hash reference checks:
|
||||
|
||||
1. Per-font canvas distinctness — whitelisted named fonts are backed by the
|
||||
host list-head glyphs (so measureText widths are host-independent), but each
|
||||
must still rasterise to a DISTINCT image at tiny probe sizes. Otherwise an
|
||||
image-dedup font probe collapses them to ~1 name and the reported font set
|
||||
looks fabricated. (C++: per-font sub-pixel draw offset in DrawText.)
|
||||
2. Solid WebGL readback purity under render-noise — a fixed solid-colour WebGL
|
||||
readback (which reference checks hash against a universal constant) must stay
|
||||
byte-exact even with per-seed render-noise enabled, while high-entropy
|
||||
renders stay noised. (C++: render-noise skips near-uniform WebGL readbacks.)
|
||||
Solid WebGL readback purity under render-noise — a fixed solid-colour WebGL
|
||||
readback (which reference checks hash against a universal constant) must stay
|
||||
byte-exact even with per-seed render-noise enabled, while high-entropy
|
||||
renders stay noised. (C++: render-noise skips near-uniform WebGL readbacks.)
|
||||
|
||||
(Per-font canvas distinctness is no longer guarded here: the font-collapse +
|
||||
per-font draw offset were removed on 2026-06-20 in favour of real bundled
|
||||
Windows fonts, which rasterise to distinct images by nature.)
|
||||
|
||||
Runs against about:blank, no network/proxy. Part of the e2e release gate.
|
||||
Run: pytest tests/test_canvas_render_stealth.py -m e2e -v
|
||||
|
|
@ -23,34 +21,6 @@ from __future__ import annotations
|
|||
import pytest
|
||||
|
||||
from invisible_playwright import InvisiblePlaywright
|
||||
from invisible_playwright import prefs as _prefs
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
|
||||
# Diverse-codepoint probe string — maximises per-font rendering differences, the
|
||||
# way an image-dedup font probe drives a tiny canvas.
|
||||
_PROBE = ("\U0001f6cd1>'`amlρiюदे來˦"
|
||||
"\U00025578に◌\U0002003eԩԨ")
|
||||
|
||||
|
||||
def _named_fonts(limit: int = 30) -> list[str]:
|
||||
"""The whitelisted NAMED fonts (absolute collapse-target width >= 10) for the
|
||||
test seed — these are the ones the per-font offset must keep distinct."""
|
||||
prof = generate_profile(42)
|
||||
metrics = _prefs._font_metrics_for_platform(prof._raw.get("font_metrics", "") or "")
|
||||
out: list[str] = []
|
||||
for ent in metrics.split(","):
|
||||
name, _, val = ent.partition("|")
|
||||
if not val:
|
||||
continue
|
||||
try:
|
||||
if float(val.replace("px", "")) >= 10.0:
|
||||
out.append(name)
|
||||
except ValueError:
|
||||
pass
|
||||
return out[:limit]
|
||||
|
||||
|
||||
_FONTS = _named_fonts()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
|
@ -68,34 +38,6 @@ def noised_page(firefox_binary):
|
|||
yield p
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_named_fonts_render_distinct_canvas_images(noised_page):
|
||||
"""Each whitelisted named font must produce a DISTINCT tiny-canvas image so an
|
||||
image-dedup font probe keeps every name. Regression: without the per-font draw
|
||||
offset all whitelisted fonts share the list-head glyphs -> ~1-2 distinct
|
||||
images -> degenerate detected-font set."""
|
||||
assert len(_FONTS) >= 10, "expected a non-trivial named-font whitelist to probe"
|
||||
distinct = noised_page.evaluate(
|
||||
"""(args) => {
|
||||
const [fonts, V] = args;
|
||||
const c = document.createElement('canvas'); c.width = 90; c.height = 12;
|
||||
const d = c.getContext('2d'); d.fillStyle = 'red';
|
||||
const seen = new Set();
|
||||
for (const f of fonts) {
|
||||
d.clearRect(0, 0, 90, 12);
|
||||
d.font = 'normal 4px "' + f + '"';
|
||||
d.fillText(V, 5, 8);
|
||||
seen.add(c.toDataURL());
|
||||
}
|
||||
return seen.size;
|
||||
}""",
|
||||
[_FONTS, _PROBE],
|
||||
)
|
||||
# broken (offset removed) collapses to ~1-2; require nearly all distinct.
|
||||
assert distinct >= len(_FONTS) - 2, \
|
||||
f"only {distinct}/{len(_FONTS)} distinct font images (per-font offset regressed?)"
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_solid_webgl_readback_stays_pure_under_noise(noised_page):
|
||||
"""A solid-colour WebGL readback must remain byte-exact (only {0,255}) with
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ _REQUIRED_PREFS_KEYS = (
|
|||
"media.encoder.webm.enabled",
|
||||
"media.mediasource.webm.enabled",
|
||||
"media.mediasource.mp4.enabled",
|
||||
"zoom.stealth.font.whitelist",
|
||||
"zoom.stealth.font.metrics",
|
||||
"zoom.stealth.font.fontlist",
|
||||
"zoom.stealth.font.system_ui",
|
||||
"ui.systemUsesDarkTheme",
|
||||
"intl.accept_languages",
|
||||
"general.useragent.locale",
|
||||
|
|
@ -197,23 +197,26 @@ def test_http_proxy_returned_unchanged_no_socks_mutations():
|
|||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# IT7: profile.fonts reaches prefs as a comma-joined whitelist
|
||||
# IT7: profile.fonts reaches prefs as a comma-joined fontlist
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_profile_fonts_propagate_to_prefs_whitelist():
|
||||
def test_profile_fonts_propagate_to_prefs_fontlist():
|
||||
"""IT7 — every font in ``profile.fonts`` appears in the comma-joined
|
||||
``zoom.stealth.font.whitelist`` pref, in order."""
|
||||
``zoom.stealth.font.fontlist`` pref, in order. The binary applies this
|
||||
list to the native system font allow-list at construction; system-ui is
|
||||
forced to Segoe UI."""
|
||||
profile = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(profile)
|
||||
|
||||
assert profile.fonts, "fixture seed=42 produced empty fonts list"
|
||||
whitelist = prefs["zoom.stealth.font.whitelist"]
|
||||
assert isinstance(whitelist, str)
|
||||
assert whitelist == ",".join(profile.fonts)
|
||||
fontlist = prefs["zoom.stealth.font.fontlist"]
|
||||
assert isinstance(fontlist, str)
|
||||
assert fontlist == ",".join(profile.fonts)
|
||||
for font in profile.fonts:
|
||||
assert font in whitelist
|
||||
assert font in fontlist
|
||||
assert prefs["zoom.stealth.font.system_ui"] == "Segoe UI"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -357,23 +360,3 @@ def test_linux_msaa_pin_propagates_through_pipeline(monkeypatch):
|
|||
assert prefs["webgl.msaa-force"] is True
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# IT13 (extra): Linux font metrics receive the GTK/DejaVu compensation
|
||||
# block. End-to-end check that ``_LINUX_GENERIC_FONT_FACTORS`` is
|
||||
# prepended to the per-font metrics string sampled from the profile.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_linux_font_metrics_include_generic_factors(monkeypatch):
|
||||
"""IT13 — on Linux the font metrics pref starts with the generic
|
||||
width-scale factors (GTK/DejaVu compensation) so glyph widths match
|
||||
Windows. Without this, Linux sessions leak via metric drift."""
|
||||
from invisible_playwright.prefs import _LINUX_GENERIC_FONT_FACTORS
|
||||
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
profile = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(profile)
|
||||
|
||||
metrics = prefs["zoom.stealth.font.metrics"]
|
||||
assert metrics.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ def test_default_context_omits_locale_when_empty():
|
|||
def test_build_env_injects_webrtc_egress_when_discovered():
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # what __enter__ resolves behind a proxy
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "203.0.113.9"
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ def test_build_env_no_webrtc_keys_without_proxy(monkeypatch):
|
|||
monkeypatch.delenv("STEALTHFOX_WEBRTC_PUBLIC_IP", raising=False)
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = None # no proxy → real STUN already truthful
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert "STEALTHFOX_WEBRTC_PUBLIC_IP" not in env
|
||||
assert "STEALTHFOX_WEBRTC_DISABLE_IPV6" not in env
|
||||
|
||||
|
|
@ -201,6 +201,29 @@ def test_build_env_caller_env_override_wins(monkeypatch):
|
|||
monkeypatch.setenv("STEALTHFOX_WEBRTC_PUBLIC_IP", "198.51.100.5")
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # auto-discovered
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "198.51.100.5" # caller wins
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_injects_font_list_and_system_ui():
|
||||
# The binary reads these at the gfxPlatformFontList constructor (process
|
||||
# start); Playwright delivers firefox_user_prefs over juggler AFTER start, so
|
||||
# the env var is the only at-construction channel. Without it host fonts leak
|
||||
# on Linux/macOS (the wrapper's pref-only delivery was a cross-OS gap).
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
env = ip._build_env({
|
||||
"zoom.stealth.font.fontlist": "arial,calibri,segoe ui",
|
||||
"zoom.stealth.font.system_ui": "Segoe UI",
|
||||
})
|
||||
assert env["STEALTHFOX_FONTLIST"] == "arial,calibri,segoe ui"
|
||||
assert env["STEALTHFOX_SYSTEMUI"] == "Segoe UI"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_no_font_keys_when_absent():
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
env = ip._build_env({})
|
||||
assert "STEALTHFOX_FONTLIST" not in env
|
||||
assert "STEALTHFOX_SYSTEMUI" not in env
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import pytest
|
|||
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
from invisible_playwright.prefs import (
|
||||
_LINUX_GENERIC_FONT_FACTORS,
|
||||
_accept_language,
|
||||
_font_metrics_for_platform,
|
||||
_WIN_LIGHT_COLORS,
|
||||
translate_profile_to_prefs,
|
||||
)
|
||||
|
|
@ -82,29 +80,6 @@ def test_accept_language_underscore_normalized():
|
|||
assert _accept_language("pt_BR") == "pt-BR, pt"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# _font_metrics_for_platform
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_windows_applies_named_factors(monkeypatch):
|
||||
# FM2: Windows/mac apply the per-NAMED-font factors (so whitelisted named
|
||||
# families don't collapse to the list-head width on the canvas measureText
|
||||
# path), but WITHOUT the Linux generic-family compensation (generics bypass
|
||||
# the whitelist and render native there).
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
out = _font_metrics_for_platform("Arial|1.0,Verdana|0.9,")
|
||||
assert out == "Arial|1.0,Verdana|0.9,"
|
||||
assert "sans-serif|" not in out # no generic compensation on Windows
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_empty_input_returns_empty():
|
||||
# FM3: Empty input always returns "" regardless of platform.
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Platform-specific GPU / MSAA (Windows)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -385,34 +360,6 @@ def test_lan_ip_seed_zero_has_no_zero_octets():
|
|||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux_prepends_generic_factors(monkeypatch):
|
||||
# FM1: Linux prepends the GTK/DejaVu compensation block to the
|
||||
# per-font metrics string sampled from the profile.
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
out = _font_metrics_for_platform("Arial|1.0,Verdana|0.9,")
|
||||
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
assert out.endswith("Arial|1.0,Verdana|0.9,")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux_empty_input_returns_empty(monkeypatch):
|
||||
# FM1b: even on Linux, empty profile metrics short-circuits before
|
||||
# the prepend so we never emit a metrics pref containing only the
|
||||
# generic block (which would surface as a tampering signal).
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux2_variant_uses_linux_branch(monkeypatch):
|
||||
# FM1c: ``sys.platform`` can be ``linux2`` on older Pythons / odd
|
||||
# WSL builds. ``startswith("linux")`` accepts both.
|
||||
monkeypatch.setattr(sys, "platform", "linux2")
|
||||
out = _font_metrics_for_platform("Verdana|0.9,")
|
||||
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_renderer_set_from_profile_on_linux(monkeypatch):
|
||||
# PG1: on Linux (as on EVERY host) we apply the camoufox-derived Windows-ANGLE GPU persona,
|
||||
|
|
|
|||
|
|
@ -216,12 +216,15 @@ def test_screen_tier_4200x2000_is_ultrawide_via_width_branch():
|
|||
# ── derive_font_prefs / derive_font_whitelist ───────────────────────────
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_returns_whitelist_and_metrics_keys():
|
||||
"""FP1 [HAPPY]: result has the two expected string keys."""
|
||||
def test_derive_font_prefs_returns_whitelist_key():
|
||||
"""FP1 [HAPPY]: result is a single-key dict with the font family list.
|
||||
|
||||
The per-family ``metrics`` string was removed on 2026-06-20: fonts now
|
||||
render from the bundled real Windows files (genuine widths) and per-session
|
||||
metric uniqueness comes from the HarfBuzz jitter, not fabricated factors."""
|
||||
out = derive_font_prefs("integrated_modern", random.Random(42))
|
||||
assert set(out.keys()) == {"whitelist", "metrics"}
|
||||
assert set(out.keys()) == {"whitelist"}
|
||||
assert isinstance(out["whitelist"], str)
|
||||
assert isinstance(out["metrics"], str)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
@ -249,15 +252,6 @@ def test_derive_font_prefs_unknown_class_falls_back_to_integrated_modern():
|
|||
assert fallback == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_metrics_and_whitelist_are_coherent():
|
||||
"""FP5 [ECP]: every name in whitelist has a metrics entry and vice versa."""
|
||||
out = derive_font_prefs("mid_range", random.Random(99))
|
||||
wl_names = out["whitelist"].split(",")
|
||||
metrics_names = [s.split("|", 1)[0] for s in out["metrics"].split(",")]
|
||||
assert wl_names == metrics_names
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_whitelist_alphabetically_sorted():
|
||||
"""FP6 [ECP]: whitelist names are sorted (ordering invariant for stable dedup)."""
|
||||
|
|
@ -280,10 +274,12 @@ def test_derive_font_whitelist_legacy_shim_matches_dict_form():
|
|||
# machine never lacks them, so a session that drops one advertises a font set that
|
||||
# doesn't match any real Windows profile (image-dedup font probes then report a
|
||||
# short/degenerate name list → server-side OS-font-set checks fail). Calibri in
|
||||
# particular sat in `optional` (a bug); these five caused the detected set to come
|
||||
# up short on some seeds. Regression guard for the 2026-06-18 optional→core move.
|
||||
# particular sat in `optional` (a bug); these caused the detected set to come up
|
||||
# short on some seeds. Regression guard for the 2026-06-18 optional→core move.
|
||||
# NB: the exact Win11 family is "franklin gothic medium" (there is no bare
|
||||
# "franklin gothic" family); the 2026-06-20 bundle reconciliation uses real names.
|
||||
_STANDARD_WINDOWS_FONTS = [
|
||||
"calibri", "franklin gothic", "gadugi", "javanese text", "myanmar text",
|
||||
"calibri", "franklin gothic medium", "gadugi", "javanese text", "myanmar text",
|
||||
]
|
||||
_ALL_GPU_CLASSES = [
|
||||
"integrated_old", "integrated_modern", "mid_range", "high_end",
|
||||
|
|
@ -294,16 +290,14 @@ _ALL_GPU_CLASSES = [
|
|||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_standard_windows_fonts_always_present_every_class_and_seed(gpu_class):
|
||||
"""FP7 [regression]: the standard-Windows fonts appear in BOTH whitelist and
|
||||
metrics for every gpu_class across many seeds (i.e. they are core, not
|
||||
profile-optional). Guards against a standard font silently becoming optional."""
|
||||
"""FP7 [regression]: the standard-Windows fonts appear in the whitelist for
|
||||
every gpu_class across many seeds (i.e. they are core, not profile-optional).
|
||||
Guards against a standard font silently becoming optional."""
|
||||
for seed in range(40):
|
||||
out = derive_font_prefs(gpu_class, random.Random(seed))
|
||||
wl = set(out["whitelist"].split(","))
|
||||
metrics_names = {s.split("|", 1)[0] for s in out["metrics"].split(",")}
|
||||
for font in _STANDARD_WINDOWS_FONTS:
|
||||
assert font in wl, f"{font!r} missing from whitelist (class={gpu_class}, seed={seed})"
|
||||
assert font in metrics_names, f"{font!r} missing from metrics (class={gpu_class}, seed={seed})"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
@ -320,33 +314,13 @@ def test_standard_windows_fonts_are_in_core_pool():
|
|||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_derive_font_prefs_no_duplicate_families(gpu_class):
|
||||
"""FP9 [regression]: no family appears twice in whitelist/metrics, even when a
|
||||
"""FP9 [regression]: no family appears twice in the whitelist, even when a
|
||||
profile's optional list also names a core font. Guards the dedup in
|
||||
derive_font_prefs (a duplicate family would emit a malformed pref pair)."""
|
||||
derive_font_prefs (a duplicate family would emit a malformed list)."""
|
||||
for seed in range(30):
|
||||
out = derive_font_prefs(gpu_class, random.Random(seed))
|
||||
wl = out["whitelist"].split(",")
|
||||
metrics_names = [s.split("|", 1)[0] for s in out["metrics"].split(",")]
|
||||
assert len(wl) == len(set(wl)), f"duplicate in whitelist (class={gpu_class}, seed={seed})"
|
||||
assert len(metrics_names) == len(set(metrics_names)), \
|
||||
f"duplicate in metrics (class={gpu_class}, seed={seed})"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_derive_font_prefs_named_fonts_emit_absolute_widths(gpu_class):
|
||||
"""FP10 [regression]: every emitted metrics value is a positive number; named
|
||||
(non-generic) fonts carry an ABSOLUTE collapse-target width (>= 10), which the
|
||||
binary self-calibrates per host. A value < 10 here would mean a font slipped
|
||||
through as a bare multiplicative factor and would render at the wrong width."""
|
||||
out = derive_font_prefs(gpu_class, random.Random(3))
|
||||
for entry in out["metrics"].split(","):
|
||||
name, _, val = entry.partition("|")
|
||||
v = float(val.replace("px", ""))
|
||||
assert v > 0.0, f"non-positive metrics value for {name!r}"
|
||||
# the standard named fonts must be absolute (collapse-target) widths
|
||||
if name in _STANDARD_WINDOWS_FONTS:
|
||||
assert v >= 10.0, f"{name!r} emitted as factor {v} (<10), expected absolute width"
|
||||
|
||||
|
||||
# ── Forge / sample ──────────────────────────────────────────────────────
|
||||
|
|
@ -364,7 +338,7 @@ _EXPECTED_KEYS = {
|
|||
"av1_enabled", "webm_encoder_enabled",
|
||||
"mediasource_webm", "mediasource_mp4", "webspeech_synth",
|
||||
"storage_quota_mb", "dark_theme",
|
||||
"font_whitelist", "font_metrics",
|
||||
"font_whitelist",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -453,10 +427,9 @@ def test_forge_sample_avail_h_defaults_to_h_minus_40_when_missing(monkeypatch):
|
|||
|
||||
@pytest.mark.unit
|
||||
def test_forge_sample_includes_font_keys():
|
||||
"""FS9 [ECP]: font_whitelist + font_metrics present and non-empty."""
|
||||
"""FS9 [ECP]: font_whitelist present and non-empty (the joined family list)."""
|
||||
out = sample(42)
|
||||
assert out["font_whitelist"]
|
||||
assert out["font_metrics"]
|
||||
assert "," in out["font_whitelist"] # at least the core fonts joined
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue