@travetto/image 5.0.0-rc.10 → 5.0.0-rc.11

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 CHANGED
@@ -15,18 +15,26 @@ yarn add @travetto/image
15
15
 
16
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).
17
17
 
18
- The utility's primary structure revolves around the [CommandOperation](https://github.com/travetto/travetto/tree/main/module/command/src/command.ts#L12) from the [Command](https://github.com/travetto/travetto/tree/main/module/command#readme "Support for executing complex commands at runtime.") module. The [CommandOperation](https://github.com/travetto/travetto/tree/main/module/command/src/command.ts#L12) allows for declaration of a local executable, and a fall-back docker container (mainly meant for development). The [ImageConverter](https://github.com/travetto/travetto/tree/main/module/image/src/convert.ts#L38) utilizes [ImageMagick](https://imagemagick.org/index.php), [pngquant](https://pngquant.org/), and [Jpegoptim](https://github.com/tjko/jpegoptim) as the backing for image resizing and png compression, respectively.
18
+ The [ImageUtil](https://github.com/travetto/travetto/tree/main/module/image/src/convert.ts#L54) 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.
19
27
 
20
28
  **Code: Simple Image Resize**
21
29
  ```typescript
22
30
  import { createReadStream, createWriteStream } from 'node:fs';
23
31
  import { pipeline } from 'node:stream/promises';
24
32
 
25
- import { ImageConverter } from '@travetto/image';
33
+ import { ImageUtil } from '@travetto/image';
26
34
 
27
35
  export class ResizeService {
28
36
  async resizeImage(imgPath: string, width: number, height: number): Promise<string> {
29
- const stream = await ImageConverter.resize(createReadStream(imgPath), { w: width, h: height });
37
+ const stream = await ImageUtil.resize(createReadStream(imgPath), { w: width, h: height });
30
38
  const out = imgPath.replace(/[.][^.]+$/, (ext) => `.resized${ext}`);
31
39
  await pipeline(stream, createWriteStream(out));
32
40
  return out;
package/__index__.ts CHANGED
@@ -1,2 +1 @@
1
- /// <reference path="./src/trv.d.ts" />
2
- export * from './src/convert';
1
+ export * from './src/util';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/image",
3
- "version": "5.0.0-rc.10",
3
+ "version": "5.0.0-rc.11",
4
4
  "description": "Image support, resizing, and optimization",
5
5
  "keywords": [
6
6
  "images",
@@ -24,7 +24,8 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@travetto/command": "^5.0.0-rc.10",
27
- "@travetto/runtime": "^5.0.0-rc.10"
27
+ "@travetto/runtime": "^5.0.0-rc.10",
28
+ "sharp": "^0.33.4"
28
29
  },
29
30
  "travetto": {
30
31
  "displayName": "Image"
package/src/util.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { Readable } from 'node:stream';
2
+ import { pipeline } from 'node:stream/promises';
3
+ import { buffer as toBuffer } from 'node:stream/consumers';
4
+ import { ChildProcess } from 'node:child_process';
5
+
6
+ import type { Sharp } from 'sharp';
7
+
8
+ import { CommandOperation } from '@travetto/command';
9
+ import { castTo } from '@travetto/runtime';
10
+
11
+ /**
12
+ * Image resize options
13
+ */
14
+ export interface ResizeOptions {
15
+ /**
16
+ * New height
17
+ */
18
+ h?: number;
19
+ /**
20
+ * New width
21
+ */
22
+ w?: number;
23
+ /**
24
+ * Should the image be optimized?
25
+ */
26
+ optimize?: boolean;
27
+ /**
28
+ * Strict resolution
29
+ */
30
+ strictResolution?: boolean;
31
+ /**
32
+ * Sub process, allows for externalization of memory
33
+ */
34
+ asSubprocess?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Image optimize options
39
+ */
40
+ export interface OptimizeOptions {
41
+ format?: 'png' | 'jpeg';
42
+ /**
43
+ * Sub process, allows for externalization of memory
44
+ */
45
+ asSubprocess?: boolean;
46
+ }
47
+
48
+
49
+ type ImageType = Readable | Buffer;
50
+
51
+ /**
52
+ * Simple support for image manipulation.
53
+ */
54
+ export class ImageUtil {
55
+
56
+ /**
57
+ * Resize/conversion util
58
+ */
59
+ static CONVERTER = new CommandOperation({
60
+ containerImage: 'jameskyburz/graphicsmagick-alpine:v1.0.0',
61
+ localCheck: ['gm', ['-version']]
62
+ });
63
+
64
+ /**
65
+ * Compressor
66
+ */
67
+ static PNG_COMPRESSOR = new CommandOperation({
68
+ containerImage: 'agregad/pngquant:latest',
69
+ localCheck: ['pngquant', ['-h']]
70
+ });
71
+
72
+ /**
73
+ * Compressor
74
+ */
75
+ static JPEG_COMPRESSOR = new CommandOperation({
76
+ containerImage: 'shomatan/jpegoptim:1.4.4',
77
+ localCheck: ['jpegoptim', ['-h']]
78
+ });
79
+
80
+ static async #subprocessReturn<T extends ImageType>(proc: ChildProcess, input: T): Promise<T> {
81
+ if (Buffer.isBuffer(input)) {
82
+ const [, buffer] = await Promise.all([
83
+ pipeline(Readable.from(input), proc.stdin!),
84
+ toBuffer(proc.stdout!)
85
+ ]);
86
+ return castTo(buffer);
87
+ } else {
88
+ pipeline(input, proc.stdin!);
89
+ return castTo(proc.stdout);
90
+ }
91
+ }
92
+
93
+ static async #sharpReturn<T extends ImageType>(output: Sharp, input: T, optimize?: boolean, format?: 'jpeg' | 'png'): Promise<T> {
94
+ if (optimize) {
95
+ output = output
96
+ .jpeg({ quality: 80, progressive: true, force: format === 'jpeg' })
97
+ .png({ compressionLevel: 9, quality: 80, adaptiveFiltering: true, force: format === 'png' });
98
+ }
99
+ const stream = Buffer.isBuffer(input) ? Readable.from(input) : input;
100
+ pipeline(stream, output);
101
+ return castTo(Buffer.isBuffer(input) ? output.toBuffer() : output);
102
+ }
103
+
104
+ /**
105
+ * Resize image
106
+ */
107
+ static async resize<T extends ImageType>(image: T, options: ResizeOptions = {}): Promise<T> {
108
+ const dims = [options.w, options.h].map(d => (!d && options.strictResolution === false) ? undefined : d);
109
+ if (options.asSubprocess) {
110
+ return this.#subprocessReturn(
111
+ await this.CONVERTER.exec('gm', 'convert', '-resize', dims.join('x'), '-auto-orient',
112
+ ...(options.optimize ? ['-strip', '-quality', '86'] : []), '-', '-'),
113
+ image);
114
+ } else {
115
+ const { default: sharp } = await import('sharp');
116
+
117
+ return this.#sharpReturn(
118
+ sharp().resize({
119
+ width: dims[0],
120
+ height: dims[1],
121
+ fit: options.strictResolution !== false ? 'fill' : 'inside'
122
+ }),
123
+ image,
124
+ options.optimize,
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Optimize an image
131
+ */
132
+ static async optimize<T extends ImageType>(image: T, options: OptimizeOptions = {}): Promise<T> {
133
+ if (options.asSubprocess) {
134
+ switch (options.format) {
135
+ case 'png': return this.#subprocessReturn(
136
+ await this.PNG_COMPRESSOR.exec('pngquant', '--quality', '40-80', '--speed', '1', '--force', '-'), image);
137
+ default:
138
+ case 'jpeg': return this.#subprocessReturn(
139
+ await this.JPEG_COMPRESSOR.exec('jpegoptim', '-m70', '-s', '--stdin', '--stdout'), image);
140
+ }
141
+ } else {
142
+ const { default: sharp } = await import('sharp');
143
+ return this.#sharpReturn(sharp(), image, true, options.format);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Get Image Dimensions
149
+ */
150
+ static async getDimensions(image: Buffer | string): Promise<{ width: number, height: number }> {
151
+ const { default: sharp } = await import('sharp');
152
+ return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height! }));
153
+ }
154
+ }
package/src/convert.ts DELETED
@@ -1,130 +0,0 @@
1
- import { Readable } from 'node:stream';
2
- import { buffer as toBuffer } from 'node:stream/consumers';
3
- import { createReadStream } from 'node:fs';
4
- import { ChildProcess } from 'node:child_process';
5
- import { pipeline } from 'node:stream/promises';
6
-
7
- import { CommandOperation } from '@travetto/command';
8
- import { castTo } from '@travetto/runtime';
9
-
10
- /**
11
- * Image output options
12
- */
13
- export interface ImageOptions {
14
- /**
15
- * New height
16
- */
17
- h?: number;
18
- /**
19
- * New width
20
- */
21
- w?: number;
22
- /**
23
- * Should the image be optimized?
24
- */
25
- optimize?: boolean;
26
- /**
27
- * Strict resolution
28
- */
29
- strictResolution?: boolean;
30
- }
31
-
32
- type ImageType = Readable | Buffer;
33
-
34
- /**
35
- * Simple support for image manipulation. Built upon @travetto/command, it can
36
- * run imagemagick and pngquant locally or via docker as needed.
37
- */
38
- export class ImageConverter {
39
-
40
- /**
41
- * Resize/conversion util
42
- */
43
- static CONVERTER = new CommandOperation({
44
- containerImage: 'jameskyburz/graphicsmagick-alpine:v1.0.0',
45
- localCheck: ['gm', ['-version']]
46
- });
47
-
48
- /**
49
- * Compressor
50
- */
51
- static PNG_COMPRESSOR = new CommandOperation({
52
- containerImage: 'agregad/pngquant:latest',
53
- localCheck: ['pngquant', ['-h']]
54
- });
55
-
56
- /**
57
- * Compressor
58
- */
59
- static JPEG_COMPRESSOR = new CommandOperation({
60
- containerImage: 'shomatan/jpegoptim:1.4.4',
61
- localCheck: ['jpegoptim', ['-h']]
62
- });
63
-
64
- static async #stream<T extends ImageType>(proc: ChildProcess, input: T): Promise<T> {
65
- if (Buffer.isBuffer(input)) {
66
- Readable.from(input).pipe(proc.stdin!);
67
- return castTo(toBuffer(proc.stdout!));
68
- } else {
69
- input.pipe(proc.stdin!); // Start the process
70
- return castTo(proc.stdout);
71
- }
72
- }
73
-
74
- /**
75
- * Resize image using imagemagick
76
- */
77
- static async resize<T extends ImageType>(image: T, options: ImageOptions): Promise<T> {
78
- const dims = [options.w, options.h].map(d => (d && options.strictResolution !== false) ? `${d}!` : d).join('x');
79
-
80
- const proc = await this.CONVERTER.exec(
81
- 'gm', 'convert', '-resize', dims, '-auto-orient',
82
- ...(options.optimize ? ['-strip', '-quality', '86'] : []),
83
- '-', '-');
84
-
85
- return this.#stream(proc, image);
86
- }
87
-
88
- /**
89
- * Optimize image
90
- */
91
- static async optimize<T extends ImageType>(format: 'png' | 'jpeg', image: T): Promise<T> {
92
- let proc: ChildProcess;
93
- switch (format) {
94
- case 'png': {
95
- proc = await this.PNG_COMPRESSOR.exec(
96
- 'pngquant', '--quality', '40-80', '--speed', '1', '--force', '-');
97
- break;
98
- }
99
- case 'jpeg': {
100
- proc = await this.JPEG_COMPRESSOR.exec('jpegoptim', '-m70', '-s', '--stdin', '--stdout');
101
- break;
102
- }
103
- }
104
- return this.#stream(proc, image);
105
- }
106
-
107
- /**
108
- * Get Image Dimensions
109
- * @param image
110
- */
111
- static async getDimensions(image: Readable | Buffer | string): Promise<{ width: number, height: number }> {
112
- const proc = await this.CONVERTER.exec(
113
- 'gm', 'identify', '-format', '%wX%h', '-',
114
- );
115
-
116
- if (typeof image === 'string') {
117
- image = createReadStream(image);
118
- }
119
-
120
- const [_, output] = await Promise.all([
121
- pipeline(image, proc.stdin!),
122
- toBuffer(proc.stdout!)
123
- ]);
124
-
125
- const text = output.toString('utf8');
126
- const [w, h] = text.split('X').map(x => parseFloat(x));
127
-
128
- return { width: w, height: h };
129
- }
130
- }
package/src/resource.ts DELETED
@@ -1,51 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import { Readable } from 'node:stream';
3
- import { pipeline } from 'node:stream/promises';
4
- import path from 'node:path';
5
-
6
- import { Env, FileLoader, Runtime } from '@travetto/runtime';
7
-
8
- import { ImageConverter } from './convert';
9
-
10
- /**
11
- * Resource provider for images that allows for real-time optimization
12
- */
13
- export class ImageOptimizingResourceLoader extends FileLoader {
14
-
15
- #cacheRoot: string;
16
-
17
- constructor(paths: string[] = [], cacheRoot?: string) {
18
- super(Runtime.resourcePaths(paths));
19
-
20
- this.#cacheRoot = cacheRoot ?? path.resolve(Env.TRV_IMAGE_CACHE.val || Runtime.toolPath('image_cache'));
21
- }
22
-
23
- async #openFile(pth: string): Promise<fs.FileHandle> {
24
- return fs.open(path.join(this.#cacheRoot, pth.replace(/[\\/]/g, '__')));
25
- }
26
-
27
- /**
28
- * Fetch image, compress and return as buffer
29
- */
30
- async readOptimized(rel: string): Promise<Buffer> {
31
- const cachedOutput = path.resolve(this.#cacheRoot, rel);
32
- await fs.mkdir(path.dirname(cachedOutput), { recursive: true });
33
-
34
- const handle = await this.#openFile(cachedOutput);
35
- const exists = !!(await handle.stat().catch(() => false));
36
-
37
- if (!exists) {
38
- let stream: Buffer | Readable = await this.readStream(rel);
39
- if (/[.]png$/.test(rel)) {
40
- stream = await ImageConverter.optimize('png', stream);
41
- } else if (/[.]jpe?g$/i.test(rel)) {
42
- stream = await ImageConverter.optimize('jpeg', stream);
43
- }
44
- await pipeline(stream, handle.createWriteStream());
45
- }
46
-
47
- const buffer = await handle.readFile();
48
- await handle.close();
49
- return buffer;
50
- }
51
- }
package/src/trv.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import '@travetto/runtime';
2
-
3
- declare global {
4
- interface TravettoEnv {
5
- /**
6
- * Where should optimized images be stored by default
7
- * @default undefined
8
- */
9
- TRV_IMAGE_CACHE: string;
10
- }
11
- }