nuintun / qrcode

A pure JavaScript QRCode encode and decode library.
https://nuintun.github.io/qrcode
MIT License
194 stars 26 forks source link

预估 TimingLine 检测 #329

Closed nuintun closed 1 year ago

nuintun commented 1 year ago
/**
 * @module timing
 */

import { Point } from '/common/Point';
import { toInt32 } from '/common/utils';
import { Pattern } from '/detector/Pattern';
import { PlotLine } from '/common/PlotLine';
import { BitMatrix } from '/common/BitMatrix';
import { FinderPatternGroup } from '/detector/FinderPatternGroup';

function calculateTimingRatio(axis: number, control: number): number {
  return control > axis ? 1 : control < axis ? -1 : 0;
}

function getTimingPointXAxis(pattern: Pattern, ratio: number): number {
  const [left, , right] = Pattern.rect(pattern);

  return ratio > 0 ? right : ratio < 0 ? left : pattern.x;
}

function getTimingPointYAxis(pattern: Pattern, ratio: number): number {
  const [, top, , bottom] = Pattern.rect(pattern);

  return ratio > 0 ? bottom : ratio < 0 ? top : pattern.y;
}

function calculateTimingLine(start: Pattern, end: Pattern, control: Pattern, isVertical?: boolean): [start: Point, end: Point] {
  const { x: endX, y: endY } = end;
  const { x: startX, y: startY } = start;
  const { x: controlX, y: controlY } = control;
  const xRatio = calculateTimingRatio(endX, controlX);
  const yRatio = calculateTimingRatio(endY, controlY);
  const endXTranslate = getTimingPointXAxis(end, xRatio);
  const endYTranslate = getTimingPointYAxis(end, yRatio);
  const startXTranslate = getTimingPointXAxis(start, xRatio);
  const startYTranslate = getTimingPointYAxis(start, yRatio);

  if (xRatio === 0 || yRatio === 0) {
    return [new Point(startXTranslate, startYTranslate), new Point(endXTranslate, endYTranslate)];
  }

  if (isVertical ? xRatio === yRatio : xRatio !== yRatio) {
    return [new Point(startX, startYTranslate), new Point(endX, endYTranslate)];
  }

  return [new Point(startXTranslate, startY), new Point(endXTranslate, endY)];
}

function isValidTimingLine(matrix: BitMatrix, start: Point, end: Point, size: number): boolean {
  const maxModules = size + 8;
  const points = new PlotLine(start, end).points();

  let modules = 1;
  let lastBit = matrix.get(toInt32(start.x), toInt32(start.y));

  for (const [x, y] of points) {
    const bit = matrix.get(x, y);

    if (bit !== lastBit) {
      modules++;
      lastBit = bit;

      if (modules > maxModules) {
        return false;
      }
    }
  }

  return modules >= size - 14 - (size - 17) / 4;
}

export function checkModulesInTimingLine(
  matrix: BitMatrix,
  { size, topLeft, topRight, bottomLeft }: FinderPatternGroup,
  isVertical?: boolean
): boolean {
  const [start, end] = isVertical
    ? calculateTimingLine(topLeft, bottomLeft, topRight, true)
    : calculateTimingLine(topLeft, topRight, bottomLeft);

  return isValidTimingLine(matrix, start, end, size);
}
/**
 * @module pattern
 */

export type PatternRect = readonly [
  // Left border center x
  left: number,
  // Top border center y
  top: number,
  // Right border center x
  right: number,
  // Bottom border center y
  bottom: number
];
/**
 * @module Pattern
 */

import { Point } from '/common/Point';
import { toInt32 } from '/common/utils';
import { PatternRect } from './utils/pattern';
import { PatternRatios } from './PatternRatios';

function getRatio({ ratios }: PatternRatios): number {
  return ratios[toInt32(ratios.length / 2)] / 2;
}

export class Pattern extends Point {
  #noise: number;
  #width: number;
  #height: number;
  #rect: PatternRect;
  #moduleSize: number;
  #combined: number = 1;
  #ratios: PatternRatios;
  #intersectRadius: number;

  public static noise(pattern: Pattern): number {
    return pattern.#noise;
  }

  public static combined(pattern: Pattern): number {
    return pattern.#combined;
  }

  public static rect(pattern: Pattern): PatternRect {
    return pattern.#rect;
  }

  constructor(ratios: PatternRatios, x: number, y: number, width: number, height: number, noise: number) {
    super(x, y);

    const { modules } = ratios;
    const halfWidth = width / 2;
    const halfHeight = height / 2;
    const ratio = getRatio(ratios);
    const xModuleSize = width / modules;
    const yModuleSize = height / modules;
    const xModuleSizeHalf = xModuleSize / 2;
    const yModuleSizeHalf = yModuleSize / 2;
    const moduleSize = (xModuleSize + yModuleSize) / 2;

    this.#noise = noise;
    this.#width = width;
    this.#height = height;
    this.#ratios = ratios;
    this.#moduleSize = moduleSize;
    this.#rect = Object.freeze([
      x - halfWidth + xModuleSizeHalf,
      y - halfHeight + yModuleSizeHalf,
      x + halfWidth - xModuleSizeHalf,
      y + halfHeight - yModuleSizeHalf
    ]);
    this.#intersectRadius = moduleSize * ratio;
  }

  public get width(): number {
    return this.#width;
  }

  public get height(): number {
    return this.#height;
  }

  public get moduleSize(): number {
    return this.#moduleSize;
  }

  public equals(x: number, y: number, width: number, height: number): boolean {
    const { modules } = this.#ratios;
    const intersectRadius = this.#intersectRadius;

    if (Math.abs(x - this.x) <= intersectRadius && Math.abs(y - this.y) <= intersectRadius) {
      const moduleSizeThis = this.#moduleSize;
      const moduleSize = (width + height) / modules / 2;
      const moduleSizeDiff = Math.abs(moduleSize - moduleSizeThis);

      if (moduleSizeDiff < 1 || moduleSizeDiff <= moduleSizeThis) {
        return true;
      }
    }

    return false;
  }

  public combine(x: number, y: number, width: number, height: number, noise: number): Pattern {
    const combined = this.#combined;
    const nextCombined = combined + 1;
    const combinedX = (combined * this.x + x) / nextCombined;
    const combinedY = (combined * this.y + y) / nextCombined;
    const combinedNoise = (combined * this.#noise + noise) / nextCombined;
    const combinedWidth = (combined * this.#width + width) / nextCombined;
    const combinedHeight = (combined * this.#height + height) / nextCombined;
    const pattern = new Pattern(this.#ratios, combinedX, combinedY, combinedWidth, combinedHeight, combinedNoise);

    pattern.#combined = nextCombined;

    return pattern;
  }
}