This commit is contained in:
lovebird 2025-03-17 16:23:02 +01:00
parent c1c5d84a9f
commit 32b7d4bfed
151 changed files with 68 additions and 23124 deletions

View File

@ -2,6 +2,6 @@
export default new Map([
["src/content/infopages/community.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fcommunity.mdx&astroContentModuleFlag=true")],
["src/content/infopages/contact.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fcontact.mdx&astroContentModuleFlag=true")],
["src/content/infopages/resources.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fresources.mdx&astroContentModuleFlag=true")],
["src/content/infopages/software.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fsoftware.mdx&astroContentModuleFlag=true")]]);
["src/content/infopages/software.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fsoftware.mdx&astroContentModuleFlag=true")],
["src/content/infopages/resources.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Finfopages%2Fresources.mdx&astroContentModuleFlag=true")]]);

File diff suppressed because one or more lines are too long

View File

@ -1,55 +0,0 @@
components->Img -> renderImg -> getImage(sizes, format ...) -> element
getImage -> getProcessedImage(args[0] : src:local path)
args[0]
{
src: "https://assets.osr-plastic.org//products/sheetpress/cassandra-edczmax-rc2/media/gallery/perspective.jpg",
type: "Img",
sizes: "(min-width: 800px) 800px, 800vw",
format: "avif",
breakpoints: undefined,
placeholder: "blurred",
artDirectives: [
],
fallbackFormat: "avif",
includeSourceFormat: false,
formatOptions: {
jpg: {
quality: 80,
},
png: {
quality: 80,
},
webp: {
quality: 50,
},
},
transformConfigs: {
},
}
->
{
uuid: "B16DBD24",
images: [
{
sources: [
{
src: "/_astro/perspective@1320w.e183f84c.avif",
format: "avif",
srcset: "/_astro/perspective@320w.3435f7a3.avif 320w, /_astro/perspective@653w.e53b07bb.avif 653w, /_astro/perspective@920w.a7628e27.avif 920w, /_astro/perspective@1120w.90dfde67.avif 1120w, /_astro/perspective@1253w.084f9de3.avif 1253w, /_astro/perspective@1320w.e183f84c.avif 1320w",
},
],
sizes: {
width: 1320,
height: 1980,
},
fallback: "data:image/avif;base64,
imagesizes: "(min-width: 800px) 800px, 800vw",
},
],
}

View File

@ -29,9 +29,7 @@ export default async function ({
if (imagesData.has(hash)) {
return imagesData.get(hash);
}
const start = performance.now();
// const start = performance.now();
const { path, base, rest, image, imageWidth, imageHeight, imageFormat } =
await getProcessedImage(src, transformConfigs);
@ -63,7 +61,7 @@ export default async function ({
rest
),
async () => {
await delay(100);
await delay(250);
return await getArtDirectedImages(
artDirectives,
placeholder,
@ -93,11 +91,9 @@ export default async function ({
imagesData.set(hash, returnObject);
const end = performance.now();
//const end = performance.now();
console.log(
`Responsive Image sets generated for ${type} at ${args[0].src} in ${end - start}ms`
);
//console.log( `Responsive Image sets generated for ${type} at ${args[0].src} in ${end - start}ms`);
return returnObject;
} catch (error) {

View File

@ -1,47 +1,47 @@
// @ts-check
export function getConfigOptions(config, ext, imageWidth) {
const { w, width = w, format = ext, base64, raw, inline, ...rest } = config;
const imageFormat = format === "jpeg" ? "jpg" : format;
const widths = width
? width.split(";").map((w) => parseInt(w))
: [imageWidth];
const extension = format === "jpg" ? "jpeg" : format;
const type = `image/${extension}`;
const options = {
format: imageFormat,
...rest,
};
return {
type,
widths,
options,
extension,
raw: typeof raw === "string",
inline: typeof base64 === "string" || typeof inline === "string",
};
}
export function getAssetPath(base, assetFileNames, ext, width, hash) {
const regexExecArray = /(?<=\[hash:)\d+(?=\])/g.exec(assetFileNames),
hashLength = regexExecArray ? regexExecArray[0] : 8,
extname = `.${ext}`,
name = base;
width = width + "w";
hash = hash.slice(0, hashLength);
const assetPath = assetFileNames
.replace("[name]", name)
.replace("[width]", width)
.replace(regexExecArray ? `[hash:${hashLength}]` : "[hash]", hash)
.replace("[ext]", ext)
.replace("[extname]", extname);
return assetPath;
}
// @ts-check
export function getConfigOptions(config, ext, imageWidth) {
const { w, width = w, format = ext, base64, raw, inline, ...rest } = config;
const imageFormat = format === "jpeg" ? "jpg" : format;
const widths = width
? width.split(";").map((w) => parseInt(w))
: [imageWidth];
const extension = format === "jpg" ? "jpeg" : format;
const type = `image/${extension}`;
const options = {
format: imageFormat,
...rest,
};
return {
type,
widths,
options,
extension,
raw: typeof raw === "string",
inline: typeof base64 === "string" || typeof inline === "string",
};
}
export function getAssetPath(base, assetFileNames, ext, width, hash) {
const regexExecArray = /(?<=\[hash:)\d+(?=\])/g.exec(assetFileNames),
hashLength = regexExecArray ? regexExecArray[0] : 8,
extname = `.${ext}`,
name = base;
width = width + "w";
hash = hash.slice(0, hashLength);
const assetPath = assetFileNames
.replace("[name]", name)
.replace("[width]", width)
.replace(regexExecArray ? `[hash:${hashLength}]` : "[hash]", hash)
.replace("[ext]", ext)
.replace("[extname]", extname);
return assetPath;
}

View File

@ -1,16 +1,14 @@
// @ts-check
import { store } from "../plugin/index.js";
import { getCachedBuffer } from "../plugin/utils/cache.js";
export async function middleware(request, response) {
const imageObject = store.get(request.url);
if (imageObject) {
const { hash, type, image, buffer } = imageObject;
response.setHeader("Content-Type", type);
response.setHeader("Cache-Control", "no-cache");
return buffer || (await getCachedBuffer(hash, image));
}
}
// @ts-check
import { store } from "../plugin/index.js";
import { getCachedBuffer } from "../plugin/utils/cache.js";
export async function middleware(request, response) {
const imageObject = store.get(request.url);
if (imageObject) {
const { hash, type, image, buffer } = imageObject;
response.setHeader("Content-Type", type);
response.setHeader("Cache-Control", "no-cache");
return buffer || (await getCachedBuffer(hash, image));
}
}

View File

@ -1,4 +0,0 @@
*.test.ts
test-fixtures
astroViteConfigs.js
vitest.config.ts

View File

@ -1,39 +0,0 @@
# **Astro ImageTools**
**Astro ImageTools** is a collection of tools for optimizing images, background images, and generating responsive images for the **Astro JS** framework.
## Features
Below is a short list of features that **Astro ImageTools** offers. For more information, please see component-specific or API-specific documentation.
- ✅ **Regular Image Optimization** (`<img>` and `<picture>`)
- ✅ **Background Image Optimization**
- ✅ **Responsive Images**
- ✅ **Simple and intuitive Art Direction API**
- ✅ **Lazy Loading**
- ✅ **Programmatic APIs**
- ✅ **Asynchronous Decoding**
- ✅ **Unique Breakpoints Calculation**
- ✅ **Preloading for urgent images**
- ✅ **SVG Tracing and Posterization**
- ✅ **100% Scoped CSS**
- ✅ **Four kind of Layouts: `constrained`, `fixed`, `fullWidth` & `fill`**
- ✅ **Three kind of Placeholder Images: `blurred`, `dominantColor` & `tracedSVG`**
- ✅ **Long list of supported Image Formats**
- ✅ **Long List of supported Configuration Options**
- ✅ **Supports Remote Images and Data URIs too**
- ✅ **Support for _`sharp`less_ Environments**
- ✅ **Both Memory-based and FS-based Caching for better Performance**
- ✅ **Respects to _Semantics of HTML_ as much as possible**
## Getting Started
To get started with **Astro ImageTools**, first check out the [Installation](https://astro-imagetools-docs.vercel.app/en/installation) documentation for instructions on how to install the `astro-imagetools` package.
If you are looking for the available components and APIs, please check out the [Components and APIs](https://astro-imagetools-docs.vercel.app/en/components-and-apis) documentation.
If you want to view live examples of the components, APIs, layouts, and placeholder images, check out the [Astro ImageTools Demo](https://astro-imagetools-demo.vercel.app/) website.
If you want to report any issues or have found a missing feature, please report it on [GitHub](https://github.com/RafidMuhymin/astro-imagetools/)!
Good luck out there, Astronaut. 🧑‍🚀

View File

@ -1 +0,0 @@
export default function importImage(url: string): Promise<string>;

View File

@ -1,23 +0,0 @@
import load from "../plugin/hooks/load.js";
import { getSrcPath } from "./utils/getSrcPath.js";
import getResolvedSrc from "./utils/getResolvedSrc.js";
export default async function importImage(path) {
try {
const { search, protocol, pathname } = new URL(path);
const { src: id, base } = await getResolvedSrc(
protocol === "data:" ? protocol + pathname : path
);
const src = (await load(id + search, base)).slice(16, -1);
return src;
} catch (error) {
const id = await getSrcPath(path);
const src = (await load(id)).slice(16, -1);
return src;
}
}

View File

@ -1,6 +0,0 @@
export { default as renderImg } from "./renderImg.js";
export { default as renderPicture } from "./renderPicture.js";
export { default as renderBackgroundImage } from "./renderBackgroundImage.js";
export { default as renderBackgroundPicture } from "./renderBackgroundPicture.js";
export { default as importImage } from "./importImage.js";
export { getImageDetails, loadImage } from "./utils/imagetools.js"

View File

@ -1,8 +0,0 @@
import type {
BackgroundImageConfigOptions,
BackgroundImageHTMLData,
} from "../types";
export default function renderBackgroundImage(
config: BackgroundImageConfigOptions
): Promise<BackgroundImageHTMLData>;

View File

@ -1,160 +0,0 @@
// @ts-check
import crypto from "node:crypto";
import getImage from "./utils/getImage.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getContainerElement from "./utils/getContainerElement.js";
export default async function renderBackgroundImage(props) {
const type = "BackgroundImage";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
tag,
content,
preload,
attributes,
placeholder,
breakpoints,
backgroundSize,
backgroundPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
artDirectives,
} = filteredProps;
const {
link: linkAttributes = {},
style: styleAttributes = {},
container: containerAttributes = {},
} = attributes;
const sizes = "";
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
});
const className = `astro-imagetools-background-image-${uuid}`;
const { imagesizes } = images[images.length - 1];
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const backgroundImageStylesArray = images.map(({ media, sources }) => {
const uuid = crypto.randomBytes(4).toString("hex").toUpperCase();
const fallbackUrlCustomVariable = `--astro-imagetools-background-image-fallback-url${uuid}`;
const newSources = {};
sources.forEach(({ src, format, srcset }) => {
const sources = srcset
.split(", ")
.map((source) => [
source.slice(0, source.lastIndexOf(" ")),
source.slice(source.lastIndexOf(" ") + 1, -1),
]);
sources.forEach(([path, width]) => {
if (!newSources[width]) {
newSources[width] = [];
}
newSources[width].push({ src, format, path });
});
});
const widths = Object.keys(newSources)
.map((width) => parseInt(width))
.reverse();
const maxWidth = Math.max(...widths);
const styles = widths
.map((width) => {
const sources = newSources[width];
const styles = sources
.map(
({ format, path }, i) =>
`
${i !== sources.length - 1 ? `.${format} ` : ""}.${className} {
background-repeat: no-repeat;
background-image: url(${path}),
var(${fallbackUrlCustomVariable});
background-size: ${backgroundSize};
background-position: ${backgroundPosition};
}
`
)
.reverse()
.join("");
return width === maxWidth
? styles
: `
@media screen and (max-width: ${width}px) {
${styles}
}
`;
})
.join("");
return {
fallbackUrlCustomVariable,
styles: media
? `
@media ${media} {
${styles}
}
`
: styles,
};
});
const containerStyles = `
.${className} {
position: relative;
${images
.map(({ fallback }, i) => {
const fallbackUrlCustomVariable =
backgroundImageStylesArray[i].fallbackUrlCustomVariable;
return `${fallbackUrlCustomVariable}: url("${encodeURI(fallback)}");`;
})
.join("\n")}
}
`;
const backgroundStyles =
backgroundImageStylesArray.map(({ styles }) => styles).join("\n") +
containerStyles;
const style = getStyleElement({ styleAttributes, backgroundStyles });
const htmlElement = getContainerElement({
tag,
content,
className,
containerAttributes,
});
return { link, style, htmlElement };
}

View File

@ -1,8 +0,0 @@
import type {
BackgroundPictureConfigOptions,
BackgroundPictureHTMLData,
} from "../types";
export default function renderBackgroundPicture(
config: BackgroundPictureConfigOptions
): Promise<BackgroundPictureHTMLData>;

View File

@ -1,127 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js";
import getImgElement from "./utils/getImgElement.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getLayoutStyles from "./utils/getLayoutStyles.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getPictureElement from "./utils/getPictureElement.js";
import getBackgroundStyles from "./utils/getBackgroundStyles.js";
import getContainerElement from "./utils/getContainerElement.js";
export default async function renderBackgroundPicture(props) {
const type = "BackgroundPicture";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
tag,
content,
sizes,
preload,
loading,
decoding,
attributes,
placeholder,
breakpoints,
objectFit,
objectPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
fadeInTransition,
artDirectives,
} = filteredProps;
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
picture: pictureAttributes = {},
container: containerAttributes = {},
} = attributes;
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
});
const className = `astro-imagetools-picture-${uuid}`,
containerClassName = `astro-imagetools-background-picture-${uuid}`;
const { imagesizes } = images[images.length - 1];
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isBackgroundPicture: true, containerClassName }
);
const style = getStyleElement({ styleAttributes, backgroundStyles });
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const layoutStyles = getLayoutStyles({ isBackgroundImage: true });
// Background Images shouldn't convey important information
const alt = "";
const sources = images.flatMap(({ media, sources, sizes, imagesizes }) =>
sources.map(({ format, src, srcset }) =>
src
? getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
})
: `<source
srcset="${srcset}"
sizes="${imagesizes}"
width="${sizes.width}"
height="${sizes.height}"
type="${`image/${format}`}"
${media ? `media="${media}"` : ""}
/>`
)
);
const picture = getPictureElement({
sources,
className,
layoutStyles,
pictureAttributes,
isBackgroundPicture: true,
});
const htmlElement = getContainerElement({
tag,
content: picture + content,
containerAttributes,
isBackgroundPicture: true,
containerClassName,
});
return { link, style, htmlElement };
}

