@zaplier/sdk 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -173,8 +173,10 @@ function hash32(input) {
173
173
  * Hash fingerprint components into a stable identifier
174
174
  */
175
175
  function hashFingerprint(components) {
176
- // Convert components to canonical string representation
177
- const canonical = JSON.stringify(components, Object.keys(components).sort());
176
+ // Convert components to canonical string representation using deep sorting
177
+ // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
178
+ // Use canonicalizeStable instead which handles deep sorting and normalization.
179
+ const canonical = JSON.stringify(canonicalizeStable(components));
178
180
  // Use MurmurHash3 x64 for consistent hashing
179
181
  return x64hash128(canonical);
180
182
  }
@@ -1027,7 +1029,13 @@ function supportsWebGL() {
1027
1029
  // Quick stability check
1028
1030
  const webglContext = gl;
1029
1031
  const renderer = webglContext.getParameter(webglContext.RENDERER);
1030
- return Boolean(renderer && typeof renderer === "string");
1032
+ const isSupported = Boolean(renderer && typeof renderer === "string");
1033
+ // Cleanup context to avoid "Too many active WebGL contexts" warning
1034
+ const ext = webglContext.getExtension("WEBGL_lose_context");
1035
+ if (ext) {
1036
+ ext.loseContext();
1037
+ }
1038
+ return isSupported;
1031
1039
  }
1032
1040
  catch {
1033
1041
  return false;
@@ -1341,11 +1349,11 @@ function isBrowserFingerprintingAvailable() {
1341
1349
  * Text to render for canvas fingerprinting with maximum differentiation
1342
1350
  */
1343
1351
  const CANVAS_TEXTS = [
1344
- 'RabbitTracker Canvas 🎨🔒2024',
1345
- 'Żółć gęślą jaźń €$¢£¥',
1346
- 'αβγδεζηθικλμνξο',
1347
- '中文测试字体渲染',
1348
- '🌟🎯🚀💎🌊🎨'
1352
+ "Zap Canvas 🎨🔒2024",
1353
+ "Żółć gęślą jaźń €$¢£¥",
1354
+ "αβγδεζηθικλμνξο",
1355
+ "中文测试字体渲染",
1356
+ "🌟🎯🚀💎🌊🎨",
1349
1357
  ];
1350
1358
  /**
1351
1359
  * Enhanced geometric shapes for canvas fingerprinting with sub-pixel precision
@@ -1353,19 +1361,19 @@ const CANVAS_TEXTS = [
1353
1361
  function drawAdvancedGeometry(ctx) {
1354
1362
  // Enable high-quality rendering for sub-pixel differences
1355
1363
  ctx.imageSmoothingEnabled = true;
1356
- ctx.imageSmoothingQuality = 'high';
1364
+ ctx.imageSmoothingQuality = "high";
1357
1365
  // Complex gradient with multiple stops
1358
1366
  const radialGradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 50);
1359
- radialGradient.addColorStop(0, 'rgba(255, 0, 0, 0.8)');
1360
- radialGradient.addColorStop(0.3, 'rgba(0, 255, 0, 0.6)');
1361
- radialGradient.addColorStop(0.7, 'rgba(0, 0, 255, 0.4)');
1362
- radialGradient.addColorStop(1, 'rgba(255, 255, 0, 0.2)');
1367
+ radialGradient.addColorStop(0, "rgba(255, 0, 0, 0.8)");
1368
+ radialGradient.addColorStop(0.3, "rgba(0, 255, 0, 0.6)");
1369
+ radialGradient.addColorStop(0.7, "rgba(0, 0, 255, 0.4)");
1370
+ radialGradient.addColorStop(1, "rgba(255, 255, 0, 0.2)");
1363
1371
  ctx.fillStyle = radialGradient;
1364
1372
  ctx.fillRect(10, 10, 130, 100);
1365
1373
  // Sub-pixel positioned shapes for GPU/anti-aliasing differentiation
1366
- ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
1374
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
1367
1375
  ctx.fillRect(15.5, 15.3, 49.7, 49.2);
1368
- ctx.fillStyle = '#f60';
1376
+ ctx.fillStyle = "#f60";
1369
1377
  ctx.fillRect(70.3, 10.7, 50.1, 50.9);
1370
1378
  // Complex bezier curves that stress different rendering engines
1371
1379
  ctx.beginPath();
@@ -1373,21 +1381,21 @@ function drawAdvancedGeometry(ctx) {
1373
1381
  ctx.bezierCurveTo(25.2, 120.1, 75.8, 90.3, 125.5, 120.7);
1374
1382
  ctx.bezierCurveTo(125.5, 120.7, 90.1, 150.2, 60.8, 140.9);
1375
1383
  ctx.closePath();
1376
- ctx.fillStyle = 'rgba(200, 100, 50, 0.6)';
1384
+ ctx.fillStyle = "rgba(200, 100, 50, 0.6)";
1377
1385
  ctx.fill();
1378
1386
  // Overlapping circles with complex blending
1379
- ctx.globalCompositeOperation = 'multiply';
1387
+ ctx.globalCompositeOperation = "multiply";
1380
1388
  ctx.beginPath();
1381
1389
  ctx.arc(50.7, 80.3, 20.1, 0, Math.PI * 2);
1382
- ctx.fillStyle = 'rgba(255, 0, 100, 0.5)';
1390
+ ctx.fillStyle = "rgba(255, 0, 100, 0.5)";
1383
1391
  ctx.fill();
1384
1392
  ctx.beginPath();
1385
1393
  ctx.arc(70.3, 80.7, 20.9, 0, Math.PI * 2);
1386
- ctx.fillStyle = 'rgba(0, 255, 100, 0.5)';
1394
+ ctx.fillStyle = "rgba(0, 255, 100, 0.5)";
1387
1395
  ctx.fill();
1388
- ctx.globalCompositeOperation = 'source-over';
1396
+ ctx.globalCompositeOperation = "source-over";
1389
1397
  // Thin lines to test anti-aliasing
1390
- ctx.strokeStyle = 'rgba(50, 50, 50, 0.8)';
1398
+ ctx.strokeStyle = "rgba(50, 50, 50, 0.8)";
1391
1399
  ctx.lineWidth = 0.5;
1392
1400
  ctx.setLineDash([2.3, 1.7]);
1393
1401
  for (let i = 0; i < 10; i++) {
@@ -1399,29 +1407,29 @@ function drawAdvancedGeometry(ctx) {
1399
1407
  // Reset line dash
1400
1408
  ctx.setLineDash([]);
1401
1409
  // Pattern with complex repeating elements
1402
- const patternCanvas = document.createElement('canvas');
1410
+ const patternCanvas = document.createElement("canvas");
1403
1411
  patternCanvas.width = 20;
1404
1412
  patternCanvas.height = 20;
1405
- const patternCtx = patternCanvas.getContext('2d');
1413
+ const patternCtx = patternCanvas.getContext("2d");
1406
1414
  if (patternCtx) {
1407
- patternCtx.fillStyle = 'rgba(150, 75, 200, 0.3)';
1415
+ patternCtx.fillStyle = "rgba(150, 75, 200, 0.3)";
1408
1416
  patternCtx.fillRect(0, 0, 10, 10);
1409
1417
  patternCtx.fillRect(10, 10, 10, 10);
1410
- const pattern = ctx.createPattern(patternCanvas, 'repeat');
1418
+ const pattern = ctx.createPattern(patternCanvas, "repeat");
1411
1419
  if (pattern) {
1412
1420
  ctx.fillStyle = pattern;
1413
1421
  ctx.fillRect(150, 120, 80, 60);
1414
1422
  }
1415
1423
  }
1416
1424
  // Shadow effects for additional GPU differentiation
1417
- ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
1425
+ ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
1418
1426
  ctx.shadowBlur = 3.2;
1419
1427
  ctx.shadowOffsetX = 2.1;
1420
1428
  ctx.shadowOffsetY = 2.7;
1421
- ctx.fillStyle = 'rgba(100, 200, 150, 0.8)';
1429
+ ctx.fillStyle = "rgba(100, 200, 150, 0.8)";
1422
1430
  ctx.fillRect(160, 30, 60, 40);
1423
1431
  // Reset shadow
1424
- ctx.shadowColor = 'transparent';
1432
+ ctx.shadowColor = "transparent";
1425
1433
  ctx.shadowBlur = 0;
1426
1434
  ctx.shadowOffsetX = 0;
1427
1435
  ctx.shadowOffsetY = 0;
@@ -1431,23 +1439,34 @@ function drawAdvancedGeometry(ctx) {
1431
1439
  */
1432
1440
  function drawAdvancedText(ctx) {
1433
1441
  const fonts = [
1434
- '14px Arial, sans-serif',
1442
+ "14px Arial, sans-serif",
1435
1443
  '13px "Times New Roman", serif',
1436
- '12px Georgia, serif',
1437
- '15px Helvetica, Arial, sans-serif',
1444
+ "12px Georgia, serif",
1445
+ "15px Helvetica, Arial, sans-serif",
1438
1446
  '11px "Courier New", monospace',
1439
- '13px Verdana, sans-serif',
1440
- '16px Impact, fantasy',
1441
- '12px "Comic Sans MS", cursive'
1447
+ "13px Verdana, sans-serif",
1448
+ "16px Impact, fantasy",
1449
+ '12px "Comic Sans MS", cursive',
1442
1450
  ];
1443
1451
  const colors = [
1444
- '#000', '#333', '#666', '#999',
1445
- 'rgba(255, 0, 0, 0.8)', 'rgba(0, 255, 0, 0.7)',
1446
- 'rgba(0, 0, 255, 0.6)', 'rgba(128, 64, 192, 0.9)'
1452
+ "#000",
1453
+ "#333",
1454
+ "#666",
1455
+ "#999",
1456
+ "rgba(255, 0, 0, 0.8)",
1457
+ "rgba(0, 255, 0, 0.7)",
1458
+ "rgba(0, 0, 255, 0.6)",
1459
+ "rgba(128, 64, 192, 0.9)",
1447
1460
  ];
1448
1461
  // Test different text baselines and alignments
1449
- const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'bottom'];
1450
- const alignments = ['left', 'center', 'right'];
1462
+ const baselines = [
1463
+ "top",
1464
+ "hanging",
1465
+ "middle",
1466
+ "alphabetic",
1467
+ "bottom",
1468
+ ];
1469
+ const alignments = ["left", "center", "right"];
1451
1470
  let y = 250;
1452
1471
  CANVAS_TEXTS.forEach((text, textIndex) => {
1453
1472
  fonts.forEach((font, fontIndex) => {
@@ -1456,11 +1475,11 @@ function drawAdvancedText(ctx) {
1456
1475
  const alignIndex = fontIndex % alignments.length;
1457
1476
  ctx.font = font;
1458
1477
  ctx.fillStyle = colors[colorIndex] || colors[0];
1459
- ctx.textBaseline = baselines[baselineIndex] || 'top';
1460
- ctx.textAlign = alignments[alignIndex] || 'left';
1478
+ ctx.textBaseline = baselines[baselineIndex] || "top";
1479
+ ctx.textAlign = alignments[alignIndex] || "left";
1461
1480
  // Sub-pixel positioning for enhanced differentiation
1462
- const x = 10 + (fontIndex * 0.7) % 200;
1463
- const yPos = y + (fontIndex * 18.3) % 100;
1481
+ const x = 10 + ((fontIndex * 0.7) % 200);
1482
+ const yPos = y + ((fontIndex * 18.3) % 100);
1464
1483
  ctx.fillText(text, x, yPos);
1465
1484
  // Add stroke text for additional GPU stress testing
1466
1485
  if (fontIndex % 2 === 0) {
@@ -1476,20 +1495,20 @@ function drawAdvancedText(ctx) {
1476
1495
  ctx.translate(100, 400);
1477
1496
  ctx.rotate(Math.PI / 6);
1478
1497
  ctx.scale(1.2, 0.8);
1479
- ctx.font = 'italic 14px Arial';
1480
- ctx.fillStyle = 'rgba(200, 100, 50, 0.8)';
1481
- ctx.fillText('Transformed & Rotated Text', 0, 0);
1498
+ ctx.font = "italic 14px Arial";
1499
+ ctx.fillStyle = "rgba(200, 100, 50, 0.8)";
1500
+ ctx.fillText("Transformed & Rotated Text", 0, 0);
1482
1501
  ctx.restore();
1483
1502
  // Test very small and very large text
1484
- ctx.font = '6px Arial';
1485
- ctx.fillStyle = '#000';
1486
- ctx.fillText('Tiny text rendering test', 10, 500);
1487
- ctx.font = '32px Arial';
1488
- ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
1489
- ctx.fillText('Large', 10, 540);
1503
+ ctx.font = "6px Arial";
1504
+ ctx.fillStyle = "#000";
1505
+ ctx.fillText("Tiny text rendering test", 10, 500);
1506
+ ctx.font = "32px Arial";
1507
+ ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
1508
+ ctx.fillText("Large", 10, 540);
1490
1509
  // Reset text properties
1491
- ctx.textAlign = 'left';
1492
- ctx.textBaseline = 'top';
1510
+ ctx.textAlign = "left";
1511
+ ctx.textBaseline = "top";
1493
1512
  }
1494
1513
  /**
1495
1514
  * Analyze sub-pixel differences for enhanced GPU/anti-aliasing detection
@@ -1499,9 +1518,9 @@ function analyzeSubPixels(ctx, canvas) {
1499
1518
  // Create test pattern for sub-pixel analysis
1500
1519
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1501
1520
  // Draw shapes at sub-pixel coordinates to stress anti-aliasing
1502
- ctx.fillStyle = 'rgb(100, 150, 200)';
1521
+ ctx.fillStyle = "rgb(100, 150, 200)";
1503
1522
  ctx.fillRect(10.3, 10.7, 20.1, 20.9);
1504
- ctx.strokeStyle = 'rgb(200, 100, 50)';
1523
+ ctx.strokeStyle = "rgb(200, 100, 50)";
1505
1524
  ctx.lineWidth = 1.3;
1506
1525
  ctx.beginPath();
1507
1526
  ctx.moveTo(15.2, 35.8);
@@ -1532,7 +1551,7 @@ function analyzeSubPixels(ctx, canvas) {
1532
1551
  return `${avgRed}-${avgGreen}-${avgBlue}-${avgVariance}`;
1533
1552
  }
1534
1553
  catch (error) {
1535
- return 'subpixel-error';
1554
+ return "subpixel-error";
1536
1555
  }
1537
1556
  }
1538
1557
  /**
@@ -1545,47 +1564,52 @@ function detectAdvancedInconsistencies(textHash, geometryHash, subPixelData, ctx
1545
1564
  // Test 1: Check if getImageData works consistently
1546
1565
  const imageData = ctx.getImageData(0, 0, 1, 1);
1547
1566
  if (!imageData || imageData.data.length === 0) {
1548
- reasons.push('getImageData-blocked');
1567
+ reasons.push("getImageData-blocked");
1549
1568
  suspiciousSignals += 3;
1550
1569
  }
1551
1570
  // Test 2: Check hash lengths (blocked canvas often returns default values)
1552
1571
  if (textHash.length < 8 || geometryHash.length < 8) {
1553
- reasons.push('short-hash');
1572
+ reasons.push("short-hash");
1554
1573
  suspiciousSignals += 2;
1555
1574
  }
1556
1575
  // Test 3: Check for known blocked patterns
1557
1576
  const knownBlockedHashes = [
1558
- 'error', 'blocked', 'disabled',
1559
- '00000000', 'ffffffff', '12345678'
1577
+ "error",
1578
+ "blocked",
1579
+ "disabled",
1580
+ "00000000",
1581
+ "ffffffff",
1582
+ "12345678",
1560
1583
  ];
1561
- if (knownBlockedHashes.includes(textHash) || knownBlockedHashes.includes(geometryHash)) {
1562
- reasons.push('known-blocked-hash');
1584
+ if (knownBlockedHashes.includes(textHash) ||
1585
+ knownBlockedHashes.includes(geometryHash)) {
1586
+ reasons.push("known-blocked-hash");
1563
1587
  suspiciousSignals += 3;
1564
1588
  }
1565
1589
  // Test 4: Sub-pixel analysis indicates blocking
1566
- if (subPixelData === 'subpixel-error' || subPixelData.includes('0-0-0')) {
1567
- reasons.push('subpixel-anomaly');
1590
+ if (subPixelData === "subpixel-error" || subPixelData.includes("0-0-0")) {
1591
+ reasons.push("subpixel-anomaly");
1568
1592
  suspiciousSignals += 2;
1569
1593
  }
1570
1594
  // Test 5: Same hash for different operations (indicates fake canvas)
1571
- if (textHash === geometryHash && textHash !== 'error') {
1572
- reasons.push('identical-hashes');
1595
+ if (textHash === geometryHash && textHash !== "error") {
1596
+ reasons.push("identical-hashes");
1573
1597
  suspiciousSignals += 2;
1574
1598
  }
1575
1599
  // Test 6: toDataURL performance (some privacy tools slow it down)
1576
- const canvas = document.createElement('canvas');
1600
+ const canvas = document.createElement("canvas");
1577
1601
  canvas.width = 10;
1578
1602
  canvas.height = 10;
1579
- const testCtx = canvas.getContext('2d');
1603
+ const testCtx = canvas.getContext("2d");
1580
1604
  if (testCtx) {
1581
1605
  const start = performance.now();
1582
- testCtx.fillStyle = 'red';
1606
+ testCtx.fillStyle = "red";
1583
1607
  testCtx.fillRect(0, 0, 10, 10);
1584
1608
  canvas.toDataURL();
1585
1609
  const duration = performance.now() - start;
1586
1610
  // Suspiciously slow canvas operations
1587
1611
  if (duration > 50) {
1588
- reasons.push('slow-canvas');
1612
+ reasons.push("slow-canvas");
1589
1613
  suspiciousSignals += 1;
1590
1614
  }
1591
1615
  }
@@ -1597,7 +1621,7 @@ function detectAdvancedInconsistencies(textHash, geometryHash, subPixelData, ctx
1597
1621
  return {
1598
1622
  isInconsistent: true,
1599
1623
  confidence: 1,
1600
- reasons: ['detection-error']
1624
+ reasons: ["detection-error"],
1601
1625
  };
1602
1626
  }
1603
1627
  }
@@ -1608,29 +1632,30 @@ async function getCanvasFingerprint() {
1608
1632
  const startTime = performance.now();
1609
1633
  try {
1610
1634
  // Create larger canvas for more detailed fingerprinting
1611
- const canvas = document.createElement('canvas');
1635
+ const canvas = document.createElement("canvas");
1612
1636
  canvas.width = 300;
1613
1637
  canvas.height = 600;
1614
- const ctx = canvas.getContext('2d', {
1638
+ const ctx = canvas.getContext("2d", {
1615
1639
  alpha: true,
1616
1640
  desynchronized: false,
1617
- colorSpace: 'srgb'
1641
+ colorSpace: "srgb",
1642
+ willReadFrequently: true,
1618
1643
  });
1619
1644
  if (!ctx) {
1620
- throw new Error('Canvas 2D context not available');
1645
+ throw new Error("Canvas 2D context not available");
1621
1646
  }
1622
1647
  // Test canvas winding (different behavior across browsers/GPUs)
1623
1648
  ctx.rect(0, 0, 10, 10);
1624
1649
  ctx.rect(2, 2, 6, 6);
1625
- const isClockwise = ctx.isPointInPath(5, 5, 'evenodd');
1650
+ const isClockwise = ctx.isPointInPath(5, 5, "evenodd");
1626
1651
  // ENHANCED: Draw advanced text with multiple fonts and effects
1627
1652
  drawAdvancedText(ctx);
1628
- const textData = canvas.toDataURL('image/png');
1653
+ const textData = canvas.toDataURL("image/png");
1629
1654
  const textHash = hash32(textData);
1630
1655
  // Clear and draw advanced geometry
1631
1656
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1632
1657
  drawAdvancedGeometry(ctx);
1633
- const geometryData = canvas.toDataURL('image/png');
1658
+ const geometryData = canvas.toDataURL("image/png");
1634
1659
  const geometryHash = hash32(geometryData);
1635
1660
  // ENHANCED: Analyze sub-pixels for GPU/anti-aliasing differentiation
1636
1661
  const subPixelAnalysis = analyzeSubPixels(ctx, canvas);
@@ -1640,10 +1665,10 @@ async function getCanvasFingerprint() {
1640
1665
  ctx.clearRect(0, 0, canvas.width, canvas.height);
1641
1666
  // Draw both text and geometry together
1642
1667
  drawAdvancedText(ctx);
1643
- ctx.globalCompositeOperation = 'multiply';
1668
+ ctx.globalCompositeOperation = "multiply";
1644
1669
  drawAdvancedGeometry(ctx);
1645
- ctx.globalCompositeOperation = 'source-over';
1646
- const compositeData = canvas.toDataURL('image/png');
1670
+ ctx.globalCompositeOperation = "source-over";
1671
+ const compositeData = canvas.toDataURL("image/png");
1647
1672
  const compositeHash = hash32(compositeData);
1648
1673
  const endTime = performance.now();
1649
1674
  const result = {
@@ -1655,27 +1680,27 @@ async function getCanvasFingerprint() {
1655
1680
  subPixelAnalysis, // Sub-pixel characteristics for GPU differentiation
1656
1681
  compositeHash, // Combined text+geometry hash
1657
1682
  inconsistencyConfidence: inconsistencyAnalysis.confidence,
1658
- blockingReasons: inconsistencyAnalysis.reasons
1683
+ blockingReasons: inconsistencyAnalysis.reasons,
1659
1684
  };
1660
1685
  return {
1661
1686
  value: result,
1662
- duration: endTime - startTime
1687
+ duration: endTime - startTime,
1663
1688
  };
1664
1689
  }
1665
1690
  catch (error) {
1666
1691
  return {
1667
1692
  value: {
1668
- text: 'error',
1669
- geometry: 'error',
1693
+ text: "error",
1694
+ geometry: "error",
1670
1695
  winding: false,
1671
1696
  isInconsistent: true,
1672
- subPixelAnalysis: 'error',
1673
- compositeHash: 'error',
1697
+ subPixelAnalysis: "error",
1698
+ compositeHash: "error",
1674
1699
  inconsistencyConfidence: 1,
1675
- blockingReasons: ['canvas-error']
1700
+ blockingReasons: ["canvas-error"],
1676
1701
  },
1677
1702
  duration: performance.now() - startTime,
1678
- error: error instanceof Error ? error.message : 'Canvas fingerprinting failed'
1703
+ error: error instanceof Error ? error.message : "Canvas fingerprinting failed",
1679
1704
  };
1680
1705
  }
1681
1706
  }
@@ -1684,9 +1709,9 @@ async function getCanvasFingerprint() {
1684
1709
  */
1685
1710
  function isCanvasAvailable$1() {
1686
1711
  try {
1687
- const canvas = document.createElement('canvas');
1688
- const ctx = canvas.getContext('2d');
1689
- return ctx !== null && typeof ctx.fillText === 'function';
1712
+ const canvas = document.createElement("canvas");
1713
+ const ctx = canvas.getContext("2d");
1714
+ return ctx !== null && typeof ctx.fillText === "function";
1690
1715
  }
1691
1716
  catch {
1692
1717
  return false;
@@ -3784,7 +3809,7 @@ function analyzeRenderingQuality(iframeDoc, container, fontFamily = "Arial") {
3784
3809
  const canvas = iframeDoc.createElement("canvas");
3785
3810
  canvas.width = 100;
3786
3811
  canvas.height = 50;
3787
- const ctx = canvas.getContext("2d");
3812
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
3788
3813
  if (!ctx)
3789
3814
  return { quality: 0, antiAliasing: false, smoothing: 0 };
3790
3815
  // Draw text at sub-pixel position to trigger anti-aliasing
@@ -3837,7 +3862,7 @@ function detectSystemFontHints(iframeDoc, container) {
3837
3862
  const canvas = iframeDoc.createElement("canvas");
3838
3863
  canvas.width = 100;
3839
3864
  canvas.height = 50;
3840
- const ctx = canvas.getContext("2d");
3865
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
3841
3866
  if (!ctx) {
3842
3867
  return { clearType: false, hintingLevel: 0, subpixelRendering: false };
3843
3868
  }