Files
beall/node_modules/@11ty/eleventy-img/src/image.js
2026-03-31 16:38:22 -07:00

931 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require("node:fs");
const fsp = fs.promises;
const path = require("node:path");
const getImageSize = require("image-size");
const debugUtil = require("debug");
const { createHashSync } = require("@11ty/eleventy-utils");
const { Fetch } = require("@11ty/eleventy-fetch");
const sharp = require("./adapters/sharp.js");
const brotliSize = require("./adapters/brotli-size.js");
const Util = require("./util.js");
const ImagePath = require("./image-path.js");
const generateHTML = require("./generate-html.js");
const GLOBAL_OPTIONS = require("./global-options.js").defaults;
const { existsCache, memCache, diskCache } = require("./caches.js");
const debug = debugUtil("Eleventy:Image");
const debugAssets = debugUtil("Eleventy:Assets");
const MIME_TYPES = {
"jpeg": "image/jpeg",
"webp": "image/webp",
"png": "image/png",
"svg": "image/svg+xml",
"avif": "image/avif",
"gif": "image/gif",
};
const FORMAT_ALIASES = {
"jpg": "jpeg",
// if youre working from a mime type input, lets alias it back to svg
"svg+xml": "svg",
};
const ANIMATED_TYPES = [
"webp",
"gif",
];
const TRANSPARENCY_TYPES = [
"avif",
"png",
"webp",
"gif",
"svg",
];
const MINIMUM_TRANSPARENCY_TYPES = [
"png",
"gif",
"svg",
];
class Image {
#input;
#contents = {};
#queue;
#queuePromise;
#buildLogger;
#computedHash;
#directoryManager;
constructor(src, options = {}) {
if(!src) {
throw new Error("`src` is a required argument to the eleventy-img utility (can be a String file path, String URL, or Buffer).");
}
this.src = src;
this.isRemoteUrl = typeof src === "string" && Util.isRemoteUrl(src);
this.rawOptions = options;
this.options = Object.assign({}, GLOBAL_OPTIONS, options);
// Compatible with eleventy-dev-server and Eleventy 3.0.0-alpha.7+ in serve mode.
if(this.options.transformOnRequest && !this.options.urlFormat) {
this.options.urlFormat = function({ src, width, format }/*, imageOptions*/, options) {
return `/.11ty/image/?src=${encodeURIComponent(src)}&width=${width}&format=${format}${options.generatedVia ? `&via=${options.generatedVia}` : ""}`;
};
this.options.statsOnly = true;
}
if(this.isRemoteUrl) {
this.cacheOptions = Object.assign({
type: "buffer",
// deprecated in Eleventy Image, but we already prefer this.cacheOptions.duration automatically
duration: this.options.cacheDuration,
// Issue #117: re-use eleventy-img dryRun option value for eleventy-fetch dryRun
dryRun: this.options.dryRun,
}, this.options.cacheOptions);
// v6.0.0 this now inherits eleventy-fetch option defaults
this.assetCache = Fetch(src, this.cacheOptions);
}
}
setQueue(queue) {
this.#queue = queue;
}
setBuildLogger(buildLogger) {
this.#buildLogger = buildLogger;
}
setDirectoryManager(manager) {
this.#directoryManager = manager;
}
get directoryManager() {
if(!this.#directoryManager) {
throw new Error("Missing #directoryManager");
}
return this.#directoryManager;
}
get buildLogger() {
if(!this.#buildLogger) {
throw new Error("Missing #buildLogger. Call `setBuildLogger`");
}
return this.#buildLogger;
}
// In memory cache is up front, handles promise de-duping from input (this does not use getHash)
// Note: output cache is also in play below (uses getHash)
getInMemoryCacheKey() {
let opts = Util.getSortedObject(this.options);
opts.__originalSrc = this.src;
if(this.isRemoteUrl) {
opts.sourceUrl = this.src; // the source url
} else if(Buffer.isBuffer(this.src)) {
opts.sourceUrl = this.src.toString();
opts.__originalSize = this.src.length;
} else {
// Important: do not cache this
opts.__originalSize = fs.statSync(this.src).size;
}
return JSON.stringify(opts, function(key, value) {
// allows `transform` functions to be truthy for in-memory key
if (typeof value === "function") {
return "<fn>" + (value.name || "");
}
return value;
});
}
getFileContents(overrideLocalFilePath) {
if(!overrideLocalFilePath && this.isRemoteUrl) {
return false;
}
let src = overrideLocalFilePath || this.src;
if(!this.#contents[src]) {
// perf: check to make sure its not a string first
if(typeof src !== "string" && Buffer.isBuffer(src)) {
this.#contents[src] = src;
} else {
debugAssets("[11ty/eleventy-img] Reading %o", src);
this.#contents[src] = fs.readFileSync(src);
}
}
// Always <Buffer>
return this.#contents[src];
}
static getValidWidths(originalWidth, widths = [], allowUpscale = false, minimumThreshold = 1) {
// replace any falsy values with the original width
let valid = widths.map(width => !width || width === 'auto' ? originalWidth : width);
// Convert strings to numbers, "400" (floats are not allowed in sharp)
valid = valid.map(width => parseInt(width, 10));
// Replace any larger-than-original widths with the original width if upscaling is not allowed.
// This ensures that if a larger width has been requested, we're at least providing the closest
// non-upscaled image that we can.
if (!allowUpscale) {
let lastWidthWasBigEnough = true; // first one is always valid
valid = valid.sort((a, b) => a - b).map(width => {
if(width > originalWidth) {
if(lastWidthWasBigEnough) {
return originalWidth;
}
return -1;
}
lastWidthWasBigEnough = originalWidth > Math.floor(width * minimumThreshold);
return width;
}).filter(width => width > 0);
}
// Remove duplicates (e.g., if null happens to coincide with an explicit width
// or a user passes in multiple duplicate values, or multiple larger-than-original
// widths have resulted in the original width being included multiple times)
valid = [...new Set(valid)];
// sort ascending
return valid.sort((a, b) => a - b);
}
static getFormatsArray(formats, autoFormat, svgShortCircuit, isAnimated, hasTransparency) {
if(formats && formats.length) {
if(typeof formats === "string") {
formats = formats.split(",");
}
formats = formats.map(format => {
if(autoFormat) {
if((!format || format === "auto")) {
format = autoFormat;
}
}
if(FORMAT_ALIASES[format]) {
return FORMAT_ALIASES[format];
}
return format;
});
if(svgShortCircuit !== "size") {
// svg must come first for possible short circuiting
formats.sort((a, b) => {
if(a === "svg") {
return -1;
} else if(b === "svg") {
return 1;
}
return 0;
});
}
if(isAnimated) {
let validAnimatedFormats = formats.filter(f => ANIMATED_TYPES.includes(f));
// override formats if a valid animated format is found, otherwise leave as-is
if(validAnimatedFormats.length > 0) {
debug("Filtering non-animated formats from output: from %o to %o", formats, validAnimatedFormats);
formats = validAnimatedFormats;
} else {
debug("No animated output formats found for animated image, using original formats (may be a static image): %o", formats);
}
}
if(hasTransparency) {
let minimumValidTransparencyFormats = formats.filter(f => MINIMUM_TRANSPARENCY_TYPES.includes(f));
// override formats if a valid animated format is found, otherwise leave as-is
if(minimumValidTransparencyFormats.length > 0) {
let validTransparencyFormats = formats.filter(f => TRANSPARENCY_TYPES.includes(f));
debug("Filtering non-transparency-friendly formats from output: from %o to %o", formats, validTransparencyFormats);
formats = validTransparencyFormats;
} else {
debug("At least one transparency-friendly output format of %o must be included if the source image has an alpha channel, skipping formatFiltering and using original formats: %o", MINIMUM_TRANSPARENCY_TYPES, formats);
}
}
// Remove duplicates (e.g., if null happens to coincide with an explicit format
// or a user passes in multiple duplicate values)
formats = [...new Set(formats)];
return formats;
}
return [];
}
#transformRawFiles(files = []) {
let byType = {};
for(let file of files) {
if(!byType[file.format]) {
byType[file.format] = [];
}
byType[file.format].push(file);
}
for(let type in byType) {
// sort by width, ascending (for `srcset`)
byType[type].sort((a, b) => {
return a.width - b.width;
});
}
let filterLargeRasterImages = this.options.svgShortCircuit === "size";
let svgEntry = byType.svg;
let svgSize = svgEntry && svgEntry.length && svgEntry[0].size;
if(filterLargeRasterImages && svgSize) {
for(let type of Object.keys(byType)) {
if(type === "svg") {
continue;
}
let svgAdded = false;
let originalFormatKept = false;
byType[type] = byType[type].map(entry => {
if(entry.size > svgSize) {
if(!svgAdded) {
svgAdded = true;
// need at least one raster smaller than SVG to do this trick
if(originalFormatKept) {
return svgEntry[0];
}
// all rasters are bigger
return false;
}
return false;
}
originalFormatKept = true;
return entry;
}).filter(entry => entry);
}
}
return byType;
}
#finalizeResults(results = {}) {
// used when results are passed to generate HTML, we maintain some internal metadata about the options used.
let imgAttributes = this.options.htmlOptions?.imgAttributes || {};
imgAttributes.src = this.src;
Object.defineProperty(results, "eleventyImage", {
enumerable: false,
writable: false,
value: {
htmlOptions: {
whitespaceMode: this.options.htmlOptions?.whitespaceMode,
imgAttributes,
pictureAttributes: this.options.htmlOptions?.pictureAttributes,
fallback: this.options.htmlOptions?.fallback,
},
}
});
// renamed `return` to `returnType` to match Fetch API in v6.0.0-beta.3
if(this.options.returnType === "html" || this.options.return === "html") {
return generateHTML(results);
}
return results;
}
getSharpOptionsForFormat(format) {
if(format === "webp") {
return this.options.sharpWebpOptions;
} else if(format === "jpeg") {
return this.options.sharpJpegOptions;
} else if(format === "png") {
return this.options.sharpPngOptions;
} else if(format === "avif") {
return this.options.sharpAvifOptions;
}
return {};
}
async getInput() {
// internal cache
if(!this.#input) {
if(this.isRemoteUrl) {
// fetch remote image Buffer
this.#input = this.assetCache.queue();
} else {
// not actually a promise, this is sync
this.#input = this.getFileContents();
}
}
return this.#input;
}
getHash() {
if (this.#computedHash) {
return this.#computedHash;
}
// debug("Creating hash for %o", this.src);
let hashContents = [];
if(existsCache.exists(this.src)) {
let fileContents = this.getFileContents();
// If the file starts with whitespace or the '<' character, it might be SVG.
// Otherwise, skip the expensive buffer.toString() call
// (no point in unicode encoding a binary file)
let fileContentsPrefix = fileContents?.slice(0, 1)?.toString()?.trim();
if (!fileContentsPrefix || fileContentsPrefix[0] == "<") {
// remove all newlines for hashing for better cross-OS hash compatibility (Issue #122)
let fileContentsStr = fileContents.toString();
let firstFour = fileContentsStr.trim().slice(0, 5);
if(firstFour === "<svg " || firstFour === "<?xml") {
fileContents = fileContentsStr.replace(/\r|\n/g, '');
}
}
hashContents.push(fileContents);
} else {
// probably a remote URL
hashContents.push(this.src);
// `useCacheValidityInHash` was removed in v6.0.0, but well keep this as part of the hash to maintain consistent hashes across versions
if(this.isRemoteUrl && this.assetCache && this.cacheOptions) {
hashContents.push(`ValidCache:true`);
}
}
// We ignore all keys not relevant to the file processing/output (including `widths`, which is a suffix added to the filename)
// e.g. `widths: [300]` and `widths: [300, 600]`, with all else being equal the 300px output of each should have the same hash
let keysToKeep = [
"sharpOptions",
"sharpWebpOptions",
"sharpPngOptions",
"sharpJpegOptions",
"sharpAvifOptions"
].sort();
let hashObject = {};
// The code currently assumes are keysToKeep are Object literals (see Util.getSortedObject)
for(let key of keysToKeep) {
if(this.options[key]) {
hashObject[key] = Util.getSortedObject(this.options[key]);
}
}
hashContents.push(JSON.stringify(hashObject));
let base64hash = createHashSync(...hashContents);
let truncated = base64hash.substring(0, this.options.hashLength);
this.#computedHash = truncated;
return truncated;
}
getStat(outputFormat, width, height) {
let url;
let outputFilename;
if(this.options.urlFormat && typeof this.options.urlFormat === "function") {
let hash;
if(!this.options.statsOnly) {
hash = this.getHash();
}
url = this.options.urlFormat({
hash,
src: this.src,
width,
format: outputFormat,
}, this.options);
} else {
let hash = this.getHash();
outputFilename = ImagePath.getFilename(hash, this.src, width, outputFormat, this.options);
if(Util.isFullUrl(this.options.urlPath)) {
url = new URL(outputFilename, this.options.urlPath).toString();
} else {
url = ImagePath.convertFilePathToUrl(this.options.urlPath, outputFilename);
}
}
let statEntry = {
format: outputFormat,
width: width,
height: height,
url: url,
sourceType: MIME_TYPES[outputFormat],
srcset: `${url} ${width}w`,
// Not available in stats* functions below
// size // only after processing
};
if(outputFilename) {
statEntry.filename = outputFilename; // optional
statEntry.outputPath = path.join(this.options.outputDir, outputFilename); // optional
}
return statEntry;
}
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
// Orientations 5 to 8 mean image is rotated ±90º (width/height are flipped)
needsRotation(orientation) {
// Sharp's metadata API exposes undefined EXIF orientations >8 as 1 (normal) but check anyways
return orientation >= 5 && orientation <= 8;
}
isAnimated(metadata) {
// sharp options have animated image support enabled
if(!this.options?.sharpOptions?.animated) {
return false;
}
let isAnimationFriendlyFormat = ANIMATED_TYPES.includes(metadata.format);
if(!isAnimationFriendlyFormat) {
return false;
}
if(metadata?.pages) {
// input has multiple pages: https://sharp.pixelplumbing.com/api-input#metadata
// this is *unknown* when not called from `resize` (limited metadata available)
return metadata?.pages > 1;
}
// Best guess
return isAnimationFriendlyFormat;
}
getEntryFormat(metadata) {
return metadata.format || this.options.overrideInputFormat;
}
// metadata so far: width, height, format
// src is used to calculate the output file names
getFullStats(metadata) {
let results = [];
let isImageAnimated = this.isAnimated(metadata) && Array.isArray(this.options.formatFiltering) && this.options.formatFiltering.includes("animated");
let hasAlpha = metadata.hasAlpha && Array.isArray(this.options.formatFiltering) && this.options.formatFiltering.includes("transparent");
let entryFormat = this.getEntryFormat(metadata);
let outputFormats = Image.getFormatsArray(this.options.formats, entryFormat, this.options.svgShortCircuit, isImageAnimated, hasAlpha);
if (this.needsRotation(metadata.orientation)) {
[metadata.height, metadata.width] = [metadata.width, metadata.height];
}
if(metadata.pageHeight) {
// When the { animated: true } option is provided to sharp, animated
// image formats like gifs or webp will have an inaccurate `height` value
// in their metadata which is actually the height of every single frame added together.
// In these cases, the metadata will contain an additional `pageHeight` property which
// is the height that the image should be displayed at.
metadata.height = metadata.pageHeight;
}
for(let outputFormat of outputFormats) {
if(!outputFormat || outputFormat === "auto") {
throw new Error("When using statsSync or statsByDimensionsSync, `formats: [null | 'auto']` to use the native image format is not supported.");
}
if(outputFormat === "svg") {
if(entryFormat === "svg") {
let svgStats = this.getStat("svg", metadata.width, metadata.height);
// SVG metadata.size is only available with Buffer input (remote urls)
if(metadata.size) {
// Note this is unfair for comparison with raster formats because its uncompressed (no GZIP, etc)
svgStats.size = metadata.size;
}
results.push(svgStats);
if(this.options.svgShortCircuit === true) {
break;
} else {
continue;
}
} else {
debug("Skipping SVG output for %o: received raster input.", this.src);
continue;
}
} else { // not outputting SVG (might still be SVG input though!)
let widths = Image.getValidWidths(metadata.width, this.options.widths, metadata.format === "svg" && this.options.svgAllowUpscale, this.options.minimumThreshold);
for(let width of widths) {
let height = Image.getAspectRatioHeight(metadata, width);
results.push(this.getStat(outputFormat, width, height));
}
}
}
return this.#transformRawFiles(results);
}
static getDimensionsFromSharp(sharpInstance, stat) {
let dims = {};
if(sharpInstance.options.width > -1) {
dims.width = sharpInstance.options.width;
dims.resized = true;
}
if(sharpInstance.options.height > -1) {
dims.height = sharpInstance.options.height;
dims.resized = true;
}
if(dims.width || dims.height) {
if(!dims.width) {
dims.width = Image.getAspectRatioWidth(stat, dims.height);
}
if(!dims.height) {
dims.height = Image.getAspectRatioHeight(stat, dims.width);
}
}
return dims;
}
static getAspectRatioWidth(originalDimensions, newHeight) {
return Math.floor(newHeight * originalDimensions.width / originalDimensions.height);
}
static getAspectRatioHeight(originalDimensions, newWidth) {
// Warning: if this is a guess via statsByDimensionsSync and that guess is wrong
// The aspect ratio will be wrong and any height/widths returned will be wrong!
return Math.floor(newWidth * originalDimensions.height / originalDimensions.width);
}
getOutputSize(contents, filePath) {
if(contents) {
if(this.options.svgCompressionSize === "br") {
return brotliSize(contents);
}
if("length" in contents) {
return contents.length;
}
}
// fallback to looking on local file system
if(!filePath) {
throw new Error("`filePath` expected.");
}
return fs.statSync(filePath).size;
}
isOutputCached(targetFile, sourceInput) {
if(!this.options.useCache) {
return false;
}
// last cache was a miss, so we must write to disk
if(this.assetCache && !this.assetCache.wasLastFetchCacheHit()) {
return false;
}
if(!diskCache.isCached(targetFile, sourceInput, !Util.isRequested(this.options.generatedVia))) {
return false;
}
return true;
}
// src should be a file path to an image or a buffer
async resize(input) {
let sharpInputImage = sharp(input, Object.assign({
// Deprecated by sharp, use `failOn` option instead
// https://github.com/lovell/sharp/blob/1533bf995acda779313fc178d2b9d46791349961/lib/index.d.ts#L915
failOnError: false,
}, this.options.sharpOptions));
// Must find the image format from the metadata
// File extensions lie or may not be present in the src url!
let sharpMetadata = await sharpInputImage.metadata();
let outputFilePromises = [];
let fullStats = this.getFullStats(sharpMetadata);
for(let outputFormat in fullStats) {
for(let stat of fullStats[outputFormat]) {
if(this.isOutputCached(stat.outputPath, input)) {
// Cached images already exist in output
let outputFileContents;
if(this.options.dryRun || outputFormat === "svg" && this.options.svgCompressionSize === "br") {
outputFileContents = this.getFileContents(stat.outputPath);
}
if(this.options.dryRun) {
stat.buffer = outputFileContents;
}
stat.size = this.getOutputSize(outputFileContents, stat.outputPath);
outputFilePromises.push(Promise.resolve(stat));
continue;
}
let sharpInstance = sharpInputImage.clone();
let transform = this.options.transform;
let isTransformResize = false;
if(transform) {
if(typeof transform !== "function") {
throw new Error("Expected `function` type in `transform` option. Received: " + transform);
}
await transform(sharpInstance);
// Resized in a transform (maybe for a crop)
let dims = Image.getDimensionsFromSharp(sharpInstance, stat);
if(dims.resized) {
isTransformResize = true;
// Overwrite current `stat` object with new sizes and file names
stat = this.getStat(stat.format, dims.width, dims.height);
}
}
// https://github.com/11ty/eleventy-img/issues/244
sharpInstance.keepIccProfile();
// Output images do not include orientation metadata (https://github.com/11ty/eleventy-img/issues/52)
// Use sharp.rotate to bake orientation into the image (https://github.com/lovell/sharp/blob/v0.32.6/docs/api-operation.md#rotate):
// > If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation.
// > The use of rotate without an angle will remove the EXIF Orientation tag, if any.
if(this.options.fixOrientation || this.needsRotation(sharpMetadata.orientation)) {
sharpInstance.rotate();
}
if(!isTransformResize) {
if(stat.width < sharpMetadata.width || (this.options.svgAllowUpscale && sharpMetadata.format === "svg")) {
let resizeOptions = {
width: stat.width
};
if(sharpMetadata.format !== "svg" || !this.options.svgAllowUpscale) {
resizeOptions.withoutEnlargement = true;
}
sharpInstance.resize(resizeOptions);
}
}
// Format hooks take priority over Sharp processing.
// format hooks are only used for SVG out of the box
if(this.options.formatHooks && this.options.formatHooks[outputFormat]) {
let hookResult = await this.options.formatHooks[outputFormat].call(stat, sharpInstance);
if(hookResult) {
stat.size = this.getOutputSize(hookResult);
if(this.options.dryRun) {
stat.buffer = Buffer.from(hookResult);
outputFilePromises.push(Promise.resolve(stat));
} else {
this.directoryManager.createFromFile(stat.outputPath);
debugAssets("[11ty/eleventy-img] Writing %o", stat.outputPath);
outputFilePromises.push(fsp.writeFile(stat.outputPath, hookResult).then(() => stat));
}
}
} else { // not a format hook
let sharpFormatOptions = this.getSharpOptionsForFormat(outputFormat);
let hasFormatOptions = Object.keys(sharpFormatOptions).length > 0;
if(hasFormatOptions || outputFormat && sharpMetadata.format !== outputFormat) {
// https://github.com/lovell/sharp/issues/3680
// Fix heic regression in sharp 0.33
if(outputFormat === "heic" && !sharpFormatOptions.compression) {
sharpFormatOptions.compression = "av1";
}
sharpInstance.toFormat(outputFormat, sharpFormatOptions);
}
if(!this.options.dryRun && stat.outputPath) {
// Should never write when dryRun is true
this.directoryManager.createFromFile(stat.outputPath);
debugAssets("[11ty/eleventy-img] Writing %o", stat.outputPath);
outputFilePromises.push(
sharpInstance.toFile(stat.outputPath)
.then(info => {
stat.size = info.size;
return stat;
})
);
} else {
outputFilePromises.push(sharpInstance.toBuffer({ resolveWithObject: true }).then(({ data, info }) => {
stat.buffer = data;
stat.size = info.size;
return stat;
}));
}
}
if(stat.outputPath) {
if(this.options.dryRun) {
debug( "Generated %o", stat.url );
} else {
debug( "Wrote %o", stat.outputPath );
}
}
}
}
return Promise.all(outputFilePromises).then(files => this.#finalizeResults(this.#transformRawFiles(files)));
}
async getStatsOnly() {
if(typeof this.src !== "string" || !this.options.statsOnly) {
return;
}
let input;
if(Util.isRemoteUrl(this.src)) {
if(this.rawOptions.remoteImageMetadata?.width && this.rawOptions.remoteImageMetadata?.height) {
return this.getFullStats({
width: this.rawOptions.remoteImageMetadata.width,
height: this.rawOptions.remoteImageMetadata.height,
format: this.rawOptions.remoteImageMetadata.format, // only required if you want to use the "auto" format
guess: true,
});
}
// Fetch remote image to operate on it
// `remoteImageMetadata` is no longer required for statsOnly on remote images
input = await this.getInput();
}
// Local images
try {
// Related to https://github.com/11ty/eleventy-img/issues/295
let { width, height, type } = getImageSize(input || this.src);
return this.getFullStats({
width,
height,
format: type // only required if you want to use the "auto" format
});
} catch(e) {
throw new Error(`Eleventy Image error (statsOnly): \`image-size\` on "${this.src}" failed. Original error: ${e.message}`);
}
}
// returns raw Promise
queue() {
if(!this.#queue) {
return Promise.reject(new Error("Missing #queue."));
}
if(this.#queuePromise) {
return this.#queuePromise;
}
debug("Processing %o (in-memory cache miss), options: %o", this.src, this.options);
this.#queuePromise = this.#queue.add(async () => {
try {
if(typeof this.src === "string" && this.options.statsOnly) {
return this.getStatsOnly();
}
this.buildLogger.log(`Processing ${this.buildLogger.getFriendlyImageSource(this.src)}`, this.options);
let input = await this.getInput();
return this.resize(input);
} catch(e) {
this.buildLogger.error(`Error: ${e.message} (via ${this.buildLogger.getFriendlyImageSource(this.src)})`, this.options);
if(this.options.failOnError) {
throw e;
}
}
});
return this.#queuePromise;
}
// Factory to return from cache if available
static create(src, options = {}) {
let img = new Image(src, options);
// use resolved options for this
if(!img.options.useCache) {
return img;
}
let key = img.getInMemoryCacheKey();
let cached = memCache.get(key, !options.transformOnRequest && !Util.isRequested(options.generatedVia));
if(cached) {
return cached;
}
memCache.add(key, img);
return img;
}
/* `statsSync` doesnt generate any files, but will tell you where
* the asynchronously generated files will end up! This is useful
* in synchronous-only template environments where you need the
* image URLs synchronously but cant rely on the files being in
* the correct location yet.
*
* `options.dryRun` is still asynchronous but also doesnt generate
* any files.
*/
statsSync() {
if(this.isRemoteUrl) {
throw new Error("`statsSync` is not supported with remote sources. Use `statsByDimensionsSync(src, width, height, options)` instead.");
}
let dimensions = getImageSize(this.src);
return this.getFullStats({
width: dimensions.width,
height: dimensions.height,
format: dimensions.type,
});
}
static statsSync(src, opts) {
if(typeof src === "string" && Util.isRemoteUrl(src)) {
throw new Error("`statsSync` is not supported with remote sources. Use `statsByDimensionsSync(src, width, height, options)` instead.");
}
let img = Image.create(src, opts);
return img.statsSync();
}
statsByDimensionsSync(width, height) {
let dimensions = {
width,
height,
guess: true
};
return this.getFullStats(dimensions);
}
static statsByDimensionsSync(src, width, height, opts) {
let img = Image.create(src, opts);
return img.statsByDimensionsSync(width, height);
}
}
module.exports = Image;