View File

@ -1,5 +0,0 @@
import type { ImgConfigOptions, ImgHTMLData } from "../types";
export default function renderImg(
config: ImgConfigOptions
): Promise<ImgHTMLData>;

View File

@ -1,96 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js";
import getImgElement from "./utils/getImgElement.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getLayoutStyles from "./utils/getLayoutStyles.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getBackgroundStyles from "./utils/getBackgroundStyles.js";
export default async function renderImg(props) {
const type = "Img";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
alt,
sizes,
preload,
loading,
decoding,
attributes,
layout,
breakpoints,
placeholder,
objectFit,
objectPosition,
format,
formatOptions,
} = filteredProps;
const artDirectives = [],
fallbackFormat = format,
fadeInTransition = false,
includeSourceFormat = false;
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
} = attributes;
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
});
const className = `astro-imagetools-img-${uuid}`;
const { imagesizes } = images[images.length - 1];
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isImg: true }
);
const style = getStyleElement({ styleAttributes, backgroundStyles });
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const layoutStyles = getLayoutStyles({ layout })
const sources = images.flatMap(({ sources, sizes, imagesizes }) =>
sources.map(({ src, srcset }) =>
getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
imgClassName: className,
})
)
)
const [img] = sources
return { link, style, img }
}

View File

@ -1,5 +0,0 @@
import type { PictureConfigOptions, PictureHTMLData } from "../types";
export default function renderPicture(
config: PictureConfigOptions
): Promise<PictureHTMLData>;

View File

@ -1,111 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js";
import getImgElement from "./utils/getImgElement.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getLayoutStyles from "./utils/getLayoutStyles.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getPictureElement from "./utils/getPictureElement.js";
import getBackgroundStyles from "./utils/getBackgroundStyles.js";
export default async function renderPicture(props) {
const type = "Picture";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
alt,
sizes,
preload,
loading,
decoding,
attributes,
layout,
placeholder,
breakpoints,
objectFit,
objectPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
fadeInTransition,
artDirectives,
} = filteredProps;
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
picture: pictureAttributes = {},
} = attributes;
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
fallbackFormat,
includeSourceFormat,
formatOptions,
artDirectives,
transformConfigs,
});
const className = `astro-imagetools-picture-${uuid}`;
const { imagesizes } = images[images.length - 1];
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition
);
const style = getStyleElement({ styleAttributes, backgroundStyles });
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const layoutStyles = getLayoutStyles({ layout });
const sources = images.flatMap(({ media, sources, sizes, imagesizes }) =>
sources.map(({ format, src, srcset }) =>
src
? getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
})
: `<source
srcset="${srcset}"
sizes="${imagesizes}"
width="${sizes.width}"
height="${sizes.height}"
type="${`image/${format}`}"
${media ? `media="${media}"` : ""}
/>`
)
);
const picture = getPictureElement({
sources,
className,
layoutStyles,
pictureAttributes,
});
return { link, style, picture };
}

View File

@ -1,38 +0,0 @@
// @ts-check
import fs from "node:fs";
import { extname } from "node:path";
import * as codecs from "@astropub/codecs";
export async function getImageDetails(path, width, height, aspect) {
const extension = extname(path).slice(1);
const imageFormat = extension === "jpeg" ? "jpg" : extension;
const buffer = fs.readFileSync(path);
const decodedImage = await codecs.jpg.decode(buffer);
if (aspect && !width && !height) {
if (!width && !height) {
({ width } = decodedImage);
}
if (width) {
height = width / aspect;
}
if (height) {
width = height * aspect;
}
}
const image = await decodedImage.resize({ width, height });
const { width: imageWidth, height: imageHeight } = image;
return {
image,
imageWidth,
imageHeight,
imageFormat,
};
}

View File

@ -1,137 +0,0 @@
// @ts-check
import getSrcset from "./getSrcset.js";
import getConfigOptions from "./getConfigOptions.js";
import getFallbackImage from "./getFallbackImage.js";
import getProcessedImage from "./getProcessedImage.js";
export default async function getArtDirectedImages(
artDirectives = [],
placeholder,
format,
imagesizes,
breakpoints,
fallbackFormat,
includeSourceFormat,
formatOptions,
rest
) {
const images = await Promise.all(
artDirectives.map(
async ({
src,
media,
sizes: directiveImagesizes,
placeholder: directivePlaceholder,
breakpoints: directiveBreakpoints,
objectFit,
objectPosition,
backgroundSize,
backgroundPosition,
format: directiveFormat,
fallbackFormat: directiveFallbackFormat,
includeSourceFormat: directiveIncludeSourceFormat,
formatOptions: directiveFormatOptions = {},
...configOptions
}) => {
const {
path,
base,
rest: rest2,
image,
imageWidth,
imageHeight,
imageFormat,
} = await getProcessedImage(src, configOptions);
rest2.aspect = `${imageWidth / imageHeight}`;
const calculatedConfigs = getConfigOptions(
imageWidth,
directiveImagesizes || imagesizes,
directiveBreakpoints || breakpoints,
directiveFormat || format,
imageFormat,
directiveFallbackFormat || fallbackFormat,
directiveIncludeSourceFormat || includeSourceFormat
);
const { formats, requiredBreakpoints } = calculatedConfigs;
imagesizes = calculatedConfigs.imagesizes;
const maxWidth = requiredBreakpoints[requiredBreakpoints.length - 1];
const sources = await Promise.all(
formats.map(async (format) => {
const srcset = await getSrcset(
path,
base,
requiredBreakpoints,
format,
{
...rest,
...rest2,
...formatOptions[format],
...directiveFormatOptions[format],
}
);
return {
format,
srcset,
};
})
);
const sizes = {
width: maxWidth,
height: Math.round(maxWidth / rest2.aspect),
};
const object = {
fit: objectFit,
position: objectPosition,
};
const background = {
size: backgroundSize,
position: backgroundPosition,
};
const fallback = await getFallbackImage(
path,
directivePlaceholder || placeholder,
image,
imageFormat,
{ ...formatOptions, ...directiveFormatOptions },
{ ...rest, ...rest2 }
);
const returnValue = {
media,
sources,
sizes,
fallback,
imagesizes,
};
const isBackgroundImage = !!backgroundSize || !!backgroundPosition;
isBackgroundImage
? (returnValue.background = background)
: (returnValue.object = object);
return {
media,
sources,
sizes,
object,
fallback,
imagesizes,
};
}
)
);
return images;
}

View File

@ -1,27 +0,0 @@
// @ts-check
import printWarning from "../../utils/printWarning.js";
export default function getAttributesString({
attributes,
element = "",
excludeArray = [],
}) {
const attributesString = Object.keys(attributes)
.filter((key) => {
if (excludeArray.includes(key)) {
printWarning({
key,
element,
});
return false;
}
return true;
})
.map((key) => `${key}="${attributes[key]}"`)
.join(" ");
return attributesString;
}

View File

@ -1,97 +0,0 @@
// @ts-check
export default function getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isImg = false, isBackgroundPicture = false, containerClassName = "" } = {}
) {
const sourcesWithFallback = images.filter(({ fallback }) => fallback);
if (sourcesWithFallback.length === 0) return "";
const staticStyles = !fadeInTransition
? ""
: `
${
isBackgroundPicture
? `
.${containerClassName} * {
z-index: 1;
position: relative;
}
`
: ""
}
.${className} {
--opacity: 1;
--z-index: 0;
}
${
!isBackgroundPicture
? `
.${className} img {
z-index: 1;
position: relative;
}
`
: ""
}
.${className}::after {
inset: 0;
content: "";
left: 0;
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
transition: opacity ${
typeof fadeInTransition !== "object"
? "1s"
: (() => {
const {
delay = "0s",
duration = "1s",
timingFunction = "ease",
} = fadeInTransition;
return `${duration} ${timingFunction} ${delay}`;
})()
};
opacity: var(--opacity);
z-index: var(--z-index);
}
`;
const dynamicStyles = images
.map(({ media, fallback, object }) => {
const elementSelector = className + (!isImg ? " img" : ""),
backgroundElementSelector =
className + (fadeInTransition ? "::after" : "");
const style = `
.${elementSelector} {
object-fit: ${object?.fit || objectFit};
object-position: ${object?.position || objectPosition};
}
.${backgroundElementSelector} {
background-size: ${object?.fit || objectFit};
background-image: url("${encodeURI(fallback)}");
background-position: ${object?.position || objectPosition};
}
`;
return media ? `@media ${media} { ${style} }` : style;
})
.reverse();
const backgroundStyles = [staticStyles, ...dynamicStyles].join("");
return backgroundStyles;
}

View File

@ -1,75 +0,0 @@
// @ts-check
import printWarning from "../../utils/printWarning.js";
export default function getBreakpoints(breakpoints, imageWidth) {
if (Array.isArray(breakpoints)) {
return breakpoints.sort((a, b) => a - b);
}
const { count, minWidth = 320 } = breakpoints || {};
const maxWidth = (() => {
if (breakpoints?.maxWidth) return breakpoints.maxWidth
if (imageWidth > 3840) {
/*
printWarning({
message:
"The width of the source image is greater than 3840px. The generated breakpoints will be capped at 3840px. If you need breakpoints larger than this, please pass the maxWidth option to the breakpoints property.",
});
*/
return 3840
}
return imageWidth;
})();
const breakPoints = [];
const diff = maxWidth - minWidth;
const n =
count ||
(maxWidth <= 400
? 1
: maxWidth <= 640
? 2
: maxWidth <= 800
? 3
: maxWidth <= 1024
? 4
: maxWidth <= 1280
? 5
: maxWidth <= 1440
? 6
: maxWidth <= 1920
? 7
: maxWidth <= 2560
? 8
: maxWidth <= 2880
? 9
: maxWidth <= 3840
? 10
: 11);
let currentWidth = minWidth;
n > 1 && breakPoints.push(currentWidth);
let steps = 0;
for (let i = 1; i < n; i++) {
steps += i;
}
const pixelsPerStep = diff / steps;
for (let i = 1; i < n - 1; i++) {
const next = pixelsPerStep * (n - i) + currentWidth;
breakPoints.push(Math.round(next));
currentWidth = next;
}
breakPoints.push(maxWidth);
return [...new Set(breakPoints)];
}

View File

@ -1,34 +0,0 @@
// @ts-check
import getBreakpoints from "./getBreakpoints.js";
export default function getConfigOptions(
imageWidth,
imagesizes,
breakpoints,
format,
imageFormat,
fallbackFormat,
includeSourceFormat
) {
const formats = [
...new Set(
[format, includeSourceFormat && imageFormat]
.flat()
.filter((f) => f && f !== fallbackFormat)
),
fallbackFormat,
];
const requiredBreakpoints = getBreakpoints(breakpoints, imageWidth);
imagesizes =
typeof imagesizes === "string"
? imagesizes
: imagesizes(requiredBreakpoints);
return {
formats,
imagesizes,
requiredBreakpoints,
};
}

View File

@ -1,48 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getContainerElement({
tag,
content,
className = "",
containerAttributes,
isBackgroundPicture = false,
containerClassName = "",
}) {
const {
class: customClasses = "",
style: customInlineStyles = "",
...restContainerAttributes
} = containerAttributes;
const attributesString = getAttributesString({
attributes: restContainerAttributes,
});
const classAttribute = [
isBackgroundPicture
? "astro-imagetools-background-picture"
: "astro-imagetools-background-image",
isBackgroundPicture ? containerClassName : className,
customClasses,
]
.join(" ")
.trim();
const styleAttribute = [
isBackgroundPicture ? "position: relative;" : "",
customInlineStyles + (customInlineStyles.endsWith(";") ? "" : ";"),
]
.join(" ")
.trim();
const containerElement = `<${tag}
${attributesString}
class="${classAttribute}"
style="${styleAttribute}"
>
${content}
</${tag}>`;
return containerElement;
}

View File

