@thi.ng/imago 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/proc.js CHANGED
@@ -1,367 +1,91 @@
1
- import { typedArray } from "@thi.ng/api";
2
- import { isArrayBufferView, isNumber, isString } from "@thi.ng/checks";
1
+ import { isArrayBufferView, isString } from "@thi.ng/checks";
3
2
  import { defmulti } from "@thi.ng/defmulti";
4
- import { illegalArgs } from "@thi.ng/errors";
5
- import { readText, writeFile, writeJSON } from "@thi.ng/file-io";
6
3
  import { ROOT } from "@thi.ng/logger";
7
- import { ABGR8888, GRAY8, Lane, intBuffer } from "@thi.ng/pixel";
8
- import {
9
- ATKINSON,
10
- BURKES,
11
- DIFFUSION_2D,
12
- DIFFUSION_COLUMN,
13
- DIFFUSION_ROW,
14
- FLOYD_STEINBERG,
15
- JARVIS_JUDICE_NINKE,
16
- SIERRA2,
17
- STUCKI,
18
- defBayer,
19
- ditherWith,
20
- orderedDither
21
- } from "@thi.ng/pixel-dither";
22
- import { join, resolve } from "node:path";
23
4
  import sharp, {} from "sharp";
24
- import {
25
- GRAVITY_POSITION
26
- } from "./api.js";
27
- import { formatPath } from "./path.js";
28
- import {
29
- coerceColor,
30
- computeMargins,
31
- computeSize,
32
- ensureSize,
33
- gravityPosition,
34
- positionOrGravity
35
- } from "./units.js";
5
+ import { blurProc } from "./ops/blur.js";
6
+ import { compositeProc } from "./ops/composite.js";
7
+ import { cropProc } from "./ops/crop.js";
8
+ import { ditherProc } from "./ops/dither.js";
9
+ import { exifProc } from "./ops/exif.js";
10
+ import { extendProc } from "./ops/extend.js";
11
+ import { gammaProc } from "./ops/gamma.js";
12
+ import { grayscaleProc } from "./ops/grayscale.js";
13
+ import { hsblProc } from "./ops/hsbl.js";
14
+ import { nestProc } from "./ops/nest.js";
15
+ import { outputProc } from "./ops/output.js";
16
+ import { resizeProc } from "./ops/resize.js";
17
+ import { rotateProc } from "./ops/rotate.js";
18
+ import { ensureSize } from "./units.js";
19
+ import { createTempFile, deleteFile } from "@thi.ng/file-io";
36
20
  const LOGGER = ROOT.childLogger("imgproc");
