All files / qr-code/src/render logo.ts

100% Statements 34/34
100% Branches 17/17
100% Functions 6/6
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91                                        27x 27x 27x   27x 1x                     26x 26x 26x 26x   26x 718x 20498x 20498x   20498x     20498x       26x   67x   27x                                       12x 12x 1x 1x   11x 11x 11x 11x 3x 11x      
import type { ErrorCorrectionLevel, LogoSafety, QRMatrix, QRStyleOptions } from "../types";
import { EC_LEVELS, EC_RECOVERY_RATIO } from "../encode/capacity";
import { QRExportError } from "../errors";
import { logoBox } from "./geometry";
import { resolveStyle } from "./style";
 
/**
 * Logo occlusion analysis.
 *
 * A centre logo works because Reed–Solomon can reconstruct the codewords it
 * hides — right up until it can't. This computes how much of the symbol a
 * given logo covers and compares it against what the chosen error-correction
 * level can actually absorb.
 *
 * The result is *reported*, never silently applied: quietly raising the EC
 * level would change the symbol's version and physical density behind the
 * caller's back (SPEC decision #7). Callers get a number and a
 * recommendation, and decide for themselves.
 */
export function logoSafety(matrix: QRMatrix, options: QRStyleOptions = {}): LogoSafety {
  const style = resolveStyle(options);
  const box = logoBox(matrix, style);
  const ecBudget = EC_RECOVERY_RATIO[matrix.level];
 
  if (!box) {
    return {
      occludedModules: 0,
      occludedRatio: 0,
      ecBudget,
      safe: true,
      recommendedLevel: matrix.level,
    };
  }
 
  // Counted independently of `excavate`: painting over a module hides it just
  // as thoroughly as clearing it.
  const { clear } = box;
  const centre = (matrix.size + style.margin * 2) / 2;
  const radius = clear.width / 2;
  let occludedModules = 0;
 
  for (let y = 0; y < matrix.size; y++) {
    for (let x = 0; x < matrix.size; x++) {
      const cx = style.margin + x + 0.5;
      const cy = style.margin + y + 0.5;
      const inside =
        box.shape === "circle"
          ? (cx - centre) ** 2 + (cy - centre) ** 2 <= radius * radius
          : cx >= clear.x && cx <= clear.x + clear.width && cy >= clear.y && cy <= clear.y + clear.height;
      if (inside) occludedModules++;
    }
  }
 
  const occludedRatio = occludedModules / (matrix.size * matrix.size);
  const recommendedLevel: ErrorCorrectionLevel | null =
    EC_LEVELS.find((level) => EC_RECOVERY_RATIO[level] >= occludedRatio) ?? null;
 
  return {
    occludedModules,
    occludedRatio,
    ecBudget,
    safe: occludedRatio <= ecBudget,
    recommendedLevel,
  };
}
 
/**
 * Load an image for raster export.
 *
 * Rejects with a {@link QRExportError} rather than a bare event, because "the
 * PNG came out blank" is otherwise one of the hardest things to diagnose in a
 * QR pipeline.
 */
export function loadImage(
  src: string,
  crossOrigin?: "anonymous" | "use-credentials",
): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    if (typeof Image === "undefined") {
      reject(new QRExportError("Image loading requires a browser environment"));
      return;
    }
    const image = new Image();
    if (crossOrigin) image.crossOrigin = crossOrigin;
    image.onload = () => resolve(image);
    image.onerror = () =>
      reject(new QRExportError(`Could not load the logo image at ${src.slice(0, 80)}`));
    image.src = src;
  });
}