@ -1,50 +0,0 @@
// @ts-check
import util from "node:util";
import potrace from "potrace";
import getSrcset from "./getSrcset.js";
import { sharp } from "../../utils/runtimeChecks.js";
export default async function getFallbackImage(
src,
placeholder,
image,
format,
formatOptions,
rest
) {
const base = null;
switch (placeholder) {
case "blurred": {
const dataUri = await getSrcset(src, base, [20], format, {
inline: true,
...rest,
...formatOptions[format],
});
return dataUri;
}
case "tracedSVG": {
const { function: fn, options } = formatOptions.tracedSVG;
return `data:image/svg+xml;utf8,${""}`;
const traceSVG = util.promisify(potrace[fn])
const imageBuffer = await image.toBuffer()
const tracedSVG = await traceSVG(imageBuffer, options)
return `data:image/svg+xml;utf8,${tracedSVG}`;
}
case "dominantColor": {
if (sharp) {
var { r, g, b } = (await image.stats()).dominant;
} else {
[r, g, b] = image.color;
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" style="background: rgb(${r},${g},${b})"></svg>`;
return `data:image/svg+xml;utf8,${svg}`;
}
default:
return null;
}
}

View File

@ -1,138 +0,0 @@
// @ts-check
import filterConfigs from "../../utils/filterConfigs.js";
import {
supportedConfigs,
GlobalConfigOptions,
} from "../../utils/runtimeChecks.js";
const GlobalOnlyProperties = ["cacheDir", "assetFileNames"];
const NonGlobalSupportedConfigs = supportedConfigs.filter(
(key) => !GlobalOnlyProperties.includes(key)
);
const NonProperties = {
Img: [
"tag",
"content",
"backgroundSize",
"backgroundPosition",
"fallbackFormat",
"includeSourceFormat",
"fadeInTransition",
"artDirectives",
],
Picture: ["tag", "content", "backgroundSize", "backgroundPosition"],
BackgroundImage: [
"alt",
"loading",
"decoding",
"layout",
"objectFit",
"objectPosition",
"fadeInTransition",
],
BackgroundPicture: ["alt", "backgroundSize", "backgroundPosition"],
};
const ImgProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.Img.includes(key)
),
PictureProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.Picture.includes(key)
),
BackgroundImageProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.BackgroundImage.includes(key)
),
BackgroundPictureProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.BackgroundPicture.includes(key)
);
const SupportedProperties = {
Img: ImgProperties,
Picture: PictureProperties,
BackgroundImage: BackgroundImageProperties,
BackgroundPicture: BackgroundPictureProperties,
};
export default function getFilteredProps(type, props) {
const filteredGlobalConfigs = filterConfigs(
"Global",
GlobalConfigOptions,
SupportedProperties[type],
{ warn: false }
);
const { search, searchParams } = new URL(props.src, "file://");
props.src = props.src.replace(search, "");
const paramOptions = Object.fromEntries(searchParams);
const filteredLocalProps = filterConfigs(
type,
{
...paramOptions,
...props,
},
SupportedProperties[type]
);
const resolvedProps = {
...filteredGlobalConfigs,
...filteredLocalProps,
};
const {
src,
alt,
tag = "section",
content = "",
sizes = function (breakpoints) {
const maxWidth = breakpoints[breakpoints.length - 1];
return `(min-width: ${maxWidth}px) ${maxWidth}px, 100vw`;
},
preload,
loading = preload ? "eager" : "lazy",
decoding = "async",
attributes = {},
layout = "constrained",
placeholder = "blurred",
breakpoints,
objectFit = "cover",
objectPosition = "50% 50%",
backgroundSize = "cover",
backgroundPosition = "50% 50%",
format = type === "Img" ? undefined : ["avif", "webp"],
fallbackFormat,
includeSourceFormat = true,
formatOptions = {
tracedSVG: {
function: "trace",
},
},
fadeInTransition = true,
artDirectives,
...transformConfigs
} = resolvedProps;
// prettier-ignore
const allProps = {
src, alt, tag, content, sizes, preload, loading, decoding, attributes, layout, placeholder,
breakpoints, objectFit, objectPosition, backgroundSize, backgroundPosition, format,
fallbackFormat, includeSourceFormat, formatOptions, fadeInTransition, artDirectives,
...transformConfigs,
};
const filteredProps = filterConfigs(
type,
allProps,
SupportedProperties[type],
{ warn: false }
);
return {
filteredProps,
transformConfigs,
};
}

View File

@ -1,49 +0,0 @@
import { describe, expect, it } from "vitest";
import getFilteredProps from "./getFilteredProps";
describe("getFilteredProps", () => {
it("should should merge in default props", () => {
const result = getFilteredProps("Img", { src: "/img.jpeg", alt: "alt" });
expect(result).toEqual({
filteredProps: {
alt: "alt",
attributes: {},
breakpoints: undefined,
decoding: "async",
format: undefined,
formatOptions: {
tracedSVG: {
function: "trace",
},
},
layout: "constrained",
loading: "lazy",
objectFit: "cover",
objectPosition: "50% 50%",
placeholder: "blurred",
preload: undefined,
sizes: expect.any(Function),
src: "/img.jpeg",
},
transformConfigs: {},
});
});
it("should accept empty string for `alt` prop on Img", () => {
const result = getFilteredProps("Img", { src: "/img.jpeg", alt: "" });
expect(result).toMatchObject({
filteredProps: {
alt: "",
},
});
});
it("should accept empty string for `alt` prop on Picture", () => {
const result = getFilteredProps("Picture", { src: "/img.jpeg", alt: "" });
expect(result).toMatchObject({
filteredProps: {
alt: "",
},
});
});
});

View File

@ -1,77 +0,0 @@
// @ts-check
import crypto from "node:crypto";
import objectHash from "object-hash";
import getImageSources from "./getImageSources.js";
import getProcessedImage from "./getProcessedImage.js";
import getArtDirectedImages from "./getArtDirectedImages.js";
const imagesData = new Map();
export default async function ({
src,
type,
sizes: imagesizes,
format,
breakpoints,
placeholder,
fallbackFormat,
includeSourceFormat,
formatOptions,
artDirectives,
transformConfigs,
}) {
const args = Array.from(arguments);
const hash = objectHash(args);
if (imagesData.has(hash)) {
return imagesData.get(hash);
}
const { path, base, rest, image, imageWidth, imageHeight, imageFormat } =
await getProcessedImage(src, transformConfigs);
src = path;
rest.aspect = `${imageWidth / imageHeight}`;
if (!fallbackFormat) {
fallbackFormat = imageFormat;
}
const [mainImage, artDirectedImages] = await Promise.all([
getImageSources(
src,
base,
image,
format,
imageWidth,
imagesizes,
breakpoints,
placeholder,
imageFormat,
formatOptions,
fallbackFormat,
includeSourceFormat,
rest
),
getArtDirectedImages(
artDirectives,
placeholder,
format,
imagesizes,
breakpoints,
fallbackFormat,
includeSourceFormat,
formatOptions,
rest
),
]);
const images = [...artDirectedImages, mainImage];
//const uuid = crypto.randomBytes(4).toString("hex").toUpperCase();
const md5 = crypto.createHash('md5').update(args[0].src).digest('hex');
const returnObject = {
uuid:md5.substr(0, 8),
images,
};
imagesData.set(hash, returnObject)
return returnObject
}

View File

@ -1,73 +0,0 @@
// @ts-check
import getSrcset from "./getSrcset.js";
import getConfigOptions from "./getConfigOptions.js";
import getFallbackImage from "./getFallbackImage.js";
export default async function getImageSources(
src,
base,
image,
format,
imageWidth,
imagesizes,
breakpoints,
placeholder,
imageFormat,
formatOptions,
fallbackFormat,
includeSourceFormat,
rest
) {
const calculatedConfigs = getConfigOptions(
imageWidth,
imagesizes,
breakpoints,
format,
imageFormat,
fallbackFormat,
includeSourceFormat
);
const { formats, requiredBreakpoints } = calculatedConfigs;
imagesizes = calculatedConfigs.imagesizes;
const maxWidth = requiredBreakpoints[requiredBreakpoints.length - 1];
const sliceLength = -(maxWidth.toString().length + 2);
const sources = await Promise.all(
formats.map(async (format) => {
const srcset = await getSrcset(src, base, requiredBreakpoints, format, {
...rest,
...formatOptions[format],
});
const srcsets = srcset.split(", ");
const srcObject =
format === fallbackFormat
? { src: srcsets[srcsets.length - 1].slice(0, sliceLength) }
: {};
return {
...srcObject,
format,
srcset,
};
})
);
const sizes = {
width: maxWidth,
height: Math.round(maxWidth / rest.aspect),
};
const fallback = await getFallbackImage(
src,
placeholder,
image,
fallbackFormat,
formatOptions,
rest
);
return { sources, sizes, fallback, imagesizes };
}

View File

@ -1,80 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
imgClassName = "",
}) {
const {
class: customClasses = "",
style: customInlineStyles = "",
onload: customOnload = "",
...restImgAttributes
} = imgAttributes;
const attributesString = getAttributesString({
attributes: restImgAttributes,
element: "img",
excludeArray: [
"src",
"alt",
"srcset",
"sizes",
"width",
"height",
"loading",
"decoding",
],
});
const classAttribute = ["astro-imagetools-img", imgClassName, customClasses]
.join(" ")
.trim();
const styleAttribute = [
"display: inline-block; overflow: hidden; vertical-align: middle;",
customInlineStyles + (customInlineStyles.endsWith(";") ? "" : ";"),
layoutStyles,
]
.join(" ")
.trim();
const onloadAttribute = [
!imgClassName && style
? fadeInTransition
? `parentElement.style.setProperty('--z-index', 1); parentElement.style.setProperty('--opacity', 0);`
: `parentElement.style.backgroundImage = 'unset';`
: "",
customOnload,
]
.join(" ")
.trim();
const imgElement = `<img
${attributesString}
src="${src}"
${typeof alt === "string" ? `alt="${alt}"` : ""}
srcset="${srcset}"
sizes="${imagesizes}"
width="${sizes.width}"
height="${sizes.height}"
${loading ? `loading="${loading}"` : ""}
${decoding ? `decoding="${decoding}"` : ""}
class="${classAttribute}"
style="${styleAttribute}"
onload="${onloadAttribute}"
/>`;
return imgElement;
}

View File

@ -1,16 +0,0 @@
// @ts-check
export default function getLayoutStyles({
layout = null,
isBackgroundImage = false,
}) {
return isBackgroundImage
? "width: 100%; height: 100%;"
: layout === "fill"
? `width: 100%; height: 100%;`
: layout === "fullWidth"
? `width: 100%; height: auto;`
: layout === "fixed"
? ""
: "max-width: 100%; height: auto;";
}

View File

@ -1,34 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getLinkElement({
images = [],
preload = "",
imagesizes = "",
linkAttributes,
}) {
const imagesrcset =
preload &&
images[images.length - 1]?.sources.find(
({ format: fmt }) => fmt === preload
)?.srcset;
const attributesString = getAttributesString({
element: "link",
attributes: linkAttributes,
excludeArray: ["as", "rel", "imagesizes", "imagesrcset"],
});
const linkElement =
preload && images.length
? `<link
${attributesString}
as="image"
rel="preload"
imagesizes="${imagesizes}"
imagesrcset="${imagesrcset}"
/>`
: "";
return linkElement;
}

View File

@ -1,14 +0,0 @@
import { describe, expect, it } from "vitest";
import getLinkElement from "./getLinkElement";
describe("getLinkElement", () => {
it("returns an empty string if preload is not set", () => {
const result = getLinkElement({ linkAttributes: {} });
expect(result).toBe("");
});
it("returns an empty string if no images are provided", () => {
const result = getLinkElement({ linkAttributes: {}, preload: "webp" });
expect(result).toBe("");
});
});

View File

@ -1,43 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getPictureElement({
sources,
className,
layoutStyles,
pictureAttributes,
isBackgroundPicture = false,
}) {
const {
class: customClasses = "",
style: customInlineStyles = "",
...restPictureAttributes
} = pictureAttributes;
const attributesString = getAttributesString({
attributes: restPictureAttributes,
});
const classAttribute = ["astro-imagetools-picture", className, customClasses]
.join(" ")
.trim();
const styleAttribute = [
isBackgroundPicture
? `position: absolute; z-index: 0; width: 100%; height: 100%; display: inline-block;`
: `position: relative; display: inline-block;`,
customInlineStyles + (customInlineStyles.endsWith(";") ? "" : ";"),
layoutStyles,
]
.join(" ")
.trim();
const pictureElement = `<picture
${attributesString}
class="${classAttribute}"
style="${styleAttribute}"
>${sources.join("\n")}
</picture>`;
return pictureElement;
}

View File

@ -1,58 +0,0 @@
// @ts-check
import { fileURLToPath } from "node:url";
import { extname, relative, resolve } from "node:path";
import { getSrcPath } from "./getSrcPath.js";
import getResolvedSrc from "./getResolvedSrc.js";
import { cwd } from "../../utils/runtimeChecks.js";
import throwErrorIfUnsupported from "./throwErrorIfUnsupported.js";
import { getImageDetails } from "./imagetools.js"
export default async function getProcessedImage(src, transformConfigs) {
throwErrorIfUnsupported(src, extname(src).slice(1));
let base;
debugger
if (src.match("(http://|https://|data:image/).*")) {
({ src, base } = await getResolvedSrc(src))
} else {
const {
default: { isSsrBuild },
} = await import("../../astroViteConfigs.js");
if (isSsrBuild) {
const filename = fileURLToPath(import.meta.url)
const assetPath = resolve(filename, "../../client") + src
src = "/" + relative(cwd, assetPath)
}
}
const {
w,
h,
ar,
width = w,
height = h,
aspect = ar,
...rest
} = transformConfigs;
const path = src.replace(/\\/g, `/`);
const { image, imageWidth, imageHeight, imageFormat } = await getImageDetails(
await getSrcPath(src),
width,
height,
aspect
);
return {
path,
base,
rest,
image,
imageWidth,
imageHeight,
imageFormat,
};
}

View File

@ -1,44 +0,0 @@
// @ts-check
import fs from "node:fs";
import crypto from "node:crypto";
import { join, parse, relative } from "node:path";
import throwErrorIfUnsupported from "./throwErrorIfUnsupported.js";
import {
cwd,
fsCachePath,
supportedImageTypes,
} from "../../utils/runtimeChecks.js";
const { fileTypeFromBuffer } = await import("file-type");
export default async function getResolvedSrc(src) {
const token = crypto.createHash("md5").update(src).digest("hex");
const md5 = crypto.createHash('md5').update(src).digest('hex');
//let filepath = fsCachePath + token;
let filepath = fsCachePath + md5;
const fileExists = (() => {
for (const type of supportedImageTypes) {
const fileExists = fs.existsSync(filepath + `.${type}`);
if (fileExists) {
filepath += `.${type}`;
return true;
}
}
})();
if (!fileExists) {
const buffer = Buffer.from(await (await fetch(src)).arrayBuffer())
const { ext } = (await fileTypeFromBuffer(buffer)) || {}
throwErrorIfUnsupported(src, ext)
filepath += `.${ext}`
fs.writeFileSync(filepath, buffer)
}
const base = /^https?:/.test(src)
? parse(new URL(src).pathname).name
: undefined
//src = join("/", relative(cwd, filepath))
return { src: filepath, base }
}

View File

@ -1,32 +0,0 @@
import fs from "node:fs";
import path from "node:path";
// To strip off params when checking for file on disk.
const paramPattern = /\?.*/;
/**
* getSrcPath allows the use of `src` attributes relative to either the public folder or project root.
*
* It first checks to see if the src is a file relative to the project root.
* If the file isn't found, it will look in the public folder.
* Finally, if it still can't be found, the original input will be returned.
*/
export async function getSrcPath(src) {
const { default: astroViteConfigs } = await import(
"../../astroViteConfigs.js"
);
// If this is already resolved to a file, return it.
if (fs.existsSync(src.replace(paramPattern, ""))) return src;
const rootPath = path.join(astroViteConfigs.rootDir, src);
const rootTest = rootPath.replace(paramPattern, "");
if (fs.existsSync(rootTest)) return rootPath;
const publicPath = path.join(astroViteConfigs.publicDir, src);
const publicTest = publicPath.replace(paramPattern, "");
if (fs.existsSync(publicTest)) return publicPath;
// Fallback
return src;
}

