@thi.ng/imago 0.1.0 → 0.3.0

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