@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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/node.js +3130 -0
  4. package/dist/types/core/artifacts.d.ts +2 -0
  5. package/dist/types/core/assemble.d.ts +36 -0
  6. package/dist/types/core/errors.d.ts +6 -0
  7. package/dist/types/core/export-sizes.d.ts +21 -0
  8. package/dist/types/core/image.d.ts +14 -0
  9. package/dist/types/core/imaging/browser.d.ts +23 -0
  10. package/dist/types/core/imaging/cloudflare.d.ts +2 -0
  11. package/dist/types/core/imaging/index.d.ts +5 -0
  12. package/dist/types/core/imaging/node.d.ts +4 -0
  13. package/dist/types/core/imaging/pixels.d.ts +28 -0
  14. package/dist/types/core/imaging/types.d.ts +45 -0
  15. package/dist/types/core/mask.d.ts +11 -0
  16. package/dist/types/core/module-cut.d.ts +107 -0
  17. package/dist/types/core/palette.d.ts +80 -0
  18. package/dist/types/core/pattern-cut.d.ts +96 -0
  19. package/dist/types/core/pattern.d.ts +119 -0
  20. package/dist/types/core/placement.d.ts +9 -0
  21. package/dist/types/core/qr.d.ts +57 -0
  22. package/dist/types/core/rotate.d.ts +149 -0
  23. package/dist/types/core/squircle.d.ts +2 -0
  24. package/dist/types/core/types.d.ts +475 -0
  25. package/dist/types/engine/engine.d.ts +68 -0
  26. package/dist/types/engine/index.d.ts +34 -0
  27. package/dist/types/engine/mapping.d.ts +14 -0
  28. package/dist/types/engine/pipeline.d.ts +77 -0
  29. package/dist/types/engine/types.d.ts +66 -0
  30. package/dist/types/node.d.ts +17 -0
  31. package/dist/types/png-guard.d.ts +17 -0
  32. package/dist/types/recipe.d.ts +132 -0
  33. package/dist/types/schema.d.ts +211 -0
  34. package/dist/types/worker-shim.d.ts +12 -0
  35. package/package.json +79 -0
  36. package/src/core/artifacts.ts +6 -0
  37. package/src/core/assemble.ts +1195 -0
  38. package/src/core/errors.ts +24 -0
  39. package/src/core/export-sizes.ts +179 -0
  40. package/src/core/image.ts +57 -0
  41. package/src/core/imaging/browser.ts +186 -0
  42. package/src/core/imaging/cloudflare.ts +15 -0
  43. package/src/core/imaging/index.ts +14 -0
  44. package/src/core/imaging/node.ts +95 -0
  45. package/src/core/imaging/pixels.ts +121 -0
  46. package/src/core/imaging/types.ts +56 -0
  47. package/src/core/mask.ts +295 -0
  48. package/src/core/module-cut.ts +523 -0
  49. package/src/core/palette.ts +227 -0
  50. package/src/core/pattern-cut.ts +522 -0
  51. package/src/core/pattern.ts +478 -0
  52. package/src/core/placement.ts +94 -0
  53. package/src/core/qr.ts +695 -0
  54. package/src/core/rotate.ts +267 -0
  55. package/src/core/squircle.ts +53 -0
  56. package/src/core/types.ts +477 -0
  57. package/src/engine/engine.ts +316 -0
  58. package/src/engine/index.ts +58 -0
  59. package/src/engine/mapping.ts +66 -0
  60. package/src/engine/pipeline.ts +501 -0
  61. package/src/engine/types.ts +61 -0
  62. package/src/node.ts +45 -0
  63. package/src/png-guard.ts +66 -0
  64. package/src/recipe.ts +161 -0
  65. package/src/schema.ts +137 -0
  66. package/src/wasm.d.ts +5 -0
  67. package/src/worker-shim.ts +15 -0