View File

@ -1,67 +0,0 @@
import path from "node:path";
import { describe, expect, it, afterAll, vi } from "vitest";
import { getSrcPath } from "./getSrcPath";
vi.mock("../../astroViteConfigs.js", () => {
return {
default: {
rootDir: buildPath(),
// Custom publicDir
publicDir: buildPath("out"),
},
};
});
/**
* Build an absolute path to the target in the fixture directory
*/
function buildPath(target = "") {
return path.resolve(__dirname, "../../test-fixtures/getSrcPath", target);
}
describe("getLinkElement", () => {
afterAll(() => {
vi.unmock("../../astroViteConfigs.js");
});
it("finds a file in the root of the project", async () => {
const result = await getSrcPath("root.jpeg");
expect(result).toBe(buildPath("root.jpeg"));
});
it("finds a file in the public folder", async () => {
const result = await getSrcPath("out.jpeg");
expect(result).toBe(buildPath("out/out.jpeg"));
});
it("returns an absolute path unchanged, if it exists", async () => {
const result = await getSrcPath(buildPath("out/out.jpeg"));
expect(result).toBe(buildPath("out/out.jpeg"));
});
it("handles query parameters", async () => {
const result = await getSrcPath("root.jpeg?w=200");
expect(result).toBe(buildPath("root.jpeg?w=200"));
});
it("handles query parameters for public-resolved files", async () => {
const result = await getSrcPath("out.jpeg?w=200");
expect(result).toBe(buildPath("out/out.jpeg?w=200"));
});
it("returns the original input if the file is not found", async () => {
const result = await getSrcPath(
"https://cdn.nedis.com/images/products_high_res/TVRC2080BK_P30.JPG"
);
expect(result).toBe(
"https://cdn.nedis.com/images/products_high_res/TVRC2080BK_P30.JPG"
);
});
it("finds relative paths correctly", async () => {
const outResult = await getSrcPath("./out/out.jpeg");
const rootResult = await getSrcPath("./root.jpeg");
expect(outResult).toBe(buildPath("out/out.jpeg"));
expect(rootResult).toBe(buildPath("root.jpeg"));
});
});

View File

@ -1,39 +0,0 @@
// @ts-check
import { getSrcPath } from "./getSrcPath.js";
export default async function getSrcset(
src,
base,
breakpoints,
format,
options
) {
options = {
format,
w: breakpoints,
...options,
};
const keys = Object.keys(options);
const params = keys.length
? keys
.map((key) =>
Array.isArray(options[key])
? `&${key}=${options[key].join(";")}`
: `&${key}=${options[key]}`
)
.join("")
: "";
const id = `${src}?${params.slice(1)}`;
const fullPath = await getSrcPath(id);
const { default: load } = await import("../../plugin/hooks/load.js");
// @ts-ignore
const srcset = (await load(fullPath, base)).slice(16, -1);
return srcset;
}

View File

@ -1,15 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getStyleElement({
styleAttributes,
backgroundStyles = "",
}) {
const attributesString = getAttributesString({
attributes: styleAttributes,
});
const styleElement = `<style ${attributesString}>${backgroundStyles}</style>`;
return styleElement;
}

View File

@ -1,35 +0,0 @@
// @ts-check
import {
builtins,
applyTransforms,
generateTransforms,
} from "imagetools-core"
import sharp from "sharp"
export const loadImage = async (path) => { return await sharp(path) }
export async function getImageDetails(path, width, height, aspect) {
const loadedImage = await loadImage(path)
if (aspect && !width && !height) {
if (!width && !height) {
({ width } = await loadedImage.metadata())
}
if (width) {
height = width / aspect
}
if (height) {
width = height * aspect
}
}
const { image, metadata } = await applyTransforms(
generateTransforms({ width, height }, builtins, new URLSearchParams()).transforms,loadedImage
)
const {
width: imageWidth,
height: imageHeight,
format: imageFormat,
} = metadata
return { image, imageWidth, imageHeight, imageFormat }
}

View File

@ -1,14 +0,0 @@
// @ts-check
import { supportedImageTypes } from "../../utils/runtimeChecks.js";
export default function throwErrorIfUnsupported(src, ext) {
if (!ext && typeof ext !== "string") {
throw new Error(`Failed to load ${src}; Invalid image format`);
}
if (ext && !supportedImageTypes.includes(ext.toLowerCase())) {
throw new Error(
`Failed to load ${src}; Invalid image format ${ext} or the format is not supported by astro-imagetools`
);
}
}

View File

@ -1,187 +0,0 @@
export default {
"environment": "build",
"isSsrBuild": false,
"projectBase": "",
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\polymech-site\\public\\",
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\polymech-site\\",
"config": {
"root": "file:///C:/Users/zx/Desktop/polymech/polymech-site/",
"srcDir": "file:///C:/Users/zx/Desktop/polymech/polymech-site/src/",
"publicDir": "file:///C:/Users/zx/Desktop/polymech/polymech-site/public/",
"outDir": "file:///C:/Users/zx/Desktop/polymech/polymech-site/dist/",
"cacheDir": "file:///C:/Users/zx/Desktop/polymech/polymech-site/node_modules/.astro/",
"site": "https://polymech.io",
"compressHTML": true,
"base": "/",
"trailingSlash": "ignore",
"output": "static",
"scopedStyleStrategy": "attribute",
"integrations": [
{
"name": "@astrojs/react",
"hooks": {}
},
{
"name": "imagetools",
"hooks": {}
},
{
"name": "astro-webmanifest",
"hooks": {}
}
],
"build": {
"format": "directory",
"client": "file:///C:/Users/zx/Desktop/polymech/polymech-site/dist/client/",
"server": "file:///C:/Users/zx/Desktop/polymech/polymech-site/dist/server/",
"assets": "_astro",
"serverEntry": "entry.mjs",
"redirects": true,
"inlineStylesheets": "auto",
"concurrency": 1
},
"server": {
"open": false,
"host": false,
"port": 4321,
"streaming": true
},
"redirects": {},
"image": {
"endpoint": {
"route": "/_image"
},
"service": {
"entrypoint": "astro/assets/services/sharp",
"config": {}
},
"domains": [],
"remotePatterns": []
},
"devToolbar": {
"enabled": false
},
"markdown": {
"syntaxHighlight": "shiki",
"shikiConfig": {
"langs": [],
"langAlias": {},
"theme": "github-light-default",
"themes": {},
"wrap": false,
"transformers": []
},
"remarkPlugins": [],
"rehypePlugins": [],
"remarkRehype": {},
"gfm": true,
"smartypants": true
},
"vite": {
"plugins": [
[
{
"name": "@tailwindcss/vite:scan",
"enforce": "pre"
},
{
"name": "@tailwindcss/vite:generate:serve",
"apply": "serve",
"enforce": "pre"
},
{
"name": "@tailwindcss/vite:generate:build",
"apply": "build",
"enforce": "pre"
}
],
[
{
"name": "vite:react-babel",
"enforce": "pre"
},
{
"name": "vite:react-refresh",
"enforce": "pre"
}
],
{
"name": "@astrojs/react:opts"
}
],
"build": {
"target": "esnext",
"assetsDir": "./assets",
"modulePreload": {
"polyfill": false
},
"commonjsOptions": {
"esmExternals": true
}
},
"ssr": {
"external": [
"cacache",
"glob",
"xlsx",
"sharp",
"@polymech/kbot-d"
],
"noExternal": [
"@mui/material",
"@mui/base",
"@babel/runtime",
"use-immer",
"@material-tailwind/react"
]
},
"optimizeDeps": {
"include": [
"@astrojs/react/client.js"
],
"exclude": [
"@astrojs/react/server.js"
]
}
},
"i18n": {
"defaultLocale": "en",
"locales": [
"es",
"en",
"de",
"fr",
"it",
"ar",
"ja",
"zh"
],
"routing": {
"prefixDefaultLocale": false,
"redirectToDefaultLocale": true,
"fallbackType": "redirect"
}
},
"security": {
"checkOrigin": true
},
"env": {
"schema": {},
"validateSecrets": false
},
"experimental": {
"clientPrerender": false,
"contentIntellisense": false,
"responsiveImages": false,
"serializeConfig": false
},
"legacy": {
"collections": false
}
},
"mode": "production",
"outDir": "C:\\Users\\zx\\Desktop\\polymech\\polymech-site\\dist\\",
"assetsDir": "_astro",
"sourcemap": false,
"assetFileNames": "/_astro/[name]@[width].[hash][extname]"
}

View File

@ -1,46 +0,0 @@
---
import renderBackgroundImage from "../api/renderBackgroundImage.js";
import type { BackgroundImageConfigOptions } from "../types.d";
const content = await Astro.slots.render("default");
declare interface Props
extends Pick<
BackgroundImageConfigOptions,
Exclude<keyof BackgroundImageConfigOptions, "content">
> {}
const { link, style, htmlElement } = await renderBackgroundImage({
content,
...(Astro.props as Props),
});
---
<Fragment set:html={link + style + htmlElement} />
<script>
const { classList } = document.documentElement;
const addClass = classList.add.bind(classList);
addClass("jpeg");
addClass("png");
const isFormatSupported = (format, dataUri) => {
const image = new Image();
image.src = `data:image/${format};base64,${dataUri}`;
image.onload = addClass(format);
};
// TODO: Check support for JXL images
// isFormatSupported("jxl", "/woAEBAJCAQBACwASxLFgoUJEP3D/wA=");
isFormatSupported("webp", "UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==");
isFormatSupported(
"avif",
"AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A="
);
</script>

View File

@ -1,19 +0,0 @@
---
import renderBackgroundPicture from "../api/renderBackgroundPicture.js";
import { BackgroundPictureConfigOptions } from "../types.d";
declare interface Props
extends Pick<
BackgroundPictureConfigOptions,
Exclude<keyof BackgroundPictureConfigOptions, "content">
> {}
const content = await Astro.slots.render("default");
const { link, style, htmlElement } = await renderBackgroundPicture({
content,
...(Astro.props as Props),
});
---
<Fragment set:html={link + style + htmlElement} />

View File

@ -1,10 +0,0 @@
---
import renderImage from "../api/renderImage.js";
import type { PictureConfigOptions as ImageConfigOptions } from "../types.d";
const { link, style, image } = await renderImage(
Astro.props as ImageConfigOptions
);
---
<Fragment set:html={link + style + image} />

View File

@ -1,4 +0,0 @@
<!-- prettier-ignore -->
<script is:inline>
const{classList:e}=document.documentElement,A=e.add.bind(e);A("jpeg");A("png");const g=(B,d)=>{const a=new Image;a.src=`data:image/${B};base64,${d}`,a.onload=A(B)};g("webp","UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==");g("avif","AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=");
</script>

View File

@ -1,10 +0,0 @@
---
import renderImg from "../api/renderImg.js";
import type { ImgConfigOptions } from "../types.d";
declare interface Props extends ImgConfigOptions {}
const { link, style, img } = await renderImg(Astro.props as Props);
---
<Fragment set:html={link + style + img} />

View File

@ -1,10 +0,0 @@
---
import renderPicture from "../api/renderPicture.js";
import type { PictureConfigOptions } from "../types.d";
declare interface Props extends PictureConfigOptions {}
const { link, style, picture } = await renderPicture(Astro.props as Props);
---
<Fragment set:html={link + style + picture} />

View File

@ -1,5 +0,0 @@
export { default as Img } from "./Img.astro";
export { default as Picture } from "./Picture.astro";
export { default as BackgroundImage } from "./BackgroundImage.astro";
export { default as BackgroundPicture } from "./BackgroundPicture.astro";
export { default as ImageSupportDetection } from "./ImageSupportDetection.astro";

View File

@ -1,3 +0,0 @@
import type { GlobalConfigOptions } from "./types";
export function defineConfig(config: GlobalConfigOptions): GlobalConfigOptions;

View File

@ -1,3 +0,0 @@
export function defineConfig(config) {
return config;
}

View File

@ -1,3 +0,0 @@
import astroImageTools from "./integration/index.js";
export { astroImageTools as imagetools };

View File

@ -1,102 +0,0 @@
// @ts-check
import fs from "node:fs"
import { fileURLToPath } from "node:url"
import { posix as path, resolve } from "node:path"
import { saveAndCopyAsset } from "./utils/saveAndCopyAsset.js"
import vitePluginAstroImageTools, { store } from "../plugin/index.js"
import { fsCachePath } from "../utils/runtimeChecks.js"
import { sync as write } from "@polymech/fs/write"
import { sync as read } from "@polymech/fs/read"
import { sync as exists } from "@polymech/fs/exists"
import pMap from "p-map"
import { createLogger } from "@polymech/log"
const logger = createLogger("imagetools:integration")
const filename = fileURLToPath(import.meta.url);
const astroViteConfigsPath = resolve(filename, "../../astroViteConfigs.js");
export default {
name: "imagetools",
hooks: {
"astro:config:setup": async function ({ config, command, updateConfig }) {
const environment = command;
const isSsrBuild =
command === "build" && !!config.adapter && config.output === "server";
let projectBase = path.normalize(config.base);
if (projectBase.startsWith("./")) projectBase = projectBase.slice(1);
if (!projectBase.startsWith("/")) projectBase = "/" + projectBase;
if (projectBase.endsWith("/")) projectBase = projectBase.slice(0, -1);
const astroViteConfigs = {
environment,
isSsrBuild,
projectBase,
publicDir: fileURLToPath(config.publicDir.href),
rootDir: fileURLToPath(config.root.href),
config
};
await fs.promises.writeFile(
astroViteConfigsPath,
`export default ${JSON.stringify(astroViteConfigs)}`
);
updateConfig({
vite: {
plugins: [vitePluginAstroImageTools],
},
});
},
"astro:build:done": async function closeBundle() {
const { default: astroViteConfigs } = await import("../astroViteConfigs.js")
const { mode, outDir, assetsDir, isSsrBuild } = astroViteConfigs
if (mode === "production") {
logger.debug(`imagetools : astro:build:done : start`)
const allEntries = [...store.entries()]
const assetPaths = allEntries.filter(([, { hash = null } = {}]) => hash)
write(`${outDir}/imagetools_assets.json`, assetPaths)
const startTime = Date.now();
let totalSize = 0;
const ret = await pMap(assetPaths, async ([assetPath, { hash, image, buffer }]) => {
debugger
const src = fsCachePath + hash;
if(!exists(src)) {
logger.error(`imagetools:saveAndCopyAsset : asset ${assetPath} not found at ${src}`)
return false
}
const imageStartTime = Date.now();
try {
const img = await saveAndCopyAsset(
hash,
image,
buffer,
outDir,
assetsDir,
assetPath,
isSsrBuild
)
const imageEndTime = Date.now();
const stats = fs.fstatSync(fs.openSync(src, 'r'));
totalSize += stats.size;
logger.debug(`imagetools:saveAndCopyAsset : processed asset ${assetPath} in ${imageEndTime - imageStartTime}ms`)
return img
} catch (error) {
debugger
logger.error(`imagetools:saveAndCopyAsset : failed to process asset ${assetPath}:`, error)
}
}, { concurrency: 1 })
const endTime = Date.now();
logger.debug(`imagetools : astro:build:done : end, total processed ${assetPaths.length}: ${endTime - startTime}ms | ${(totalSize / (1024 * 1024)).toFixed(2)} MB`)
return ret
}
}
}
};

