Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export async function getImage(
isRemoteImage(resolvedOptions.src) &&
isRemotePath(resolvedOptions.src)
) {
const result = await inferRemoteSize(resolvedOptions.src); // Directly probe the image URL
const getRemoteSize = (url: string) =>
service.getRemoteSize?.(url, imageConfig) ?? inferRemoteSize(url);
const result = await getRemoteSize(resolvedOptions.src); // Directly probe the image URL
resolvedOptions.width ??= result.width;
resolvedOptions.height ??= result.height;
originalWidth = result.width;
Expand Down
12 changes: 12 additions & 0 deletions packages/astro/src/assets/services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import type { AstroConfig } from '../../types/public/config.js';
import { DEFAULT_HASH_PROPS, DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from '../consts.js';
import type {
ImageFit,
ImageMetadata,
ImageOutputFormat,
ImageTransform,
UnresolvedSrcSetValue,
} from '../types.js';
import { isESMImportedImage, isRemoteImage } from '../utils/imageKind.js';
import { inferRemoteSize } from '../utils/remoteProbe.js';

export type ImageService = LocalImageService | ExternalImageService;

Expand Down Expand Up @@ -77,6 +79,13 @@ interface SharedServiceProps<T extends Record<string, any> = Record<string, any>
options: ImageTransform,
imageConfig: ImageConfig<T>,
) => ImageTransform | Promise<ImageTransform>;
/**
* Infers the dimensions of a remote image by streaming its data and analyzing it progressively until sufficient metadata is available.
*/
getRemoteSize?: (
url: string,
imageConfig: ImageConfig<T>,
) => Omit<ImageMetadata, 'src' | 'fsPath'> | Promise<Omit<ImageMetadata, 'src' | 'fsPath'>>;
}

export type ExternalImageService<T extends Record<string, any> = Record<string, any>> =
Expand Down Expand Up @@ -401,6 +410,9 @@ export const baseService: Omit<LocalImageService, 'transform'> = {

return transform;
},
getRemoteSize(url, _imageConfig) {
return inferRemoteSize(url);
},
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/assets/services/sharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const sharpService: LocalImageService<SharpImageServiceConfig> = {
parseURL: baseService.parseURL,
getHTMLAttributes: baseService.getHTMLAttributes,
getSrcSet: baseService.getSrcSet,
getRemoteSize: baseService.getRemoteSize,
async transform(inputBuffer, transformOptions, config) {
if (!sharp) sharp = await loadSharp();
const transform: BaseServiceTransform = transformOptions as BaseServiceTransform;
Expand Down
12 changes: 10 additions & 2 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,16 +142,19 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
if (id === resolvedVirtualModuleId) {
return {
code: `
export { getConfiguredImageService, isLocalService } from "astro/assets";
import { getConfiguredImageService as _getConfiguredImageService } from "astro/assets";
export { isLocalService } from "astro/assets";
import { getImage as getImageInternal } from "astro/assets";
export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro";
export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro";
export { inferRemoteSize } from "astro/assets/utils/inferRemoteSize.js";
import { inferRemoteSize as inferRemoteSizeInternal } from "astro/assets/utils/inferRemoteSize.js";

export { default as Font } from "astro/components/Font.astro";
import * as fontsMod from 'virtual:astro:assets/fonts/internal';
import { createGetFontData } from "astro/assets/fonts/runtime";

export const getConfiguredImageService = _getConfiguredImageService;

export const viteFSConfig = ${JSON.stringify(resolvedConfig.server.fs ?? {})};

export const safeModulePaths = new Set(${JSON.stringify(
Expand All @@ -172,6 +175,11 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
enumerable: false,
configurable: true,
});
export const inferRemoteSize = async (url) => {
const service = await _getConfiguredImageService()

return service.getRemoteSize?.(url, imageConfig) ?? inferRemoteSizeInternal(url)
}
// This is used by the @astrojs/node integration to locate images.
// It's unused on other platforms, but on some platforms like Netlify (and presumably also Vercel)
// new URL("dist/...") is interpreted by the bundler as a signal to include that directory
Expand Down
35 changes: 28 additions & 7 deletions packages/astro/test/core-image-layout.test.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ describe('astro:image:layout', () => {
describe('local image service', () => {
/** @type {import('./test-utils').DevServer} */
let devServer;
const walrusImagePath =
'https://images.unsplash.com/photo-1690941380217-24dfa9a1d21f?q=80&w=1476&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
const imageScale = 2;

before(async () => {
fixture = await loadFixture({
root: './fixtures/core-image-layout/',
image: {
service: testImageService({ foo: 'bar' }),
service: testImageService({
foo: 'bar',
transform: { path: walrusImagePath, scale: imageScale },
}),
domains: ['avatars.githubusercontent.com'],
},
});
Expand Down Expand Up @@ -224,13 +230,14 @@ describe('astro:image:layout', () => {
});

describe('remote images', () => {
let $;
before(async () => {
let res = await fixture.fetch('/remote');
let html = await res.text();
$ = cheerio.load(html);
});

describe('srcset', () => {
let $;
before(async () => {
let res = await fixture.fetch('/remote');
let html = await res.text();
$ = cheerio.load(html);
});
it('has srcset', () => {
let $img = $('#constrained img');
assert.ok($img.attr('srcset'));
Expand Down Expand Up @@ -263,6 +270,20 @@ describe('astro:image:layout', () => {
assert.deepEqual(widths, [640, 750, 828, 1080, 1280, 1668, 2048, 2560]);
});
});

describe('inferSize', () => {
it('default inferSize works', () => {
let $img = $('#infer-size img');
assert.equal($img.attr('width'), '2670');
assert.equal($img.attr('height'), '1780');
});

it('custom inferSize works', () => {
let $img = $('#infer-size-custom img');
assert.equal($img.attr('width'), (1476 * imageScale).toString());
assert.equal($img.attr('height'), (978 * imageScale).toString());
});
});
});

describe('picture component', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Image, Picture } from "astro:assets";

const penguin = "https://images.unsplash.com/photo-1670392957807-b0504fc5160a?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
const walrus = "https://images.unsplash.com/photo-1690941380217-24dfa9a1d21f?q=80&w=1476&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"

---

Expand All @@ -23,3 +24,10 @@ const penguin = "https://images.unsplash.com/photo-1670392957807-b0504fc5160a?q=
<Image src={penguin} alt="a penguin" width={800} height={600} layout="full-width"/>
</div>

<div id="infer-size">
<Image src={penguin} alt="a penguin" inferSize={true}/>
</div>

<div id="infer-size-custom">
<Image src={walrus} alt="a walrus" inferSize={true}/>
</div>
12 changes: 11 additions & 1 deletion packages/astro/test/test-image-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { baseService } from '../dist/assets/services/service.js';

/**
* stub image service that returns images as-is without optimization
* @param {{ foo?: string }} [config]
* @param {{ foo?: string, transform?: { path: string, scale: number } }} [config]
*/
export function testImageService(config = {}) {
return {
Expand Down Expand Up @@ -32,4 +32,14 @@ export default {
format: transform.format,
};
},
async getRemoteSize(url, serviceConfig) {
const baseSize = await baseService.getRemoteSize(url, serviceConfig);

if (serviceConfig.service.config.transform?.path === url) {
const scale = serviceConfig.service.config.transform.scale;
return { ...baseSize, width: baseSize.width * scale, height: baseSize.height * scale };
}

return baseSize;
},
};
Loading