@@ -0,0 +1,477 @@
1
+ import type { LoadedPng } from './image'
2
+ import type { DecodedQr } from './qr'
3
+
4
+ export interface ImageDimensions {
5
+ width: number
6
+ height: number
7
+ }
8
+
9
+ export interface BoundingBox {
10
+ x: number
11
+ y: number
12
+ width: number
13
+ height: number
14
+ }
15
+
16
+ export interface Point {
17
+ x: number
18
+ y: number
19
+ }
20
+
21
+ export interface RegionMask extends ImageDimensions {
22
+ /** Row-major binary coverage: 0 is outside, 255 is inside M. */
23
+ data: Uint8Array
24
+ source: 'auto' | 'file'
25
+ area: number
26
+ bounds: BoundingBox
27
+ centroid: Point
28
+ detection?: {
29
+ strongBlackThreshold: number
30
+ tolerantLumaThreshold: number
31
+ separationRadius: number
32
+ densityThreshold: number
33
+ candidateAreas: number[]
34
+ dominanceRatio: number | null
35
+ }
36
+ }
37
+
38
+ export interface QrMetadata extends ImageDimensions {
39
+ decodedText: string
40
+ version: number
41
+ qrModules: number
42
+ quietZoneModules: 2
43
+ totalModules: number
44
+ sourceModulePixels: number
45
+ quietZoneLightRatio: number
46
+ /** `added` when the input was a bare code grid and the two-module margin was rebuilt locally. */
47
+ quietZoneSource?: 'added'
48
+ /** Trimmed-away margin of a code-grid input, in source pixels. */
49
+ sourceTrim?: QrSourceTrim
50
+ }
51
+
52
+ export interface QrSourceTrim {
53
+ left: number
54
+ top: number
55
+ right: number
56
+ bottom: number
57
+ modulePixels: number
58
+ }
59
+
60
+ export interface QrPlacement {
61
+ x: number
62
+ y: number
63
+ size: number
64
+ /** Clockwise degrees the finished plate is rotated around the square's centre; canonical [0, 360). */
65
+ rotation: number
66
+ modulePixels: number
67
+ totalModules: number
68
+ mode: 'auto' | 'manual'
69
+ /** Extra decorative modules reserved around the QR; retained for report compatibility. */
70
+ artPaddingModules: number
71
+ }
72
+
73
+ export interface ResolvedLayout {
74
+ /** The poster exactly as supplied. */
75
+ poster: LoadedPng
76
+ /** The QR file exactly as supplied, kept for input reporting. */
77
+ qrSource: LoadedPng
78
+ /** The QR image placement uses: the input, or a code grid re-padded with a quiet zone. */
79
+ qrImage: LoadedPng
80
+ maskInput?: LoadedPng
81
+ regionMask: RegionMask
82
+ decoded: DecodedQr
83
+ qrMetadata: QrMetadata
84
+ placement: QrPlacement
85
+ normalizedQr: Uint8Array
86
+ }
87
+
88
+ export interface VerificationCheck {
89
+ name:
90
+ | 'sourceQr'
91
+ | 'normalizedQr'
92
+ | 'beforeAi'
93
+ | 'halfScale'
94
+ | 'jpeg80'
95
+ | 'poster'
96
+ | 'posterHalfScale'
97
+ | 'posterJpeg80'
98
+ | 'outsideRegionPixels'
99
+ | 'qrPixels'
100
+ | 'qrPlateCorners'
101
+ | 'moduleCut'
102
+ | 'alphaPreserved'
103
+ | 'transparentBackground'
104
+ passed: boolean
105
+ decodedText?: string
106
+ decoder?: 'zxing' | 'jsqr'
107
+ version?: number
108
+ error?: string
109
+ }
110
+
111
+ export interface VerificationResult {
112
+ expectedText: string
113
+ checks: VerificationCheck[]
114
+ qualified: boolean
115
+ /** Checks that a mode deliberately does not run, so a qualified report stays honest. */
116
+ skippedChecks?: VerificationCheck['name'][]
117
+ }
118
+
119
+ export interface ReportV1 {
120
+ schemaVersion: 1
121
+ status: 'prepared' | 'verification_failed'
122
+ qualified: boolean
123
+ dryRun: true
124
+ createdAt: string
125
+ durationMs: number
126
+ inputs: {
127
+ poster: { path: string; sha256: string; width: number; height: number }
128
+ qr: { path: string; sha256: string; width: number; height: number }
129
+ mask?: { path: string; sha256: string }
130
+ }
131
+ region: {
132
+ source: 'auto' | 'file'
133
+ area: number
134
+ bounds: BoundingBox
135
+ centroid: Point
136
+ detection?: RegionMask['detection']
137
+ }
138
+ qr: QrMetadata & {
139
+ normalizedSize: number
140
+ normalizedModulePixels: number
141
+ }
142
+ placement: QrPlacement
143
+ artifacts: Record<'regionMask' | 'qr' | 'layoutPreview' | 'editMask' | 'beforeAi', string>
144
+ verification: VerificationResult
145
+ warnings: string[]
146
+ }
147
+
148
+ export interface QrBoxInput {
149
+ x: number
150
+ y: number
151
+ size: number
152
+ /** Optional clockwise rotation in degrees; defaults to 0 (upright). */
153
+ rotation?: number
154
+ }
155
+
156
+ export interface PosterInputOptions {
157
+ inputPath: string
158
+ outputDir: string
159
+ maskPath?: string
160
+ qrBox?: QrBoxInput
161
+ force?: boolean
162
+ }
163
+
164
+ /** Generate a QR from content, or retain the legacy QR PNG input for programmatic callers. */
165
+ export type QrInputOptions =
166
+ | { content: string; qrPath?: never; expectedText?: never }
167
+ | { content?: never; qrPath: string; expectedText?: string }
168
+
169
+ export type PreparePosterOptions = PosterInputOptions &
170
+ QrInputOptions & {
171
+ dryRun: true
172
+ }
173
+
174
+ export interface PrepareResult {
175
+ report: ReportV1
176
+ outputDir: string
177
+ }
178
+
179
+ export interface CompositePosterInputs {
180
+ original: Uint8Array
181
+ generated: Uint8Array
182
+ regionMask: RegionMask
183
+ qr: Uint8Array
184
+ placement: QrPlacement
185
+ }
186
+
187
+ export type GeneratePosterOptions = PosterInputOptions &
188
+ QrInputOptions & {
189
+ apiKey?: string
190
+ baseUrl?: string
191
+ model?: string
192
+ prompt?: string
193
+ generatedImagePath?: string
194
+ }
195
+
196
+ export interface ReportV2 extends Omit<ReportV1, 'schemaVersion' | 'status' | 'dryRun' | 'artifacts'> {
197
+ schemaVersion: 2
198
+ status: 'generated' | 'verification_failed' | 'generation_failed'
199
+ dryRun: false
200
+ artifacts: ReportV1['artifacts'] & {
201
+ aiRaw?: string
202
+ poster?: string
203
+ patternReference?: string
204
+ referenceCanvas?: string
205
+ }
206
+ generation: {
207
+ source: 'qwen' | 'file'
208
+ model: string
209
+ prompt: string
210
+ canvas: ImageDimensions
211
+ durationMs: number
212
+ requestId?: string
213
+ usage?: unknown
214
+ error?: string
215
+ }
216
+ phoneScan: 'untested'
217
+ }
218
+
219
+ export interface GenerateResult {
220
+ report: ReportV2
221
+ outputDir: string
222
+ }
223
+
224
+ export type PatternPreviewOptions = PosterInputOptions &
225
+ QrInputOptions & {
226
+ /** Module pitch in poster pixels; defaults to the pitch the pipeline places on the poster. */
227
+ modulePixels?: number
228
+ /** Seed for the random text line; defaults to a fresh random seed per run. */
229
+ seed?: number
230
+ }
231
+
232
+ export interface PatternReport {
233
+ schemaVersion: 3
234
+ mode: 'pattern-preview'
235
+ status: 'generated'
236
+ createdAt: string
237
+ durationMs: number
238
+ inputs: ReportV1['inputs']
239
+ region: ReportV1['region']
240
+ placement: QrPlacement
241
+ pitchSource: 'placement' | 'override'
242
+ pattern: {
243
+ seed: number
244
+ alphabet: string
245
+ textLength: number
246
+ textSha256: string
247
+ ecc: 'M'
248
+ version: number
249
+ qrModules: number
250
+ quietZoneModules: 2
251
+ totalModules: number
252
+ modulePixels: number
253
+ pixelStyle: 'square' | 'rounded' | 'dot'
254
+ removedTypes: Array<'Position' | 'Alignment'>
255
+ /** Dropped marker cells are refilled with seeded random bits, never left light. */
256
+ markerRefill: 'seeded-random'
257
+ refilledModules: number
258
+ codeSize: number
259
+ canvas: ImageDimensions
260
+ crop: { left: number; top: number }
261
+ }
262
+ artifacts: {
263
+ pattern: string
264
+ patternSha256: string
265
+ }
266
+ warnings: string[]
267
+ }
268
+
269
+ export interface PatternPreviewResult {
270
+ report: PatternReport
271
+ outputDir: string
272
+ }
273
+
274
+ export interface PatternCutOptions {
275
+ /** Rendered pattern PNG to cut, poster-sized. */
276
+ inputPath: string
277
+ /** Same-size mask PNG: transparent or dark pixels select the cut shape. */
278
+ maskPath: string
279
+ outputDir: string
280
+ /** Corner fillet radius in pixels; defaults to 5. */
281
+ radius?: number
282
+ /** Douglas-Peucker tolerance in pixels for the traced outline; defaults to 3. */
283
+ smoothTolerance?: number
284
+ force?: boolean
285
+ }
286
+
287
+ export interface PatternCutReport {
288
+ schemaVersion: 4
289
+ mode: 'pattern-cut'
290
+ status: 'generated'
291
+ createdAt: string
292
+ durationMs: number
293
+ inputs: {
294
+ pattern: { path: string; sha256: string; width: number; height: number }
295
+ mask: { path: string; sha256: string; width: number; height: number }
296
+ }
297
+ cut: {
298
+ radius: number
299
+ smoothTolerance: number
300
+ /** A mask pixel selects the shape when it is transparent or dark. */
301
+ keep: 'transparent-or-dark'
302
+ minLoopArea: number
303
+ /** Solid black band retained inside the letter outline, in pixels. */
304
+ borderWidth: number
305
+ }
306
+ shape: {
307
+ loopsTraced: number
308
+ loopsKept: number
309
+ specksDropped: number
310
+ verticesTraced: number
311
+ verticesSimplified: number
312
+ holes: number
313
+ /** Area of the simplified cut polygon before corner rounding. */
314
+ area: number
315
+ radiusClamped: boolean
316
+ bounds: BoundingBox
317
+ }
318
+ artifacts: {
319
+ svg: string
320
+ svgSha256: string
321
+ png: string
322
+ pngSha256: string
323
+ }
324
+ warnings: string[]
325
+ }
326
+
327
+ export interface PatternCutResult {
328
+ report: PatternCutReport
329
+ outputDir: string
330
+ }
331
+
332
+ export type AssemblePosterOptions = PosterInputOptions &
333
+ QrInputOptions & {
334
+ /** Seed for the pattern random text line and the marker refill; defaults to a fresh seed per run. */
335
+ seed?: number
336
+ /** Depth of the light band kept beside each finder marker: one whole module. */
337
+ qrMargin?: 1
338
+ /** Keep one safe-module ring along the inside edge of the selected region light. */
339
+ regionMargin?: boolean
340
+ /**
341
+ * Plate corner treatment: zero keeps the diagonal corner block beside each finder marker light,
342
+ * and any positive value hands those three blocks to the texture. Defaults to two module pitches
343
+ * (12px on the bundled version-5 fixture), which rounds the plate. The region silhouette is always
344
+ * whole modules, so there is no fillet to size.
345
+ */
346
+ radius?: number
347
+ /** Outer dark rim thickness in modules, 0 disables the rim. Range 0-5. */
348
+ rimModules?: number
349
+ /** When true the rim's outer corners are rounded with antialiased edges. */
350
+ rimRounded?: boolean
351
+ /** Not used by assembly: the cut is module-aligned rather than traced. Rejected when supplied. */
352
+ smoothTolerance?: number
353
+ }
354
+
355
+ export interface AssembleReport {
356
+ schemaVersion: 8
357
+ mode: 'assemble'
358
+ status: 'generated' | 'verification_failed'
359
+ qualified: boolean
360
+ createdAt: string
361
+ durationMs: number
362
+ inputs: ReportV1['inputs']
363
+ region: ReportV1['region']
364
+ qr: ReportV1['qr'] & {
365
+ /** The pixels actually composited: the code grid plus the light band beside the markers. */
366
+ overlay: {
367
+ /** The light band is kept beside the finder markers only. */
368
+ band: 'markers'
369
+ /** Depth of the light band beside each marker, in modules. */
370
+ quietZoneModules: number
371
+ /** Finder footprint each band arm spans, in modules. */
372
+ markerModules: number
373
+ /** Crop of the normalized QR the plate copies verbatim: the code grid, no quiet zone. */
374
+ crop: { left: number; top: number; size: number }
375
+ x: number
376
+ y: number
377
+ }
378
+ }
379
+ placement: QrPlacement
380
+ pattern: PatternReport['pattern'] & {
381
+ /** The settings palette the generated QR and the texture were rendered with. */
382
+ colors: {
383
+ pixel: string
384
+ marker: string
385
+ background: string
386
+ }
387
+ /**
388
+ * The texture window is phase-locked to the placed QR, so both share one module lattice.
389
+ * `phase` is the residual offset of the texture's module boundaries from the QR lattice:
390
+ * `0,0` means they coincide.
391
+ */
392
+ alignment: {
393
+ alignedToQr: boolean
394
+ phase: { x: number; y: number }
395
+ }
396
+ }
397
+ cut: {
398
+ /** Poster-space module pitch the cut is quantized to; equals the placed QR pitch. */
399
+ modulePixels: number
400
+ /** Poster-space origin of the module lattice, in `[0, modulePixels)`. */
401
+ lattice: { x: number; y: number }
402
+ /** Requested `--cut-radius`; zero leaves the plate square, a positive value rounds it. */
403
+ radius: number
404
+ /** Modules whose whole pixel block is inside the painted region. */
405
+ safeModules: number
406
+ /** Modules the region covers only in part; the cut leaves them as the original artwork. */
407
+ droppedPartialModules: number
408
+ /** Region pixels inside those dropped modules. */
409
+ droppedPartialPixels: number
410
+ /** Modules the cut draws: safe modules minus the QR plate hole. */
411
+ drawnModules: number
412
+ /** Outer rings of drawn modules forced dark; rounded is antialiased. */
413
+ rim: { modules: number; style: 'cell' | 'rounded-antialiased' }
414
+ /** Light ring of safe modules along the region edge, 0 when disabled. */
415
+ regionMarginModules: 0 | 1
416
+ /** Modules of the plate handed back to the texture: the diagonal block at each marker corner. */
417
+ plateCornerModules: number
418
+ /** The cut shape is the detected or supplied painted region itself. */
419
+ keep: 'region-mask'
420
+ /** Whole modules replace the original pixels; antialiased blends the edge. */
421
+ edgeBlend: 'cell-aligned-over-original' | 'antialiased'
422
+ }
423
+ /** The plate the texture is cut around and the QR is drawn in; all of it is whole modules. */
424
+ qrPlate: {
425
+ /** The light band is kept beside the finder markers only. */
426
+ band: 'markers'
427
+ /** Depth of the light band beside each marker: one whole module. */
428
+ marginModules: 1
429
+ /** The band actually painted, in pixels: `marginModules * modulePixels`. */
430
+ marginPixels: number
431
+ /** Finder footprint each band arm spans, in modules. */
432
+ markerModules: number
433
+ /** Light cells the band keeps beside the three markers, corner blocks included. */
434
+ bandCells: number
435
+ /** The code grid the plate copies verbatim; the band sits outside it. */
436
+ box: BoundingBox
437
+ /** Modules the hole covers: code grid plus band, corner blocks already handed back. */
438
+ holeModules: number
439
+ /** Modules handed back to the texture, 0 when `--cut-radius` is zero. */
440
+ cornerModules: number
441
+ /** Pixels those modules leave as texture instead of the QR's own light band. */
442
+ cornerTexturePixels: number
443
+ }
444
+ shape: {
445
+ /** Canvas-pixel bounds of the drawn modules. */
446
+ bounds: BoundingBox
447
+ /** Pixels the drawn modules cover: texture, rim, or light margin. */
448
+ area: number
449
+ /** Modules the cut draws. */
450
+ modules: number
451
+ /** Drawn modules forced dark as the rim. */
452
+ rimModules: number
453
+ /** Drawn modules painted light as the region edge or tight-QR quiet margin. */
454
+ marginModules: number
455
+ /** Drawn modules carrying the texture. */
456
+ textureModules: number
457
+ }
458
+ artifacts: {
459
+ poster: string
460
+ posterSha256: string
461
+ regionMask: string
462
+ qr: string
463
+ qrSha256: string
464
+ patternCutPng: string
465
+ patternCutPngSha256: string
466
+ patternCutSvg: string
467
+ patternCutSvgSha256: string
468
+ }
469
+ verification: VerificationResult
470
+ phoneScan: 'untested'
471
+ warnings: string[]
472
+ }
473
+
474
+ export interface AssembleResult {
475
+ report: AssembleReport
476
+ outputDir: string
477
+ }