@thi.ng/imago 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/README.md +291 -0
- package/api.d.ts +161 -0
- package/api.js +26 -0
- package/index.d.ts +6 -0
- package/index.js +5 -0
- package/ops.d.ts +16 -0
- package/ops.js +30 -0
- package/package.json +125 -0
- package/path.d.ts +22 -0
- package/path.js +34 -0
- package/proc.d.ts +27 -0
- package/proc.js +362 -0
- package/units.d.ts +24 -0
- package/units.js +85 -0
package/index.d.ts
ADDED
package/index.js
ADDED
package/ops.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BlurSpec, CompSpec, CropSpec, DitherSpec, EXIFSpec, ExtendSpec, GammaSpec, GrayscaleSpec, HSBLSpec, NestSpec, OutputSpec, ProcSpec, ResizeSpec, RotateSpec } from "./api.js";
|
|
2
|
+
export declare const defSpec: <T extends ProcSpec>(type: T["type"]) => (opts: Omit<T, "type">) => T;
|
|
3
|
+
export declare const blur: (opts: Omit<BlurSpec, "type">) => BlurSpec;
|
|
4
|
+
export declare const composite: (opts: Omit<CompSpec, "type">) => CompSpec;
|
|
5
|
+
export declare const crop: (opts: Omit<CropSpec, "type">) => CropSpec;
|
|
6
|
+
export declare const dither: (opts: Omit<DitherSpec, "type">) => DitherSpec;
|
|
7
|
+
export declare const exif: (opts: Omit<EXIFSpec, "type">) => EXIFSpec;
|
|
8
|
+
export declare const extend: (opts: Omit<ExtendSpec, "type">) => ExtendSpec;
|
|
9
|
+
export declare const gamma: (opts: Omit<GammaSpec, "type">) => GammaSpec;
|
|
10
|
+
export declare const grayscale: (opts: Omit<GrayscaleSpec, "type">) => GrayscaleSpec;
|
|
11
|
+
export declare const hsbl: (opts: Omit<HSBLSpec, "type">) => HSBLSpec;
|
|
12
|
+
export declare const nest: (opts: Omit<NestSpec, "type">) => NestSpec;
|
|
13
|
+
export declare const output: (opts: Omit<OutputSpec, "type">) => OutputSpec;
|
|
14
|
+
export declare const resize: (opts: Omit<ResizeSpec, "type">) => ResizeSpec;
|
|
15
|
+
export declare const rotate: (opts: Omit<RotateSpec, "type">) => RotateSpec;
|
|
16
|
+
//# sourceMappingURL=ops.d.ts.map
|
package/ops.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const defSpec = (type) => (opts) => ({ type, ...opts });
|
|
2
|
+
const blur = defSpec("blur");
|
|
3
|
+
const composite = defSpec("composite");
|
|
4
|
+
const crop = defSpec("crop");
|
|
5
|
+
const dither = defSpec("dither");
|
|
6
|
+
const exif = defSpec("exif");
|
|
7
|
+
const extend = defSpec("extend");
|
|
8
|
+
const gamma = defSpec("gamma");
|
|
9
|
+
const grayscale = defSpec("gray");
|
|
10
|
+
const hsbl = defSpec("hsbl");
|
|
11
|
+
const nest = defSpec("nest");
|
|
12
|
+
const output = defSpec("output");
|
|
13
|
+
const resize = defSpec("resize");
|
|
14
|
+
const rotate = defSpec("rotate");
|
|
15
|
+
export {
|
|
16
|
+
blur,
|
|
17
|
+
composite,
|
|
18
|
+
crop,
|
|
19
|
+
defSpec,
|
|
20
|
+
dither,
|
|
21
|
+
exif,
|
|
22
|
+
extend,
|
|
23
|
+
gamma,
|
|
24
|
+
grayscale,
|
|
25
|
+
hsbl,
|
|
26
|
+
nest,
|
|
27
|
+
output,
|
|
28
|
+
resize,
|
|
29
|
+
rotate
|
|
30
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thi.ng/imago",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSON & API-based declarative and extensible image processing trees/pipelines",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"typings": "./index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/thi-ng/umbrella.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/thi-ng/umbrella/tree/develop/packages/imago#readme",
|
|
14
|
+
"funding": [
|
|
15
|
+
{
|
|
16
|
+
"type": "github",
|
|
17
|
+
"url": "https://github.com/sponsors/postspectacular"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "patreon",
|
|
21
|
+
"url": "https://patreon.com/thing_umbrella"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"author": "Karsten Schmidt (https://thi.ng)",
|
|
25
|
+
"license": "Apache-2.0",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "yarn build:esbuild && yarn build:decl",
|
|
28
|
+
"build:decl": "tsc --declaration --emitDeclarationOnly",
|
|
29
|
+
"build:esbuild": "esbuild --format=esm --platform=neutral --target=es2022 --tsconfig=tsconfig.json --outdir=. src/**/*.ts",
|
|
30
|
+
"clean": "rimraf --glob '*.js' '*.d.ts' '*.map' doc",
|
|
31
|
+
"doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
|
|
32
|
+
"doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
|
|
33
|
+
"doc:readme": "bun ../../tools/src/module-stats.ts && bun ../../tools/src/readme.ts",
|
|
34
|
+
"pub": "yarn npm publish --access public",
|
|
35
|
+
"test": "bun test"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@thi.ng/api": "^8.9.25",
|
|
39
|
+
"@thi.ng/checks": "^3.5.0",
|
|
40
|
+
"@thi.ng/date": "^2.6.0",
|
|
41
|
+
"@thi.ng/defmulti": "^3.0.25",
|
|
42
|
+
"@thi.ng/errors": "^2.4.18",
|
|
43
|
+
"@thi.ng/file-io": "^1.3.2",
|
|
44
|
+
"@thi.ng/logger": "^3.0.2",
|
|
45
|
+
"@thi.ng/pixel": "^6.1.10",
|
|
46
|
+
"@thi.ng/pixel-dither": "^1.1.108",
|
|
47
|
+
"sharp": "^0.33.2"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@microsoft/api-extractor": "^7.40.1",
|
|
51
|
+
"@thi.ng/vectors": "^7.10.10",
|
|
52
|
+
"esbuild": "^0.20.0",
|
|
53
|
+
"rimraf": "^5.0.5",
|
|
54
|
+
"typedoc": "^0.25.7",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"avif",
|
|
59
|
+
"batch",
|
|
60
|
+
"bitmap",
|
|
61
|
+
"blur",
|
|
62
|
+
"color",
|
|
63
|
+
"crop",
|
|
64
|
+
"composite",
|
|
65
|
+
"dither",
|
|
66
|
+
"exif",
|
|
67
|
+
"fileformat",
|
|
68
|
+
"gif",
|
|
69
|
+
"grayscale",
|
|
70
|
+
"image",
|
|
71
|
+
"jpeg",
|
|
72
|
+
"nested",
|
|
73
|
+
"no-browser",
|
|
74
|
+
"nodejs",
|
|
75
|
+
"pipeline",
|
|
76
|
+
"png",
|
|
77
|
+
"process",
|
|
78
|
+
"resize",
|
|
79
|
+
"svg",
|
|
80
|
+
"tiff",
|
|
81
|
+
"transformation",
|
|
82
|
+
"tree",
|
|
83
|
+
"typescript",
|
|
84
|
+
"webp"
|
|
85
|
+
],
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"browser": {
|
|
90
|
+
"process": false,
|
|
91
|
+
"setTimeout": false
|
|
92
|
+
},
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=18"
|
|
95
|
+
},
|
|
96
|
+
"files": [
|
|
97
|
+
"./*.js",
|
|
98
|
+
"./*.d.ts"
|
|
99
|
+
],
|
|
100
|
+
"exports": {
|
|
101
|
+
".": {
|
|
102
|
+
"default": "./index.js"
|
|
103
|
+
},
|
|
104
|
+
"./api": {
|
|
105
|
+
"default": "./api.js"
|
|
106
|
+
},
|
|
107
|
+
"./ops": {
|
|
108
|
+
"default": "./ops.js"
|
|
109
|
+
},
|
|
110
|
+
"./path": {
|
|
111
|
+
"default": "./path.js"
|
|
112
|
+
},
|
|
113
|
+
"./proc": {
|
|
114
|
+
"default": "./proc.js"
|
|
115
|
+
},
|
|
116
|
+
"./units": {
|
|
117
|
+
"default": "./units.js"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"thi.ng": {
|
|
121
|
+
"status": "alpha",
|
|
122
|
+
"year": 2024
|
|
123
|
+
},
|
|
124
|
+
"gitHead": "4513a1c703bdbf0f0867f03e547e47692e415fac\n"
|
|
125
|
+
}
|
package/path.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import type { TypedArray } from "@thi.ng/api";
|
|
3
|
+
import type { ImgProcCtx } from "./api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Expands/replaces all `{xyz}`-templated identifiers in given file path.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The following IDs are supported. Any others will remain as is.
|
|
9
|
+
*
|
|
10
|
+
* - date: yyyyMMdd
|
|
11
|
+
* - time: HHmmss
|
|
12
|
+
* - name: original base filename (w/o ext)
|
|
13
|
+
* - sha1/224/256/384/512: truncated hash of output
|
|
14
|
+
* - w: current width
|
|
15
|
+
* - h: current height
|
|
16
|
+
*
|
|
17
|
+
* @param path
|
|
18
|
+
* @param buf
|
|
19
|
+
* @param ctx
|
|
20
|
+
*/
|
|
21
|
+
export declare const formatPath: (path: string, buf: Buffer | TypedArray, ctx: ImgProcCtx) => string;
|
|
22
|
+
//# sourceMappingURL=path.d.ts.map
|
package/path.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FMT_HHmmss_ALT, FMT_yyyyMMdd_ALT } from "@thi.ng/date";
|
|
2
|
+
import { illegalArgs as unsupported } from "@thi.ng/errors";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
const formatPath = (path, buf, ctx) => path.replace(/\{(\w+)\}/g, (match, id) => {
|
|
6
|
+
switch (id) {
|
|
7
|
+
case "name": {
|
|
8
|
+
!path && unsupported(
|
|
9
|
+
"cannot format `{name}`, image has no file source"
|
|
10
|
+
);
|
|
11
|
+
const name = basename(ctx.path);
|
|
12
|
+
const idx = name.lastIndexOf(".");
|
|
13
|
+
return idx > 0 ? name.substring(0, idx) : name;
|
|
14
|
+
}
|
|
15
|
+
case "sha1":
|
|
16
|
+
case "sha224":
|
|
17
|
+
case "sha256":
|
|
18
|
+
case "sha384":
|
|
19
|
+
case "sha512":
|
|
20
|
+
return createHash(id).update(buf).digest("hex").substring(0, 8);
|
|
21
|
+
case "w":
|
|
22
|
+
return String(ctx.size[0]);
|
|
23
|
+
case "h":
|
|
24
|
+
return String(ctx.size[1]);
|
|
25
|
+
case "date":
|
|
26
|
+
return FMT_yyyyMMdd_ALT();
|
|
27
|
+
case "time":
|
|
28
|
+
return FMT_HHmmss_ALT();
|
|
29
|
+
}
|
|
30
|
+
return match;
|
|
31
|
+
});
|
|
32
|
+
export {
|
|
33
|
+
formatPath
|
|
34
|
+
};
|
package/proc.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import sharp, { type Sharp } from "sharp";
|
|
3
|
+
import { type CompLayer, type ImgProcCtx, type ImgProcOpts, type ProcSpec } from "./api.js";
|
|
4
|
+
export declare const LOGGER: import("@thi.ng/logger").ProxyLogger;
|
|
5
|
+
export declare const processImage: (src: string | Buffer | Sharp, procs: ProcSpec[], opts?: Partial<ImgProcOpts>, parentCtx?: ImgProcCtx) => Promise<sharp.Sharp>;
|
|
6
|
+
/**
|
|
7
|
+
* Extensible polymorphic function performing a single image processing step.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* The function returns a tuple of `[img, bake-flag]`. If the flag (2nd value)
|
|
11
|
+
* is true, the returned image will be serialized/baked to an internal buffer
|
|
12
|
+
* (also wiping any EXIF!) after the current processing step and then used as
|
|
13
|
+
* input for the next processing step.
|
|
14
|
+
*
|
|
15
|
+
* Due to most ops in sharp's API merely setting internal state rather than
|
|
16
|
+
* applying changes directly, all size or channel changing procs will require
|
|
17
|
+
* "baking" in order to produce predictable results... The {@link processImage}
|
|
18
|
+
* function checks this flag after each processing step and its down to each
|
|
19
|
+
* processor to determine if baking is required or not...
|
|
20
|
+
*
|
|
21
|
+
* @param spec
|
|
22
|
+
* @param img
|
|
23
|
+
* @param ctx
|
|
24
|
+
**/
|
|
25
|
+
export declare const process: import("@thi.ng/defmulti").MultiFn3<ProcSpec, sharp.Sharp, ImgProcCtx, Promise<[sharp.Sharp, boolean]>>;
|
|
26
|
+
export declare const defLayer: import("@thi.ng/defmulti").MultiFn3<CompLayer, sharp.Sharp, ImgProcCtx, Promise<sharp.OverlayOptions>>;
|
|
27
|
+
//# sourceMappingURL=proc.d.ts.map
|
package/proc.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { typedArray } from "@thi.ng/api";
|
|
2
|
+
import { isArrayBufferView, isNumber, isString } from "@thi.ng/checks";
|
|
3
|
+
import { defmulti } from "@thi.ng/defmulti";
|
|
4
|
+
import { illegalArgs } from "@thi.ng/errors";
|
|
5
|
+
import { readText, writeFile, writeJSON } from "@thi.ng/file-io";
|
|
6
|
+
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
|
+
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";
|
|
36
|
+
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) => {
|
|
49
|
+
let img = isString(src) || isArrayBufferView(src.buffer) ? sharp(src) : src;
|
|
50
|
+
const meta = await img.metadata();
|
|
51
|
+
ensureSize(meta);
|
|
52
|
+
const ctx = {
|
|
53
|
+
path: isString(src) ? src : parentCtx?.path,
|
|
54
|
+
logger: opts?.logger || LOGGER,
|
|
55
|
+
size: [meta.width, meta.height],
|
|
56
|
+
channels: meta.channels,
|
|
57
|
+
meta,
|
|
58
|
+
opts
|
|
59
|
+
};
|
|
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;
|
|
67
|
+
}
|
|
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
|
+
}
|
|
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;
|
|
140
|
+
}
|
|
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
|
+
);
|
|
240
|
+
}
|
|
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];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
const defLayer = defmulti(
|
|
318
|
+
(x) => x.type,
|
|
319
|
+
{},
|
|
320
|
+
{
|
|
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
|
+
}
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
export {
|
|
358
|
+
LOGGER,
|
|
359
|
+
defLayer,
|
|
360
|
+
process,
|
|
361
|
+
processImage
|
|
362
|
+
};
|
package/units.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Nullable } from "@thi.ng/api";
|
|
2
|
+
import type { Metadata } from "sharp";
|
|
3
|
+
import { type Color, type CompLayer, type Dim, type Gravity, type Sides, type Size, type SizeRef, type SizeUnit } from "./api.js";
|
|
4
|
+
export declare const ensureSize: (meta: Metadata) => false;
|
|
5
|
+
export declare const coerceColor: (col: Color) => string | {
|
|
6
|
+
r: number;
|
|
7
|
+
g: number;
|
|
8
|
+
b: number;
|
|
9
|
+
alpha?: number | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare const positionOrGravity: (pos: CompLayer["pos"], gravity: Nullable<Gravity>, [w, h]: Dim, [parentW, parentH]: Dim, unit?: SizeUnit) => {
|
|
12
|
+
gravity: string;
|
|
13
|
+
left?: undefined;
|
|
14
|
+
top?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
left: number | undefined;
|
|
17
|
+
top: number | undefined;
|
|
18
|
+
gravity?: undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
export declare const gravityPosition: (gravity: Gravity, [w, h]: Dim, [parentW, parentH]: Dim) => number[];
|
|
21
|
+
export declare const refSize: ([w, h]: Dim, ref?: SizeRef) => number;
|
|
22
|
+
export declare const computeSize: (size: Size, curr: Dim, unit?: SizeUnit) => Dim;
|
|
23
|
+
export declare const computeMargins: (size: Size | Sides, curr: Dim, ref?: SizeRef, unit?: SizeUnit) => Sides;
|
|
24
|
+
//# sourceMappingURL=units.d.ts.map
|