View File

@ -1,34 +0,0 @@
import fs from "node:fs/promises";
import { posix as path } from "node:path";
import { fsCachePath } from "../../utils/runtimeChecks.js";
import { sync as mkdir } from "@polymech/fs/dir"
const copied = []
export async function saveAndCopyAsset(
hash,
image,
buffer,
outDir,
assetsDir,
assetPath,
isSsrBuild
) {
debugger
const src = fsCachePath + hash
const dest = path.join(outDir, isSsrBuild ? "/client" : "", assetPath)
assetsDir = path.join(outDir, isSsrBuild ? "/client" : "/", assetsDir)
if (copied.includes(assetPath)) return
mkdir(assetsDir)
console.log("Copying", src, "to", dest)
await fs.copyFile(src, dest).catch(async (error) => {
if (error.code === "ENOENT") {
const imageBuffer = buffer || (await image.toBuffer());
await Promise.all(
[src, dest].map(async (dir) => {
await fs.writeFile(dir, imageBuffer);
})
);
} else throw error;
})
copied.push(assetPath)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +0,0 @@
{
"name": "imagetools",
"version": "0.9.0",
"description": "Image Optimization tools for the Astro JS framework",
"type": "module",
"types": "./types.d.ts",
"exports": {
".": "./index.js",
"./ssr": "./ssr/index.js",
"./api": "./api/index.js",
"./config": "./config.mjs",
"./components": "./components/index.js"
},
"scripts": {
"test:watch": "vitest",
"test": "vitest run"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RafidMuhymin/astro-imagetools.git"
},
"keywords": [
"astro",
"astro-component",
"image",
"images",
"optimization",
"responsive-image",
"vite",
"vite-plugin",
"sharp",
"imagetools",
"codecs",
"astropub"
],
"author": "Rafid Muhymin",
"license": "MIT",
"bugs": {
"url": "https://github.com/RafidMuhymin/astro-imagetools/issues"
},
"homepage": "https://github.com/RafidMuhymin/astro-imagetools#readme",
"dependencies": {
"@astropub/codecs": "^0.4.4",
"@polymech/cache": "file:../../../polymech-mono/packages/cache",
"@polymech/fs": "file:../../../polymech-mono/packages/fs",
"@polymech/log": "file:../../../polymech-mono/packages/log",
"file-type": "17.1.1",
"find-cache-dir": "3.3.2",
"find-up": "^7.0.0",
"imagetools-core": "^7.0.2",
"object-hash": "3.0.0",
"p-map": "^7.0.3",
"potrace": "2.1.8"
},
"peerDependencies": {
"astro": ">=0.26 || >=1.0.0-beta"
},
"devDependencies": {
"vitest": "^0.12.4"
},
"engines": {
"node": "^14.15.0 || >=16.0.0"
}
}

View File

@ -1,19 +0,0 @@
// @ts-check
export default function config() {
return {
optimizeDeps: {
exclude: ["@astropub/codecs", "imagetools-core", "sharp"],
},
ssr: {
external: [
"sharp",
"potrace",
"file-type",
"object-hash",
"find-cache-dir",
"@astropub/codecs",
],
},
};
}

View File

@ -1,164 +0,0 @@
// @ts-check
import path from "node:path";
import objectHash from "object-hash";
import { store } from "../index.js";
import { getCachedBuffer } from "../utils/cache.js";
import { getSrcPath } from "../../api/utils/getSrcPath.js";
import { getAssetPath, getConfigOptions } from "../utils/shared.js";
import { sharp, supportedImageTypes } from "../../utils/runtimeChecks.js";
import { getLoadedImage, getTransformedImage } from "../utils/imagetools.js"
export default async function load(id) {
try {
var fileURL = new URL(`file://${id}`);
} catch (error) {
return null;
}
const { search, searchParams } = fileURL;
id = id.replace(search, "");
const ext = path.extname(id).slice(1);
if (!supportedImageTypes.includes(ext)) return null;
const { default: astroViteConfigs } = await import(
// @ts-ignore
"../../astroViteConfigs.js"
);
const { environment, projectBase, assetFileNames } = astroViteConfigs;
const src = await getSrcPath(id);
const rootRelativePosixSrc = path.posix.normalize(
path.relative("", src).split(path.sep).join(path.posix.sep)
);
const getHash = (width) =>
objectHash(
{ width, options, rootRelativePosixSrc },
// @ts-ignore
{ algorithm: "sha256" }
);
const base =
typeof arguments[1] === "string"
? arguments[1]
: path.basename(src, path.extname(src));
const config = Object.fromEntries(searchParams);
const { image: loadedImage, width: imageWidth } =
store.get(src) || store.set(src, await getLoadedImage(src, ext)).get(src);
const { type, widths, options, extension, raw, inline } = getConfigOptions(
config,
ext,
imageWidth
);
if (raw) {
const testConfig = { ...config };
delete testConfig.raw;
delete testConfig.inline;
delete testConfig.base64;
if (Object.keys(testConfig).length > 0) {
throw new Error(
"If raw is set, no other options can be set except inline and base64"
);
}
}
if (inline) {
if (widths.length > 1) {
throw new Error(
`The base64 or inline parameter can't be used with multiple widths`
);
}
const [width] = widths;
debugger
const hash = getHash(width);
if (store.has(hash)) {
return `export default "${store.get(hash)}"`;
} else {
const config = { width, ...options };
const { image, buffer } = raw
? {
image: sharp ? loadedImage : null,
buffer: !sharp ? loadedImage.data : null,
}
: await getTransformedImage({
src,
image: loadedImage,
config,
type,
});
const dataUri = `data:${type};base64,${(
buffer || (await getCachedBuffer(hash, image))
).toString("base64")}`;
store.set(hash, dataUri);
return `export default "${dataUri}"`;
}
} else {
const sources = await Promise.all(
widths.map(async (width) => {
const hash = getHash(width);
const assetPath = getAssetPath(
base,
assetFileNames,
extension,
width,
hash
);
if (!store.has(assetPath)) {
const config = { width, ...options };
const { image, buffer } = raw
? {
image: sharp && loadedImage,
buffer: !sharp && loadedImage.data,
}
: await getTransformedImage({
src,
image: loadedImage,
config,
type,
});
const imageObject = { hash, type, image, buffer };
store.set(assetPath, imageObject);
}
const modulePath =
environment === "dev" ? assetPath : projectBase + assetPath;
return { width, modulePath };
})
);
const srcset =
sources.length > 1
? sources
.map(({ width, modulePath }) => `${modulePath} ${width}w`)
.join(", ")
: sources[0].modulePath;
return `export default "${srcset}"`;
}
}

View File

@ -1,62 +0,0 @@
// @ts-check
import path from "node:path";
import crypto from "node:crypto";
import MagicString from "magic-string";
import { cwd } from "../../utils/runtimeChecks.js";
const regexTestPattern =
/<img\s+src\s*=(?:"|')([^("|')]*)(?:"|')\s*alt\s*=\s*(?:"|')([^("|')]*)(?:"|')[^>]*>/;
const regexExecPattern = new RegExp(regexTestPattern, "gs");
const regexRenderPattern = /\$\$render`(.*)`/gs;
export default async function transform(code, id) {
if (id.endsWith(".md") && regexTestPattern.test(code)) {
const { default: astroViteConfigs } = await import(
// @ts-ignore
"../../astroViteConfigs.js"
);
const { sourcemap } = astroViteConfigs;
// Extract the "$$render`" part of the markdown string
const [result] = [...code.matchAll(regexRenderPattern)];
const [, renderString] = result;
const renderIndex = result.index + "$$render`".length;
const matches = renderString.matchAll(regexExecPattern);
if (matches !== null) {
const s = new MagicString(code);
const uuid = crypto.randomBytes(4).toString("hex");
const Picture = "Picture" + uuid;
const renderComponent = "renderComponent" + uuid;
s.prepend(
`import { Picture as ${Picture} } from "astro-imagetools/components";\nimport { renderComponent as ${renderComponent} } from "${
cwd + "/node_modules/astro/dist/runtime/server/index.js"
}"\n;`
);
for (const match of matches) {
const [matchedText, rawSrc, alt] = match;
const src = rawSrc.match("(http://|https://|data:image/).*")
? rawSrc
: path.resolve(path.dirname(id), rawSrc).replace(cwd, "");
s.overwrite(
renderIndex + match.index,
renderIndex + match.index + matchedText.length,
`\${${renderComponent}($$result, "${Picture}", ${Picture}, { "src": "${src}", "alt": "${alt}" })}`
);
}
return {
code: s.toString(),
map: sourcemap ? s.generateMap({ hires: true }) : null,
};
}
}
}

View File

@ -1,88 +0,0 @@
// @ts-check
import fs from "node:fs";
import stream from "node:stream";
import { fileURLToPath } from "node:url";
import { posix as path, resolve } from "node:path";
import load from "./hooks/load.js";
import config from "./hooks/config.js";
import transform from "./hooks/transform.js";
import { middleware } from "../ssr/index.js";
import { GlobalConfigOptions } from "../utils/runtimeChecks.js";
if (!globalThis.astroImageToolsStore)
globalThis.astroImageToolsStore = new Map();
export const store = globalThis.astroImageToolsStore;
const filename = fileURLToPath(import.meta.url);
const astroViteConfigsPath = resolve(filename, "../../astroViteConfigs.js");
const vitePluginAstroImageTools = {
name: "vite-plugin-astro-imagetools",
enforce: "pre",
config,
async configResolved(config) {
const { mode } = config;
const { outDir, sourcemap } = config.build;
let inheritedPattern =
config.build.rollupOptions.output?.assetFileNames?.replace(
"[name]",
"[name]@[width]"
);
let assetFileNames = path.normalize(
GlobalConfigOptions.assetFileNames ||
inheritedPattern ||
`/_astro/[name]@[width].[hash][extname]`
);
const { dir: assetsDir } = path.posix.parse(
assetFileNames.replaceAll(path.sep, path.posix.sep)
);
if (!assetFileNames.startsWith("/"))
assetFileNames = path.join("/", assetFileNames);
const astroViteConfigs = JSON.parse(
(await fs.promises.readFile(astroViteConfigsPath, "utf8")).slice(15)
);
const newAstroViteConfigs = {
...astroViteConfigs,
mode,
outDir,
assetsDir,
sourcemap,
assetFileNames,
};
await fs.promises.writeFile(
astroViteConfigsPath,
`export default ${JSON.stringify(newAstroViteConfigs, null, 2)}`
);
},
load,
transform,
configureServer(server) {
server.middlewares.use(async (request, response, next) => {
const buffer = await middleware(request, response);
if (buffer) {
return stream.Readable.from(buffer).pipe(response);
}
next();
});
},
};
export default vitePluginAstroImageTools;

View File

@ -1,17 +0,0 @@
// @ts-check
import fs from "node:fs";
import { fsCachePath } from "../../utils/runtimeChecks.js";
export async function getCachedBuffer(hash, image) {
const cacheFilePath = fsCachePath + hash;
if (fs.existsSync(cacheFilePath)) {
return fs.promises.readFile(cacheFilePath);
}
const buffer = await image.clone().toBuffer();
await fs.promises.writeFile(cacheFilePath, buffer);
return buffer;
}

View File

@ -1,41 +0,0 @@
// @ts-check
import fs from "node:fs";
import * as codecs from "@astropub/codecs";
const resizedImages = new Map();
export const getLoadedImage = async (src, ext) => {
const buffer = fs.readFileSync(src);
const image = await codecs[ext].decode(buffer);
const { width } = image;
const resizedImageKey = `${src}@${image.width}`;
resizedImages.set(resizedImageKey, image);
return { image, width };
};
export const getTransformedImage = async ({ src, image, config, type }) => {
const { width, format, quality } = config;
const resizedImageKey = `${src}@${width}`;
const resizedImage =
resizedImages.get(resizedImageKey) ||
resizedImages
.set(resizedImageKey, await image.resize({ width }))
.get(resizedImageKey);
const encodedImage = quality
? await codecs[format].encode(resizedImage, {
quality: parseInt(quality),
})
: await resizedImage.encode(type);
const buffer = Buffer.from(encodedImage.data);
return { image, buffer };
};

View File

@ -1,23 +0,0 @@
// @ts-check
import {
builtins,
applyTransforms,
generateTransforms,
} from "imagetools-core";
import sharp from "sharp"
export const getLoadedImage = async (src) => {
const image = await sharp(src)
const { width } = await image.metadata()
return { image, width }
}
export const getTransformedImage = async ({ image, config }) => {
const { transforms } = generateTransforms(config, builtins, new URLSearchParams())
const { image: encodedImage } = await applyTransforms(
transforms,
image.clone()
)
return { image: encodedImage, buffer: null }
}

View File

@ -1,47 +0,0 @@
// @ts-check
export function getConfigOptions(config, ext, imageWidth) {
const { w, width = w, format = ext, base64, raw, inline, ...rest } = config;
const imageFormat = format === "jpeg" ? "jpg" : format;
const widths = width
? width.split(";").map((w) => parseInt(w))
: [imageWidth];
const extension = format === "jpg" ? "jpeg" : format;
const type = `image/${extension}`;
const options = {
format: imageFormat,
...rest,
};
return {
type,
widths,
options,
extension,
raw: typeof raw === "string",
inline: typeof base64 === "string" || typeof inline === "string",
};
}
export function getAssetPath(base, assetFileNames, ext, width, hash) {
const regexExecArray = /(?<=\[hash:)\d+(?=\])/g.exec(assetFileNames),
hashLength = regexExecArray ? regexExecArray[0] : 8,
extname = `.${ext}`,
name = base;
width = width + "w";
hash = hash.slice(0, hashLength);
const assetPath = assetFileNames
.replace("[name]", name)
.replace("[width]", width)
.replace(regexExecArray ? `[hash:${hashLength}]` : "[hash]", hash)
.replace("[ext]", ext)
.replace("[extname]", extname);
return assetPath;
}

View File

@ -1,6 +0,0 @@
import type { IncomingMessage, ServerResponse } from "http";
export function middleware(
request: IncomingMessage,
response: ServerResponse
): Buffer;

View File

@ -1,16 +0,0 @@
// @ts-check
import { store } from "../plugin/index.js";
import { getCachedBuffer } from "../plugin/utils/cache.js";
export async function middleware(request, response) {
const imageObject = store.get(request.url);
if (imageObject) {
const { hash, type, image, buffer } = imageObject;
response.setHeader("Content-Type", type);
response.setHeader("Cache-Control", "no-cache");
return buffer || (await getCachedBuffer(hash, image));
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 KiB

View File

@ -1,268 +0,0 @@
declare type format =
| "heic"
| "heif"
| "avif"
| "jpg"
| "jpeg"
| "png"
| "tiff"
| "webp"
| "gif";
declare type PotraceOptions = TraceOptions | PosterizeOptions;
declare interface SharedTracingOptions {
turnPolicy?: "black" | "white" | "left" | "right" | "minority" | "majority";
turdSize?: number;
alphaMax?: number;
optCurve?: boolean;
optTolerance?: number;
threshold?: number;
blackOnWhite?: boolean;
color?: "auto" | string;
background?: "transparent" | string;
}
declare interface TraceOptions {
function?: "trace";
options?: SharedTracingOptions;
}
declare interface PosterizeOptions {
function?: "posterize";
options?: SharedTracingOptions & {
fill?: "spread" | "dominant" | "median" | "mean";
ranges?: "auto" | "equal";
steps?: number | number[];
};
}
declare interface FormatOptions {
formatOptions?: Partial<Record<format, ImageToolsConfigs>> & {
tracedSVG?: PotraceOptions;
};
}
declare interface PictureFormatOptions extends FormatOptions {
format?: format | format[] | [] | null;
fallbackFormat?: format;
includeSourceFormat?: boolean;
}
declare interface ImgFormatOptions extends FormatOptions {
format?: format;
}
declare interface ImageToolsConfigs {
flip?: boolean;
flop?: boolean;
invert?: boolean;
flatten?: boolean;
normalize?: boolean;
grayscale?: boolean;
hue?: number;
saturation?: number;
brightness?: number;
w?: number;
h?: number;
ar?: number;
width?: number;
height?: number;
aspect?: number;
background?: string;
tint?: string;
blur?: number | boolean;
median?: number | boolean;
rotate?: number;
quality?: number;
fit?: "cover" | "contain" | "fill" | "inside" | "outside";
kernel?: "nearest" | "cubic" | "mitchell" | "lanczos2" | "lanczos3";
position?:
| "top"
| "right top"
| "right"
| "right bottom"
| "bottom"
| "left bottom"
| "left"
| "left top"
| "north"
| "northeast"
| "east"
| "southeast"
| "south"
| "southwest"
| "west"
| "northwest"
| "center"
| "centre"
| "cover"
| "entropy"
| "attention";
}
declare interface ObjectStyles {
objectPosition?: string;
objectFit?: "fill" | "contain" | "cover" | "none" | "scale-down";
}
declare interface BackgroundStyles {
backgroundPosition?: string;
backgroundSize?: "fill" | "contain" | "cover" | "none" | "scale-down";
}
declare interface ArtDirective
extends PrimaryProps,
ObjectStyles,
PictureFormatOptions,
ImageToolsConfigs {
media: string;
}
declare interface BackgroundImageArtDirective
extends PrimaryProps,
BackgroundStyles,
PictureFormatOptions,
ImageToolsConfigs {
media: string;
}
declare type sizesFunction = {
(breakpoints: number[]): string;
};
declare type breakpointsFunction = {
(imageWidth: number): number[];
};
declare interface PrimaryProps {
src: string;
sizes?: string | sizesFunction;
placeholder?: "dominantColor" | "blurred" | "tracedSVG" | "none";
breakpoints?:
| number[]
| breakpointsFunction
| {
count?: number;
minWidth?: number;
maxWidth?: number;
};
}
declare interface ConfigOptions extends PrimaryProps, ImageToolsConfigs {
alt: string;
preload?: format;
loading?: "lazy" | "eager" | "auto" | null;
decoding?: "async" | "sync" | "auto" | null;
layout?: "constrained" | "fixed" | "fullWidth" | "fill";
}
declare interface Attributes {
container?: Record<any, string>;
picture?: Record<any, string>;
style?: Record<any, string>;
link?: Omit<Record<any, string>, "as" | "rel" | "imagesizes" | "imagesrcset">;
img?: Omit<
Record<any, string>,
| "src"
| "alt"
| "srcset"
| "sizes"
| "width"
| "height"
| "loading"
| "decoding"
>;
}
export interface PictureConfigOptions
extends ConfigOptions,
ObjectStyles,
PictureFormatOptions {
artDirectives?: ArtDirective[];
attributes?: Omit<Attributes, "container">;
fadeInTransition?:
| boolean
| {
delay?: string;
duration?: string;
timingFunction?: string;
};
}
export interface ImgConfigOptions
extends ConfigOptions,
ObjectStyles,
ImgFormatOptions {
attributes?: Omit<Attributes, "picture" | "container">;
}
declare interface BackgroundProps {
tag?: string;
content?: string;
}
export interface BackgroundImageConfigOptions
extends BackgroundProps,
BackgroundStyles,
Pick<
PictureConfigOptions,
Exclude<
keyof PictureConfigOptions,
| "alt"
| "sizes"
| "loading"
| "decoding"
| "layout"
| "objectFit"
| "objectPosition"
| "artDirective"
| "fadeInTransition"
>
> {
attributes?: Omit<Attributes, "img" | "picture">;
artDirectives?: BackgroundImageArtDirective[];
}
export interface BackgroundPictureConfigOptions
extends BackgroundProps,
Pick<
PictureConfigOptions,
Exclude<keyof PictureConfigOptions, "alt" | "layout">
> {
attributes?: Attributes;
}
export interface GlobalConfigOptions
extends BackgroundStyles,
Pick<
PictureConfigOptions,
Exclude<keyof PictureConfigOptions, "src" | "alt" | "artDirectives">
> {
tag?: string;
cacheDir?: string;
assetFileNames?: string;
}
declare interface HTMLData {
link: string;
style: string;
}
export interface ImageHTMLData extends HTMLData {
image: string;
}
export interface PictureHTMLData extends HTMLData {
picture: string;
}
export interface ImgHTMLData extends HTMLData {
img: string;
}
export interface BackgroundImageHTMLData extends HTMLData {
htmlElement: string;
}
export type BackgroundPictureHTMLData = BackgroundImageHTMLData;

View File

@ -1,51 +0,0 @@
// @ts-check
import printWarning from "./printWarning.js";
export default function filterConfigs(
type,
configs,
supportedConfigs,
{ warn = true } = {}
) {
const clonedConfigs = { ...configs };
const requiredConfigs = [];
type !== "Global" && requiredConfigs.push("src");
["Img", "Picture"].includes(type) && requiredConfigs.push("alt");
requiredConfigs.forEach((key) => {
if (typeof clonedConfigs[key] === "undefined") {
throw new Error(`The "${key}" property is required by ${type}`);
}
});
Object.keys(clonedConfigs).forEach((key) => {
if (!supportedConfigs.includes(key)) {
if (warn) {
if (key !== "class") {
printWarning({ key, type });
} else if (!onlyAstroClass(clonedConfigs[key])) {
/*
printWarning({
message: `Do not provide a "class" directly to ${type}. Instead, use attributes: https://astro-imagetools-docs.vercel.app/en/components/${type}#attributes`,
});
*/
}
}
delete clonedConfigs[key];
}
});
return clonedConfigs;
}
/**
* Checks if the `class` attribute string is only an astro-generated scoped style class.
*/
function onlyAstroClass(classAttr) {
const astroClassPattern = /^astro-[0-9A-Z]{8}$/;
return astroClassPattern.test(classAttr);
}

View File

@ -1,100 +0,0 @@
import { describe, expect, afterAll, it, vi, beforeEach } from "vitest";
import { supportedConfigs } from "./runtimeChecks";
import filterConfigs from "./filterConfigs";
import printWarning from "./printWarning.js";
// Workaround for https://github.com/vitest-dev/vitest/issues/855
vi.mock("./printWarning.js", async () => {
return { default: vi.fn() };
});
const warningSpy = vi.mocked(printWarning);
describe("filterConfigs", () => {
beforeEach(() => {
warningSpy.mockReset();
});
afterAll(() => {
vi.unmock("./printWarning.js");
});
it("should require a `src` attribute for all components", () => {
expect(() => {
filterConfigs("Img", { alt: "" }, supportedConfigs);
}).toThrowError('The "src" property is required by Img');
expect(() => {
filterConfigs("Picture", { alt: "" }, supportedConfigs);
}).toThrowError('The "src" property is required by Picture');
expect(() => {
filterConfigs("BackgroundImage", {}, supportedConfigs);
}).toThrowError('The "src" property is required by BackgroundImage');
expect(() => {
filterConfigs("BackgroundPicture", {}, supportedConfigs);
}).toThrowError('The "src" property is required by BackgroundPicture');
expect(() => {
filterConfigs("Global", {}, supportedConfigs);
}).not.toThrowError();
});
it("should require an `alt` attribute for Picture and Img, but not others", () => {
expect(() => {
filterConfigs("Img", { src: "src" }, supportedConfigs);
}).toThrowError('The "alt" property is required by Img');
expect(() => {
filterConfigs("Picture", { src: "src" }, supportedConfigs);
}).toThrowError('The "alt" property is required by Picture');
expect(() => {
filterConfigs("BackgroundImage", { src: "src" }, supportedConfigs);
}).not.toThrowError();
expect(() => {
filterConfigs("BackgroundPicture", { src: "src" }, supportedConfigs);
}).not.toThrowError();
expect(() => {
filterConfigs("Global", {}, supportedConfigs);
}).not.toThrowError();
});
it("should remove unsupported configs", () => {
const filteredConfig = filterConfigs("Global", { foo: "foo" }, [], {
warn: false,
});
const filteredConfigFooSupported = filterConfigs(
"Global",
{ foo: "foo" },
["foo"],
{
warn: false,
}
);
expect(filteredConfig).not.toContain({ foo: "foo" });
expect(filteredConfigFooSupported).toContain({ foo: "foo" });
});
it("should warn about unsupported configs", () => {
filterConfigs("Global", { foo: "foo" }, []);
expect(warningSpy).toHaveBeenCalledWith({ type: "Global", key: "foo" });
});
it("should warn about unsupported 'class' config", () => {
filterConfigs(
"Img",
{ class: "astro-ASDF1234 my-class", src: "src", alt: "" },
supportedConfigs
);
expect(warningSpy).toHaveBeenCalledWith({
message:
'Do not provide a "class" directly to Img. Instead, use attributes: https://astro-imagetools-docs.vercel.app/en/components/Img#attributes',
});
});
it("should not warn about astro-generated 'class' config", () => {
const filteredConfig = filterConfigs(
"Img",
{ class: "astro-ASDF1234", src: "src", alt: "" },
supportedConfigs
);
expect(warningSpy).not.toHaveBeenCalled();
// class is still stripped out
expect(filteredConfig).not.toContain({ class: "astro-ASDF1234" });
});
});

View File

@ -1,57 +0,0 @@
// @ts-check
const colours = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
underscore: "\x1b[4m",
blink: "\x1b[5m",
reverse: "\x1b[7m",
hidden: "\x1b[8m",
fg: {
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
},
bg: {
black: "\x1b[40m",
red: "\x1b[41m",
green: "\x1b[42m",
yellow: "\x1b[43m",
blue: "\x1b[44m",
magenta: "\x1b[45m",
cyan: "\x1b[46m",
white: "\x1b[47m",
},
};
export default function printWarning({
key = "",
type = "",
message = "",
element = "",
}) {
const flag =
colours.bright + colours.fg.cyan + "[astro-imagetools]" + colours.reset;
const keyLog = key
? " " + colours.bg.yellow + ` ${key} ` + colours.reset
: "";
const messageLog =
colours.fg.yellow +
(message ||
(!element
? `is not a valid ${type} Config Option`
: `can't be defined inside attributes.${element}`)) +
colours.reset;
console.log(flag + keyLog, messageLog);
}

View File

@ -1,62 +0,0 @@
// @ts-check
import fs from "node:fs";
import path from "node:path";
import { findUpSync } from 'find-up'
import findCacheDir from "find-cache-dir";
import filterConfigs from "./filterConfigs.js";
export const sharp = await (async () => { return true })()
export const supportedImageTypes = [
"avif",
"jpeg",
"jpg",
"png",
"webp",
...(sharp ? ["heic", "heif", "tiff", "gif"] : ["jxl", "wp2"]),
];
// prettier-ignore
export const supportedConfigs = [
"src", "alt", "tag", "content", "sizes", "preload", "loading", "decoding", "attributes",
"layout", "placeholder", "breakpoints", "objectFit", "objectPosition", "backgroundSize",
"backgroundPosition", "format", "fallbackFormat", "includeSourceFormat", "formatOptions",
"fadeInTransition", "artDirectives", "flip", "flop", "invert", "flatten", "normalize",
"grayscale", "hue", "saturation", "brightness", "w", "h", "ar", "width", "height", "aspect",
"background", "tint", "blur", "median", "rotate", "quality", "fit", "kernel", "position",
"cacheDir", "assetFileNames",
];
const configFile = findUpSync([
"astro-imagetools.config.js",
"astro-imagetools.config.mjs",
])
const configFunction = configFile
? await import(configFile).catch(async () => await import( /* @vite-ignore */ "/" + configFile))
: null
const rawGlobalConfigOptions = configFunction?.default ?? {}
const NonGlobalConfigOptions = ["src", "alt", "content"]
const GlobalConfigs = supportedConfigs.filter(
(key) => !NonGlobalConfigOptions.includes(key)
);
const GlobalConfigOptions = filterConfigs(
"Global",
rawGlobalConfigOptions,
GlobalConfigs
)
export { GlobalConfigOptions }
export const cwd = process.cwd().split(path.sep).join(path.posix.sep)
const { cacheDir } = GlobalConfigOptions
const fsCachePath =
(cacheDir
? cwd + cacheDir
: findCacheDir({
name: "astro-imagetools",
})) + "/"
fs.existsSync(fsCachePath) || fs.mkdirSync(fsCachePath, { recursive: true });
export { fsCachePath }

View File

@ -1,36 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("GlobalConfigOptions", () => {
beforeEach(() => {
// Need to reset the modules so that we can change the mock implementation between tests
vi.resetModules();
});
it("Should be an empty object by default, if a config file isn't found", async () => {
// Simulate not finding a config file
vi.doMock("find-up", () => {
return {
findUp: async () => undefined,
};
});
// Need to import this after the mocks are set up with `doMock`.
const { GlobalConfigOptions } = await import("./runtimeChecks");
expect(GlobalConfigOptions).toEqual({});
});
it("should return the configuration from a global config file", async () => {
// Find a config file, and mock the contents of that file
vi.doMock("find-up", () => {
return {
findUp: async () => "mockedConfigFile",
};
});
vi.doMock("mockedConfigFile", () => {
return {
default: { breakpoints: [800, 1200] },
};
});
const { GlobalConfigOptions } = await import("./runtimeChecks");
expect(GlobalConfigOptions).toEqual({ breakpoints: [800, 1200] });
});
});

View File

@ -1,7 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// https://vitest.dev/config/#configuration
},
});

