PageSourceSearch

https://miqr.mx/assets/utils/qr_canvas-42041151.js

js miqr.mx collected 2026-10-02 01:05:02 UTC 19,803 bytes, 663 lines download raw bytes

1export function roundRect(ctx, x, y, w, h, r) {
2  if (w < 2 * r) r = w / 2;
3  if (h < 2 * r) r = h / 2;
4  ctx.beginPath();
5  ctx.moveTo(x + r, y);
6  ctx.lineTo(x + w - r, y);
7  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
8  ctx.lineTo(x + w, y + h - r);
9  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
10  ctx.lineTo(x + r, y + h);
11  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
12  ctx.lineTo(x, y + r);
13  ctx.quadraticCurveTo(x, y, x + r, y);
14  ctx.closePath();
15}
16
17export function leafRect(ctx, x, y, w, h, r) {
18  ctx.beginPath();
19  ctx.moveTo(x + r, y);
20  ctx.lineTo(x + w, y);
21  ctx.lineTo(x + w, y + h - r);
22  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
23  ctx.lineTo(x, y + h);
24  ctx.lineTo(x, y + r);
25  ctx.quadraticCurveTo(x, y, x + r, y);
26  ctx.closePath();
27}
28
29export function drawModule(ctx, x, y, size, style, extra) {
30  const s = size,
31    h = s / 2,
32    r = s * 0.35;
33  switch (style) {
34    case 'rounded':
35      const rad = s * 0.3;
36      ctx.beginPath();
37      ctx.moveTo(x + rad, y);
38      ctx.lineTo(x + s - rad, y);
39      ctx.quadraticCurveTo(x + s, y, x + s, y + rad);
40      ctx.lineTo(x + s, y + s - rad);
41      ctx.quadraticCurveTo(x + s, y + s, x + s - rad, y + s);
42      ctx.lineTo(x + rad, y + s);
43      ctx.quadraticCurveTo(x, y + s, x, y + s - rad);
44      ctx.lineTo(x, y + rad);
45      ctx.quadraticCurveTo(x, y, x + rad, y);
46      ctx.closePath();
47      ctx.fill();
48      break;
49    case 'dots':
50      ctx.beginPath();
51      ctx.arc(x + h, y + h, r, 0, Math.PI * 2);
52      ctx.fill();
53      break;
54    case 'diamond':
55      ctx.beginPath();
56      ctx.moveTo(x + h, y);
57      ctx.lineTo(x + s, y + h);
58      ctx.lineTo(x + h, y + s);
59      ctx.lineTo(x, y + h);
60      ctx.closePath();
61      ctx.fill();
62      break;
63    case 'bars':
64      ctx.fillRect(x + s * 0.15, y, s * 0.7, s);
65      break;
66    case 'connected':
67      const g = s * 0.08,
68        cr = s * 0.35,
69        n = extra?.neighbors || {},
70        x1 = n.left ? x - g : x + g,
71        x2 = n.right ? x + s + g : x + s - g,
72        y1 = n.top ? y - g : y + g,
73        y2 = n.bottom ? y + s + g : y + s - g,
74        rr = cr;
75      ctx.beginPath();
76      ctx.moveTo(x1 + (n.left ? 0 : rr), y1);
77      ctx.lineTo(x2 - (n.right ? 0 : rr), y1);
78      if (!n.right) ctx.quadraticCurveTo(x2, y1, x2, y1 + rr);
79      else ctx.lineTo(x2, y1);
80      ctx.lineTo(x2, y2 - (n.bottom ? 0 : rr));
81      if (!n.bottom) ctx.quadraticCurveTo(x2, y2, x2 - rr, y2);
82      else ctx.lineTo(x2, y2);
83      ctx.lineTo(x1 + (n.left ? 0 : rr), y2);
84      if (!n.left) ctx.quadraticCurveTo(x1, y2, x1, y2 - rr);
85      else ctx.lineTo(x1, y2);
86      ctx.lineTo(x1, y1 + (n.top ? 0 : rr));
87      if (!n.top) ctx.quadraticCurveTo(x1, y1, x1 + rr, y1);
88      else ctx.lineTo(x1, y1);
89      ctx.closePath();
90      ctx.fill();
91      break;
92    default:
93      ctx.fillRect(x, y, s, s);
94  }
95}
96
97export function drawFinderPattern(ctx, x, y, cellSize, fg, bg, style) {
98  const s7 = cellSize * 7,
99    s5 = cellSize * 5,
100    s3 = cellSize * 3,
101    r7 = cellSize * 1.5,
102    r5 = cellSize * 1.0,
103    r3 = cellSize * 0.8;
104  switch (style) {
105    case 'rounded':
106      ctx.fillStyle = fg;
107      roundRect(ctx, x, y, s7, s7, r7);
108      ctx.fill();
109      ctx.fillStyle = bg;
110      roundRect(ctx, x + cellSize, y + cellSize, s5, s5, r5);
111      ctx.fill();
112      ctx.fillStyle = fg;
113      roundRect(ctx, x + cellSize * 2, y + cellSize * 2, s3, s3, r3);
114      ctx.fill();
115      break;
116    case 'circle':
117      const cx = x + s7 / 2,
118        cy = y + s7 / 2;
119      ctx.fillStyle = fg;
120      ctx.beginPath();
121      ctx.arc(cx, cy, s7 / 2, 0, Math.PI * 2);
122      ctx.fill();
123      ctx.fillStyle = bg;
124      ctx.beginPath();
125      ctx.arc(cx, cy, s5 / 2, 0, Math.PI * 2);
126      ctx.fill();
127      ctx.fillStyle = fg;
128      ctx.beginPath();
129      ctx.arc(cx, cy, s3 / 2, 0, Math.PI * 2);
130      ctx.fill();
131      break;
132    case 'leaf':
133      ctx.fillStyle = fg;
134      leafRect(ctx, x, y, s7, s7, r7);
135      ctx.fill();
136      ctx.fillStyle = bg;
137      leafRect(ctx, x + cellSize, y + cellSize, s5, s5, r5);
138      ctx.fill();
139      ctx.fillStyle = fg;
140      leafRect(ctx, x + cellSize * 2, y + cellSize * 2, s3, s3, r3);
141      ctx.fill();
142      break;
143    default:
144      ctx.fillStyle = fg;
145      ctx.fillRect(x, y, s7, s7);
146      ctx.fillStyle = bg;
147      ctx.fillRect(x + cellSize, y + cellSize, s5, s5);
148      ctx.fillStyle = fg;
149      ctx.fillRect(x + cellSize * 2, y + cellSize * 2, s3, s3);
150  }
151}
152
153export function drawFramePreset(
154  ctx,
155  frameStyle,
156  fg,
157  cellSize,
158  mc,
159  margin,
160  offX,
161  offY,
162  size,
163  labels = {},
164) {
165  if (!frameStyle || frameStyle === 'none') return;
166  const qrStartX = offX + margin * cellSize,
167    qrStartY = offY + margin * cellSize,
168    qrSize = mc * cellSize,
169    qrEndX = qrStartX + qrSize,
170    qrEndY = qrStartY + qrSize,
171    qrCenter = size / 2;
172
173  const gap = Math.max(8, Math.floor(size * 0.06));
174  const fX = qrStartX - gap,
175    fY = qrStartY - gap,
176    fW = qrSize + gap * 2,
177    fH = qrSize + gap * 2;
178
179  ctx.lineJoin = 'round';
180  ctx.lineCap = 'round';
181
182  if (frameStyle === 'classic') {
183    ctx.strokeStyle = fg;
184    ctx.lineWidth = Math.max(2, Math.floor(size * 0.015));
185    ctx.strokeRect(fX, fY, fW, fH);
186  } else if (frameStyle === 'scan') {
187    ctx.strokeStyle = fg;
188    ctx.lineWidth = Math.max(2, Math.floor(size * 0.015));
189    ctx.strokeRect(fX, fY, fW, fH);
190    const fontSize = Math.max(Math.floor(size * 0.08), 10);
191    ctx.font = `800 ${fontSize}px -apple-system, sans-serif`;
192    ctx.fillStyle = fg;
193    ctx.textAlign = 'center';
194    ctx.fillText(labels.scanMe || '', qrCenter, qrEndY + gap + fontSize);
195  } else if (frameStyle === 'badge') {
196    const badgeW = Math.floor(size * 0.7),
197      badgeH = Math.floor(size * 0.16),
198      badgeX = (size - badgeW) / 2,
199      badgeY = fY - badgeH - Math.floor(size * 0.01);
200    ctx.fillStyle = fg;
201    roundRect(ctx, badgeX, badgeY, badgeW, badgeH, Math.floor(size * 0.04));
202    ctx.fill();
203    ctx.fillStyle = '#ffffff';
204    ctx.textAlign = 'center';
205    const fontSize = Math.max(10, Math.floor(badgeH * 0.5));
206    ctx.font = `800 ${fontSize}px -apple-system, sans-serif`;
207    ctx.fillText(labels.scanHere || '', qrCenter, badgeY + badgeH * 0.65);
208    ctx.strokeStyle = fg;
209    ctx.lineWidth = Math.max(2, Math.floor(size * 0.015));
210    ctx.strokeRect(fX, fY, fW, fH);
211  } else if (frameStyle === 'phone') {
212    const pW = fW + Math.floor(size * 0.06),
213      pH = fH + Math.floor(size * 0.2),
214      pX = (size - pW) / 2,
215      pY = (size - pH) / 2 + Math.floor(size * 0.02);
216    ctx.strokeStyle = fg;
217    ctx.lineWidth = Math.max(4, Math.floor(size * 0.035));
218    roundRect(ctx, pX, pY, pW, pH, Math.floor(size * 0.1));
219    ctx.stroke();
220    ctx.fillStyle = fg;
221    const nW = Math.floor(pW * 0.35),
222      nH = Math.floor(pH * 0.05);
223    roundRect(ctx, qrCenter - nW / 2, pY - 2, nW, nH, nH / 2);
224    ctx.fill();
225    const hW = Math.floor(pW * 0.25),
226      hH = Math.max(2, Math.floor(pH * 0.015));
227    roundRect(ctx, qrCenter - hW / 2, pY + pH - hH * 2.5, hW, hH, hH / 2);
228    ctx.fill();
229  } else if (frameStyle === 'chat') {
230    ctx.strokeStyle = fg;
231    ctx.lineWidth = Math.max(3, Math.floor(size * 0.02));
232    roundRect(ctx, fX, fY, fW, fH, Math.floor(size * 0.08));
233    ctx.stroke();
234    const tW = Math.floor(fW * 0.15);
235    ctx.beginPath();
236    ctx.moveTo(qrCenter - tW, fY + fH);
237    ctx.lineTo(qrCenter, fY + fH + tW);
238    ctx.lineTo(qrCenter + tW, fY + fH);
239    ctx.fillStyle = fg;
240    ctx.fill();
241  } else if (frameStyle === 'bracket') {
242    const len = Math.floor(fW * 0.25);
243    ctx.strokeStyle = fg;
244    ctx.lineWidth = Math.max(4, Math.floor(size * 0.025));
245    ctx.beginPath();
246    ctx.moveTo(fX, fY + len);
247    ctx.lineTo(fX, fY);
248    ctx.lineTo(fX + len, fY);
249    ctx.stroke();
250    ctx.beginPath();
251    ctx.moveTo(fX + fW - len, fY);
252    ctx.lineTo(fX + fW, fY);
253    ctx.lineTo(fX + fW, fY + len);
254    ctx.stroke();
255    ctx.beginPath();
256    ctx.moveTo(fX, fY + fH - len);
257    ctx.lineTo(fX, fY + fH);
258    ctx.lineTo(fX + len, fY + fH);
259    ctx.stroke();
260    ctx.beginPath();
261    ctx.moveTo(fX + fW - len, fY + fH);
262    ctx.lineTo(fX + fW, fY + fH);
263    ctx.lineTo(fX + fW, fY + fH - len);
264    ctx.stroke();
265  }
266}
267
268export function drawLogo(ctx, x, y, size, logoImage) {
269  if (!logoImage) return;
270  const logoSize = Math.floor(size * 0.22),
271    lx = x + Math.floor((size - logoSize) / 2),
272    ly = y + Math.floor((size - logoSize) / 2),
273    pad = 3;
274  ctx.fillStyle = '#ffffff';
275  ctx.fillRect(lx - pad, ly - pad, logoSize + pad * 2, logoSize + pad * 2);
276  ctx.drawImage(logoImage, lx, ly, logoSize, logoSize);
277}
278
279export function drawCaption(ctx, text, size, fg) {
280  if (!text) return 0;
281  const fontSize = Math.max(Math.floor(size * 0.08), 10);
282  ctx.font = `600 ${fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
283  ctx.fillStyle = fg;
284  ctx.textAlign = 'center';
285  ctx.fillText(text, size / 2, size + fontSize + 4);
286  return fontSize + 8;
287}
288
289export function lerpColor(c1, c2, t) {
290  const r1 = parseInt(c1.slice(1, 3), 16),
291    g1 = parseInt(c1.slice(3, 5), 16),
292    b1 = parseInt(c1.slice(5, 7), 16),
293    r2 = parseInt(c2.slice(1, 3), 16),
294    g2 = parseInt(c2.slice(3, 5), 16),
295    b2 = parseInt(c2.slice(5, 7), 16),
296    r = Math.round(r1 + (r2 - r1) * t),
297    g = Math.round(g1 + (g2 - g1) * t),
298    b = Math.round(b1 + (b2 - b1) * t);
299  return `rgb(${r},${g},${b})`;
300}
301
302export function shiftHue(hex) {
303  let r = parseInt(hex.slice(1, 3), 16) / 255,
304    g = parseInt(hex.slice(3, 5), 16) / 255,
305    b = parseInt(hex.slice(5, 7), 16) / 255;
306  const max = Math.max(r, g, b),
307    min = Math.min(r, g, b),
308    d = max - min;
309  let h = 0,
310    s = max === 0 ? 0 : d / max,
311    v = max;
312  if (d !== 0) {
313    if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
314    else if (max === g) h = ((b - r) / d + 2) / 6;
315    else h = ((r - g) / d + 4) / 6;
316  }
317  h = (h + 0.33) % 1;
318  const i = Math.floor(h * 6),
319    f = h * 6 - i,
320    p = v * (1 - s),
321    q = v * (1 - f * s),
322    t2 = v * (1 - (1 - f) * s);
323  let r2, g2, b2;
324  switch (i % 6) {
325    case 0:
326      r2 = v;
327      g2 = t2;
328      b2 = p;
329      break;
330    case 1:
331      r2 = q;
332      g2 = v;
333      b2 = p;
334      break;
335    case 2:
336      r2 = p;
337      g2 = v;
338      b2 = t2;
339      break;
340    case 3:
341      r2 = p;
342      g2 = q;
343      b2 = v;
344      break;
345    case 4:
346      r2 = t2;
347      g2 = p;
348      b2 = v;
349      break;
350    case 5:
351      r2 = v;
352      g2 = p;
353      b2 = q;
354      break;
355  }
356  return (
357    '#' +
358    [r2, g2, b2]
359      .map((x) =>
360        Math.round(x * 255)
361          .toString(16)
362          .padStart(2, '0'),
363      )
364      .join('')
365  );
366}
367
368export function isStrictScanPayload(text) {
369  const t = (text || '').trim();
370  return t.includes('BEGIN:VCARD') || t.startsWith('WIFI:');
371}
372
373export function isFinderCell(r, c, mc) {
374  if (r < 7 && c < 7) return true;
375  if (r < 7 && c >= mc - 7) return true;
376  if (r >= mc - 7 && c < 7) return true;
377  return false;
378}
379
380export function getNeighbors(qr, row, col) {
381  const mc = qr.getModuleCount();
382  return {
383    top: row > 0 && qr.isDark(row - 1, col),
384    bottom: row < mc - 1 && qr.isDark(row + 1, col),
385    left: col > 0 && qr.isDark(row, col - 1),
386    right: col < mc - 1 && qr.isDark(row, col + 1),
387  };
388}
389
390export function buildSVG(
391  qr,
392  { caption, fg, bg, targetSize, logoData = null, visualBlending = false },
393) {
394  const mc = qr.getModuleCount(),
395    margin = 2;
396
397  const { cellSize, offsetX, offsetY, qrPx } = calculateQRLayout(targetSize, mc, margin);
398  const esc = (s) =>
399    (s || '').replace(
400      /[&<>"']/g,
401      (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
402    );
403
404  const isBlending = visualBlending && logoData;
405  const captionH = caption ? Math.max(Math.floor(targetSize * 0.08), 14) + 12 : 0;
406  const totalH = targetSize + captionH;
407
408  let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${targetSize}" height="${totalH}" viewBox="0 0 ${targetSize} ${totalH}">`;
409
410  svg += `<rect width="${targetSize}" height="${totalH}" fill="${esc(bg)}" rx="16"/>`;
411
412  if (visualBlending && logoData) {
413    svg += `<image x="${offsetX}" y="${offsetY}" width="${qrPx}" height="${qrPx}" xlink:href="${esc(logoData)}" opacity="0.6" preserveAspectRatio="xMidYMid slice" clip-path="url(#qr-clip)"/>`;
414    svg += `<defs><clipPath id="qr-clip"><rect x="${offsetX}" y="${offsetY}" width="${qrPx}" height="${qrPx}" rx="16"/></clipPath></defs>`;
415
416    const overlayColor = bg.toLowerCase() === '#ffffff' ? 'white' : 'black';
417    svg += `<rect x="${offsetX}" y="${offsetY}" width="${qrPx}" height="${qrPx}" fill="${overlayColor}" opacity="0.05" rx="16"/>`;
418  }
419
420  for (let r = 0; r < mc; r++) {
421    for (let c = 0; c < mc; c++) {
422      if (isFinderCell(r, c, mc)) continue;
423      if (qr.isDark(r, c)) {
424        const mx = offsetX + (c + margin) * cellSize;
425        const my = offsetY + (r + margin) * cellSize;
426        svg += `<rect x="${mx}" y="${my}" width="${cellSize}" height="${cellSize}" fill="${esc(fg)}"/>`;
427      }
428    }
429  }
430
431  const s7 = cellSize * 7;
432  const finders = [
433    { x: offsetX + margin * cellSize, y: offsetY + margin * cellSize },
434    { x: offsetX + (mc - 7 + margin) * cellSize, y: offsetY + margin * cellSize },
435    { x: offsetX + margin * cellSize, y: offsetY + (mc - 7 + margin) * cellSize },
436  ];
437
438  finders.forEach((f) => {
439    if (isBlending) {
440      svg += `<rect x="${f.x - cellSize}" y="${f.y - cellSize}" width="${s7 + cellSize * 2}" height="${s7 + cellSize * 2}" fill="${esc(bg)}" opacity="0.8"/>`;
441    }
442
443    svg += `<rect x="${f.x}" y="${f.y}" width="${s7}" height="${s7}" fill="${esc(fg)}"/>`;
444    svg += `<rect x="${f.x + cellSize}" y="${f.y + cellSize}" width="${cellSize * 5}" height="${cellSize * 5}" fill="${esc(bg)}"/>`;
445    svg += `<rect x="${f.x + cellSize * 2}" y="${f.y + cellSize * 2}" width="${cellSize * 3}" height="${cellSize * 3}" fill="${esc(fg)}"/>`;
446  });
447
448  if (!isBlending && logoData) {
449    const logoSize = Math.floor(qrPx * 0.22);
450    const lx = offsetX + Math.floor((qrPx - logoSize) / 2);
451    const ly = offsetY + Math.floor((qrPx - logoSize) / 2);
452
453    svg += `<rect x="${lx - 3}" y="${ly - 3}" width="${logoSize + 6}" height="${logoSize + 6}" fill="white"/>`;
454    svg += `<image x="${lx}" y="${ly}" width="${logoSize}" height="${logoSize}" xlink:href="${esc(logoData)}"/>`;
455  }
456
457  if (caption) {
458    const fontSize = Math.max(Math.floor(targetSize * 0.08), 14);
459    svg += `<text x="${targetSize / 2}" y="${targetSize + fontSize}" text-anchor="middle" font-family="-apple-system, sans-serif" font-weight="600" font-size="${fontSize}" fill="${esc(fg)}">${esc(caption)}</text>`;
460  }
461
462  svg += `</svg>`;
463  return svg;
464}
465
466export function renderSmallQR(
467  qrcodeInstance,
468  canvas,
469  {
470    content,
471    fg = '#1a1a1a',
472    bg = '#ffffff',
473    style = 'square',
474    fStyle = 'square',
475    frameStyle = 'none',
476    frameColor = null,
477    size = 44,
478    margin = 1,
479    labels = {},
480  } = {},
481) {
482  const qr = qrcodeInstance(0, 'M');
483  qr.addData(content || 'miqr.mx', 'Byte');
484  qr.make();
485
486  canvas.width = size;
487  canvas.height = size;
488  const ctx = canvas.getContext('2d');
489  ctx.fillStyle = bg;
490  ctx.fillRect(0, 0, size, size);
491
492  const mc = qr.getModuleCount(),
493    cs = Math.floor(size / (mc + margin * 2)),
494    off = Math.floor((size - (mc + margin * 2) * cs) / 2);
495
496  for (let r = 0; r < mc; r++) {
497    for (let c = 0; c < mc; c++) {
498      if (isFinderCell(r, c, mc)) continue;
499      if (!qr.isDark(r, c)) continue;
500      ctx.fillStyle = fg;
501      const extra = style === 'connected' ? { neighbors: getNeighbors(qr, r, c) } : null;
502      drawModule(ctx, off + (c + margin) * cs, off + (r + margin) * cs, cs, style, extra);
503    }
504  }
505
506  drawFinderPattern(ctx, off + margin * cs, off + margin * cs, cs, fg, bg, fStyle);
507  drawFinderPattern(ctx, off + (mc - 7 + margin) * cs, off + margin * cs, cs, fg, bg, fStyle);
508  drawFinderPattern(ctx, off + margin * cs, off + (mc - 7 + margin) * cs, cs, fg, bg, fStyle);
509
510  if (frameStyle !== 'none') {
511    drawFramePreset(ctx, frameStyle, frameColor || fg, cs, mc, margin, off, off, size, labels);
512  }
513}
514
515export function calculateQRLayout(targetSize, mc, margin) {
516  const topPad = Math.floor(targetSize * 0.2);
517  const bottomPad = Math.floor(targetSize * 0.25);
518  const sidePad = Math.floor(targetSize * 0.08);
519  const availableW = targetSize - sidePad * 2;
520  const availableH = targetSize - (topPad + bottomPad);
521  const availableSize = Math.min(availableW, availableH);
522  const cellSize = Math.floor(availableSize / (mc + margin * 2));
523  const qrPx = (mc + margin * 2) * cellSize;
524  const offsetX = Math.floor((targetSize - qrPx) / 2);
525  const offsetY = Math.floor((targetSize - (topPad + bottomPad) - qrPx) / 2) + topPad;
526  return { cellSize, offsetX, offsetY, qrPx, topPad, bottomPad, sidePad };
527}
528
529export function drawBackground(ctx, x, y, size, backgroundImage, alpha = 0.5) {
530  if (!backgroundImage) return;
531  ctx.save();
532  ctx.globalAlpha = alpha;
533
534  roundRect(ctx, x, y, size, size, 16);
535  ctx.clip();
536  const aspect = backgroundImage.width / backgroundImage.height;
537  let drawW, drawH, drawX, drawY;
538  if (aspect > 1) {
539    drawH = size;
540    drawW = size * aspect;
541    drawX = x - (drawW - size) / 2;
542    drawY = y;
543  } else {
544    drawW = size;
545    drawH = size / aspect;
546    drawX = x;
547    drawY = y - (drawH - size) / 2;
548  }
549  ctx.drawImage(backgroundImage, drawX, drawY, drawW, drawH);
550  ctx.restore();
551}
552
553export function drawQRComponents(
554  ctx,
555  qr,
556  {
557    fg,
558    bg,
559    style,
560    fStyle,
561    frameStyle,
562    frameColor,
563    cellSize,
564    mc,
565    margin,
566    offsetX,
567    offsetY,
568    targetSize,
569    labels,
570    backgroundImage = null,
571    visualBlending = false,
572  },
573) {
574  const qrPx = (mc + margin * 2) * cellSize;
575  const isBlending = visualBlending && backgroundImage;
576
577  if (isBlending) {
578    drawBackground(ctx, offsetX, offsetY, qrPx, backgroundImage, 0.6);
579    ctx.fillStyle = bg.toLowerCase() === '#ffffff' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)';
580    ctx.fillRect(offsetX, offsetY, qrPx, qrPx);
581  }
582
583  if (frameStyle !== 'none') {
584    drawFramePreset(
585      ctx,
586      frameStyle,
587      frameColor || fg,
588      cellSize,
589      mc,
590      margin,
591      offsetX,
592      offsetY,
593      targetSize,
594      labels,
595    );
596  }
597
598  ctx.save();
599  if (isBlending) {
600    ctx.globalAlpha = 1.0;
601  }
602  for (let r = 0; r < mc; r++) {
603    for (let c = 0; c < mc; c++) {
604      if (isFinderCell(r, c, mc)) continue;
605      if (!qr.isDark(r, c)) continue;
606      ctx.fillStyle = fg;
607      const extra = style === 'connected' ? { neighbors: getNeighbors(qr, r, c) } : null;
608      drawModule(
609        ctx,
610        offsetX + (c + margin) * cellSize,
611        offsetY + (r + margin) * cellSize,
612        cellSize,
613        style,
614        extra,
615      );
616    }
617  }
618  ctx.restore();
619
620  ctx.save();
621  if (isBlending) {
622    const s7 = cellSize * 7;
623    const finders = [
624      { x: offsetX + margin * cellSize, y: offsetY + margin * cellSize },
625      { x: offsetX + (mc - 7 + margin) * cellSize, y: offsetY + margin * cellSize },
626      { x: offsetX + margin * cellSize, y: offsetY + (mc - 7 + margin) * cellSize },
627    ];
628    finders.forEach((f) => {
629      ctx.fillStyle = bg;
630      ctx.globalAlpha = 0.8;
631      ctx.fillRect(f.x - cellSize, f.y - cellSize, s7 + cellSize * 2, s7 + cellSize * 2);
632    });
633    ctx.globalAlpha = 1.0;
634  }
635  drawFinderPattern(
636    ctx,
637    offsetX + margin * cellSize,
638    offsetY + margin * cellSize,
639    cellSize,
640    fg,
641    bg,
642    fStyle,
643  );
644  drawFinderPattern(
645    ctx,
646    offsetX + (mc - 7 + margin) * cellSize,
647    offsetY + margin * cellSize,
648    cellSize,
649    fg,
650    bg,
651    fStyle,
652  );
653  drawFinderPattern(
654    ctx,
655    offsetX + margin * cellSize,
656    offsetY + (mc - 7 + margin) * cellSize,
657    cellSize,
658    fg,
659    bg,
660    fStyle,
661  );
662  ctx.restore();
663}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.