Skip to content

Commit

Permalink
Expose inferRemoteSize function (#11098)
Browse files Browse the repository at this point in the history
* feat: expose and rename `inferSize`

* feat: separate `ISize` type

* feat: reformat function to use `ImageMetadata`

* nit(assets): re-use image-metadata code for remote images

* chore: changeset

* chore: changeset

* feat(assets): Export from `astro:assets`

* fix: proper errors

* fix: dont export from astro/assets

* fix: ests

* Update .changeset/large-geese-play.md

Co-authored-by: Sarah Rainsberger <[email protected]>

* fix: ests

* Update .changeset/large-geese-play.md

Co-authored-by: Sarah Rainsberger <[email protected]>

---------

Co-authored-by: Erika <[email protected]>
Co-authored-by: Sarah Rainsberger <[email protected]>
  • Loading branch information
3 people authored and ematipico committed Jul 18, 2024
1 parent d54671f commit 9bbfad3
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 26 deletions.
20 changes: 20 additions & 0 deletions .changeset/large-geese-play.md
Original file line number Diff line number Diff line change
@@ -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 `<Image>` and `<Picture>` 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);
---
<Image src={imageUrl} width={width / 2} height={height} densities={[1.5, 2]} />
```
1 change: 1 addition & 0 deletions packages/astro/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ declare module 'astro:assets' {
) => Promise<import('./dist/assets/types.js').GetImageResult>;
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;
};
Expand Down
1 change: 1 addition & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 5 additions & 12 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImageService> {
if (!globalThis?.astroAsset?.imageService) {
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/assets/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,3 +12,4 @@ export {
type RemotePattern,
} from './remotePattern.js';
export { hashTransform, propsToFilename } from './transformToPath.js';
export { inferRemoteSize } from './remoteProbe.js';
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 16 additions & 7 deletions packages/astro/src/assets/utils/remoteProbe.ts
Original file line number Diff line number Diff line change
@@ -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<ISize> {
export async function inferRemoteSize(url: string): Promise<Omit<ImageMetadata, 'src' | 'fsPath'>> {
// 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();
Expand All @@ -31,17 +35,22 @@ export async function probe(url: string): Promise<ISize> {

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),
});
}
3 changes: 2 additions & 1 deletion packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/content/runtime-assets.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/test/core-image-infersize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ describe('astro:image:infersize', () => {
true
);
});

it('direct function call work', async () => {
let $dimensions = $('#direct');
assert.equal($dimensions.text().trim(), '64x64');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -10,3 +13,7 @@ const remoteImg = await getImage({
<Image src="https://avatars.githubusercontent.com/u/622227?s=64," inferSize={true} , alt="" />
<Picture src="https://avatars.githubusercontent.com/u/622227?s=64," inferSize={true} , alt="" />
<img src={remoteImg.src} {...remoteImg.attributes} id="getImage" />

<div id="direct">
{width}x{height}
</div>

0 comments on commit 9bbfad3

Please sign in to comment.