Skip to content

Commit

Permalink
feat: Adds support for SVG files
Browse files Browse the repository at this point in the history
  • Loading branch information
saneef committed Dec 2, 2023
1 parent 47063d1 commit 278aa5e
Show file tree
Hide file tree
Showing 10 changed files with 869 additions and 26 deletions.
36 changes: 26 additions & 10 deletions lib/img2picture.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -36,15 +36,17 @@ const { isAllowedExtension } = require("./utils/file");
* @property {object} [sharpPngOptions]
* @property {object} [sharpJpegOptions]
* @property {object} [sharpAvifOptions]
* @property {boolean | "size"} [svgShortCircuit]
* @property {"br"} [svgCompressionSize]
*/

/** @type {Img2PictureOptions} */
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,
Expand All @@ -59,6 +61,7 @@ const defaultOptions = {
sharpPngOptions: {},
sharpJpegOptions: {},
sharpAvifOptions: {},
svgShortCircuit: "size",
};

/**
Expand Down Expand Up @@ -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 ${objectToAttributes(pictureAttrs)}>${picture
.map((d) => {
const [[tag, obj]] = Object.entries(d);
return `<${tag} ${objectToAttributes(obj)}></${tag}>`;
})
// When `svgShortCircuit=true` only `<img>` will be there.
if (tagsObject.img) {
return tagObjectToHTML(tagsObject);
}

return `<picture ${objectToHTMLAttributes(pictureAttrs)}>${tagsObject.picture
.map(tagObjectToHTML)
.join("")}</picture>`;
}

function tagObjectToHTML(object) {
const [[tag, obj]] = Object.entries(object);
return `<${tag} ${objectToHTMLAttributes(obj)}></${tag}>`;
}

/**
* Generate responsive image files, and return a `<picture>` element populated
* with generated file paths and sizes.
Expand All @@ -158,6 +169,8 @@ async function generateImage(attrs, options) {
sharpWebpOptions,
urlPath,
widthStep = 150,
svgShortCircuit,
svgCompressionSize,
} = options;
const { src, "data-img2picture-widths": imgAttrWidths } = attrs;

Expand All @@ -183,6 +196,9 @@ async function generateImage(attrs, options) {
sharpAvifOptions,
dryRun,
cacheOptions,
// @ts-ignore
svgShortCircuit,
svgCompressionSize,
});

return generatePicture(metadata, attrs, options);
Expand Down
4 changes: 2 additions & 2 deletions lib/utils/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -37,6 +37,6 @@ function removeObjectProperties(obj, props = []) {
}

module.exports = {
objectToAttributes,
objectToHTMLAttributes,
removeObjectProperties,
};
9 changes: 9 additions & 0 deletions tests/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ test('Retain alt=""', async (t) => {
t.snapshot(result);
});

test('Add alt="", if missing', async (t) => {
const input = '<img src="/images/shapes.png">';
const outputPath = "file.html";

const transformer = img2picture(baseConfig);
const result = await transformer(input, outputPath);
t.snapshot(result);
});

test("Don't hoist 'class' from <img> on <picture> when 'hoistImgClass=false", async (t) => {
const input = '<img class="w-full" src="/images/shapes.png" alt="Shapes">';
const outputPath = "file.html";
Expand Down
726 changes: 726 additions & 0 deletions tests/fixtures/images/Ghostscript_Tiger.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 1 addition & 14 deletions tests/ignores.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,7 @@ test("Don't optimize <img> tag with data-img2picture-ignore", async (t) => {
t.is(result, output);
});

test("Don't optimize SVG", async (t) => {
const input = '<img src="/images/nothing.svg" alt="Nothing">';
const outputPath = "file.html";
const output =
'<html><head></head><body><img src="/images/nothing.svg" alt="Nothing"></body></html>';
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 = '<img src="/images/nothing.gif" alt="Nothing">';
const outputPath = "file.html";
const output =
Expand Down
6 changes: 6 additions & 0 deletions tests/snapshots/attributes.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Generated by [AVA](https://avajs.dev).

## Retain alt=""

> Snapshot 1
'<html><head></head><body><picture><source type="image/avif" srcset="/images/shapes-150w.avif 150w, /images/shapes-300w.avif 300w, /images/shapes-450w.avif 450w, /images/shapes-600w.avif 600w, /images/shapes-750w.avif 750w, /images/shapes-900w.avif 900w, /images/shapes-1050w.avif 1050w, /images/shapes-1200w.avif 1200w, /images/shapes-1350w.avif 1350w" sizes="100vw"><source type="image/webp" srcset="/images/shapes-150w.webp 150w, /images/shapes-300w.webp 300w, /images/shapes-450w.webp 450w, /images/shapes-600w.webp 600w, /images/shapes-750w.webp 750w, /images/shapes-900w.webp 900w, /images/shapes-1050w.webp 1050w, /images/shapes-1200w.webp 1200w, /images/shapes-1350w.webp 1350w" sizes="100vw"><source type="image/jpeg" srcset="/images/shapes-150w.jpeg 150w, /images/shapes-300w.jpeg 300w, /images/shapes-450w.jpeg 450w, /images/shapes-600w.jpeg 600w, /images/shapes-750w.jpeg 750w, /images/shapes-900w.jpeg 900w, /images/shapes-1050w.jpeg 1050w, /images/shapes-1200w.jpeg 1200w, /images/shapes-1350w.jpeg 1350w" sizes="100vw"><img alt="" loading="lazy" decoding="async" src="/images/shapes-150w.jpeg" width="1350" height="1350"></picture></body></html>'

## Add alt="", if missing

> Snapshot 1
'<html><head></head><body><picture><source type="image/avif" srcset="/images/shapes-150w.avif 150w, /images/shapes-300w.avif 300w, /images/shapes-450w.avif 450w, /images/shapes-600w.avif 600w, /images/shapes-750w.avif 750w, /images/shapes-900w.avif 900w, /images/shapes-1050w.avif 1050w, /images/shapes-1200w.avif 1200w, /images/shapes-1350w.avif 1350w" sizes="100vw"><source type="image/webp" srcset="/images/shapes-150w.webp 150w, /images/shapes-300w.webp 300w, /images/shapes-450w.webp 450w, /images/shapes-600w.webp 600w, /images/shapes-750w.webp 750w, /images/shapes-900w.webp 900w, /images/shapes-1050w.webp 1050w, /images/shapes-1200w.webp 1200w, /images/shapes-1350w.webp 1350w" sizes="100vw"><source type="image/jpeg" srcset="/images/shapes-150w.jpeg 150w, /images/shapes-300w.jpeg 300w, /images/shapes-450w.jpeg 450w, /images/shapes-600w.jpeg 600w, /images/shapes-750w.jpeg 750w, /images/shapes-900w.jpeg 900w, /images/shapes-1050w.jpeg 1050w, /images/shapes-1200w.jpeg 1200w, /images/shapes-1350w.jpeg 1350w" sizes="100vw"><img alt="" loading="lazy" decoding="async" src="/images/shapes-150w.jpeg" width="1350" height="1350"></picture></body></html>'
Expand Down
Binary file modified tests/snapshots/attributes.js.snap
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/snapshots/svgs.js.md
Original file line number Diff line number Diff line change
@@ -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 <picture>

> Snapshot 1
'<html><head></head><body><picture><source type="image/avif" srcset="/images/Ghostscript_Tiger-150w.avif 150w, /images/Ghostscript_Tiger-300w.avif 300w, /images/Ghostscript_Tiger-450w.avif 450w, /images/Ghostscript_Tiger-600w.avif 600w, /images/Ghostscript_Tiger-750w.avif 750w, /images/Ghostscript_Tiger-900w.avif 900w, /images/Ghostscript_Tiger-1050w.avif 1050w, /images/Ghostscript_Tiger-1200w.avif 1200w, /images/Ghostscript_Tiger-1350w.avif 1350w" sizes="100vw"><source type="image/webp" srcset="/images/Ghostscript_Tiger-150w.webp 150w, /images/Ghostscript_Tiger-300w.webp 300w, /images/Ghostscript_Tiger-450w.webp 450w, /images/Ghostscript_Tiger-600w.webp 600w, /images/Ghostscript_Tiger-900w.svg 900w" sizes="100vw"><source type="image/svg+xml" srcset="/images/Ghostscript_Tiger-900w.svg 900w" sizes="100vw"><source type="image/jpeg" srcset="/images/Ghostscript_Tiger-150w.jpeg 150w, /images/Ghostscript_Tiger-300w.jpeg 300w, /images/Ghostscript_Tiger-450w.jpeg 450w, /images/Ghostscript_Tiger-900w.svg 900w" sizes="100vw"><img alt="Ghostscript Tiger" loading="lazy" decoding="async" src="/images/Ghostscript_Tiger-150w.jpeg" width="900" height="900"></picture></body></html>'

## Optimizes SVG image to other formats when `svgShortCircuit: false`

> Snapshot 1
'<html><head></head><body><picture><source type="image/svg+xml" srcset="/images/Ghostscript_Tiger-900w.svg 900w" sizes="100vw"><source type="image/avif" srcset="/images/Ghostscript_Tiger-150w.avif 150w, /images/Ghostscript_Tiger-300w.avif 300w, /images/Ghostscript_Tiger-450w.avif 450w, /images/Ghostscript_Tiger-600w.avif 600w, /images/Ghostscript_Tiger-750w.avif 750w, /images/Ghostscript_Tiger-900w.avif 900w, /images/Ghostscript_Tiger-1050w.avif 1050w, /images/Ghostscript_Tiger-1200w.avif 1200w, /images/Ghostscript_Tiger-1350w.avif 1350w" sizes="100vw"><source type="image/webp" srcset="/images/Ghostscript_Tiger-150w.webp 150w, /images/Ghostscript_Tiger-300w.webp 300w, /images/Ghostscript_Tiger-450w.webp 450w, /images/Ghostscript_Tiger-600w.webp 600w, /images/Ghostscript_Tiger-750w.webp 750w, /images/Ghostscript_Tiger-900w.webp 900w, /images/Ghostscript_Tiger-1050w.webp 1050w, /images/Ghostscript_Tiger-1200w.webp 1200w, /images/Ghostscript_Tiger-1350w.webp 1350w" sizes="100vw"><source type="image/jpeg" srcset="/images/Ghostscript_Tiger-150w.jpeg 150w, /images/Ghostscript_Tiger-300w.jpeg 300w, /images/Ghostscript_Tiger-450w.jpeg 450w, /images/Ghostscript_Tiger-600w.jpeg 600w, /images/Ghostscript_Tiger-750w.jpeg 750w, /images/Ghostscript_Tiger-900w.jpeg 900w, /images/Ghostscript_Tiger-1050w.jpeg 1050w, /images/Ghostscript_Tiger-1200w.jpeg 1200w, /images/Ghostscript_Tiger-1350w.jpeg 1350w" sizes="100vw"><img alt="Ghostscript Tiger" loading="lazy" decoding="async" src="/images/Ghostscript_Tiger-150w.jpeg" width="1350" height="1350"></picture></body></html>'

## Process SVG image and skip to other formats when `svgShortCircuit: true`

> Snapshot 1
'<html><head></head><body><img alt="Ghostscript Tiger" loading="lazy" decoding="async" src="/images/Ghostscript_Tiger-900w.svg" width="900" height="900"></body></html>'
Binary file added tests/snapshots/svgs.js.snap
Binary file not shown.
76 changes: 76 additions & 0 deletions tests/svgs.js
Original file line number Diff line number Diff line change
@@ -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 <picture>", async (t) => {
const input =
'<img src="/images/Ghostscript_Tiger.svg" alt="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 =
'<img src="/images/Ghostscript_Tiger.svg" alt="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 =
'<img src="/images/Ghostscript_Tiger.svg" alt="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);
});

0 comments on commit 278aa5e

Please sign in to comment.