@zubyjs/image 1.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/handler.d.ts +8 -0
- package/handler.js +64 -0
- package/imageLoader.d.ts +12 -0
- package/imageLoader.js +52 -0
- package/index.d.ts +23 -0
- package/index.js +69 -0
- package/optimizeImage.d.ts +2 -0
- package/optimizeImage.js +47 -0
- package/options/optionsToPath.d.ts +7 -0
- package/options/optionsToPath.js +9 -0
- package/options/pathToOptions.d.ts +8 -0
- package/options/pathToOptions.js +26 -0
- package/package.json +47 -0
- package/types.d.ts +29 -0
- package/types.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @zubyjs/image
|
|
2
|
+
|
|
3
|
+
The plugin for Zuby.js that adds image optimization support to your project
|
|
4
|
+
on top of the existing `<Image>` component.
|
|
5
|
+
|
|
6
|
+
It supports both build-time and runtime optimizations
|
|
7
|
+
and therefore works seamlessly with both static and server output modes.
|
|
8
|
+
|
|
9
|
+
By default, it performs build-time optimizations for images
|
|
10
|
+
on pre-rendered pages and runtime optimizations for images on server
|
|
11
|
+
rendered pages.
|
|
12
|
+
|
|
13
|
+
This plugin uses [sharp](https://www.npmjs.com/package/sharp) binaries under the hood.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
First, install the `@zubyjs/image` package using your favorite package manager.
|
|
18
|
+
If you aren't sure, you can use npm:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @zubyjs/image
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then add the `@zubyjs/image` plugin to your `zuby.config.mjs` file under the `plugins` option:
|
|
25
|
+
|
|
26
|
+
```diff lang="js" title="zuby.config.mjs"
|
|
27
|
+
import { defineConfig } from 'zuby';
|
|
28
|
+
import preact from '@zubyjs/preact';
|
|
29
|
+
+ import image from '@zubyjs/image';
|
|
30
|
+
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
outDir: '.zuby',
|
|
33
|
+
jsx: preact(),
|
|
34
|
+
+ plugins: [
|
|
35
|
+
+ image()
|
|
36
|
+
+ ]
|
|
37
|
+
^^^^^^^^
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
And that's it!
|
|
42
|
+
NOTE: Always make sure that all zuby packages are in sync in your `package.json` file:
|
|
43
|
+
|
|
44
|
+
```diff lang="json"
|
|
45
|
+
{
|
|
46
|
+
"name": "my-zuby-app",
|
|
47
|
+
"version": "1.0.0",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"zuby": "latest",
|
|
50
|
+
"@zubyjs/image": "latest"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Image Component
|
|
58
|
+
|
|
59
|
+
Now you can use the `<Image>` component in your Zuby.js project
|
|
60
|
+
as you're used to. Example:
|
|
61
|
+
|
|
62
|
+
```diff lang="jsx" title="pages/index.jsx"
|
|
63
|
+
+ import Image from '@zubyjs/preact/image';
|
|
64
|
+
|
|
65
|
+
export default function Home() {
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
- <img src="/path/to/image.jpg" alt="My image" />
|
|
69
|
+
+ <Image src="/path/to/image.jpg" alt="My image" />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
NOTE: Upper example is showing usage with `@zubyjs/preact` JsxProvider. If you are using different one,
|
|
76
|
+
please replace `@zubyjs/preact/image` with the correct package name.
|
|
77
|
+
|
|
78
|
+
### Image Component Props
|
|
79
|
+
|
|
80
|
+
Image component accepts following props:
|
|
81
|
+
|
|
82
|
+
- `src` - path to the image
|
|
83
|
+
- `alt` - alt text for the image
|
|
84
|
+
- `width` - width of the image
|
|
85
|
+
- `height` - height of the image
|
|
86
|
+
- `quality` - quality of the image (default: 80)
|
|
87
|
+
- `format` - format of the image (default: webp)
|
|
88
|
+
|
|
89
|
+
If no quality or format is provided,
|
|
90
|
+
the default quality from `zuby.config.mjs` will be used.
|
|
91
|
+
This can be changed globally by setting `image.defaultFormat` and `image.defaultQuality` in `zuby.config.mjs`.
|
|
92
|
+
|
|
93
|
+
If no `width` and `height` are provided,
|
|
94
|
+
the image will be rendered in its original size with max resolution 1920px.
|
|
95
|
+
|
|
96
|
+
While resizing the image, the original aspect ratio is always preserved.
|
|
97
|
+
The `Image` will try to load the src image with the nearest size to the provided `width` and `height`
|
|
98
|
+
and then resize it to the exact size using CSS.
|
|
99
|
+
|
|
100
|
+
## Runtime Image Optimization
|
|
101
|
+
|
|
102
|
+
For runtime image optimization,
|
|
103
|
+
the plugin will add new endpoint to your server at `/_image`.
|
|
104
|
+
This endpoint accepts input in the following format:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
/_image/${quality}/${width}/${height}/${src}.${format}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The format is used to find the optimized images in the `client` directory on filesystem
|
|
111
|
+
and allows you to easily serve already optimized images from the CDN or custom web server.
|
|
112
|
+
|
|
113
|
+
For example to optimize image on path `/images/dog.jpg`
|
|
114
|
+
to 80% quality, 300px width, 200px height and output format webp
|
|
115
|
+
you can use the following URL:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
/_image/80/300/200/path/to/image.jpg.webp
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To omit any of the parameters and use the default values
|
|
122
|
+
or the original image values, you can use `auto` as the value.
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
/_image/auto/300/200/path/to/image.jpg.webp
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If the output format is same as the input format,
|
|
130
|
+
you can leave the extension unchanged.
|
|
131
|
+
Example:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
/_image/auto/300/200/path/to/image.jpg
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Build-time Image Optimization
|
|
138
|
+
|
|
139
|
+
During the build, the plugin will optimize all images on pre-rendered pages
|
|
140
|
+
and put them into the `client/_image` directory.
|
|
141
|
+
Those images won't be optimized again on runtime.
|
|
142
|
+
|
|
143
|
+
If you're satisfied with the build-time optimization results,
|
|
144
|
+
you can copy the `client/_image` directory to your public directory
|
|
145
|
+
to skip the build-time optimization for those images in the future.
|
|
146
|
+
|
|
147
|
+
The build-time optimization is very useful for static output mode
|
|
148
|
+
and can save a lot of time and resources on the server.
|
|
149
|
+
On the other hand, it can significantly increase your build time.
|
|
150
|
+
|
|
151
|
+
If you want to disable the build-time optimization,
|
|
152
|
+
you can set `buildTime` plugin option to `false`
|
|
153
|
+
in your `zuby.config.mjs` or set maximum number of images to optimize
|
|
154
|
+
using `maxBuildTime` option. The default value is 1000 images
|
|
155
|
+
and the rest is optimized on-demand on runtime.
|
|
156
|
+
|
|
157
|
+
```diff lang="js" title="zuby.config.mjs"
|
|
158
|
+
import { defineConfig } from 'zuby';
|
|
159
|
+
import preact from '@zubyjs/preact';
|
|
160
|
+
+ import image from '@zubyjs/image';
|
|
161
|
+
|
|
162
|
+
export default defineConfig({
|
|
163
|
+
outDir: '.zuby',
|
|
164
|
+
jsx: preact(),
|
|
165
|
+
plugins: [
|
|
166
|
+
image({
|
|
167
|
+
+ maxBuildTime: 100,
|
|
168
|
+
+ buildTime: true
|
|
169
|
+
^^^^^^^^
|
|
170
|
+
})
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
This package is part of Zuby.js workspace and maintained by the team behind the [Zuby package](https://www.npmjs.com/package/zuby).
|
|
178
|
+
Please refer to it for more details how to contribute.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/handler.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HandlerContext } from 'zuby/contexts/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* The image handler that optimizes images
|
|
4
|
+
* on /_image/:path* endpoint.
|
|
5
|
+
* @param context
|
|
6
|
+
* @param _next
|
|
7
|
+
*/
|
|
8
|
+
export default function Handler(context: HandlerContext, _next: () => void): Promise<Response>;
|
package/handler.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import optimizeImage from './optimizeImage.js';
|
|
2
|
+
import { HTTP_HEADERS } from 'zuby/constants.js';
|
|
3
|
+
import { IMAGE_FORMATS_ARRAY } from 'zuby/types.js';
|
|
4
|
+
import { pathToOptions } from './options/pathToOptions.js';
|
|
5
|
+
/**
|
|
6
|
+
* The image handler that optimizes images
|
|
7
|
+
* on /_image/:path* endpoint.
|
|
8
|
+
* @param context
|
|
9
|
+
* @param _next
|
|
10
|
+
*/
|
|
11
|
+
export default async function Handler(context, _next) {
|
|
12
|
+
try {
|
|
13
|
+
const { src, format, quality, width, height } = pathToOptions(context.url.pathname);
|
|
14
|
+
if (!src) {
|
|
15
|
+
return new Response(`ERROR: Missing 'src' attribute.\r\nThe format is '/_image/{quality}/{width}/{height}/{src}'`, { status: 400 });
|
|
16
|
+
}
|
|
17
|
+
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
18
|
+
return new Response(`ERROR: The 'src' attribute needs to be relative`, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
if ((format && !IMAGE_FORMATS_ARRAY.includes(format)) ||
|
|
21
|
+
(src && !IMAGE_FORMATS_ARRAY.some(ext => src.endsWith(ext)))) {
|
|
22
|
+
return new Response(`ERROR: Unsupported 'format' or 'src' file extension. Supported formats are: ${IMAGE_FORMATS_ARRAY.join(', ')}`, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
const origin = context.url.origin.replace('localhost', '127.0.0.1');
|
|
25
|
+
const imageUrl = new URL(src, origin);
|
|
26
|
+
if (imageUrl.pathname.startsWith('/_image/')) {
|
|
27
|
+
return new Response(`ERROR: Cannot fetch self URL '/_image/:path*' to avoid loop.`, {
|
|
28
|
+
status: 400,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const imageRes = await fetch(imageUrl);
|
|
32
|
+
if (!imageRes.ok) {
|
|
33
|
+
return new Response(`ERROR: Failed to fetch image from the provided 'src' attribute.`, {
|
|
34
|
+
status: 400,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!imageRes.headers
|
|
38
|
+
.get(HTTP_HEADERS.ContentType)
|
|
39
|
+
?.toString()
|
|
40
|
+
.match(/image\//)) {
|
|
41
|
+
return new Response(`ERROR: Unsupported content-type returned while fetching the image.`, {
|
|
42
|
+
status: 400,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const input = Buffer.from(await imageRes.arrayBuffer());
|
|
46
|
+
const { output, outputFormat } = await optimizeImage({
|
|
47
|
+
input,
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
format: format,
|
|
51
|
+
quality,
|
|
52
|
+
});
|
|
53
|
+
return new Response(output, {
|
|
54
|
+
headers: {
|
|
55
|
+
[HTTP_HEADERS.ContentType]: `image/${outputFormat}`,
|
|
56
|
+
[HTTP_HEADERS.CacheControl]: 'public, max-age=31536000, immutable',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
console.error(e);
|
|
62
|
+
return new Response('ERROR: An error occurred while optimizing your image.', { status: 500 });
|
|
63
|
+
}
|
|
64
|
+
}
|
package/imageLoader.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ImageLoaderOptions } from 'zuby/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The ImageLoader for @zubyjs/image
|
|
4
|
+
* that is used to generate image URLs
|
|
5
|
+
*/
|
|
6
|
+
export default function imageLoader({ context, src, isAbsolute, format, quality, width, height, }: ImageLoaderOptions): string;
|
|
7
|
+
/**
|
|
8
|
+
* Returns shared global image URLs set
|
|
9
|
+
* available across the whole project
|
|
10
|
+
* on both client and server side
|
|
11
|
+
*/
|
|
12
|
+
export declare function getImageUrls(): Set<string>;
|
package/imageLoader.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { optionsToPath } from './options/optionsToPath.js';
|
|
2
|
+
let warningShown = false;
|
|
3
|
+
/**
|
|
4
|
+
* The ImageLoader for @zubyjs/image
|
|
5
|
+
* that is used to generate image URLs
|
|
6
|
+
*/
|
|
7
|
+
export default function imageLoader({ context, src, isAbsolute, format, quality, width = 1920, height, }) {
|
|
8
|
+
const { buildId } = context;
|
|
9
|
+
if (isAbsolute) {
|
|
10
|
+
// Just add buildId to the image URL
|
|
11
|
+
if (src.includes('?'))
|
|
12
|
+
return `${src}&${buildId}`;
|
|
13
|
+
return `${src}?${buildId}`;
|
|
14
|
+
}
|
|
15
|
+
if (!width && !height && !warningShown) {
|
|
16
|
+
console.warn('WARNING: You should always provide at least width or height to the Image component to avoid layout shift.');
|
|
17
|
+
warningShown = true;
|
|
18
|
+
}
|
|
19
|
+
// Normalize the src,
|
|
20
|
+
// remove any query params
|
|
21
|
+
src = `/${src}`.replace(/\/+/g, '/').replace(/\?(.*)$/, '');
|
|
22
|
+
// If output format is different from the input format,
|
|
23
|
+
// add additional extension to the src
|
|
24
|
+
if (format && !src.endsWith(`.${format}`)) {
|
|
25
|
+
src = src.replace(/\.(.+)$/, `.$1.${format}`);
|
|
26
|
+
}
|
|
27
|
+
// Generate the image URL pointing to the image handler
|
|
28
|
+
const url = optionsToPath({
|
|
29
|
+
src,
|
|
30
|
+
quality,
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
format,
|
|
34
|
+
buildId,
|
|
35
|
+
});
|
|
36
|
+
// Collect all generated image URLs,
|
|
37
|
+
// so we can optimize them later during build
|
|
38
|
+
const imageUrls = getImageUrls();
|
|
39
|
+
imageUrls.add(url);
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns shared global image URLs set
|
|
44
|
+
* available across the whole project
|
|
45
|
+
* on both client and server side
|
|
46
|
+
*/
|
|
47
|
+
export function getImageUrls() {
|
|
48
|
+
if (!globalThis._zubyImageUrls) {
|
|
49
|
+
globalThis._zubyImageUrls = new Set();
|
|
50
|
+
}
|
|
51
|
+
return globalThis._zubyImageUrls;
|
|
52
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ZubyPlugin } from 'zuby/types.js';
|
|
2
|
+
export interface ImagePluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Set this option to false to disable
|
|
5
|
+
* build time image optimization.
|
|
6
|
+
* @default true
|
|
7
|
+
*/
|
|
8
|
+
buildTime?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The maximum number of images that can be optimized
|
|
11
|
+
* during the build. The rest of the images will be
|
|
12
|
+
* optimized on-demand during runtime.
|
|
13
|
+
* @default 1000
|
|
14
|
+
*/
|
|
15
|
+
maxBuildTime?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Zuby.js plugin that allows you to
|
|
19
|
+
* optimize images in your project
|
|
20
|
+
* @returns ZubyPlugin
|
|
21
|
+
*/
|
|
22
|
+
declare const _default: ({ buildTime, maxBuildTime, }?: ImagePluginOptions) => ZubyPlugin;
|
|
23
|
+
export default _default;
|
package/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import { dirname, resolve } from 'path';
|
|
3
|
+
import { normalizePath } from 'zuby/utils/pathUtils.js';
|
|
4
|
+
import { getImageUrls } from './imageLoader.js';
|
|
5
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
6
|
+
import optimizeImage from './optimizeImage.js';
|
|
7
|
+
import { pathToOptions } from './options/pathToOptions.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { performance } from 'node:perf_hooks';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
/**
|
|
13
|
+
* Zuby.js plugin that allows you to
|
|
14
|
+
* optimize images in your project
|
|
15
|
+
* @returns ZubyPlugin
|
|
16
|
+
*/
|
|
17
|
+
export default ({ buildTime = true, maxBuildTime = 1000, } = {}) => ({
|
|
18
|
+
name: 'zuby-image-plugin',
|
|
19
|
+
description: 'optimizing images...',
|
|
20
|
+
buildStep: true,
|
|
21
|
+
hooks: {
|
|
22
|
+
'zuby:config:setup': ({ config, addHandler }) => {
|
|
23
|
+
const { customLogger: logger } = config;
|
|
24
|
+
config.image = config.image || {};
|
|
25
|
+
if (config.image.loader) {
|
|
26
|
+
logger?.warn(`WARNING: The image.loader config in 'zuby.config.js' is already set.`);
|
|
27
|
+
logger?.warn(`Zuby.js allows only one image loader to be set at a time.`);
|
|
28
|
+
logger?.warn(`Using image loader from '@zubyjs/image' package.`);
|
|
29
|
+
}
|
|
30
|
+
config.image.loader = normalizePath(resolve(__dirname, 'imageLoader.js'));
|
|
31
|
+
addHandler(normalizePath(resolve(__dirname, 'handler.js')), '/_image/:path*');
|
|
32
|
+
},
|
|
33
|
+
'zuby:build:done': async ({ config, logger }) => {
|
|
34
|
+
const { outDir } = config;
|
|
35
|
+
let optimizedImages = 0;
|
|
36
|
+
const startTime = performance.now();
|
|
37
|
+
const imageUrls = getImageUrls();
|
|
38
|
+
if (!buildTime || imageUrls.size === 0) {
|
|
39
|
+
logger?.info(`Skipping build time image optimization...`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const urlString of imageUrls) {
|
|
43
|
+
if (optimizedImages >= maxBuildTime) {
|
|
44
|
+
logger?.warn(`The maximum number of images that can be optimized during the build has been reached.`);
|
|
45
|
+
logger?.warn(`The rest of the images will be optimized on-demand during runtime.`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const url = new URL(urlString, 'http://127.0.0.1');
|
|
49
|
+
const imageOptions = pathToOptions(url.pathname);
|
|
50
|
+
const inputFile = normalizePath(resolve(outDir, 'client', imageOptions.src));
|
|
51
|
+
const outputFile = normalizePath(resolve(outDir, 'client', url.pathname.replace(/^\//, '')));
|
|
52
|
+
const outputDir = dirname(outputFile);
|
|
53
|
+
if (existsSync(outputFile))
|
|
54
|
+
continue;
|
|
55
|
+
if (!existsSync(outputDir))
|
|
56
|
+
mkdirSync(outputDir, { recursive: true });
|
|
57
|
+
logger?.info(`Optimizing: ${chalk.blue(`${imageOptions.src} => ${url.pathname}`)}`);
|
|
58
|
+
await optimizeImage({
|
|
59
|
+
...imageOptions,
|
|
60
|
+
inputFile,
|
|
61
|
+
outputFile,
|
|
62
|
+
});
|
|
63
|
+
optimizedImages++;
|
|
64
|
+
}
|
|
65
|
+
const finishTime = performance.now();
|
|
66
|
+
logger?.info(chalk.green(`✓ optimized ${optimizedImages} images in ${Math.ceil(finishTime - startTime)}ms`));
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
package/optimizeImage.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export default async function optimizeImage({ input, inputFile, outputFile, quality = 75, width, height, format, }) {
|
|
3
|
+
// Read the image from buffer or filesystem
|
|
4
|
+
const image = sharp(input || inputFile);
|
|
5
|
+
const { format: originalFormat = 'webp', width: originalWidth = 0, height: originalHeight = 0, size: inputSize, } = await image.metadata();
|
|
6
|
+
const widthRatio = originalWidth / originalHeight;
|
|
7
|
+
const heightRatio = originalHeight / originalWidth;
|
|
8
|
+
format = format || originalFormat;
|
|
9
|
+
width = width || (height || originalHeight) * widthRatio;
|
|
10
|
+
height = height || (width || originalWidth) * heightRatio;
|
|
11
|
+
width = Math.round(width);
|
|
12
|
+
height = Math.round(height);
|
|
13
|
+
if (height !== originalHeight || width !== originalWidth) {
|
|
14
|
+
image.resize(width, height);
|
|
15
|
+
}
|
|
16
|
+
if (format === 'webp') {
|
|
17
|
+
image.webp({
|
|
18
|
+
quality,
|
|
19
|
+
alphaQuality: quality,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else if (format || quality) {
|
|
23
|
+
image.toFormat(format, {
|
|
24
|
+
progressive: true,
|
|
25
|
+
force: true,
|
|
26
|
+
quality,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
let outputFileSize = undefined;
|
|
30
|
+
// Save the image to filesystem
|
|
31
|
+
if (outputFile) {
|
|
32
|
+
const outputInfo = await image.toFile(outputFile);
|
|
33
|
+
outputFileSize = outputInfo.size;
|
|
34
|
+
}
|
|
35
|
+
// Return the image as buffer if no output file is set
|
|
36
|
+
const output = outputFile ? undefined : await image.toBuffer();
|
|
37
|
+
return {
|
|
38
|
+
output,
|
|
39
|
+
outputFile,
|
|
40
|
+
outputFormat: format,
|
|
41
|
+
outputWidth: width,
|
|
42
|
+
outputHeight: height,
|
|
43
|
+
outputQuality: quality,
|
|
44
|
+
outputSize: outputFileSize || output?.length,
|
|
45
|
+
inputSize,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts image options object to a path
|
|
3
|
+
* that is later parsed by the handler.
|
|
4
|
+
* @param options
|
|
5
|
+
*/
|
|
6
|
+
export function optionsToPath(options) {
|
|
7
|
+
const { src, quality, width, height, buildId } = options;
|
|
8
|
+
return `/_image/${quality || 'auto'}/${width || 'auto'}/${height || 'auto'}${src}?${buildId}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ImageOptions } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses the path config options
|
|
4
|
+
* to an image options object.
|
|
5
|
+
* @param path
|
|
6
|
+
*/
|
|
7
|
+
export declare function pathToOptions(path: string): ImageOptions;
|
|
8
|
+
export declare function fromAuto(value: string | undefined): string | undefined;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the path config options
|
|
3
|
+
* to an image options object.
|
|
4
|
+
* @param path
|
|
5
|
+
*/
|
|
6
|
+
export function pathToOptions(path) {
|
|
7
|
+
const [_empty, _prefix, quality, width, height, ...srcSegments] = path.split('/') || [];
|
|
8
|
+
const originalSrc = srcSegments.join('/');
|
|
9
|
+
const src = originalSrc
|
|
10
|
+
.replace(/\/+/g, '/')
|
|
11
|
+
// Remove leading slash
|
|
12
|
+
.replace(/^\//g, '')
|
|
13
|
+
// Remove double extension
|
|
14
|
+
.replace(/\.(.+)\.(.+)$/, '.$1');
|
|
15
|
+
const format = originalSrc.split('.').pop();
|
|
16
|
+
return {
|
|
17
|
+
src,
|
|
18
|
+
format: format,
|
|
19
|
+
quality: Number(fromAuto(quality)),
|
|
20
|
+
width: Number(fromAuto(width)),
|
|
21
|
+
height: Number(fromAuto(height)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function fromAuto(value) {
|
|
25
|
+
return value === 'auto' ? undefined : value;
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zubyjs/image",
|
|
3
|
+
"version": "1.0.65",
|
|
4
|
+
"description": "Zuby.js image plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"release": "cd ./dist && npm publish --access public && cd ..",
|
|
9
|
+
"bump-version": "npm version patch",
|
|
10
|
+
"build": "rm -rf dist/ stage/ && mkdir dist && tsc && cp -rf package.json README.md stage/image/src/* dist/ && rm -rf stage/",
|
|
11
|
+
"push-build": "npm run build && cd dist && yalc push --force && cd ..",
|
|
12
|
+
"test": "exit 0"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"directory": "dist",
|
|
16
|
+
"linkDirectory": true
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"zuby": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"sharp": "^0.32.6"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://gitlab.com/futrou/zuby.js/-/issues",
|
|
27
|
+
"email": "zuby@futrou.com"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://gitlab.com/futrou/zuby.js.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://zubyjs.com",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"zuby-plugin",
|
|
37
|
+
"zuby",
|
|
38
|
+
"image",
|
|
39
|
+
"optimization"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/sharp": "^0.32.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { ImageFormat } from 'zuby/types.js';
|
|
3
|
+
export interface ImageOptions {
|
|
4
|
+
src: string;
|
|
5
|
+
quality?: number;
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
format?: ImageFormat;
|
|
9
|
+
buildId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface OptimizeImageOptions {
|
|
12
|
+
input?: Buffer;
|
|
13
|
+
inputFile?: string;
|
|
14
|
+
outputFile?: string;
|
|
15
|
+
quality?: number;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
format?: ImageFormat;
|
|
19
|
+
}
|
|
20
|
+
export interface OptimizeImageOutput {
|
|
21
|
+
output?: Buffer;
|
|
22
|
+
outputFile?: string;
|
|
23
|
+
outputFormat: ImageFormat;
|
|
24
|
+
outputWidth: number;
|
|
25
|
+
outputHeight: number;
|
|
26
|
+
outputQuality: number;
|
|
27
|
+
outputSize?: number;
|
|
28
|
+
inputSize?: number;
|
|
29
|
+
}
|
package/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|