next-image-export-optimizer
Version:
Optimizes all static images for Next.js static HTML export functionality
189 lines (188 loc) • 9.4 kB
JavaScript
"use client";
import React, { useMemo, useState } from "react";
import Image from "next/legacy/image";
const splitFilePath = ({ filePath }) => {
const filenameWithExtension = filePath.split("\\").pop()?.split("/").pop() || "";
const filePathWithoutFilename = filePath.split(filenameWithExtension).shift();
const fileExtension = filePath.split(".").pop();
const filenameWithoutExtension = filenameWithExtension.substring(0, filenameWithExtension.lastIndexOf(".")) || filenameWithExtension;
return {
path: filePathWithoutFilename,
filename: filenameWithoutExtension,
extension: fileExtension || "",
};
};
const generateImageURL = (src, width, basePath, isRemoteImage = false) => {
const { filename, path, extension } = splitFilePath({ filePath: src });
const useWebp = process.env.nextImageExportOptimizer_storePicturesInWEBP != undefined
? process.env.nextImageExportOptimizer_storePicturesInWEBP == "true"
: true;
if (!["JPG", "JPEG", "WEBP", "PNG", "AVIF", "GIF"].includes(extension.toUpperCase())) {
// The images has an unsupported extension
// We will return the src
return src;
}
// If the images are stored as WEBP by the package, then we should change
// the extension to WEBP to load them correctly
let processedExtension = extension;
if (useWebp &&
["JPG", "JPEG", "PNG", "GIF"].includes(extension.toUpperCase())) {
processedExtension = "WEBP";
}
let correctedPath = path;
const lastChar = correctedPath?.substr(-1); // Selects the last character
if (lastChar != "/") {
// If the last character is not a slash
correctedPath = correctedPath + "/"; // Append a slash to it.
}
const isStaticImage = src.includes("_next/static/media");
if (!isStaticImage && basePath) {
correctedPath = basePath + "/" + correctedPath;
}
const exportFolderName = process.env.nextImageExportOptimizer_exportFolderName ||
"nextImageExportOptimizer";
const basePathPrefixForStaticImages = basePath ? basePath + "/" : "";
let generatedImageURL = `${isStaticImage ? basePathPrefixForStaticImages : correctedPath}${exportFolderName}/${filename}-opt-${width}.${processedExtension.toUpperCase()}`;
// if the generatedImageURL is not starting with a slash, then we add one as long as it is not a remote image
if (!isRemoteImage && generatedImageURL.charAt(0) !== "/") {
generatedImageURL = "/" + generatedImageURL;
}
return generatedImageURL;
};
// Credits to https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
// This is a hash function that is used to generate a hash from the image URL
const hashAlgorithm = (str, seed = 0) => {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
function urlToFilename(url) {
try {
const parsedUrl = new URL(url);
const extension = parsedUrl.pathname.split(".").pop();
if (extension) {
return hashAlgorithm(url).toString().concat(".", extension);
}
}
catch (error) {
console.error("Error parsing URL", url, error);
}
return hashAlgorithm(url).toString();
}
const imageURLForRemoteImage = ({ src, width, basePath, }) => {
const encodedSrc = urlToFilename(src);
return generateImageURL(encodedSrc, width, basePath, true);
};
const optimizedLoader = ({ src, width, basePath, }) => {
const isStaticImage = typeof src === "object";
const _src = isStaticImage ? src.src : src;
const originalImageWidth = (isStaticImage && src.width) || undefined;
// if it is a static image, we can use the width of the original image to generate a reduced srcset that returns
// the same image url for widths that are larger than the original image
if (isStaticImage && originalImageWidth && width > originalImageWidth) {
const deviceSizes = process.env.__NEXT_IMAGE_OPTS?.deviceSizes || [
640, 750, 828, 1080, 1200, 1920, 2048, 3840,
];
const imageSizes = process.env.__NEXT_IMAGE_OPTS?.imageSizes || [
16, 32, 48, 64, 96, 128, 256, 384,
];
const allSizes = [...deviceSizes, ...imageSizes];
// only use the width if it is smaller or equal to the next size in the allSizes array
let nextLargestSize = null;
for (let i = 0; i < allSizes.length; i++) {
if (Number(allSizes[i]) >= originalImageWidth &&
(nextLargestSize === null || Number(allSizes[i]) < nextLargestSize)) {
nextLargestSize = Number(allSizes[i]);
}
}
if (nextLargestSize !== null) {
return generateImageURL(_src, nextLargestSize, basePath);
}
}
// Check if the image is a remote image (starts with http or https)
if (_src.startsWith("http")) {
return imageURLForRemoteImage({ src: _src, width, basePath });
}
return generateImageURL(_src, width, basePath);
};
const fallbackLoader = ({ src, basePath, }) => {
let _src = typeof src === "object" ? src.src : src;
const isRemoteImage = _src.startsWith("http");
// if the _src does not start with a slash, then we add one as long as it is not a remote image
if (!isRemoteImage && _src.charAt(0) !== "/") {
_src = "/" + _src;
}
if (basePath) {
_src = basePath + _src;
}
return _src;
};
function ExportedImage({ src, priority = false, loading, lazyRoot = null, lazyBoundary = "200px", className, width, height, objectFit, objectPosition, layout, onLoadingComplete, unoptimized, alt = "", placeholder = "blur", basePath = "", blurDataURL, onError, ...rest }) {
const [imageError, setImageError] = useState(false);
const automaticallyCalculatedBlurDataURL = useMemo(() => {
if (blurDataURL) {
// use the user provided blurDataURL if present
return blurDataURL;
}
// check if the src is specified as a local file -> then it is an object
const isStaticImage = typeof src === "object";
let _src = isStaticImage ? src.src : src;
if (unoptimized === true) {
// return the src image when unoptimized
if (!isStaticImage) {
if (basePath && _src.startsWith("/")) {
_src = basePath + _src;
}
if (basePath && !_src.startsWith("/")) {
_src = basePath + "/" + _src;
}
}
return _src;
}
// Check if the image is a remote image (starts with http or https)
if (_src.startsWith("http")) {
return imageURLForRemoteImage({ src: _src, width: 10, basePath });
}
// otherwise use the generated image of 10px width as a blurDataURL
return generateImageURL(_src, 10, basePath);
}, [blurDataURL, src, unoptimized, basePath]);
const isStaticImage = typeof src === "object";
let _src = isStaticImage ? src.src : src;
if (!isStaticImage) {
if (basePath && _src.startsWith("/")) {
_src = basePath + _src;
}
if (basePath && !_src.startsWith("/")) {
_src = basePath + "/" + _src;
}
}
return (React.createElement(Image, { ...rest, alt: alt, ...(typeof src === "object" &&
src.width &&
!(layout === "fill") && { width: src.width }), ...(typeof src === "object" &&
src.height &&
!(layout === "fill") && { height: src.height }), ...(width && { width }), ...(height && { height }), ...(layout && { layout }), ...(loading && { loading }), ...(lazyRoot && { lazyRoot }), ...(lazyBoundary && { lazyBoundary }), ...(className && { className }), ...(objectFit && { objectFit }), ...(objectPosition && { objectPosition }), ...(onLoadingComplete && { onLoadingComplete }), ...(placeholder && { placeholder }), ...(unoptimized && { unoptimized }), ...(priority && { priority }), ...(imageError && { unoptimized: true }), loader: imageError || unoptimized === true
? () => fallbackLoader({ src, basePath })
: (e) => optimizedLoader({ src, width: e.width, basePath }), blurDataURL: automaticallyCalculatedBlurDataURL, onError: (error) => {
setImageError(true);
// execute the onError function if provided
onError && onError(error);
}, onLoadingComplete: (result) => {
// for some configurations, the onError handler is not called on an error occurrence
// so we need to check if the image is loaded correctly
if (result.naturalWidth === 0) {
// Broken image, fall back to unoptimized (meaning the original image src)
setImageError(true);
}
// execute the onLoadingComplete callback if present
onLoadingComplete && onLoadingComplete(result);
}, src: _src }));
}
export default ExportedImage;