@travetto/image 5.0.13 → 5.0.15

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 +1 -1
  2. package/package.json +1 -1
  3. package/src/util.ts +49 -54
package/README.md CHANGED
@@ -27,7 +27,7 @@ import { ImageUtil } from '@travetto/image';
27
27
 
28
28
  export class ResizeService {
29
29
  async resizeImage(imgPath: string, width: number, height: number): Promise<string> {
30
- const stream = await ImageUtil.resize(createReadStream(imgPath), { w: width, h: height });
30
+ const stream = await ImageUtil.convert(createReadStream(imgPath), { w: width, h: height });
31
31
  const out = imgPath.replace(/[.][^.]+$/, (ext) => `.resized${ext}`);
32
32
  await pipeline(stream, createWriteStream(out));
33
33
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/image",
3
- "version": "5.0.13",
3
+ "version": "5.0.15",
4
4
  "description": "Image support, resizing, and optimization",
5
5
  "keywords": [
6
6
  "images",
package/src/util.ts CHANGED
@@ -1,17 +1,18 @@
1
+ import { createReadStream } from 'node:fs';
1
2
  import { Readable } from 'node:stream';
2
3
  import { ReadableStream } from 'node:stream/web';
3
4
  import { pipeline } from 'node:stream/promises';
4
-
5
- import type { Sharp } from 'sharp';
5
+ import { Metadata } from 'sharp';
6
6
 
7
7
  import { castTo } from '@travetto/runtime';
8
8
 
9
9
  type ImageFormat = 'jpeg' | 'png' | 'avif' | 'webp';
10
+ type Input = Buffer | string | ReadableStream | Readable;
10
11
 
11
12
  /**
12
- * Image resize options
13
+ * Image convert options
13
14
  */
14
- export interface ResizeOptions {
15
+ export interface ConvertOptions {
15
16
  /**
16
17
  * New height
17
18
  */
@@ -30,74 +31,68 @@ export interface ResizeOptions {
30
31
  format?: ImageFormat;
31
32
  }
32
33
 
33
- /**
34
- * Image optimize options
35
- */
36
- export interface OptimizeOptions {
37
- format?: ImageFormat;
38
- }
39
-
40
-
41
- type ImageType = Readable | Buffer | ReadableStream;
42
-
43
34
  /**
44
35
  * Simple support for image manipulation.
45
36
  */
46
37
  export class ImageUtil {
47
38
 
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' });
54
- const stream = Buffer.isBuffer(input) ? Readable.from(input) : input;
55
- pipeline(stream, output);
56
- return castTo('pipeThrough' in input ? ReadableStream.from(output) : Buffer.isBuffer(input) ? output.toBuffer() : output);
57
- }
58
-
59
39
  /**
60
- * Resize image
40
+ * Convert image
61
41
  */
62
- static async resize<T extends ImageType>(image: T, options: ResizeOptions = {}): Promise<T> {
63
- const dims = [options.w, options.h].map(x => x ? Math.trunc(x) : undefined);
64
- const fluid = dims.some(x => !x);
65
-
42
+ static async convert<T extends Input>(image: T, { format, optimize, ...opts }: ConvertOptions): Promise<T extends string ? Readable : T> {
66
43
  const { default: sharp } = await import('sharp');
67
44
 
68
- return this.#sharpReturn(
69
- sharp().resize({
45
+ let builder = sharp();
46
+ if (opts.w || opts.h) {
47
+ const dims = [opts.w, opts.h].map(x => x ? Math.trunc(x) : undefined);
48
+ const fluid = dims.some(x => !x);
49
+ builder = builder.resize({
70
50
  width: dims[0],
71
51
  height: dims[1],
72
52
  fit: fluid ? 'inside' : 'fill'
73
- }),
74
- image,
75
- options.optimize,
76
- options.format
77
- );
78
- }
53
+ });
54
+ }
79
55
 
80
- /**
81
- * Optimize an image
82
- */
83
- static async optimize<T extends ImageType>(image: T, options: OptimizeOptions = {}): Promise<T> {
84
- const { default: sharp } = await import('sharp');
85
- return this.#sharpReturn(sharp(), image, true, options.format);
86
- }
56
+ builder = builder
57
+ .avif({ force: format === 'avif', ...optimize ? { quality: 70 } : {} })
58
+ .webp({ force: format === 'webp', ...optimize ? { quality: 80 } : {} })
59
+ .png({ force: format === 'png', ...optimize ? { compressionLevel: 9, quality: 80, adaptiveFiltering: true } : {} })
60
+ .jpeg({ force: format === 'jpeg', ...optimize ? { quality: 80, progressive: true } : {} });
87
61
 
88
- /**
89
- * Get Image Dimensions
90
- */
91
- static async getDimensions(image: Buffer | string): Promise<{ width: number, height: number, aspect: number }> {
92
- const { default: sharp } = await import('sharp');
93
- return sharp(image).metadata().then(v => ({ width: v.width!, height: v.height!, aspect: v.width! / v.height! }));
62
+ const stream = Buffer.isBuffer(image) ?
63
+ Readable.from(image) :
64
+ (typeof image === 'string' ? createReadStream(image) : image);
65
+
66
+ pipeline(stream, builder);
67
+ return castTo(
68
+ typeof image === 'string' ?
69
+ builder : Buffer.isBuffer(image) ?
70
+ builder.toBuffer() :
71
+ (image instanceof ReadableStream) ?
72
+ ReadableStream.from(builder) : builder
73
+ );
94
74
  }
95
75
 
96
76
  /**
97
- * Get image type
77
+ * Get Image metadata
98
78
  */
99
- static async getFileType(image: Buffer | string): Promise<string> {
79
+ static async getMetadata(image: Input): Promise<{
80
+ width: number;
81
+ height: number;
82
+ aspect: number;
83
+ format: ImageFormat;
84
+ }> {
100
85
  const { default: sharp } = await import('sharp');
101
- return sharp(image).metadata().then(v => v.format?.replace('heif', 'avif')!);
86
+ const out = await ((Buffer.isBuffer(image) || typeof image === 'string') ?
87
+ sharp(image).metadata() :
88
+ new Promise<Metadata>((resolve, reject) =>
89
+ pipeline(image, sharp().metadata((err, metadata) => err ? reject(err) : resolve(metadata)))
90
+ ));
91
+ return {
92
+ width: out.width!,
93
+ height: out.height!,
94
+ format: castTo(out.format?.replace('heif', 'avif')!),
95
+ aspect: out.width! / out.height!
96
+ };
102
97
  }
103
98
  }