View File

@ -1,4 +0,0 @@
*.test.ts
test-fixtures
astroViteConfigs.js
vitest.config.ts

View File

@ -1,39 +0,0 @@
# **Astro ImageTools**
**Astro ImageTools** is a collection of tools for optimizing images, background images, and generating responsive images for the **Astro JS** framework.
## Features
Below is a short list of features that **Astro ImageTools** offers. For more information, please see component-specific or API-specific documentation.
- ✅ **Regular Image Optimization** (`<img>` and `<picture>`)
- ✅ **Background Image Optimization**
- ✅ **Responsive Images**
- ✅ **Simple and intuitive Art Direction API**
- ✅ **Lazy Loading**
- ✅ **Programmatic APIs**
- ✅ **Asynchronous Decoding**
- ✅ **Unique Breakpoints Calculation**
- ✅ **Preloading for urgent images**
- ✅ **SVG Tracing and Posterization**
- ✅ **100% Scoped CSS**
- ✅ **Four kind of Layouts: `constrained`, `fixed`, `fullWidth` & `fill`**
- ✅ **Three kind of Placeholder Images: `blurred`, `dominantColor` & `tracedSVG`**
- ✅ **Long list of supported Image Formats**
- ✅ **Long List of supported Configuration Options**
- ✅ **Supports Remote Images and Data URIs too**
- ✅ **Support for _`sharp`less_ Environments**
- ✅ **Both Memory-based and FS-based Caching for better Performance**
- ✅ **Respects to _Semantics of HTML_ as much as possible**
## Getting Started
To get started with **Astro ImageTools**, first check out the [Installation](https://astro-imagetools-docs.vercel.app/en/installation) documentation for instructions on how to install the `astro-imagetools` package.
If you are looking for the available components and APIs, please check out the [Components and APIs](https://astro-imagetools-docs.vercel.app/en/components-and-apis) documentation.
If you want to view live examples of the components, APIs, layouts, and placeholder images, check out the [Astro ImageTools Demo](https://astro-imagetools-demo.vercel.app/) website.
If you want to report any issues or have found a missing feature, please report it on [GitHub](https://github.com/RafidMuhymin/astro-imagetools/)!
Good luck out there, Astronaut. 🧑‍🚀

View File

@ -1 +0,0 @@
export default function importImage(url: string): Promise<string>;

View File

@ -1,23 +0,0 @@
import load from "../plugin/hooks/load.js";
import { getSrcPath } from "./utils/getSrcPath.js";
import getResolvedSrc from "./utils/getResolvedSrc.js";
export default async function importImage(path) {
try {
const { search, protocol, pathname } = new URL(path);
const { src: id, base } = await getResolvedSrc(
protocol === "data:" ? protocol + pathname : path
);
const src = (await load(id + search, base)).slice(16, -1);
return src;
} catch (error) {
const id = await getSrcPath(path);
const src = (await load(id)).slice(16, -1);
return src;
}
}

View File

@ -1,6 +0,0 @@
export { default as renderImg } from "./renderImg.js";
export { default as renderPicture } from "./renderPicture.js";
export { default as renderBackgroundImage } from "./renderBackgroundImage.js";
export { default as renderBackgroundPicture } from "./renderBackgroundPicture.js";
export { default as importImage } from "./importImage.js";
export { getImageDetails, loadImage } from "./utils/imagetools.js"

View File

@ -1,8 +0,0 @@
import type {
BackgroundImageConfigOptions,
BackgroundImageHTMLData,
} from "../types";
export default function renderBackgroundImage(
config: BackgroundImageConfigOptions
): Promise<BackgroundImageHTMLData>;

View File

@ -1,156 +0,0 @@
// @ts-check
import crypto from "node:crypto";
import getImage from "./utils/getImage.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getContainerElement from "./utils/getContainerElement.js";
export default async function renderBackgroundImage(props) {
const type = "BackgroundImage";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
tag,
content,
preload,
attributes,
placeholder,
breakpoints,
backgroundSize,
backgroundPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
artDirectives,
} = filteredProps;
const {
link: linkAttributes = {},
style: styleAttributes = {},
container: containerAttributes = {},
} = attributes;
const sizes = "";
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
});
const className = `astro-imagetools-background-image-${uuid}`;
const { imagesizes } = images[images.length - 1];
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const backgroundImageStylesArray = images.map(({ media, sources }) => {
const uuid = crypto.randomBytes(4).toString("hex").toUpperCase()
const fallbackUrlCustomVariable = `--astro-imagetools-background-image-fallback-url${uuid}`
const newSources = {};
sources.forEach(({ src, format, srcset }) => {
const sources = srcset
.split(", ")
.map((source) => [
source.slice(0, source.lastIndexOf(" ")),
source.slice(source.lastIndexOf(" ") + 1, -1),
]);
sources.forEach(([path, width]) => {
if (!newSources[width]) {
newSources[width] = [];
}
newSources[width].push({ src, format, path });
});
});
const widths = Object.keys(newSources)
.map((width) => parseInt(width))
.reverse();
const maxWidth = Math.max(...widths);
const styles = widths
.map((width) => {
const sources = newSources[width];
const styles = sources
.map(
({ format, path }, i) =>
`
${i !== sources.length - 1 ? `.${format} ` : ""}.${className} {
background-repeat: no-repeat;
background-image: url(${path}),
var(${fallbackUrlCustomVariable});
background-size: ${backgroundSize};
background-position: ${backgroundPosition};
}
`
)
.reverse()
.join("");
return width === maxWidth
? styles
: `
@media screen and (max-width: ${width}px) {
${styles}
}
`;
})
.join("");
return {
fallbackUrlCustomVariable,
styles: media
? `
@media ${media} {
${styles}
}
`
: styles,
};
});
const containerStyles = `
.${className} {
position: relative;
${images
.map(({ fallback }, i) => {
const fallbackUrlCustomVariable =
backgroundImageStylesArray[i].fallbackUrlCustomVariable;
return `${fallbackUrlCustomVariable}: url("${encodeURI(fallback)}");`;
})
.join("\n")}
}
`;
const backgroundStyles =
backgroundImageStylesArray.map(({ styles }) => styles).join("\n") +
containerStyles;
const style = getStyleElement({ styleAttributes, backgroundStyles });
const htmlElement = getContainerElement({
tag,
content,
className,
containerAttributes,
});
return { link, style, htmlElement };
}

View File

@ -1,8 +0,0 @@
import type {
BackgroundPictureConfigOptions,
BackgroundPictureHTMLData,
} from "../types";
export default function renderBackgroundPicture(
config: BackgroundPictureConfigOptions
): Promise<BackgroundPictureHTMLData>;

View File

@ -1,127 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js";
import getImgElement from "./utils/getImgElement.js";
import getLinkElement from "./utils/getLinkElement.js";
import getStyleElement from "./utils/getStyleElement.js";
import getLayoutStyles from "./utils/getLayoutStyles.js";
import getFilteredProps from "./utils/getFilteredProps.js";
import getPictureElement from "./utils/getPictureElement.js";
import getBackgroundStyles from "./utils/getBackgroundStyles.js";
import getContainerElement from "./utils/getContainerElement.js";
export default async function renderBackgroundPicture(props) {
const type = "BackgroundPicture";
const { filteredProps, transformConfigs } = getFilteredProps(type, props);
const {
src,
tag,
content,
sizes,
preload,
loading,
decoding,
attributes,
placeholder,
breakpoints,
objectFit,
objectPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
fadeInTransition,
artDirectives,
} = filteredProps;
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
picture: pictureAttributes = {},
container: containerAttributes = {},
} = attributes;
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
});
const className = `imagetools-picture-${uuid}`,
containerClassName = `astro-imagetools-background-picture-${uuid}`;
const { imagesizes } = images[images.length - 1];
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isBackgroundPicture: true, containerClassName }
);
const style = getStyleElement({ styleAttributes, backgroundStyles });
const link = getLinkElement({ images, preload, imagesizes, linkAttributes });
const layoutStyles = getLayoutStyles({ isBackgroundImage: true });
// Background Images shouldn't convey important information
const alt = "";
const sources = images.flatMap(({ media, sources, sizes, imagesizes }) =>
sources.map(({ format, src, srcset }) =>
src
? getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
})
: `<source
srcset="${srcset}"
sizes="${imagesizes}"
width="${sizes.width}"
height="${sizes.height}"
type="${`image/${format}`}"
${media ? `media="${media}"` : ""}
/>`
)
);
const picture = getPictureElement({
sources,
className,
layoutStyles,
pictureAttributes,
isBackgroundPicture: true,
});
const htmlElement = getContainerElement({
tag,
content: picture + content,
containerAttributes,
isBackgroundPicture: true,
containerClassName,
});
return { link, style, htmlElement };
}

