From 278aa5e9dd3e629fd70f8195ea0e2bfadf18bcf0 Mon Sep 17 00:00:00 2001 From: Saneef Ansari Date: Sat, 2 Dec 2023 12:27:46 +0530 Subject: [PATCH] feat: Adds support for SVG files --- lib/img2picture.js | 36 +- lib/utils/object.js | 4 +- tests/attributes.js | 9 + tests/fixtures/images/Ghostscript_Tiger.svg | 726 ++++++++++++++++++++ tests/ignores.js | 15 +- tests/snapshots/attributes.js.md | 6 + tests/snapshots/attributes.js.snap | Bin 907 -> 933 bytes tests/snapshots/svgs.js.md | 23 + tests/snapshots/svgs.js.snap | Bin 0 -> 652 bytes tests/svgs.js | 76 ++ 10 files changed, 869 insertions(+), 26 deletions(-) create mode 100644 tests/fixtures/images/Ghostscript_Tiger.svg create mode 100644 tests/snapshots/svgs.js.md create mode 100644 tests/snapshots/svgs.js.snap create mode 100644 tests/svgs.js 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 e74a845aff68a408d14895733199d7498907f844..131747f7b59217ac63e3198235ecb86c7bffc08b 100644 GIT binary patch literal 933 zcmV;W16uq+RzV zFOg@QknUI^A+crE1!c>E1#iGR@CG~xb8&4s@innF}e_HE!Jn*|6 zk6^dkY3NHYawpx+kUBhxNw>qIBz6emlOeG!8sGtG;1TUxh{X;gVxETjlQo!!hlJHz zJI(Q?^g!}%15KH?nkQy?H)Gz;qPNSUceCi-vgo}mdao>cKa1Wki*7aZ)SG4Tt#&qm zx}9~{%14m5LL_=fm~FM1&C%E@6?05_!)h_5M}?TmY!*|Q&0;FESxjX%i>b^NV(Qc@ z#MBNIVrqxeiurCx2GwFpj|wrB*(|0qo5fUSvzW?k7E_rk#MG%*h^ZYa#MBO@V$xuc zVgvaRc4;`UEgv6FEaVa=($APpLPyDK*49E}>T*vEET*xLN9e%g>61WAhLwiKOhW-E z#n@!lry&nbc9Rc@%d%!*5UdlZM8rvGz`-$&c8iTJ@{8hrR z`~1up&Ds-z}lu-vagiK<{@+ zn0pY=u&#Fgk;t3`4@H6_5fAvgK>VFSWWeI0Sez&FHNs^h{gRPpfNT;@bU}r657I>$3@`ZlG?%yZA65|1-2zTntFL&9_Q|7Uu=COv2{we{VuTW7qAW3 z25bYimGy8E7uKeL5Qdmg;&|>?K~qPiLrPSR-^0npdMz#l>}7j zi$%55QFQ;xW|_EpufW9h)nza-=SJz~R3kH|PU+URv*`AG;GUmIdDR-1Vg^=T^X8>! z^L*T$p9gu(7?)zkMP6~{rD*ef*evM)sySU}Vy?W*%5GHi7%@xKXF%w;`;n) zrpivP$_I|%p&x;M1p1L1egyyKJPcgq`{d689KS6JN6`ok0FVGk03-nNQULi61|iSQ HM=$^Y9BIpc literal 907 zcmV;619bdBRzVe&YT!gzx(sPb|{9EVKiE}s_2C?&= ze*TUbVF3@b>-Fy|J_($xqb7~+Q4UFU=MtZ zTzg>1%P@AQ_8=h+%TjC)Xq=@E25g#O3z86d*g#`4f`FzD#rZsj_>&scM+v6g-nKc} z;2wbAJpeQ2P4mdi@8--~W%O24^iCPQ(-gg1M(;L7@0HPeP0>BGO5JRV@Ab<8#O=Jp zW;Fu8%|#;nm|C!Bn&Sy<6mx=yNxPWbqeV<%R*NajYB7abEv7K5#T4ciF=gs4VoHY= zF{Q(C#eA1wuU$;;(ITcWtHl&%wV1-J7E_qjVhVGMm@@SiF{ML`n9`w9OcHtpHb4*~ zmqeZg1GGPdz{O51pAnr#j*wTaEd&$dGCv%-G0 zanxbJj|pX<;{=G(4j83z2zY)rh$B#f2~2z(fsXKf5qgigvo%5i#SPs1QRCi$=AP!B z=APzW!94*Q3r@PDEC`^);A;_onJ~1UpBjTK&CZ2_Gsu3# z78i+pg>Y#UCxAr;07Y&&eFaY{kjdHEz{`UgFTZGB>bCIuaZ&ScNo`??Hk^m1HMV6e zo_Tp*9_Q|sUpfN7v2{$g{jRa?mu8!0n`WD4TT>6mfzjno7A%ehQ-Zny8sfl$k0L;v zIJ(SEZ~GaJaEkIbVKKT~4;UhfT~0uee!i%FF^cS8={yrx@6?#MzPb!1R@`XZoOWbZ z)M?z>ei_|g4BWF5sj6D#Qq;hzYF@n*Xly%cF)44Wk#K$)|3 zmd{m{8Gl$bu{sS*QY;u1q007B}sCEDV 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 0000000000000000000000000000000000000000..ef42caaec7260319323f66cd7be7a7335643083f GIT binary patch literal 652 zcmV;70(1RARzVHCH*piQ zB#!KtX59_(J&?wM8%Mq$v7@G)=o*7G5Ylpq9sTl`-}^`Zm-Fc)5y6T2{FNccgq9D;AbE}--tvga2i?bB`B1~4R}NfG z?syAQuQrjpjplBPX0OrgwP^Mm&3=pKUZc6!qS^J9#d|HP-A9eAS#D%^mb!Xb`Jg|Y z0qJ_)tR!pwD%m8r_DjQdei_orFGE`SWym(aj0hLs~W0kZsM~Toz9wph3?x`!gK54 mgtAyOn~r{57){?gmyf0^?9yUfZ5-UIjrj@1-RmCO3;+Oe<3Z{G literal 0 HcmV?d00001 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); +});