Note
This is one of 192 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.
🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️
- About
- Supported operations
- Status
- Positions & sizes
- Metadata handling
- Installation
- Dependencies
- API
- Authors
- License
JSON & API-based declarative and extensible image processing trees/pipelines.
Spiritual successor of an eponymous, yet never fully published CLojure/Java-based image processor from 2014...
In this new TypeScript version all image I/O and processing is delegated to sharp and @thi.ng/pixel.
Transformation trees/pipelines are simple JSON objects (but can be programmatically created):
The following pipeline performs these steps (in sequence):
- auto-rotate image (using EXIF orientation info, if available)
- add 5% white border (size relative to shortest side)
- proportionally resize image to 1920px (longest side by default)
- overlay bitmap logo layer, positioned at 45% left / 5% bottom
- add custom EXIF metadata
- output this current stage as high quality AVIF (and record expanded output path)
- crop center square region
- output as JPEG thumbnail (and record in outputs)
- compute blurhash (and record in outputs)
[
{ "op": "rotate" },
{ "op": "extend", "border": 5, "unit": "%", "ref": "min", "bg": "#fff" },
{ "op": "resize", "size": 1920 },
{
"op": "composite",
"layers": [
{
"type": "img",
"path": "logo-128.png",
"pos": { "l": 45, "b": 5 },
"unit": "%",
"blend": "screen"
}
]
},
{
"op": "exif",
"tags": {
"IFD0": {
"Copyright": "Karsten Schmidt",
"Software": "@thi.ng/imago"
}
}
},
{
"op": "output",
"id": "hires",
"path": "{name}-{sha256}-{w}x{h}.avif",
"avif": { "quality": 80 }
},
{ "op": "crop", "size": [240, 240], "gravity": "c" },
{ "op": "output", "id": "thumb", "path": "{name}-thumb.jpg" },
{ "op": "output", "id": "hash", "blurhash": 4 }
]
Then to process an image using above JSON spec (there're also API wrappers to create these operator specs programmatically):
import { processImage } from "@thi.ng/imago";
import { readJSON } from "@thi.ng/file-io";
await processImage(
"test.jpg",
readJSON("readme-example1.json"),
{ outDir: "." }
);
TODO write docs
Gaussian blur
- radius
Compositing multiple layers. The following layer types are available, and custom
layer types can be registered via the polymorphic
defLayer()
function.
- blend mode
- position & origin
- gravity
- tiled repetition
- resizable
- size
- fill color (w/ alpha)
- from typed array or buffer
- from file or inline doc
- optional background color (alpha supported)
- text color
- horizontal/vertical text align
- font family & size
- constrained to text box
Cropping a part of the image
- from edges, defined region or size & aspect ratio only
- supports px or percent units
- proportional to a given reference side/size
Supported dither modes from thi.ng/pixel-dither:
- "atkinson"
- "burkes"
- "column"
- "diffusion"
- "floyd"
- "jarvis"
- "row"
- "sierra"
- "stucki"
Set custom EXIF metadata (can be given multiple times, will be merged)
Add pixels on all sides of the image
- supports px or percent units
- proportional to a given reference side/size
- can be individually configured per side
Perform gamma correction (forward or reverse)
Grayscale conversion
Hue, saturation, brightness and lightness adjustments
Assign ICC profile (from preset: p3
, srgb
, cmyk
or from file). Can only be
given directly prior to output, overrides input ICC (if any) and only
used if output format actually supports it.
Performing nested branches/pipelines of operations with no effect on image state of current/parent pipeline...
File output in any of these formats:
- avif
- gif
- jpeg
- jp2 (JPEG 2000)
- jxl (JPEG XL)
- png
- raw (headless raw data)
- tiff
- webp
Alternatively, a blurhash of the image can be computed and stored in the outputs. In this case, no file will be written.
Output paths can contain {id}
-templated parts which will be replaced/expanded.
The following built-in IDs are supported and custom IDs will be looked up via
the
pathParts
options provided to
processImage().
Any others will remain as is. Custom IDs take precedence over built-in ones.
name
: original base filename (w/o ext)sha1
/sha224
/sha256
/sha384
/sha512
: truncated hash of output (8 chars)w
: current image widthh
: current image heightaspect
: "p" (portrait), "l" (landscape) or "sq" (square)date
: yyyyMMdd date format, e.g. 20240223time
: HHmmss time format, e.g. 234459year
: 4-digit yearmonth
: 2-digit monthweek
: 2-digit weekday
: 2-digit day in monthhour
: 2-digit hour (24h system)minute
: 2-digit minutesecond
: 2-digit second
Output paths can contain sub-directories which will be automatically created
(relative to the configured output
dir).
For example, the path template {year}/{month}/{day}/{name}-{sha1}.jpg
might
get replaced to: 2024/02/22/test-123cafe4.jpg
...
Resizing image
- gravity or position
- fit modes
- supports px or percent units
- proportional to a given reference side/size
Auto-rotate, rotate by angle and/or flip image along x/y
ALPHA - bleeding edge / work-in-progress
Search or submit any issues for this package
Border sizes, general dimensions, and positions can be specified in pixels
(default) or as percentages (using unit: "%"
). For the latter case, an
additional reference side (ref
option) can be provided. The default ref is
min
, referring to whatever is the smaller side of an image.
The ref
option/reference side can take the following values (default: both
):
both
: image width for horizontal uses, image height for vertical usesmin
: smaller side of an image (akamin(width,height)
)max
: larger side of an image (akamin(width,height)
)w
: image widthh
: image height
In some operations positioning or alignment can be abstractly stated via one of the following gravity values:
By default all input metadata will be lost in the outputs. The keepEXIF
and
keepICC
options can be used to retain EXIF and/or ICC profile information
(only if also supported in the output format).
Important: Retaining EXIF and merging it with custom additions is still WIP...
yarn add @thi.ng/imago
ESM import:
import * as imago from "@thi.ng/imago";
For Node.js REPL:
const imago = await import("@thi.ng/imago");
Package sizes (brotli'd, pre-treeshake): ESM: 4.93 KB
- @thi.ng/api
- @thi.ng/associative
- @thi.ng/blurhash
- @thi.ng/checks
- @thi.ng/date
- @thi.ng/defmulti
- @thi.ng/errors
- @thi.ng/file-io
- @thi.ng/logger
- @thi.ng/pixel
- @thi.ng/pixel-dither
- @thi.ng/prefixes
- sharp
import {
colorLayer,
composite,
crop,
extend,
imageLayer,
nest,
output,
processImage,
rawLayer,
resize,
rotate,
} from "@thi.ng/imago";
import { ConsoleLogger } from "@thi.ng/logger";
const res = await processImage(
"test.jpg",
// operator pipeline (i.e. a nested array of operator spec objects)
// the functions used here are merely syntax sugar for generating
// the spec objects to provide an anchor point for docs
// (ongoing effort, but since still a new project, mostly still forthcoming...)
[
// auto-rotate (EXIF orientation)
rotate({}),
// composite w/ semi-transparent color layer (screen)
composite({
layers: [
colorLayer({
// magenta with 50% opacity
bg: "#f0f8",
blend: "screen",
// layer size is 50x100% of image
size: [50, 100],
// aligned left (west)
gravity: "w",
// size given in percent
unit: "%",
}),
// diagonal hairline pattern overlay (with tiling) from raw
// pixel data in ABGR format, i.e. 0xAABBGGRR
rawLayer({
// prettier-ignore
buffer: new Uint32Array([
0x00000000, 0x00000000, 0x00000000, 0x80ffffff,
0x00000000, 0x00000000, 0x80ffffff, 0x00000000,
0x00000000, 0x80ffffff, 0x00000000, 0x00000000,
0x80ffffff, 0x00000000, 0x00000000, 0x00000000,
]),
channels: 4,
size: [4, 4],
tile: true,
}),
],
}),
// nested operations each operate on a clone of the current (already
// semi-transformed) image, they have no impact on the processing pipeline
// of their parent(s)
// multiple child pipelines can be spawned, here only a single one
nest({
procs: [
// this pipeline only creates blurhash (stored in `outputs` of result)
[resize({ size: 100 }), output({ id: "hash", blurhash: true })],
],
}),
// crop to 3:2 aspect ratio (always based on longest side)
crop({ size: 100, aspect: 3 / 2, unit: "%" }),
// back in the main pipleline, add 5% white border (based on smallest side)
extend({ border: 5, unit: "%", bg: "white", ref: "min" }),
// resize image to 1920 (largest side)
resize({ size: 1920 }),
// add logo watermark centered horizontally and near the bottom
composite({
layers: [
imageLayer({
path: "logo-128.png",
unit: "%",
origin: "s",
pos: { l: 50, b: 5 },
ref: "both",
blend: "screen",
}),
],
}),
output({ id: "main", path: "{date}-1920-frame.jpg" }),
],
{
logger: new ConsoleLogger("img"),
}
);
console.log(res.outputs);
// {
// hash: "UVKmR.^SIVR$_NRiM{jupLRjjEWC%goxofoM",
// main: "...../20240301-144948-1920-frame.jpg",
// }
If this project contributes to an academic publication, please cite it as:
@misc{thing-imago,
title = "@thi.ng/imago",
author = "Karsten Schmidt",
note = "https://thi.ng/imago",
year = 2024
}
© 2024 Karsten Schmidt // Apache License 2.0