@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.
Files changed (3) hide show
  1. package/README.md +4 -11
  2. package/package.json +1 -2
  3. 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 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
@@ -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.resize(createReadStream(imgPath), { w: width, h: height });
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.12",
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 type { Sharp } from 'sharp';
5
+ import { AppError, castTo } from '@travetto/runtime';
8
6
 
9
- import { CommandOperation } from '@travetto/command';
10
- import { castTo } from '@travetto/runtime';
7
+ type ImageFormat = 'jpeg' | 'png' | 'avif' | 'webp';
11
8
 
12
9
  /**
13
- * Image resize options
10
+ * Image convert options
14
11
  */
15
- export interface ResizeOptions {
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
- * Sub process, allows for externalization of memory
26
+ * Output image format, defaults to input format
30
27
  */
31
- asSubprocess?: boolean;
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/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
39
+ * Resize image
63
40
  */
64
- static PNG_COMPRESSOR = new CommandOperation({
65
- containerImage: 'agregad/pngquant:latest',
66
- localCheck: ['pngquant', ['-h']]
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
- 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);
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
- 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' });
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
- 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
- }
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
- * Optimize an image
85
+ * Get Image Dimensions
130
86
  */
131
- 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
- }
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 Image Dimensions
93
+ * Get image type
148
94
  */
149
- static async getDimensions(image: Buffer | string): Promise<{ width: number, height: number, aspect: number }> {
95
+ static async getFileType(image: Buffer | string): Promise<ImageFormat> {
150
96
  const { default: sharp } = await import('sharp');
151
- return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height!, aspect: v.width! / v.height! }));
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
  }