@11ty/eleventy-img
Version:
Low level utility to perform build-time image transformations.
931 lines (769 loc) • 30.4 kB
JavaScript
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 you’re working from a mime type input, let’s 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 it’s 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 we’ll 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` doesn’t 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 can’t rely on the files being in
* the correct location yet.
*
* `options.dryRun` is still asynchronous but also doesn’t 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;