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

100% Statements 34/34
100% Branches 16/16
100% Functions 3/3
100% Lines 30/30

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 92 93 94 95 96 97                                          150x                               150x 150x   150x 150x 150x 11x 11x 11x               150x 150x 132x 13240x     18x 446x 11226x 4080x                     150x 150x 150x 450x 450x     150x       450x     150x 150x 450x 450x 157x     150x    
import type { QRGradientDef, QRMatrix, QRPath } from "../types";
import type { ResolvedStyle } from "./style";
import { buildModuleField, eyeOrigins, horizontalRuns } from "./geometry";
import { gradientId } from "./ids";
import { eyeInnerPath, eyeOuterPath, modulePath, rectPath } from "./shapes";
 
/** Paths plus any gradient definitions they reference. */
export interface RenderedPaths {
  paths: QRPath[];
  defs: QRGradientDef[];
}
 
/**
 * Can this style be drawn as merged horizontal runs?
 *
 * Run-merging is what keeps a default code at a few dozen path commands
 * instead of one rectangle per module, but it is only *correct* for shapes
 * that tile seamlessly: the moment modules are inset or rounded, each one has
 * its own outline and merging would erase the gaps that define the style.
 */
function canMergeRuns(style: ResolvedStyle): boolean {
  return style.moduleShape === "square" && style.moduleGap === 0;
}
 
/**
 * Turn a matrix into fill paths.
 *
 * Data modules, finder rings and finder centres are built as three groups so
 * each can take its own colour; groups that end up with the *same* fill are
 * merged back into a single path, so the common case — one colour, square
 * modules — emits exactly one `<path>`.
 *
 * The finder patterns are never excavated by a logo: they are what a scanner
 * uses to locate the symbol at all, and clearing them trades a decorative
 * detail for a code that cannot be found.
 */
export function matrixToPaths(matrix: QRMatrix, style: ResolvedStyle): RenderedPaths {
  const field = buildModuleField(matrix, style);
  const { margin, size } = field;
 
  const defs: QRGradientDef[] = [];
  let foreground = style.fg;
  if (style.fgGradient) {
    const id = gradientId(style.fgGradient);
    foreground = `url(#${id})`;
    defs.push({
      id,
      gradient: style.fgGradient,
      bounds: { x: 0, y: 0, width: field.side, height: field.side },
    });
  }
 
  // Data modules.
  let data = "";
  if (canMergeRuns(style)) {
    for (const run of horizontalRuns(field)) {
      data += rectPath(margin + run.x, margin + run.y, run.length, 1);
    }
  } else {
    for (let y = 0; y < size; y++) {
      for (let x = 0; x < size; x++) {
        if (!field.isDark(x, y)) continue;
        data += modulePath(style.moduleShape, margin + x, margin + y, style.moduleGap, {
          up: field.isDark(x, y - 1),
          down: field.isDark(x, y + 1),
          left: field.isDark(x - 1, y),
          right: field.isDark(x + 1, y),
        });
      }
    }
  }
 
  // Finder patterns.
  let outer = "";
  let inner = "";
  for (const [ox, oy] of eyeOrigins(size)) {
    outer += eyeOuterPath(style.eyeShape, margin + ox, margin + oy);
    inner += eyeInnerPath(style.eyeShape, margin + ox, margin + oy);
  }
 
  const groups: QRPath[] = [
    { d: data, fill: foreground },
    { d: outer, fill: style.eyeOuter ?? foreground },
    { d: inner, fill: style.eyeInner ?? foreground },
  ].filter((group) => group.d.length > 0);
 
  // Collapse neighbouring groups that share a fill.
  const paths: QRPath[] = [];
  for (const group of groups) {
    const previous = paths[paths.length - 1];
    if (previous && previous.fill === group.fill) previous.d += group.d;
    else paths.push({ ...group });
  }
 
  return { paths, defs };
}