aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/image/src/loaders
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/image/src/loaders')
-rw-r--r--packages/integrations/image/src/loaders/index.ts311
-rw-r--r--packages/integrations/image/src/loaders/sharp.ts53
-rw-r--r--packages/integrations/image/src/loaders/squoosh.ts141
3 files changed, 0 insertions, 505 deletions
diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts
deleted file mode 100644
index 1d4f77680..000000000
--- a/packages/integrations/image/src/loaders/index.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-import { htmlColorNames, type NamedColor } from '../utils/colornames.js';
-
-/// <reference types="astro/astro-jsx" />
-export type InputFormat =
- | 'heic'
- | 'heif'
- | 'avif'
- | 'jpeg'
- | 'jpg'
- | 'png'
- | 'tiff'
- | 'webp'
- | 'gif'
- | 'svg';
-
-export type OutputFormatSupportsAlpha = 'avif' | 'png' | 'webp';
-export type OutputFormat = OutputFormatSupportsAlpha | 'jpeg' | 'jpg' | 'svg';
-
-export type ColorDefinition =
- | NamedColor
- | `#${string}`
- | `rgb(${number}, ${number}, ${number})`
- | `rgb(${number},${number},${number})`
- | `rgba(${number}, ${number}, ${number}, ${number})`
- | `rgba(${number},${number},${number},${number})`;
-
-export type CropFit = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
-
-export type CropPosition =
- | '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';
-
-export function isOutputFormat(value: string): value is OutputFormat {
- return ['avif', 'jpeg', 'jpg', 'png', 'webp', 'svg'].includes(value);
-}
-
-export function isOutputFormatSupportsAlpha(value: string): value is OutputFormatSupportsAlpha {
- return ['avif', 'png', 'webp'].includes(value);
-}
-
-export function isAspectRatioString(value: string): value is `${number}:${number}` {
- return /^\d*:\d*$/.test(value);
-}
-
-export function isColor(value: string): value is ColorDefinition {
- return (
- (htmlColorNames as string[]).includes(value.toLowerCase()) ||
- /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(value) ||
- /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i.test(value)
- );
-}
-
-export function parseAspectRatio(aspectRatio: TransformOptions['aspectRatio']) {
- if (!aspectRatio) {
- return undefined;
- }
-
- // parse aspect ratio strings, if required (ex: "16:9")
- if (typeof aspectRatio === 'number') {
- return aspectRatio;
- } else {
- const [width, height] = aspectRatio.split(':');
- return parseInt(width) / parseInt(height);
- }
-}
-
-/**
- * Defines the original image and transforms that need to be applied to it.
- */
-export interface TransformOptions {
- /**
- * Source for the original image file.
- *
- * For images in your project's repository, use the `src` relative to the `public` directory.
- * For remote images, provide the full URL.
- */
- src: string;
- /**
- * The alt tag of the image. This is used for accessibility and will be made required in a future version.
- * Empty string is allowed.
- */
- alt?: string;
- /**
- * The output format to be used in the optimized image.
- *
- * @default undefined The original image format will be used.
- */
- format?: OutputFormat | undefined;
- /**
- * The compression quality used during optimization.
- *
- * @default undefined Allows the image service to determine defaults.
- */
- quality?: number | undefined;
- /**
- * The desired width of the output image. Combine with `height` to crop the image
- * to an exact size, or `aspectRatio` to automatically calculate and crop the height.
- */
- width?: number | undefined;
- /**
- * The desired height of the output image. Combine with `height` to crop the image
- * to an exact size, or `aspectRatio` to automatically calculate and crop the width.
- */
- height?: number | undefined;
- /**
- * The desired aspect ratio of the output image. Combine with either `width` or `height`
- * to automatically calculate and crop the other dimension.
- *
- * @example 1.777 - numbers can be used for computed ratios, useful for doing `{width/height}`
- * @example "16:9" - strings can be used in the format of `{ratioWidth}:{ratioHeight}`.
- */
- aspectRatio?: number | `${number}:${number}` | undefined;
- /**
- * The background color to use when converting from a transparent image format to a
- * non-transparent format. This is useful for converting PNGs to JPEGs.
- *
- * @example "white" - a named color
- * @example "#ffffff" - a hex color
- * @example "rgb(255, 255, 255)" - an rgb color
- */
- background?: ColorDefinition | undefined;
- /**
- * How the image should be resized to fit both `height` and `width`.
- *
- * @default 'cover'
- */
- fit?: CropFit | undefined;
- /**
- * Position of the crop when fit is `cover` or `contain`.
- *
- * @default 'centre'
- */
- position?: CropPosition | undefined;
-}
-
-export interface HostedImageService<T extends TransformOptions = TransformOptions> {
- /**
- * Gets the HTML attributes needed for the server rendered `<img />` element.
- */
- getImageAttributes(transform: T): Promise<astroHTML.JSX.ImgHTMLAttributes>;
-}
-
-export interface SSRImageService<T extends TransformOptions = TransformOptions>
- extends HostedImageService<T> {
- /**
- * Gets the HTML attributes needed for the server rendered `<img />` element.
- */
- getImageAttributes(transform: T): Promise<Exclude<astroHTML.JSX.ImgHTMLAttributes, 'src'>>;
- /**
- * Serializes image transformation properties to URLSearchParams, used to build
- * the final `src` that points to the self-hosted SSR endpoint.
- *
- * @param transform @type {TransformOptions} defining the requested image transformation.
- */
- serializeTransform(transform: T): { searchParams: URLSearchParams };
- /**
- * The reverse of `serializeTransform(transform)`, this parsed the @type {TransformOptions} back out of a given URL.
- *
- * @param searchParams @type {URLSearchParams}
- * @returns @type {TransformOptions} used to generate the URL, or undefined if the URL isn't valid.
- */
- parseTransform(searchParams: URLSearchParams): T | undefined;
- /**
- * Performs the image transformations on the input image and returns both the binary data and
- * final image format of the optimized image.
- *
- * @param inputBuffer Binary buffer containing the original image.
- * @param transform @type {TransformOptions} defining the requested transformations.
- */
- transform(inputBuffer: Buffer, transform: T): Promise<{ data: Buffer; format: OutputFormat }>;
-}
-
-export type ImageService<T extends TransformOptions = TransformOptions> =
- | HostedImageService<T>
- | SSRImageService<T>;
-
-export function isHostedService(service: ImageService): service is ImageService {
- return 'getImageSrc' in service;
-}
-
-export function isSSRService(service: ImageService): service is SSRImageService {
- return 'transform' in service;
-}
-
-export abstract class BaseSSRService implements SSRImageService {
- async getImageAttributes(transform: TransformOptions) {
- // strip off the known attributes
- const { width, height, src, format, quality, aspectRatio, ...rest } = transform;
-
- return {
- ...rest,
- width: width,
- height: height,
- };
- }
-
- serializeTransform(transform: TransformOptions) {
- const searchParams = new URLSearchParams();
-
- if (transform.quality) {
- searchParams.append('q', transform.quality.toString());
- }
-
- if (transform.format) {
- searchParams.append('f', transform.format);
- }
-
- if (transform.width) {
- searchParams.append('w', transform.width.toString());
- }
-
- if (transform.height) {
- searchParams.append('h', transform.height.toString());
- }
-
- if (transform.aspectRatio) {
- searchParams.append('ar', transform.aspectRatio.toString());
- }
-
- if (transform.fit) {
- searchParams.append('fit', transform.fit);
- }
-
- if (transform.background) {
- searchParams.append('bg', transform.background);
- }
-
- if (transform.position) {
- searchParams.append('p', encodeURI(transform.position));
- }
-
- searchParams.append('href', transform.src);
-
- return { searchParams };
- }
-
- parseTransform(searchParams: URLSearchParams) {
- if (!searchParams.has('href')) {
- return undefined;
- }
-
- let transform: TransformOptions = { src: searchParams.get('href')! };
-
- if (searchParams.has('q')) {
- transform.quality = parseInt(searchParams.get('q')!);
- }
-
- if (searchParams.has('f')) {
- const format = searchParams.get('f')!;
- if (isOutputFormat(format)) {
- transform.format = format;
- }
- }
-
- if (searchParams.has('w')) {
- transform.width = parseInt(searchParams.get('w')!);
- }
-
- if (searchParams.has('h')) {
- transform.height = parseInt(searchParams.get('h')!);
- }
-
- if (searchParams.has('ar')) {
- const ratio = searchParams.get('ar')!;
-
- if (isAspectRatioString(ratio)) {
- transform.aspectRatio = ratio;
- } else {
- transform.aspectRatio = parseFloat(ratio);
- }
- }
-
- if (searchParams.has('fit')) {
- transform.fit = searchParams.get('fit') as typeof transform.fit;
- }
-
- if (searchParams.has('p')) {
- transform.position = decodeURI(searchParams.get('p')!) as typeof transform.position;
- }
-
- if (searchParams.has('bg')) {
- transform.background = searchParams.get('bg') as ColorDefinition;
- }
-
- return transform;
- }
-
- abstract transform(
- inputBuffer: Buffer,
- transform: TransformOptions
- ): Promise<{ data: Buffer; format: OutputFormat }>;
-}
diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts
deleted file mode 100644
index 517224289..000000000
--- a/packages/integrations/image/src/loaders/sharp.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import sharp from 'sharp';
-import type { SSRImageService } from '../loaders/index.js';
-import { BaseSSRService, isOutputFormatSupportsAlpha } from '../loaders/index.js';
-import type { OutputFormat, TransformOptions } from './index.js';
-
-class SharpService extends BaseSSRService {
- async transform(inputBuffer: Buffer, transform: TransformOptions) {
- if (transform.format === 'svg') {
- // sharp can't output SVG so we return the input image
- return {
- data: inputBuffer,
- format: transform.format,
- };
- }
-
- const sharpImage = sharp(inputBuffer, { failOnError: false, pages: -1 });
-
- // always call rotate to adjust for EXIF data orientation
- sharpImage.rotate();
-
- if (transform.width || transform.height) {
- const width = transform.width && Math.round(transform.width);
- const height = transform.height && Math.round(transform.height);
-
- sharpImage.resize({
- width,
- height,
- fit: transform.fit,
- position: transform.position,
- background: transform.background,
- });
- }
-
- if (transform.format) {
- sharpImage.toFormat(transform.format, { quality: transform.quality });
-
- if (transform.background && !isOutputFormatSupportsAlpha(transform.format)) {
- sharpImage.flatten({ background: transform.background });
- }
- }
-
- const { data, info } = await sharpImage.toBuffer({ resolveWithObject: true });
-
- return {
- data,
- format: info.format as OutputFormat,
- };
- }
-}
-
-const service: SSRImageService = new SharpService();
-
-export default service;
diff --git a/packages/integrations/image/src/loaders/squoosh.ts b/packages/integrations/image/src/loaders/squoosh.ts
deleted file mode 100644
index 16eed032a..000000000
--- a/packages/integrations/image/src/loaders/squoosh.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { red } from 'kleur/colors';
-import { error } from '../utils/logger.js';
-import { metadata } from '../utils/metadata.js';
-import { isRemoteImage } from '../utils/paths.js';
-import type { Operation } from '../vendor/squoosh/image.js';
-import type { OutputFormat, TransformOptions } from './index.js';
-import { BaseSSRService } from './index.js';
-
-const imagePoolModulePromise = import('../vendor/squoosh/image-pool.js');
-
-class SquooshService extends BaseSSRService {
- async processAvif(image: any, transform: TransformOptions) {
- const encodeOptions = transform.quality
- ? { avif: { quality: transform.quality } }
- : { avif: {} };
- await image.encode(encodeOptions);
- const data = await image.encodedWith.avif;
-
- return {
- data: data.binary,
- format: 'avif' as OutputFormat,
- };
- }
-
- async processJpeg(image: any, transform: TransformOptions) {
- const encodeOptions = transform.quality
- ? { mozjpeg: { quality: transform.quality } }
- : { mozjpeg: {} };
- await image.encode(encodeOptions);
- const data = await image.encodedWith.mozjpeg;
-
- return {
- data: data.binary,
- format: 'jpeg' as OutputFormat,
- };
- }
-
- async processPng(image: any) {
- await image.encode({ oxipng: {} });
- const data = await image.encodedWith.oxipng;
-
- return {
- data: data.binary,
- format: 'png' as OutputFormat,
- };
- }
-
- async processWebp(image: any, transform: TransformOptions) {
- const encodeOptions = transform.quality
- ? { webp: { quality: transform.quality } }
- : { webp: {} };
- await image.encode(encodeOptions);
- const data = await image.encodedWith.webp;
-
- return {
- data: data.binary,
- format: 'webp' as OutputFormat,
- };
- }
-
- async autorotate(
- transform: TransformOptions,
- inputBuffer: Buffer
- ): Promise<Operation | undefined> {
- // check EXIF orientation data and rotate the image if needed
- try {
- const meta = await metadata(transform.src, inputBuffer);
-
- switch (meta?.orientation) {
- case 3:
- case 4:
- return { type: 'rotate', numRotations: 2 };
- case 5:
- case 6:
- return { type: 'rotate', numRotations: 1 };
- case 7:
- case 8:
- return { type: 'rotate', numRotations: 3 };
- }
- } catch {
- error({
- level: 'info',
- prefix: false,
- message: red(`Cannot read metadata for ${transform.src}`),
- });
- }
- }
-
- async transform(inputBuffer: Buffer, transform: TransformOptions) {
- if (transform.format === 'svg') {
- // squoosh can't output SVG so we return the input image
- return {
- data: inputBuffer,
- format: transform.format,
- };
- }
-
- const operations: Operation[] = [];
-
- if (!isRemoteImage(transform.src)) {
- const autorotate = await this.autorotate(transform, inputBuffer);
-
- if (autorotate) {
- operations.push(autorotate);
- }
- } else if (transform.src.startsWith('//')) {
- transform.src = `https:${transform.src}`;
- }
-
- if (transform.width || transform.height) {
- const width = transform.width && Math.round(transform.width);
- const height = transform.height && Math.round(transform.height);
-
- operations.push({
- type: 'resize',
- width,
- height,
- });
- }
-
- if (!transform.format) {
- error({
- level: 'info',
- prefix: false,
- message: red(`Unknown image output: "${transform.format}" used for ${transform.src}`),
- });
- throw new Error(`Unknown image output: "${transform.format}" used for ${transform.src}`);
- }
- const { processBuffer } = await imagePoolModulePromise;
- const data = await processBuffer(inputBuffer, operations, transform.format, transform.quality);
-
- return {
- data: Buffer.from(data),
- format: transform.format,
- };
- }
-}
-
-const service = new SquooshService();
-
-export default service;