View File

@ -1,5 +0,0 @@
import type { ImgConfigOptions, ImgHTMLData } from "../types";
export default function renderImg(
config: ImgConfigOptions
): Promise<ImgHTMLData>;

View File

@ -1,93 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js"
import getImgElement from "./utils/getImgElement.js"
import getLinkElement from "./utils/getLinkElement.js"
import getStyleElement from "./utils/getStyleElement.js"
import getLayoutStyles from "./utils/getLayoutStyles.js"
import getFilteredProps from "./utils/getFilteredProps.js"
import getBackgroundStyles from "./utils/getBackgroundStyles.js"
export default async function renderImg(props) {
const type = "Img"
const { filteredProps, transformConfigs } = getFilteredProps(type, props)
const {
src,
alt,
sizes,
preload,
loading,
decoding,
attributes,
layout,
breakpoints,
placeholder,
objectFit,
objectPosition,
format,
formatOptions,
} = filteredProps;
const artDirectives = [],
fallbackFormat = format,
fadeInTransition = false,
includeSourceFormat = false
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
} = attributes
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
artDirectives,
fallbackFormat,
includeSourceFormat,
formatOptions,
transformConfigs,
})
const className = `imagetools-img-${uuid}`
const { imagesizes } = images[images.length - 1]
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isImg: true }
);
const style = getStyleElement({ styleAttributes, backgroundStyles })
const link = getLinkElement({ images, preload, imagesizes, linkAttributes })
const layoutStyles = getLayoutStyles({ layout })
const sources = images.flatMap(({ sources, sizes, imagesizes }) =>
sources.map(({ src, srcset }) =>
getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
imgClassName: className,
uuid
})
)
);
const [img] = sources;
return { link, style, img }
}

View File

@ -1,5 +0,0 @@
import type { PictureConfigOptions, PictureHTMLData } from "../types.d.ts"
export default function renderPicture(
config: PictureConfigOptions
): Promise<PictureHTMLData>;

View File

@ -1,106 +0,0 @@
// @ts-check
import getImage from "./utils/getImage.js"
import getImgElement from "./utils/getImgElement.js"
import getLinkElement from "./utils/getLinkElement.js"
import getStyleElement from "./utils/getStyleElement.js"
import getLayoutStyles from "./utils/getLayoutStyles.js"
import getFilteredProps from "./utils/getFilteredProps.js"
import getPictureElement from "./utils/getPictureElement.js"
import getBackgroundStyles from "./utils/getBackgroundStyles.js"
export default async function renderPicture(props) {
const type = "Picture"
const { filteredProps, transformConfigs } = getFilteredProps(type, props)
const {
src,
alt,
sizes,
preload,
loading,
decoding,
attributes,
layout,
placeholder,
breakpoints,
objectFit,
objectPosition,
format,
fallbackFormat,
includeSourceFormat,
formatOptions,
fadeInTransition,
artDirectives,
} = filteredProps;
const {
img: imgAttributes = {},
link: linkAttributes = {},
style: styleAttributes = {},
picture: pictureAttributes = {},
} = attributes;
const { uuid, images } = await getImage({
src,
type,
sizes,
format,
breakpoints,
placeholder,
fallbackFormat,
includeSourceFormat,
formatOptions,
artDirectives,
transformConfigs,
})
const className = `imagetools-picture-${uuid}`
const { imagesizes } = images[images.length - 1]
const backgroundStyles = getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition
)
const style = getStyleElement({ styleAttributes, backgroundStyles })
const link = getLinkElement({ images, preload, imagesizes, linkAttributes })
const layoutStyles = getLayoutStyles({ layout })
const sources = images.flatMap(({ media, sources, sizes, imagesizes }) =>
sources.map(({ format, src, srcset }) =>
src
? getImgElement({
src,
alt,
sizes,
style,
srcset,
loading,
decoding,
imagesizes,
fadeInTransition,
layoutStyles,
imgAttributes,
})
: `<source
srcset="${srcset}"
sizes="${imagesizes}"
width="${sizes.width}"
height="${sizes.height}"
type="${`image/${format}`}"
${media ? `media="${media}"` : ""}
/>`
)
)
const picture = getPictureElement({
sources,
className,
layoutStyles,
pictureAttributes,
})
return { link, style, picture }
}

