@gongbaodd/qr-renderer 0.1.0
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/LICENSE +21 -0
- package/README.md +25 -0
- package/dist/node.js +3130 -0
- package/dist/types/core/artifacts.d.ts +2 -0
- package/dist/types/core/assemble.d.ts +36 -0
- package/dist/types/core/errors.d.ts +6 -0
- package/dist/types/core/export-sizes.d.ts +21 -0
- package/dist/types/core/image.d.ts +14 -0
- package/dist/types/core/imaging/browser.d.ts +23 -0
- package/dist/types/core/imaging/cloudflare.d.ts +2 -0
- package/dist/types/core/imaging/index.d.ts +5 -0
- package/dist/types/core/imaging/node.d.ts +4 -0
- package/dist/types/core/imaging/pixels.d.ts +28 -0
- package/dist/types/core/imaging/types.d.ts +45 -0
- package/dist/types/core/mask.d.ts +11 -0
- package/dist/types/core/module-cut.d.ts +107 -0
- package/dist/types/core/palette.d.ts +80 -0
- package/dist/types/core/pattern-cut.d.ts +96 -0
- package/dist/types/core/pattern.d.ts +119 -0
- package/dist/types/core/placement.d.ts +9 -0
- package/dist/types/core/qr.d.ts +57 -0
- package/dist/types/core/rotate.d.ts +149 -0
- package/dist/types/core/squircle.d.ts +2 -0
- package/dist/types/core/types.d.ts +475 -0
- package/dist/types/engine/engine.d.ts +68 -0
- package/dist/types/engine/index.d.ts +34 -0
- package/dist/types/engine/mapping.d.ts +14 -0
- package/dist/types/engine/pipeline.d.ts +77 -0
- package/dist/types/engine/types.d.ts +66 -0
- package/dist/types/node.d.ts +17 -0
- package/dist/types/png-guard.d.ts +17 -0
- package/dist/types/recipe.d.ts +132 -0
- package/dist/types/schema.d.ts +211 -0
- package/dist/types/worker-shim.d.ts +12 -0
- package/package.json +79 -0
- package/src/core/artifacts.ts +6 -0
- package/src/core/assemble.ts +1195 -0
- package/src/core/errors.ts +24 -0
- package/src/core/export-sizes.ts +179 -0
- package/src/core/image.ts +57 -0
- package/src/core/imaging/browser.ts +186 -0
- package/src/core/imaging/cloudflare.ts +15 -0
- package/src/core/imaging/index.ts +14 -0
- package/src/core/imaging/node.ts +95 -0
- package/src/core/imaging/pixels.ts +121 -0
- package/src/core/imaging/types.ts +56 -0
- package/src/core/mask.ts +295 -0
- package/src/core/module-cut.ts +523 -0
- package/src/core/palette.ts +227 -0
- package/src/core/pattern-cut.ts +522 -0
- package/src/core/pattern.ts +478 -0
- package/src/core/placement.ts +94 -0
- package/src/core/qr.ts +695 -0
- package/src/core/rotate.ts +267 -0
- package/src/core/squircle.ts +53 -0
- package/src/core/types.ts +477 -0
- package/src/engine/engine.ts +316 -0
- package/src/engine/index.ts +58 -0
- package/src/engine/mapping.ts +66 -0
- package/src/engine/pipeline.ts +501 -0
- package/src/engine/types.ts +61 -0
- package/src/node.ts +45 -0
- package/src/png-guard.ts +66 -0
- package/src/recipe.ts +161 -0
- package/src/schema.ts +137 -0
- package/src/wasm.d.ts +5 -0
- package/src/worker-shim.ts +15 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PixelStyle } from './pattern';
|
|
2
|
+
import type { QrPalette } from './palette';
|
|
3
|
+
import type { AssembleReport, BoundingBox, ResolvedLayout } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Assembles the finished poster offline in whole modules. Upright placements take the golden
|
|
6
|
+
* 0° path, where the generator's marker-free matrix is sampled against the painted region on
|
|
7
|
+
* the placed QR's own lattice. A rotated placement instead validates and paints in the QR's
|
|
8
|
+
* upright frame: the region mask is inverse-rotated into that frame, the same whole-module
|
|
9
|
+
* pipeline generates the fill and plate there, and the finished overlay rides the placement
|
|
10
|
+
* transform back onto the poster (doc/plan/rotated-mask-fill.md).
|
|
11
|
+
*/
|
|
12
|
+
export declare function assembleResolved(layout: ResolvedLayout, options: {
|
|
13
|
+
seed?: number;
|
|
14
|
+
qrMargin?: 1;
|
|
15
|
+
radius?: number;
|
|
16
|
+
rimModules?: number;
|
|
17
|
+
rimRounded?: boolean;
|
|
18
|
+
regionMargin?: boolean;
|
|
19
|
+
pixelStyle?: PixelStyle;
|
|
20
|
+
transparentBlank?: boolean;
|
|
21
|
+
palette?: QrPalette;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
report: AssembleReport;
|
|
24
|
+
artifacts: Record<string, Uint8Array<ArrayBufferLike>>;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Geometry of the marker-only light band, in poster pixels: beside each 7x7 finder marker, the two
|
|
28
|
+
* arms that run along its outer edges plus the diagonal corner block, all whole cells on the module
|
|
29
|
+
* lattice. The arms span the finder footprint exactly — the separator inside the code grid already
|
|
30
|
+
* carries its own light row and column — so the band is three small Ls rather than a quiet zone
|
|
31
|
+
* around the code.
|
|
32
|
+
*/
|
|
33
|
+
export declare function markerBandRects(codeGrid: BoundingBox, qrModules: number, pitch: number, marginModules: 1): {
|
|
34
|
+
arms: BoundingBox[];
|
|
35
|
+
cornerBlocks: BoundingBox[];
|
|
36
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ErrorCode = 'QWEN_API_FAILED' | 'INVALID_INPUT' | 'MASK_AMBIGUOUS' | 'MASK_INVALID' | 'QR_INVALID' | 'QR_TEXT_MISMATCH' | 'QR_LAYOUT_INVALID' | 'COLOR_INVALID' | 'OUTPUT_EXISTS' | 'IMAGE_PROCESSING_FAILED' | 'VERIFICATION_FAILED';
|
|
2
|
+
export declare class QrPosterError extends Error {
|
|
3
|
+
readonly code: ErrorCode;
|
|
4
|
+
readonly exitCode: 2 | 3 | 4;
|
|
5
|
+
constructor(code: ErrorCode, message: string, exitCode?: 2 | 3 | 4, options?: ErrorOptions);
|
|
6
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RawImage } from './imaging/types';
|
|
2
|
+
import type { RegionMask } from './types';
|
|
3
|
+
export declare const RASTER_EXPORT_TARGETS: readonly [320, 640, 1280];
|
|
4
|
+
export interface RasterExportChoice {
|
|
5
|
+
key: string;
|
|
6
|
+
targetPitch: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function dimensionsAtPitch(width: number, height: number, originalPitch: number, targetPitch: number): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
};
|
|
14
|
+
/** Build distinct smaller choices while keeping every export on an integer QR-module pitch. */
|
|
15
|
+
export declare function rasterExportChoices(width: number, height: number, originalPitch: number): RasterExportChoice[];
|
|
16
|
+
/** Area-weighted RGBA shrink in premultiplied-alpha space. */
|
|
17
|
+
export declare function resizeAreaPremultiplied(source: RawImage, width: number, height: number): Uint8Array;
|
|
18
|
+
/** A target mask pixel is selected only when every source pixel in its footprint is selected. */
|
|
19
|
+
export declare function resizeRegionMaskConservative(source: RegionMask, width: number, height: number): RegionMask;
|
|
20
|
+
/** A self-contained full-poster SVG backed by the exact verified assembled PNG. */
|
|
21
|
+
export declare function buildPosterSvg(png: Uint8Array, width: number, height: number): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LoadedPng {
|
|
2
|
+
path: string;
|
|
3
|
+
file: Uint8Array;
|
|
4
|
+
data: Uint8Array;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadPng(path: string, label: string): Promise<LoadedPng>;
|
|
10
|
+
/** Decodes an in-memory PNG into the same shape {@link loadPng} returns. */
|
|
11
|
+
export declare function decodePng(file: Uint8Array, path: string, label: string): Promise<LoadedPng>;
|
|
12
|
+
export declare function rgbaToPng(data: Uint8Array, width: number, height: number): Promise<Uint8Array>;
|
|
13
|
+
export declare function grayscaleToPng(data: Uint8Array, width: number, height: number): Promise<Uint8Array>;
|
|
14
|
+
export declare function luma(r: number, g: number, b: number): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser imaging backend: @jsquash/png for the lossless PNG codec and
|
|
3
|
+
* @resvg/resvg-wasm as the SVG rasterizer. Pixel work (composite, flatten,
|
|
4
|
+
* nearest resize) shares the pure-TS helpers in pixels.ts so both environments
|
|
5
|
+
* implement the same document semantics.
|
|
6
|
+
*
|
|
7
|
+
* The WASM binaries are initialized lazily on the first operation. In a browser
|
|
8
|
+
* bundle both codecs auto-resolve their `.wasm` assets relative to their own
|
|
9
|
+
* module URL; in Node (parity harness) pass the raw wasm bytes via
|
|
10
|
+
* {@link installBrowserImaging}.
|
|
11
|
+
*/
|
|
12
|
+
import type { InitInput as PngWasmInput } from '@jsquash/png/codec/pkg/squoosh_png.js';
|
|
13
|
+
import type { InitInput as ResvgWasmInput } from '@resvg/resvg-wasm';
|
|
14
|
+
import type { Imaging } from './types';
|
|
15
|
+
export interface BrowserImagingOptions {
|
|
16
|
+
/** resvg wasm source; defaults to the codec's bundled URL in browser builds. */
|
|
17
|
+
resvgWasm?: ResvgWasmInput;
|
|
18
|
+
/** jSquash png wasm source; defaults to the codec's bundled URL in browser builds. */
|
|
19
|
+
pngWasm?: PngWasmInput;
|
|
20
|
+
}
|
|
21
|
+
/** Pre-initializes the codec WASMs; call once per worker/entry point. */
|
|
22
|
+
export declare function installBrowserImaging(options?: BrowserImagingOptions): Promise<void>;
|
|
23
|
+
export declare const browserImaging: Imaging;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Imaging } from './types';
|
|
2
|
+
/** Installs one backend. Entry points only (test setup, scripts, route handlers, the worker). */
|
|
3
|
+
export declare function setImaging(backend: Imaging): void;
|
|
4
|
+
/** The installed backend. Core modules call this instead of importing sharp directly. */
|
|
5
|
+
export declare function imaging(): Imaging;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-TS pixel operations shared by pixel-writing backends. The sharp backend keeps its
|
|
3
|
+
* own native compositing for existing opaque paths so their bytes stay bit-identical. Shared
|
|
4
|
+
* pixel helpers cover operations that need identical semantics in both environments, including
|
|
5
|
+
* source-over into the transparent blank poster.
|
|
6
|
+
*/
|
|
7
|
+
import type { RawImage } from './types';
|
|
8
|
+
/** Source-over composites one unpremultiplied RGBA pixel into another in place. */
|
|
9
|
+
export declare function compositePixelOver(source: Uint8Array, sourceOffset: number, destination: Uint8Array, destinationOffset: number, sourceAlphaOverride?: number): void;
|
|
10
|
+
/** Flattens alpha over an opaque white background: `out = c*a/255 + 255*(1 - a/255)`, truncated. */
|
|
11
|
+
export declare function flattenOverWhite(data: Uint8Array): Uint8Array;
|
|
12
|
+
/**
|
|
13
|
+
* Standard source-over blend of full-size unpremultiplied RGBA overlay rasters over an
|
|
14
|
+
* opaque base. Alpha compositing uses the same blend libvips performs; differences stay
|
|
15
|
+
* limited to SVG antialiasing, which the renderers already treat as free.
|
|
16
|
+
*/
|
|
17
|
+
export declare function compositeOver(base: RawImage, overlays: RawImage[]): Uint8Array;
|
|
18
|
+
/**
|
|
19
|
+
* Integer nearest-neighbor resample, matching the probed libvips(nearest) mapping rules:
|
|
20
|
+
* expansion anchors left (`floor(x * in / out)`), shrink centers the sample
|
|
21
|
+
* (`floor((x + 0.5) * in / out)`). Residual tie cases (exact-integer or half-integer
|
|
22
|
+
* sample positions) may pick one pixel lower in libvips' internal two-stage shrink —
|
|
23
|
+
* a documented deviation absorbed by the parity tolerance, never affecting intra-run
|
|
24
|
+
* verification.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resizeNearest(raw: RawImage, width: number, height: number): Uint8Array;
|
|
27
|
+
/** Copies a subrectangle without resampling. */
|
|
28
|
+
export declare function cropRgba(raw: RawImage, x: number, y: number, width: number, height: number): Uint8Array;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-agnostic imaging seam. Core modules call these operations instead of
|
|
3
|
+
* using sharp (or any other rasterizer) directly, so a Node (sharp) and a browser
|
|
4
|
+
* backend can be swapped without touching pipeline logic.
|
|
5
|
+
*
|
|
6
|
+
* Semantics each backend must honor:
|
|
7
|
+
* - `decodePng` is strict and exact: RGBA pixels, failing loudly on undecodable input.
|
|
8
|
+
* - `rasterizeSvg` returns unpremultiplied RGBA; antialiased edges may differ between
|
|
9
|
+
* rasterizers (librsvg here, the browser rasterizer later), which the poster has always
|
|
10
|
+
* treated as renderer freedom.
|
|
11
|
+
*/
|
|
12
|
+
/** A decoded raster of unpremultiplied RGBA pixels. */
|
|
13
|
+
export interface RawImage {
|
|
14
|
+
data: Uint8Array;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
/** Result of rasterizing an SVG straight to PNG bytes. */
|
|
19
|
+
export interface RenderedSvgPng {
|
|
20
|
+
png: Uint8Array;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SvgRenderOptions {
|
|
25
|
+
/** Composites the raster over opaque white before encoding (plain alpha otherwise). */
|
|
26
|
+
flatten?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface Imaging {
|
|
29
|
+
/** Strict PNG decode into exact RGBA. */
|
|
30
|
+
decodePng(file: Uint8Array): Promise<RawImage>;
|
|
31
|
+
/** Encodes RGBA pixels into PNG bytes. */
|
|
32
|
+
encodePngRgba(data: Uint8Array, width: number, height: number): Promise<Uint8Array>;
|
|
33
|
+
/** Encodes a one-byte-per-pixel grayscale raster into PNG bytes. */
|
|
34
|
+
encodePngGrayscale(data: Uint8Array, width: number, height: number): Promise<Uint8Array>;
|
|
35
|
+
/** Rasterizes an SVG at the exact size into RGBA pixels. */
|
|
36
|
+
rasterizeSvg(svg: string, width: number, height: number): Promise<RawImage>;
|
|
37
|
+
/** Rasterizes an SVG into PNG bytes, optionally flattened over opaque white. */
|
|
38
|
+
renderSvgToPng(svg: string, options: SvgRenderOptions): Promise<RenderedSvgPng>;
|
|
39
|
+
/** Composites SVG overlays (rasterized at the base size) over a base PNG into PNG bytes. */
|
|
40
|
+
composeQrPng(basePng: Uint8Array, overlaySvgs: string[]): Promise<Uint8Array>;
|
|
41
|
+
/** Flattens alpha over white, nearest-resamples to a square target size, returns PNG bytes. */
|
|
42
|
+
normalizeQrPng(file: Uint8Array, targetSize: number): Promise<Uint8Array>;
|
|
43
|
+
/** SHA-256 hex of bytes or a UTF-8 string. Async so the browser's crypto.subtle fits. */
|
|
44
|
+
sha256Hex(input: string | Uint8Array): Promise<string>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoadedPng } from './image';
|
|
2
|
+
import type { BoundingBox, Point, RegionMask } from './types';
|
|
3
|
+
/** Shared pixel rule for manual masks: opaque white selects the region. */
|
|
4
|
+
export declare function isSelectedMaskPixel(r: number, g: number, b: number, alpha: number): boolean;
|
|
5
|
+
export declare function buildManualRegionMask(mask: LoadedPng, width: number, height: number): RegionMask;
|
|
6
|
+
export declare function detectRegionMask(image: LoadedPng): RegionMask;
|
|
7
|
+
export declare function calculateMaskStats(data: Uint8Array, width: number, height: number): {
|
|
8
|
+
area: number;
|
|
9
|
+
bounds: BoundingBox;
|
|
10
|
+
centroid: Point;
|
|
11
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BoundingBox } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The poster-space module lattice the assembled cut is built on. Cell (column, row) covers one
|
|
4
|
+
* `modulePixels`-sized pixel block, and the origin is the placed QR's own lattice, so every drawn
|
|
5
|
+
* module is a whole QR cell of the texture rather than a slice of one.
|
|
6
|
+
*/
|
|
7
|
+
export interface ModuleLattice {
|
|
8
|
+
/** Poster-space x of the lattice origin; `0 <= x < modulePixels`. */
|
|
9
|
+
x: number;
|
|
10
|
+
/** Poster-space y of the lattice origin; `0 <= y < modulePixels`. */
|
|
11
|
+
y: number;
|
|
12
|
+
modulePixels: number;
|
|
13
|
+
columns: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
/** Plate window in poster pixels; its box must sit on the lattice or the hole would slice a module. */
|
|
19
|
+
export interface ModuleWindow {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
size: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SafeArea {
|
|
25
|
+
/** Row-major grid: 1 when the module's whole pixel block is inside the painted region. */
|
|
26
|
+
safe: Uint8Array;
|
|
27
|
+
safeModules: number;
|
|
28
|
+
/** Modules the region covers only in part; they keep the original artwork. */
|
|
29
|
+
partialModules: number;
|
|
30
|
+
/** Region pixels inside those partial modules, which the cut therefore never paints. */
|
|
31
|
+
droppedPartialPixels: number;
|
|
32
|
+
/** Canvas-pixel bounds of the safe modules. */
|
|
33
|
+
bounds: BoundingBox;
|
|
34
|
+
}
|
|
35
|
+
export interface PlateModules {
|
|
36
|
+
/** Row-major grid: 1 for every module the cut drops for the QR plate, corners already handed back. */
|
|
37
|
+
cells: Uint8Array;
|
|
38
|
+
/** Row-major grid: 1 for the modules handed back to the texture. */
|
|
39
|
+
corners: Uint8Array;
|
|
40
|
+
/** Modules the hole covers after the corners are handed back. */
|
|
41
|
+
holeModules: number;
|
|
42
|
+
/** Modules the texture keeps, 0 when the hand-back rectangles are empty. */
|
|
43
|
+
cornerModules: number;
|
|
44
|
+
/** Canvas-pixel bounds of the plate cells, the hand-backs already removed. */
|
|
45
|
+
bounds: BoundingBox;
|
|
46
|
+
}
|
|
47
|
+
/** Row-major safe cells in the QR's source quiet zone for a tight rectangular mask. */
|
|
48
|
+
export interface TightBlockQuietZone {
|
|
49
|
+
cells: Uint8Array;
|
|
50
|
+
modules: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Builds the module lattice covering the canvas. The origin is reduced to a phase inside one
|
|
54
|
+
* module, so a caller can pass the placement box directly and get the same lattice the QR uses.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildModuleLattice(width: number, height: number, modulePixels: number, origin: {
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
}): ModuleLattice;
|
|
60
|
+
/** Poster-space block of one lattice cell. */
|
|
61
|
+
export declare function moduleBlock(lattice: ModuleLattice, column: number, row: number): ModuleWindow;
|
|
62
|
+
/** Index of the cell containing a poster pixel, or -1 when the pixel is off the lattice. */
|
|
63
|
+
export declare function moduleCellIndex(lattice: ModuleLattice, x: number, y: number): number;
|
|
64
|
+
/**
|
|
65
|
+
* Marks the modules the cut may draw: a module is safe only when its whole pixel block is on the
|
|
66
|
+
* canvas and every one of those pixels is inside the painted region. A module the region covers
|
|
67
|
+
* only in part is dropped, so the drawn area stays a union of whole modules and the original
|
|
68
|
+
* artwork survives along the silhouette.
|
|
69
|
+
*/
|
|
70
|
+
export declare function computeSafeArea(selection: Uint8Array, width: number, height: number, lattice: ModuleLattice): SafeArea;
|
|
71
|
+
/**
|
|
72
|
+
* Keeps the normalized QR's two-module light margin quiet when a solid rectangular mask nearly
|
|
73
|
+
* touches the placed QR on all four sides. The region and placement use exclusive right/bottom
|
|
74
|
+
* bounds; partially covered cells stay out because only safe cells can be returned.
|
|
75
|
+
*/
|
|
76
|
+
export declare function computeTightBlockQuietZone(selection: Uint8Array, width: number, height: number, safe: Uint8Array, plate: Uint8Array, lattice: ModuleLattice, placement: ModuleWindow, enabled: boolean): TightBlockQuietZone;
|
|
77
|
+
/**
|
|
78
|
+
* Marks the plate on the module lattice. The hole is every module whose whole block sits inside one
|
|
79
|
+
* of the plate rectangles — the code grid plus the light arms beside the finder markers — so the cut
|
|
80
|
+
* never relies on artwork showing through the plate and no drawn edge crosses a module. The
|
|
81
|
+
* hand-back rectangles (the corner blocks `--cut-radius` carves out of the arms) leave the hole
|
|
82
|
+
* again and keep the texture, so the plate's corners stay texture instead of the QR's own light
|
|
83
|
+
* band. Rectangles may share edges and may reach past the canvas; only lattice cells are marked.
|
|
84
|
+
*/
|
|
85
|
+
export declare function computePlateModules(lattice: ModuleLattice, plateRects: BoundingBox[], handbackRects?: BoundingBox[]): PlateModules;
|
|
86
|
+
/**
|
|
87
|
+
* Marks the outer rings of drawn modules with a Chebyshev distance of at most `rimModules` from the
|
|
88
|
+
* nearest module the region does not fully cover. Those modules are forced dark, so the rim is a
|
|
89
|
+
* band of whole modules that closes on the silhouette. The plate is never a seed: the rim follows
|
|
90
|
+
* the painted region, not the QR window.
|
|
91
|
+
*/
|
|
92
|
+
export declare function computeRimModules(safe: Uint8Array, lattice: ModuleLattice, rimModules: number): Uint8Array;
|
|
93
|
+
/** Outer light margin, followed by the optional dark rim, both restricted to safe whole modules. */
|
|
94
|
+
export declare function computeRegionBands(safe: Uint8Array, lattice: ModuleLattice, rimModules: number, regionMargin: boolean): {
|
|
95
|
+
margin: Uint8Array;
|
|
96
|
+
rim: Uint8Array;
|
|
97
|
+
};
|
|
98
|
+
/** Union of whole module rectangles as an SVG path; every edge lands on an integer pixel. */
|
|
99
|
+
export declare function buildModulePath(cells: Uint8Array, lattice: ModuleLattice): string;
|
|
100
|
+
/** Rounded outer silhouette of whole-module union: outer corners filleted to `radius` with antialiasing. */
|
|
101
|
+
export declare function buildRoundedModulePath(cells: Uint8Array, lattice: ModuleLattice, radius: number): string;
|
|
102
|
+
/**
|
|
103
|
+
* Rasterizes the module path and returns its coverage. By default module edges land on integer
|
|
104
|
+
* pixel boundaries, so the raster is binary; when `allowAntialias` is true partial alpha is kept
|
|
105
|
+
* for rounded, antialiased edges.
|
|
106
|
+
*/
|
|
107
|
+
export declare function renderModuleCoverage(pathData: string, width: number, height: number, allowAntialias?: boolean): Promise<Uint8Array>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pattern palette: pixel (ink), marker (finder/alignment ink), and background
|
|
3
|
+
* (light) colors, plus the OKLCH machinery that derives suggestions from the pixel
|
|
4
|
+
* color and guards decoding contrast. Pure and dependency-free: UI, worker, and
|
|
5
|
+
* Node tests share the exact same numbers.
|
|
6
|
+
*/
|
|
7
|
+
export interface QrPalette {
|
|
8
|
+
/** Ink of every dark module: texture cells, data modules, the rim, marker-refill bits. */
|
|
9
|
+
pixel: string;
|
|
10
|
+
/** Ink of the three finder markers and the circular alignment marker. */
|
|
11
|
+
marker: string;
|
|
12
|
+
/** Light modules: quiet zone, marker rings, the plate's light band, the region margin. */
|
|
13
|
+
background: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_PALETTE: QrPalette;
|
|
16
|
+
/**
|
|
17
|
+
* The mahu-QR mascot's own palette: near-black pixel ink, vermilion marker ink,
|
|
18
|
+
* white background. A brand suggestion the UI offers in one tap — never an
|
|
19
|
+
* engine default, so `DEFAULT_PALETTE` and every golden output stay untouched.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TIGER_PRESET: QrPalette;
|
|
22
|
+
export interface Oklch {
|
|
23
|
+
/** Perceptual lightness [0, 1]. */
|
|
24
|
+
l: number;
|
|
25
|
+
/** Chroma, roughly [0, 0.37] inside sRGB. */
|
|
26
|
+
c: number;
|
|
27
|
+
/** Hue in degrees [0, 360). */
|
|
28
|
+
h: number;
|
|
29
|
+
}
|
|
30
|
+
/** Guard limits. Luma thresholds mirror qr.ts: the quiet-zone rule needs 200 and ink detection is 128. */
|
|
31
|
+
export declare const GUARD: {
|
|
32
|
+
/** Rec.601 luma a selectable background must stay above (quiet-zone rule needs 200; kept headroom). */
|
|
33
|
+
readonly lightLumaMin: 205;
|
|
34
|
+
/** Rec.601 luma every ink must stay at or below (INK_LUMA_THRESHOLD is 128; kept headroom). */
|
|
35
|
+
readonly inkLumaMax: 120;
|
|
36
|
+
/** OKLCH lightness distance each ink must keep from the background. */
|
|
37
|
+
readonly minLightnessDistance: 0.3;
|
|
38
|
+
};
|
|
39
|
+
export declare const HEX_PATTERN: RegExp;
|
|
40
|
+
export type Rgb = [number, number, number];
|
|
41
|
+
/** Parses `#rgb`/`#rrggbb` (any case); null for anything else. */
|
|
42
|
+
export declare function parseHex(hex: string): Rgb | null;
|
|
43
|
+
/** Lowercase six-digit form; null when the input is not a parseable hex color. */
|
|
44
|
+
export declare function normalizeHex(hex: string): string | null;
|
|
45
|
+
/** Rec.601 luma — the same grayscale weights the QR pipeline's ink and quiet-zone rules use. */
|
|
46
|
+
export declare function luma([r, g, b]: Rgb): number;
|
|
47
|
+
/** Convenience for renderers turning a stored hex into 0-255 channels. */
|
|
48
|
+
export declare function hexToRgb(hex: string): Rgb;
|
|
49
|
+
export declare function rgbToOklch(rgb: Rgb): Oklch;
|
|
50
|
+
/**
|
|
51
|
+
* OKLCH → sRGB, always in gamut: chroma is reduced in fixed `CHROMA_STEP` decrements
|
|
52
|
+
* until every channel lands in [0, 255], so the same OKLCH maps to the same hex.
|
|
53
|
+
*/
|
|
54
|
+
export declare function oklchToHex({ l, c, h }: Oklch, step?: number): string;
|
|
55
|
+
export declare function hexToOklch(hex: string): Oklch | null;
|
|
56
|
+
/**
|
|
57
|
+
* Suggests marker and background colors for a picked pixel color, derived in OKLCH:
|
|
58
|
+
* the background is a faint tint of the same hue near white, the marker is a tonal
|
|
59
|
+
* step of the pixel's own tone (same hue and chroma, stepped down the lightness axis)
|
|
60
|
+
* that stays decode-dark. Both always pass the palette guard.
|
|
61
|
+
*/
|
|
62
|
+
export declare function suggestPalette(pixel: string): {
|
|
63
|
+
marker: string;
|
|
64
|
+
background: string;
|
|
65
|
+
};
|
|
66
|
+
export interface PaletteIssue {
|
|
67
|
+
color: keyof QrPalette;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Decoding-safety guard for a user-edited palette: the background must stay light
|
|
72
|
+
* (the engine's quiet-zone rule counts brightness ≥ 200 as light), every ink must
|
|
73
|
+
* stay dark, and each ink must separate from the background in OKLCH lightness.
|
|
74
|
+
* The generated QR's decode round-trip remains the authoritative gate; this reports
|
|
75
|
+
* early, before a prepare request is spent.
|
|
76
|
+
*/
|
|
77
|
+
export declare function paletteGuard(palette: QrPalette): {
|
|
78
|
+
ok: boolean;
|
|
79
|
+
issues: PaletteIssue[];
|
|
80
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { LoadedPng } from './image';
|
|
2
|
+
import type { BoundingBox, Point } from './types';
|
|
3
|
+
/** Default corner fillet radius in poster pixels; matches the placed module pitch. */
|
|
4
|
+
export declare const CUT_RADIUS = 5;
|
|
5
|
+
/** Default Douglas-Peucker tolerance in pixels for the traced outline. */
|
|
6
|
+
export declare const CUT_SMOOTH_TOLERANCE = 3;
|
|
7
|
+
/** Solid black band retained inside the letter outline. */
|
|
8
|
+
export declare const CUT_BORDER_WIDTH = 20;
|
|
9
|
+
/** A mask pixel selects the cut shape when it is transparent or dark. */
|
|
10
|
+
export declare const CUT_KEEP_RULE: 'transparent-or-dark';
|
|
11
|
+
/** Loops below the fillet area are dropped as specks; shared by the cut and assembly reports. */
|
|
12
|
+
export declare function cutMinLoopArea(radius: number): number;
|
|
13
|
+
export interface CutPathStats {
|
|
14
|
+
loopsTraced: number;
|
|
15
|
+
loopsKept: number;
|
|
16
|
+
specksDropped: number;
|
|
17
|
+
verticesTraced: number;
|
|
18
|
+
verticesSimplified: number;
|
|
19
|
+
holes: number;
|
|
20
|
+
/** Area of the simplified cut polygon before corner rounding. */
|
|
21
|
+
area: number;
|
|
22
|
+
/** True when the fillet had to shrink below the requested radius on a narrow feature. */
|
|
23
|
+
radiusClamped: boolean;
|
|
24
|
+
bounds: BoundingBox;
|
|
25
|
+
}
|
|
26
|
+
export interface CutPath {
|
|
27
|
+
d: string;
|
|
28
|
+
stats: CutPathStats;
|
|
29
|
+
}
|
|
30
|
+
export interface CutPathOptions {
|
|
31
|
+
/** Corner fillet radius in pixels; defaults to {@link CUT_RADIUS}. */
|
|
32
|
+
radius?: number;
|
|
33
|
+
/** Douglas-Peucker tolerance in pixels; defaults to {@link CUT_SMOOTH_TOLERANCE}. */
|
|
34
|
+
smoothTolerance?: number;
|
|
35
|
+
}
|
|
36
|
+
interface FilletResult {
|
|
37
|
+
commands: string[];
|
|
38
|
+
clamped: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A mask pixel selects the cut shape when it is transparent or dark, which covers both artifact
|
|
42
|
+
* conventions: `edit-mask.png` (opaque white outside, transparent inside) and `region-mask.png`
|
|
43
|
+
* (white inside on black).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildShapeSelection(mask: LoadedPng): Uint8Array;
|
|
46
|
+
/**
|
|
47
|
+
* Disc morphology that turns a raw selection into a cuttable outline: a closing (dilate, erode)
|
|
48
|
+
* fills pinholes and concave nicks, an opening (erode, dilate) drops specks and convex jags. Both
|
|
49
|
+
* run at the same radius, so a pixel staircase becomes one continuous edge the fillet can round.
|
|
50
|
+
*/
|
|
51
|
+
export declare function cleanMaskSelection(selection: Uint8Array, width: number, height: number, radius: number): Uint8Array;
|
|
52
|
+
/**
|
|
53
|
+
* Traces the boundary of a binary selection as closed loops along pixel borders. The traversal keeps
|
|
54
|
+
* the selected pixels on its right, so outer boundaries come out with negative shoelace area and
|
|
55
|
+
* holes with positive area.
|
|
56
|
+
*/
|
|
57
|
+
export declare function traceMaskContours(selected: Uint8Array, width: number, height: number): Point[][];
|
|
58
|
+
/** Drops the intermediate points of straight runs, leaving only genuine corners. */
|
|
59
|
+
export declare function collapseCollinearPoints(loop: Point[]): Point[];
|
|
60
|
+
/**
|
|
61
|
+
* Splits a closed loop at its farthest vertex before simplifying, so an open-run simplifier cannot
|
|
62
|
+
* collapse the whole ring onto its duplicated start and end point.
|
|
63
|
+
*/
|
|
64
|
+
export declare function simplifyClosedLoop(loop: Point[], tolerance: number): Point[];
|
|
65
|
+
export declare function polygonArea(loop: Point[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* Rounds every corner with a fillet. The tangent distance is capped at half of each neighbouring
|
|
68
|
+
* edge and the arc radius is recomputed from the capped tangent, so arcs on small features stay
|
|
69
|
+
* inside the polygon instead of overlapping each other.
|
|
70
|
+
*/
|
|
71
|
+
export declare function filletLoop(loop: Point[], radius: number): FilletResult;
|
|
72
|
+
/** Turns a binary selection into a rounded SVG path plus the statistics the report records. */
|
|
73
|
+
export declare function buildCutPath(selected: Uint8Array, width: number, height: number, options?: CutPathOptions): CutPath;
|
|
74
|
+
export interface CutSvgOptions {
|
|
75
|
+
/** Black band drawn inside the cut edge, in pixels; zero leaves the edge bare. */
|
|
76
|
+
borderWidth?: number;
|
|
77
|
+
}
|
|
78
|
+
/** Self-contained SVG: the pattern rides along as a data URI and the cut edge stays vector. */
|
|
79
|
+
export declare function buildCutSvg(pathData: string, width: number, height: number, pattern: Uint8Array, options?: CutSvgOptions): string;
|
|
80
|
+
/**
|
|
81
|
+
* Rasterizes only the cut path and returns its antialiased coverage: 0 outside the cut shape, 255
|
|
82
|
+
* inside, and the partial values along the edge that make the fillet smooth.
|
|
83
|
+
*/
|
|
84
|
+
export declare function renderCutCoverage(pathData: string, width: number, height: number): Promise<Uint8Array>;
|
|
85
|
+
/**
|
|
86
|
+
* Rasterizes the band hugging the inside of the cut edge: a stroke of twice `width` centered on the
|
|
87
|
+
* path and clipped to the shape, so coverage is 0 outside, roughly half on the boundary itself, and
|
|
88
|
+
* 255 one border width inside.
|
|
89
|
+
*/
|
|
90
|
+
export declare function renderCutBorderCoverage(pathData: string, width: number, height: number, borderWidth: number): Promise<Uint8Array>;
|
|
91
|
+
/**
|
|
92
|
+
* Applies the cut coverage without resampling the source. An optional black band is composited
|
|
93
|
+
* inside the outline; pixels beyond it stay bit-exact and everything outside is transparent.
|
|
94
|
+
*/
|
|
95
|
+
export declare function renderCutPng(pattern: LoadedPng, pathData: string, borderWidth?: number): Promise<Uint8Array>;
|
|
96
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { QrCodeGenerateResult } from 'uqr';
|
|
2
|
+
/** Project defaults: ecc 'M', 2-module margin, dot pixel style, auto mask. */
|
|
3
|
+
export declare const PATTERN_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
+
export declare const PATTERN_ECC: 'M';
|
|
5
|
+
export declare const PATTERN_PIXEL_STYLE: 'dot';
|
|
6
|
+
export declare const PATTERN_PIXEL_STYLES: readonly ['square', 'rounded', 'dot'];
|
|
7
|
+
export type PixelStyle = (typeof PATTERN_PIXEL_STYLES)[number];
|
|
8
|
+
export declare const PATTERN_MARKER_REFILL: 'seeded-random';
|
|
9
|
+
/** Render ink and light colors; the palette overrides parse as hex. */
|
|
10
|
+
export declare const PATTERN_INK: '#000000';
|
|
11
|
+
export declare const PATTERN_LIGHT: '#ffffff';
|
|
12
|
+
/** Light modules the toolkit draws around the code; the texture's own quiet zone. */
|
|
13
|
+
export declare const PATTERN_QUIET_ZONE_MODULES: 2;
|
|
14
|
+
export interface PatternRenderWindow {
|
|
15
|
+
left: number;
|
|
16
|
+
top: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}
|
|
20
|
+
export interface PatternRenderOptions {
|
|
21
|
+
/** Background behind rendered modules. Transparent is reserved for ink-only poster overlays. */
|
|
22
|
+
background?: 'white' | 'transparent';
|
|
23
|
+
/** Light modules drawn around the matrix; matches the toolkit's default margin of 2. */
|
|
24
|
+
marginModules?: number;
|
|
25
|
+
/** Visible sub-rectangle of the full code canvas, in code pixels. */
|
|
26
|
+
window?: PatternRenderWindow;
|
|
27
|
+
/**
|
|
28
|
+
* Draws only the modules this predicate accepts, addressed in code-module coordinates
|
|
29
|
+
* (`0 .. totalModules - 1`, margin included). A rejected module renders nothing and counts as
|
|
30
|
+
* light when wedge neighbours are resolved, so the drawn area stays a union of whole modules and
|
|
31
|
+
* its edge closes on the silhouette. Without it every module is drawn on one white canvas,
|
|
32
|
+
* exactly as `--pattern-preview` renders.
|
|
33
|
+
*/
|
|
34
|
+
include?: (moduleX: number, moduleY: number) => boolean;
|
|
35
|
+
/** Suppresses black geometry for selected cells while retaining the white canvas beneath them. */
|
|
36
|
+
skipInk?: (moduleX: number, moduleY: number) => boolean;
|
|
37
|
+
/** Ink color of the dark geometry, replacing the default #000000. */
|
|
38
|
+
ink?: string;
|
|
39
|
+
/** Light color for the canvas and light modules, replacing the default #ffffff. */
|
|
40
|
+
light?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface PosterPatternOptions {
|
|
43
|
+
/** Poster canvas width in pixels. */
|
|
44
|
+
width: number;
|
|
45
|
+
/** Poster canvas height in pixels. */
|
|
46
|
+
height: number;
|
|
47
|
+
/** Module pitch in poster pixels. */
|
|
48
|
+
modulePixels: number;
|
|
49
|
+
/** Seed for the random text line and the marker refill; defaults to a fresh random seed per run. */
|
|
50
|
+
seed?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Poster-space origin of a module lattice to phase-lock the window to, typically the placed QR box.
|
|
53
|
+
* The texture's module boundaries then land on the same lattice as the QR's, so the field
|
|
54
|
+
* continues the code's rhythm. Without it the window stays centered as before.
|
|
55
|
+
*/
|
|
56
|
+
alignTo?: {
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
};
|
|
60
|
+
/** Pixel shape: square is full cell, rounded blends neighbours, dot is a circle. */
|
|
61
|
+
pixelStyle?: PixelStyle;
|
|
62
|
+
}
|
|
63
|
+
export interface PosterPattern {
|
|
64
|
+
png: Uint8Array;
|
|
65
|
+
seed: number;
|
|
66
|
+
version: number;
|
|
67
|
+
text: string;
|
|
68
|
+
/** Marker-free module matrix, marker cells refilled with seeded random bits. */
|
|
69
|
+
matrix: boolean[][];
|
|
70
|
+
qrModules: number;
|
|
71
|
+
totalModules: number;
|
|
72
|
+
codeSize: number;
|
|
73
|
+
marginModules: number;
|
|
74
|
+
crop: {
|
|
75
|
+
left: number;
|
|
76
|
+
top: number;
|
|
77
|
+
};
|
|
78
|
+
refilledModules: number;
|
|
79
|
+
}
|
|
80
|
+
/** The generated texture's lattice, its matrix, and how the window sits on it. */
|
|
81
|
+
export type PosterPatternLattice = Omit<PosterPattern, 'png'>;
|
|
82
|
+
/**
|
|
83
|
+
* Smallest QR version whose modules plus quiet zone cover the canvas at this pitch. A caller that
|
|
84
|
+
* needs to phase-lock the window (see `alignTo`) asks for one module of headroom, so the code is
|
|
85
|
+
* wider than the canvas and the window can still be shifted onto the requested lattice.
|
|
86
|
+
*/
|
|
87
|
+
export declare function selectPatternVersion(modulePixels: number, width: number, height: number, headroomPixels?: number): number;
|
|
88
|
+
/** Random text long enough to fill the version's data capacity, so no repeating pad codewords appear. */
|
|
89
|
+
export declare function createPatternText(version: number, seed: number): string;
|
|
90
|
+
/**
|
|
91
|
+
* Finder patterns and alignment patterns are dropped, then refilled with seeded random bits. Leaving
|
|
92
|
+
* those cells light would punch a 9x9 (finder) or 5x5 (alignment) white hole into the texture; the
|
|
93
|
+
* refill keeps the field even. The refill stream is derived from the run seed, so a seed reproduces
|
|
94
|
+
* the whole pattern.
|
|
95
|
+
*/
|
|
96
|
+
export declare function stripMarkerModules(matrix: QrCodeGenerateResult, seed: number): boolean[][];
|
|
97
|
+
/** Cells the marker refill replaces: the three 9x9 finder areas plus every 5x5 alignment block. */
|
|
98
|
+
export declare function countMarkerModules(matrix: QrCodeGenerateResult): number;
|
|
99
|
+
/**
|
|
100
|
+
* Renders a pixel style — square is full cells, dot is circles, rounded blends neighbours.
|
|
101
|
+
* Square and dot use the standard full-cell and circular module styles; rounded uses one
|
|
102
|
+
* inscribed circle per dark module plus corner wedges that
|
|
103
|
+
* bridge dark neighbours (and fill inner corners of light modules).
|
|
104
|
+
*/
|
|
105
|
+
export declare function renderPattern(matrix: boolean[][], modulePixels: number, pixelStyle?: PixelStyle, options?: PatternRenderOptions): Promise<Uint8Array>;
|
|
106
|
+
/**
|
|
107
|
+
* Renders the legacy rounded pixel style: one inscribed circle per dark module plus
|
|
108
|
+
* corner wedges that bridge dark neighbours (and fill inner corners of light modules).
|
|
109
|
+
* @deprecated Use renderPattern with explicit pixelStyle instead.
|
|
110
|
+
*/
|
|
111
|
+
export declare function renderRoundedPattern(matrix: boolean[][], modulePixels: number, options?: PatternRenderOptions): Promise<Uint8Array>;
|
|
112
|
+
/**
|
|
113
|
+
* Renders the poster-sized marker-free texture at a fixed module pitch. The version is the smallest
|
|
114
|
+
* whose modules plus margin cover the canvas, the code is center-cropped at whole-module offsets,
|
|
115
|
+
* and the same seed reproduces the same bytes.
|
|
116
|
+
*/
|
|
117
|
+
export declare function buildPosterPattern(options: PosterPatternOptions): Promise<PosterPatternLattice>;
|
|
118
|
+
/** Renders the poster pattern, or just its lattice, in one call. */
|
|
119
|
+
export declare function renderPosterPattern(options: PosterPatternOptions): Promise<PosterPattern>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QrBoxInput, QrPlacement, RegionMask } from './types';
|
|
2
|
+
export declare const MINIMUM_MODULE_PIXELS = 4;
|
|
3
|
+
export declare const REGION_TOO_SMALL_MESSAGE = "The painted region cannot fit the QR code at the minimum 4px module size. Supply a larger region or QR position.";
|
|
4
|
+
export declare function placeQr(mask: RegionMask, totalModules: number, requested?: QrBoxInput): QrPlacement;
|
|
5
|
+
export declare function boxIsInsideMask(mask: RegionMask, x: number, y: number, size: number, rotation?: number): boolean;
|
|
6
|
+
export declare function findClosestSquare(mask: RegionMask, size: number): {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|