@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,57 @@
|
|
|
1
|
+
import type { PixelStyle } from './pattern';
|
|
2
|
+
import type { QrPalette } from './palette';
|
|
3
|
+
import type { LoadedPng } from './image';
|
|
4
|
+
import type { QrMetadata, QrSourceTrim, VerificationCheck } from './types';
|
|
5
|
+
export declare const GENERATED_QR_PATH = "<generated>";
|
|
6
|
+
export interface GeneratedQr {
|
|
7
|
+
image: LoadedPng;
|
|
8
|
+
version: number;
|
|
9
|
+
}
|
|
10
|
+
export type MarkerStyle = 'square' | 'rounded';
|
|
11
|
+
export type MarkerShape = 'square' | 'circle' | 'octagon' | 'squircle';
|
|
12
|
+
export type MarkerInner = 'square' | 'circle' | 'plus' | 'diamond' | 'squircle';
|
|
13
|
+
export type MarkerSub = 'square' | 'circle';
|
|
14
|
+
export type FinderMarkerSettings = {
|
|
15
|
+
style: MarkerStyle;
|
|
16
|
+
shape: MarkerShape;
|
|
17
|
+
inner: MarkerInner;
|
|
18
|
+
};
|
|
19
|
+
export type FinderMarkerSettingsMap = Record<'tl' | 'tr' | 'bl', FinderMarkerSettings>;
|
|
20
|
+
/** Builds the same two-module-margin QR profile accepted from legacy PNG inputs. */
|
|
21
|
+
export declare function generateQrFromContent(content: string, ecc?: 'L' | 'M' | 'Q' | 'H', pixelStyle?: PixelStyle, markerStyle?: MarkerStyle, markerShape?: MarkerShape, markerInner?: MarkerInner, markerSub?: MarkerSub, finderMarkers?: FinderMarkerSettingsMap, palette?: QrPalette): Promise<GeneratedQr>;
|
|
22
|
+
export declare function decodeQrBuffer(buffer: Uint8Array): Promise<string>;
|
|
23
|
+
export interface DecodedQr {
|
|
24
|
+
text: string;
|
|
25
|
+
decoder: 'zxing' | 'jsqr';
|
|
26
|
+
version?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function decodeQrRaw(data: Uint8Array, width: number, height: number): string;
|
|
29
|
+
export declare function decodeQrRawDetailed(data: Uint8Array, width: number, height: number): DecodedQr;
|
|
30
|
+
export declare function inspectQrSource(image: LoadedPng, decodedText: string, detectedVersion?: number): QrMetadata;
|
|
31
|
+
export interface ResolvedQrSource {
|
|
32
|
+
/** The file as supplied, kept for input reporting. */
|
|
33
|
+
source: LoadedPng;
|
|
34
|
+
/** A square image with a two-module quiet zone, ready for metadata and normalization. */
|
|
35
|
+
image: LoadedPng;
|
|
36
|
+
/** `added` when the input was a trimmed code grid and the margin was rebuilt locally. */
|
|
37
|
+
quietZoneSource: 'source' | 'added';
|
|
38
|
+
trim?: QrSourceTrim;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves a QR input to a square image with a two-module quiet zone. The documented profile is
|
|
42
|
+
* preferred; a bare code grid (no margin, or a margin that is not a whole module) is recovered by
|
|
43
|
+
* locating the integer-scaled module grid and re-padding it with a fresh light margin, so a tight
|
|
44
|
+
* crop such as `source/qr.png` is accepted without resampling a single code pixel.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveQrSource(source: LoadedPng, detectedVersion?: number): Promise<ResolvedQrSource>;
|
|
47
|
+
export declare function normalizeQr(image: LoadedPng, targetSize: number): Promise<Uint8Array>;
|
|
48
|
+
/**
|
|
49
|
+
* Converts the normalized QR's light pixels to alpha for preview and standalone PNG export.
|
|
50
|
+
* The alpha ramps linearly between the palette's ink lightness and background lightness, and
|
|
51
|
+
* RGB channels keep each pixel's own color, so colored palettes stay visible on transparency.
|
|
52
|
+
* The default black/white palette reproduces the historical `alpha = 255 - luminance` behavior.
|
|
53
|
+
*/
|
|
54
|
+
export declare function transparentQrBackground(png: Uint8Array, palette?: QrPalette): Promise<Uint8Array>;
|
|
55
|
+
export declare function verifyQrVariant(name: VerificationCheck['name'], buffer: Uint8Array, expectedText: string): Promise<VerificationCheck>;
|
|
56
|
+
/** Crop whole modules from the central third, away from the corner finder markers. */
|
|
57
|
+
export declare function cropQrPattern(qr: Uint8Array, totalModules: number, modulePixels: number): Promise<Uint8Array>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rotation geometry for the placement step. The QR generator is unaware of
|
|
3
|
+
* rotation: it always produces the complete upright square plate. Rotation is a
|
|
4
|
+
* property of placement — clockwise degrees around the unrotated square's
|
|
5
|
+
* centre, applied once when compositing the finished plate onto the poster.
|
|
6
|
+
*
|
|
7
|
+
* `x`, `y`, and `size` keep describing the **unrotated square**, so the existing
|
|
8
|
+
* editor contract (integer origin, module-multiple size) is unchanged.
|
|
9
|
+
*/
|
|
10
|
+
/** Any placement-like box that can carry a rotation, in degrees. */
|
|
11
|
+
export interface RotatableBox {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
size: number;
|
|
15
|
+
rotation?: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Canonicalizes an angle to finite degrees in `[0, 360)`; non-finite input maps to 0. */
|
|
18
|
+
export declare function canonicalizeRotation(deg: number | undefined): number;
|
|
19
|
+
/** Centre of the unrotated placement square in poster pixels. */
|
|
20
|
+
export declare function placementCenter(box: RotatableBox): {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The placement transform precomputed: centre, canonical half size, and the
|
|
26
|
+
* rotation's cos/sin. Tight per-pixel loops (field building, assembly sampling)
|
|
27
|
+
* reuse it instead of recomputing trigonometry for every pixel, and the fast
|
|
28
|
+
* mappers are the same math as the point helpers below.
|
|
29
|
+
*/
|
|
30
|
+
export interface PlacementInverse {
|
|
31
|
+
centerX: number;
|
|
32
|
+
centerY: number;
|
|
33
|
+
size: number;
|
|
34
|
+
cos: number;
|
|
35
|
+
sin: number;
|
|
36
|
+
}
|
|
37
|
+
/** Precomputes the inverse placement transform of {@link posterToPlatePoint}. */
|
|
38
|
+
export declare function placementInverse(box: RotatableBox): PlacementInverse;
|
|
39
|
+
/** Fast {@link plateToPosterPoint} over a precomputed {@link PlacementInverse}. */
|
|
40
|
+
export declare function forwardMap(inv: PlacementInverse, localX: number, localY: number): {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
};
|
|
44
|
+
/** Fast {@link posterToPlatePoint} over a precomputed {@link PlacementInverse}. */
|
|
45
|
+
export declare function inverseMap(inv: PlacementInverse, posterX: number, posterY: number): {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Maps a plate-local point (origin = the unrotated square's top-left) forward to
|
|
51
|
+
* poster coordinates: the same centre and angle the Konva preview uses, so
|
|
52
|
+
* preview, validation, and assembly cannot disagree about the footprint.
|
|
53
|
+
*/
|
|
54
|
+
export declare function plateToPosterPoint(localX: number, localY: number, box: RotatableBox): {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Inverse of {@link plateToPosterPoint}: maps a poster-space sample point back
|
|
60
|
+
* into plate-local coordinates. Assembly and mask containment iterate
|
|
61
|
+
* destination pixels and use this to find the source pixel they read.
|
|
62
|
+
*/
|
|
63
|
+
export declare function posterToPlatePoint(posterX: number, posterY: number, box: RotatableBox): {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
/** The four corners of the rotated plate square, in consistent clockwise order. */
|
|
68
|
+
export declare function rotatedSquareCorners(box: RotatableBox): readonly [{
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
}, {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
}, {
|
|
75
|
+
x: number;
|
|
76
|
+
y: number;
|
|
77
|
+
}, {
|
|
78
|
+
x: number;
|
|
79
|
+
y: number;
|
|
80
|
+
}];
|
|
81
|
+
/** Integer pixel bounds covering the rotated plate's footprint, clipped to the canvas. */
|
|
82
|
+
export declare function rotatedFootprintBounds(box: RotatableBox, width: number, height: number): {
|
|
83
|
+
left: number;
|
|
84
|
+
top: number;
|
|
85
|
+
right: number;
|
|
86
|
+
bottom: number;
|
|
87
|
+
};
|
|
88
|
+
/** Tight integer poster-space AABB of the painted region, end coordinates exclusive. */
|
|
89
|
+
export interface PixelBounds {
|
|
90
|
+
x0: number;
|
|
91
|
+
y0: number;
|
|
92
|
+
x1: number;
|
|
93
|
+
y1: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The upright assembly's working canvas, expressed in plate-local coordinates
|
|
97
|
+
* (origin = the unrotated placement square's top-left). The frame is the integer
|
|
98
|
+
* AABB that contains every poster pixel's inverse-rotated sample point, so the
|
|
99
|
+
* whole-module pipeline can run there and the finished overlay can ride the same
|
|
100
|
+
* placement transform back onto the poster. See doc/plan/rotated-mask-fill.md.
|
|
101
|
+
*/
|
|
102
|
+
export interface QrFrame {
|
|
103
|
+
/** Plate-local x/y of the working canvas' top-left; can be negative. */
|
|
104
|
+
left: number;
|
|
105
|
+
top: number;
|
|
106
|
+
width: number;
|
|
107
|
+
height: number;
|
|
108
|
+
/** The unrotated QR box expressed in working-canvas coordinates. */
|
|
109
|
+
qr: {
|
|
110
|
+
x: number;
|
|
111
|
+
y: number;
|
|
112
|
+
size: number;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The working frame for a rotated placement: the plate-local AABB of the
|
|
117
|
+
* inverse-mapped painted-region bounds, floored/ceiled to whole pixels. Every
|
|
118
|
+
* poster pixel whose sample point can land inside the region maps into this
|
|
119
|
+
* frame, so mask sampling and overlay generation never look outside it.
|
|
120
|
+
*/
|
|
121
|
+
export declare function qrWorkingFrame(placement: RotatableBox, regionBounds: PixelBounds): QrFrame;
|
|
122
|
+
/**
|
|
123
|
+
* Samples the original region mask into the working frame with the same inverse
|
|
124
|
+
* transform assembly's plate pixels use: each working pixel centre maps forward
|
|
125
|
+
* to its poster sample point, and the working mask keeps that poster pixel's
|
|
126
|
+
* selection. Off-poster samples are 0, so safe-area checks stay whole blocks.
|
|
127
|
+
*/
|
|
128
|
+
export declare function sampleMaskIntoQrFrame(mask: Uint8Array, posterWidth: number, posterHeight: number, frame: QrFrame, placement: RotatableBox): Uint8Array;
|
|
129
|
+
/** Tight poster-space AABB of a painted-region mask, end coordinates exclusive. */
|
|
130
|
+
export declare function regionPixelBounds(mask: {
|
|
131
|
+
data: Uint8Array;
|
|
132
|
+
width: number;
|
|
133
|
+
height: number;
|
|
134
|
+
}): PixelBounds;
|
|
135
|
+
/**
|
|
136
|
+
* Whether a plate-local point lies inside the plate square `[0, size) × [0, size)`.
|
|
137
|
+
* Nearest-neighbour sampling maps a poster pixel centre here and copies the
|
|
138
|
+
* source pixel at `floor(x), floor(y)` when it does.
|
|
139
|
+
*/
|
|
140
|
+
export declare function localPointInPlate(localX: number, localY: number, size: number): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The rotated-frame invariant: every poster pixel centre that belongs to the valid
|
|
143
|
+
* placed QR square must map to a working pixel inside the working frame. A frame
|
|
144
|
+
* built from wrong region bounds would silently drop plate cells, and the assembly's
|
|
145
|
+
* `qrPixels` check never sees a plate cell it cannot address. Rejected with the
|
|
146
|
+
* layout error both validation and assembly report, so a bad placement is flagged
|
|
147
|
+
* before export instead of clipping silently (doc/plan/rotated-working-frame-clipping.md).
|
|
148
|
+
*/
|
|
149
|
+
export declare function assertFrameHoldsPlacement(frame: QrFrame, placement: RotatableBox, posterWidth: number, posterHeight: number): void;
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import type { LoadedPng } from './image';
|
|
2
|
+
import type { DecodedQr } from './qr';
|
|
3
|
+
export interface ImageDimensions {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BoundingBox {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Point {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RegionMask extends ImageDimensions {
|
|
18
|
+
/** Row-major binary coverage: 0 is outside, 255 is inside M. */
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
source: 'auto' | 'file';
|
|
21
|
+
area: number;
|
|
22
|
+
bounds: BoundingBox;
|
|
23
|
+
centroid: Point;
|
|
24
|
+
detection?: {
|
|
25
|
+
strongBlackThreshold: number;
|
|
26
|
+
tolerantLumaThreshold: number;
|
|
27
|
+
separationRadius: number;
|
|
28
|
+
densityThreshold: number;
|
|
29
|
+
candidateAreas: number[];
|
|
30
|
+
dominanceRatio: number | null;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface QrMetadata extends ImageDimensions {
|
|
34
|
+
decodedText: string;
|
|
35
|
+
version: number;
|
|
36
|
+
qrModules: number;
|
|
37
|
+
quietZoneModules: 2;
|
|
38
|
+
totalModules: number;
|
|
39
|
+
sourceModulePixels: number;
|
|
40
|
+
quietZoneLightRatio: number;
|
|
41
|
+
/** `added` when the input was a bare code grid and the two-module margin was rebuilt locally. */
|
|
42
|
+
quietZoneSource?: 'added';
|
|
43
|
+
/** Trimmed-away margin of a code-grid input, in source pixels. */
|
|
44
|
+
sourceTrim?: QrSourceTrim;
|
|
45
|
+
}
|
|
46
|
+
export interface QrSourceTrim {
|
|
47
|
+
left: number;
|
|
48
|
+
top: number;
|
|
49
|
+
right: number;
|
|
50
|
+
bottom: number;
|
|
51
|
+
modulePixels: number;
|
|
52
|
+
}
|
|
53
|
+
export interface QrPlacement {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
size: number;
|
|
57
|
+
/** Clockwise degrees the finished plate is rotated around the square's centre; canonical [0, 360). */
|
|
58
|
+
rotation: number;
|
|
59
|
+
modulePixels: number;
|
|
60
|
+
totalModules: number;
|
|
61
|
+
mode: 'auto' | 'manual';
|
|
62
|
+
/** Extra decorative modules reserved around the QR; retained for report compatibility. */
|
|
63
|
+
artPaddingModules: number;
|
|
64
|
+
}
|
|
65
|
+
export interface ResolvedLayout {
|
|
66
|
+
/** The poster exactly as supplied. */
|
|
67
|
+
poster: LoadedPng;
|
|
68
|
+
/** The QR file exactly as supplied, kept for input reporting. */
|
|
69
|
+
qrSource: LoadedPng;
|
|
70
|
+
/** The QR image placement uses: the input, or a code grid re-padded with a quiet zone. */
|
|
71
|
+
qrImage: LoadedPng;
|
|
72
|
+
maskInput?: LoadedPng;
|
|
73
|
+
regionMask: RegionMask;
|
|
74
|
+
decoded: DecodedQr;
|
|
75
|
+
qrMetadata: QrMetadata;
|
|
76
|
+
placement: QrPlacement;
|
|
77
|
+
normalizedQr: Uint8Array;
|
|
78
|
+
}
|
|
79
|
+
export interface VerificationCheck {
|
|
80
|
+
name: 'sourceQr' | 'normalizedQr' | 'beforeAi' | 'halfScale' | 'jpeg80' | 'poster' | 'posterHalfScale' | 'posterJpeg80' | 'outsideRegionPixels' | 'qrPixels' | 'qrPlateCorners' | 'moduleCut' | 'alphaPreserved' | 'transparentBackground';
|
|
81
|
+
passed: boolean;
|
|
82
|
+
decodedText?: string;
|
|
83
|
+
decoder?: 'zxing' | 'jsqr';
|
|
84
|
+
version?: number;
|
|
85
|
+
error?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface VerificationResult {
|
|
88
|
+
expectedText: string;
|
|
89
|
+
checks: VerificationCheck[];
|
|
90
|
+
qualified: boolean;
|
|
91
|
+
/** Checks that a mode deliberately does not run, so a qualified report stays honest. */
|
|
92
|
+
skippedChecks?: VerificationCheck['name'][];
|
|
93
|
+
}
|
|
94
|
+
export interface ReportV1 {
|
|
95
|
+
schemaVersion: 1;
|
|
96
|
+
status: 'prepared' | 'verification_failed';
|
|
97
|
+
qualified: boolean;
|
|
98
|
+
dryRun: true;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
durationMs: number;
|
|
101
|
+
inputs: {
|
|
102
|
+
poster: {
|
|
103
|
+
path: string;
|
|
104
|
+
sha256: string;
|
|
105
|
+
width: number;
|
|
106
|
+
height: number;
|
|
107
|
+
};
|
|
108
|
+
qr: {
|
|
109
|
+
path: string;
|
|
110
|
+
sha256: string;
|
|
111
|
+
width: number;
|
|
112
|
+
height: number;
|
|
113
|
+
};
|
|
114
|
+
mask?: {
|
|
115
|
+
path: string;
|
|
116
|
+
sha256: string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
region: {
|
|
120
|
+
source: 'auto' | 'file';
|
|
121
|
+
area: number;
|
|
122
|
+
bounds: BoundingBox;
|
|
123
|
+
centroid: Point;
|
|
124
|
+
detection?: RegionMask['detection'];
|
|
125
|
+
};
|
|
126
|
+
qr: QrMetadata & {
|
|
127
|
+
normalizedSize: number;
|
|
128
|
+
normalizedModulePixels: number;
|
|
129
|
+
};
|
|
130
|
+
placement: QrPlacement;
|
|
131
|
+
artifacts: Record<'regionMask' | 'qr' | 'layoutPreview' | 'editMask' | 'beforeAi', string>;
|
|
132
|
+
verification: VerificationResult;
|
|
133
|
+
warnings: string[];
|
|
134
|
+
}
|
|
135
|
+
export interface QrBoxInput {
|
|
136
|
+
x: number;
|
|
137
|
+
y: number;
|
|
138
|
+
size: number;
|
|
139
|
+
/** Optional clockwise rotation in degrees; defaults to 0 (upright). */
|
|
140
|
+
rotation?: number;
|
|
141
|
+
}
|
|
142
|
+
export interface PosterInputOptions {
|
|
143
|
+
inputPath: string;
|
|
144
|
+
outputDir: string;
|
|
145
|
+
maskPath?: string;
|
|
146
|
+
qrBox?: QrBoxInput;
|
|
147
|
+
force?: boolean;
|
|
148
|
+
}
|
|
149
|
+
/** Generate a QR from content, or retain the legacy QR PNG input for programmatic callers. */
|
|
150
|
+
export type QrInputOptions = {
|
|
151
|
+
content: string;
|
|
152
|
+
qrPath?: never;
|
|
153
|
+
expectedText?: never;
|
|
154
|
+
} | {
|
|
155
|
+
content?: never;
|
|
156
|
+
qrPath: string;
|
|
157
|
+
expectedText?: string;
|
|
158
|
+
};
|
|
159
|
+
export type PreparePosterOptions = PosterInputOptions & QrInputOptions & {
|
|
160
|
+
dryRun: true;
|
|
161
|
+
};
|
|
162
|
+
export interface PrepareResult {
|
|
163
|
+
report: ReportV1;
|
|
164
|
+
outputDir: string;
|
|
165
|
+
}
|
|
166
|
+
export interface CompositePosterInputs {
|
|
167
|
+
original: Uint8Array;
|
|
168
|
+
generated: Uint8Array;
|
|
169
|
+
regionMask: RegionMask;
|
|
170
|
+
qr: Uint8Array;
|
|
171
|
+
placement: QrPlacement;
|
|
172
|
+
}
|
|
173
|
+
export type GeneratePosterOptions = PosterInputOptions & QrInputOptions & {
|
|
174
|
+
apiKey?: string;
|
|
175
|
+
baseUrl?: string;
|
|
176
|
+
model?: string;
|
|
177
|
+
prompt?: string;
|
|
178
|
+
generatedImagePath?: string;
|
|
179
|
+
};
|
|
180
|
+
export interface ReportV2 extends Omit<ReportV1, 'schemaVersion' | 'status' | 'dryRun' | 'artifacts'> {
|
|
181
|
+
schemaVersion: 2;
|
|
182
|
+
status: 'generated' | 'verification_failed' | 'generation_failed';
|
|
183
|
+
dryRun: false;
|
|
184
|
+
artifacts: ReportV1['artifacts'] & {
|
|
185
|
+
aiRaw?: string;
|
|
186
|
+
poster?: string;
|
|
187
|
+
patternReference?: string;
|
|
188
|
+
referenceCanvas?: string;
|
|
189
|
+
};
|
|
190
|
+
generation: {
|
|
191
|
+
source: 'qwen' | 'file';
|
|
192
|
+
model: string;
|
|
193
|
+
prompt: string;
|
|
194
|
+
canvas: ImageDimensions;
|
|
195
|
+
durationMs: number;
|
|
196
|
+
requestId?: string;
|
|
197
|
+
usage?: unknown;
|
|
198
|
+
error?: string;
|
|
199
|
+
};
|
|
200
|
+
phoneScan: 'untested';
|
|
201
|
+
}
|
|
202
|
+
export interface GenerateResult {
|
|
203
|
+
report: ReportV2;
|
|
204
|
+
outputDir: string;
|
|
205
|
+
}
|
|
206
|
+
export type PatternPreviewOptions = PosterInputOptions & QrInputOptions & {
|
|
207
|
+
/** Module pitch in poster pixels; defaults to the pitch the pipeline places on the poster. */
|
|
208
|
+
modulePixels?: number;
|
|
209
|
+
/** Seed for the random text line; defaults to a fresh random seed per run. */
|
|
210
|
+
seed?: number;
|
|
211
|
+
};
|
|
212
|
+
export interface PatternReport {
|
|
213
|
+
schemaVersion: 3;
|
|
214
|
+
mode: 'pattern-preview';
|
|
215
|
+
status: 'generated';
|
|
216
|
+
createdAt: string;
|
|
217
|
+
durationMs: number;
|
|
218
|
+
inputs: ReportV1['inputs'];
|
|
219
|
+
region: ReportV1['region'];
|
|
220
|
+
placement: QrPlacement;
|
|
221
|
+
pitchSource: 'placement' | 'override';
|
|
222
|
+
pattern: {
|
|
223
|
+
seed: number;
|
|
224
|
+
alphabet: string;
|
|
225
|
+
textLength: number;
|
|
226
|
+
textSha256: string;
|
|
227
|
+
ecc: 'M';
|
|
228
|
+
version: number;
|
|
229
|
+
qrModules: number;
|
|
230
|
+
quietZoneModules: 2;
|
|
231
|
+
totalModules: number;
|
|
232
|
+
modulePixels: number;
|
|
233
|
+
pixelStyle: 'square' | 'rounded' | 'dot';
|
|
234
|
+
removedTypes: Array<'Position' | 'Alignment'>;
|
|
235
|
+
/** Dropped marker cells are refilled with seeded random bits, never left light. */
|
|
236
|
+
markerRefill: 'seeded-random';
|
|
237
|
+
refilledModules: number;
|
|
238
|
+
codeSize: number;
|
|
239
|
+
canvas: ImageDimensions;
|
|
240
|
+
crop: {
|
|
241
|
+
left: number;
|
|
242
|
+
top: number;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
artifacts: {
|
|
246
|
+
pattern: string;
|
|
247
|
+
patternSha256: string;
|
|
248
|
+
};
|
|
249
|
+
warnings: string[];
|
|
250
|
+
}
|
|
251
|
+
export interface PatternPreviewResult {
|
|
252
|
+
report: PatternReport;
|
|
253
|
+
outputDir: string;
|
|
254
|
+
}
|
|
255
|
+
export interface PatternCutOptions {
|
|
256
|
+
/** Rendered pattern PNG to cut, poster-sized. */
|
|
257
|
+
inputPath: string;
|
|
258
|
+
/** Same-size mask PNG: transparent or dark pixels select the cut shape. */
|
|
259
|
+
maskPath: string;
|
|
260
|
+
outputDir: string;
|
|
261
|
+
/** Corner fillet radius in pixels; defaults to 5. */
|
|
262
|
+
radius?: number;
|
|
263
|
+
/** Douglas-Peucker tolerance in pixels for the traced outline; defaults to 3. */
|
|
264
|
+
smoothTolerance?: number;
|
|
265
|
+
force?: boolean;
|
|
266
|
+
}
|
|
267
|
+
export interface PatternCutReport {
|
|
268
|
+
schemaVersion: 4;
|
|
269
|
+
mode: 'pattern-cut';
|
|
270
|
+
status: 'generated';
|
|
271
|
+
createdAt: string;
|
|
272
|
+
durationMs: number;
|
|
273
|
+
inputs: {
|
|
274
|
+
pattern: {
|
|
275
|
+
path: string;
|
|
276
|
+
sha256: string;
|
|
277
|
+
width: number;
|
|
278
|
+
height: number;
|
|
279
|
+
};
|
|
280
|
+
mask: {
|
|
281
|
+
path: string;
|
|
282
|
+
sha256: string;
|
|
283
|
+
width: number;
|
|
284
|
+
height: number;
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
cut: {
|
|
288
|
+
radius: number;
|
|
289
|
+
smoothTolerance: number;
|
|
290
|
+
/** A mask pixel selects the shape when it is transparent or dark. */
|
|
291
|
+
keep: 'transparent-or-dark';
|
|
292
|
+
minLoopArea: number;
|
|
293
|
+
/** Solid black band retained inside the letter outline, in pixels. */
|
|
294
|
+
borderWidth: number;
|
|
295
|
+
};
|
|
296
|
+
shape: {
|
|
297
|
+
loopsTraced: number;
|
|
298
|
+
loopsKept: number;
|
|
299
|
+
specksDropped: number;
|
|
300
|
+
verticesTraced: number;
|
|
301
|
+
verticesSimplified: number;
|
|
302
|
+
holes: number;
|
|
303
|
+
/** Area of the simplified cut polygon before corner rounding. */
|
|
304
|
+
area: number;
|
|
305
|
+
radiusClamped: boolean;
|
|
306
|
+
bounds: BoundingBox;
|
|
307
|
+
};
|
|
308
|
+
artifacts: {
|
|
309
|
+
svg: string;
|
|
310
|
+
svgSha256: string;
|
|
311
|
+
png: string;
|
|
312
|
+
pngSha256: string;
|
|
313
|
+
};
|
|
314
|
+
warnings: string[];
|
|
315
|
+
}
|
|
316
|
+
export interface PatternCutResult {
|
|
317
|
+
report: PatternCutReport;
|
|
318
|
+
outputDir: string;
|
|
319
|
+
}
|
|
320
|
+
export type AssemblePosterOptions = PosterInputOptions & QrInputOptions & {
|
|
321
|
+
/** Seed for the pattern random text line and the marker refill; defaults to a fresh seed per run. */
|
|
322
|
+
seed?: number;
|
|
323
|
+
/** Depth of the light band kept beside each finder marker: one whole module. */
|
|
324
|
+
qrMargin?: 1;
|
|
325
|
+
/** Keep one safe-module ring along the inside edge of the selected region light. */
|
|
326
|
+
regionMargin?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Plate corner treatment: zero keeps the diagonal corner block beside each finder marker light,
|
|
329
|
+
* and any positive value hands those three blocks to the texture. Defaults to two module pitches
|
|
330
|
+
* (12px on the bundled version-5 fixture), which rounds the plate. The region silhouette is always
|
|
331
|
+
* whole modules, so there is no fillet to size.
|
|
332
|
+
*/
|
|
333
|
+
radius?: number;
|
|
334
|
+
/** Outer dark rim thickness in modules, 0 disables the rim. Range 0-5. */
|
|
335
|
+
rimModules?: number;
|
|
336
|
+
/** When true the rim's outer corners are rounded with antialiased edges. */
|
|
337
|
+
rimRounded?: boolean;
|
|
338
|
+
/** Not used by assembly: the cut is module-aligned rather than traced. Rejected when supplied. */
|
|
339
|
+
smoothTolerance?: number;
|
|
340
|
+
};
|
|
341
|
+
export interface AssembleReport {
|
|
342
|
+
schemaVersion: 8;
|
|
343
|
+
mode: 'assemble';
|
|
344
|
+
status: 'generated' | 'verification_failed';
|
|
345
|
+
qualified: boolean;
|
|
346
|
+
createdAt: string;
|
|
347
|
+
durationMs: number;
|
|
348
|
+
inputs: ReportV1['inputs'];
|
|
349
|
+
region: ReportV1['region'];
|
|
350
|
+
qr: ReportV1['qr'] & {
|
|
351
|
+
/** The pixels actually composited: the code grid plus the light band beside the markers. */
|
|
352
|
+
overlay: {
|
|
353
|
+
/** The light band is kept beside the finder markers only. */
|
|
354
|
+
band: 'markers';
|
|
355
|
+
/** Depth of the light band beside each marker, in modules. */
|
|
356
|
+
quietZoneModules: number;
|
|
357
|
+
/** Finder footprint each band arm spans, in modules. */
|
|
358
|
+
markerModules: number;
|
|
359
|
+
/** Crop of the normalized QR the plate copies verbatim: the code grid, no quiet zone. */
|
|
360
|
+
crop: {
|
|
361
|
+
left: number;
|
|
362
|
+
top: number;
|
|
363
|
+
size: number;
|
|
364
|
+
};
|
|
365
|
+
x: number;
|
|
366
|
+
y: number;
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
placement: QrPlacement;
|
|
370
|
+
pattern: PatternReport['pattern'] & {
|
|
371
|
+
/** The settings palette the generated QR and the texture were rendered with. */
|
|
372
|
+
colors: {
|
|
373
|
+
pixel: string;
|
|
374
|
+
marker: string;
|
|
375
|
+
background: string;
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* The texture window is phase-locked to the placed QR, so both share one module lattice.
|
|
379
|
+
* `phase` is the residual offset of the texture's module boundaries from the QR lattice:
|
|
380
|
+
* `0,0` means they coincide.
|
|
381
|
+
*/
|
|
382
|
+
alignment: {
|
|
383
|
+
alignedToQr: boolean;
|
|
384
|
+
phase: {
|
|
385
|
+
x: number;
|
|
386
|
+
y: number;
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
cut: {
|
|
391
|
+
/** Poster-space module pitch the cut is quantized to; equals the placed QR pitch. */
|
|
392
|
+
modulePixels: number;
|
|
393
|
+
/** Poster-space origin of the module lattice, in `[0, modulePixels)`. */
|
|
394
|
+
lattice: {
|
|
395
|
+
x: number;
|
|
396
|
+
y: number;
|
|
397
|
+
};
|
|
398
|
+
/** Requested `--cut-radius`; zero leaves the plate square, a positive value rounds it. */
|
|
399
|
+
radius: number;
|
|
400
|
+
/** Modules whose whole pixel block is inside the painted region. */
|
|
401
|
+
safeModules: number;
|
|
402
|
+
/** Modules the region covers only in part; the cut leaves them as the original artwork. */
|
|
403
|
+
droppedPartialModules: number;
|
|
404
|
+
/** Region pixels inside those dropped modules. */
|
|
405
|
+
droppedPartialPixels: number;
|
|
406
|
+
/** Modules the cut draws: safe modules minus the QR plate hole. */
|
|
407
|
+
drawnModules: number;
|
|
408
|
+
/** Outer rings of drawn modules forced dark; rounded is antialiased. */
|
|
409
|
+
rim: {
|
|
410
|
+
modules: number;
|
|
411
|
+
style: 'cell' | 'rounded-antialiased';
|
|
412
|
+
};
|
|
413
|
+
/** Light ring of safe modules along the region edge, 0 when disabled. */
|
|
414
|
+
regionMarginModules: 0 | 1;
|
|
415
|
+
/** Modules of the plate handed back to the texture: the diagonal block at each marker corner. */
|
|
416
|
+
plateCornerModules: number;
|
|
417
|
+
/** The cut shape is the detected or supplied painted region itself. */
|
|
418
|
+
keep: 'region-mask';
|
|
419
|
+
/** Whole modules replace the original pixels; antialiased blends the edge. */
|
|
420
|
+
edgeBlend: 'cell-aligned-over-original' | 'antialiased';
|
|
421
|
+
};
|
|
422
|
+
/** The plate the texture is cut around and the QR is drawn in; all of it is whole modules. */
|
|
423
|
+
qrPlate: {
|
|
424
|
+
/** The light band is kept beside the finder markers only. */
|
|
425
|
+
band: 'markers';
|
|
426
|
+
/** Depth of the light band beside each marker: one whole module. */
|
|
427
|
+
marginModules: 1;
|
|
428
|
+
/** The band actually painted, in pixels: `marginModules * modulePixels`. */
|
|
429
|
+
marginPixels: number;
|
|
430
|
+
/** Finder footprint each band arm spans, in modules. */
|
|
431
|
+
markerModules: number;
|
|
432
|
+
/** Light cells the band keeps beside the three markers, corner blocks included. */
|
|
433
|
+
bandCells: number;
|
|
434
|
+
/** The code grid the plate copies verbatim; the band sits outside it. */
|
|
435
|
+
box: BoundingBox;
|
|
436
|
+
/** Modules the hole covers: code grid plus band, corner blocks already handed back. */
|
|
437
|
+
holeModules: number;
|
|
438
|
+
/** Modules handed back to the texture, 0 when `--cut-radius` is zero. */
|
|
439
|
+
cornerModules: number;
|
|
440
|
+
/** Pixels those modules leave as texture instead of the QR's own light band. */
|
|
441
|
+
cornerTexturePixels: number;
|
|
442
|
+
};
|
|
443
|
+
shape: {
|
|
444
|
+
/** Canvas-pixel bounds of the drawn modules. */
|
|
445
|
+
bounds: BoundingBox;
|
|
446
|
+
/** Pixels the drawn modules cover: texture, rim, or light margin. */
|
|
447
|
+
area: number;
|
|
448
|
+
/** Modules the cut draws. */
|
|
449
|
+
modules: number;
|
|
450
|
+
/** Drawn modules forced dark as the rim. */
|
|
451
|
+
rimModules: number;
|
|
452
|
+
/** Drawn modules painted light as the region edge or tight-QR quiet margin. */
|
|
453
|
+
marginModules: number;
|
|
454
|
+
/** Drawn modules carrying the texture. */
|
|
455
|
+
textureModules: number;
|
|
456
|
+
};
|
|
457
|
+
artifacts: {
|
|
458
|
+
poster: string;
|
|
459
|
+
posterSha256: string;
|
|
460
|
+
regionMask: string;
|
|
461
|
+
qr: string;
|
|
462
|
+
qrSha256: string;
|
|
463
|
+
patternCutPng: string;
|
|
464
|
+
patternCutPngSha256: string;
|
|
465
|
+
patternCutSvg: string;
|
|
466
|
+
patternCutSvgSha256: string;
|
|
467
|
+
};
|
|
468
|
+
verification: VerificationResult;
|
|
469
|
+
phoneScan: 'untested';
|
|
470
|
+
warnings: string[];
|
|
471
|
+
}
|
|
472
|
+
export interface AssembleResult {
|
|
473
|
+
report: AssembleReport;
|
|
474
|
+
outputDir: string;
|
|
475
|
+
}
|