stuff like that :)

This commit is contained in:
babayaga 2025-12-26 18:54:45 +01:00
parent 427e57820b
commit 8aea9145c6
374 changed files with 196 additions and 43274 deletions

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,58 +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,
};
}

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,80 +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;
}
try {
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.createHash('md5').update(src).digest('hex').substring(0, 5)
const uuid = crypto.randomBytes(4).toString("hex").toUpperCase()
const returnObject = {
uuid,
images,
}
imagesData.set(hash, returnObject)
return returnObject;
} catch (error) {
console.error(`Error getImage :${src}`, error)
}
}

View File

@ -1,74 +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 = "",
uuid = ""
}) {
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 = ["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}"
uuid="${uuid}"
onload="${onloadAttribute}"
/>`;
return imgElement;
}

View File

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

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 = ["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,52 +0,0 @@
import { fileURLToPath } from "node:url"
import { extname, relative, resolve } from "node:path"
import { getSrcPath } from "./getSrcPath.js"
import getResolvedSrc from "./getResolvedSrc.js"
import { cwd, sharp, fsCachePath } 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;
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,45 +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 token = crypto.randomBytes(4).toString("hex").toUpperCase()
let filepath = fsCachePath + token;
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,41 +0,0 @@
// @ts-check
import {
builtins,
loadImage,
applyTransforms,
generateTransforms,
} from "imagetools-core";
export {
loadImage
} from "imagetools-core";
export async function getImageDetails(path, width, height, aspect) {
const loadedImage = 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).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,12 +0,0 @@
export default {
"environment": "build",
"isSsrBuild": false,
"projectBase": "",
"publicDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\public\\",
"rootDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\",
"mode": "production",
"outDir": "C:\\Users\\zx\\Desktop\\polymech\\site-min\\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,8 +0,0 @@
---
import renderImage from "../api/renderImg.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,7 +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,2 +0,0 @@
import imagetools from "./integration/index.js"
export { imagetools }

View File

@ -1,79 +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";
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),
};
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(
// @ts-ignore
"../astroViteConfigs.js"
);
const { mode, outDir, assetsDir, isSsrBuild } = astroViteConfigs;
if (mode === "production") {
const allEntries = [...store.entries()];
const assetPaths = allEntries.filter(
([, { hash = null } = {}]) => hash
);
await Promise.all(
assetPaths.map(
async ([assetPath, { hash, image, buffer }]) =>
await saveAndCopyAsset(
hash,
image,
buffer,
outDir,
assetsDir,
assetPath,
isSsrBuild
)
)
);
}
},
},
};

View File

@ -1,46 +0,0 @@
import fs from "node:fs/promises";
import { posix as path } from "node:path";
import { fsCachePath } from "../../utils/runtimeChecks.js";
const copied = [];
let assetsDirExists;
export async function saveAndCopyAsset(
hash,
image,
buffer,
outDir,
assetsDir,
assetPath,
isSsrBuild
) {
const src = fsCachePath + hash;
const dest = path.join(outDir, isSsrBuild ? "/client" : "", assetPath);
assetsDir = path.join(outDir, isSsrBuild ? "/client" : "/", assetsDir);
if (copied.includes(assetPath)) return;
if (!assetsDirExists) {
await fs.mkdir(assetsDir, {
recursive: true,
});
assetsDirExists = true;
}
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,69 +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/commons": "file:../../../polymech-mono/packages/commons",
"@polymech/log": "file:../../../polymech-mono/packages/log",
"@polymech/fs": "file:../../../polymech-mono/packages/fs",
"file-type": "17.1.1",
"find-cache-dir": "3.3.2",
"find-up": "^6.3.0",
"node-addon-api": "^8.3.0",
"node-gyp": "^11.1.0",
"object-hash": "3.0.0",
"potrace": "2.1.8",
"sharp": "^0.33.5"
},
"optionalDependencies": {
"imagetools-core": "3.0.2"
},
"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,144 +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";
const { getLoadedImage, getTransformedImage } = await import("../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
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,63 +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);
//@todo
const uuid = crypto.randomBytes(4).toString("hex");
const Picture = "Picture" + uuid;
const renderComponent = "renderComponent" + uuid;
s.prepend(
`import { Picture as ${Picture} } from "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,86 +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,13 +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,26 +0,0 @@
// @ts-check
import {
builtins,
loadImage,
applyTransforms,
generateTransforms,
} from "imagetools-core";
export const getLoadedImage = async (src) => {
const image = loadImage(src);
const { width } = await image.metadata();
return { image, width };
};
export const getTransformedImage = async ({ image, config }) => {
const { transforms } = generateTransforms(config, builtins);
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,14 +0,0 @@
<img src="/_astro/perspective-bare@1988w.1118b833.jpeg"
alt="Sheetpress Cassandra - EDC450"
srcset="/_astro/perspective-bare@320w.eeb3c31c.jpeg 320w,
/_astro/perspective-bare@737w.089cce48.jpeg 737w,
/_astro/perspective-bare@1094w.3196ee2f.jpeg 1094w,
/_astro/perspective-bare@1392w.3b9d575f.jpeg 1392w,
/_astro/perspective-bare@1631w.364fe88c.jpeg 1631w,
/_astro/perspective-bare@1809w.1d7248da.jpeg 1809w,
/_astro/perspective-bare@1928w.22f075d7.jpeg 1928w,
/_astro/perspective-bare@1988w.1118b833.jpeg 1988w"
sizes="(min-width: 1988px) 1988px, 100vw" width="1988" height="1536"
loading="lazy" decoding="async" class="imagetools-img"
style="display: inline-block; overflow: hidden; vertical-align: middle; ; max-width: 100%; height: auto;"
onload="parentElement.style.setProperty('--z-index', 1); parentElement.style.setProperty('--opacity', 0);">

View File

@ -1,269 +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";
class: string;
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,49 +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,65 +0,0 @@
import fs from "node:fs"
import path from "node:path"
import filterConfigs from "./filterConfigs.js"
import { cache_path } from "@polymech/cache"
import { sync as dir } from "@polymech/fs/dir"
export const sharp = await (async () => {
try {
if (await import("sharp")) {
return true;
}
} catch (error) {
return false;
}
})();
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 = await findUp([
"astro-imagetools.config.js",
"astro-imagetools.config.mjs",
]);
*/
/*
const configFunction = configFile
? await import(configFile).catch(async () => await import("/" + configFile))
: null;
*/
const configFunction = null
//const rawGlobalConfigOptions = configFunction?.default ?? {};
const rawGlobalConfigOptions = {}
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)
let fsCachePath = `${cache_path('imagetools')}/`
dir(fsCachePath)
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,17 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2020: true,
},
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
},
plugins: ["unicorn"],
extends: ["eslint:recommended"],
rules: {
"unicorn/prefer-node-protocol": "error",
},
};

