@travetto/image 5.0.0-rc.9 → 5.0.1
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 +12 -4
- package/__index__.ts +1 -2
- package/package.json +4 -3
- package/src/util.ts +155 -0
- package/src/convert.ts +0 -130
- package/src/resource.ts +0 -51
- package/src/trv.d.ts +0 -11
package/README.md
CHANGED
|
@@ -13,20 +13,28 @@ 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 [
|
|
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
|
|
18
|
+
The [ImageUtil](https://github.com/travetto/travetto/tree/main/module/image/src/util.ts#L55) 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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Image support, resizing, and optimization",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"images",
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"directory": "module/image"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@travetto/
|
|
27
|
-
"@travetto/
|
|
26
|
+
"@travetto/command": "^5.0.1",
|
|
27
|
+
"@travetto/runtime": "^5.0.1",
|
|
28
|
+
"sharp": "^0.33.5"
|
|
28
29
|
},
|
|
29
30
|
"travetto": {
|
|
30
31
|
"displayName": "Image"
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { buffer as toBuffer } from 'node:stream/consumers';
|
|
5
|
+
import { ChildProcess } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
import type { Sharp } from 'sharp';
|
|
8
|
+
|
|
9
|
+
import { CommandOperation } from '@travetto/command';
|
|
10
|
+
import { castTo } from '@travetto/runtime';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Image resize options
|
|
14
|
+
*/
|
|
15
|
+
export interface ResizeOptions {
|
|
16
|
+
/**
|
|
17
|
+
* New height
|
|
18
|
+
*/
|
|
19
|
+
h?: number;
|
|
20
|
+
/**
|
|
21
|
+
* New width
|
|
22
|
+
*/
|
|
23
|
+
w?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Should the image be optimized?
|
|
26
|
+
*/
|
|
27
|
+
optimize?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Strict resolution
|
|
30
|
+
*/
|
|
31
|
+
strictResolution?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Sub process, allows for externalization of memory
|
|
34
|
+
*/
|
|
35
|
+
asSubprocess?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Image optimize options
|
|
40
|
+
*/
|
|
41
|
+
export interface OptimizeOptions {
|
|
42
|
+
format?: 'png' | 'jpeg';
|
|
43
|
+
/**
|
|
44
|
+
* Sub process, allows for externalization of memory
|
|
45
|
+
*/
|
|
46
|
+
asSubprocess?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
type ImageType = Readable | Buffer | ReadableStream;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Simple support for image manipulation.
|
|
54
|
+
*/
|
|
55
|
+
export class ImageUtil {
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resize/conversion util
|
|
59
|
+
*/
|
|
60
|
+
static CONVERTER = new CommandOperation({
|
|
61
|
+
containerImage: 'jameskyburz/graphicsmagick-alpine:v1.0.0',
|
|
62
|
+
localCheck: ['gm', ['-version']]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compressor
|
|
67
|
+
*/
|
|
68
|
+
static PNG_COMPRESSOR = new CommandOperation({
|
|
69
|
+
containerImage: 'agregad/pngquant:latest',
|
|
70
|
+
localCheck: ['pngquant', ['-h']]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compressor
|
|
75
|
+
*/
|
|
76
|
+
static JPEG_COMPRESSOR = new CommandOperation({
|
|
77
|
+
containerImage: 'shomatan/jpegoptim:1.4.4',
|
|
78
|
+
localCheck: ['jpegoptim', ['-h']]
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
static async #subprocessReturn<T extends ImageType>(proc: ChildProcess, input: T): Promise<T> {
|
|
82
|
+
if (Buffer.isBuffer(input)) {
|
|
83
|
+
const [, buffer] = await Promise.all([
|
|
84
|
+
pipeline(Readable.from(input), proc.stdin!),
|
|
85
|
+
toBuffer(proc.stdout!)
|
|
86
|
+
]);
|
|
87
|
+
return castTo(buffer);
|
|
88
|
+
} else {
|
|
89
|
+
pipeline(castTo<Readable>(input), proc.stdin!);
|
|
90
|
+
return castTo('pipeThrough' in input ? ReadableStream.from(proc.stdout!) : proc.stdout);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async #sharpReturn<T extends ImageType>(output: Sharp, input: T, optimize?: boolean, format?: 'jpeg' | 'png'): Promise<T> {
|
|
95
|
+
if (optimize) {
|
|
96
|
+
output = output
|
|
97
|
+
.jpeg({ quality: 80, progressive: true, force: format === 'jpeg' })
|
|
98
|
+
.png({ compressionLevel: 9, quality: 80, adaptiveFiltering: true, force: format === 'png' });
|
|
99
|
+
}
|
|
100
|
+
const stream = Buffer.isBuffer(input) ? Readable.from(input) : input;
|
|
101
|
+
pipeline(stream, output);
|
|
102
|
+
return castTo('pipeThrough' in input ? ReadableStream.from(output) : Buffer.isBuffer(input) ? output.toBuffer() : output);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resize image
|
|
107
|
+
*/
|
|
108
|
+
static async resize<T extends ImageType>(image: T, options: ResizeOptions = {}): Promise<T> {
|
|
109
|
+
const dims = [options.w, options.h].map(d => (!d && options.strictResolution === false) ? undefined : d);
|
|
110
|
+
if (options.asSubprocess) {
|
|
111
|
+
return this.#subprocessReturn(
|
|
112
|
+
await this.CONVERTER.exec('gm', 'convert', '-resize', dims.join('x'), '-auto-orient',
|
|
113
|
+
...(options.optimize ? ['-strip', '-quality', '86'] : []), '-', '-'),
|
|
114
|
+
image);
|
|
115
|
+
} else {
|
|
116
|
+
const { default: sharp } = await import('sharp');
|
|
117
|
+
|
|
118
|
+
return this.#sharpReturn(
|
|
119
|
+
sharp().resize({
|
|
120
|
+
width: dims[0],
|
|
121
|
+
height: dims[1],
|
|
122
|
+
fit: options.strictResolution !== false ? 'fill' : 'inside'
|
|
123
|
+
}),
|
|
124
|
+
image,
|
|
125
|
+
options.optimize,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Optimize an image
|
|
132
|
+
*/
|
|
133
|
+
static async optimize<T extends ImageType>(image: T, options: OptimizeOptions = {}): Promise<T> {
|
|
134
|
+
if (options.asSubprocess) {
|
|
135
|
+
switch (options.format) {
|
|
136
|
+
case 'png': return this.#subprocessReturn(
|
|
137
|
+
await this.PNG_COMPRESSOR.exec('pngquant', '--quality', '40-80', '--speed', '1', '--force', '-'), image);
|
|
138
|
+
default:
|
|
139
|
+
case 'jpeg': return this.#subprocessReturn(
|
|
140
|
+
await this.JPEG_COMPRESSOR.exec('jpegoptim', '-m70', '-s', '--stdin', '--stdout', '-'), image);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const { default: sharp } = await import('sharp');
|
|
144
|
+
return this.#sharpReturn(sharp(), image, true, options.format);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get Image Dimensions
|
|
150
|
+
*/
|
|
151
|
+
static async getDimensions(image: Buffer | string): Promise<{ width: number, height: number }> {
|
|
152
|
+
const { default: sharp } = await import('sharp');
|
|
153
|
+
return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height! }));
|
|
154
|
+
}
|
|
155
|
+
}
|
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
|
-
}
|