feat: remove @astrojs/image completely (#7922)
|
@ -23,7 +23,6 @@
|
|||
'@astrojs/prefetch': minor
|
||||
'@astrojs/markdoc': minor
|
||||
'@astrojs/underscore-redirects': minor
|
||||
'@astrojs/image': minor
|
||||
'@astrojs/mdx': minor
|
||||
'@astrojs/internal-helpers': minor
|
||||
---
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) |
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />');
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
||||
Astro’s `<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. doesn’t 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 Astro’s 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.
|
62
packages/integrations/image/client.d.ts
vendored
|
@ -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;
|
||||
}
|
|
@ -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} />
|
|
@ -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>
|
|
@ -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`);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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`),
|
||||
});
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 }>;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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',
|
||||
];
|
|
@ -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) + '"';
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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('/');
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import type { AstroConfig } from 'astro';
|
||||
|
||||
export function isServerLikeOutput(config: AstroConfig) {
|
||||
return config.output === 'server' || config.output === 'hybrid';
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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`)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export default Buffer.from("AGFzbQEAAAABDAJgAn9/AGADf39/AAMGBQAAAAABBQMBABAGEQJ/AEGAgMAAC38AQYCAwAALBy4EBm1lbW9yeQIABnJvdGF0ZQAECl9fZGF0YV9lbmQDAAtfX2hlYXBfYmFzZQMBCpsJBUkBAX8gACABbCIAQf////8DcSICBEBBCCEBIABBAnRBCGohAANAIAAgASgCADYCACABQQRqIQEgAEEEaiEAIAJBf2oiAg0ACwsLzQMBFH8gAUECdCERIAAgAWwiDEECdEEEaiESA0ACQAJAAkACQCAEQQFxRQRAIAMgAU8NAiADQQFqIQgMAQsgA0EPaiICIANJIggNASACIAFJIgVFDQEgASADQRBqIAgbIAEgBRshCCACIQMLIAEgA0EQaiICIAIgAUsbIQ0gA0F/cyETIBIgA0ECdGshFEEAIQVBACEOA0ACQAJAIA5FBEAgBSAASQ0BQQEhBAwGCyAAIAVBEGogBUEPaiICIAVJIgcbIAAgAiAASRshBUEBIQQgByACIABPcg0FDAELIAUiAkEBaiEFC0EBIQ4gAyANTw0AIAAgAkEQaiIPIAAgD0kbQQJ0IAJBAnRrIRUgEyABIAJsaiEHIBQgASACQQFqbEECdGohCSADIQoDQCAAIApsIgYgAmoiBEEQaiAAIAZqIA8gAEkbIgYgBEkgDCAGSXINAyAEIAZHBEAgBEECdEEIaiELIBUhBiAHIRAgCSEEA0AgDCABIBBqIhBNDQUgBCALKAIANgIAIAQgEWohBCALQQRqIQsgBkF8aiIGDQALCyAHQX9qIQcgCUF8aiEJIA0gCkEBaiIKRw0ACwwACwALDwsACyAIIQMMAAsAC1MBAX8CQCAAIAFsQQJ0IgJBCGoiAEEIRg0AIAAgAmpBfGohAEEAIQEDQCABIAJGDQEgACABQQhqKAIANgIAIABBfGohACACIAFBBGoiAUcNAAsLC9oDARN/IABBf2ohEEEAIAFBAnRrIREgACABbCIMQQJ0QQhqIRIDQAJAAkACQAJAIARBAXFFBEAgAyABTw0CIANBAWohCQwBCyADQQ9qIgIgA0kiCQ0BIAIgAUkiBUUNASABIANBEGogCRsgASAFGyEJIAIhAwsgASADQRBqIgIgAiABSxshDSASIANBAnRqIRNBACEFQQAhBgNAAkACQCAGQQFxRQRAIAUgAEkNAUEBIQQMBgsgACAFQRBqIAVBD2oiAiAFSSIIGyAAIAIgAEkbIQVBASEEIAggAiAAT3INBQwBCyAFIgJBAWohBQtBASEGIAMgDU8NACAAIAJBEGoiDiAAIA5JG0ECdCACQQJ0ayEUIAMgASAAIAJrbGohCCATIAEgECACa2xBAnRqIQogAyELA0AgACALbCIHIAJqIgRBEGogACAHaiAOIABJGyIHIARJIAwgB0lyDQMgBCAHRwRAIARBAnRBCGohBiAUIQcgCCEPIAohBANAIAwgDyABayIPTQ0FIAQgBigCADYCACAEIBFqIQQgBkEEaiEGIAdBfGoiBw0ACwtBASEGIAhBAWohCCAKQQRqIQogDSALQQFqIgtHDQALDAALAAsPCwALIAkhAwwACwALUAACQAJAAkACQCACQbMBTARAIAJFDQIgAkHaAEcNASAAIAEQAQ8LIAJBtAFGDQIgAkGOAkYNAwsACyAAIAEQAA8LIAAgARACDwsgACABEAMLAE0JcHJvZHVjZXJzAghsYW5ndWFnZQEEUnVzdAAMcHJvY2Vzc2VkLWJ5AQVydXN0Yx0xLjQ3LjAgKDE4YmY2YjRmMCAyMDIwLTEwLTA3KQ==", 'base64');
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
}
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@test/image-assets-prefix",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@astrojs/image": "workspace:*",
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.4 MiB |
|
@ -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>
|
|
@ -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' })]
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.2 KiB |
|
@ -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 = () => {};
|
Before Width: | Height: | Size: 7.6 KiB |
|
@ -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>
|
|
@ -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' })]
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 270 KiB |
|
@ -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 = () => {};
|
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 270 KiB |
|
@ -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 |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 1.4 MiB |
|
@ -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>
|
|
@ -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' })]
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 270 KiB |