summaryrefslogtreecommitdiff
path: root/packages/integrations/image/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/image/src')
-rw-r--r--packages/integrations/image/src/constants.ts3
-rw-r--r--packages/integrations/image/src/get-image.ts128
-rw-r--r--packages/integrations/image/src/get-picture.ts79
-rw-r--r--packages/integrations/image/src/index.ts54
-rw-r--r--packages/integrations/image/src/loaders/sharp.ts1
-rw-r--r--packages/integrations/image/src/types.ts21
-rw-r--r--packages/integrations/image/src/utils.ts14
7 files changed, 244 insertions, 56 deletions
diff --git a/packages/integrations/image/src/constants.ts b/packages/integrations/image/src/constants.ts
new file mode 100644
index 000000000..db52614c5
--- /dev/null
+++ b/packages/integrations/image/src/constants.ts
@@ -0,0 +1,3 @@
+export const PKG_NAME = '@astrojs/image';
+export const ROUTE_PATTERN = '/_image';
+export const OUTPUT_DIR = '/_image';
diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts
new file mode 100644
index 000000000..ae423c3de
--- /dev/null
+++ b/packages/integrations/image/src/get-image.ts
@@ -0,0 +1,128 @@
+import slash from 'slash';
+import { ROUTE_PATTERN } from './constants.js';
+import { ImageAttributes, ImageMetadata, ImageService, isSSRService, OutputFormat, TransformOptions } from './types.js';
+import { parseAspectRatio } from './utils.js';
+
+export interface GetImageTransform extends Omit<TransformOptions, 'src'> {
+ src: string | ImageMetadata | Promise<{ default: ImageMetadata }>;
+}
+
+function resolveSize(transform: TransformOptions): TransformOptions {
+ // keep width & height as provided
+ if (transform.width && transform.height) {
+ return transform;
+ }
+
+ if (!transform.width && !transform.height) {
+ throw new Error(`"width" and "height" cannot both be undefined`);
+ }
+
+ if (!transform.aspectRatio) {
+ throw new Error(`"aspectRatio" must be included if only "${transform.width ? "width": "height"}" is provided`)
+ }
+
+ let aspectRatio: number;
+
+ // parse aspect ratio strings, if required (ex: "16:9")
+ if (typeof transform.aspectRatio === 'number') {
+ aspectRatio = transform.aspectRatio;
+ } else {
+ const [width, height] = transform.aspectRatio.split(':');
+ aspectRatio = Number.parseInt(width) / Number.parseInt(height);
+ }
+
+ if (transform.width) {
+ // only width was provided, calculate height
+ return {
+ ...transform,
+ width: transform.width,
+ height: Math.round(transform.width / aspectRatio)
+ } as TransformOptions;
+ } else if (transform.height) {
+ // only height was provided, calculate width
+ return {
+ ...transform,
+ width: Math.round(transform.height * aspectRatio),
+ height: transform.height
+ };
+ }
+
+ return transform;
+}
+
+async function resolveTransform(input: GetImageTransform): Promise<TransformOptions> {
+ // for remote images, only validate the width and height props
+ if (typeof input.src === 'string') {
+ return resolveSize(input as TransformOptions);
+ }
+
+ // resolve the metadata promise, usually when the ESM import is inlined
+ const metadata = 'then' in input.src
+ ? (await input.src).default
+ : input.src;
+
+ let { width, height, aspectRatio, format = metadata.format, ...rest } = input;
+
+ if (!width && !height) {
+ // neither dimension was provided, use the file metadata
+ width = metadata.width;
+ height = metadata.height;
+ } else if (width) {
+ // one dimension was provided, calculate the other
+ let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height;
+ height = height || Math.round(width / ratio);
+ } else if (height) {
+ // one dimension was provided, calculate the other
+ let ratio = parseAspectRatio(aspectRatio) || metadata.width / metadata.height;
+ width = width || Math.round(height * ratio);
+ }
+
+ return {
+ ...rest,
+ src: metadata.src,
+ width,
+ height,
+ aspectRatio,
+ format: format as OutputFormat,
+ }
+}
+
+/**
+ * Gets the HTML attributes required to build an `<img />` for the transformed image.
+ *
+ * @param loader @type {ImageService} The image service used for transforming images.
+ * @param transform @type {TransformOptions} The transformations requested for the optimized image.
+ * @returns @type {ImageAttributes} The HTML attributes to be included on the built `<img />` element.
+ */
+ export async function getImage(
+ loader: ImageService,
+ transform: GetImageTransform
+): Promise<ImageAttributes> {
+ (globalThis as any).loader = loader;
+
+ const resolved = await resolveTransform(transform);
+ const attributes = await loader.getImageAttributes(resolved);
+
+ // For SSR services, build URLs for the injected route
+ if (isSSRService(loader)) {
+ const { searchParams } = loader.serializeTransform(resolved);
+
+ // cache all images rendered to HTML
+ if (globalThis && (globalThis as any).addStaticImage) {
+ (globalThis as any)?.addStaticImage(resolved);
+ }
+
+ const src =
+ globalThis && (globalThis as any).filenameFormat
+ ? (globalThis as any).filenameFormat(resolved, searchParams)
+ : `${ROUTE_PATTERN}?${searchParams.toString()}`;
+
+ return {
+ ...attributes,
+ src: slash(src), // Windows compat
+ };
+ }
+
+ // For hosted services, return the `<img />` attributes as-is
+ return attributes;
+}
diff --git a/packages/integrations/image/src/get-picture.ts b/packages/integrations/image/src/get-picture.ts
new file mode 100644
index 000000000..370da0678
--- /dev/null
+++ b/packages/integrations/image/src/get-picture.ts
@@ -0,0 +1,79 @@
+import { lookup } from 'mrmime';
+import { extname } from 'path';
+import { getImage } from './get-image.js';
+import { ImageAttributes, ImageMetadata, ImageService, OutputFormat, TransformOptions } from './types.js';
+import { parseAspectRatio } from './utils.js';
+
+export interface GetPictureParams {
+ loader: ImageService;
+ src: string | ImageMetadata | Promise<{ default: ImageMetadata }>;
+ widths: number[];
+ formats: OutputFormat[];
+ aspectRatio?: TransformOptions['aspectRatio'];
+}
+
+export interface GetPictureResult {
+ image: ImageAttributes;
+ sources: { type: string; srcset: string; }[];
+}
+
+async function resolveAspectRatio({ src, aspectRatio }: GetPictureParams) {
+ if (typeof src === 'string') {
+ return parseAspectRatio(aspectRatio);
+ } else {
+ const metadata = 'then' in src ? (await src).default : src;
+ return parseAspectRatio(aspectRatio) || metadata.width / metadata.height;
+ }
+}
+
+async function resolveFormats({ src, formats }: GetPictureParams) {
+ const unique = new Set(formats);
+
+ if (typeof src === 'string') {
+ unique.add(extname(src).replace('.', '') as OutputFormat);
+ } else {
+ const metadata = 'then' in src ? (await src).default : src;
+ unique.add(extname(metadata.src).replace('.', '') as OutputFormat);
+ }
+
+ return [...unique];
+}
+
+export async function getPicture(params: GetPictureParams): Promise<GetPictureResult> {
+ const { loader, src, widths, formats } = params;
+
+ const aspectRatio = await resolveAspectRatio(params);
+
+ if (!aspectRatio) {
+ throw new Error('`aspectRatio` must be provided for remote images');
+ }
+
+ async function getSource(format: OutputFormat) {
+ const imgs = await Promise.all(widths.map(async (width) => {
+ const img = await getImage(loader, { src, format, width, height: Math.round(width / aspectRatio!) });
+ return `${img.src} ${width}w`;
+ }))
+
+ return {
+ type: lookup(format) || format,
+ srcset: imgs.join(',')
+ };
+ }
+
+ // always include the original image format
+ const allFormats = await resolveFormats(params);
+
+ const image = await getImage(loader, {
+ src,
+ width: Math.max(...widths),
+ aspectRatio,
+ format: allFormats[allFormats.length - 1]
+ });
+
+ const sources = await Promise.all(allFormats.map(format => getSource(format)));
+
+ return {
+ sources,
+ image
+ }
+}
diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts
index f87fcd4b2..8b06484e7 100644
--- a/packages/integrations/image/src/index.ts
+++ b/packages/integrations/image/src/index.ts
@@ -1,14 +1,11 @@
import type { AstroConfig, AstroIntegration } from 'astro';
import fs from 'fs/promises';
import path from 'path';
-import slash from 'slash';
import { fileURLToPath } from 'url';
-import type {
- ImageAttributes,
- IntegrationOptions,
- SSRImageService,
- TransformOptions,
-} from './types';
+import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './constants.js';
+export * from './get-image.js';
+export * from './get-picture.js';
+import { IntegrationOptions, TransformOptions } from './types.js';
import {
ensureDir,
isRemoteImage,
@@ -18,49 +15,6 @@ import {
} from './utils.js';
import { createPlugin } from './vite-plugin-astro-image.js';
-const PKG_NAME = '@astrojs/image';
-const ROUTE_PATTERN = '/_image';
-const OUTPUT_DIR = '/_image';
-
-/**
- * Gets the HTML attributes required to build an `<img />` for the transformed image.
- *
- * @param loader @type {ImageService} The image service used for transforming images.
- * @param transform @type {TransformOptions} The transformations requested for the optimized image.
- * @returns @type {ImageAttributes} The HTML attributes to be included on the built `<img />` element.
- */
-export async function getImage(
- loader: SSRImageService,
- transform: TransformOptions
-): Promise<ImageAttributes> {
- (globalThis as any).loader = loader;
-
- const attributes = await loader.getImageAttributes(transform);
-
- // For SSR services, build URLs for the injected route
- if (typeof loader.transform === 'function') {
- const { searchParams } = loader.serializeTransform(transform);
-
- // cache all images rendered to HTML
- if (globalThis && (globalThis as any).addStaticImage) {
- (globalThis as any)?.addStaticImage(transform);
- }
-
- const src =
- globalThis && (globalThis as any).filenameFormat
- ? (globalThis as any).filenameFormat(transform, searchParams)
- : `${ROUTE_PATTERN}?${searchParams.toString()}`;
-
- return {
- ...attributes,
- src: slash(src), // Windows compat
- };
- }
-
- // For hosted services, return the <img /> attributes as-is
- return attributes;
-}
-
const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => {
const resolvedOptions = {
serviceEntryPoint: '@astrojs/image/sharp',
diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts
index b82a75044..86c18839d 100644
--- a/packages/integrations/image/src/loaders/sharp.ts
+++ b/packages/integrations/image/src/loaders/sharp.ts
@@ -4,6 +4,7 @@ import { isAspectRatioString, isOutputFormat } from '../utils.js';
class SharpService implements SSRImageService {
async getImageAttributes(transform: TransformOptions) {
+ // strip off the known attributes
const { width, height, src, format, quality, aspectRatio, ...rest } = transform;
return {
diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts
index b55feb7c5..427aaf7cf 100644
--- a/packages/integrations/image/src/types.ts
+++ b/packages/integrations/image/src/types.ts
@@ -1,5 +1,6 @@
-export type { Image } from '../components/index';
-export * from './index';
+/// <reference types="astro/astro-jsx" />
+export type { Image, Picture } from '../components/index.js';
+export * from './index.js';
export type InputFormat =
| 'heic'
@@ -72,7 +73,8 @@ export interface TransformOptions {
aspectRatio?: number | `${number}:${number}`;
}
-export type ImageAttributes = Partial<HTMLImageElement>;
+export type ImageAttributes = astroHTML.JSX.ImgHTMLAttributes;
+export type PictureAttributes = astroHTML.JSX.HTMLAttributes;
export interface HostedImageService<T extends TransformOptions = TransformOptions> {
/**
@@ -81,10 +83,9 @@ export interface HostedImageService<T extends TransformOptions = TransformOption
getImageAttributes(transform: T): Promise<ImageAttributes>;
}
-export interface SSRImageService<T extends TransformOptions = TransformOptions>
- extends HostedImageService<T> {
+export interface SSRImageService<T extends TransformOptions = TransformOptions> extends HostedImageService<T> {
/**
- * Gets the HTML attributes needed for the server rendered `<img />` element.
+ * Gets tthe HTML attributes needed for the server rendered `<img />` element.
*/
getImageAttributes(transform: T): Promise<Exclude<ImageAttributes, 'src'>>;
/**
@@ -115,6 +116,14 @@ 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 interface ImageMetadata {
src: string;
width: number;
diff --git a/packages/integrations/image/src/utils.ts b/packages/integrations/image/src/utils.ts
index 95e0fb2a1..44c338cf4 100644
--- a/packages/integrations/image/src/utils.ts
+++ b/packages/integrations/image/src/utils.ts
@@ -58,3 +58,17 @@ export function propsToFilename({ src, width, height, format }: TransformOptions
return format ? src.replace(ext, format) : src;
}
+
+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);
+ }
+}