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"