View File

@ -1,36 +0,0 @@
// @ts-check
import fs from "node:fs";
import { extname } from "node:path";
import * as codecs from "@astropub/codecs";
export async function getImageDetails(path, width, height, aspect) {
const extension = extname(path).slice(1);
const imageFormat = extension === "jpeg" ? "jpg" : extension;
const buffer = fs.readFileSync(path);
const decodedImage = await codecs.jpg.decode(buffer);
if (aspect && !width && !height) {
if (!width && !height) {
({ width } = decodedImage);
}
if (width) {
height = width / aspect;
}
if (height) {
width = height * aspect;
}
}
const image = await decodedImage.resize({ width, height });
const { width: imageWidth, height: imageHeight } = image;
return {
image,
imageWidth,
imageHeight,
imageFormat,
};
}

View File

@ -1,137 +0,0 @@
// @ts-check
import getSrcset from "./getSrcset.js";
import getConfigOptions from "./getConfigOptions.js";
import getFallbackImage from "./getFallbackImage.js";
import getProcessedImage from "./getProcessedImage.js";
export default async function getArtDirectedImages(
artDirectives = [],
placeholder,
format,
imagesizes,
breakpoints,
fallbackFormat,
includeSourceFormat,
formatOptions,
rest
) {
const images = await Promise.all(
artDirectives.map(
async ({
src,
media,
sizes: directiveImagesizes,
placeholder: directivePlaceholder,
breakpoints: directiveBreakpoints,
objectFit,
objectPosition,
backgroundSize,
backgroundPosition,
format: directiveFormat,
fallbackFormat: directiveFallbackFormat,
includeSourceFormat: directiveIncludeSourceFormat,
formatOptions: directiveFormatOptions = {},
...configOptions
}) => {
const {
path,
base,
rest: rest2,
image,
imageWidth,
imageHeight,
imageFormat,
} = await getProcessedImage(src, configOptions);
rest2.aspect = `${imageWidth / imageHeight}`;
const calculatedConfigs = getConfigOptions(
imageWidth,
directiveImagesizes || imagesizes,
directiveBreakpoints || breakpoints,
directiveFormat || format,
imageFormat,
directiveFallbackFormat || fallbackFormat,
directiveIncludeSourceFormat || includeSourceFormat
);
const { formats, requiredBreakpoints } = calculatedConfigs;
imagesizes = calculatedConfigs.imagesizes;
const maxWidth = requiredBreakpoints[requiredBreakpoints.length - 1];
const sources = await Promise.all(
formats.map(async (format) => {
const srcset = await getSrcset(
path,
base,
requiredBreakpoints,
format,
{
...rest,
...rest2,
...formatOptions[format],
...directiveFormatOptions[format],
}
);
return {
format,
srcset,
};
})
);
const sizes = {
width: maxWidth,
height: Math.round(maxWidth / rest2.aspect),
};
const object = {
fit: objectFit,
position: objectPosition,
};
const background = {
size: backgroundSize,
position: backgroundPosition,
};
const fallback = await getFallbackImage(
path,
directivePlaceholder || placeholder,
image,
imageFormat,
{ ...formatOptions, ...directiveFormatOptions },
{ ...rest, ...rest2 }
);
const returnValue = {
media,
sources,
sizes,
fallback,
imagesizes,
};
const isBackgroundImage = !!backgroundSize || !!backgroundPosition;
isBackgroundImage
? (returnValue.background = background)
: (returnValue.object = object);
return {
media,
sources,
sizes,
object,
fallback,
imagesizes,
};
}
)
);
return images;
}

View File

@ -1,27 +0,0 @@
// @ts-check
import printWarning from "../../utils/printWarning.js";
export default function getAttributesString({
attributes,
element = "",
excludeArray = [],
}) {
const attributesString = Object.keys(attributes)
.filter((key) => {
if (excludeArray.includes(key)) {
printWarning({
key,
element,
});
return false;
}
return true;
})
.map((key) => `${key}="${attributes[key]}"`)
.join(" ");
return attributesString;
}

View File

@ -1,97 +0,0 @@
// @ts-check
export default function getBackgroundStyles(
images,
className,
objectFit,
objectPosition,
fadeInTransition,
{ isImg = false, isBackgroundPicture = false, containerClassName = "" } = {}
) {
const sourcesWithFallback = images.filter(({ fallback }) => fallback);
if (sourcesWithFallback.length === 0) return "";
const staticStyles = !fadeInTransition
? ""
: `
${
isBackgroundPicture
? `
.${containerClassName} * {
z-index: 1;
position: relative;
}
`
: ""
}
.${className} {
--opacity: 1;
--z-index: 0;
}
${
!isBackgroundPicture
? `
.${className} img {
z-index: 1;
position: relative;
}
`
: ""
}
.${className}::after {
inset: 0;
content: "";
left: 0;
width: 100%;
height: 100%;
position: absolute;
pointer-events: none;
transition: opacity ${
typeof fadeInTransition !== "object"
? "1s"
: (() => {
const {
delay = "0s",
duration = "1s",
timingFunction = "ease",
} = fadeInTransition;
return `${duration} ${timingFunction} ${delay}`;
})()
};
opacity: var(--opacity);
z-index: var(--z-index);
}
`;
const dynamicStyles = images
.map(({ media, fallback, object }) => {
const elementSelector = className + (!isImg ? " img" : ""),
backgroundElementSelector =
className + (fadeInTransition ? "::after" : "");
const style = `
.${elementSelector} {
object-fit: ${object?.fit || objectFit};
object-position: ${object?.position || objectPosition};
}
.${backgroundElementSelector} {
background-size: ${object?.fit || objectFit};
background-image: url("${encodeURI(fallback)}");
background-position: ${object?.position || objectPosition};
}
`;
return media ? `@media ${media} { ${style} }` : style;
})
.reverse();
const backgroundStyles = [staticStyles, ...dynamicStyles].join("");
return backgroundStyles;
}

View File

@ -1,77 +0,0 @@
// @ts-check
import printWarning from "../../utils/printWarning.js";
export default function getBreakpoints(breakpoints, imageWidth) {
if (Array.isArray(breakpoints)) {
return breakpoints.sort((a, b) => a - b);
}
const { count, minWidth = 320 } = breakpoints || {};
const maxWidth = (() => {
if (breakpoints?.maxWidth) return breakpoints.maxWidth;
if (imageWidth > 3840) {
printWarning({
message:
"The width of the source image is greater than 3840px. The generated breakpoints will be capped at 3840px. If you need breakpoints larger than this, please pass the maxWidth option to the breakpoints property.",
});
return 3840;
}
return imageWidth;
})();
const breakPoints = [];
const diff = maxWidth - minWidth;
const n =
count ||
(maxWidth <= 400
? 1
: maxWidth <= 640
? 2
: maxWidth <= 800
? 3
: maxWidth <= 1024
? 4
: maxWidth <= 1280
? 5
: maxWidth <= 1440
? 6
: maxWidth <= 1920
? 7
: maxWidth <= 2560
? 8
: maxWidth <= 2880
? 9
: maxWidth <= 3840
? 10
: 11);
let currentWidth = minWidth;
n > 1 && breakPoints.push(currentWidth);
let steps = 0;
for (let i = 1; i < n; i++) {
steps += i;
}
const pixelsPerStep = diff / steps;
for (let i = 1; i < n - 1; i++) {
const next = pixelsPerStep * (n - i) + currentWidth;
breakPoints.push(Math.round(next));
currentWidth = next;
}
breakPoints.push(maxWidth);
return [...new Set(breakPoints)];
}

View File

@ -1,34 +0,0 @@
// @ts-check
import getBreakpoints from "./getBreakpoints.js";
export default function getConfigOptions(
imageWidth,
imagesizes,
breakpoints,
format,
imageFormat,
fallbackFormat,
includeSourceFormat
) {
const formats = [
...new Set(
[format, includeSourceFormat && imageFormat]
.flat()
.filter((f) => f && f !== fallbackFormat)
),
fallbackFormat,
];
const requiredBreakpoints = getBreakpoints(breakpoints, imageWidth);
imagesizes =
typeof imagesizes === "string"
? imagesizes
: imagesizes(requiredBreakpoints);
return {
formats,
imagesizes,
requiredBreakpoints,
};
}

View File

@ -1,48 +0,0 @@
// @ts-check
import getAttributesString from "./getAttributesString.js";
export default function getContainerElement({
tag,
content,
className = "",
containerAttributes,
isBackgroundPicture = false,
containerClassName = "",
}) {
const {
class: customClasses = "",
style: customInlineStyles = "",
...restContainerAttributes
} = containerAttributes;
const attributesString = getAttributesString({
attributes: restContainerAttributes,
});
const classAttribute = [
isBackgroundPicture
? "astro-imagetools-background-picture"
: "astro-imagetools-background-image",
isBackgroundPicture ? containerClassName : className,
customClasses,
]
.join(" ")
.trim();
const styleAttribute = [
isBackgroundPicture ? "position: relative;" : "",
customInlineStyles + (customInlineStyles.endsWith(";") ? "" : ";"),
]
.join(" ")
.trim();
const containerElement = `<${tag}
${attributesString}
class="${classAttribute}"
style="${styleAttribute}"
>
${content}
</${tag}>`;
return containerElement;
}

View File

@ -1,56 +0,0 @@
// @ts-check
import util from "node:util";
import potrace from "potrace";
import getSrcset from "./getSrcset.js";
import { sharp } from "../../utils/runtimeChecks.js";
export default async function getFallbackImage(
src,
placeholder,
image,
format,
formatOptions,
rest
) {
const base = null;
switch (placeholder) {
case "blurred": {
const dataUri = await getSrcset(src, base, [20], format, {
inline: true,
...rest,
...formatOptions[format],
});
return dataUri;
}
case "tracedSVG": {
const { function: fn, options } = formatOptions.tracedSVG;
const traceSVG = util.promisify(potrace[fn]);
const imageBuffer = sharp
? await image.toBuffer()
: Buffer.from(
(await image.encode(`image/${format === "jpg" ? "jpeg" : format}`))
.data
);
const tracedSVG = await traceSVG(imageBuffer, options);
return `data:image/svg+xml;utf8,${tracedSVG}`;
}
case "dominantColor": {
if (sharp) {
var { r, g, b } = (await image.stats()).dominant;
} else {
[r, g, b] = image.color;
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" style="background: rgb(${r},${g},${b})"></svg>`;
return `data:image/svg+xml;utf8,${svg}`;
}
default:
return null;
}
}

View File

@ -1,138 +0,0 @@
// @ts-check
import filterConfigs from "../../utils/filterConfigs.js";
import {
supportedConfigs,
GlobalConfigOptions,
} from "../../utils/runtimeChecks.js";
const GlobalOnlyProperties = ["cacheDir", "assetFileNames"];
const NonGlobalSupportedConfigs = supportedConfigs.filter(
(key) => !GlobalOnlyProperties.includes(key)
);
const NonProperties = {
Img: [
"tag",
"content",
"backgroundSize",
"backgroundPosition",
"fallbackFormat",
"includeSourceFormat",
"fadeInTransition",
"artDirectives",
],
Picture: ["tag", "content", "backgroundSize", "backgroundPosition"],
BackgroundImage: [
"alt",
"loading",
"decoding",
"layout",
"objectFit",
"objectPosition",
"fadeInTransition",
],
BackgroundPicture: ["alt", "backgroundSize", "backgroundPosition"],
};
const ImgProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.Img.includes(key)
),
PictureProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.Picture.includes(key)
),
BackgroundImageProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.BackgroundImage.includes(key)
),
BackgroundPictureProperties = NonGlobalSupportedConfigs.filter(
(key) => !NonProperties.BackgroundPicture.includes(key)
);
const SupportedProperties = {
Img: ImgProperties,
Picture: PictureProperties,
BackgroundImage: BackgroundImageProperties,
BackgroundPicture: BackgroundPictureProperties,
};
export default function getFilteredProps(type, props) {
const filteredGlobalConfigs = filterConfigs(
"Global",
GlobalConfigOptions,
SupportedProperties[type],
{ warn: false }
);
const { search, searchParams } = new URL(props.src, "file://");
props.src = props.src.replace(search, "");
const paramOptions = Object.fromEntries(searchParams);
const filteredLocalProps = filterConfigs(
type,
{
...paramOptions,
...props,
},
SupportedProperties[type]
);
const resolvedProps = {
...filteredGlobalConfigs,
...filteredLocalProps,
};
const {
src,
alt,
tag = "section",
content = "",
sizes = function (breakpoints) {
const maxWidth = breakpoints[breakpoints.length - 1];
return `(min-width: ${maxWidth}px) ${maxWidth}px, 100vw`;
},
preload,
loading = preload ? "eager" : "lazy",
decoding = "async",
attributes = {},
layout = "constrained",
placeholder = "blurred",
breakpoints,
objectFit = "cover",
objectPosition = "50% 50%",
backgroundSize = "cover",
backgroundPosition = "50% 50%",
format = type === "Img" ? undefined : ["avif", "webp"],
fallbackFormat,
includeSourceFormat = true,
formatOptions = {
tracedSVG: {
function: "trace",
},
},
fadeInTransition = true,
artDirectives,
...transformConfigs
} = resolvedProps;
// prettier-ignore
const allProps = {
src, alt, tag, content, sizes, preload, loading, decoding, attributes, layout, placeholder,
breakpoints, objectFit, objectPosition, backgroundSize, backgroundPosition, format,
fallbackFormat, includeSourceFormat, formatOptions, fadeInTransition, artDirectives,
...transformConfigs,
};
const filteredProps = filterConfigs(
type,
allProps,
SupportedProperties[type],
{ warn: false }
);
return {
filteredProps,
transformConfigs,
};
}

Some files were not shown because too many files have changed in this diff Show More