0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

feat: remove @astrojs/image completely (#7922)

This commit is contained in:
Erika 2023-08-09 21:03:59 +02:00 committed by GitHub
parent a6a16a6baf
commit 08c3afb860
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
182 changed files with 38 additions and 20267 deletions

View file

@ -23,7 +23,6 @@
'@astrojs/prefetch': minor
'@astrojs/markdoc': minor
'@astrojs/underscore-redirects': minor
'@astrojs/image': minor
'@astrojs/mdx': minor
'@astrojs/internal-helpers': minor
---

View file

@ -9,7 +9,6 @@
"@astrojs/alpinejs": "0.2.2",
"@astrojs/cloudflare": "6.6.2",
"@astrojs/deno": "4.3.0",
"@astrojs/image": "0.17.2",
"@astrojs/lit": "2.1.0",
"@astrojs/markdoc": "0.4.4",
"@astrojs/mdx": "0.19.7",

View file

@ -62,7 +62,6 @@ Join us on [Discord](https://astro.build/chat) to meet other maintainers. We'll
| [@astrojs/tailwind](packages/integrations/tailwind) | [![astro version](https://img.shields.io/npm/v/@astrojs/tailwind.svg?label=%20)](packages/integrations/tailwind/CHANGELOG.md) |
| [@astrojs/turbolinks](packages/integrations/turbolinks) | [![astro version](https://img.shields.io/npm/v/@astrojs/turbolinks.svg?label=%20)](packages/integrations/turbolinks/CHANGELOG.md) |
| [@astrojs/alpinejs](packages/integrations/alpinejs) | [![astro version](https://img.shields.io/npm/v/@astrojs/alpinejs.svg?label=%20)](packages/integrations/alpinejs/CHANGELOG.md) |
| [@astrojs/image](packages/integrations/image) | [![astro version](https://img.shields.io/npm/v/@astrojs/image.svg?label=%20)](packages/integrations/image/CHANGELOG.md) |
| [@astrojs/mdx](packages/integrations/mdx) | [![astro version](https://img.shields.io/npm/v/@astrojs/mdx.svg?label=%20)](packages/integrations/mdx/CHANGELOG.md) |
| [@astrojs/prefetch](packages/integrations/prefetch) | [![astro version](https://img.shields.io/npm/v/@astrojs/prefetch.svg?label=%20)](packages/integrations/prefetch/CHANGELOG.md) |

View file

@ -33,7 +33,7 @@ export async function decodeBuffer(
.join('')
// TODO (future PR): support more formats
if (firstChunkString.includes('GIF')) {
throw Error(`GIF images are not supported, please install the @astrojs/image/sharp plugin`)
throw Error(`GIF images are not supported, please use the Sharp image service`)
}
const key = Object.entries(supportedFormats).find(([, { detectors }]) =>
detectors.some((detector) => detector.exec(firstChunkString))
@ -78,7 +78,7 @@ export async function encodeJpeg(
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['mozjpeg']
const m = await e.enc()
await maybeDelay()

View file

@ -48,7 +48,7 @@ export async function handleRequest({
// Add config.base back to url before passing it to SSR
url.pathname = removeTrailingForwardSlash(config.base) + url.pathname;
// HACK! @astrojs/image uses query params for the injected route in `dev`
// HACK! astro:assets uses query params for the injected route in `dev`
if (!buildingToSSR && pathname !== '/_image') {
// Prevent user from depending on search params when not doing SSR.
// NOTE: Create an array copy here because deleting-while-iterating

View file

@ -85,8 +85,6 @@ export async function setUpEnvTs({
let referenceDefs: string[] = [];
if (settings.config.experimental.assets) {
referenceDefs.push('/// <reference types="astro/client-image" />');
} else if (settings.config.integrations.find((i) => i.name === '@astrojs/image')) {
referenceDefs.push('/// <reference types="@astrojs/image/client" />');
} else {
referenceDefs.push('/// <reference types="astro/client" />');
}

File diff suppressed because one or more lines are too long

View file

@ -1,739 +0,0 @@
# @astrojs/image 📷
> ⚠️ This integration will be deprecated in Astro v3.0 (Fall 2023) in favor of the `astro:assets` module. Please see the [Assets documentation](https://docs.astro.build/en/guides/assets/) for more information.
This **[Astro integration][astro-integration]** optimizes images in your [Astro project](https://astro.build). It is supported in Astro v2 only for all static sites and for [some server-side rendering deploy hosts](#installation).
- <strong>[Why `@astrojs/image`?](#why-astrojsimage)</strong>
- <strong>[Installation](#installation)</strong>
- <strong>[Usage](#usage)</strong>
- <strong>[Debugging](#debugging)</strong>
- <strong>[Configuration](#configuration)</strong>
- <strong>[Examples](#examples)</strong>
- <strong>[Troubleshooting](#troubleshooting)</strong>
- <strong>[Contributing](#contributing)</strong>
- <strong>[Changelog](#changelog)</strong>
## Why `@astrojs/image`?
Images play a big role in overall site performance and usability. Serving properly sized images makes all the difference but is often tricky to automate.
This integration provides `<Image />` and `<Picture>` components as well as a basic image transformer, with full support for static sites and server-side rendering. The built-in image transformer is also replaceable, opening the door for future integrations that work with your favorite hosted image service.
## Installation
Along with our integration, we recommend installing [sharp](https://sharp.pixelplumbing.com/) when appropriate.
The `@astrojs/image` default image transformer is based on [Squoosh](https://github.com/GoogleChromeLabs/squoosh) and uses WebAssembly libraries to support most deployment environments, including those that do not support sharp, such as StackBlitz.
For faster builds and more fine-grained control of image transformations, install sharp in addition to `@astrojs/image` if
- You are building a static site with Astro.
- You are using an SSR deployment host that supports NodeJS using `@astrojs/netlify/functions`, `@astrojs/vercel/serverless` or `@astrojs/node`.
Note that `@astrojs/image` is not currently supported on
- Cloudflare SSR
- `@astrojs/deno`
- `@astrojs/netlify/edge-functions`
- `@astrojs/vercel/edge`
### Quick Install
The `astro add` command-line tool automates the installation for you. Run one of the following commands in a new terminal window. (If you aren't sure which package manager you're using, run the first command.) Then, follow the prompts, and type "y" in the terminal (meaning "yes") for each one.
```sh
# Using NPM
npx astro add image
# Using Yarn
yarn astro add image
# Using PNPM
pnpm astro add image
```
If you run into any issues, [feel free to report them to us on GitHub](https://github.com/withastro/astro/issues) and try the manual installation steps below.
### Manual Install
First, install the `@astrojs/image` package using your package manager. If you're using npm or aren't sure, run this in the terminal:
```sh
npm install @astrojs/image
```
Then, apply this integration to your `astro.config.*` file using the `integrations` property:
```js ins={3} "image()"
// astro.config.mjs
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
export default defineConfig({
// ...
integrations: [image()],
});
```
### Installing `sharp` (optional)
First, install the `sharp` package using your package manager. If you're using npm or aren't sure, run this in the terminal:
```sh
npm install sharp
```
Then, update the integration in your `astro.config.*` file to use the built-in `sharp` image transformer.
```js ins={8}
// astro.config.mjs
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
export default defineConfig({
// ...
integrations: [
image({
serviceEntryPoint: '@astrojs/image/sharp',
}),
],
});
```
### Update `env.d.ts`
For the best development experience, add the integrations type definitions to your project's `env.d.ts` file.
```typescript
// Replace `astro/client` with `@astrojs/image/client`
/// <reference types="@astrojs/image/client" />
```
Or, alternatively if your project is using the types through a `tsconfig.json`
```json
{
"compilerOptions": {
// Replace `astro/client` with `@astrojs/image/client`
"types": ["@astrojs/image/client"]
}
}
```
## Usage
```astro title="src/pages/index.astro"
---
import { Image, Picture } from '@astrojs/image/components';
import heroImage from '../assets/hero.png';
---
// optimized image, keeping the original width, height, and image format
<Image src={heroImage} alt="descriptive text" />
// specify multiple sizes for responsive images or art direction
<Picture
src={heroImage}
widths={[200, 400, 800]}
sizes="(max-width: 800px) 100vw, 800px"
alt="descriptive text"
/>
---
```
The included image transformers support resizing images and encoding them to different image formats. Third-party image services will be able to add support for custom transformations as well (ex: `blur`, `filter`, `rotate`, etc).
Astros `<Image />` and `<Picture />` components require the `alt` attribute, which provides descriptive text for images. A warning will be logged if alt text is missing, and a future release of the integration will throw an error if no alt text is provided.
If the image is merely decorative (i.e. doesnt contribute to the understanding of the page), set `alt=""` so that the image is properly understood and ignored by screen readers.
### `<Image />`
The built-in `<Image />` component is used to create an optimized `<img />` for both remote images accessed by URL as well as local images imported from your project's `src/` directory.
In addition to the component-specific properties, any valid HTML attribute for the `<img />` included in the `<Image />` component will be included in the built `<img />`.
#### src
<p>
**Type:** `string` | `ImageMetadata` | `Promise<ImageMetadata>`<br>
**Required:** `true`
</p>
Source for the original image file.
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
For images located in your project's `src/`: use the file path relative to the `src/` directory. (e.g. `src="../assets/source-pic.png"`)
For images located in your `public/` directory: use the URL path relative to the `public/` directory. (e.g. `src="/images/public-image.jpg"`). These work like remote images.
#### alt
<p>
**Type:** `string`<br>
**Required:** `true`
</p>
Defines an alternative text description of the image.
Set to an empty string (`alt=""`) if the image is not a key part of the content (e.g. it's decoration or a tracking pixel).
#### format
<p>
**Type:** `'avif' | 'jpeg' | 'jpg' | 'png' | 'svg' | 'webp'`<br>
**Default:** `undefined`
</p>
The output format to be used in the optimized image. The original image format will be used if `format` is not provided.
This property is required for remote images when using the default image transformer Squoosh, this is because the original format cannot be inferred.
Added in v0.15.0: You can use the `<Image />` component when working with SVG images, but the `svg` option can only be used when the original image is a `.svg` file. Other image formats (like `.png` or `.jpg`) cannot be converted into vector images. The `.svg` image itself will not be transformed, but the final `<img />` will be properly optimized by the integration.
#### quality
<p>
**Type:** `number`<br>
**Default:** `undefined`
</p>
The compression quality used during optimization. The image service will use its own default quality depending on the image format if not provided.
#### width
<p>
**Type:** `number`<br>
**Default:** `undefined`
</p>
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.
Dimensions are optional for local images, the original image size will be used if not provided.
For remote images, including images in `public/`, the integration needs to be able to calculate dimensions for the optimized image. This can be done by providing `width` and `height` or by providing one dimension and an `aspectRatio`.
#### height
<p>
**Type:** `number`<br>
**Default:** `undefined`
</p>
The desired height of the output image. Combine with `width` to crop the image to an exact size, or `aspectRatio` to automatically calculate and crop the width.
Dimensions are optional for local images, the original image size will be used if not provided.
For remote images, including images in `public/`, the integration needs to be able to calculate dimensions for the optimized image. This can be done by providing `width` and `height` or by providing one dimension and an `aspectRatio`.
#### aspectRatio
<p>
**Type:** `number` | `string`<br>
**Default:** `undefined`
</p>
The desired aspect ratio of the output image. Combine with either `width` or `height` to automatically calculate and crop the other dimension.
A `string` can be provided in the form of `{width}:{height}`, ex: `16:9` or `3:4`.
A `number` can also be provided, useful when the aspect ratio is calculated at build time. This can be an inline number such as `1.777` or inlined as a JSX expression like `aspectRatio={16/9}`.
For remote images, including images in `public/`, the integration needs to be able to calculate dimensions for the optimized image. This can be done by providing `width` and `height` or by providing one dimension and an `aspectRatio`.
#### background
<p>
**Type:** `ColorDefinition`<br>
**Default:** `undefined`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead.
The background color is used to fill the remaining background when using `contain` for the `fit` property.
The background color is also used for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format
doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used
as default replacement for transparent pixels.
The parameter accepts a `string` as value.
The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal
color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, an RGB definition in the form
`rgb(100,100,100)`, an RGBA definition in the form `rgba(100,100,100, 0.5)`.
#### fit
<p>
**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'` <br>
**Default:** `'cover'`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead. Read more about [how `sharp` resizes images](https://sharp.pixelplumbing.com/api-resize).
How the image should be resized to fit both `height` and `width`.
#### position
<p>
**Type:** `'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'` <br>
**Default:** `'centre'`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead. Read more about [how `sharp` resizes images](https://sharp.pixelplumbing.com/api-resize).
Position of the crop when fit is `cover` or `contain`.
### `<Picture />`
The built-in `<Picture />` component is used to create an optimized `<picture />` for both remote images accessed by URL as well as local images imported from your project's `src/` directory.
In addition to the component-specific properties, any valid HTML attribute for the `<img />` included in the `<Picture />` component will be included in the built `<img />`.
#### src
<p>
**Type:** `string` | `ImageMetadata` | `Promise<ImageMetadata>`<br>
**Required:** `true`
</p>
Source for the original image file.
For remote images, provide the full URL. (e.g. `src="https://astro.build/assets/blog/astro-1-release-update.avif"`)
For images located in your project's `src/`: use the file path relative to the `src/` directory. (e.g. `src="../assets/source-pic.png"`)
For images located in your `public/` directory: use the URL path relative to the `public/` directory. (e.g. `src="/images/public-image.jpg"`). These work like remote images.
#### alt
<p>
**Type:** `string`<br>
**Required:** `true`
</p>
Defines an alternative text description of the image.
Set to an empty string (`alt=""`) if the image is not a key part of the content (e.g. it's decoration or a tracking pixel).
#### sizes
<p>
**Type:** `string`<br>
**Required:** `true`
</p>
The HTMLImageElement property `sizes` allows you to specify the layout width of the image for each of a list of media conditions.
See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) for more details.
#### widths
<p>
**Type:** `number[]`<br>
**Required:** `true`
</p>
The list of sizes that should be built for responsive images. This is combined with `aspectRatio` to calculate the final dimensions of each built image.
```astro
// Builds three images: 400x400, 800x800, and 1200x1200
<Picture src={img} widths={[400, 800, 1200]} aspectRatio="1:1" alt="descriptive text" />
```
#### aspectRatio
<p>
**Type:** `number` | `string`<br>
**Default:** `undefined`
</p>
The desired aspect ratio of the output image. This is combined with `widths` to calculate the final dimensions of each built image.
A `string` can be provided in the form of `{width}:{height}`, ex: `16:9` or `3:4`.
A `number` can also be provided, useful when the aspect ratio is calculated at build time. This can be an inline number such as `1.777` or inlined as a JSX expression like `aspectRatio={16/9}`.
For remote images, including images in `public/`, `aspectRatio` is required to ensure the correct `height` can be calculated at build time.
#### formats
<p>
**Type:** `Array<'avif' | 'jpeg' | 'png' | 'webp'>`<br>
**Default:** `undefined`
</p>
The output formats to be used in the optimized image. If not provided, `webp` and `avif` will be used in addition to the original image format.
For remote images, including images in `public/`, the original image format is unknown. If not provided, only `webp` and `avif` will be used.
#### background
<p>
**Type:** `ColorDefinition`<br>
**Default:** `undefined`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead.
The background color to use for replacing the alpha channel with `sharp`'s `flatten` method. In case the output format
doesn't support transparency (i.e. `jpeg`), it's advisable to include a background color, otherwise black will be used
as default replacement for transparent pixels.
The parameter accepts a `string` as value.
The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_colornames.asp), a hexadecimal
color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form
`rgb(100,100,100)`.
#### fit
<p>
**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'` <br>
**Default:** `'cover'`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead. Read more about [how `sharp` resizes images](https://sharp.pixelplumbing.com/api-resize).
How the image should be resized to fit both `height` and `width`.
#### position
<p>
**Type:** `'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'` <br>
**Default:** `'centre'`
</p>
> This is not supported by the default Squoosh service. See the [installation section](#installing-sharp-optional) for details on using the `sharp` service instead. Read more about [how `sharp` resizes images](https://sharp.pixelplumbing.com/api-resize).
Position of the crop when fit is `cover` or `contain`.
### `getImage`
This is the helper function used by the `<Image />` component to build `<img />` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `<Image />` component.
This helper takes in an object with the same properties as the `<Image />` component and returns an object with attributes that should be included on the final `<img />` element.
This can be helpful if you need to add preload links to a page's `<head>`.
```astro
---
import { getImage } from '@astrojs/image';
const { src } = await getImage({
src: import('../assets/hero.png'),
alt: 'My hero image',
});
---
<html>
<head>
<link rel="preload" as="image" href={src} alt="alt text" />
</head>
</html>
```
### `getPicture`
This is the helper function used by the `<Picture />` component to build multiple sizes and formats for responsive images. This helper can be used directly for more complex use cases that aren't currently supported by the `<Picture />` component.
This helper takes in an object with the same properties as the `<Picture />` component and returns an object attributes that should be included on the final `<img />` element **and** a list of sources that should be used to render all `<source>`s for the `<picture>` element.
## Configuration
The integration can be configured to run with a different image service, either a hosted image service or a full image transformer that runs locally in your build or SSR deployment.
> During development, local images may not have been published yet and would not be available to hosted image services. Local images will always use the built-in image service when using `astro dev`.
### config.serviceEntryPoint
The `serviceEntryPoint` should resolve to the image service installed from NPM. The default entry point is `@astrojs/image/squoosh`, which resolves to the entry point exported from this integration's `package.json`.
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
export default defineConfig({
integrations: [
image({
// Example: The entrypoint for a third-party image service installed from NPM
serviceEntryPoint: 'my-image-service/astro.js',
}),
],
});
```
### config.logLevel
The `logLevel` controls can be used to control how much detail is logged by the integration during builds. This may be useful to track down a specific image or transformation that is taking a long time to build.
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
export default defineConfig({
integrations: [
image({
// supported levels: 'debug' | 'info' | 'warn' | 'error' | 'silent'
// default: 'info'
logLevel: 'debug',
}),
],
});
```
### config.cacheDir
During static builds, the integration will cache transformed images to avoid rebuilding the same image for every build. This can be particularly helpful if you are using a hosting service that allows you to cache build assets for future deployments.
Local images will be cached for 1 year and invalidated when the original image file is changed. Remote images will be cached based on the `fetch()` response's cache headers, similar to how a CDN would manage the cache.
By default, transformed images will be cached to `./node_modules/.astro/image`. This can be configured in the integration's config options.
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
export default defineConfig({
integrations: [
image({
// may be useful if your hosting provider allows caching between CI builds
cacheDir: './.cache/image',
}),
],
});
```
Caching can also be disabled by using `cacheDir: false`.
## Examples
### Local images
Image files in your project's `src/` directory can be imported in frontmatter and passed directly to the `<Image />` component as the `src=` attribute value. `alt` is also required.
All other properties are optional and will default to the original image file's properties if not provided.
```astro
---
import { Image } from '@astrojs/image/components';
import heroImage from '../assets/hero.png';
---
// optimized image, keeping the original width, height, and image format
<Image src={heroImage} alt="descriptive text" />
// height will be recalculated to match the original aspect ratio
<Image src={heroImage} width={300} alt="descriptive text" />
// cropping to a specific width and height
<Image src={heroImage} width={300} height={600} alt="descriptive text" />
// cropping to a specific aspect ratio and converting to an avif format
<Image src={heroImage} width={300} aspectRatio="16:9" format="avif" alt="descriptive text" />
// image imports can also be inlined directly
<Image src={import('../assets/hero.png')} alt="descriptive text" />
```
#### Images in `/public`
The `<Image />` component can also be used with images stored in the `public/` directory and the `src=` attribute is relative to the public folder. It will be treated as a remote image, which requires either both `width` and `height`, or one dimension and an `aspectRatio` attribute.
Your original image will be copied unprocessed to the build folder, like all files located in public/, and Astros image integration will also return optimized versions of the image.
For example, use an image located at `public/social.png` in either static or SSR builds like so:
```astro title="src/pages/page.astro"
---
import { Image } from '@astrojs/image/components';
import socialImage from '/social.png';
---
// In static builds: the image will be built and optimized to `/dist`. // In SSR builds: the image
will be optimized by the server when requested by a browser.
<Image src={socialImage} width={1280} aspectRatio="16:9" alt="descriptive text" />
```
### Remote images
Remote images can be transformed with the `<Image />` component. The `<Image />` component needs to know the final dimensions for the `<img />` element to avoid content layout shifts. For remote images, this means you must either provide `width` and `height`, or one of the dimensions plus the required `aspectRatio`.
```astro
---
import { Image } from '@astrojs/image/components';
const imageUrl = 'https://astro.build/assets/press/full-logo-dark.png';
---
// cropping to a specific width and height
<Image src={imageUrl} width={750} height={250} format="avif" alt="descriptive text" />
// height will be recalculated to match the aspect ratio
<Image src={imageUrl} width={750} aspectRatio={16 / 9} format="avif" alt="descriptive text" />
```
### Responsive pictures
The `<Picture />` component can be used to automatically build a `<picture>` with multiple sizes and formats. Check out [MDN](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#art_direction) for a deep dive into responsive images and art direction.
By default, the picture will include formats for `avif` and `webp`. For local images only, the image's original format will also be included.
For remote images, an `aspectRatio` is required to ensure the correct `height` can be calculated at build time.
```astro
---
import { Picture } from '@astrojs/image/components';
import hero from '../assets/hero.png';
const imageUrl =
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png';
---
// Local image with multiple sizes
<Picture
src={hero}
widths={[200, 400, 800]}
sizes="(max-width: 800px) 100vw, 800px"
alt="descriptive text"
/>
// Remote image (aspect ratio is required)
<Picture
src={imageUrl}
widths={[200, 400, 800]}
aspectRatio="4:3"
sizes="(max-width: 800px) 100vw, 800px"
alt="descriptive text"
/>
// Inlined imports are supported
<Picture
src={import('../assets/hero.png')}
widths={[200, 400, 800]}
sizes="(max-width: 800px) 100vw, 800px"
alt="descriptive text"
/>
```
### Setting Default Values
Currently, there is no way to specify default values for all `<Image />` and `<Picture />` components. Required attributes should be set on each individual component.
As an alternative, you can wrap these components in another Astro component for reuse. For example, you could create a component for your blog post images:
```astro title="src/components/BlogPostImage.astro"
---
import { Picture } from '@astrojs/image/components';
const { src, ...attrs } = Astro.props;
---
<Picture src={src} widths={[400, 800, 1500]} sizes="(max-width: 767px) 100vw, 736px" {...attrs} />
<style>
img,
picture :global(img),
svg {
margin-block: 2.5rem;
border-radius: 0.75rem;
}
</style>
```
### Using `<img>` with the Image Integration
The official image integration will change image imports to return an object rather than a source string.
The object has the following properties, derived from the imported file:
```ts
{
src: string;
width: number;
height: number;
format: 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp';
}
```
If you have the image integration installed, refer to the `src` property of the object when using `<img>`.
```astro ".src"
---
import rocket from '../images/rocket.svg';
---
<img src={rocket.src} alt="A rocketship in space." />
```
Alternatively, add `?url` to your imports to tell them to return a source string.
```astro "?url"
---
import rocket from '../images/rocket.svg?url';
---
<img src={rocket} alt="A rocketship in space." />
```
## Troubleshooting
- If your installation doesn't seem to be working, try restarting the dev server.
- If you edit and save a file and don't see your site update accordingly, try refreshing the page.
- If refreshing the page doesn't update your preview, or if a new installation doesn't seem to be working, then restart the dev server.
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
You can also check our [Astro Integration Documentation][astro-integration] for more on integrations.
[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
## Contributing
This package is maintained by Astro's Core team. You're welcome to submit an issue or PR!
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for a history of changes to this integration.

View file

@ -1,62 +0,0 @@
/// <reference types="astro/client-base" />
type InputFormat =
| 'avif'
| 'gif'
| 'heic'
| 'heif'
| 'jpeg'
| 'jpg'
| 'png'
| 'tiff'
| 'webp'
| 'svg';
interface ImageMetadata {
src: string;
width: number;
height: number;
format: InputFormat;
}
// images
declare module '*.avif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.gif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.heic' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.heif' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.jpeg' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.jpg' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.png' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.tiff' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.webp' {
const metadata: ImageMetadata;
export default metadata;
}
declare module '*.svg' {
const metadata: ImageMetadata;
export default metadata;
}

View file

@ -1,18 +0,0 @@
---
// @ts-ignore
import { getImage } from '../dist/index.js';
import { warnForMissingAlt } from './index.js';
import type { ImageComponentLocalImageProps, ImageComponentRemoteImageProps } from './index.js';
export type Props = ImageComponentLocalImageProps | ImageComponentRemoteImageProps;
const { loading = 'lazy', decoding = 'async', ...props } = Astro.props;
if (props.alt === undefined || props.alt === null) {
warnForMissingAlt();
}
const attrs = await getImage(props);
---
<img {...attrs} {loading} {decoding} />

View file

@ -1,46 +0,0 @@
---
import { getPicture } from '../dist/index.js';
import { warnForMissingAlt } from './index.js';
import type { PictureComponentLocalImageProps, PictureComponentRemoteImageProps } from './index.js';
import type { GetPictureResult } from '../src/lib/get-picture.js';
export type Props = PictureComponentLocalImageProps | PictureComponentRemoteImageProps;
const {
src,
alt,
sizes,
widths,
aspectRatio,
fit,
background,
position,
formats = ['avif', 'webp'],
loading = 'lazy',
decoding = 'async',
...attrs
} = Astro.props;
if (alt === undefined || alt === null) {
warnForMissingAlt();
}
const { image, sources }: GetPictureResult = await getPicture({
src,
widths,
formats,
aspectRatio,
fit,
background,
position,
alt,
});
delete image.width;
delete image.height;
---
<picture>
{sources.map((attrs) => <source {...attrs} {sizes} />)}
<img {...image} {loading} {decoding} {...attrs} />
</picture>

View file

@ -1,74 +0,0 @@
/// <reference types="astro/astro-jsx" />
export { default as Image } from './Image.astro';
export { default as Picture } from './Picture.astro';
import type { HTMLAttributes } from 'astro/types';
import type { TransformOptions, OutputFormat } from '../dist/loaders/index.js';
import type { ImageMetadata } from '../dist/vite-plugin-astro-image.js';
import type { AstroBuiltinAttributes } from 'astro';
export interface ImageComponentLocalImageProps
extends Omit<TransformOptions, 'src'>,
Omit<ImgHTMLAttributes, 'src' | 'width' | 'height'> {
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
}
export interface ImageComponentRemoteImageProps extends TransformOptions, ImgHTMLAttributes {
src: string;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
format?: OutputFormat;
width: number;
height: number;
}
export interface PictureComponentLocalImageProps
extends GlobalHTMLAttributes,
Omit<TransformOptions, 'src'>,
Pick<ImgHTMLAttributes, 'loading' | 'decoding' | 'fetchpriority'> {
src: ImageMetadata | Promise<{ default: ImageMetadata }>;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
widths: number[];
sizes?: HTMLImageElement['sizes'];
formats?: OutputFormat[];
}
export interface PictureComponentRemoteImageProps
extends GlobalHTMLAttributes,
TransformOptions,
Pick<ImgHTMLAttributes, 'loading' | 'decoding' | 'fetchpriority'> {
src: string;
/** Defines an alternative text description of the image. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel). */
alt: string;
widths: number[];
aspectRatio: TransformOptions['aspectRatio'];
sizes?: HTMLImageElement['sizes'];
formats?: OutputFormat[];
background?: TransformOptions['background'];
}
export type ImgHTMLAttributes = HTMLAttributes<'img'>;
export type GlobalHTMLAttributes = Omit<
astroHTML.JSX.HTMLAttributes,
keyof Omit<AstroBuiltinAttributes, 'class:list'>
>;
let altWarningShown = false;
export function warnForMissingAlt() {
if (altWarningShown === true) {
return;
}
altWarningShown = true;
console.warn(`\n[@astrojs/image] "alt" text was not provided for an <Image> or <Picture> component.
A future release of @astrojs/image may throw a build error when "alt" text is missing.
The "alt" attribute holds a text description of the image, which isn't mandatory but is incredibly useful for accessibility. Set to an empty string (alt="") if the image is not a key part of the content (it's decoration or a tracking pixel).\n`);
}

View file

@ -1,74 +0,0 @@
{
"name": "@astrojs/image",
"description": "Load and transform images in your Astro site",
"version": "1.0.0-beta.0",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/withastro/astro.git",
"directory": "packages/integrations/image"
},
"keywords": [
"astro-integration",
"astro-component",
"withastro",
"image"
],
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/image/",
"exports": {
".": "./dist/index.js",
"./endpoint": "./dist/endpoint.js",
"./sharp": "./dist/loaders/sharp.js",
"./squoosh": "./dist/loaders/squoosh.js",
"./components": "./components/index.js",
"./package.json": "./package.json",
"./client": "./client.d.ts",
"./dist/*": "./dist/*"
},
"files": [
"components",
"dist",
"client.d.ts"
],
"scripts": {
"build": "astro-scripts build \"src/**/*.ts\" && tsc",
"build:ci": "astro-scripts build \"src/**/*.ts\"",
"dev": "astro-scripts dev \"src/**/*.ts\"",
"test": "mocha --exit --timeout 20000 test"
},
"dependencies": {
"@altano/tiny-async-pool": "^1.0.2",
"http-cache-semantics": "^4.1.1",
"image-size": "^1.0.2",
"kleur": "^4.1.5",
"magic-string": "^0.30.2",
"mime": "^3.0.0"
},
"devDependencies": {
"@types/http-cache-semantics": "^4.0.1",
"@types/mime": "^2.0.3",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.7",
"cheerio": "1.0.0-rc.12",
"fast-glob": "^3.2.12",
"mocha": "^9.2.2",
"rollup-plugin-copy": "^3.4.0",
"sharp": "^0.32.1",
"srcset-parse": "^1.1.0",
"vite": "^4.4.6"
},
"peerDependencies": {
"astro": "workspace:^3.0.0-beta.0",
"sharp": ">=0.31.0"
},
"peerDependenciesMeta": {
"sharp": {
"optional": true
}
}
}

View file

@ -1,85 +0,0 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { LoggerLevel } from '../utils/logger.js';
import { debug, warn } from '../utils/logger.js';
const CACHE_FILE = `cache.json`;
interface Cache {
[filename: string]: { expires: number };
}
export class ImageCache {
#cacheDir: URL;
#cacheFile: URL;
#cache: Cache = {};
#logLevel: LoggerLevel;
constructor(dir: URL, logLevel: LoggerLevel) {
this.#logLevel = logLevel;
this.#cacheDir = dir;
this.#cacheFile = this.#toAbsolutePath(CACHE_FILE);
}
#toAbsolutePath(file: string) {
return new URL(path.join(this.#cacheDir.toString(), file));
}
async init() {
try {
const str = await fs.readFile(this.#cacheFile, 'utf-8');
this.#cache = JSON.parse(str) as Cache;
} catch {
// noop
debug({ message: 'no cache file found', level: this.#logLevel });
}
}
async finalize() {
try {
await fs.mkdir(path.dirname(fileURLToPath(this.#cacheFile)), { recursive: true });
await fs.writeFile(this.#cacheFile, JSON.stringify(this.#cache));
} catch {
// noop
warn({ message: 'could not save the cache file', level: this.#logLevel });
}
}
async get(file: string): Promise<Buffer | undefined> {
if (!this.has(file)) {
return undefined;
}
try {
const filepath = this.#toAbsolutePath(file);
return await fs.readFile(filepath);
} catch {
warn({ message: `could not load cached file for "${file}"`, level: this.#logLevel });
return undefined;
}
}
async set(file: string, buffer: Buffer, opts: Cache['string']): Promise<void> {
try {
const filepath = this.#toAbsolutePath(file);
await fs.mkdir(path.dirname(fileURLToPath(filepath)), { recursive: true });
await fs.writeFile(filepath, buffer);
this.#cache[file] = opts;
} catch {
// noop
warn({ message: `could not save cached copy of "${file}"`, level: this.#logLevel });
}
}
has(file: string): boolean {
if (!(file in this.#cache)) {
return false;
}
const { expires } = this.#cache[file];
return expires > Date.now();
}
}

View file

@ -1,240 +0,0 @@
import { doWork } from '@altano/tiny-async-pool';
import type { AstroConfig } from 'astro';
import CachePolicy from 'http-cache-semantics';
import { bgGreen, black, cyan, dim, green } from 'kleur/colors';
import fs from 'node:fs/promises';
import OS from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { SSRImageService, TransformOptions } from '../loaders/index.js';
import { debug, info, warn, type LoggerLevel } from '../utils/logger.js';
import { isRemoteImage, prependForwardSlash } from '../utils/paths.js';
import { ImageCache } from './cache.js';
async function loadLocalImage(src: string | URL) {
try {
const data = await fs.readFile(src);
// Vite's file hash will change if the file is changed at all,
// we can safely cache local images here.
const timeToLive = new Date();
timeToLive.setFullYear(timeToLive.getFullYear() + 1);
return {
data,
expires: timeToLive.getTime(),
};
} catch {
return undefined;
}
}
function webToCachePolicyRequest({ url, method, headers: _headers }: Request): CachePolicy.Request {
let headers: CachePolicy.Headers = {};
// Be defensive here due to a cookie header bug in node@18.14.1 + undici
try {
headers = Object.fromEntries(_headers.entries());
} catch {}
return {
method,
url,
headers,
};
}
function webToCachePolicyResponse({ status, headers: _headers }: Response): CachePolicy.Response {
let headers: CachePolicy.Headers = {};
// Be defensive here due to a cookie header bug in node@18.14.1 + undici
try {
headers = Object.fromEntries(_headers.entries());
} catch {}
return {
status,
headers,
};
}
async function loadRemoteImage(src: string) {
try {
if (src.startsWith('//')) {
src = `https:${src}`;
}
const req = new Request(src);
const res = await fetch(req);
if (!res.ok) {
return undefined;
}
// calculate an expiration date based on the response's TTL
const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
const expires = policy.storable() ? policy.timeToLive() : 0;
return {
data: Buffer.from(await res.arrayBuffer()),
expires: Date.now() + expires,
};
} catch (err: unknown) {
console.error(err);
return undefined;
}
}
function getTimeStat(timeStart: number, timeEnd: number) {
const buildTime = timeEnd - timeStart;
return buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
}
export interface SSGBuildParams {
loader: SSRImageService;
staticImages: Map<string, Map<string, TransformOptions>>;
config: AstroConfig;
outDir: URL;
logLevel: LoggerLevel;
cacheDir?: URL;
}
export async function ssgBuild({
loader,
staticImages,
config,
outDir,
logLevel,
cacheDir,
}: SSGBuildParams) {
let cache: ImageCache | undefined = undefined;
if (cacheDir) {
cache = new ImageCache(cacheDir, logLevel);
await cache.init();
}
const timer = performance.now();
const cpuCount = OS.cpus().length;
info({
level: logLevel,
prefix: false,
message: `${bgGreen(
black(
` optimizing ${staticImages.size} image${
staticImages.size > 1 ? 's' : ''
} in batches of ${cpuCount} `
)
)}`,
});
async function processStaticImage([src, transformsMap]: [
string,
Map<string, TransformOptions>,
]): Promise<void> {
let inputFile: string | undefined = undefined;
let inputBuffer: Buffer | undefined = undefined;
// tracks the cache duration for the original source image
let expires = 0;
// Strip leading assetsPrefix or base added by addStaticImage
if (config.build.assetsPrefix) {
if (src.startsWith(config.build.assetsPrefix)) {
src = prependForwardSlash(src.slice(config.build.assetsPrefix.length));
}
} else if (config.base) {
if (src.startsWith(config.base)) {
src = prependForwardSlash(src.slice(config.base.length));
}
}
if (isRemoteImage(src)) {
// try to load the remote image
const res = await loadRemoteImage(src);
inputBuffer = res?.data;
expires = res?.expires || 0;
} else {
const inputFileURL = new URL(`.${src}`, outDir);
inputFile = fileURLToPath(inputFileURL);
const res = await loadLocalImage(inputFile);
inputBuffer = res?.data;
expires = res?.expires || 0;
}
if (!inputBuffer) {
warn({ level: logLevel, message: `"${src}" image could not be fetched` });
return;
}
const transforms = Array.from(transformsMap.entries());
debug({ level: logLevel, prefix: false, message: `${green('▶')} transforming ${src}` });
let timeStart = performance.now();
// process each transformed version
for (const [filename, transform] of transforms) {
timeStart = performance.now();
let outputFile: string;
let outputFileURL: URL;
if (isRemoteImage(src)) {
outputFileURL = new URL(
path.join(`./${config.build.assets}`, path.basename(filename)),
outDir
);
outputFile = fileURLToPath(outputFileURL);
} else {
outputFileURL = new URL(path.join(`./${config.build.assets}`, filename), outDir);
outputFile = fileURLToPath(outputFileURL);
}
const pathRelative = outputFile.replace(fileURLToPath(outDir), '');
let data: Buffer | undefined;
// try to load the transformed image from cache, if available
if (cache?.has(pathRelative)) {
data = await cache.get(pathRelative);
}
// a valid cache file wasn't found, transform the image and cache it
if (!data) {
const transformed = await loader.transform(inputBuffer, transform);
data = transformed.data;
// cache the image, if available
if (cache) {
await cache.set(pathRelative, data, { expires });
}
}
const outputFolder = new URL('./', outputFileURL);
await fs.mkdir(outputFolder, { recursive: true });
await fs.writeFile(outputFile, data);
const timeEnd = performance.now();
const timeChange = getTimeStat(timeStart, timeEnd);
const timeIncrease = `(+${timeChange})`;
debug({
level: logLevel,
prefix: false,
message: ` ${cyan('created')} ${dim(pathRelative)} ${dim(timeIncrease)}`,
});
}
}
// transform each original image file in batches
await doWork(cpuCount, staticImages, processStaticImage);
// saves the cache's JSON manifest to file
if (cache) {
await cache.finalize();
}
info({
level: logLevel,
prefix: false,
message: dim(`Completed in ${getTimeStat(timer, performance.now())}.\n`),
});
}

View file

@ -1,55 +0,0 @@
import type { APIRoute } from 'astro';
import mime from 'mime';
// @ts-expect-error
import loader from 'virtual:image-loader';
import { etag } from './utils/etag.js';
import { isRemoteImage } from './utils/paths.js';
async function loadRemoteImage(src: URL) {
try {
const res = await fetch(src);
if (!res.ok) {
return undefined;
}
return Buffer.from(await res.arrayBuffer());
} catch (err: unknown) {
console.error(err);
return undefined;
}
}
export const GET: APIRoute = async ({ request }) => {
try {
const url = new URL(request.url);
const transform = loader.parseTransform(url.searchParams);
let inputBuffer: Buffer | undefined = undefined;
// TODO: handle config subpaths?
const sourceUrl = isRemoteImage(transform.src)
? new URL(transform.src)
: new URL(transform.src, url.origin);
inputBuffer = await loadRemoteImage(sourceUrl);
if (!inputBuffer) {
return new Response('Not Found', { status: 404 });
}
const { data, format } = await loader.transform(inputBuffer, transform);
return new Response(data, {
status: 200,
headers: {
'Content-Type': mime.getType(format) || '',
'Cache-Control': 'public, max-age=31536000',
ETag: etag(data.toString()),
Date: new Date().toUTCString(),
},
});
} catch (err: unknown) {
console.error(err);
return new Response(`Server Error: ${err}`, { status: 500 });
}
};

View file

@ -1,167 +0,0 @@
import type { AstroConfig, AstroIntegration } from 'astro';
import { ssgBuild } from './build/ssg.js';
import type { ImageService, SSRImageService, TransformOptions } from './loaders/index.js';
import type { LoggerLevel } from './utils/logger.js';
import { joinPaths, prependForwardSlash, propsToFilename } from './utils/paths.js';
import { isServerLikeOutput } from './utils/prerender.js';
import { createPlugin } from './vite-plugin-astro-image.js';
export { getImage } from './lib/get-image.js';
export { getPicture } from './lib/get-picture.js';
const PKG_NAME = '@astrojs/image';
const ROUTE_PATTERN = '/_image';
const UNSUPPORTED_ADAPTERS = new Set([
'@astrojs/cloudflare',
'@astrojs/deno',
'@astrojs/netlify/edge-functions',
'@astrojs/vercel/edge',
]);
interface BuildConfig {
client: URL;
server: URL;
assets: string;
}
interface ImageIntegration {
loader?: ImageService;
defaultLoader: SSRImageService;
addStaticImage?: (transform: TransformOptions) => string;
}
declare global {
// eslint-disable-next-line no-var
var astroImage: ImageIntegration;
}
export interface IntegrationOptions {
/**
* Entry point for the @type {HostedImageService} or @type {LocalImageService} to be used.
*/
serviceEntryPoint?: '@astrojs/image/squoosh' | '@astrojs/image/sharp' | string;
logLevel?: LoggerLevel;
cacheDir?: false | string;
}
export default function integration(options: IntegrationOptions = {}): AstroIntegration {
const resolvedOptions = {
serviceEntryPoint: '@astrojs/image/squoosh',
logLevel: 'info' as LoggerLevel,
cacheDir: './node_modules/.astro/image',
...options,
};
let _config: AstroConfig;
let _buildConfig: BuildConfig;
// During SSG builds, this is used to track all transformed images required.
const staticImages = new Map<string, Map<string, TransformOptions>>();
function getViteConfiguration(isDev: boolean) {
return {
plugins: [createPlugin(_config, resolvedOptions)],
build: {
rollupOptions: {
external: ['sharp'],
},
},
ssr: {
noExternal: ['@astrojs/image', resolvedOptions.serviceEntryPoint],
// Externalize CJS dependencies used by `serviceEntryPoint`. Vite dev mode has trouble
// loading these modules with `ssrLoadModule`, but works in build.
external: isDev ? ['http-cache-semantics', 'image-size', 'mime'] : [],
},
assetsInclude: ['**/*.wasm'],
};
}
return {
name: PKG_NAME,
hooks: {
'astro:config:setup': async ({ command, config, updateConfig, injectRoute }) => {
_config = config;
updateConfig({
vite: getViteConfiguration(command === 'dev'),
});
if (command === 'dev' || isServerLikeOutput(config)) {
injectRoute({
pattern: ROUTE_PATTERN,
entryPoint: '@astrojs/image/endpoint',
});
}
const { default: defaultLoader } = await import(
resolvedOptions.serviceEntryPoint === '@astrojs/image/sharp'
? './loaders/sharp.js'
: './loaders/squoosh.js'
);
globalThis.astroImage = {
defaultLoader,
};
},
'astro:config:done': ({ config }) => {
_config = config;
_buildConfig = config.build;
},
'astro:build:start': () => {
const adapterName = _config.adapter?.name;
if (adapterName && UNSUPPORTED_ADAPTERS.has(adapterName)) {
throw new Error(
`@astrojs/image is not supported with the ${adapterName} adapter. Please choose a Node.js compatible adapter.`
);
}
},
'astro:build:setup': async () => {
// Used to cache all images rendered to HTML
// Added to globalThis to share the same map in Node and Vite
function addStaticImage(transform: TransformOptions) {
const srcTranforms = staticImages.has(transform.src)
? staticImages.get(transform.src)!
: new Map<string, TransformOptions>();
const filename = propsToFilename(transform, resolvedOptions.serviceEntryPoint);
srcTranforms.set(filename, transform);
staticImages.set(transform.src, srcTranforms);
// Prepend the Astro config's base path, if it was used.
// Doing this here makes sure that base is ignored when building
// staticImages to /dist, but the rendered HTML will include the
// base prefix for `src`.
if (_config.build.assetsPrefix) {
return joinPaths(_config.build.assetsPrefix, _buildConfig.assets, filename);
} else {
return prependForwardSlash(joinPaths(_config.base, _buildConfig.assets, filename));
}
}
// Helpers for building static images should only be available for SSG
if (_config.output === 'static') {
globalThis.astroImage.addStaticImage = addStaticImage;
}
},
'astro:build:generated': async ({ dir }) => {
// for SSG builds, build all requested image transforms to dist
const loader = globalThis?.astroImage?.loader;
if (loader && 'transform' in loader && staticImages.size > 0) {
const cacheDir = !!resolvedOptions.cacheDir
? new URL(resolvedOptions.cacheDir, _config.root)
: undefined;
await ssgBuild({
loader,
staticImages,
config: _config,
outDir: dir,
logLevel: resolvedOptions.logLevel,
cacheDir,
});
}
},
},
};
}

View file

@ -1,159 +0,0 @@
/// <reference types="astro/astro-jsx" />
import type { ImageService, OutputFormat, TransformOptions } from '../loaders/index.js';
import { isSSRService, parseAspectRatio } from '../loaders/index.js';
import { isRemoteImage } from '../utils/paths.js';
import type { ImageMetadata } from '../vite-plugin-astro-image.js';
export interface GetImageTransform extends Omit<TransformOptions, 'src'> {
src: string | ImageMetadata | Promise<{ default: ImageMetadata }>;
alt: string;
}
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, background, 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,
background,
};
}
/**
* Gets the HTML attributes required to build an `<img />` for the transformed image.
*
* @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(
transform: GetImageTransform
): Promise<astroHTML.JSX.ImgHTMLAttributes> {
if (!transform.src) {
throw new Error('[@astrojs/image] `src` is required');
}
let loader = globalThis.astroImage?.loader;
if (!loader) {
// @ts-expect-error
const { default: mod } = await import('virtual:image-loader').catch(() => {
throw new Error(
'[@astrojs/image] Builtin image loader not found. (Did you remember to add the integration to your Astro config?)'
);
});
loader = mod as ImageService;
globalThis.astroImage = globalThis.astroImage || {};
globalThis.astroImage.loader = loader;
}
const resolved = await resolveTransform(transform);
const attributes = await loader.getImageAttributes(resolved);
// `.env` must be optional to support running in environments outside of `vite` (such as `astro.config`)
// @ts-expect-error
const isDev = import.meta.env?.DEV;
const isLocalImage = !isRemoteImage(resolved.src);
const _loader = isDev && isLocalImage ? globalThis.astroImage.defaultLoader : loader;
if (!_loader) {
throw new Error('@astrojs/image: loader not found!');
}
const { searchParams } = isSSRService(_loader)
? _loader.serializeTransform(resolved)
: globalThis.astroImage.defaultLoader.serializeTransform(resolved);
const imgSrc =
!isLocalImage && resolved.src.startsWith('//') ? `https:${resolved.src}` : resolved.src;
let src: string;
if (/^[\/\\]?@astroimage/.test(imgSrc)) {
src = `${imgSrc}?${searchParams.toString()}`;
} else {
searchParams.set('href', imgSrc);
src = `/_image?${searchParams.toString()}`;
}
// cache all images rendered to HTML
if (globalThis.astroImage?.addStaticImage) {
src = globalThis.astroImage.addStaticImage(resolved);
}
return {
...attributes,
src,
};
}

View file

@ -1,105 +0,0 @@
/// <reference types="astro/astro-jsx" />
import mime from 'mime';
import { parseAspectRatio, type OutputFormat, type TransformOptions } from '../loaders/index.js';
import { extname } from '../utils/paths.js';
import type { ImageMetadata } from '../vite-plugin-astro-image.js';
import { getImage } from './get-image.js';
export interface GetPictureParams {
src: string | ImageMetadata | Promise<{ default: ImageMetadata }>;
alt: string;
widths: number[];
formats: OutputFormat[];
aspectRatio?: TransformOptions['aspectRatio'];
fit?: TransformOptions['fit'];
background?: TransformOptions['background'];
position?: TransformOptions['position'];
}
export interface GetPictureResult {
image: astroHTML.JSX.ImgHTMLAttributes;
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 Array.from(unique).filter(Boolean);
}
export async function getPicture(params: GetPictureParams): Promise<GetPictureResult> {
const { src, alt, widths, fit, position, background } = params;
if (!src) {
throw new Error('[@astrojs/image] `src` is required');
}
if (!widths || !Array.isArray(widths)) {
throw new Error('[@astrojs/image] at least one `width` is required. ex: `widths={[100]}`');
}
const aspectRatio = await resolveAspectRatio(params);
if (!aspectRatio) {
throw new Error('`aspectRatio` must be provided for remote images');
}
// always include the original image format
const allFormats = await resolveFormats(params);
const lastFormat = allFormats[allFormats.length - 1];
const maxWidth = Math.max(...widths);
let image: astroHTML.JSX.ImgHTMLAttributes;
async function getSource(format: OutputFormat) {
const imgs = await Promise.all(
widths.map(async (width) => {
const img = await getImage({
src,
alt,
format,
width,
fit,
position,
background,
aspectRatio,
});
if (format === lastFormat && width === maxWidth) {
image = img;
}
return `${img.src?.replaceAll(' ', encodeURI)} ${width}w`;
})
);
return {
type: mime.getType(format) || format,
srcset: imgs.join(','),
};
}
const sources = await Promise.all(allFormats.map((format) => getSource(format)));
return {
sources,
// @ts-expect-error image will always be defined
image,
};
}

View file

@ -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 }>;
}

View file

@ -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;

View file

@ -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;

View file

@ -1,288 +0,0 @@
export type NamedColor =
| 'aliceblue'
| 'antiquewhite'
| 'aqua'
| 'aquamarine'
| 'azure'
| 'beige'
| 'bisque'
| 'black'
| 'blanchedalmond'
| 'blue'
| 'blueviolet'
| 'brown'
| 'burlywood'
| 'cadetblue'
| 'chartreuse'
| 'chocolate'
| 'coral'
| 'cornflowerblue'
| 'cornsilk'
| 'crimson'
| 'cyan'
| 'darkblue'
| 'darkcyan'
| 'darkgoldenrod'
| 'darkgray'
| 'darkgreen'
| 'darkkhaki'
| 'darkmagenta'
| 'darkolivegreen'
| 'darkorange'
| 'darkorchid'
| 'darkred'
| 'darksalmon'
| 'darkseagreen'
| 'darkslateblue'
| 'darkslategray'
| 'darkturquoise'
| 'darkviolet'
| 'deeppink'
| 'deepskyblue'
| 'dimgray'
| 'dodgerblue'
| 'firebrick'
| 'floralwhite'
| 'forestgreen'
| 'fuchsia'
| 'gainsboro'
| 'ghostwhite'
| 'gold'
| 'goldenrod'
| 'gray'
| 'green'
| 'greenyellow'
| 'honeydew'
| 'hotpink'
| 'indianred'
| 'indigo'
| 'ivory'
| 'khaki'
| 'lavender'
| 'lavenderblush'
| 'lawngreen'
| 'lemonchiffon'
| 'lightblue'
| 'lightcoral'
| 'lightcyan'
| 'lightgoldenrodyellow'
| 'lightgray'
| 'lightgreen'
| 'lightpink'
| 'lightsalmon'
| 'lightseagreen'
| 'lightskyblue'
| 'lightslategray'
| 'lightsteelblue'
| 'lightyellow'
| 'lime'
| 'limegreen'
| 'linen'
| 'magenta'
| 'maroon'
| 'mediumaquamarine'
| 'mediumblue'
| 'mediumorchid'
| 'mediumpurple'
| 'mediumseagreen'
| 'mediumslateblue'
| 'mediumspringgreen'
| 'mediumturquoise'
| 'mediumvioletred'
| 'midnightblue'
| 'mintcream'
| 'mistyrose'
| 'moccasin'
| 'navajowhite'
| 'navy'
| 'oldlace'
| 'olive'
| 'olivedrab'
| 'orange'
| 'orangered'
| 'orchid'
| 'palegoldenrod'
| 'palegreen'
| 'paleturquoise'
| 'palevioletred'
| 'papayawhip'
| 'peachpuff'
| 'peru'
| 'pink'
| 'plum'
| 'powderblue'
| 'purple'
| 'rebeccapurple'
| 'red'
| 'rosybrown'
| 'royalblue'
| 'saddlebrown'
| 'salmon'
| 'sandybrown'
| 'seagreen'
| 'seashell'
| 'sienna'
| 'silver'
| 'skyblue'
| 'slateblue'
| 'slategray'
| 'snow'
| 'springgreen'
| 'steelblue'
| 'tan'
| 'teal'
| 'thistle'
| 'tomato'
| 'turquoise'
| 'violet'
| 'wheat'
| 'white'
| 'whitesmoke'
| 'yellow'
| 'yellowgreen';
export const htmlColorNames: NamedColor[] = [
'aliceblue',
'antiquewhite',
'aqua',
'aquamarine',
'azure',
'beige',
'bisque',
'black',
'blanchedalmond',
'blue',
'blueviolet',
'brown',
'burlywood',
'cadetblue',
'chartreuse',
'chocolate',
'coral',
'cornflowerblue',
'cornsilk',
'crimson',
'cyan',
'darkblue',
'darkcyan',
'darkgoldenrod',
'darkgray',
'darkgreen',
'darkkhaki',
'darkmagenta',
'darkolivegreen',
'darkorange',
'darkorchid',
'darkred',
'darksalmon',
'darkseagreen',
'darkslateblue',
'darkslategray',
'darkturquoise',
'darkviolet',
'deeppink',
'deepskyblue',
'dimgray',
'dodgerblue',
'firebrick',
'floralwhite',
'forestgreen',
'fuchsia',
'gainsboro',
'ghostwhite',
'gold',
'goldenrod',
'gray',
'green',
'greenyellow',
'honeydew',
'hotpink',
'indianred',
'indigo',
'ivory',
'khaki',
'lavender',
'lavenderblush',
'lawngreen',
'lemonchiffon',
'lightblue',
'lightcoral',
'lightcyan',
'lightgoldenrodyellow',
'lightgray',
'lightgreen',
'lightpink',
'lightsalmon',
'lightsalmon',
'lightseagreen',
'lightskyblue',
'lightslategray',
'lightsteelblue',
'lightyellow',
'lime',
'limegreen',
'linen',
'magenta',
'maroon',
'mediumaquamarine',
'mediumblue',
'mediumorchid',
'mediumpurple',
'mediumseagreen',
'mediumslateblue',
'mediumslateblue',
'mediumspringgreen',
'mediumturquoise',
'mediumvioletred',
'midnightblue',
'mintcream',
'mistyrose',
'moccasin',
'navajowhite',
'navy',
'oldlace',
'olive',
'olivedrab',
'orange',
'orangered',
'orchid',
'palegoldenrod',
'palegreen',
'paleturquoise',
'palevioletred',
'papayawhip',
'peachpuff',
'peru',
'pink',
'plum',
'powderblue',
'purple',
'rebeccapurple',
'red',
'rosybrown',
'royalblue',
'saddlebrown',
'salmon',
'sandybrown',
'seagreen',
'seashell',
'sienna',
'silver',
'skyblue',
'slateblue',
'slategray',
'snow',
'springgreen',
'steelblue',
'tan',
'teal',
'thistle',
'tomato',
'turquoise',
'violet',
'wheat',
'white',
'whitesmoke',
'yellow',
'yellowgreen',
];

View file

@ -1,44 +0,0 @@
/**
* FNV-1a Hash implementation
* @author Travis Webb (tjwebb) <me@traviswebb.com>
*
* Ported from https://github.com/tjwebb/fnv-plus/blob/master/index.js
*
* Simplified, optimized and add modified for 52 bit, which provides a larger hash space
* and still making use of Javascript's 53-bit integer space.
*/
export const fnv1a52 = (str: string) => {
const len = str.length;
let i = 0,
t0 = 0,
v0 = 0x2325,
t1 = 0,
v1 = 0x8422,
t2 = 0,
v2 = 0x9ce4,
t3 = 0,
v3 = 0xcbf2;
while (i < len) {
v0 ^= str.charCodeAt(i++);
t0 = v0 * 435;
t1 = v1 * 435;
t2 = v2 * 435;
t3 = v3 * 435;
t2 += v0 << 8;
t3 += v1 << 8;
t1 += t0 >>> 16;
v0 = t0 & 65535;
t2 += t1 >>> 16;
v1 = t1 & 65535;
v3 = (t3 + (t2 >>> 16)) & 65535;
v2 = t2 & 65535;
}
return (v3 & 15) * 281474976710656 + v2 * 4294967296 + v1 * 65536 + (v0 ^ (v3 >> 4));
};
export const etag = (payload: string, weak = false) => {
const prefix = weak ? 'W/"' : '"';
return prefix + fnv1a52(payload).toString(36) + payload.length.toString(36) + '"';
};

View file

@ -1,12 +0,0 @@
export default function execOnce<T extends (...args: any[]) => ReturnType<T>>(fn: T): T {
let used = false;
let result: ReturnType<T>;
return ((...args: any[]) => {
if (!used) {
used = true;
result = fn(...args);
}
return result;
}) as T;
}

View file

@ -1,74 +0,0 @@
// eslint-disable no-console
import { bold, cyan, dim, green, red, yellow } from 'kleur/colors';
const PREFIX = '@astrojs/image';
// Hey, locales are pretty complicated! Be careful modifying this logic...
// If we throw at the top-level, international users can't use Astro.
//
// Using `[]` sets the default locale properly from the system!
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#parameters
//
// Here be the dragons we've slain:
// https://github.com/withastro/astro/issues/2625
// https://github.com/withastro/astro/issues/3309
const dateTimeFormat = new Intl.DateTimeFormat([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; // same as Pino
export interface LogMessage {
level: LoggerLevel;
message: string;
prefix?: boolean;
timestamp?: boolean;
}
export const levels: Record<LoggerLevel, number> = {
debug: 20,
info: 30,
warn: 40,
error: 50,
silent: 90,
};
function getPrefix(level: LoggerLevel, timestamp: boolean) {
let prefix = '';
if (timestamp) {
prefix += dim(dateTimeFormat.format(new Date()) + ' ');
}
switch (level) {
case 'debug':
prefix += bold(green(`[${PREFIX}] `));
break;
case 'info':
prefix += bold(cyan(`[${PREFIX}] `));
break;
case 'warn':
prefix += bold(yellow(`[${PREFIX}] `));
break;
case 'error':
prefix += bold(red(`[${PREFIX}] `));
break;
}
return prefix;
}
const log =
(_level: LoggerLevel, dest: (message: string) => void) =>
({ message, level, prefix = true, timestamp = true }: LogMessage) => {
if (levels[_level] >= levels[level]) {
dest(`${prefix ? getPrefix(level, timestamp) : ''}${message}`);
}
};
export const info = log('info', console.info);
export const debug = log('debug', console.debug);
export const warn = log('warn', console.warn);
export const error = log('error', console.error);

View file

@ -1,28 +0,0 @@
import sizeOf from 'image-size';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import type { InputFormat } from '../loaders/index.js';
import type { ImageMetadata } from '../vite-plugin-astro-image.js';
export interface Metadata extends ImageMetadata {
orientation?: number;
}
export async function metadata(src: URL | string, data?: Buffer): Promise<Metadata | undefined> {
const file = data || (await fs.readFile(src));
const { width, height, type, orientation } = sizeOf(file);
const isPortrait = (orientation || 0) >= 5;
if (!width || !height || !type) {
return undefined;
}
return {
// We shouldn't call fileURLToPath function if it starts with /@astroimage/ because it will throw Invalid URL error
src: typeof src === 'string' && /^[\/\\]?@astroimage/.test(src) ? src : fileURLToPath(src),
width: isPortrait ? height : width,
height: isPortrait ? width : height,
format: type as InputFormat,
orientation,
};
}

View file

@ -1,90 +0,0 @@
import type { TransformOptions } from '../loaders/index.js';
import { shorthash } from './shorthash.js';
export function isRemoteImage(src: string) {
return /^(https?:)?\/\//.test(src);
}
function removeQueryString(src: string) {
const index = src.lastIndexOf('?');
return index > 0 ? src.substring(0, index) : src;
}
export function extname(src: string) {
const base = basename(src);
const index = base.lastIndexOf('.');
if (index <= 0) {
return '';
}
return base.substring(index);
}
function removeExtname(src: string) {
const index = src.lastIndexOf('.');
if (index <= 0) {
return src;
}
return src.substring(0, index);
}
function basename(src: string) {
return removeQueryString(src.replace(/^.*[\\\/]/, ''));
}
export function propsToFilename(transform: TransformOptions, serviceEntryPoint: string) {
// strip off the querystring first, then remove the file extension
let filename = removeQueryString(transform.src);
// take everything from transform except alt, which is not used in the hash
const { alt, ...rest } = transform;
const hashFields = { ...rest, serviceEntryPoint };
filename = basename(filename);
const ext = extname(filename);
filename = removeExtname(filename);
const outputExt = transform.format ? `.${transform.format}` : ext;
return `/${filename}_${shorthash(JSON.stringify(hashFields))}${outputExt}`;
}
export function appendForwardSlash(path: string) {
return path.endsWith('/') ? path : path + '/';
}
export function prependForwardSlash(path: string) {
return path[0] === '/' ? path : '/' + path;
}
export function removeTrailingForwardSlash(path: string) {
return path.endsWith('/') ? path.slice(0, path.length - 1) : path;
}
export function removeLeadingForwardSlash(path: string) {
return path.startsWith('/') ? path.substring(1) : path;
}
export function trimSlashes(path: string) {
return path.replace(/^\/|\/$/g, '');
}
function isString(path: unknown): path is string {
return typeof path === 'string' || path instanceof String;
}
export function joinPaths(...paths: (string | undefined)[]) {
return paths
.filter(isString)
.map((path, i) => {
if (i === 0) {
return removeTrailingForwardSlash(path);
} else if (i === paths.length - 1) {
return removeLeadingForwardSlash(path);
} else {
return trimSlashes(path);
}
})
.join('/');
}

View file

@ -1,5 +0,0 @@
import type { AstroConfig } from 'astro';
export function isServerLikeOutput(config: AstroConfig) {
return config.output === 'server' || config.output === 'hybrid';
}

View file

@ -1,67 +0,0 @@
/**
* shortdash - https://github.com/bibig/node-shorthash
*
* @license
*
* (The MIT License)
*
* Copyright (c) 2013 Bibig <bibig@me.com>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
const dictionary = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY';
const binary = dictionary.length;
// refer to: http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
function bitwise(str: string) {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
hash = (hash << 5) - hash + ch;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
export function shorthash(text: string) {
let num: number;
let result = '';
let integer = bitwise(text);
const sign = integer < 0 ? 'Z' : ''; // If it's negative, start with Z, which isn't in the dictionary
integer = Math.abs(integer);
while (integer >= binary) {
num = integer % binary;
integer = Math.floor(integer / binary);
result = dictionary[num] + result;
}
if (integer > 0) {
result = dictionary[integer] + result;
}
return sign + result;
}

View file

@ -1,122 +0,0 @@
/* tslint-disable ban-types */
import { parentPort, Worker } from 'worker_threads';
function uuid() {
return Array.from({ length: 16 }, () => Math.floor(Math.random() * 256).toString(16)).join('');
}
interface Job<I> {
msg: I;
resolve: (result: any) => void;
reject: (reason: any) => void;
}
export default class WorkerPool<I, O> {
public numWorkers: number;
public jobQueue: TransformStream<Job<I>, Job<I>>;
public workerQueue: TransformStream<Worker, Worker>;
public done: Promise<void>;
constructor(numWorkers: number, workerFile: string) {
this.numWorkers = numWorkers;
this.jobQueue = new TransformStream();
this.workerQueue = new TransformStream();
const writer = this.workerQueue.writable.getWriter();
for (let i = 0; i < numWorkers; i++) {
writer.write(new Worker(workerFile));
}
writer.releaseLock();
this.done = this._readLoop();
}
async _readLoop() {
const reader = this.jobQueue.readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
await this._terminateAll();
return;
}
if (!value) {
throw new Error('Reader did not return any value');
}
const { msg, resolve, reject } = value;
const worker = await this._nextWorker();
this.jobPromise(worker, msg)
.then((result) => resolve(result))
.catch((reason) => reject(reason))
.finally(() => {
// Return the worker to the pool
const writer = this.workerQueue.writable.getWriter();
writer.write(worker);
writer.releaseLock();
});
}
}
async _nextWorker() {
const reader = this.workerQueue.readable.getReader();
const { value } = await reader.read();
reader.releaseLock();
if (!value) {
throw new Error('No worker left');
}
return value;
}
async _terminateAll() {
for (let n = 0; n < this.numWorkers; n++) {
const worker = await this._nextWorker();
worker.terminate();
}
this.workerQueue.writable.close();
}
async join() {
this.jobQueue.writable.getWriter().close();
await this.done;
}
dispatchJob(msg: I): Promise<O> {
return new Promise((resolve, reject) => {
const writer = this.jobQueue.writable.getWriter();
writer.write({ msg, resolve, reject });
writer.releaseLock();
});
}
private jobPromise(worker: Worker, msg: I) {
return new Promise((resolve, reject) => {
const id = uuid();
worker.postMessage({ msg, id });
worker.on('message', function f({ error, result, id: rid }) {
if (rid !== id) {
return;
}
if (error) {
reject(error);
return;
}
worker.off('message', f);
resolve(result);
});
});
}
static useThisThreadAsWorker<I, O>(cb: (msg: I) => O) {
parentPort!.on('message', async (data) => {
const { msg, id } = data;
try {
const result = await cb(msg);
parentPort!.postMessage({ result, id });
} catch (e: any) {
parentPort!.postMessage({ error: e.message, id });
}
});
}
}

View file

@ -1,245 +0,0 @@
Skip to content
Pull requests
Issues
Marketplace
Explore
@tony-sull
vercel /
next.js
Public
Code
Issues 1.1k
Pull requests 216
Discussions
Actions
Security 8
Insights
next.js/packages/next/server/lib/squoosh/LICENSE
@timneutkens
timneutkens Move next-server directory files to server directory (#26756)
Latest commit 5b9ad8d on Jun 30, 2021
History
1 contributor
202 lines (169 sloc) 11.1 KB
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Footer
© 2022 GitHub, Inc.
Footer navigation
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About

View file

@ -1,32 +0,0 @@
// eslint-disable-next-line no-shadow
export const enum AVIFTune {
auto,
psnr,
ssim,
}
export interface EncodeOptions {
cqLevel: number
denoiseLevel: number
cqAlphaLevel: number
tileRowsLog2: number
tileColsLog2: number
speed: number
subsample: number
chromaDeltaQ: boolean
sharpness: number
tune: AVIFTune
}
export interface AVIFModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions
): Uint8Array
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<AVIFModule>
export default moduleFactory

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,361 +0,0 @@
import { instantiateEmscriptenWasm } from './emscripten-utils.js'
interface DecodeModule extends EmscriptenWasm.Module {
decode: (data: Uint8Array) => ImageData
}
type DecodeModuleFactory = EmscriptenWasm.ModuleFactory<DecodeModule>
interface RotateModuleInstance {
exports: {
memory: WebAssembly.Memory
rotate(width: number, height: number, rotate: number): void
}
}
interface ResizeWithAspectParams {
input_width: number
input_height: number
target_width?: number
target_height?: number
}
export interface ResizeOptions {
width?: number
height?: number
method: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
premultiply: boolean
linearRGB: boolean
}
export interface RotateOptions {
numRotations: number
}
// MozJPEG
import type { MozJPEGModule as MozJPEGEncodeModule } from './mozjpeg/mozjpeg_enc'
import mozDec from './mozjpeg/mozjpeg_node_dec.js'
import mozDecWasm from './mozjpeg/mozjpeg_node_dec.wasm.js'
import mozEnc from './mozjpeg/mozjpeg_node_enc.js'
import mozEncWasm from './mozjpeg/mozjpeg_node_enc.wasm.js'
// WebP
import type { WebPModule as WebPEncodeModule } from './webp/webp_enc'
import webpDec from './webp/webp_node_dec.js'
import webpDecWasm from './webp/webp_node_dec.wasm.js'
import webpEnc from './webp/webp_node_enc.js'
import webpEncWasm from './webp/webp_node_enc.wasm.js'
// AVIF
import type { AVIFModule as AVIFEncodeModule } from './avif/avif_enc'
import avifDec from './avif/avif_node_dec.js'
import avifDecWasm from './avif/avif_node_dec.wasm.js'
import avifEnc from './avif/avif_node_enc.js'
import avifEncWasm from './avif/avif_node_enc.wasm.js'
// PNG
import * as pngEncDec from './png/squoosh_png.js'
import pngEncDecWasm from './png/squoosh_png_bg.wasm.js'
const pngEncDecInit = () =>
pngEncDec.default(pngEncDecWasm)
// OxiPNG
import * as oxipng from './png/squoosh_oxipng.js'
import oxipngWasm from './png/squoosh_oxipng_bg.wasm.js'
const oxipngInit = () => oxipng.default(oxipngWasm)
// Resize
import * as resize from './resize/squoosh_resize.js'
import resizeWasm from './resize/squoosh_resize_bg.wasm.js'
const resizeInit = () => resize.default(resizeWasm)
// rotate
import rotateWasm from './rotate/rotate.wasm.js'
// Our decoders currently rely on a `ImageData` global.
import ImageData from './image_data.js'
(global as any).ImageData = ImageData
function resizeNameToIndex(
name: 'triangle' | 'catrom' | 'mitchell' | 'lanczos3'
) {
switch (name) {
case 'triangle':
return 0
case 'catrom':
return 1
case 'mitchell':
return 2
case 'lanczos3':
return 3
default:
throw Error(`Unknown resize algorithm "${name}"`)
}
}
function resizeWithAspect({
input_width,
input_height,
target_width,
target_height,
}: ResizeWithAspectParams): { width: number; height: number } {
if (!target_width && !target_height) {
throw Error('Need to specify at least width or height when resizing')
}
if (target_width && target_height) {
return { width: target_width, height: target_height }
}
if (!target_width) {
return {
width: Math.round((input_width / input_height) * target_height!),
height: target_height!,
}
}
return {
width: target_width,
height: Math.round((input_height / input_width) * target_width),
}
}
export const preprocessors = {
resize: {
name: 'Resize',
description: 'Resize the image before compressing',
instantiate: async () => {
await resizeInit()
return (
buffer: Uint8Array,
input_width: number,
input_height: number,
{ width, height, method, premultiply, linearRGB }: ResizeOptions
) => {
;({ width, height } = resizeWithAspect({
input_width,
input_height,
target_width: width,
target_height: height,
}))
const imageData = new ImageData(
resize.resize(
buffer,
input_width,
input_height,
width,
height,
resizeNameToIndex(method),
premultiply,
linearRGB
),
width,
height
)
resize.cleanup()
return imageData
}
},
defaultOptions: {
method: 'lanczos3',
fitMethod: 'stretch',
premultiply: true,
linearRGB: true,
},
},
rotate: {
name: 'Rotate',
description: 'Rotate image',
instantiate: async () => {
return async (
buffer: Uint8Array,
width: number,
height: number,
{ numRotations }: RotateOptions
) => {
const degrees = (numRotations * 90) % 360
const sameDimensions = degrees === 0 || degrees === 180
const size = width * height * 4
const instance = (
await WebAssembly.instantiate(rotateWasm)
).instance as RotateModuleInstance
const { memory } = instance.exports
const additionalPagesNeeded = Math.ceil(
(size * 2 - memory.buffer.byteLength + 8) / (64 * 1024)
)
if (additionalPagesNeeded > 0) {
memory.grow(additionalPagesNeeded)
}
const view = new Uint8ClampedArray(memory.buffer)
view.set(buffer, 8)
instance.exports.rotate(width, height, degrees)
return new ImageData(
view.slice(size + 8, size * 2 + 8),
sameDimensions ? width : height,
sameDimensions ? height : width
)
}
},
defaultOptions: {
numRotations: 0,
},
},
} as const
export const codecs = {
mozjpeg: {
name: 'MozJPEG',
extension: 'jpg',
detectors: [/^\xFF\xD8\xFF/],
dec: () =>
instantiateEmscriptenWasm(mozDec as DecodeModuleFactory, mozDecWasm),
enc: () =>
instantiateEmscriptenWasm(
mozEnc as EmscriptenWasm.ModuleFactory<MozJPEGEncodeModule>,
mozEncWasm
),
defaultEncoderOptions: {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: 3 /*YCbCr*/,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
webp: {
name: 'WebP',
extension: 'webp',
detectors: [/^RIFF....WEBPVP8[LX ]/s],
dec: () =>
instantiateEmscriptenWasm(webpDec as DecodeModuleFactory, webpDecWasm),
enc: () =>
instantiateEmscriptenWasm(
webpEnc as EmscriptenWasm.ModuleFactory<WebPEncodeModule>,
webpEncWasm
),
defaultEncoderOptions: {
quality: 75,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 100,
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
},
autoOptimize: {
option: 'quality',
min: 0,
max: 100,
},
},
avif: {
name: 'AVIF',
extension: 'avif',
detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/],
dec: () =>
instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm),
enc: async () => {
return instantiateEmscriptenWasm(
avifEnc as EmscriptenWasm.ModuleFactory<AVIFEncodeModule>,
avifEncWasm
)
},
defaultEncoderOptions: {
cqLevel: 33,
cqAlphaLevel: -1,
denoiseLevel: 0,
tileColsLog2: 0,
tileRowsLog2: 0,
speed: 6,
subsample: 1,
chromaDeltaQ: false,
sharpness: 0,
tune: 0 /* AVIFTune.auto */,
},
autoOptimize: {
option: 'cqLevel',
min: 62,
max: 0,
},
},
oxipng: {
name: 'OxiPNG',
extension: 'png',
detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/],
dec: async () => {
await pngEncDecInit()
return {
decode: (buffer: Buffer | Uint8Array) => {
const imageData = pngEncDec.decode(buffer)
pngEncDec.cleanup()
return imageData
},
}
},
enc: async () => {
await pngEncDecInit()
await oxipngInit()
return {
encode: (
buffer: Uint8ClampedArray | ArrayBuffer,
width: number,
height: number,
opts: { level: number }
) => {
const simplePng = pngEncDec.encode(
new Uint8Array(buffer),
width,
height
)
const imageData = oxipng.optimise(simplePng, opts.level, false)
oxipng.cleanup()
return imageData
},
}
},
defaultEncoderOptions: {
level: 2,
},
autoOptimize: {
option: 'level',
min: 6,
max: 1,
},
},
} as const

View file

@ -1,121 +0,0 @@
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
declare namespace EmscriptenWasm {
type ModuleFactory<T extends Module = Module> = (
moduleOverrides?: ModuleOpts
) => Promise<T>
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'
// Options object for modularized Emscripten files. Shoe-horned by @surma.
// FIXME: This an incomplete definition!
interface ModuleOpts {
mainScriptUrlOrBlob?: string
noInitialRun?: boolean
locateFile?: (url: string) => string
onRuntimeInitialized?: () => void
}
interface Module {
print(str: string): void
printErr(str: string): void
arguments: string[]
environment: EnvironmentType
preInit: { (): void }[]
preRun: { (): void }[]
postRun: { (): void }[]
preinitializedWebGLContext: WebGLRenderingContext
noInitialRun: boolean
noExitRuntime: boolean
logReadFiles: boolean
filePackagePrefixURL: string
wasmBinary: ArrayBuffer
destroy(object: object): void
getPreloadedPackage(
remotePackageName: string,
remotePackageSize: number
): ArrayBuffer
instantiateWasm(
imports: WebAssembly.Imports,
successCallback: (module: WebAssembly.Module) => void
): WebAssembly.Exports
locateFile(url: string): string
onCustomMessage(event: MessageEvent): void
Runtime: any
ccall(
ident: string,
returnType: string | null,
argTypes: string[],
args: any[]
): any
cwrap(ident: string, returnType: string | null, argTypes: string[]): any
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void
getValue(ptr: number, type: string, noSafe?: boolean): number
ALLOC_NORMAL: number
ALLOC_STACK: number
ALLOC_STATIC: number
ALLOC_DYNAMIC: number
ALLOC_NONE: number
allocate(slab: any, types: string, allocator: number, ptr: number): number
allocate(slab: any, types: string[], allocator: number, ptr: number): number
Pointer_stringify(ptr: number, length?: number): string
UTF16ToString(ptr: number): string
stringToUTF16(str: string, outPtr: number): void
UTF32ToString(ptr: number): string
stringToUTF32(str: string, outPtr: number): void
// USE_TYPED_ARRAYS == 1
HEAP: Int32Array
IHEAP: Int32Array
FHEAP: Float64Array
// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array
HEAP16: Int16Array
HEAP32: Int32Array
HEAPU8: Uint8Array
HEAPU16: Uint16Array
HEAPU32: Uint32Array
HEAPF32: Float32Array
HEAPF64: Float64Array
TOTAL_STACK: number
TOTAL_MEMORY: number
FAST_MEMORY: number
addOnPreRun(cb: () => any): void
addOnInit(cb: () => any): void
addOnPreMain(cb: () => any): void
addOnExit(cb: () => any): void
addOnPostRun(cb: () => any): void
// Tools
intArrayFromString(
stringy: string,
dontAddNull?: boolean,
length?: number
): number[]
intArrayToString(array: number[]): string
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void
writeArrayToMemory(array: number[], buffer: number): void
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void
addRunDependency(id: any): void
removeRunDependency(id: any): void
preloadedImages: any
preloadedAudios: any
_malloc(size: number): number
_free(ptr: number): void
// Augmentations below by @surma.
onRuntimeInitialized: () => void | null
}
}

View file

@ -1,39 +0,0 @@
//
import { fileURLToPath, pathToFileURL } from 'node:url'
export function pathify(path: string): string {
if (path.startsWith('file://')) {
path = fileURLToPath(path)
}
return path
}
export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
factory: EmscriptenWasm.ModuleFactory<T>,
bytes: Uint8Array,
): Promise<T> {
return factory({
// @ts-expect-error
wasmBinary: bytes,
locateFile(file: string) {
return file
}
})
}
export function dirname(url: string) {
return url.substring(0, url.lastIndexOf('/'))
}
/**
* On certain serverless hosts, our ESM bundle is transpiled to CJS before being run, which means
* import.meta.url is undefined, so we'll fall back to __filename in those cases
* We should be able to remove this once https://github.com/netlify/zip-it-and-ship-it/issues/750 is fixed
*/
export function getModuleURL(url: string | undefined): string {
if (!url) {
return pathToFileURL(__filename).toString();
}
return url
}

View file

@ -1,149 +0,0 @@
import { cpus } from 'node:os';
import { fileURLToPath } from 'node:url';
import { isMainThread } from 'node:worker_threads';
import type { OutputFormat } from '../../loaders/index.js';
import execOnce from '../../utils/execOnce.js';
import WorkerPool from '../../utils/workerPool.js';
import { getModuleURL } from './emscripten-utils.js';
import type { Operation } from './image.js';
import * as impl from './impl.js';
const getWorker = execOnce(() => {
return new WorkerPool(
// There will be at most 7 workers needed since each worker will take
// at least 1 operation type.
Math.max(1, Math.min(cpus().length - 1, 7)),
fileURLToPath(getModuleURL(import.meta.url))
);
});
type DecodeParams = {
operation: 'decode';
buffer: Buffer;
};
type ResizeParams = {
operation: 'resize';
imageData: ImageData;
height?: number;
width?: number;
};
type RotateParams = {
operation: 'rotate';
imageData: ImageData;
numRotations: number;
};
type EncodeAvifParams = {
operation: 'encodeavif';
imageData: ImageData;
quality: number;
};
type EncodeJpegParams = {
operation: 'encodejpeg';
imageData: ImageData;
quality: number;
};
type EncodePngParams = {
operation: 'encodepng';
imageData: ImageData;
};
type EncodeWebpParams = {
operation: 'encodewebp';
imageData: ImageData;
quality: number;
};
type JobMessage =
| DecodeParams
| ResizeParams
| RotateParams
| EncodeAvifParams
| EncodeJpegParams
| EncodePngParams
| EncodeWebpParams;
function handleJob(params: JobMessage) {
switch (params.operation) {
case 'decode':
return impl.decodeBuffer(params.buffer);
case 'resize':
return impl.resize({
image: params.imageData as any,
width: params.width,
height: params.height,
});
case 'rotate':
return impl.rotate(params.imageData as any, params.numRotations);
case 'encodeavif':
return impl.encodeAvif(params.imageData as any, { quality: params.quality });
case 'encodejpeg':
return impl.encodeJpeg(params.imageData as any, { quality: params.quality });
case 'encodepng':
return impl.encodePng(params.imageData as any);
case 'encodewebp':
return impl.encodeWebp(params.imageData as any, { quality: params.quality });
default:
throw Error(`Invalid job "${(params as any).operation}"`);
}
}
export async function processBuffer(
buffer: Buffer,
operations: Operation[],
encoding: OutputFormat,
quality?: number
): Promise<Uint8Array> {
const worker = await getWorker();
let imageData = await worker.dispatchJob({
operation: 'decode',
buffer,
});
for (const operation of operations) {
if (operation.type === 'rotate') {
imageData = await worker.dispatchJob({
operation: 'rotate',
imageData,
numRotations: operation.numRotations,
});
} else if (operation.type === 'resize') {
imageData = await worker.dispatchJob({
operation: 'resize',
imageData,
height: operation.height,
width: operation.width,
});
}
}
switch (encoding) {
case 'avif':
return (await worker.dispatchJob({
operation: 'encodeavif',
imageData,
quality,
})) as Uint8Array;
case 'jpeg':
case 'jpg':
return (await worker.dispatchJob({
operation: 'encodejpeg',
imageData,
quality,
})) as Uint8Array;
case 'png':
return (await worker.dispatchJob({
operation: 'encodepng',
imageData,
})) as Uint8Array;
case 'webp':
return (await worker.dispatchJob({
operation: 'encodewebp',
imageData,
quality,
})) as Uint8Array;
default:
throw Error(`Unsupported encoding format`);
}
}
if (!isMainThread) {
WorkerPool.useThisThreadAsWorker(handleJob);
}

View file

@ -1,43 +0,0 @@
import type { OutputFormat } from '../../loaders/index.js';
import * as impl from './impl.js';
type RotateOperation = {
type: 'rotate'
numRotations: number
}
type ResizeOperation = {
type: 'resize'
width?: number
height?: number
}
export type Operation = RotateOperation | ResizeOperation
export async function processBuffer(
buffer: Buffer,
operations: Operation[],
encoding: OutputFormat,
quality?: number
): Promise<Uint8Array> {
let imageData = await impl.decodeBuffer(buffer)
for (const operation of operations) {
if (operation.type === 'rotate') {
imageData = await impl.rotate(imageData, operation.numRotations);
} else if (operation.type === 'resize') {
imageData = await impl.resize({ image: imageData, width: operation.width, height: operation.height })
}
}
switch (encoding) {
case 'avif':
return await impl.encodeAvif(imageData, { quality });
case 'jpeg':
case 'jpg':
return await impl.encodeJpeg(imageData, { quality });
case 'png':
return await impl.encodePng(imageData);
case 'webp':
return await impl.encodeWebp(imageData, { quality });
default:
throw Error(`Unsupported encoding format`)
}
}

View file

@ -1,33 +0,0 @@
export default class ImageData {
static from(input: ImageData): ImageData {
return new ImageData(input.data || input._data, input.width, input.height)
}
private _data: Buffer | Uint8Array | Uint8ClampedArray
width: number
height: number
get data(): Buffer {
if (Object.prototype.toString.call(this._data) === '[object Object]') {
return Buffer.from(Object.values(this._data))
}
if (
this._data instanceof Buffer ||
this._data instanceof Uint8Array ||
this._data instanceof Uint8ClampedArray
) {
return Buffer.from(this._data)
}
throw new Error('invariant')
}
constructor(
data: Buffer | Uint8Array | Uint8ClampedArray,
width: number,
height: number
) {
this._data = data
this.width = width
this.height = height
}
}

View file

@ -1,142 +0,0 @@
import { preprocessors, codecs as supportedFormats } from './codecs.js'
import ImageData from './image_data.js'
type EncoderKey = keyof typeof supportedFormats
const DELAY_MS = 1000
let _promise: Promise<void> | undefined
function delayOnce(ms: number): Promise<void> {
if (!_promise) {
_promise = new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
return _promise
}
function maybeDelay(): Promise<void> {
const isAppleM1 = process.arch === 'arm64' && process.platform === 'darwin'
if (isAppleM1) {
return delayOnce(DELAY_MS)
}
return Promise.resolve()
}
export async function decodeBuffer(
_buffer: Buffer | Uint8Array
): Promise<ImageData> {
const buffer = Buffer.from(_buffer)
const firstChunk = buffer.slice(0, 16)
const firstChunkString = Array.from(firstChunk)
.map((v) => String.fromCodePoint(v))
.join('')
// TODO (future PR): support more formats
if (firstChunkString.includes('GIF')) {
throw Error(`GIF images are not supported, please install the @astrojs/image/sharp plugin`)
}
const key = Object.entries(supportedFormats).find(([, { detectors }]) =>
detectors.some((detector) => detector.exec(firstChunkString))
)?.[0] as EncoderKey | undefined
if (!key) {
throw Error(`Buffer has an unsupported format`)
}
const encoder = supportedFormats[key]
const mod = await encoder.dec()
const rgba = mod.decode(new Uint8Array(buffer))
return rgba
}
export async function rotate(
image: ImageData,
numRotations: number
): Promise<ImageData> {
image = ImageData.from(image)
const m = await preprocessors['rotate'].instantiate()
return await m(image.data, image.width, image.height, { numRotations })
}
type ResizeOpts = { image: ImageData } & { width?: number; height?: number }
export async function resize({ image, width, height }: ResizeOpts) {
image = ImageData.from(image)
const p = preprocessors['resize']
const m = await p.instantiate()
await maybeDelay()
return await m(image.data, image.width, image.height, {
...p.defaultOptions,
width,
height,
})
}
export async function encodeJpeg(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['mozjpeg']
const m = await e.enc()
await maybeDelay()
const quality = opts.quality || e.defaultEncoderOptions.quality
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
quality,
})
return r
}
export async function encodeWebp(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['webp']
const m = await e.enc()
await maybeDelay()
const quality = opts.quality || e.defaultEncoderOptions.quality
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
quality,
})
return r
}
export async function encodeAvif(
image: ImageData,
opts: { quality?: number }
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['avif']
const m = await e.enc()
await maybeDelay()
const val = e.autoOptimize.min
// AVIF doesn't use a 0-100 quality, default to 75 and convert to cqLevel below
const quality = opts.quality || 75
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
// Think of cqLevel as the "amount" of quantization (0 to 62),
// so a lower value yields higher quality (0 to 100).
cqLevel: quality === 0 ? val : Math.round(val - (quality / 100) * val),
})
return r
}
export async function encodePng(
image: ImageData
): Promise<Uint8Array> {
image = ImageData.from(image)
const e = supportedFormats['oxipng']
const m = await e.enc()
await maybeDelay()
const r = await m.encode(image.data, image.width, image.height, {
...e.defaultEncoderOptions,
})
return r
}

View file

@ -1,38 +0,0 @@
// eslint-disable-next-line no-shadow
export const enum MozJpegColorSpace {
GRAYSCALE = 1,
RGB,
YCbCr,
}
export interface EncodeOptions {
quality: number
baseline: boolean
arithmetic: boolean
progressive: boolean
optimize_coding: boolean
smoothing: number
color_space: MozJpegColorSpace
quant_table: number
trellis_multipass: boolean
trellis_opt_zero: boolean
trellis_opt_table: boolean
trellis_loops: number
auto_subsample: boolean
chroma_subsample: number
separate_chroma_quality: boolean
chroma_quality: number
}
export interface MozJPEGModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions
): Uint8Array
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>
export default moduleFactory

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,120 +0,0 @@
// @ts-nocheck
let wasm
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
})
cachedTextDecoder.decode()
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} data
* @param {number} level
* @param {boolean} interlace
* @returns {Uint8Array}
*/
export function optimise(data, level, interlace) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.optimise(retptr, ptr0, len0, level, interlace)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
imports.wbg = {}
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1))
}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

File diff suppressed because one or more lines are too long

View file

@ -1,184 +0,0 @@
// @ts-nocheck
let wasm
let cachedTextDecoder = new TextDecoder('utf-8', {
ignoreBOM: true,
fatal: true,
})
cachedTextDecoder.decode()
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len))
}
let cachegetUint8ClampedMemory0 = null
function getUint8ClampedMemory0() {
if (
cachegetUint8ClampedMemory0 === null ||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
}
return cachegetUint8ClampedMemory0
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
}
const heap = new Array(32).fill(undefined)
heap.push(undefined, null, true, false)
let heap_next = heap.length
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1)
const idx = heap_next
heap_next = heap[idx]
heap[idx] = obj
return idx
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} data
* @param {number} width
* @param {number} height
* @returns {Uint8Array}
*/
export function encode(data, width, height) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.encode(retptr, ptr0, len0, width, height)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
function getObject(idx) {
return heap[idx]
}
function dropObject(idx) {
if (idx < 36) return
heap[idx] = heap_next
heap_next = idx
}
function takeObject(idx) {
const ret = getObject(idx)
dropObject(idx)
return ret
}
/**
* @param {Uint8Array} data
* @returns {ImageData}
*/
export function decode(data) {
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
const ret = wasm.decode(ptr0, len0)
return takeObject(ret)
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
imports.wbg = {}
imports.wbg.__wbg_newwithownedu8clampedarrayandsh_787b2db8ea6bfd62 =
function (arg0, arg1, arg2, arg3) {
const v0 = getClampedArrayU8FromWasm0(arg0, arg1).slice()
wasm.__wbindgen_free(arg0, arg1 * 1)
const ret = new ImageData(v0, arg2 >>> 0, arg3 >>> 0)
return addHeapObject(ret)
}
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1))
}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8ClampedMemory0 = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

File diff suppressed because one or more lines are too long

View file

@ -1,141 +0,0 @@
// @ts-nocheck
let wasm
let cachegetUint8Memory0 = null
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer)
}
return cachegetUint8Memory0
}
let WASM_VECTOR_LEN = 0
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1)
getUint8Memory0().set(arg, ptr / 1)
WASM_VECTOR_LEN = arg.length
return ptr
}
let cachegetInt32Memory0 = null
function getInt32Memory0() {
if (
cachegetInt32Memory0 === null ||
cachegetInt32Memory0.buffer !== wasm.memory.buffer
) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer)
}
return cachegetInt32Memory0
}
let cachegetUint8ClampedMemory0 = null
function getUint8ClampedMemory0() {
if (
cachegetUint8ClampedMemory0 === null ||
cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer
) {
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer)
}
return cachegetUint8ClampedMemory0
}
function getClampedArrayU8FromWasm0(ptr, len) {
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len)
}
/**
* @param {Uint8Array} input_image
* @param {number} input_width
* @param {number} input_height
* @param {number} output_width
* @param {number} output_height
* @param {number} typ_idx
* @param {boolean} premultiply
* @param {boolean} color_space_conversion
* @returns {Uint8ClampedArray}
*/
export function resize(
input_image,
input_width,
input_height,
output_width,
output_height,
typ_idx,
premultiply,
color_space_conversion
) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16)
const ptr0 = passArray8ToWasm0(input_image, wasm.__wbindgen_malloc)
const len0 = WASM_VECTOR_LEN
wasm.resize(
retptr,
ptr0,
len0,
input_width,
input_height,
output_width,
output_height,
typ_idx,
premultiply,
color_space_conversion
)
const r0 = getInt32Memory0()[retptr / 4 + 0]
const r1 = getInt32Memory0()[retptr / 4 + 1]
const v1 = getClampedArrayU8FromWasm0(r0, r1).slice()
wasm.__wbindgen_free(r0, r1 * 1)
return v1
} finally {
wasm.__wbindgen_add_to_stack_pointer(16)
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
return await WebAssembly.instantiateStreaming(module, imports)
}
const bytes = await module.arrayBuffer()
return await WebAssembly.instantiate(bytes, imports)
} else {
const instance = await WebAssembly.instantiate(module, imports)
if (instance instanceof WebAssembly.Instance) {
return { instance, module }
} else {
return instance
}
}
}
async function init(input) {
const imports = {}
if (
typeof input === 'string' ||
(typeof Request === 'function' && input instanceof Request) ||
(typeof URL === 'function' && input instanceof URL)
) {
input = fetch(input)
}
const { instance, module } = await load(await input, imports)
wasm = instance.exports
init.__wbindgen_wasm_module = module
return wasm
}
export default init
// Manually remove the wasm and memory references to trigger GC
export function cleanup() {
wasm = null
cachegetUint8Memory0 = null
cachegetInt32Memory0 = null
}

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
export default Buffer.from("AGFzbQEAAAABDAJgAn9/AGADf39/AAMGBQAAAAABBQMBABAGEQJ/AEGAgMAAC38AQYCAwAALBy4EBm1lbW9yeQIABnJvdGF0ZQAECl9fZGF0YV9lbmQDAAtfX2hlYXBfYmFzZQMBCpsJBUkBAX8gACABbCIAQf////8DcSICBEBBCCEBIABBAnRBCGohAANAIAAgASgCADYCACABQQRqIQEgAEEEaiEAIAJBf2oiAg0ACwsLzQMBFH8gAUECdCERIAAgAWwiDEECdEEEaiESA0ACQAJAAkACQCAEQQFxRQRAIAMgAU8NAiADQQFqIQgMAQsgA0EPaiICIANJIggNASACIAFJIgVFDQEgASADQRBqIAgbIAEgBRshCCACIQMLIAEgA0EQaiICIAIgAUsbIQ0gA0F/cyETIBIgA0ECdGshFEEAIQVBACEOA0ACQAJAIA5FBEAgBSAASQ0BQQEhBAwGCyAAIAVBEGogBUEPaiICIAVJIgcbIAAgAiAASRshBUEBIQQgByACIABPcg0FDAELIAUiAkEBaiEFC0EBIQ4gAyANTw0AIAAgAkEQaiIPIAAgD0kbQQJ0IAJBAnRrIRUgEyABIAJsaiEHIBQgASACQQFqbEECdGohCSADIQoDQCAAIApsIgYgAmoiBEEQaiAAIAZqIA8gAEkbIgYgBEkgDCAGSXINAyAEIAZHBEAgBEECdEEIaiELIBUhBiAHIRAgCSEEA0AgDCABIBBqIhBNDQUgBCALKAIANgIAIAQgEWohBCALQQRqIQsgBkF8aiIGDQALCyAHQX9qIQcgCUF8aiEJIA0gCkEBaiIKRw0ACwwACwALDwsACyAIIQMMAAsAC1MBAX8CQCAAIAFsQQJ0IgJBCGoiAEEIRg0AIAAgAmpBfGohAEEAIQEDQCABIAJGDQEgACABQQhqKAIANgIAIABBfGohACACIAFBBGoiAUcNAAsLC9oDARN/IABBf2ohEEEAIAFBAnRrIREgACABbCIMQQJ0QQhqIRIDQAJAAkACQAJAIARBAXFFBEAgAyABTw0CIANBAWohCQwBCyADQQ9qIgIgA0kiCQ0BIAIgAUkiBUUNASABIANBEGogCRsgASAFGyEJIAIhAwsgASADQRBqIgIgAiABSxshDSASIANBAnRqIRNBACEFQQAhBgNAAkACQCAGQQFxRQRAIAUgAEkNAUEBIQQMBgsgACAFQRBqIAVBD2oiAiAFSSIIGyAAIAIgAEkbIQVBASEEIAggAiAAT3INBQwBCyAFIgJBAWohBQtBASEGIAMgDU8NACAAIAJBEGoiDiAAIA5JG0ECdCACQQJ0ayEUIAMgASAAIAJrbGohCCATIAEgECACa2xBAnRqIQogAyELA0AgACALbCIHIAJqIgRBEGogACAHaiAOIABJGyIHIARJIAwgB0lyDQMgBCAHRwRAIARBAnRBCGohBiAUIQcgCCEPIAohBANAIAwgDyABayIPTQ0FIAQgBigCADYCACAEIBFqIQQgBkEEaiEGIAdBfGoiBw0ACwtBASEGIAhBAWohCCAKQQRqIQogDSALQQFqIgtHDQALDAALAAsPCwALIAkhAwwACwALUAACQAJAAkACQCACQbMBTARAIAJFDQIgAkHaAEcNASAAIAEQAQ8LIAJBtAFGDQIgAkGOAkYNAwsACyAAIAEQAA8LIAAgARACDwsgACABEAMLAE0JcHJvZHVjZXJzAghsYW5ndWFnZQEEUnVzdAAMcHJvY2Vzc2VkLWJ5AQVydXN0Yx0xLjQ3LjAgKDE4YmY2YjRmMCAyMDIwLTEwLTA3KQ==", 'base64');

View file

@ -1,42 +0,0 @@
export interface EncodeOptions {
quality: number
target_size: number
target_PSNR: number
method: number
sns_strength: number
filter_strength: number
filter_sharpness: number
filter_type: number
partitions: number
segments: number
pass: number
show_compressed: number
preprocessing: number
autofilter: number
partition_limit: number
alpha_compression: number
alpha_filtering: number
alpha_quality: number
lossless: number
exact: number
image_hint: number
emulate_jpeg_size: number
thread_level: number
low_memory: number
near_lossless: number
use_delta_palette: number
use_sharp_yuv: number
}
export interface WebPModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions
): Uint8Array
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>
export default moduleFactory

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,140 +0,0 @@
import type { AstroConfig } from 'astro';
import MagicString from 'magic-string';
import mime from 'mime';
import fs from 'node:fs/promises';
import { basename, extname } from 'node:path';
import { Readable } from 'node:stream';
import { pathToFileURL } from 'node:url';
import type { Plugin, ResolvedConfig } from 'vite';
import type { IntegrationOptions } from './index.js';
import type { InputFormat } from './loaders/index.js';
import { metadata } from './utils/metadata.js';
import { appendForwardSlash } from './utils/paths.js';
export interface ImageMetadata {
src: string;
width: number;
height: number;
format: InputFormat;
}
export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin {
const filter = (id: string) =>
/^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id);
const virtualModuleId = 'virtual:image-loader';
let resolvedConfig: ResolvedConfig;
return {
name: '@astrojs/image',
enforce: 'pre',
configResolved(viteConfig) {
resolvedConfig = viteConfig;
},
async resolveId(id) {
// The virtual model redirects imports to the ImageService being used
// This ensures the module is available in `astro dev` and is included
// in the SSR server bundle.
if (id === virtualModuleId) {
return await this.resolve(options.serviceEntryPoint);
}
},
async load(id) {
// only claim image ESM imports
if (!filter(id)) {
return null;
}
const url = pathToFileURL(id);
const meta = await metadata(url);
if (!meta) {
return;
}
if (!this.meta.watchMode) {
const pathname = decodeURI(url.pathname);
const filename = basename(pathname, extname(pathname) + `.${meta.format}`);
const handle = this.emitFile({
name: filename,
source: await fs.readFile(url),
type: 'asset',
});
meta.src = `__ASTRO_IMAGE_ASSET__${handle}__`;
} else {
meta.src = '/@astroimage' + url.pathname;
}
return `export default ${JSON.stringify(meta)}`;
},
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url?.startsWith('/@astroimage/')) {
// Reconstructing URL to get rid of query parameters in path
const url = new URL(req.url.slice('/@astroimage'.length), 'file:');
const file = await fs.readFile(url);
const meta = await metadata(url);
if (!meta) {
return next();
}
const transform = await globalThis.astroImage.defaultLoader.parseTransform(
url.searchParams
);
// if no transforms were added, the original file will be returned as-is
let data = file;
let format = meta.format;
if (transform) {
const result = await globalThis.astroImage.defaultLoader.transform(file, transform);
data = result.data;
format = result.format;
}
res.setHeader('Content-Type', mime.getType(format) || '');
res.setHeader('Cache-Control', 'max-age=360000');
const stream = Readable.from(data);
return stream.pipe(res);
}
return next();
});
},
async renderChunk(code) {
const assetUrlRE = /__ASTRO_IMAGE_ASSET__([a-z\d]{8})__(?:_(.*?)__)?/g;
let match;
let s;
while ((match = assetUrlRE.exec(code))) {
s = s || (s = new MagicString(code));
const [full, hash, postfix = ''] = match;
const file = this.getFileName(hash);
const prefix = config.build.assetsPrefix
? appendForwardSlash(config.build.assetsPrefix)
: config.base;
const outputFilepath = prefix + file + postfix;
s.overwrite(match.index, match.index + full.length, outputFilepath);
}
if (s) {
return {
code: s.toString(),
map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null,
};
} else {
return null;
}
},
};
}

View file

@ -1,22 +0,0 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
const assetsPrefixRegex = /^http:\/\/localhost:4321\/_astro\/.*/;
describe('Assets Prefix', function () {
/** @type {import('../../../astro/test/test-utils').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({ root: './fixtures/assets-prefix/' });
await fixture.build();
});
it('images src has assets prefix', async () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const img = $('#social-jpg');
expect(img.attr('src')).to.match(assetsPrefixRegex);
});
});

View file

@ -1,127 +0,0 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import sharp from 'sharp';
import { fileURLToPath } from 'node:url';
import { loadFixture } from './test-utils.js';
describe('SSG image with background - dev', function () {
let fixture;
let devServer;
let $;
before(async () => {
fixture = await loadFixture({ root: './fixtures/background-color-image/' });
devServer = await fixture.startDevServer();
const html = await fixture.fetch('/').then((res) => res.text());
$ = cheerio.load(html);
});
after(async () => {
await devServer.stop();
});
[
{
title: 'Named color',
id: '#named',
bg: 'dimgray',
},
{
title: 'Hex color',
id: '#hex',
bg: '#696969',
},
{
title: 'Hex color short',
id: '#hex-short',
bg: '#666',
},
{
title: 'RGB color',
id: '#rgb',
bg: 'rgb(105,105,105)',
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
bg: 'rgb(105, 105, 105)',
},
].forEach(({ title, id, bg }) => {
it(title, async () => {
const image = $(id);
const src = image.attr('src');
const [, params] = src.split('?');
const searchParams = new URLSearchParams(params);
expect(searchParams.get('bg')).to.equal(bg);
});
});
});
describe('SSG image with background - build', function () {
let fixture;
let $;
let html;
before(async () => {
fixture = await loadFixture({ root: './fixtures/background-color-image/' });
await fixture.build();
html = await fixture.readFile('/index.html');
$ = cheerio.load(html);
});
async function verifyImage(pathname, expectedBg) {
const url = new URL('./fixtures/background-color-image/dist/' + pathname, import.meta.url);
const dist = fileURLToPath(url);
const data = await sharp(dist).raw().toBuffer();
// check that the first RGB pixel indeed has the requested background color
expect(data[0]).to.equal(expectedBg[0]);
expect(data[1]).to.equal(expectedBg[1]);
expect(data[2]).to.equal(expectedBg[2]);
}
[
{
title: 'Named color',
id: '#named',
bg: [105, 105, 105],
},
{
title: 'Hex color',
id: '#hex',
bg: [105, 105, 105],
},
{
title: 'Hex color short',
id: '#hex-short',
bg: [102, 102, 102],
},
{
title: 'RGB color',
id: '#rgb',
bg: [105, 105, 105],
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
bg: [105, 105, 105],
},
{
title: 'RGBA color',
id: '#rgba',
bg: [105, 105, 105],
},
{
title: 'RGBA color with spaces',
id: '#rgba-spaced',
bg: [105, 105, 105],
},
].forEach(({ title, id, bg }) => {
it(title, async () => {
const image = $(id);
const src = image.attr('src');
await verifyImage(src, bg);
});
});
});

View file

@ -1,120 +0,0 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import testAdapter from '../../../astro/test/test-adapter.js';
let fixture;
describe('SSR image with background', function () {
before(async () => {
fixture = await loadFixture({
root: './fixtures/background-color-image/',
adapter: testAdapter({ streaming: false }),
output: 'server',
});
await fixture.build();
});
[
{
title: 'Named color',
id: '#named',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: 'dimgray',
},
},
{
title: 'Hex color',
id: '#hex',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: '#696969',
},
},
{
title: 'Hex color short',
id: '#hex-short',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: '#666',
},
},
{
title: 'RGB color',
id: '#rgb',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: 'rgb(105,105,105)',
},
},
{
title: 'RGB color with spaces',
id: '#rgb-spaced',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: 'rgb(105, 105, 105)',
},
},
{
title: 'RGBA color',
id: '#rgba',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: 'rgb(105,105,105,0.5)',
},
},
{
title: 'RGBA color with spaces',
id: '#rgba-spaced',
query: {
f: 'jpeg',
w: '256',
h: '256',
href: /^\/_astro\/file-icon.\w{8}.png/,
bg: 'rgb(105, 105, 105, 0.5)',
},
},
].forEach(({ title, id, query }) => {
it(title, async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/');
const response = await app.render(request);
const html = await response.text();
const $ = cheerio.load(html);
const image = $(id);
const src = image.attr('src');
const [, params] = src.split('?');
const searchParams = new URLSearchParams(params);
for (const [key, value] of Object.entries(query)) {
if (typeof value === 'string') {
expect(searchParams.get(key)).to.equal(value);
} else {
expect(searchParams.get(key)).to.match(value);
}
}
});
});
});

View file

@ -1,10 +0,0 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
integrations: [image()],
build: {
assetsPrefix: 'http://localhost:4321',
}
});

View file

@ -1,9 +0,0 @@
{
"name": "@test/image-assets-prefix",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"astro": "workspace:*"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,13 +0,0 @@
---
import socialJpg from '../assets/social.png';
import { Image } from '@astrojs/image/components';
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
</body>
</html>

View file

@ -1,8 +0,0 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'http://localhost:4321',
integrations: [image({ logLevel: 'silent', serviceEntryPoint: '@astrojs/image/sharp' })]
});

View file

@ -1,11 +0,0 @@
{
"name": "@test/background-color-image",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*",
"sharp": "^0.32.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,44 +0,0 @@
import mime from 'mime';
import fs from 'node:fs';
import { createServer } from 'node:http';
import { handler as ssrHandler } from '../dist/server/entry.mjs';
const clientRoot = new URL('../dist/client/', import.meta.url);
async function handle(req, res) {
ssrHandler(req, res, async (err) => {
if (err) {
res.writeHead(500);
res.end(err.stack);
return;
}
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
});
}
const server = createServer((req, res) => {
handle(req, res).catch((err) => {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(err.toString());
});
});
server.listen(8085);
console.log('Serving at http://localhost:8085');
// Silence weird <time> warning
console.error = () => {};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,25 +0,0 @@
---
import { Image } from '@astrojs/image/components';
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="named" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="dimgray" alt="named" />
<br />
<Image id="hex" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="#696969" alt="hex" />
<br />
<Image id="hex-short" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="#666" alt="hex-short" />
<br />
<Image id="rgb" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="rgb(105,105,105)" alt="rgb" />
<br />
<Image id="rgb-spaced" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="rgb(105, 105, 105)" alt="rgb-spaced" />
<br />
<Image id="rgba" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="rgb(105,105,105,0.5)" alt="rgba" />
<br />
<Image id="rgba-spaced" src={import('../assets/file-icon.png')} width={256} format="jpeg" background="rgb(105, 105, 105, 0.5)" alt="rgba-spaced" />
<br />
</body>
</html>

View file

@ -1,8 +0,0 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'http://localhost:4321',
integrations: [image({ logLevel: 'silent', serviceEntryPoint: '@astrojs/image/sharp' })]
});

View file

@ -1,11 +0,0 @@
{
"name": "@test/basic-image",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*",
"sharp": "^0.32.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

View file

@ -1,44 +0,0 @@
import mime from 'mime';
import fs from 'node:fs';
import { createServer } from 'node:http';
import { handler as ssrHandler } from '../dist/server/entry.mjs';
const clientRoot = new URL('../dist/client/', import.meta.url);
async function handle(req, res) {
ssrHandler(req, res, async (err) => {
if (err) {
res.writeHead(500);
res.end(err.stack);
return;
}
let local = new URL('.' + req.url, clientRoot);
try {
const data = await fs.promises.readFile(local);
res.writeHead(200, {
'Content-Type': mime.getType(req.url),
});
res.end(data);
} catch {
res.writeHead(404);
res.end();
}
});
}
const server = createServer((req, res) => {
handle(req, res).catch((err) => {
console.error(err);
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(err.toString());
});
});
server.listen(8085);
console.log('Serving at http://localhost:8085');
// Silence weird <time> warning
console.error = () => {};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

View file

@ -1,22 +0,0 @@
<svg width="192" height="256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M131.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53L99.258 60.56a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
fill="url(#paint0_linear)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="#FF5D01" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M136.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.35-11.349 10.742 0 10.73 9.373 10.72 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
fill="url(#paint1_linear)" />
<defs>
<linearGradient id="paint0_linear" x1="144.599" y1="5.423" x2="95.791" y2="173.38" gradientUnits="userSpaceOnUse">
<stop stop-color="#000014" />
<stop offset="1" stop-color="#150426" />
</linearGradient>
<linearGradient id="paint1_linear" x1="168.336" y1="130.49" x2="126.065" y2="218.982"
gradientUnits="userSpaceOnUse">
<stop stop-color="#FF1639" />
<stop offset="1" stop-color="#FF1639" stop-opacity="0" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,37 +0,0 @@
---
import socialJpg from '../assets/social.jpg';
import logoSvg from '../assets/logo.svg';
import introJpg from '../assets/blog/introducing astro.jpg';
import outsideSrc from '../../social.png';
import { Image } from '@astrojs/image/components';
const publicImage = new URL('./hero.jpg', Astro.url);
---
<html>
<head>
<!-- Head Stuff -->
</head>
<body>
<Image id="hero" src={publicImage.pathname} width={768} height={414} format="webp" alt="hero" />
<br />
<Image id="spaces" src={introJpg} width={768} height={414} format="webp" alt="spaces" />
<br />
<Image id="social-jpg" src={socialJpg} width={506} height={253} alt="social-jpg" />
<br />
<Image id="no-transforms" src={socialJpg} alt="no-transforms" />
<br />
<Image id="outside-src" src={outsideSrc} alt="outside-src" />
<br />
<Image id="logo-svg" src={logoSvg} alt="logo-svg" />
<br />
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" />
<br />
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />
<br />
<Image id="query" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png?token=abc" width={544} height={184} format="webp" alt="query" />
<br />
<Image id="bg-color" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="jpeg" alt="Google" background="#333333" />
<br />
<Image id="ipsum" src="https://dummyimage.com/200x300" width={200} height={300} alt="ipsum" format="jpeg" />
</body>
</html>

View file

@ -1,8 +0,0 @@
import { defineConfig } from 'astro/config';
import image from '@astrojs/image';
// https://astro.build/config
export default defineConfig({
site: 'http://localhost:4321',
integrations: [image({ logLevel: 'silent', serviceEntryPoint: '@astrojs/image/sharp' })]
});

View file

@ -1,11 +0,0 @@
{
"name": "@test/basic-picture",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/image": "workspace:*",
"@astrojs/node": "workspace:*",
"astro": "workspace:*",
"sharp": "^0.32.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Some files were not shown because too many files have changed in this diff Show more