diff --git a/lib/img2picture.js b/lib/img2picture.js index 3efb305..668a0f1 100644 --- a/lib/img2picture.js +++ b/lib/img2picture.js @@ -5,7 +5,7 @@ const path = require("path"); const debug = require("debug")("img2picture"); const { removeObjectProperties, - objectToAttributes, + objectToHTMLAttributes, } = require("./utils/object"); const { parseStringToNumbers } = require("./utils/number"); const { isRemoteUrl, getPathFromUrl } = require("./utils/url"); @@ -36,6 +36,8 @@ const { isAllowedExtension } = require("./utils/file"); * @property {object} [sharpPngOptions] * @property {object} [sharpJpegOptions] * @property {object} [sharpAvifOptions] + * @property {boolean | "size"} [svgShortCircuit] + * @property {"br"} [svgCompressionSize] */ /** @type {Img2PictureOptions} */ @@ -43,8 +45,8 @@ const defaultOptions = { eleventyInputDir: ".", imagesOutputDir: "_site", urlPath: "", - extensions: ["jpg", "png", "jpeg"], - formats: ["avif", "webp", "jpeg"], + extensions: ["jpg", "png", "jpeg", "svg"], + formats: ["avif", "webp", "svg", "jpeg"], sizes: "100vw", minWidth: 150, maxWidth: 1500, @@ -59,6 +61,7 @@ const defaultOptions = { sharpPngOptions: {}, sharpJpegOptions: {}, sharpAvifOptions: {}, + svgShortCircuit: "size", }; /** @@ -122,17 +125,25 @@ function generatePicture(metadata, attrs, options) { decoding: attrs.decoding || "async", }; - // @ts-ignore - const { picture } = Image.generateObject(metadata, imgAttrs); + const tagsObject = /** @type {object} */ ( + Image.generateObject(metadata, imgAttrs) + ); - return `${picture - .map((d) => { - const [[tag, obj]] = Object.entries(d); - return `<${tag} ${objectToAttributes(obj)}>`; - }) + // When `svgShortCircuit=true` only `` will be there. + if (tagsObject.img) { + return tagObjectToHTML(tagsObject); + } + + return `${tagsObject.picture + .map(tagObjectToHTML) .join("")}`; } +function tagObjectToHTML(object) { + const [[tag, obj]] = Object.entries(object); + return `<${tag} ${objectToHTMLAttributes(obj)}>`; +} + /** * Generate responsive image files, and return a `` element populated * with generated file paths and sizes. @@ -158,6 +169,8 @@ async function generateImage(attrs, options) { sharpWebpOptions, urlPath, widthStep = 150, + svgShortCircuit, + svgCompressionSize, } = options; const { src, "data-img2picture-widths": imgAttrWidths } = attrs; @@ -183,6 +196,9 @@ async function generateImage(attrs, options) { sharpAvifOptions, dryRun, cacheOptions, + // @ts-ignore + svgShortCircuit, + svgCompressionSize, }); return generatePicture(metadata, attrs, options); diff --git a/lib/utils/object.js b/lib/utils/object.js index 2c29d4b..6b426f8 100644 --- a/lib/utils/object.js +++ b/lib/utils/object.js @@ -7,7 +7,7 @@ const { escapeAttribute } = require("entities/lib/escape.js"); * @param {object} obj The object * @returns {string} */ -function objectToAttributes(obj) { +function objectToHTMLAttributes(obj) { return Object.keys(obj).reduce((acc, key) => { // Ignore empty class attribute let value = obj[key]; @@ -37,6 +37,6 @@ function removeObjectProperties(obj, props = []) { } module.exports = { - objectToAttributes, + objectToHTMLAttributes, removeObjectProperties, }; diff --git a/tests/attributes.js b/tests/attributes.js index c9fe893..35a7248 100644 --- a/tests/attributes.js +++ b/tests/attributes.js @@ -24,6 +24,15 @@ test('Retain alt=""', async (t) => { t.snapshot(result); }); +test('Add alt="", if missing', async (t) => { + const input = ''; + const outputPath = "file.html"; + + const transformer = img2picture(baseConfig); + const result = await transformer(input, outputPath); + t.snapshot(result); +}); + test("Don't hoist 'class' from on when 'hoistImgClass=false", async (t) => { const input = 'Shapes'; const outputPath = "file.html"; diff --git a/tests/fixtures/images/Ghostscript_Tiger.svg b/tests/fixtures/images/Ghostscript_Tiger.svg new file mode 100644 index 0000000..d0c3b9e --- /dev/null +++ b/tests/fixtures/images/Ghostscript_Tiger.svg @@ -0,0 +1,726 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/ignores.js b/tests/ignores.js index 2cd3d5a..0f25f02 100644 --- a/tests/ignores.js +++ b/tests/ignores.js @@ -39,20 +39,7 @@ test("Don't optimize tag with data-img2picture-ignore", async (t) => { t.is(result, output); }); -test("Don't optimize SVG", async (t) => { - const input = 'Nothing'; - const outputPath = "file.html"; - const output = - 'Nothing'; - const transformer = img2picture({ - eleventyInputDir: sourcePath, - imagesOutputDir: outputBase, - }); - const result = await transformer(input, outputPath); - t.is(result, output); -}); - -test("Don't optimize GIF", async (t) => { +test("Don't optimize file extensions not listed in `options.extensions`, here GIF", async (t) => { const input = 'Nothing'; const outputPath = "file.html"; const output = diff --git a/tests/snapshots/attributes.js.md b/tests/snapshots/attributes.js.md index dcb44df..935475a 100644 --- a/tests/snapshots/attributes.js.md +++ b/tests/snapshots/attributes.js.md @@ -6,6 +6,12 @@ Generated by [AVA](https://avajs.dev). ## Retain alt="" +> Snapshot 1 + + '' + +## Add alt="", if missing + > Snapshot 1 '' diff --git a/tests/snapshots/attributes.js.snap b/tests/snapshots/attributes.js.snap index e74a845..131747f 100644 Binary files a/tests/snapshots/attributes.js.snap and b/tests/snapshots/attributes.js.snap differ diff --git a/tests/snapshots/svgs.js.md b/tests/snapshots/svgs.js.md new file mode 100644 index 0000000..09db3eb --- /dev/null +++ b/tests/snapshots/svgs.js.md @@ -0,0 +1,23 @@ +# Snapshot report for `tests/svgs.js` + +The actual snapshot is saved in `svgs.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## Optimizes SVG image, generates AVIF, WebP, SVG, JPEG formats, and puts them in + +> Snapshot 1 + + 'Ghostscript Tiger' + +## Optimizes SVG image to other formats when `svgShortCircuit: false` + +> Snapshot 1 + + 'Ghostscript Tiger' + +## Process SVG image and skip to other formats when `svgShortCircuit: true` + +> Snapshot 1 + + 'Ghostscript Tiger' diff --git a/tests/snapshots/svgs.js.snap b/tests/snapshots/svgs.js.snap new file mode 100644 index 0000000..ef42caa Binary files /dev/null and b/tests/snapshots/svgs.js.snap differ diff --git a/tests/svgs.js b/tests/svgs.js new file mode 100644 index 0000000..1fcebc4 --- /dev/null +++ b/tests/svgs.js @@ -0,0 +1,76 @@ +const path = require("path"); +const test = require("ava"); +const { rimraf } = require("rimraf"); +const imageSize = require("image-size"); +const { existsSync } = require("node:fs"); + +const img2picture = require("../lib/img2picture.js"); +const { filenameFormat } = require("./utils.js"); + +const sourcePath = path.join("tests/fixtures"); +const outputBase = path.join("tests/output/svgs"); + +test.before("Cleanup Output Images", async () => rimraf(outputBase)); +test.after.always("Cleanup Output Images", async () => rimraf(outputBase)); + +const baseOptions = { + eleventyInputDir: sourcePath, + imagesOutputDir: outputBase, + urlPath: "/images/", + filenameFormat, + formats: ["avif", "webp", "svg", "jpeg"], +}; + +test("Optimizes SVG image, generates AVIF, WebP, SVG, JPEG formats, and puts them in ", async (t) => { + const input = + 'Ghostscript Tiger'; + const outputPath = "file.html"; + const imagesOutputDir = path.join(outputBase, "default"); + const transformer = img2picture({ + ...baseOptions, + imagesOutputDir, + }); + const result = await transformer(input, outputPath); + t.snapshot(result); + + const svgFilePath = path.join(imagesOutputDir, "Ghostscript_Tiger-900w.svg"); + t.true(existsSync(svgFilePath)); + + const { width: smallestImgWidth } = imageSize( + path.join(imagesOutputDir, "Ghostscript_Tiger-150w.jpeg"), + ); + const { width: largestImgWidth } = imageSize( + path.join(imagesOutputDir, "Ghostscript_Tiger-1350w.jpeg"), + ); + + t.is(smallestImgWidth, 150); + t.is(largestImgWidth, 1350); +}); + +test("Optimizes SVG image to other formats when `svgShortCircuit: false`", async (t) => { + const input = + 'Ghostscript Tiger'; + const outputPath = "file.html"; + + const transformer = img2picture({ + ...baseOptions, + svgShortCircuit: false, + imagesOutputDir: path.join(outputBase, "svgShortCircuit-false"), + }); + const result = await transformer(input, outputPath); + t.snapshot(result); +}); + +test("Process SVG image and skip to other formats when `svgShortCircuit: true`", async (t) => { + const input = + 'Ghostscript Tiger'; + const outputPath = "file.html"; + + const transformer = img2picture({ + ...baseOptions, + svgShortCircuit: true, + imagesOutputDir: path.join(outputBase, "svgShortCircuit-true"), + }); + const result = await transformer(input, outputPath); + t.snapshot(result); +});