View File

@ -1,65 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
lint:
env:
ASTRO_TELEMETRY_DISABLED: true
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2.2.1
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Prettier
run: pnpm run format:check
- name: ESLint
run: pnpm run lint
test:
name: "Test: ${{ matrix.os }} (node@${{ matrix.node_version }})"
env:
ASTRO_TELEMETRY_DISABLED: true
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node_version: [14, 16]
include:
- os: windows-latest
node_version: 16
- os: macos-latest
node_version: 16
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2.2.1
- name: Setup node@${{ matrix.node_version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node_version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Test
run: pnpm --filter astro-imagetools run test

View File

@ -1,20 +0,0 @@
# dependencies
node_modules/
# build output
dist/
# logs
*.log
# npm
package-lock.json
# macOS-specific files
.DS_Store
# env
*.env
# astro-imagetools
packages/astro-imagetools/astroViteConfigs.js

View File

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

View File

@ -1,2 +0,0 @@
## force pnpm to hoist
shamefully-hoist = true

View File

@ -1,2 +0,0 @@
pnpm-lock.yaml
demo/dist

View File

@ -1,9 +0,0 @@
{
"overrides": [
{
"files": "**/*.astro",
"options": { "parser": "astro" }
}
],
"plugins": ["prettier-plugin-astro"]
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Rafid Muhymin Wafi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,159 +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,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 = `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,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;
}

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