diff --git a/.changeset/large-geese-play.md b/.changeset/large-geese-play.md new file mode 100644 index 000000000000..2cfd9788df9a --- /dev/null +++ b/.changeset/large-geese-play.md @@ -0,0 +1,20 @@ +--- +"astro": minor +--- + +Adds a new `inferRemoteSize()` function that can be used to infer the dimensions of a remote image. + +Previously, the ability to infer these values was only available by adding the [`inferSize`] attribute to the `` and `` components or `getImage()`. Now, you can also access this data outside of these components. + +This is useful for when you need to know the dimensions of an image for styling purposes or to calculate different densities for responsive images. + +```astro +--- +import { inferRemoteSize, Image } from 'astro:assets'; + +const imageUrl = 'https://...'; +const { width, height } = await inferRemoteSize(imageUrl); +--- + + +``` diff --git a/packages/astro/client.d.ts b/packages/astro/client.d.ts index 0870d3dcc566..d74e5fa488cb 100644 --- a/packages/astro/client.d.ts +++ b/packages/astro/client.d.ts @@ -54,6 +54,7 @@ declare module 'astro:assets' { ) => Promise; imageConfig: import('./dist/@types/astro.js').AstroConfig['image']; getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService; + inferRemoteSize: typeof import('./dist/assets/utils/index.js').inferRemoteSize; Image: typeof import('./components/Image.astro').default; Picture: typeof import('./components/Picture.astro').default; }; diff --git a/packages/astro/package.json b/packages/astro/package.json index 1fc93648d908..877713ffac1d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -63,6 +63,7 @@ "./actions/runtime/*": "./dist/actions/runtime/*", "./assets": "./dist/assets/index.js", "./assets/utils": "./dist/assets/utils/index.js", + "./assets/utils/inferRemoteSize.js": "./dist/assets/utils/remoteProbe.js", "./assets/endpoint/*": "./dist/assets/endpoint/*.js", "./assets/services/sharp": "./dist/assets/services/sharp.js", "./assets/services/squoosh": "./dist/assets/services/squoosh.js", diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts index 8770f27b5998..7265e85dfb10 100644 --- a/packages/astro/src/assets/internal.ts +++ b/packages/astro/src/assets/internal.ts @@ -10,7 +10,7 @@ import { isImageMetadata, } from './types.js'; import { isESMImportedImage, isRemoteImage, resolveSrc } from './utils/imageKind.js'; -import { probe } from './utils/remoteProbe.js'; +import { inferRemoteSize } from './utils/remoteProbe.js'; export async function getConfiguredImageService(): Promise { if (!globalThis?.astroAsset?.imageService) { @@ -66,17 +66,10 @@ export async function getImage( // Infer size for remote images if inferSize is true if (options.inferSize && isRemoteImage(resolvedOptions.src)) { - try { - const result = await probe(resolvedOptions.src); // Directly probe the image URL - resolvedOptions.width ??= result.width; - resolvedOptions.height ??= result.height; - delete resolvedOptions.inferSize; // Delete so it doesn't end up in the attributes - } catch { - throw new AstroError({ - ...AstroErrorData.FailedToFetchRemoteImageDimensions, - message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(resolvedOptions.src), - }); - } + const result = await inferRemoteSize(resolvedOptions.src); // Directly probe the image URL + resolvedOptions.width ??= result.width; + resolvedOptions.height ??= result.height; + delete resolvedOptions.inferSize; // Delete so it doesn't end up in the attributes } const originalFilePath = isESMImportedImage(resolvedOptions.src) diff --git a/packages/astro/src/assets/utils/index.ts b/packages/astro/src/assets/utils/index.ts index 4fb4e42db373..69e7c88dc401 100644 --- a/packages/astro/src/assets/utils/index.ts +++ b/packages/astro/src/assets/utils/index.ts @@ -1,4 +1,4 @@ -export { emitESMImage } from './emitAsset.js'; +export { emitESMImage } from './node/emitAsset.js'; export { isESMImportedImage, isRemoteImage } from './imageKind.js'; export { imageMetadata } from './metadata.js'; export { getOrigQueryParams } from './queryParams.js'; @@ -12,3 +12,4 @@ export { type RemotePattern, } from './remotePattern.js'; export { hashTransform, propsToFilename } from './transformToPath.js'; +export { inferRemoteSize } from './remoteProbe.js'; diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/node/emitAsset.ts similarity index 93% rename from packages/astro/src/assets/utils/emitAsset.ts rename to packages/astro/src/assets/utils/node/emitAsset.ts index 1b6bb207bbb1..3a590e3a6814 100644 --- a/packages/astro/src/assets/utils/emitAsset.ts +++ b/packages/astro/src/assets/utils/node/emitAsset.ts @@ -2,9 +2,9 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type * as vite from 'vite'; -import { prependForwardSlash, slash } from '../../core/path.js'; -import type { ImageMetadata } from '../types.js'; -import { imageMetadata } from './metadata.js'; +import { prependForwardSlash, slash } from '../../../core/path.js'; +import type { ImageMetadata } from '../../types.js'; +import { imageMetadata } from '../metadata.js'; type FileEmitter = vite.Rollup.EmitFile; diff --git a/packages/astro/src/assets/utils/remoteProbe.ts b/packages/astro/src/assets/utils/remoteProbe.ts index 1cda4ca45818..c71413069e68 100644 --- a/packages/astro/src/assets/utils/remoteProbe.ts +++ b/packages/astro/src/assets/utils/remoteProbe.ts @@ -1,11 +1,15 @@ -import { lookup } from './vendor/image-size/lookup.js'; -import type { ISize } from './vendor/image-size/types/interface.ts'; +import { AstroError, AstroErrorData } from '../../core/errors/index.js'; +import type { ImageMetadata } from '../types.js'; +import { imageMetadata } from './metadata.js'; -export async function probe(url: string): Promise { +export async function inferRemoteSize(url: string): Promise> { // Start fetching the image const response = await fetch(url); if (!response.body || !response.ok) { - throw new Error('Failed to fetch image'); + throw new AstroError({ + ...AstroErrorData.FailedToFetchRemoteImageDimensions, + message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url), + }); } const reader = response.body.getReader(); @@ -31,17 +35,22 @@ export async function probe(url: string): Promise { try { // Attempt to determine the size with each new chunk - const dimensions = lookup(accumulatedChunks); + const dimensions = await imageMetadata(accumulatedChunks, url); + if (dimensions) { await reader.cancel(); // stop stream as we have size now + return dimensions; } } catch (error) { - // This catch block is specifically for `sizeOf` failures, + // This catch block is specifically for `imageMetadata` errors // which might occur if the accumulated data isn't yet sufficient. } } } - throw new Error('Failed to parse the size'); + throw new AstroError({ + ...AstroErrorData.NoImageMetadata, + message: AstroErrorData.NoImageMetadata.message(url), + }); } diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index eda4b6cfbc2a..42401f5dc4fb 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -14,7 +14,7 @@ import { } from '../core/path.js'; import { isServerLikeOutput } from '../core/util.js'; import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js'; -import { emitESMImage } from './utils/emitAsset.js'; +import { emitESMImage } from './utils/node/emitAsset.js'; import { getAssetsPrefix } from './utils/getAssetsPrefix.js'; import { isESMImportedImage } from './utils/imageKind.js'; import { getProxyCode } from './utils/proxy.js'; @@ -133,6 +133,7 @@ export default function assets({ import { getImage as getImageInternal } from "astro/assets"; export { default as Image } from "astro/components/Image.astro"; export { default as Picture } from "astro/components/Picture.astro"; + export { inferRemoteSize } from "astro/assets/utils/inferRemoteSize.js"; export const imageConfig = ${JSON.stringify(settings.config.image)}; // This is used by the @astrojs/node integration to locate images. diff --git a/packages/astro/src/content/runtime-assets.ts b/packages/astro/src/content/runtime-assets.ts index 26d98fd6e8db..42969042c678 100644 --- a/packages/astro/src/content/runtime-assets.ts +++ b/packages/astro/src/content/runtime-assets.ts @@ -1,7 +1,7 @@ import type { PluginContext } from 'rollup'; import { z } from 'zod'; import type { ImageMetadata, OmitBrand } from '../assets/types.js'; -import { emitESMImage } from '../assets/utils/emitAsset.js'; +import { emitESMImage } from '../assets/utils/node/emitAsset.js'; export function createImage( pluginContext: PluginContext, diff --git a/packages/astro/test/core-image-infersize.test.js b/packages/astro/test/core-image-infersize.test.js index 9abf24b1f79e..355d6a6911f2 100644 --- a/packages/astro/test/core-image-infersize.test.js +++ b/packages/astro/test/core-image-infersize.test.js @@ -70,6 +70,11 @@ describe('astro:image:infersize', () => { true ); }); + + it('direct function call work', async () => { + let $dimensions = $('#direct'); + assert.equal($dimensions.text().trim(), '64x64'); + }); }); }); }); diff --git a/packages/astro/test/fixtures/core-image-infersize/src/pages/index.astro b/packages/astro/test/fixtures/core-image-infersize/src/pages/index.astro index 947c9a4f6bcd..ef7bf57c012d 100644 --- a/packages/astro/test/fixtures/core-image-infersize/src/pages/index.astro +++ b/packages/astro/test/fixtures/core-image-infersize/src/pages/index.astro @@ -1,6 +1,9 @@ --- // https://avatars.githubusercontent.com/u/622227?s=64 is a .jpeg -import { Image, Picture, getImage } from 'astro:assets'; +import { Image, Picture, getImage, inferRemoteSize } from 'astro:assets'; + +const { width, height } = await inferRemoteSize('https://avatars.githubusercontent.com/u/622227?s=64'); + const remoteImg = await getImage({ src: 'https://avatars.githubusercontent.com/u/622227?s=64', inferSize: true, @@ -10,3 +13,7 @@ const remoteImg = await getImage({ + +
+ {width}x{height} +
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07544ef848db..03a30ede5fa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9298,7 +9298,6 @@ packages: libsql@0.3.12: resolution: {integrity: sha512-to30hj8O3DjS97wpbKN6ERZ8k66MN1IaOfFLR6oHqd25GMiPJ/ZX0VaZ7w+TsPmxcFS3p71qArj/hiedCyvXCg==} - cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lilconfig@2.1.0: