@arraypress/covers 0.0.0-stage → 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 (245) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +29 -0
  3. package/README.md +846 -2
  4. package/fonts/OFL.txt +93 -0
  5. package/fonts/inter-latin-400.woff +0 -0
  6. package/fonts/inter-latin-500.woff +0 -0
  7. package/fonts/inter-latin-600.woff +0 -0
  8. package/fonts/inter-latin-700.woff +0 -0
  9. package/package.json +124 -4
  10. package/recipes/aurora.json +132 -0
  11. package/recipes/bauhaus.json +113 -0
  12. package/recipes/bigType.json +103 -0
  13. package/recipes/brand.json +1143 -0
  14. package/recipes/contour.json +109 -0
  15. package/recipes/cutout.json +191 -0
  16. package/recipes/diagram.json +92 -0
  17. package/recipes/dither.json +170 -0
  18. package/recipes/duotone.json +75 -0
  19. package/recipes/gridTitle.json +136 -0
  20. package/recipes/iconTile.json +214 -0
  21. package/recipes/issue.json +221 -0
  22. package/recipes/mark.json +170 -0
  23. package/recipes/photo.json +156 -0
  24. package/recipes/riso.json +387 -0
  25. package/recipes/stripes.json +206 -0
  26. package/recipes/waveform.json +154 -0
  27. package/schema/recipe.schema.json +8822 -0
  28. package/src/animate/index.d.ts +19 -0
  29. package/src/animate/index.js +116 -0
  30. package/src/astro.d.ts +31 -0
  31. package/src/astro.js +86 -0
  32. package/src/builtins.js +4 -0
  33. package/src/color.js +246 -0
  34. package/src/diagram.js +248 -0
  35. package/src/effects/ascii.d.ts +7 -0
  36. package/src/effects/ascii.js +93 -0
  37. package/src/effects/buffer.d.ts +60 -0
  38. package/src/effects/buffer.js +472 -0
  39. package/src/effects/detail.d.ts +12 -0
  40. package/src/effects/detail.js +124 -0
  41. package/src/effects/digital.d.ts +14 -0
  42. package/src/effects/digital.js +216 -0
  43. package/src/effects/distort.d.ts +20 -0
  44. package/src/effects/distort.js +314 -0
  45. package/src/effects/dither.d.ts +8 -0
  46. package/src/effects/dither.js +147 -0
  47. package/src/effects/drop-shadow.d.ts +4 -0
  48. package/src/effects/drop-shadow.js +45 -0
  49. package/src/effects/extrude.d.ts +5 -0
  50. package/src/effects/extrude.js +55 -0
  51. package/src/effects/film.d.ts +11 -0
  52. package/src/effects/film.js +204 -0
  53. package/src/effects/glow.d.ts +8 -0
  54. package/src/effects/glow.js +56 -0
  55. package/src/effects/grade.d.ts +30 -0
  56. package/src/effects/grade.js +274 -0
  57. package/src/effects/halftone.d.ts +8 -0
  58. package/src/effects/halftone.js +95 -0
  59. package/src/effects/index.d.ts +125 -0
  60. package/src/effects/index.js +277 -0
  61. package/src/effects/ink.d.ts +12 -0
  62. package/src/effects/ink.js +70 -0
  63. package/src/effects/lens.d.ts +14 -0
  64. package/src/effects/lens.js +277 -0
  65. package/src/effects/light.d.ts +10 -0
  66. package/src/effects/light.js +59 -0
  67. package/src/effects/looks.d.ts +9 -0
  68. package/src/effects/looks.js +74 -0
  69. package/src/effects/mask.d.ts +52 -0
  70. package/src/effects/mask.js +314 -0
  71. package/src/effects/motion.d.ts +6 -0
  72. package/src/effects/motion.js +60 -0
  73. package/src/effects/newsprint.d.ts +6 -0
  74. package/src/effects/newsprint.js +89 -0
  75. package/src/effects/optics.d.ts +10 -0
  76. package/src/effects/optics.js +213 -0
  77. package/src/effects/options.d.ts +6 -0
  78. package/src/effects/options.js +106 -0
  79. package/src/effects/paint-presets.d.ts +10 -0
  80. package/src/effects/paint-presets.js +47 -0
  81. package/src/effects/paint.d.ts +49 -0
  82. package/src/effects/paint.js +66 -0
  83. package/src/effects/pixels.d.ts +24 -0
  84. package/src/effects/pixels.js +138 -0
  85. package/src/effects/polar.d.ts +10 -0
  86. package/src/effects/polar.js +265 -0
  87. package/src/effects/region.d.ts +8 -0
  88. package/src/effects/region.js +136 -0
  89. package/src/effects/round-border.d.ts +5 -0
  90. package/src/effects/round-border.js +62 -0
  91. package/src/effects/screen.d.ts +14 -0
  92. package/src/effects/screen.js +212 -0
  93. package/src/effects/sort.d.ts +8 -0
  94. package/src/effects/sort.js +125 -0
  95. package/src/effects/symmetry.d.ts +6 -0
  96. package/src/effects/symmetry.js +74 -0
  97. package/src/effects/tile.d.ts +7 -0
  98. package/src/effects/tile.js +55 -0
  99. package/src/effects/tone.d.ts +16 -0
  100. package/src/effects/tone.js +117 -0
  101. package/src/effects/transform.d.ts +35 -0
  102. package/src/effects/transform.js +127 -0
  103. package/src/effects/util.d.ts +16 -0
  104. package/src/effects/util.js +85 -0
  105. package/src/extract.js +91 -0
  106. package/src/flatten.js +234 -0
  107. package/src/fonts.d.ts +55 -0
  108. package/src/fonts.js +393 -0
  109. package/src/formats/cut.d.ts +10 -0
  110. package/src/formats/cut.js +77 -0
  111. package/src/formats/index.d.ts +6 -0
  112. package/src/formats/index.js +9 -0
  113. package/src/formats/presets.d.ts +41 -0
  114. package/src/formats/presets.js +89 -0
  115. package/src/formats/print.d.ts +25 -0
  116. package/src/formats/print.js +117 -0
  117. package/src/formats/zones.d.ts +10 -0
  118. package/src/formats/zones.js +64 -0
  119. package/src/geometry.js +187 -0
  120. package/src/grid.js +41 -0
  121. package/src/handmade.js +253 -0
  122. package/src/icons.js +191 -0
  123. package/src/index.d.ts +1083 -0
  124. package/src/index.js +27 -0
  125. package/src/inter-data.js +27 -0
  126. package/src/json-recipe.js +616 -0
  127. package/src/kit/index.d.ts +20 -0
  128. package/src/kit/index.js +58 -0
  129. package/src/layer-options.js +56 -0
  130. package/src/layers/barcode.d.ts +19 -0
  131. package/src/layers/barcode.js +63 -0
  132. package/src/layers/code128.d.ts +4 -0
  133. package/src/layers/code128.js +28 -0
  134. package/src/layers/expr.d.ts +6 -0
  135. package/src/layers/expr.js +121 -0
  136. package/src/layers/geometry.d.ts +6 -0
  137. package/src/layers/geometry.js +238 -0
  138. package/src/layers/index.d.ts +18 -0
  139. package/src/layers/index.js +15 -0
  140. package/src/layers/perforation.d.ts +14 -0
  141. package/src/layers/perforation.js +47 -0
  142. package/src/layers/repeat.d.ts +29 -0
  143. package/src/layers/repeat.js +90 -0
  144. package/src/layers/shape.d.ts +46 -0
  145. package/src/layers/shape.js +115 -0
  146. package/src/layers/soft.d.ts +5 -0
  147. package/src/layers/soft.js +43 -0
  148. package/src/layers/svg.d.ts +23 -0
  149. package/src/layers/svg.js +109 -0
  150. package/src/layers/table.d.ts +25 -0
  151. package/src/layers/table.js +80 -0
  152. package/src/layers/tear.d.ts +21 -0
  153. package/src/layers/tear.js +74 -0
  154. package/src/layers/texture.d.ts +17 -0
  155. package/src/layers/texture.js +106 -0
  156. package/src/layers/util.d.ts +8 -0
  157. package/src/layers/util.js +48 -0
  158. package/src/layers.js +1056 -0
  159. package/src/lint/image-size.d.ts +4 -0
  160. package/src/lint/image-size.js +34 -0
  161. package/src/lint/index.d.ts +16 -0
  162. package/src/lint/index.js +127 -0
  163. package/src/lint/rules.d.ts +5 -0
  164. package/src/lint/rules.js +19 -0
  165. package/src/node.d.ts +2 -0
  166. package/src/node.js +20 -0
  167. package/src/palette.js +185 -0
  168. package/src/platform/bytes.d.ts +22 -0
  169. package/src/platform/bytes.js +103 -0
  170. package/src/platform/compress.d.ts +12 -0
  171. package/src/platform/compress.js +81 -0
  172. package/src/platform/harfbuzz.d.ts +9 -0
  173. package/src/platform/harfbuzz.js +80 -0
  174. package/src/platform/index.d.ts +8 -0
  175. package/src/platform/index.js +13 -0
  176. package/src/platform/load.d.ts +6 -0
  177. package/src/platform/load.js +72 -0
  178. package/src/platform/no-harfbuzz.d.ts +2 -0
  179. package/src/platform/no-harfbuzz.js +11 -0
  180. package/src/platform/node.d.ts +26 -0
  181. package/src/platform/node.js +86 -0
  182. package/src/platform/settings.d.ts +28 -0
  183. package/src/platform/settings.js +51 -0
  184. package/src/png.js +126 -0
  185. package/src/print.js +659 -0
  186. package/src/raster/index.d.ts +24 -0
  187. package/src/raster/index.js +171 -0
  188. package/src/recipes.js +79 -0
  189. package/src/render/backdrop.d.ts +6 -0
  190. package/src/render/backdrop.js +55 -0
  191. package/src/render/content.d.ts +5 -0
  192. package/src/render/content.js +58 -0
  193. package/src/render/context.d.ts +82 -0
  194. package/src/render/grain.d.ts +7 -0
  195. package/src/render/grain.js +113 -0
  196. package/src/render/group.d.ts +23 -0
  197. package/src/render/group.js +67 -0
  198. package/src/render/key.d.ts +5 -0
  199. package/src/render/key.js +91 -0
  200. package/src/render/report.d.ts +5 -0
  201. package/src/render/report.js +19 -0
  202. package/src/render/tiles.d.ts +6 -0
  203. package/src/render/tiles.js +77 -0
  204. package/src/render/use.d.ts +27 -0
  205. package/src/render/use.js +73 -0
  206. package/src/render/wrap.d.ts +14 -0
  207. package/src/render/wrap.js +199 -0
  208. package/src/render.js +553 -0
  209. package/src/rng.js +95 -0
  210. package/src/svg.js +69 -0
  211. package/src/text.d.ts +29 -0
  212. package/src/text.js +237 -0
  213. package/src/type/auto-color.d.ts +9 -0
  214. package/src/type/auto-color.js +44 -0
  215. package/src/type/fallback.d.ts +11 -0
  216. package/src/type/fallback.js +92 -0
  217. package/src/type/glyphs.d.ts +24 -0
  218. package/src/type/glyphs.js +87 -0
  219. package/src/type/index.d.ts +5 -0
  220. package/src/type/index.js +10 -0
  221. package/src/type/kern.d.ts +4 -0
  222. package/src/type/kern.js +144 -0
  223. package/src/type/layout-ref.d.ts +10 -0
  224. package/src/type/layout-ref.js +74 -0
  225. package/src/type/line.d.ts +11 -0
  226. package/src/type/line.js +105 -0
  227. package/src/type/live.d.ts +6 -0
  228. package/src/type/live.js +39 -0
  229. package/src/type/name.d.ts +4 -0
  230. package/src/type/name.js +32 -0
  231. package/src/type/path.d.ts +18 -0
  232. package/src/type/path.js +89 -0
  233. package/src/type/pixel.d.ts +9 -0
  234. package/src/type/pixel.js +115 -0
  235. package/src/type/sfnt.d.ts +20 -0
  236. package/src/type/sfnt.js +258 -0
  237. package/src/type/spans.d.ts +33 -0
  238. package/src/type/spans.js +138 -0
  239. package/src/type/symbols.d.ts +7 -0
  240. package/src/type/symbols.js +89 -0
  241. package/src/type/typeset.d.ts +10 -0
  242. package/src/type/typeset.js +352 -0
  243. package/src/type/woff2.d.ts +6 -0
  244. package/src/type/woff2.js +301 -0
  245. package/src/version.js +4 -0