37
- const DITHER_KERNELS = {
38
- atkinson: ATKINSON,
39
- burkes: BURKES,
40
- column: DIFFUSION_COLUMN,
41
- diffusion: DIFFUSION_2D,
42
- floyd: FLOYD_STEINBERG,
43
- jarvis: JARVIS_JUDICE_NINKE,
44
- row: DIFFUSION_ROW,
45
- sierra: SIERRA2,
46
- stucki: STUCKI
47
- };
48
- const processImage = async (src, procs, opts = {}, parentCtx) => {
21
+ const processImage = async (src, specs, opts = {}, parentCtx) => {
49
22
  let img = isString(src) || isArrayBufferView(src.buffer) ? sharp(src) : src;
50
23
  const meta = await img.metadata();
51
24
  ensureSize(meta);
52
25
  const ctx = {
53
26
  path: isString(src) ? src : parentCtx?.path,
54
- outputs: parentCtx ? parentCtx.outputs : [],
27
+ outputs: parentCtx ? parentCtx.outputs : {},
55
28
  logger: opts.logger || LOGGER,
56
29
  size: [meta.width, meta.height],
57
- channels: meta.channels,
30
+ exif: parentCtx ? structuredClone(parentCtx.exif) : {},
58
31
  meta,
59
32
  opts
60
33
  };
61
- let bake;
62
- for (let proc of procs) {
63
- ctx.logger.debug("processing spec:", proc);
64
- [img, bake] = await process(proc, img, ctx);
65
- if (!bake) {
66
- ctx.logger.debug("skip baking processor's results...");
67
- continue;
34
+ if (!parentCtx) {
35
+ if (meta.exif && opts.keepEXIF) {
36
+ ctx.logger.warn("TODO keeping input EXIF still unsupported");
37
+ }
38
+ if (meta.icc && opts.keepICC) {
39
+ ctx.logger.debug("storing input ICC profile...");
40
+ ctx.iccFile = createTempFile(meta.icc, ctx.logger);
68
41
  }
69
- const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
70
- ctx.size = [info.width, info.height];
71
- ctx.channels = info.channels;
72
- img = sharp(data, {
73
- raw: {
74
- width: info.width,
75
- height: info.height,
76
- channels: info.channels
77
- }
78
- });
79
42
  }
80
- return { img, meta, outputs: ctx.outputs };
81
- };
82
- const process = defmulti(
83
- (spec) => spec.type,
84
- {},
85
- {
86
- blur: async (spec, input) => {
87
- const { radius } = spec;
88
- return [input.blur(1 + radius / 2), false];
89
- },
90
- composite: async (spec, input, ctx) => {
91
- const { layers } = spec;
92
- const layerSpecs = await Promise.all(
93
- layers.map((l) => defLayer(l, input, ctx))
94
- );
95
- return [input.composite(layerSpecs), false];
96
- },
97
- crop: async (spec, input, ctx) => {
98
- const { border, gravity, pos, size, ref, unit } = spec;
99
- if (border == null && size == null)
100
- illegalArgs("require `border` or `size` option");
101
- if (border != null) {
102
- const sides = computeMargins(border, ctx.size, ref, unit);
103
- const [left2, right, top2, bottom] = sides;
104
- return [
105
- input.extract({
106
- left: left2,
107
- top: top2,
108
- width: ctx.size[0] - left2 - right,
109
- height: ctx.size[1] - top2 - bottom
110
- }),
111
- true
112
- ];
113
- }
114
- const $size = computeSize(size, ctx.size, unit);
115
- let left = 0, top = 0;
116
- if (pos) {
117
- ({ left = 0, top = 0 } = positionOrGravity(pos, gravity, $size, ctx.size, unit) || {});
118
- } else {
119
- [left, top] = gravityPosition(gravity || "c", $size, ctx.size);
120
- }
121
- return [
122
- input.extract({
123
- left,
124
- top,
125
- width: $size[0],
126
- height: $size[1]
127
- }),
128
- true
129
- ];
130
- },
131
- dither: async (spec, input, ctx) => {
132
- let { mode, num = 2, rgb = false, size = 8 } = spec;
133
- const [w, h] = ctx.size;
134
- let raw;
135
- if (rgb) {
136
- const tmp = await input.clone().ensureAlpha(1).toColorspace("srgb").raw().toBuffer({ resolveWithObject: true });
137
- raw = tmp.data.buffer;
138
- rgb = tmp.info.channels === 4;
139
- } else {
140
- raw = (await input.clone().grayscale().raw().toBuffer()).buffer;
43
+ try {
44
+ let bake;
45
+ for (let spec of specs) {
46
+ ctx.logger.debug("processing spec:", spec);
47
+ [img, bake] = await processor(spec, img, ctx);
48
+ if (!bake) {
49
+ ctx.logger.debug("skip baking processor's results...");
50
+ continue;
141
51
  }
142
- let img = intBuffer(
143
- w,
144
- h,
145
- rgb ? ABGR8888 : GRAY8,
146
- typedArray(rgb ? "u32" : "u8", raw)
147
- );
148
- if (mode === "bayer") {
149
- orderedDither(
150
- img,
151
- defBayer(size),
152
- rgb ? [num, num, num] : [num]
153
- );
154
- } else {
155
- ditherWith(DITHER_KERNELS[mode], img, {
156
- channels: rgb ? [Lane.RED, Lane.GREEN, Lane.BLUE] : void 0
157
- });
158
- }
159
- if (!rgb)
160
- img = img.as(ABGR8888);
161
- return [
162
- sharp(new Uint8Array(img.data.buffer), {
163
- raw: {
164
- width: img.width,
165
- height: img.height,
166
- channels: 4
167
- }
168
- }),
169
- true
170
- ];
171
- },
172
- exif: async (spec, input) => {
173
- const { tags } = spec;
174
- return [input.withExif(tags), false];
175
- },
176
- extend: async (spec, input, ctx) => {
177
- const { bg, border, mode, ref, unit } = spec;
178
- const sides = computeMargins(border, ctx.size, ref, unit);
179
- const [left, right, top, bottom] = sides;
180
- return [
181
- input.extend({
182
- left,
183
- right,
184
- top,
185
- bottom,
186
- background: coerceColor(bg || "#000"),
187
- extendWith: mode
188
- }),
189
- true
190
- ];
191
- },
192
- gamma: async (spec, input) => {
193
- const { gamma } = spec;
194
- return [input.gamma(gamma, 1), false];
195
- },
196
- gray: async (spec, input) => {
197
- const { gamma } = spec;
198
- if (gamma !== false) {
199
- input = input.gamma(isNumber(gamma) ? gamma : void 0);
200
- }
201
- return [input.grayscale(), true];
202
- },
203
- hsbl: async (spec, input) => {
204
- const { h = 0, s = 1, b = 1, l = 0 } = spec;
205
- return [
206
- input.modulate({
207
- hue: h,
208
- brightness: b,
209
- saturation: s,
210
- lightness: l * 255
211
- }),
212
- true
213
- ];
214
- },
215
- nest: async (spec, input, ctx) => {
216
- const { procs } = spec;
217
- ctx.logger.debug("--- nest start ---");
218
- await processImage(input.clone(), procs, ctx.opts, ctx);
219
- ctx.logger.debug("--- nest end ---");
220
- return [input, false];
221
- },
222
- output: async (spec, input, ctx) => {
223
- const opts = spec;
224
- const outDir = resolve(ctx.opts.outDir || ".");
225
- let output = input.clone();
226
- if (opts.raw) {
227
- const { alpha = false, meta = false } = opts.raw !== true ? opts.raw : {};
228
- if (alpha)
229
- output = output.ensureAlpha();
230
- const { data, info } = await output.raw().toBuffer({ resolveWithObject: true });
231
- const path2 = join(
232
- outDir,
233
- formatPath(opts.path, ctx, spec, data)
234
- );
235
- writeFile(path2, data, null, ctx.logger);
236
- ctx.outputs.push(path2);
237
- if (meta) {
238
- writeJSON(
239
- path2 + ".meta.json",
240
- info,
241
- void 0,
242
- void 0,
243
- ctx.logger
244
- );
52
+ const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
53
+ ctx.size = [info.width, info.height];
54
+ img = sharp(data, {
55
+ raw: {
56
+ width: info.width,
57
+ height: info.height,
58
+ channels: info.channels
245
59
  }
246
- return [input, false];
247
- }
248
- let format = /\.(\w+)$/.exec(opts.path)?.[1];
249
- switch (format) {
250
- case "avif":
251
- if (opts.avif)
252
- output = output.avif(opts.avif);
253
- break;
254
- case "gif":
255
- if (opts.gif)
256
- output = output.gif(opts.gif);
257
- break;
258
- case "jpg":
259
- case "jpeg":
260
- if (opts.jpeg)
261
- output = output.jpeg(opts.jpeg);
262
- break;
263
- case "jp2":
264
- if (opts.jp2)
265
- output = output.jp2(opts.jp2);
266
- break;
267
- case "jxl":
268
- if (opts.jxl)
269
- output = output.jxl(opts.jxl);
270
- break;
271
- case "png":
272
- if (opts.png)
273
- output = output.png(opts.png);
274
- break;
275
- case "tiff":
276
- if (opts.tiff)
277
- output = output.tiff(opts.tiff);
278
- break;
279
- case "webp":
280
- if (opts.webp)
281
- output = output.webp(opts.webp);
282
- break;
283
- }
284
- if (opts.tile)
285
- output = output.tile(opts.tile);
286
- if (format)
287
- output = output.toFormat(format);
288
- const result = await output.toBuffer();
289
- const path = join(
290
- outDir,
291
- formatPath(opts.path, ctx, spec, result)
292
- );
293
- writeFile(path, result, null, ctx.logger);
294
- ctx.outputs.push(path);
295
- return [input, false];
296
- },
297
- resize: async (spec, input, ctx) => {
298
- const { bg, filter, fit, gravity, size, unit } = spec;
299
- const [width, height] = computeSize(size, ctx.size, unit);
300
- return [
301
- input.resize({
302
- width,
303
- height,
304
- fit,
305
- kernel: filter,
306
- position: gravity ? GRAVITY_POSITION[gravity] : void 0,
307
- background: bg ? coerceColor(bg) : void 0
308
- }),
309
- true
310
- ];
311
- },
312
- rotate: async (spec, input, _) => {
313
- const { angle, bg, flipX, flipY } = spec;
314
- if (flipX)
315
- input = input.flop();
316
- if (flipY)
317
- input = input.flip();
318
- return [input.rotate(angle, { background: coerceColor(bg) }), true];
60
+ });
319
61
  }
62
+ return { img, meta, outputs: ctx.outputs };
63
+ } finally {
64
+ if (ctx.iccFile)
65
+ deleteFile(ctx.iccFile, ctx.logger);
320
66
  }
321
- );
322
- const defLayer = defmulti(
323
- (x) => x.type,
67
+ };
68
+ const processor = defmulti(
69
+ (spec) => spec.op,
324
70
  {},
325
71
  {
326
- img: async (layer, _, ctx) => {
327
- const { gravity, path, pos, size, unit, ...opts } = layer;
328
- const input = sharp(path);
329
- const meta = await input.metadata();
330
- let imgSize = [meta.width, meta.height];
331
- const $pos = positionOrGravity(
332
- pos,
333
- gravity,
334
- imgSize,
335
- ctx.size,
336
- unit
337
- );
338
- if (!size)
339
- return { input: path, ...$pos, ...opts };
340
- ensureSize(meta);
341
- imgSize = computeSize(size, imgSize, unit);
342
- return {
343
- input: await input.resize(imgSize[0], imgSize[1]).png({ compressionLevel: 0 }).toBuffer(),
344
- ...$pos,
345
- ...opts
346
- };
347
- },
348
- svg: async (layer, _, ctx) => {
349
- let { body, gravity, path, pos, unit, ...opts } = layer;
350
- if (path)
351
- body = readText(path, ctx.logger);
352
- const w = +(/width="(\d+)"/.exec(body)?.[1] || 0);
353
- const h = +(/height="(\d+)"/.exec(body)?.[1] || 0);
354
- return {
355
- input: Buffer.from(body),
356
- ...positionOrGravity(pos, gravity, [w, h], ctx.size, unit),
357
- ...opts
358
- };
359
- }
72
+ blur: blurProc,
73
+ composite: compositeProc,
74
+ crop: cropProc,
75
+ dither: ditherProc,
76
+ exif: exifProc,
77
+ extend: extendProc,
78
+ gamma: gammaProc,
79
+ gray: grayscaleProc,
80
+ hsbl: hsblProc,
81
+ nest: nestProc,
82
+ output: outputProc,
83
+ resize: resizeProc,
84
+ rotate: rotateProc
360
85
  }
361
86
  );
362
87
  export {
363
88
  LOGGER,
364
- defLayer,
365
- process,
366
- processImage
89
+ processImage,
90
+ processor
367
91
  };