@travetto/image 5.0.12 → 5.0.14
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 +4 -11
- package/package.json +1 -2
- package/src/util.ts +52 -103
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
|
|
@@ -34,7 +27,7 @@ import { ImageUtil } from '@travetto/image';
|
|
|
34
27
|
|
|
35
28
|
export class ResizeService {
|
|
36
29
|
async resizeImage(imgPath: string, width: number, height: number): Promise<string> {
|
|
37
|
-
const stream = await ImageUtil.
|
|
30
|
+
const stream = await ImageUtil.convert(createReadStream(imgPath), { w: width, h: height });
|
|
38
31
|
const out = imgPath.replace(/[.][^.]+$/, (ext) => `.resized${ext}`);
|
|
39
32
|
await pipeline(stream, createWriteStream(out));
|
|
40
33
|
return out;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/image",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.14",
|
|
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,18 +1,15 @@
|
|
|
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
|
-
import
|
|
5
|
+
import { AppError, castTo } from '@travetto/runtime';
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
import { castTo } from '@travetto/runtime';
|
|
7
|
+
type ImageFormat = 'jpeg' | 'png' | 'avif' | 'webp';
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
* Image
|
|
10
|
+
* Image convert options
|
|
14
11
|
*/
|
|
15
|
-
export interface
|
|
12
|
+
export interface ConvertOptions {
|
|
16
13
|
/**
|
|
17
14
|
* New height
|
|
18
15
|
*/
|
|
@@ -26,23 +23,11 @@ export interface ResizeOptions {
|
|
|
26
23
|
*/
|
|
27
24
|
optimize?: boolean;
|
|
28
25
|
/**
|
|
29
|
-
*
|
|
26
|
+
* Output image format, defaults to input format
|
|
30
27
|
*/
|
|
31
|
-
|
|
28
|
+
format?: ImageFormat;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
/**
|
|
35
|
-
* Image optimize options
|
|
36
|
-
*/
|
|
37
|
-
export interface OptimizeOptions {
|
|
38
|
-
format?: 'png' | 'jpeg';
|
|
39
|
-
/**
|
|
40
|
-
* Sub process, allows for externalization of memory
|
|
41
|
-
*/
|
|
42
|
-
asSubprocess?: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
31
|
type ImageType = Readable | Buffer | ReadableStream;
|
|
47
32
|
|
|
48
33
|
/**
|
|
@@ -51,103 +36,67 @@ type ImageType = Readable | Buffer | ReadableStream;
|
|
|
51
36
|
export class ImageUtil {
|
|
52
37
|
|
|
53
38
|
/**
|
|
54
|
-
* Resize
|
|
55
|
-
*/
|
|
56
|
-
static CONVERTER = new CommandOperation({
|
|
57
|
-
containerImage: 'jameskyburz/graphicsmagick-alpine:v1.0.0',
|
|
58
|
-
localCheck: ['gm', ['-version']]
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Compressor
|
|
39
|
+
* Resize image
|
|
63
40
|
*/
|
|
64
|
-
static
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
41
|
+
static async convert<T extends ImageType>(image: T, options: ConvertOptions): Promise<T> {
|
|
42
|
+
if (options.optimize && !options.format) {
|
|
43
|
+
if (Buffer.isBuffer(image)) {
|
|
44
|
+
options.format = await this.getFileType(image);
|
|
45
|
+
}
|
|
46
|
+
throw new AppError('Format is required for optimizing');
|
|
47
|
+
}
|
|
68
48
|
|
|
69
|
-
|
|
70
|
-
* Compressor
|
|
71
|
-
*/
|
|
72
|
-
static JPEG_COMPRESSOR = new CommandOperation({
|
|
73
|
-
containerImage: 'shomatan/jpegoptim:1.4.4',
|
|
74
|
-
localCheck: ['jpegoptim', ['-h']]
|
|
75
|
-
});
|
|
49
|
+
const { default: sharp } = await import('sharp');
|
|
76
50
|
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
const [,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return castTo('pipeThrough' in input ? ReadableStream.from(proc.stdout!) : proc.stdout);
|
|
51
|
+
let builder = sharp();
|
|
52
|
+
if (options.w || options.h) {
|
|
53
|
+
const dims = [options.w, options.h].map(x => x ? Math.trunc(x) : undefined);
|
|
54
|
+
const fluid = dims.some(x => !x);
|
|
55
|
+
builder = builder.resize({
|
|
56
|
+
width: dims[0],
|
|
57
|
+
height: dims[1],
|
|
58
|
+
fit: fluid ? 'inside' : 'fill'
|
|
59
|
+
});
|
|
87
60
|
}
|
|
88
|
-
}
|
|
89
61
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
62
|
+
switch (options.format) {
|
|
63
|
+
case 'jpeg':
|
|
64
|
+
builder = builder.jpeg(options.optimize ? { quality: 80, progressive: true } : {});
|
|
65
|
+
break;
|
|
66
|
+
case 'png':
|
|
67
|
+
builder = builder.png(options.optimize ? { compressionLevel: 9, quality: 80, adaptiveFiltering: true } : {});
|
|
68
|
+
break;
|
|
69
|
+
case 'avif':
|
|
70
|
+
builder = builder.avif(options.optimize ? { quality: 70 } : {});
|
|
71
|
+
break;
|
|
72
|
+
case 'webp':
|
|
73
|
+
builder = builder.webp(options.optimize ? { quality: 80 } : {});
|
|
74
|
+
break;
|
|
95
75
|
}
|
|
96
|
-
const stream = Buffer.isBuffer(input) ? Readable.from(input) : input;
|
|
97
|
-
pipeline(stream, output);
|
|
98
|
-
return castTo('pipeThrough' in input ? ReadableStream.from(output) : Buffer.isBuffer(input) ? output.toBuffer() : output);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Resize image
|
|
103
|
-
*/
|
|
104
|
-
static async resize<T extends ImageType>(image: T, options: ResizeOptions = {}): Promise<T> {
|
|
105
|
-
const dims = [options.w, options.h].map(x => x ? Math.trunc(x) : undefined);
|
|
106
|
-
const fluid = dims.some(x => !x);
|
|
107
|
-
|
|
108
|
-
if (options.asSubprocess) {
|
|
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');
|
|
115
76
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}),
|
|
122
|
-
image,
|
|
123
|
-
options.optimize,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
77
|
+
const stream = Buffer.isBuffer(image) ? Readable.from(image) : image;
|
|
78
|
+
pipeline(stream, builder);
|
|
79
|
+
return castTo('pipeThrough' in image ?
|
|
80
|
+
ReadableStream.from(builder) :
|
|
81
|
+
Buffer.isBuffer(image) ? builder.toBuffer() : builder);
|
|
126
82
|
}
|
|
127
83
|
|
|
128
84
|
/**
|
|
129
|
-
*
|
|
85
|
+
* Get Image Dimensions
|
|
130
86
|
*/
|
|
131
|
-
static async
|
|
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
|
-
}
|
|
87
|
+
static async getDimensions(image: Buffer | string): Promise<{ width: number, height: number, aspect: number }> {
|
|
88
|
+
const { default: sharp } = await import('sharp');
|
|
89
|
+
return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height!, aspect: v.width! / v.height! }));
|
|
144
90
|
}
|
|
145
91
|
|
|
146
92
|
/**
|
|
147
|
-
* Get
|
|
93
|
+
* Get image type
|
|
148
94
|
*/
|
|
149
|
-
static async
|
|
95
|
+
static async getFileType(image: Buffer | string): Promise<ImageFormat> {
|
|
150
96
|
const { default: sharp } = await import('sharp');
|
|
151
|
-
return sharp(image).metadata().then(v =>
|
|
97
|
+
return sharp(image).metadata().then(v =>
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
99
|
+
v.format?.replace('heif', 'avif')! as ImageFormat
|
|
100
|
+
);
|
|
152
101
|
}
|
|
153
102
|
}
|