diff options
Diffstat (limited to 'packages/integrations/image/src/loaders')
-rw-r--r-- | packages/integrations/image/src/loaders/index.ts | 311 | ||||
-rw-r--r-- | packages/integrations/image/src/loaders/sharp.ts | 53 | ||||
-rw-r--r-- | packages/integrations/image/src/loaders/squoosh.ts | 141 |
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; |