@travetto/image 5.0.12 → 5.0.13
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 +3 -10
- package/package.json +1 -2
- package/src/util.ts +32 -82
package/README.md
CHANGED
|
@@ -13,17 +13,10 @@ npm install @travetto/image
|
|
|
13
13
|
yarn add @travetto/image
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides functionality for image resizing, and
|
|
16
|
+
This module provides functionality for image resizing, and image optimization. This is primarily meant to be used in conjunction with other modules, like the [Email Compilation Support](https://github.com/travetto/travetto/tree/main/module/email-compiler#readme "Email compiling module") module. It can also be invoked directly as needed (as it can be very handy for batch processing images on the command line).
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Out-of-process operations using [ImageMagick](https://imagemagick.org/index.php),[pngquant](https://pngquant.org/) and [Jpegoptim](https://github.com/tjko/jpegoptim).
|
|
21
|
-
|
|
22
|
-
## In-Process
|
|
23
|
-
The in process operations leverage [sharp](https://sharp.pixelplumbing.com/) and will perform within expectations, and will execute substantially faster than invoking a subprocess. The primary caveats here being that [Jpegoptim](https://github.com/tjko/jpegoptim) and [pngquant](https://pngquant.org/) are better geared for image optimization. Additionally, by running these processes in-memory, there will be shared contention within the process.
|
|
24
|
-
|
|
25
|
-
## Out-of-Process
|
|
26
|
-
The out-of-process executions will leverage external tools ([ImageMagick](https://imagemagick.org/index.php),[pngquant](https://pngquant.org/) and [Jpegoptim](https://github.com/tjko/jpegoptim)) via [CommandOperation](https://github.com/travetto/travetto/tree/main/module/command/src/command.ts#L12)s. These tools are tried and tested, but come with the overhead of invoking a separate process to operate. The benefit here is externalized memory usage, and a more robust optimization flow.
|
|
18
|
+
## Sharp
|
|
19
|
+
The in process operations leverage [sharp](https://sharp.pixelplumbing.com/) and will perform within expectations, and will execute substantially faster than invoking a subprocess.
|
|
27
20
|
|
|
28
21
|
**Code: Simple Image Resize**
|
|
29
22
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/image",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.13",
|
|
4
4
|
"description": "Image support, resizing, and optimization",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"images",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"directory": "module/image"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@travetto/command": "^5.0.11",
|
|
27
26
|
"@travetto/runtime": "^5.0.11",
|
|
28
27
|
"sharp": "^0.33.5"
|
|
29
28
|
},
|
package/src/util.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { pipeline } from 'node:stream/promises';
|
|
4
|
-
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
5
|
-
import { ChildProcess } from 'node:child_process';
|
|
6
4
|
|
|
7
5
|
import type { Sharp } from 'sharp';
|
|
8
6
|
|
|
9
|
-
import { CommandOperation } from '@travetto/command';
|
|
10
7
|
import { castTo } from '@travetto/runtime';
|
|
11
8
|
|
|
9
|
+
type ImageFormat = 'jpeg' | 'png' | 'avif' | 'webp';
|
|
10
|
+
|
|
12
11
|
/**
|
|
13
12
|
* Image resize options
|
|
14
13
|
*/
|
|
@@ -26,20 +25,16 @@ export interface ResizeOptions {
|
|
|
26
25
|
*/
|
|
27
26
|
optimize?: boolean;
|
|
28
27
|
/**
|
|
29
|
-
*
|
|
28
|
+
* Output image format, defaults to input format
|
|
30
29
|
*/
|
|
31
|
-
|
|
30
|
+
format?: ImageFormat;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
34
|
* Image optimize options
|
|
36
35
|
*/
|
|
37
36
|
export interface OptimizeOptions {
|
|
38
|
-
format?:
|
|
39
|
-
/**
|
|
40
|
-
* Sub process, allows for externalization of memory
|
|
41
|
-
*/
|
|
42
|
-
asSubprocess?: boolean;
|
|
37
|
+
format?: ImageFormat;
|
|
43
38
|
}
|
|
44
39
|
|
|
45
40
|
|
|
@@ -50,49 +45,12 @@ type ImageType = Readable | Buffer | ReadableStream;
|
|
|
50
45
|
*/
|
|
51
46
|
export class ImageUtil {
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Compressor
|
|
63
|
-
*/
|
|
64
|
-
static PNG_COMPRESSOR = new CommandOperation({
|
|
65
|
-
containerImage: 'agregad/pngquant:latest',
|
|
66
|
-
localCheck: ['pngquant', ['-h']]
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Compressor
|
|
71
|
-
*/
|
|
72
|
-
static JPEG_COMPRESSOR = new CommandOperation({
|
|
73
|
-
containerImage: 'shomatan/jpegoptim:1.4.4',
|
|
74
|
-
localCheck: ['jpegoptim', ['-h']]
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
static async #subprocessReturn<T extends ImageType>(proc: ChildProcess, input: T): Promise<T> {
|
|
78
|
-
if (Buffer.isBuffer(input)) {
|
|
79
|
-
const [, buffer] = await Promise.all([
|
|
80
|
-
pipeline(Readable.from(input), proc.stdin!),
|
|
81
|
-
toBuffer(proc.stdout!)
|
|
82
|
-
]);
|
|
83
|
-
return castTo(buffer);
|
|
84
|
-
} else {
|
|
85
|
-
pipeline(castTo<Readable>(input), proc.stdin!);
|
|
86
|
-
return castTo('pipeThrough' in input ? ReadableStream.from(proc.stdout!) : proc.stdout);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
static async #sharpReturn<T extends ImageType>(output: Sharp, input: T, optimize?: boolean, format?: 'jpeg' | 'png'): Promise<T> {
|
|
91
|
-
if (optimize) {
|
|
92
|
-
output = output
|
|
93
|
-
.jpeg({ quality: 80, progressive: true, force: format === 'jpeg' })
|
|
94
|
-
.png({ compressionLevel: 9, quality: 80, adaptiveFiltering: true, force: format === 'png' });
|
|
95
|
-
}
|
|
48
|
+
static async #sharpReturn<T extends ImageType>(output: Sharp, input: T, optimize?: boolean, format?: ImageFormat): Promise<T> {
|
|
49
|
+
output = output
|
|
50
|
+
.jpeg({ ...(optimize ? { quality: 80, progressive: true } : {}), force: format === 'jpeg' })
|
|
51
|
+
.png({ ...(optimize ? { compressionLevel: 9, quality: 80, adaptiveFiltering: true } : {}), force: format === 'png' })
|
|
52
|
+
.avif({ ...(optimize ? { quality: 70 } : {}), force: format === 'avif' })
|
|
53
|
+
.webp({ ...(optimize ? { quality: 80 } : {}), force: format === 'webp' });
|
|
96
54
|
const stream = Buffer.isBuffer(input) ? Readable.from(input) : input;
|
|
97
55
|
pipeline(stream, output);
|
|
98
56
|
return castTo('pipeThrough' in input ? ReadableStream.from(output) : Buffer.isBuffer(input) ? output.toBuffer() : output);
|
|
@@ -105,42 +63,26 @@ export class ImageUtil {
|
|
|
105
63
|
const dims = [options.w, options.h].map(x => x ? Math.trunc(x) : undefined);
|
|
106
64
|
const fluid = dims.some(x => !x);
|
|
107
65
|
|
|
108
|
-
|
|
109
|
-
return this.#subprocessReturn(
|
|
110
|
-
await this.CONVERTER.exec('gm', 'convert', '-resize', dims.map(x => x || '').join('x'), '-auto-orient',
|
|
111
|
-
...(options.optimize ? ['-strip', '-quality', '86'] : []), '-', '-'),
|
|
112
|
-
image);
|
|
113
|
-
} else {
|
|
114
|
-
const { default: sharp } = await import('sharp');
|
|
66
|
+
const { default: sharp } = await import('sharp');
|
|
115
67
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
68
|
+
return this.#sharpReturn(
|
|
69
|
+
sharp().resize({
|
|
70
|
+
width: dims[0],
|
|
71
|
+
height: dims[1],
|
|
72
|
+
fit: fluid ? 'inside' : 'fill'
|
|
73
|
+
}),
|
|
74
|
+
image,
|
|
75
|
+
options.optimize,
|
|
76
|
+
options.format
|
|
77
|
+
);
|
|
126
78
|
}
|
|
127
79
|
|
|
128
80
|
/**
|
|
129
81
|
* Optimize an image
|
|
130
82
|
*/
|
|
131
83
|
static async optimize<T extends ImageType>(image: T, options: OptimizeOptions = {}): Promise<T> {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
case 'png': return this.#subprocessReturn(
|
|
135
|
-
await this.PNG_COMPRESSOR.exec('pngquant', '--quality', '40-80', '--speed', '1', '--force', '-'), image);
|
|
136
|
-
default:
|
|
137
|
-
case 'jpeg': return this.#subprocessReturn(
|
|
138
|
-
await this.JPEG_COMPRESSOR.exec('jpegoptim', '-m70', '-s', '--stdin', '--stdout', '-'), image);
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
const { default: sharp } = await import('sharp');
|
|
142
|
-
return this.#sharpReturn(sharp(), image, true, options.format);
|
|
143
|
-
}
|
|
84
|
+
const { default: sharp } = await import('sharp');
|
|
85
|
+
return this.#sharpReturn(sharp(), image, true, options.format);
|
|
144
86
|
}
|
|
145
87
|
|
|
146
88
|
/**
|
|
@@ -150,4 +92,12 @@ export class ImageUtil {
|
|
|
150
92
|
const { default: sharp } = await import('sharp');
|
|
151
93
|
return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height!, aspect: v.width! / v.height! }));
|
|
152
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get image type
|
|
98
|
+
*/
|
|
99
|
+
static async getFileType(image: Buffer | string): Promise<string> {
|
|
100
|
+
const { default: sharp } = await import('sharp');
|
|
101
|
+
return sharp(image).metadata().then(v => v.format?.replace('heif', 'avif')!);
|
|
102
|
+
}
|
|
153
103
|
}
|