@arraypress/covers 1.0.0 → 1.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 (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +296 -11
  3. package/package.json +33 -5
  4. package/recipes/brand.json +1143 -0
  5. package/schema/recipe.schema.json +7300 -770
  6. package/src/animate/index.d.ts +19 -0
  7. package/src/animate/index.js +116 -0
  8. package/src/builtins.js +4 -0
  9. package/src/effects/ascii.d.ts +7 -0
  10. package/src/effects/ascii.js +93 -0
  11. package/src/effects/buffer.d.ts +60 -0
  12. package/src/effects/buffer.js +472 -0
  13. package/src/effects/detail.d.ts +12 -0
  14. package/src/effects/detail.js +124 -0
  15. package/src/effects/digital.d.ts +14 -0
  16. package/src/effects/digital.js +216 -0
  17. package/src/effects/distort.d.ts +20 -0
  18. package/src/effects/distort.js +314 -0
  19. package/src/effects/dither.d.ts +8 -0
  20. package/src/effects/dither.js +147 -0
  21. package/src/effects/drop-shadow.d.ts +4 -0
  22. package/src/effects/drop-shadow.js +45 -0
  23. package/src/effects/extrude.d.ts +5 -0
  24. package/src/effects/extrude.js +55 -0
  25. package/src/effects/film.d.ts +11 -0
  26. package/src/effects/film.js +204 -0
  27. package/src/effects/glow.d.ts +8 -0
  28. package/src/effects/glow.js +56 -0
  29. package/src/effects/grade.d.ts +30 -0
  30. package/src/effects/grade.js +274 -0
  31. package/src/effects/halftone.d.ts +8 -0
  32. package/src/effects/halftone.js +95 -0
  33. package/src/effects/index.d.ts +125 -0
  34. package/src/effects/index.js +277 -0
  35. package/src/effects/ink.d.ts +12 -0
  36. package/src/effects/ink.js +70 -0
  37. package/src/effects/lens.d.ts +14 -0
  38. package/src/effects/lens.js +277 -0
  39. package/src/effects/light.d.ts +10 -0
  40. package/src/effects/light.js +59 -0
  41. package/src/effects/looks.d.ts +9 -0
  42. package/src/effects/looks.js +74 -0
  43. package/src/effects/mask.d.ts +52 -0
  44. package/src/effects/mask.js +314 -0
  45. package/src/effects/motion.d.ts +6 -0
  46. package/src/effects/motion.js +60 -0
  47. package/src/effects/newsprint.d.ts +6 -0
  48. package/src/effects/newsprint.js +89 -0
  49. package/src/effects/optics.d.ts +10 -0
  50. package/src/effects/optics.js +213 -0
  51. package/src/effects/options.d.ts +6 -0
  52. package/src/effects/options.js +106 -0
  53. package/src/effects/paint-presets.d.ts +10 -0
  54. package/src/effects/paint-presets.js +47 -0
  55. package/src/effects/paint.d.ts +49 -0
  56. package/src/effects/paint.js +66 -0
  57. package/src/effects/pixels.d.ts +24 -0
  58. package/src/effects/pixels.js +138 -0
  59. package/src/effects/polar.d.ts +10 -0
  60. package/src/effects/polar.js +265 -0
  61. package/src/effects/region.d.ts +8 -0
  62. package/src/effects/region.js +136 -0
  63. package/src/effects/round-border.d.ts +5 -0
  64. package/src/effects/round-border.js +62 -0
  65. package/src/effects/screen.d.ts +14 -0
  66. package/src/effects/screen.js +212 -0
  67. package/src/effects/sort.d.ts +8 -0
  68. package/src/effects/sort.js +125 -0
  69. package/src/effects/symmetry.d.ts +6 -0
  70. package/src/effects/symmetry.js +74 -0
  71. package/src/effects/tile.d.ts +7 -0
  72. package/src/effects/tile.js +55 -0
  73. package/src/effects/tone.d.ts +16 -0
  74. package/src/effects/tone.js +117 -0
  75. package/src/effects/transform.d.ts +35 -0
  76. package/src/effects/transform.js +127 -0
  77. package/src/effects/util.d.ts +16 -0
  78. package/src/effects/util.js +85 -0
  79. package/src/extract.js +4 -3
  80. package/src/fonts.d.ts +55 -0
  81. package/src/fonts.js +246 -53
  82. package/src/formats/cut.d.ts +10 -0
  83. package/src/formats/cut.js +77 -0
  84. package/src/formats/index.d.ts +6 -0
  85. package/src/formats/index.js +9 -0
  86. package/src/formats/presets.d.ts +41 -0
  87. package/src/formats/presets.js +89 -0
  88. package/src/formats/print.d.ts +25 -0
  89. package/src/formats/print.js +117 -0
  90. package/src/formats/zones.d.ts +10 -0
  91. package/src/formats/zones.js +64 -0
  92. package/src/geometry.js +1 -1
  93. package/src/icons.js +43 -9
  94. package/src/index.d.ts +205 -19
  95. package/src/index.js +12 -4
  96. package/src/inter-data.js +27 -0
  97. package/src/json-recipe.js +181 -23
  98. package/src/kit/index.d.ts +20 -0
  99. package/src/kit/index.js +58 -0
  100. package/src/layer-options.js +23 -4
  101. package/src/layers/barcode.d.ts +19 -0
  102. package/src/layers/barcode.js +63 -0
  103. package/src/layers/code128.d.ts +4 -0
  104. package/src/layers/code128.js +28 -0
  105. package/src/layers/expr.d.ts +6 -0
  106. package/src/layers/expr.js +121 -0
  107. package/src/layers/geometry.d.ts +6 -0
  108. package/src/layers/geometry.js +238 -0
  109. package/src/layers/index.d.ts +18 -0
  110. package/src/layers/index.js +15 -0
  111. package/src/layers/perforation.d.ts +14 -0
  112. package/src/layers/perforation.js +47 -0
  113. package/src/layers/repeat.d.ts +29 -0
  114. package/src/layers/repeat.js +90 -0
  115. package/src/layers/shape.d.ts +46 -0
  116. package/src/layers/shape.js +115 -0
  117. package/src/layers/soft.d.ts +5 -0
  118. package/src/layers/soft.js +43 -0
  119. package/src/layers/svg.d.ts +23 -0
  120. package/src/layers/svg.js +109 -0
  121. package/src/layers/table.d.ts +25 -0
  122. package/src/layers/table.js +80 -0
  123. package/src/layers/tear.d.ts +21 -0
  124. package/src/layers/tear.js +74 -0
  125. package/src/layers/texture.d.ts +17 -0
  126. package/src/layers/texture.js +106 -0
  127. package/src/layers/util.d.ts +8 -0
  128. package/src/layers/util.js +48 -0
  129. package/src/layers.js +125 -41
  130. package/src/lint/image-size.d.ts +4 -0
  131. package/src/lint/image-size.js +34 -0
  132. package/src/lint/index.d.ts +16 -0
  133. package/src/lint/index.js +127 -0
  134. package/src/lint/rules.d.ts +5 -0
  135. package/src/lint/rules.js +19 -0
  136. package/src/node.d.ts +2 -0
  137. package/src/node.js +20 -0
  138. package/src/platform/bytes.d.ts +22 -0
  139. package/src/platform/bytes.js +103 -0
  140. package/src/platform/compress.d.ts +12 -0
  141. package/src/platform/compress.js +81 -0
  142. package/src/platform/harfbuzz.d.ts +9 -0
  143. package/src/platform/harfbuzz.js +80 -0
  144. package/src/platform/index.d.ts +8 -0
  145. package/src/platform/index.js +13 -0
  146. package/src/platform/load.d.ts +6 -0
  147. package/src/platform/load.js +72 -0
  148. package/src/platform/no-harfbuzz.d.ts +2 -0
  149. package/src/platform/no-harfbuzz.js +11 -0
  150. package/src/platform/node.d.ts +26 -0
  151. package/src/platform/node.js +86 -0
  152. package/src/platform/settings.d.ts +28 -0
  153. package/src/platform/settings.js +51 -0
  154. package/src/png.js +77 -28
  155. package/src/print.js +8 -7
  156. package/src/raster/index.d.ts +24 -0
  157. package/src/raster/index.js +171 -0
  158. package/src/recipes.js +4 -3
  159. package/src/render/backdrop.d.ts +6 -0
  160. package/src/render/backdrop.js +55 -0
  161. package/src/render/content.d.ts +5 -0
  162. package/src/render/content.js +58 -0
  163. package/src/render/context.d.ts +82 -0
  164. package/src/render/grain.d.ts +7 -0
  165. package/src/render/grain.js +113 -0
  166. package/src/render/group.d.ts +23 -0
  167. package/src/render/group.js +67 -0
  168. package/src/render/key.d.ts +5 -0
  169. package/src/render/key.js +91 -0
  170. package/src/render/report.d.ts +5 -0
  171. package/src/render/report.js +19 -0
  172. package/src/render/tiles.d.ts +6 -0
  173. package/src/render/tiles.js +77 -0
  174. package/src/render/use.d.ts +27 -0
  175. package/src/render/use.js +73 -0
  176. package/src/render/wrap.d.ts +14 -0
  177. package/src/render/wrap.js +199 -0
  178. package/src/render.js +303 -212
  179. package/src/text.d.ts +29 -0
  180. package/src/text.js +22 -15
  181. package/src/type/auto-color.d.ts +9 -0
  182. package/src/type/auto-color.js +44 -0
  183. package/src/type/fallback.d.ts +11 -0
  184. package/src/type/fallback.js +92 -0
  185. package/src/type/glyphs.d.ts +24 -0
  186. package/src/type/glyphs.js +87 -0
  187. package/src/type/index.d.ts +5 -0
  188. package/src/type/index.js +10 -0
  189. package/src/type/kern.d.ts +4 -0
  190. package/src/type/kern.js +144 -0
  191. package/src/type/layout-ref.d.ts +10 -0
  192. package/src/type/layout-ref.js +74 -0
  193. package/src/type/line.d.ts +11 -0
  194. package/src/type/line.js +105 -0
  195. package/src/type/live.d.ts +6 -0
  196. package/src/type/live.js +39 -0
  197. package/src/type/name.d.ts +4 -0
  198. package/src/type/name.js +32 -0
  199. package/src/type/path.d.ts +18 -0
  200. package/src/type/path.js +89 -0
  201. package/src/type/pixel.d.ts +9 -0
  202. package/src/type/pixel.js +115 -0
  203. package/src/type/sfnt.d.ts +20 -0
  204. package/src/type/sfnt.js +258 -0
  205. package/src/type/spans.d.ts +33 -0
  206. package/src/type/spans.js +138 -0
  207. package/src/type/symbols.d.ts +7 -0
  208. package/src/type/symbols.js +89 -0
  209. package/src/type/typeset.d.ts +10 -0
  210. package/src/type/typeset.js +352 -0
  211. package/src/type/woff2.d.ts +6 -0
  212. package/src/type/woff2.js +301 -0
  213. package/src/version.js +4 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Raster grain for `renderImage`: seeded pixel noise blended in JS (librsvg's
3
+ * feTurbulence is ~20× slower at cover sizes).
4
+ *
5
+ * @module render/grain
6
+ */
7
+
8
+ import { VARY_FLOOR, VARY_PEAK } from '../layers.js';
9
+
10
+ /**
11
+ * Seeded film-grain values, one per pixel (three with `monochrome: false`),
12
+ * at `size` px per grain (upscaled smoothly). Each value is the mean of two
13
+ * uniform draws, a triangular distribution: fewer harsh extremes than flat
14
+ * noise, closer to film.
15
+ *
16
+ * @param {any} sharp - The sharp module.
17
+ * @param {number} width @param {number} height
18
+ * @param {import('../index.js').GrainLayer} layer
19
+ * @param {import('../rng.js').Rng} rng - The layer's stream.
20
+ * @returns {Promise<{ data: Buffer, channels: 1 | 3 }>} Full-size noise.
21
+ */
22
+ export async function noise(sharp, width, height, layer, rng) {
23
+ const size = Math.max(1, layer.size ?? 1);
24
+ const channels = layer.monochrome === false ? 3 : 1;
25
+ const nw = Math.ceil(width / size);
26
+ const nh = Math.ceil(height / size);
27
+ const data = Buffer.alloc(nw * nh * channels);
28
+ // xorshift32 seeded from the layer's stream: one 32-bit draw gives two
29
+ // values, so 1.4M pixels cost a few milliseconds.
30
+ let x = (rng.next() * 4294967296) >>> 0 || 1;
31
+ for (let i = 0; i < data.length; i++) {
32
+ x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
33
+ data[i] = ((x & 255) + ((x >>> 8) & 255)) >>> 1;
34
+ }
35
+ if (nw === width && nh === height) return { data, channels };
36
+ const out = await sharp(data, { raw: { width: nw, height: nh, channels } })
37
+ .resize(width, height, { kernel: 'cubic', fit: 'fill' })
38
+ .raw()
39
+ .toBuffer();
40
+ return { data: out, channels };
41
+ }
42
+
43
+ /** Separable blend modes (W3C Compositing Level 1), channel values 0–1. */
44
+ const BLENDS = /** @type {Record<string, (b: number, s: number) => number>} */ ({
45
+ normal: (_b, s) => s,
46
+ multiply: (b, s) => b * s,
47
+ screen: (b, s) => b + s - b * s,
48
+ overlay: (b, s) => (b <= 0.5 ? 2 * b * s : 1 - 2 * (1 - b) * (1 - s)),
49
+ 'soft-light': (b, s) => {
50
+ if (s <= 0.5) return b - (1 - 2 * s) * b * (1 - b);
51
+ const d = b <= 0.25 ? ((16 * b - 12) * b + 4) * b : Math.sqrt(b);
52
+ return b + (2 * s - 1) * (d - b);
53
+ },
54
+ });
55
+
56
+ /**
57
+ * A 256×256 lookup (backdrop × noise → result) for one blend and strength:
58
+ * cheaper than the blend maths per pixel.
59
+ *
60
+ * @param {(b: number, s: number) => number} blend
61
+ * @param {number} amount
62
+ * @returns {Uint8Array}
63
+ */
64
+ function grainTable(blend, amount) {
65
+ const table = new Uint8Array(65536);
66
+ for (let b = 0; b < 256; b++) {
67
+ for (let s = 0; s < 256; s++) {
68
+ const cb = b / 255;
69
+ table[(b << 8) | s] = Math.round((cb + amount * (blend(cb, s / 255) - cb)) * 255);
70
+ }
71
+ }
72
+ return table;
73
+ }
74
+
75
+ /**
76
+ * Blend grain into RGBA pixels in place, at the layer's `amount`.
77
+ * Done in JS rather than with sharp's `composite` blend modes, whose
78
+ * overlay with a partly transparent layer flattens the noise to almost
79
+ * nothing (measured σ 0.5 against the expected 5.9 on mid grey).
80
+ *
81
+ * @param {Buffer} pixels - RGBA pixels (modified).
82
+ * @param {{ data: Buffer, channels: 1 | 3, width: number, height: number }} grain - Noise from {@link noise}, and the canvas size.
83
+ * @param {import('../index.js').GrainLayer} layer - The grain layer.
84
+ * @returns {void}
85
+ */
86
+ export function applyGrain(pixels, grain, layer) {
87
+ const amount = Math.max(0, Math.min(1, (layer.amount ?? 0.08) * (layer.opacity ?? 1)));
88
+ const blend = BLENDS[layer.blend ?? 'overlay'];
89
+ if (!blend) throw new TypeError(`Unknown grain blend "${layer.blend}" (${Object.keys(BLENDS).join(', ')})`);
90
+ const table = grainTable(blend, amount);
91
+ const { data, channels } = grain;
92
+ if (layer.vary === 'edges') {
93
+ // Heavier toward the edges: per-pixel strength from the centre out, as in the SVG mask.
94
+ const { width, height } = grain;
95
+ const peak = Math.min(1, amount * VARY_PEAK);
96
+ const tables = Array.from({ length: 17 }, (_, k) => grainTable(blend, peak * (VARY_FLOOR + (1 - VARY_FLOOR) * (k / 16))));
97
+ const half = Math.hypot(width, height) / 2;
98
+ for (let y = 0, i = 0, j = 0; y < height; y++) {
99
+ for (let x = 0; x < width; x++, i += 4, j += channels) {
100
+ const t = tables[Math.min(16, Math.round((Math.hypot(x - width / 2, y - height / 2) / half) * 16))];
101
+ pixels[i] = t[(pixels[i] << 8) | data[j]];
102
+ pixels[i + 1] = t[(pixels[i + 1] << 8) | data[j + (channels === 3 ? 1 : 0)]];
103
+ pixels[i + 2] = t[(pixels[i + 2] << 8) | data[j + (channels === 3 ? 2 : 0)]];
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ for (let i = 0, j = 0; i < pixels.length; i += 4, j += channels) {
109
+ pixels[i] = table[(pixels[i] << 8) | data[j]];
110
+ pixels[i + 1] = table[(pixels[i + 1] << 8) | data[j + (channels === 3 ? 1 : 0)]];
111
+ pixels[i + 2] = table[(pixels[i + 2] << 8) | data[j + (channels === 3 ? 2 : 0)]];
112
+ }
113
+ }
@@ -0,0 +1,23 @@
1
+ // The `group` layer.
2
+ import type { Length, Layer, BaseLayer, Anchor } from '../index.js';
3
+ import type { RenderContext, Markup } from './context.js';
4
+
5
+ /** `group`: a stack drawn as one, on its own design grid. */
6
+ export interface GroupLayer extends BaseLayer {
7
+ type: 'group';
8
+ layers: Layer[];
9
+ /** The children's coordinate grid, e.g. `[0, 0, 400, 500]`. */
10
+ viewBox?: [number, number, number, number];
11
+ x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
12
+ /** Default `'contain'`. */
13
+ fit?: 'contain' | 'cover' | 'stretch' | 'width' | 'height';
14
+ /** Where a fitted grid sits. Default `'center'`. */
15
+ align?: Anchor;
16
+ clipToBox?: boolean;
17
+ }
18
+
19
+
20
+ /** Draw a group: its stack on its grid, fitted into its box. */
21
+ export function renderGroup(layer: GroupLayer, ctx: RenderContext, decorate?: (m: Markup, gctx: RenderContext) => Promise<Markup>): Promise<Markup>;
22
+ /** An anchor as fractions `[x, y]` of the free space. */
23
+ export function alignOf(a?: string): [number, number];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The `group` layer: a nested stack on its own design grid.
3
+ *
4
+ * @module render/group
5
+ */
6
+
7
+ import { attrs, esc, num } from '../svg.js';
8
+ import { pos } from '../layers/util.js';
9
+ import { scaledReport } from './report.js';
10
+ import { IDENTITY, compose } from '../effects/transform.js';
11
+
12
+ /**
13
+ * `group` — a stack of layers drawn as one: in a box (`x`, `y`, `width`,
14
+ * `height`, px), on its own design grid (`viewBox: [x, y, w, h]`, so a
15
+ * flyer drawn on a 400×500 grid keeps its numbers at any output size), and
16
+ * fitted by `fit` (`'contain'`, `'cover'`, `'stretch'`, `'width'`, `'height'`).
17
+ * The group's own `transform`, `effects` and masks apply to the whole
18
+ * (effects and masks in the grid's units, through `decorate`).
19
+ *
20
+ * @param {any} layer
21
+ * @param {any} ctx
22
+ * @param {(m: { defs: string, body: string }, gctx: any) => Promise<{ defs: string, body: string }>} [decorate] - Wraps the children, in the grid's units.
23
+ * @returns {Promise<{ defs: string, body: string }>}
24
+ */
25
+ export async function renderGroup(layer, ctx, decorate) {
26
+ const bx = pos(layer.x ?? 0, 'x', ctx.canvas);
27
+ const by = pos(layer.y ?? 0, 'y', ctx.canvas);
28
+ const bw = layer.width != null ? pos(layer.width, 'x', ctx.canvas) : ctx.width - bx;
29
+ const bh = layer.height != null ? pos(layer.height, 'y', ctx.canvas) : ctx.height - by;
30
+ const vb = layer.viewBox ? (Array.isArray(layer.viewBox) ? layer.viewBox : String(layer.viewBox).split(/[\s,]+/).map(Number)) : [0, 0, bw, bh];
31
+ const [vx, vy, vw, vh] = vb;
32
+ const fit = layer.fit ?? 'contain';
33
+ let sx = bw / vw;
34
+ let sy = bh / vh;
35
+ if (fit === 'contain') sx = sy = Math.min(sx, sy);
36
+ else if (fit === 'cover') sx = sy = Math.max(sx, sy);
37
+ else if (fit === 'width') sy = sx;
38
+ else if (fit === 'height') sx = sy;
39
+ const [ax, ay] = alignOf(layer.align);
40
+ const tx = bx + (bw - vw * sx) * ax;
41
+ const ty = by + (bh - vh * sy) * ay;
42
+ const k = Math.min(sx, sy);
43
+ /** @type {import('../effects/transform.js').Affine} Grid units to the box. */
44
+ const grid = [sx, 0, 0, sy, tx - vx * sx, ty - vy * sy];
45
+ const parts = await ctx.stack(layer.layers ?? [], { tag: 'g', width: vw, height: vh, scale: k, res: Math.max(sx, sy), matrix: grid });
46
+ let defs = parts.map((/** @type {any} */ p) => p.defs).join('');
47
+ let body = parts.map((/** @type {any} */ p) => p.body).join('');
48
+ if (decorate) {
49
+ const gctx = { ...ctx, width: vw, height: vh, canvas: { width: vw, height: vh }, inset: undefined, scale: (ctx.scale ?? 1) * k, res: (ctx.res ?? 1) * Math.max(sx, sy), report: scaledReport(ctx.report, k), matrix: compose(ctx.matrix ?? IDENTITY, grid) };
50
+ ({ defs, body } = await decorate({ defs, body }, gctx));
51
+ }
52
+ const identity = tx === 0 && ty === 0 && sx === 1 && sy === 1 && vx === 0 && vy === 0;
53
+ if (!identity) body = `<g transform="translate(${num(tx, 3)} ${num(ty, 3)}) scale(${num(sx, 6)} ${num(sy, 6)})${vx || vy ? ` translate(${num(-vx, 3)} ${num(-vy, 3)})` : ''}">${body}</g>`;
54
+ if (fit === 'cover' || layer.clipToBox) {
55
+ defs += `<clipPath id="${ctx.id}gc"><rect${attrs({ x: bx, y: by, width: bw, height: bh })}/></clipPath>`;
56
+ body = `<g clip-path="url(#${ctx.id}gc)">${body}</g>`;
57
+ }
58
+ const a = attrs({ opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined, style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${esc(layer.blend)}` : undefined });
59
+ return { defs, body: a ? `<g${a}>${body}</g>` : body };
60
+ }
61
+
62
+ /** @param {string | undefined} a @returns {[number, number]} */
63
+ export function alignOf(a = 'center') {
64
+ const x = a.includes('left') ? 0 : a.includes('right') ? 1 : 0.5;
65
+ const y = a.includes('top') ? 0 : a.includes('bottom') ? 1 : 0.5;
66
+ return [x, y];
67
+ }
@@ -0,0 +1,5 @@
1
+ // Content-hash cache keys.
2
+ import type { Cover, CoverOptions, RenderImageOptions } from '../index.js';
3
+
4
+ /** A stable cache key for a cover and its output options (40 hex characters). */
5
+ export function coverKey(input: CoverOptions | Cover, options?: RenderImageOptions): Promise<string>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Content-hash cache keys (Web Crypto: the same keys on every platform).
3
+ *
4
+ * @module render/key
5
+ */
6
+
7
+ import { loadBytes } from '../platform/index.js';
8
+ import { hexDigest, u8, isBytes } from '../platform/bytes.js';
9
+ import { fontBytes } from '../fonts.js';
10
+ import { cover } from '../render.js';
11
+ import { VERSION } from '../version.js';
12
+
13
+ /**
14
+ * A stable cache key for a cover and its output options: a SHA-256 of the
15
+ * resolved cover (font and image bytes hashed by content), the output
16
+ * format and quality, and this package's version.
17
+ *
18
+ * @param {import('../index.js').CoverOptions | import('../index.js').Cover} input - The cover.
19
+ * @param {import('../index.js').RenderImageOptions} [options]
20
+ * @returns {Promise<string>} 40 hex characters.
21
+ *
22
+ * @example
23
+ * await coverKey({ recipe: 'aurora', seed: 'a' }, { format: 'webp' }); // 'f3a9…'
24
+ */
25
+ export async function coverKey(input, options = {}) {
26
+ const c = cover(input);
27
+ const head = `covers@${VERSION}\n${options.format ?? 'webp'}:${options.quality ?? ''}:${options.effort ?? ''}:${options.lossless ?? c.output?.lossless ?? ''}\n`;
28
+ // Files a layer reads (an image's src, a mesh's photo, a waveform's audio) are hashed by content.
29
+ const file = async (/** @type {any} */ v) => (typeof v === 'string' && !v.startsWith('data:') ? digest(await loadBytes(v)) : isBytes(v) ? digest(v) : v);
30
+ const layers = await Promise.all(c.layers.map(async (l) => {
31
+ const x = /** @type {any} */ (l);
32
+ if (l.type === 'image') return { ...l, src: await file(x.src) };
33
+ if (l.type === 'mesh' && x.from) return { ...l, from: await file(x.from) };
34
+ if (l.type === 'waveform' && x.audio) return { ...l, audio: await file(x.audio) };
35
+ return l;
36
+ }));
37
+ const body = await hashBytes({
38
+ width: c.width,
39
+ height: c.height,
40
+ seed: c.seed,
41
+ palette: c.palette,
42
+ layers,
43
+ radius: c.radius,
44
+ fonts: await Promise.all(c.fonts.map(async (f) => ({ ...f, data: await digest(f.data ?? await fontBytes(f)) }))),
45
+ ...(c.print ? { print: c.print } : {}),
46
+ ...(c.side && c.side !== 'front' ? { side: c.side } : {}),
47
+ });
48
+ return (await hexDigest('SHA-256', head + stable(body))).slice(0, 40);
49
+ }
50
+
51
+ /** @param {Uint8Array | ArrayBuffer} data @returns {Promise<string>} SHA-1 hex. */
52
+ function digest(data) {
53
+ return hexDigest('SHA-1', u8(data));
54
+ }
55
+
56
+ /**
57
+ * Replace every byte array in a value with its SHA-1 (what {@link stable}
58
+ * writes for bytes), so the hashing itself can stay synchronous.
59
+ *
60
+ * @param {unknown} v
61
+ * @returns {Promise<unknown>}
62
+ */
63
+ async function hashBytes(v) {
64
+ if (isBytes(v)) return { [BYTES]: await digest(/** @type {any} */ (v)) };
65
+ if (Array.isArray(v)) return Promise.all(v.map(hashBytes));
66
+ if (v && typeof v === 'object' && !(v instanceof URL)) {
67
+ const out = /** @type {Record<string, unknown>} */ ({});
68
+ for (const [k, x] of Object.entries(v)) out[k] = await hashBytes(x);
69
+ return out;
70
+ }
71
+ return v;
72
+ }
73
+
74
+ const BYTES = Symbol('bytes');
75
+
76
+ /**
77
+ * JSON with sorted keys and bytes hashed, for hashing.
78
+ *
79
+ * @param {unknown} v
80
+ * @returns {string}
81
+ */
82
+ function stable(v) {
83
+ if (v && typeof v === 'object' && BYTES in v) return JSON.stringify(/** @type {any} */ (v)[BYTES]);
84
+ if (Array.isArray(v)) return `[${v.map(stable).join(',')}]`;
85
+ if (v instanceof URL) return JSON.stringify(v.href);
86
+ if (v && typeof v === 'object') {
87
+ return `{${Object.keys(v).sort().filter((k) => /** @type {any} */ (v)[k] !== undefined).map((k) => `${JSON.stringify(k)}:${stable(/** @type {any} */ (v)[k])}`).join(',')}}`;
88
+ }
89
+ if (typeof v === 'function') return JSON.stringify(String(v));
90
+ return JSON.stringify(v) ?? 'null';
91
+ }
@@ -0,0 +1,5 @@
1
+ // Scaling what a render reports.
2
+ import type { DrawnText, DrawnImage } from './context.js';
3
+
4
+ /** A report hook for a stack drawn `k` times larger (text sizes become canvas px). */
5
+ export function scaledReport(report: ((e: DrawnText | DrawnImage) => void) | undefined, k: number): ((e: DrawnText | DrawnImage) => void) | undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * What a render reports for `lint`, scaled to canvas px.
3
+ *
4
+ * @module render/report
5
+ */
6
+
7
+ /**
8
+ * A report hook for a stack drawn `k` times larger: text sizes become
9
+ * canvas px (the box is dropped, being in the stack's own units). Other
10
+ * entries carry their own scale and pass through.
11
+ *
12
+ * @param {((e: any) => void) | undefined} report
13
+ * @param {number} k
14
+ * @returns {((e: any) => void) | undefined}
15
+ */
16
+ export function scaledReport(report, k) {
17
+ if (!report || k === 1) return report;
18
+ return (e) => report(e.kind === 'text' ? { ...e, size: e.size * k, capHeight: e.capHeight * k, box: null } : e);
19
+ }
@@ -0,0 +1,6 @@
1
+ // Large SVGs with filters, rasterised in tiles.
2
+
3
+ /** An SVG document's RGBA pixels at its own size (in tiles when large and filtered). */
4
+ export function svgPixels(sharp: unknown, svg: string, width: number, height: number, known?: number): Promise<Uint8Array>;
5
+ /** How far any filter in a document reaches, output px, estimated from its markup. */
6
+ export function reach(svg: string): number;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Rasterising a large SVG with filters in tiles.
3
+ *
4
+ * librsvg (through libvips) slows down sharply past a few megapixels when
5
+ * a document has filters: a 3000² displacement map takes ~28 s in one
6
+ * render and ~5 s as four 1500² tiles. So a large document with filters is
7
+ * drawn as tiles, each with a margin round it (librsvg clips a filter's
8
+ * input to the viewport, so a blur or displacement at a tile's edge needs
9
+ * the pixels beyond it: the margin is the furthest any filter reaches),
10
+ * cropped and assembled. Tiles render in parallel. A document whose
11
+ * filters reach further than a tile renders in one piece.
12
+ *
13
+ * @module render/tiles
14
+ */
15
+
16
+ import { filterReach } from '../effects/region.js';
17
+
18
+ /** Documents below this many pixels render in one piece. */
19
+ const MIN_TILED = 2.4e6;
20
+
21
+ /** Tile side, px (before margins). */
22
+ const TILE = 1024;
23
+
24
+ /**
25
+ * An SVG document's RGBA pixels at its own size.
26
+ *
27
+ * @param {any} sharp
28
+ * @param {string} svg - A document whose root is `width`×`height` with `viewBox="0 0 width height"`.
29
+ * @param {number} width
30
+ * @param {number} height
31
+ * @param {number} [known=0] - How far the filters the layers drew reach, output px (they report it at their own resolution).
32
+ * @returns {Promise<Uint8Array>}
33
+ */
34
+ export async function svgPixels(sharp, svg, width, height, known = 0) {
35
+ const margin = width * height < MIN_TILED || !/<filter\b/.test(svg) ? Infinity : Math.max(known + 16, reach(svg));
36
+ // Filters reaching further than a tile (a heavy blur) render in one piece.
37
+ if (margin > TILE) return sharp(Buffer.from(svg)).ensureAlpha().raw().toBuffer();
38
+ const open = /^<svg\b[^>]*>/.exec(svg);
39
+ if (!open) return sharp(Buffer.from(svg)).ensureAlpha().raw().toBuffer();
40
+ const rest = svg.slice(open[0].length);
41
+ const root = open[0].replace(/\s(width|height|viewBox)="[^"]*"/g, '').replace(/>$/, '');
42
+ const out = Buffer.alloc(width * height * 4);
43
+ /** @type {(() => Promise<void>)[]} */
44
+ const jobs = [];
45
+ for (let y = 0; y < height; y += TILE) {
46
+ for (let x = 0; x < width; x += TILE) {
47
+ const tw = Math.min(TILE, width - x);
48
+ const th = Math.min(TILE, height - y);
49
+ jobs.push(async () => {
50
+ const vw = tw + 2 * margin;
51
+ const vh = th + 2 * margin;
52
+ const doc = `${root} width="${vw}" height="${vh}" viewBox="${x - margin} ${y - margin} ${vw} ${vh}">${rest}`;
53
+ const px = await sharp(Buffer.from(doc)).ensureAlpha().extract({ left: margin, top: margin, width: tw, height: th }).raw().toBuffer();
54
+ for (let r = 0; r < th; r++) px.copy(out, ((y + r) * width + x) * 4, r * tw * 4, (r + 1) * tw * 4);
55
+ });
56
+ }
57
+ }
58
+ // A few at a time: each holds its filter surfaces.
59
+ const lanes = 4;
60
+ await Promise.all(Array.from({ length: lanes }, async (_, i) => { for (let j = i; j < jobs.length; j += lanes) await jobs[j](); }));
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * How far, in output px, any filter in a document reaches, estimated from
66
+ * the markup: the primitives' reach times the largest scale it's drawn at.
67
+ * (Effects report their own reach at their own resolution; this covers
68
+ * filters written by other layers.)
69
+ *
70
+ * @param {string} svg
71
+ * @returns {number}
72
+ */
73
+ export function reach(svg) {
74
+ let scale = 1;
75
+ for (const m of svg.matchAll(/scale\(([^)]*)\)/g)) for (const v of m[1].trim().split(/[\s,]+/)) scale = Math.max(scale, Math.abs(Number(v)) || 0);
76
+ return Math.ceil(filterReach(svg) * scale) + 16;
77
+ }
@@ -0,0 +1,27 @@
1
+ // Reusable stacks: `defs` and `use`.
2
+ import type { Layer, BaseLayer, Length } from '../index.js';
3
+ import type { LayerTransform } from '../effects/transform.js';
4
+ import type { RenderContext, Markup } from './context.js';
5
+
6
+ /** `defs`: named layers (each with an `id`) that `use` draws; nothing is drawn here. */
7
+ export interface DefsLayer extends BaseLayer { type: 'defs'; layers: Layer[] }
8
+
9
+ /** `use`: a named layer drawn again, placed. */
10
+ export interface UseLayer extends BaseLayer {
11
+ type: 'use';
12
+ /** The `id` of a layer in an earlier `defs`. */
13
+ ref: string;
14
+ x?: Length | `${number}%`; y?: Length | `${number}%`;
15
+ /** Degrees, about the copy's origin. */
16
+ rotate?: number;
17
+ scale?: number | [number, number];
18
+ /** A transform instead of x, y, rotate and scale. */
19
+ place?: LayerTransform | string;
20
+ /** Options for this copy only (drawn on its own). */
21
+ with?: Record<string, unknown>;
22
+ }
23
+
24
+ /** Register a defs layer's named layers. */
25
+ export function registerDefs(layer: DefsLayer, ctx: RenderContext): Markup;
26
+ /** Draw a named layer, placed. */
27
+ export function renderUse(layer: UseLayer, ctx: RenderContext): Promise<Markup>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Reusable stacks: a `defs` layer names layers (by `id`) without drawing
3
+ * them; a `use` layer draws one, placed (`x`, `y`, `scale`, `rotate`, or a
4
+ * `transform`). A plain `use` draws the same markup every time (it's drawn
5
+ * once and referenced); `with` overrides options for that copy, which is
6
+ * drawn on its own.
7
+ *
8
+ * @module render/use
9
+ */
10
+
11
+ import { attrs, num } from '../svg.js';
12
+ import { pos, wrap } from '../layers/util.js';
13
+ import { transformAttr } from '../effects/transform.js';
14
+
15
+ /**
16
+ * `defs`: register named layers for `use`.
17
+ *
18
+ * @param {any} layer
19
+ * @param {any} ctx
20
+ * @returns {{ defs: string, body: string }}
21
+ */
22
+ export function registerDefs(layer, ctx) {
23
+ for (const l of layer.layers ?? []) {
24
+ if (!l?.id) throw new TypeError('Every layer in a defs layer needs an id (use refers to it)');
25
+ ctx.defs.set(l.id, l);
26
+ }
27
+ return { defs: '', body: '' };
28
+ }
29
+
30
+ /**
31
+ * `use`: draw a named layer, placed.
32
+ *
33
+ * @param {any} layer
34
+ * @param {any} ctx
35
+ * @returns {Promise<{ defs: string, body: string }>}
36
+ */
37
+ export async function renderUse(layer, ctx) {
38
+ const def = ctx.defs?.get(layer.ref);
39
+ if (!def) throw new TypeError(`use: no layer "${layer.ref}" in a defs layer drawn before it`);
40
+ const place = placement(layer, ctx);
41
+ if (layer.with) {
42
+ const parts = await ctx.stack([{ ...def, ...layer.with }], { tag: 'u' });
43
+ const body = parts.map((/** @type {any} */ p) => p.body).join('');
44
+ return { defs: parts.map((/** @type {any} */ p) => p.defs).join(''), body: wrap(layer, place ? `<g transform="${place}">${body}</g>` : body) };
45
+ }
46
+ // Drawn once per canvas size, then referenced.
47
+ const key = `${layer.ref}@${num(ctx.width)}x${num(ctx.height)}`;
48
+ let drawn = ctx.defs.get(`#${key}`);
49
+ let defs = '';
50
+ if (!drawn) {
51
+ const parts = await ctx.stack([def], { tag: 'u' });
52
+ drawn = { id: `${ctx.id}u` };
53
+ ctx.defs.set(`#${key}`, drawn);
54
+ defs = parts.map((/** @type {any} */ p) => p.defs).join('') + `<g id="${drawn.id}">${parts.map((/** @type {any} */ p) => p.body).join('')}</g>`;
55
+ }
56
+ return { defs, body: wrap(layer, `<use${attrs({ href: `#${drawn.id}`, transform: place })}/>`) };
57
+ }
58
+
59
+ /**
60
+ * Where a copy goes: `transform` as written, else `x`, `y`, `rotate`
61
+ * (about the copy's origin) and `scale`.
62
+ *
63
+ * @param {any} l @param {any} ctx
64
+ * @returns {string | undefined}
65
+ */
66
+ function placement(l, ctx) {
67
+ if (l.place) return transformAttr(l.place, ctx.canvas);
68
+ const parts = [];
69
+ if (l.x != null || l.y != null) parts.push(`translate(${num(pos(l.x ?? 0, 'x', ctx.canvas), 3)} ${num(pos(l.y ?? 0, 'y', ctx.canvas), 3)})`);
70
+ if (l.rotate) parts.push(`rotate(${num(l.rotate, 3)})`);
71
+ if (l.scale != null && l.scale !== 1) parts.push(Array.isArray(l.scale) ? `scale(${num(l.scale[0], 4)} ${num(l.scale[1], 4)})` : `scale(${num(l.scale, 4)})`);
72
+ return parts.length ? parts.join(' ') : undefined;
73
+ }
@@ -0,0 +1,14 @@
1
+ // The options every layer takes on top of its own.
2
+ import type { Layer, Cover } from '../index.js';
3
+ import type { RenderContext, Markup } from './context.js';
4
+
5
+ /** Options handled for every layer type. */
6
+ export const WRAP_KEYS: readonly ('effects' | 'mask' | 'knockout' | 'clip' | 'area')[];
7
+ /** Case keywords (a text layer's `transform` is its case when it's one of these). */
8
+ export const CASES: ReadonlySet<string>;
9
+ /** Whether a layer uses any of them (1.0 layers take the 1.0 path). */
10
+ export function wrapped(layer: Layer): boolean;
11
+ /** Draw a layer with its area, effects, transform and masks; blend and opacity outermost. */
12
+ export function renderWrapped(layer: Layer, ctx: RenderContext, scope: { c: Cover }, renderOne: (layer: Layer, ctx: RenderContext) => Promise<Markup>): Promise<Markup>;
13
+ /** A layer's effects, then its masks, round its markup (in `ctx`'s units: a group's grid inside a group). */
14
+ export function decorate(m: Markup, layer: Layer, ctx: RenderContext): Promise<Markup>;