@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.
Files changed (3) hide show
  1. package/README.md +3 -10
  2. package/package.json +1 -2
  3. 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 png 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).
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
- The [ImageUtil](https://github.com/travetto/travetto/tree/main/module/image/src/util.ts#L51) functionality supports two operation modes:
19
- * In-process operations using [sharp](https://sharp.pixelplumbing.com/)
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.12",
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
- * Sub process, allows for externalization of memory
28
+ * Output image format, defaults to input format
30
29
  */
31
- asSubprocess?: boolean;
30
+ format?: ImageFormat;
32
31
  }
33
32
 
34
33
  /**
35
34
  * Image optimize options
36
35
  */
37
36
  export interface OptimizeOptions {
38
- format?: 'png' | 'jpeg';
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
- * Resize/conversion util
55
- */
56
- static CONVERTER = new CommandOperation({
57
- containerImage: 'jameskyburz/graphicsmagick-alpine:v1.0.0',
58
- localCheck: ['gm', ['-version']]
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
- 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');
66
+ const { default: sharp } = await import('sharp');
115
67
 
116
- return this.#sharpReturn(
117
- sharp().resize({
118
- width: dims[0],
119
- height: dims[1],
120
- fit: fluid ? 'inside' : 'fill'
121
- }),
122
- image,
123
- options.optimize,
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
- if (options.asSubprocess) {
133
- switch (options.format) {
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
  }