2021-10-17 04:00:24 +02:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const os = require(`os`);
|
|
|
|
const fs = require("fs");
|
|
|
|
const path = require("path");
|
|
|
|
|
|
|
|
const pretty_ms = require("pretty-ms");
|
|
|
|
const debug = require("debug")("image");
|
|
|
|
const imagickal = require("imagickal");
|
|
|
|
const htmlentities = require("html-entities");
|
|
|
|
|
|
|
|
const a = require("./Ansi.js");
|
|
|
|
|
|
|
|
function calculate_size(width, height, size_spec) {
|
|
|
|
if(size_spec.indexOf("%") > -1) {
|
|
|
|
// It's a percentage
|
|
|
|
const multiplier = parseInt(size_spec.replace(/%/, ""), 10) / 100;
|
|
|
|
return {
|
|
|
|
width: Math.ceil(width * multiplier),
|
|
|
|
height: Math.ceil(height * multiplier)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// It's an absolute image width
|
|
|
|
const new_width = parseInt(size_spec, 10);
|
|
|
|
return {
|
|
|
|
width: new_width,
|
|
|
|
height: Math.ceil(new_width/width * height)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Main task list - we make sure it completes before exiting.
|
|
|
|
var queue = null;
|
|
|
|
|
|
|
|
async function make_queue() {
|
|
|
|
// 1: Setup task queue
|
|
|
|
const PQueue = (await import("p-queue")).default;
|
|
|
|
let concurrency = os.cpus().length;
|
|
|
|
if(process.env["MAX_CONCURRENT"])
|
|
|
|
concurrency = parseInt(process.env["MAX_CONCURRENT"], 10);
|
|
|
|
debug(`Image conversion queue concurrency: `, concurrency);
|
|
|
|
queue = new PQueue({ concurrency });
|
|
|
|
queue.on("idle", () => console.log(`IMAGE ${a.fcyan}all conversions complete${a.reset}`));
|
|
|
|
process.on("exit", async () => {
|
|
|
|
debug(`Waiting for image conversions to finish...`);
|
|
|
|
await queue.onEmpty();
|
|
|
|
debug(`All image conversions complete.`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function srcset(source_image, target_dir, urlpath, format = "__AUTO__", sizes = [ "25%", "50%", "100%" ], quality = 95, strip = true) {
|
|
|
|
if(queue === null) await make_queue();
|
|
|
|
|
|
|
|
const source_parsed = path.parse(source_image);
|
|
|
|
// ext contains the dot . already
|
|
|
|
const target_format = format == "__AUTO__" ? source_parsed.ext.replace(/\./g, "") : format;
|
|
|
|
|
|
|
|
const source_size = await imagickal.dimensions(source_image);
|
|
|
|
|
|
|
|
debug(`SOURCE_SIZE`, source_size, `TARGET_FORMAT`, target_format);
|
|
|
|
|
|
|
|
let setitems = await Promise.all(sizes.map(async (size) => {
|
2021-10-17 04:04:50 +02:00
|
|
|
let target_filename = `${source_parsed.name}_${size}.${target_format}`
|
|
|
|
.replace(/%/, "pcent");
|
2021-10-17 04:00:24 +02:00
|
|
|
let target_current = path.join(
|
|
|
|
target_dir,
|
|
|
|
target_filename
|
|
|
|
);
|
|
|
|
queue.add(async () => {
|
|
|
|
const start = new Date();
|
|
|
|
await imagickal.transform(source_image, target_current, {
|
|
|
|
resize: { width: size },
|
|
|
|
quality,
|
|
|
|
strip
|
|
|
|
});
|
2022-07-06 02:41:27 +02:00
|
|
|
console.log(`${a.fmagenta}${a.hicol}IMAGE${a.reset}\t${a.fcyan}${queue.size}/${queue.pending} tasks${a.reset}\t${a.fyellow}${pretty_ms(new Date() - start)}${a.reset}\t${a.fgreen}${target_current}${a.reset}`);
|
2021-10-17 04:00:24 +02:00
|
|
|
});
|
|
|
|
// const size_target = await imagickal.dimensions(target_current);
|
|
|
|
|
|
|
|
const predict = calculate_size(source_size.width, source_size.height, size);
|
|
|
|
// debug(`size spec:`, size, `size predicted: ${predict.width}x${predict.height} actual: ${size_target.width}x${size_target.height}`);
|
|
|
|
return `${path.resolve(urlpath, target_filename)} ${predict.width}w`;
|
|
|
|
}));
|
|
|
|
|
|
|
|
return setitems.join(", ");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a string of HTML for a <picture> element, converting images to the specified formats in the process.
|
|
|
|
* @param {string} source_image The filepath to the source image.
|
|
|
|
* @param {string} alt The alt (alternative) text. Automatically run though htmlentities.
|
|
|
|
* @param {string} target_dir The target directory to save converted images to.
|
|
|
|
* @param {string} urlpath The path to the aforementionoed target directory as a URL. Image paths in the HTML will be prefixed with this value.
|
|
|
|
* @param {string} [formats="__AUTO__"] A list of formats to convert the source image to. Defaults to automatically determining the most optimal formats based on the input format. [must be lowercase]
|
|
|
|
* @param {Array} [sizes=["25%","50%", "100%" ]] The sizes, as imagemagick size specs, to convert the source image to.
|
|
|
|
* @param {Number} [quality=95] The quality value to use when converting images.
|
|
|
|
* @param {Boolean} [strip=true] Whether to strip all metadata from images when converting them [saves some space]
|
|
|
|
* @return {Promise<string>} A Promise that returns a generated string of HTML.
|
|
|
|
*/
|
|
|
|
async function picture(source_image, alt, target_dir, urlpath, formats = "__AUTO__", sizes = [ "25%", "50%", "100%" ], quality = 95, strip = true) {
|
|
|
|
const source_parsed = path.parse(source_image);
|
|
|
|
const source_format = source_parsed.ext.toLowerCase().replace(".", "");
|
|
|
|
|
|
|
|
if(formats == "__AUTO__") {
|
|
|
|
switch(source_format) {
|
|
|
|
case "png":
|
|
|
|
case "gif": // * shudder *
|
|
|
|
case "bmp":
|
|
|
|
formats = [ "png" ];
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// jxl = JPEG XL <https://jpegxl.info/> - not currently supported by the old version of imagemagick shipped via apt :-/
|
|
|
|
// Imagemagick v7+ does support it, but isn't shipped yet :-(
|
|
|
|
formats = [ "jpeg", "webp", "avif", /*"jxl"*/ ];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const target_original = path.join(target_dir, source_parsed.base);
|
|
|
|
await fs.promises.copyFile(source_image, target_original);
|
|
|
|
|
|
|
|
const sources = await Promise.all(formats.map(async (format) => {
|
|
|
|
debug(`${format} ${source_image}`);
|
|
|
|
|
|
|
|
return {
|
|
|
|
mime: `image/${format}`,
|
|
|
|
srcset: await srcset(
|
|
|
|
source_image,
|
|
|
|
target_dir, urlpath,
|
|
|
|
format, sizes,
|
|
|
|
quality, strip
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}));
|
|
|
|
|
|
|
|
let result = `<picture>\n\t`;
|
|
|
|
result += sources.map(source => `<source srcset="${source.srcset}" type="${source.mime}" />`).join(`\n\t`);
|
|
|
|
result += `\n\t<img loading="lazy" decoding="async" src="${urlpath}/${source_parsed.base}" alt="${htmlentities.encode(alt)}" />\n`;
|
|
|
|
result += `</picture>\n`
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
var picture_memoize = null;
|
|
|
|
|
|
|
|
async function setup_memoize() {
|
|
|
|
const pMemoize = (await import("p-memoize")).default;
|
|
|
|
picture_memoize = pMemoize(picture);
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = async function(...args) {
|
|
|
|
if(picture_memoize === null) await setup_memoize();
|
|
|
|
return await picture_memoize(...args);
|
|
|
|
};
|