From 114f4db3be6df2055918cc35cdc62f0edf645861 Mon Sep 17 00:00:00 2001 From: Niels <47059882+Niels-IO@users.noreply.github.com> Date: Fri, 1 Dec 2023 21:56:46 +0100 Subject: [PATCH 1/2] Optimize SVG images --- example/pages/index.js | 56 ++++++++++++- .../next-image-export-optimizer-hashes.json | 5 +- example/public/images/vercel.svg | 4 + example/remoteOptimizedImages.js | 1 + example/src/ExportedImage.tsx | 26 +++--- example/test/e2e/imageSizeTest.spec.mjs | 80 ++++++++++++++++++- example/test/e2e/unoptimizedTest.spec.mjs | 19 +++++ playwright-basePath.config.js | 1 + playwright.config.js | 1 + src/optimizeImages.ts | 44 +++++++--- 10 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 example/public/images/vercel.svg diff --git a/example/pages/index.js b/example/pages/index.js index db08cef..449da40 100644 --- a/example/pages/index.js +++ b/example/pages/index.js @@ -3,6 +3,7 @@ import ExportedImageLegacy from "../src/legacy/ExportedImage"; // import ExportedImageLegacy from "next-image-export-optimizer/legacy/ExportedImage"; // import ExportedImage from "next-image-export-optimizer"; import ExportedImage from "../src/ExportedImage"; +import vercelLogo from "../public/vercel.svg"; import styles from "../styles/Home.module.css"; import testPictureStatic from "../public/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.jpg"; @@ -210,13 +211,60 @@ export default function Home() { basePath={basePath} /> - {/* */} + style={{ objectFit: "contain" }} + basePath={basePath} + /> + + +
+ +
+ ); diff --git a/example/public/images/next-image-export-optimizer-hashes.json b/example/public/images/next-image-export-optimizer-hashes.json index 803cd31..7e15915 100644 --- a/example/public/images/next-image-export-optimizer-hashes.json +++ b/example/public/images/next-image-export-optimizer-hashes.json @@ -6,9 +6,12 @@ "subfolder/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg": "eCN-BKBwsstx+QGPEOXBodpbU1DUWgpf1DhnuXeoG8w=", "subfolder/subfolder2/ollie-barker-jones-K52HVSPVvKI-unsplash.jpg": "AWW05lFZl-Qt-8gAeuDu3bnMm9m6lyTUH80yXpit0Og=", "/transparentImage.png": "XH3+oYb10y7DOx5iqAbzi64ChyebfrxLgJNJolxjpPw=", + "/vercel.svg": "uHBQYU7ivU446A23G-w9S1uTnW6fa5zBPO0CPLFtOZw=", "/animated.c00e0188.png": "1u18UQP7SYClRgh+v8TnzU82uY96MSKW3bxNat8HYOo=", "/chris-zhang-Jq8-3Bmh1pQ-unsplash_small.0fa13b23.jpg": "w8j9FhKoGEyo52uc8zEMt7XCeMUZsGCQjEjnWzkams4=", "/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.921260e0.jpg": "F4KuoW3LZSTHxrqqDmnFlIcTPSHwtJTKuB2djCCjEnw=", "/chris-zhang-Jq8-3Bmh1pQ-unsplash_static_asset.921260e0.jpg": "TKroa8LFPMSjLnRq67yFY71qpejsNlpVOV7TkeASSGA=", - "/reactapp.dev_images_nextImageExportOptimizer_christopher-gower-m_HRfLhgABo-unsplash-opt-2048.WEBP": "YtPJvMpqxVbOf++q4Q3h-aYZjThU0rfwOPjZYOtvefg=" + "/vercel.1be6ab75.svg": "Q4YZswLklKEkYQU7cv14dsGdoMhqNLL-+Jrf-8wLDzg=", + "/reactapp.dev_images_nextImageExportOptimizer_christopher-gower-m_HRfLhgABo-unsplash-opt-2048.WEBP": "YtPJvMpqxVbOf++q4Q3h-aYZjThU0rfwOPjZYOtvefg=", + "/reactapp.dev_nextjs.svg": "BxmC8w9UBrcnK9wKWpUxw0KmLw8CijHhUCOLcdsYZhI=" } \ No newline at end of file diff --git a/example/public/images/vercel.svg b/example/public/images/vercel.svg new file mode 100644 index 0000000..fbf0e25 --- /dev/null +++ b/example/public/images/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/example/remoteOptimizedImages.js b/example/remoteOptimizedImages.js index b12aa5a..a6425d1 100644 --- a/example/remoteOptimizedImages.js +++ b/example/remoteOptimizedImages.js @@ -1,5 +1,6 @@ module.exports = [ "https://reactapp.dev/images/nextImageExportOptimizer/christopher-gower-m_HRfLhgABo-unsplash-opt-2048.WEBP", + "https://reactapp.dev/nextjs.svg", // 'https://example.com/image1.jpg', // 'https://example.com/image2.jpg', // 'https://example.com/image3.jpg', diff --git a/example/src/ExportedImage.tsx b/example/src/ExportedImage.tsx index 1ac8dda..d9ed7de 100644 --- a/example/src/ExportedImage.tsx +++ b/example/src/ExportedImage.tsx @@ -33,7 +33,7 @@ const generateImageURL = ( : true; if ( - !["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes( + !["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF", "SVG"].includes( extension.toUpperCase() ) ) { @@ -47,9 +47,11 @@ const generateImageURL = ( if ( useWebp && - ["JPG", "JPEG", "PNG", "GIF"].includes(extension.toUpperCase()) + ["JPG", "JPEG", "PNG", "GIF", "SVG"].includes(extension.toUpperCase()) ) { processedExtension = "WEBP"; + } else if (extension.toUpperCase() === "SVG") { + processedExtension = "PNG"; } let correctedPath = path; @@ -236,17 +238,12 @@ const ExportedImage = forwardRef( return generateImageURL(_src, 10, basePath); }, [blurDataURL, src, unoptimized, basePath]); - // check if the src is a SVG image -> then we should not use the blurDataURL and use unoptimized - const isSVG = - typeof src === "object" ? src.src.endsWith(".svg") : src.endsWith(".svg"); - const [blurComplete, setBlurComplete] = useState(false); // Currently, we have to handle the blurDataURL ourselves as the new Image component // is expecting a base64 encoded string, but the generated blurDataURL is a normal URL const blurStyle = placeholder === "blur" && - !isSVG && automaticallyCalculatedBlurDataURL && automaticallyCalculatedBlurDataURL.startsWith("/") && !blurComplete @@ -260,10 +257,20 @@ const ExportedImage = forwardRef( const isStaticImage = typeof src === "object"; let _src = isStaticImage ? src.src : src; - if (basePath && !isStaticImage && _src.startsWith("/")) { + if ( + basePath && + !isStaticImage && + _src.startsWith("/") && + !_src.startsWith("http") + ) { _src = basePath + _src; } - if (basePath && !isStaticImage && !_src.startsWith("/")) { + if ( + basePath && + !isStaticImage && + !_src.startsWith("/") && + !_src.startsWith("http") + ) { _src = basePath + "/" + _src; } @@ -285,7 +292,6 @@ const ExportedImage = forwardRef( })} {...(unoptimized && { unoptimized })} {...(priority && { priority })} - {...(isSVG && { unoptimized: true })} style={{ ...style, ...blurStyle }} loader={ imageError || unoptimized === true diff --git a/example/test/e2e/imageSizeTest.spec.mjs b/example/test/e2e/imageSizeTest.spec.mjs index bba4d39..1516ec0 100644 --- a/example/test/e2e/imageSizeTest.spec.mjs +++ b/example/test/e2e/imageSizeTest.spec.mjs @@ -241,6 +241,65 @@ const correctSrcAnimatedGIFImage = { imagesWebP ? "WEBP" : "GIF" }`, }; + +const correctSrcSVGImage = { + 640: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 750: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 777: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 828: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1080: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1200: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1920: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 2048: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 3840: `http://localhost:8080${basePath}/images/nextImageExportOptimizer/vercel-opt-384.${ + imagesWebP ? "WEBP" : "PNG" + }`, +}; +const correctSrcSVGImageRemote = { + 640: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-640.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 750: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-750.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 777: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-777.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 828: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-828.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1080: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-1080.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1200: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-1200.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 1920: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-1920.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 2048: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-2048.${ + imagesWebP ? "WEBP" : "PNG" + }`, + 3840: `http://localhost:8080${basePath}/nextImageExportOptimizer/reactapp.dev_nextjs-opt-3840.${ + imagesWebP ? "WEBP" : "PNG" + }`, +}; function generateSrcset(widths, correctSrc) { const baseURL = "http://localhost:8080"; return widths @@ -299,7 +358,26 @@ for (let index = 0; index < widths.length; index++) { // check the number of images on the page const images = await page.$$("img"); - expect(images.length).toBe(10); + expect(images.length).toBe(15); + + // check the SVG images + const imgSVG = await page.locator("#test_image_svg"); + await imgSVG.click(); + + const imageSVG = await getImageById(page, "test_image_svg"); + + expect(imageSVG.currentSrc).toBe(correctSrcSVGImage[width.toString()]); + expect(imageSVG.naturalWidth).toBe(384); + + const imgSVG_remote = await page.locator("#test_image_svg_remote"); + await imgSVG_remote.click(); + + const imageSVG_remote = await getImageById(page, "test_image_svg_remote"); + + expect(imageSVG_remote.currentSrc).toBe( + correctSrcSVGImageRemote[width.toString()] + ); + expect(imageSVG_remote.naturalWidth).toBe(width); }); test("should check the image size for the appdir", async ({ page }) => { await page.goto(`${basePath}/appdir`, { diff --git a/example/test/e2e/unoptimizedTest.spec.mjs b/example/test/e2e/unoptimizedTest.spec.mjs index e6aede2..2ba4bd6 100644 --- a/example/test/e2e/unoptimizedTest.spec.mjs +++ b/example/test/e2e/unoptimizedTest.spec.mjs @@ -36,5 +36,24 @@ test.describe(`Test unoptimized image prop`, () => { expect(image_legacy.currentSrc).toBe( `http://localhost:8080${basePath}/images/chris-zhang-Jq8-3Bmh1pQ-unsplash.jpg` ); + + const svg = await page.locator("#test_image_unoptimized_svg"); + await svg.click(); + + const svg_image = await getImageById(page, "test_image_unoptimized_svg"); + + expect(svg_image.currentSrc).toBe( + `http://localhost:8080${basePath}/_next/static/media/vercel.1be6ab75.svg` + ); + + const svg_remote = await page.locator("#test_image_unoptimized_svg_remote"); + await svg_remote.click(); + + const svg_image_remote = await getImageById( + page, + "test_image_unoptimized_svg_remote" + ); + // TODO: Could be improved by first using a local copy of this remote image + expect(svg_image_remote.currentSrc).toBe(`https://reactapp.dev/nextjs.svg`); }); }); diff --git a/playwright-basePath.config.js b/playwright-basePath.config.js index 3e88992..35573a2 100644 --- a/playwright-basePath.config.js +++ b/playwright-basePath.config.js @@ -26,6 +26,7 @@ const config = { use: { baseURL: "http://localhost:8080/", }, + retries: 2, testDir: "example/test/e2e", projects: [ { diff --git a/playwright.config.js b/playwright.config.js index 78db4c8..29b433c 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -25,6 +25,7 @@ const config = { use: { baseURL: "http://localhost:8080/", }, + retries: 2, testDir: "example/test/e2e", projects: [ { diff --git a/src/optimizeImages.ts b/src/optimizeImages.ts index 2e863e0..7f2d139 100755 --- a/src/optimizeImages.ts +++ b/src/optimizeImages.ts @@ -9,9 +9,9 @@ const getAllFilesAsObject = require("./utils/getAllFilesAsObject"); const getHash = require("./utils/getHash"); const getRemoteImageURLs = require("./utils/getRemoteImageURLs"); -const fs = require("fs"); -const sharp = require("sharp"); -const path = require("path"); +import fs from "fs"; +import sharp from "sharp"; +import path from "path"; const loadConfig = require("next/dist/server/config").default; @@ -78,6 +78,16 @@ const nextImageExportOptimizer = async function () { let storePicturesInWEBP = true; let blurSize: number[] = []; let exportFolderName = "nextImageExportOptimizer"; + const imageExtensions = [ + ".PNG", + ".GIF", + ".JPG", + ".JPEG", + ".AVIF", + ".WEBP", + ".SVG", + ]; + const { remoteImageFilenames, remoteImageURLs } = await getRemoteImageURLs( nextConfigFolder, folderPathForRemoteImages @@ -217,7 +227,7 @@ const nextImageExportOptimizer = async function () { const hashFilePath = `${imageFolderPath}/next-image-export-optimizer-hashes.json`; try { let rawData = fs.readFileSync(hashFilePath); - imageHashes = JSON.parse(rawData); + imageHashes = JSON.parse(rawData.toString()); } catch (e) { // No image hashes yet } @@ -276,7 +286,7 @@ const nextImageExportOptimizer = async function () { if (filenameSplit.length === 1) return false; const extension = filenameSplit.pop()!.toUpperCase(); // Only include file with image extensions - return ["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes(extension); + return imageExtensions.includes(`.${extension}`); } ); console.log( @@ -348,6 +358,11 @@ const nextImageExportOptimizer = async function () { const filename = path.parse(file).name; if (storePicturesInWEBP) { extension = "WEBP"; + } else { + // if the image is an SVG, we turned it into a PNG and need to change the extension + if (extension === "SVG") { + extension = "PNG"; + } } const isStaticImage = basePath === staticImageFolderPath; @@ -388,6 +403,7 @@ const nextImageExportOptimizer = async function () { const transformer = sharp(imageBuffer, { animated: true, limitInputPixels: false, // disable pixel limit + density: 600, }); transformer.rotate(); @@ -400,6 +416,7 @@ const nextImageExportOptimizer = async function () { let nextLargestSize = -1; for (let i = 0; i < widths.length; i++) { if ( + metaWidth && Number(widths[i]) >= metaWidth && (nextLargestSize === -1 || Number(widths[i]) < nextLargestSize) ) { @@ -438,7 +455,6 @@ const nextImageExportOptimizer = async function () { continue; } - const resize = metaWidth && metaWidth > width; if (resize) { transformer.resize(width); @@ -461,7 +477,7 @@ const nextImageExportOptimizer = async function () { } else if (extension === "JPEG" || extension === "JPG") { transformer.jpeg({ quality }); } else if (extension === "GIF") { - transformer.gif({ quality }); + transformer.gif(); } // Write the optimized image to the file system @@ -500,10 +516,14 @@ const nextImageExportOptimizer = async function () { console.log("Copy optimized images to build folder..."); for (let index = 0; index < allGeneratedImages.length; index++) { const filePath = allGeneratedImages[index]; - const fileInBuildFolder = path.join( - exportFolderPath, - filePath.split("public").pop() - ); + const splitPath = filePath.split("public"); + const lastElement = splitPath.pop(); + + if (lastElement === undefined) { + throw new Error(`Failed to split filePath: ${filePath}`); + } + + const fileInBuildFolder = path.join(exportFolderPath, lastElement); // Create the folder for the optimized images in the build directory if it does not exists ensureDirectoryExists(fileInBuildFolder); @@ -560,8 +580,6 @@ const nextImageExportOptimizer = async function () { return results; } - const imageExtensions = [".PNG", ".GIF", ".JPG", ".JPEG", ".AVIF", ".WEBP"]; - const imagePaths: string[] = []; for (const subfolderPath of optimizedImagesFolders) { const paths = findImageFiles(subfolderPath, imageExtensions); From 581aa957d5fa973a5e41190735155208cc3d2aa1 Mon Sep 17 00:00:00 2001 From: Niels <47059882+Niels-IO@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:13:28 +0100 Subject: [PATCH 2/2] Try to fix flakey test --- example/test/e2e/imageSizeTest.spec.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/example/test/e2e/imageSizeTest.spec.mjs b/example/test/e2e/imageSizeTest.spec.mjs index 1516ec0..12c892a 100644 --- a/example/test/e2e/imageSizeTest.spec.mjs +++ b/example/test/e2e/imageSizeTest.spec.mjs @@ -556,6 +556,7 @@ for (let index = 0; index < widths.length; index++) { await img.click(); const image = await getImageById(page, "test_image_static_fixed"); + await expect(image).toHaveScreenshot(); expect(image.currentSrc).toBe( `http://localhost:8080${basePath}/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.921260e0-opt-384.${ imagesWebP ? "WEBP" : "JPG" @@ -567,6 +568,7 @@ for (let index = 0; index < widths.length; index++) { page, "test_image_static_fixed_future" ); + await expect(image_future).toHaveScreenshot(); expect(image_future.currentSrc).toBe( `http://localhost:8080${basePath}/nextImageExportOptimizer/chris-zhang-Jq8-3Bmh1pQ-unsplash_static.921260e0-opt-384.${ imagesWebP ? "WEBP" : "JPG"