diff --git a/img.js b/img.js
index aaaf264..dd1e129 100644
--- a/img.js
+++ b/img.js
@@ -2,8 +2,9 @@ const path = require("path");
const fs = require("fs");
const fsp = fs.promises;
const { URL } = require("url");
-const shorthash = require("short-hash");
+const { createHash } = require("crypto");
const {default: PQueue} = require("p-queue");
+const base64url = require("base64url");
const getImageSize = require("image-size");
const sharp = require("sharp");
const debug = require("debug")("EleventyImg");
@@ -123,8 +124,52 @@ function getValidWidths(originalWidth, widths = [], allowUpscale = false) {
return filtered.sort((a, b) => a - b);
}
+let imgHashCache = {};
+function getHash(src, imgOptions={}, length=10) {
+ if(src in imgHashCache) return imgHashCache[src];
+
+ if(typeof src === "string" && isFullUrl(src) && !("remoteAssetContent" in imgOptions)) {
+ throw new Error("When using getHash with URLs, imgOptions.remoteAssetContent should be set to the content of the remote asset.");
+ }
+
+ const hash = createHash("sha256");
+
+ let opts = Object.assign({
+ "userOptions": {},
+ "sharpOptions": {},
+ "sharpWebpOptions": {},
+ "sharpPngOptions": {},
+ "sharpJpegOptions": {},
+ "sharpAvifOptions": {},
+ "remoteAssetContent": {}
+ }, imgOptions);
+
+ opts = {
+ userOptions: opts.userOptions,
+ sharpOptions: opts.sharpOptions,
+ sharpWebpOptions: opts.sharpWebpOptions,
+ sharpPngOptions: opts.sharpPngOptions,
+ sharpJpegOptions: opts.sharpJpegOptions,
+ sharpAvifOptions: opts.sharpAvifOptions,
+ remoteAssetContent: opts.remoteAssetContent
+ };
+
+ if(fs.existsSync(src)) {
+ const fileContent = fs.readFileSync(src);
+ hash.update(fileContent);
+ } else {
+ hash.update(src);
+ }
+
+ hash.update(JSON.stringify(opts));
+ imgHashCache[src] = base64url.encode(hash.digest()).substring(0, length);
+
+ return imgHashCache[src];
+}
+
function getFilename(src, width, format, options = {}) {
- let id = shorthash(src);
+ let id = getHash(src, options);
+
if (typeof options.filenameFormat === "function") {
let filename = options.filenameFormat(id, src, width, format, options);
// if options.filenameFormat returns falsy, use fallback filename
@@ -147,7 +192,8 @@ function getStats(src, format, urlPath, width, height, options = {}) {
let outputExtension = options.extensions[format] || format;
if(options.urlFormat && typeof options.urlFormat === "function") {
- let id = shorthash(src);
+ let id = getHash(src, options);
+
url = options.urlFormat({
id,
src,
@@ -285,6 +331,16 @@ async function resizeImage(src, options = {}) {
let fullStats = getFullStats(src, metadata, options);
for(let outputFormat in fullStats) {
for(let stat of fullStats[outputFormat]) {
+ if(options.useCache && fs.existsSync(stat.outputPath)){
+ stat.size = fs.statSync(stat.outputPath).size;
+ if(options.dryRun) {
+ stat.buffer = fs.readFileSync(src);
+ }
+
+ outputFilePromises.push(Promise.resolve(stat));
+ continue;
+ }
+
let sharpInstance = sharpImage.clone();
if(stat.width < metadata.width || (options.svgAllowUpscale && metadata.format === "svg")) {
let resizeOptions = {
@@ -398,6 +454,8 @@ function queueImage(src, opts) {
// eleventy-cache-assets 2.0.3 and below
input = await assetCache.fetch(cacheOptions);
}
+
+ options.remoteAssetContent = input; // Only set for remote assets with URL
} else {
input = src;
}
@@ -428,11 +486,19 @@ Object.defineProperty(module.exports, "concurrency", {
* the correct location yet.
*/
function statsSync(src, opts) {
+ if(typeof src === "string" && isFullUrl(src) && !("remoteAssetContent" in opts)) {
+ throw new Error("When using statsSync or statsByDimensionsSync with URLs, options.remoteAssetContent should be set to the content of the remote asset.");
+ }
+
let dimensions = getImageSize(src);
return getFullStats(src, dimensions, opts);
}
function statsByDimensionsSync(src, width, height, opts) {
+ if(typeof src === "string" && isFullUrl(src) && !("remoteAssetContent" in opts)) {
+ throw new Error("When using statsSync or statsByDimensionsSync with URLs, options.remoteAssetContent should be set to the content of the remote asset.");
+ }
+
let dimensions = { width, height, guess: true };
return getFullStats(src, dimensions, opts);
}
@@ -441,6 +507,7 @@ module.exports.statsSync = statsSync;
module.exports.statsByDimensionsSync = statsByDimensionsSync;
module.exports.getFormats = getFormatsArray;
module.exports.getWidths = getValidWidths;
+module.exports.getHash = getHash;
const generateHTML = require("./generate-html");
module.exports.generateHTML = generateHTML;
diff --git a/package.json b/package.json
index c6aa62e..3ea92f8 100644
--- a/package.json
+++ b/package.json
@@ -36,10 +36,10 @@
"dependencies": {
"@11ty/eleventy-cache-assets": "^2.3.0",
"debug": "^4.3.2",
+ "base64url": "^3.0.1",
"image-size": "^1.0.0",
"p-queue": "^6.6.2",
"sharp": "^0.29.0",
- "short-hash": "^1.0.0"
},
"devDependencies": {
"ava": "^3.15.0",
diff --git a/test/test-markup.js b/test/test-markup.js
index 08f7b73..73902aa 100644
--- a/test/test-markup.js
+++ b/test/test-markup.js
@@ -10,7 +10,7 @@ test("Image markup (defaults)", async t => {
t.is(generateHTML(results, {
alt: ""
- }), ``);
+ }), ``);
});
test("Image service", async t => {
@@ -23,7 +23,8 @@ test("Image service", async t => {
widths: [600], // 260-440 in layout
urlFormat: function({ width, format }) {
return `${serviceApiDomain}/api/image/?url=${encodeURIComponent(screenshotUrl)}&width=${width}&format=${format}`;
- }
+ },
+ remoteAssetContent: 'remote asset content'
};
let results = eleventyImage.statsByDimensionsSync(screenshotUrl, 1440, 900, options);
@@ -45,13 +46,13 @@ test("Image object (defaults)", async t => {
{
"source": {
type: "image/webp",
- srcset: "/img/97854483-1280.webp 1280w",
+ srcset: "/img/Bok0Qhed6a-1280.webp 1280w",
}
},
{
"img": {
alt: "",
- src: "/img/97854483-1280.jpeg",
+ src: "/img/Bok0Qhed6a-1280.jpeg",
width: 1280,
height: 853,
}
@@ -70,9 +71,9 @@ test("Image markup (two widths)", async t => {
alt: "",
sizes: "100vw",
}), [``].join(""));
});
@@ -95,7 +96,7 @@ test("Image markup (two formats)", async t => {
t.is(generateHTML(results, {
alt: ""
- }), ``);
+ }), ``);
});
test("Image markup (one format)", async t => {
@@ -107,7 +108,7 @@ test("Image markup (one format)", async t => {
t.is(generateHTML(results, {
alt: "",
sizes: "100vw"
- }), ``);
+ }), ``);
});
test("Image markup (auto format)", async t => {
@@ -119,7 +120,7 @@ test("Image markup (auto format)", async t => {
t.is(generateHTML(results, {
alt: "",
sizes: "100vw"
- }), ``);
+ }), ``);
});
test("Image markup (one format, two widths)", async t => {
@@ -132,7 +133,7 @@ test("Image markup (one format, two widths)", async t => {
t.is(generateHTML(results, {
alt: "",
sizes: "100vw"
- }), ``);
+ }), ``);
});
test("Image markup (throws on invalid object)", async t => {
@@ -160,8 +161,8 @@ test("Image markup (defaults, inlined)", async t => {
}, {
whitespaceMode: "block"
}), ``);
});
@@ -175,7 +176,7 @@ test("svgShortCircuit and generateHTML: Issue #48", async t => {
let html = eleventyImage.generateHTML(stats, {
alt: "Tiger",
});
- t.is(html, ``);
+ t.is(html, ``);
});
test("Filter out empty format arrays", async t => {
diff --git a/test/test.js b/test/test.js
index 8fea378..7d6590d 100644
--- a/test/test.js
+++ b/test/test.js
@@ -170,7 +170,7 @@ test("Use 'auto' format as original", async t => {
t.is(stats.auto, undefined);
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -181,7 +181,7 @@ test("Try to use a width larger than original", async t => {
outputDir: "./test/img/"
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -192,7 +192,7 @@ test("Try to use a width larger than original (two sizes)", async t => {
outputDir: "./test/img/"
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -203,7 +203,7 @@ test("Try to use a width larger than original (with a null in there)", async t =
outputDir: "./test/img/"
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -214,7 +214,7 @@ test("Just falsy width", async t => {
outputDir: "./test/img/"
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -226,7 +226,7 @@ test("Use exact same width as original", async t => {
});
t.is(stats.jpeg.length, 1);
// breaking change in 0.5: always use width in filename
- t.is(stats.jpeg[0].outputPath, path.join("test/img/97854483-1280.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/Bok0Qhed6a-1280.jpeg"));
t.is(stats.jpeg[0].width, 1280);
});
@@ -237,7 +237,7 @@ test("Try to use a width larger than original (statsSync)", t => {
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].url, "/img/97854483-1280.jpeg");
+ t.is(stats.jpeg[0].url, "/img/Bok0Qhed6a-1280.jpeg");
t.is(stats.jpeg[0].width, 1280);
});
@@ -248,7 +248,7 @@ test("Use exact same width as original (statsSync)", t => {
});
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].url, "/img/97854483-1280.jpeg"); // no width in filename
+ t.is(stats.jpeg[0].url, "/img/Bok0Qhed6a-1280.jpeg"); // no width in filename
t.is(stats.jpeg[0].width, 1280);
});
@@ -270,19 +270,20 @@ test("Use custom function to define file names", async (t) => {
});
t.is(stats.jpeg.length, 2);
- t.is(stats.jpeg[0].outputPath, path.join("test/img/bio-2017-97854483-600.jpeg"));
- t.is(stats.jpeg[0].url, "/img/bio-2017-97854483-600.jpeg");
- t.is(stats.jpeg[0].srcset, "/img/bio-2017-97854483-600.jpeg 600w");
+ t.is(stats.jpeg[0].outputPath, path.join("test/img/bio-2017-Bok0Qhed6a-600.jpeg"));
+ t.is(stats.jpeg[0].url, "/img/bio-2017-Bok0Qhed6a-600.jpeg");
+ t.is(stats.jpeg[0].srcset, "/img/bio-2017-Bok0Qhed6a-600.jpeg 600w");
t.is(stats.jpeg[0].width, 600);
- t.is(stats.jpeg[1].outputPath, path.join("test/img/bio-2017-97854483-1280.jpeg"));
- t.is(stats.jpeg[1].url, "/img/bio-2017-97854483-1280.jpeg");
- t.is(stats.jpeg[1].srcset, "/img/bio-2017-97854483-1280.jpeg 1280w");
+ t.is(stats.jpeg[1].outputPath, path.join("test/img/bio-2017-Bok0Qhed6a-1280.jpeg"));
+ t.is(stats.jpeg[1].url, "/img/bio-2017-Bok0Qhed6a-1280.jpeg");
+ t.is(stats.jpeg[1].srcset, "/img/bio-2017-Bok0Qhed6a-1280.jpeg 1280w");
t.is(stats.jpeg[1].width, 1280);
});
test("Unavatar test", t => {
let stats = eleventyImage.statsByDimensionsSync("https://unavatar.now.sh/twitter/zachleat?fallback=false", 400, 400, {
- widths: [75]
+ widths: [75],
+ remoteAssetContent: 'remote asset content'
});
t.is(stats.webp.length, 1);
@@ -386,7 +387,7 @@ test("Sync by dimension with jpeg input (wrong dimensions, supplied are smaller
// this won’t upscale so it will miss out on higher resolution images but there won’t be any broken image URLs in the output
t.is(stats.jpeg.length, 1);
- t.is(stats.jpeg[0].outputPath, path.join("img/97854483-164.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("img/Bok0Qhed6a-164.jpeg"));
});
test("Sync by dimension with jpeg input (wrong dimensions, supplied are larger than real)", t => {
@@ -396,8 +397,8 @@ test("Sync by dimension with jpeg input (wrong dimensions, supplied are larger t
});
t.is(stats.jpeg.length, 2);
- t.is(stats.jpeg[0].outputPath, path.join("img/97854483-164.jpeg"));
- t.is(stats.jpeg[1].outputPath, path.join("img/97854483-328.jpeg"));
+ t.is(stats.jpeg[0].outputPath, path.join("img/Bok0Qhed6a-164.jpeg"));
+ t.is(stats.jpeg[1].outputPath, path.join("img/Bok0Qhed6a-328.jpeg"));
});
test("Keep a cache, reuse with same file names and options", async t => {
@@ -545,14 +546,14 @@ test("Using `jpg` in formats Issue #64", async t => {
t.deepEqual(stats, {
jpeg: [
{
- filename: '97854483-1280.jpeg',
+ filename: 'Bok0Qhed6a-1280.jpeg',
format: 'jpeg',
height: 853,
- outputPath: path.join('img/97854483-1280.jpeg'),
+ outputPath: path.join('img/Bok0Qhed6a-1280.jpeg'),
size: 276231,
sourceType: "image/jpeg",
- srcset: '/img/97854483-1280.jpeg 1280w',
- url: '/img/97854483-1280.jpeg',
+ srcset: '/img/Bok0Qhed6a-1280.jpeg 1280w',
+ url: '/img/Bok0Qhed6a-1280.jpeg',
width: 1280,
},
]