@@ -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>;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * The options every layer takes on top of its own: `area`, `effects`,
3
+ * `transform`, `mask`, `knockout` and `clip`.
4
+ *
5
+ * @module render/wrap
6
+ */
7
+
8
+ import { attrs, esc, num } from '../svg.js';
9
+ import { applyEffects, transformAttr, rasterEffect, isRaster, RESOLVED, maskImage } from '../effects/index.js';
10
+ import { transformMatrix, stretchOf, compose, IDENTITY } from '../effects/transform.js';
11
+ import { renderGroup } from './group.js';
12
+ import { scaledReport } from './report.js';
13
+ import { contentBox } from './content.js';
14
+
15
+ /** Layer options handled here, for every layer type. */
16
+ export const WRAP_KEYS = ['effects', 'mask', 'knockout', 'clip', 'area'];
17
+
18
+ /** Text-case keywords (the text layer's `transform` is its case when it's one of these). */
19
+ export const CASES = new Set(['uppercase', 'lowercase', 'none']);
20
+
21
+ /**
22
+ * Whether a layer uses the common 1.1 options (so 1.0 layers take exactly
23
+ * the 1.0 path).
24
+ *
25
+ * @param {any} layer
26
+ * @returns {boolean}
27
+ */
28
+ export function wrapped(layer) {
29
+ if (WRAP_KEYS.some((k) => layer[k] != null)) return true;
30
+ return isGeometric(layer);
31
+ }
32
+
33
+ /** @param {any} layer @returns {boolean} Whether `transform` is geometry (not a text case). */
34
+ const isGeometric = (layer) => layer.transform != null && !(layer.type === 'text' && typeof layer.transform === 'string' && CASES.has(layer.transform));
35
+
36
+ /**
37
+ * A layer with `area`, `effects`, `transform`, `mask`, `knockout` or
38
+ * `clip`. Blend and opacity go on the outermost group, so a masked or
39
+ * filtered layer still blends with what's under it. On a `group` with a
40
+ * `viewBox`, effects and masks work in the group's own grid.
41
+ *
42
+ * @param {any} layer
43
+ * @param {any} ctx
44
+ * @param {{ c: import('../index.js').Cover }} scope
45
+ * @param {(layer: any, ctx: any) => Promise<{ defs: string, body: string }>} renderOne - Draws the layer itself.
46
+ * @returns {Promise<{ defs: string, body: string }>}
47
+ */
48
+ export async function renderWrapped(layer, ctx, scope, renderOne) {
49
+ const inner = { ...layer, opacity: undefined, blend: undefined, effects: undefined, mask: undefined, knockout: undefined, clip: undefined, area: undefined };
50
+ const geometric = isGeometric(layer);
51
+ if (geometric) inner.transform = undefined;
52
+ const grid = layer.type === 'group' && layer.viewBox != null;
53
+
54
+ const box = areaBox(layer.area, ctx, scope);
55
+ /** @type {any} */
56
+ let lctx = box ? { ...ctx, width: box.width, height: box.height, canvas: { width: box.width, height: box.height }, inset: undefined, matrix: compose(ctx.matrix ?? IDENTITY, [1, 0, 0, 1, box.x, box.y]) } : ctx;
57
+ // What's drawn inside a transform is drawn that much larger (images resample for it, lint measures it).
58
+ if (geometric) {
59
+ const t = transformMatrix(layer.transform, lctx.canvas);
60
+ const k = stretchOf(t);
61
+ lctx = { ...lctx, matrix: compose(lctx.matrix ?? IDENTITY, t) };
62
+ if (k.max !== 1 || k.mean !== 1) Object.assign(lctx, { scale: (lctx.scale ?? 1) * k.mean, res: (lctx.res ?? 1) * k.max, report: scaledReport(lctx.report, k.mean) });
63
+ }
64
+
65
+ let { defs, body } = grid
66
+ ? await renderGroup(inner, lctx, (/** @type {any} */ part, /** @type {any} */ gctx) => decorate(part, layer, gctx))
67
+ : await decorateEffects(await renderOne(inner, lctx), layer.effects, lctx);
68
+ if (geometric) {
69
+ const t = transformAttr(layer.transform, lctx.canvas);
70
+ if (t) body = `<g transform="${t}">${body}</g>`;
71
+ }
72
+ if (box) body = `<g transform="translate(${num(box.x)} ${num(box.y)})">${body}</g>`;
73
+ if (!grid) ({ defs, body } = await decorateMasks({ defs, body }, layer, ctx));
74
+ 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 });
75
+ if (a) body = `<g${a}>${body}</g>`;
76
+ return { defs, body };
77
+ }
78
+
79
+ /**
80
+ * `area`: the box a layer is drawn in (the trim, the safe area, or
81
+ * fractions of the canvas), or null for the whole canvas.
82
+ *
83
+ * @param {any} a
84
+ * @param {any} ctx
85
+ * @param {{ c: import('../index.js').Cover }} scope
86
+ * @returns {{ x: number, y: number, width: number, height: number } | null}
87
+ */
88
+ function areaBox(a, ctx, scope) {
89
+ if (!a || a === 'bleed') return null;
90
+ if (a === 'trim' || a === 'safe') {
91
+ const print = /** @type {any} */ (scope.c).print;
92
+ const inset = print ? print.bleed + (a === 'safe' ? print.safe : 0) : 0;
93
+ return { x: inset, y: inset, width: ctx.width - 2 * inset, height: ctx.height - 2 * inset };
94
+ }
95
+ return { x: (a.x ?? 0) * ctx.width, y: (a.y ?? 0) * ctx.height, width: (a.width ?? 1 - (a.x ?? 0)) * ctx.width, height: (a.height ?? 1 - (a.y ?? 0)) * ctx.height };
96
+ }
97
+
98
+ /**
99
+ * A layer's effects, then its masks, round its markup.
100
+ *
101
+ * @param {{ defs: string, body: string }} m
102
+ * @param {any} layer
103
+ * @param {any} ctx
104
+ * @returns {Promise<{ defs: string, body: string }>}
105
+ */
106
+ export async function decorate(m, layer, ctx) {
107
+ return decorateMasks(await decorateEffects(m, layer.effects, ctx), layer, ctx);
108
+ }
109
+
110
+ /**
111
+ * Effects, in order. Pixel effects are computed on pixels where something
112
+ * can draw them (sharp, a canvas) and copy effects drawn as copies; the
113
+ * rest are SVG filters. An effect's layer `mask` is drawn first (as
114
+ * markup, and as an image for a filter to mix through).
115
+ *
116
+ * @param {{ defs: string, body: string }} m
117
+ * @param {any} list
118
+ * @param {any} ctx
119
+ * @returns {Promise<{ defs: string, body: string }>}
120
+ */
121
+ async function decorateEffects(m, list, ctx) {
122
+ if (!list) return m;
123
+ const effects = await Promise.all((Array.isArray(list) ? list : [list]).map((e, i) => withMask(typeof e === 'string' ? { kind: e } : e, ctx, i)));
124
+ let { defs, body } = m;
125
+ const filters = async (/** @type {number} */ from, /** @type {number} */ to) => {
126
+ if (to <= from) return;
127
+ // In renderImage, filters work over what the layer paints, not the whole canvas.
128
+ const out = applyEffects(body, effects.slice(from, to), ctx, from, await contentBox({ defs, body }, ctx), defs);
129
+ defs += out.defs;
130
+ body = out.body;
131
+ };
132
+ let from = 0;
133
+ for (let i = 0; i < effects.length; i++) {
134
+ const fx = effects[i];
135
+ if (!isRaster(fx, ctx)) continue;
136
+ await filters(from, i);
137
+ from = i + 1;
138
+ const raster = await rasterEffect(fx, { defs, body }, ctx, i);
139
+ if (raster) ({ defs, body } = raster);
140
+ else await filters(i, i + 1);
141
+ }
142
+ await filters(from, effects.length);
143
+ return { defs, body };
144
+ }
145
+
146
+ /**
147
+ * An effect with a layer `mask`: the mask's layers drawn (kept on the
148
+ * mask), and for a filter effect drawn as an image too, where something
149
+ * can draw pixels. Other effects come back as they are.
150
+ *
151
+ * @param {any} fx
152
+ * @param {any} ctx
153
+ * @param {number} i
154
+ * @returns {Promise<any>}
155
+ */
156
+ async function withMask(fx, ctx, i) {
157
+ if (!fx?.mask?.layers || !ctx.stack) return fx;
158
+ const parts = await ctx.stack(fx.mask.layers, { tag: `fm${i}` });
159
+ const mask = { ...fx.mask, [RESOLVED]: { defs: parts.map((/** @type {any} */ p) => p.defs).join(''), body: parts.map((/** @type {any} */ p) => p.body).join('') } };
160
+ if (!isRaster(fx, ctx)) mask[RESOLVED].image = await maskImage(mask, ctx, fx.pad ?? 0.3, fx.kind === 'motion' ? fx.angle ?? 0 : 0);
161
+ return { ...fx, mask };
162
+ }
163
+
164
+ /**
165
+ * Masks: sub-stacks drawn as a luminance mask. `knockout` cuts its layers
166
+ * out of this one; `clip` shows this one only where they paint; `mask`
167
+ * uses their luminance as drawn (a white-to-black gradient fades).
168
+ *
169
+ * @param {{ defs: string, body: string }} m
170
+ * @param {any} layer
171
+ * @param {any} ctx
172
+ * @returns {Promise<{ defs: string, body: string }>}
173
+ */
174
+ async function decorateMasks(m, layer, ctx) {
175
+ let { defs, body } = m;
176
+ const masks = [['mask', layer.mask], ['knockout', layer.knockout], ['clip', layer.clip]].filter(([, v]) => v != null);
177
+ for (const [kind, list] of masks) {
178
+ const sub = await ctx.stack(Array.isArray(list) ? list : [list], { tag: `${kind[0]}` });
179
+ const mid = `${ctx.id}${kind[0]}m`;
180
+ const fid = `${ctx.id}${kind[0]}f`;
181
+ const subDefs = sub.map((/** @type {any} */ p) => p.defs).join('');
182
+ let subBody = sub.map((/** @type {any} */ p) => p.body).join('');
183
+ const pad = Math.max(ctx.width, ctx.height);
184
+ const region = { x: -pad, y: -pad, width: ctx.width + 2 * pad, height: ctx.height + 2 * pad };
185
+ let mdefs = subDefs;
186
+ if (kind === 'knockout' || kind === 'clip') {
187
+ // Whatever the sub-layers paint becomes black (cut) or white (kept), keeping their alpha
188
+ // (in renderImage, over just what they paint).
189
+ const v = kind === 'knockout' ? 0 : 1;
190
+ const painted = await contentBox({ defs: subDefs, body: subBody }, ctx);
191
+ mdefs += `<filter${attrs({ id: fid, filterUnits: 'userSpaceOnUse', ...(painted ?? region), 'color-interpolation-filters': 'sRGB' })}><feColorMatrix type="matrix" values="0 0 0 0 ${v} 0 0 0 0 ${v} 0 0 0 0 ${v} 0 0 0 1 0"/></filter>`;
192
+ subBody = `<g filter="url(#${fid})">${subBody}</g>`;
193
+ if (kind === 'knockout') subBody = `<rect${attrs({ ...region, fill: '#fff' })}/>${subBody}`;
194
+ }
195
+ defs += `${mdefs}<mask${attrs({ id: mid, maskUnits: 'userSpaceOnUse', ...region })}>${subBody}</mask>`;
196
+ body = `<g mask="url(#${mid})">${body}</g>`;
197
+ }
198
+ return { defs, body };
199
+ }