@thi.ng/imago
Version:
JSON & API-based declarative and extensible image processing trees/pipelines
130 lines (129 loc) • 3.85 kB
JavaScript
import { encode } from "@thi.ng/blurhash";
import { isNumber, isPlainObject } from "@thi.ng/checks";
import { illegalArgs } from "@thi.ng/errors";
import { writeFile, writeJSON } from "@thi.ng/file-io";
import { firstNonNullKey } from "@thi.ng/object-utils";
import { formatPath } from "../path.js";
const outputProc = async (spec, input, ctx) => {
const opts = spec;
let output = input.clone();
if (opts.blurhash) {
await __outputBlurHash(opts, output, ctx);
return [input, false];
}
if (opts.raw) {
await __outputRaw(opts, output, ctx);
return [input, false];
}
if (ctx.meta.exif && ctx.opts.keepEXIF) {
ctx.logger.warn(
"TODO injecting & merging EXIF in output still not supported"
);
}
if (Object.keys(ctx.exif).length) {
ctx.logger.debug("setting custom EXIF", ctx.exif);
output = output.withExif(ctx.exif);
}
if (ctx.iccFile && ctx.opts.keepICC) {
ctx.logger.debug("using stored ICC profile:", ctx.iccFile);
output = output.withIccProfile(ctx.iccFile);
}
let format = opts.path ? /\.(\w+)$/.exec(opts.path)?.[1] : firstNonNullKey(opts, [
"avif",
"gif",
"jp2",
"jpeg",
"jxl",
"png",
"raw",
"tile",
"tiff",
"webp"
]);
switch (format) {
case "avif":
if (opts.avif) output = output.avif(opts.avif);
break;
case "gif":
if (opts.gif) output = output.gif(opts.gif);
break;
case "jpg":
case "jpeg":
if (opts.jpeg) output = output.jpeg(opts.jpeg);
break;
case "jp2":
if (opts.jp2) output = output.jp2(opts.jp2);
break;
case "jxl":
if (opts.jxl) output = output.jxl(opts.jxl);
break;
case "png":
if (opts.png) output = output.png(opts.png);
break;
case "tiff":
if (opts.tiff) output = output.tiff(opts.tiff);
break;
case "webp":
if (opts.webp) output = output.webp(opts.webp);
break;
}
if (opts.tile) output = output.tile(opts.tile);
if (format) output = output.toFormat(format);
const result = await output.toBuffer();
if (opts.path !== void 0) {
const path = formatPath(opts.path, ctx, spec, result);
writeFile(path, result, null, ctx.logger);
ctx.outputs[opts.id] = path;
} else {
ctx.outputs[opts.id] = format && opts.dataURL ? asDataURL(`image/${format}`, result) : result;
}
return [input, false];
};
const __outputRaw = async (opts, output, ctx) => {
const { alpha = false, meta = false } = isPlainObject(opts.raw) ? opts.raw : {};
if (alpha) output = output.ensureAlpha();
else output = output.removeAlpha();
const { data, info } = await output.raw().toBuffer({ resolveWithObject: true });
if (meta) {
ctx.outputMeta[opts.id] = { ...info, exif: ctx.exif };
}
if (opts.path !== void 0) {
const path = formatPath(opts.path, ctx, opts, data);
writeFile(path, data, null, ctx.logger);
ctx.outputs[opts.id] = path;
if (meta) {
writeJSON(
path + ".meta.json",
ctx.outputMeta[opts.id],
void 0,
void 0,
ctx.logger
);
}
} else {
ctx.outputs[opts.id] = data;
}
};
const __outputBlurHash = async (opts, output, ctx) => {
const { data, info } = await output.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
const detail = opts.blurhash === true ? 4 : opts.blurhash;
const [dx, dy] = isNumber(detail) ? [detail, detail] : detail;
const hash = encode(
new Uint32Array(data.buffer),
info.width,
info.height,
dx,
dy
);
ctx.logger.debug("computed blurhash:", hash);
ctx.outputs[opts.id] = hash;
};
const asDataURL = (mime, data) => {
if (data.length > 32768)
illegalArgs("encoded image too large for dataURL (max. 32KB allowed)");
return `data:${mime};base64,${data.toString("base64")}`;
};
export {
asDataURL,
outputProc
};