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