@travetto/image 5.0.13 → 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 +1 -1
  2. package/package.json +1 -1
  3. package/src/util.ts +42 -43
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.14",
4
4
  "description": "Image support, resizing, and optimization",
5
5
  "keywords": [
6
6
  "images",
package/src/util.ts CHANGED
@@ -2,16 +2,14 @@ import { Readable } from 'node:stream';
2
2
  import { ReadableStream } from 'node:stream/web';
3
3
  import { pipeline } from 'node:stream/promises';
4
4
 
5
- import type { Sharp } from 'sharp';
6
-
7
- import { castTo } from '@travetto/runtime';
5
+ import { AppError, castTo } from '@travetto/runtime';
8
6
 
9
7
  type ImageFormat = 'jpeg' | 'png' | 'avif' | 'webp';
10
8
 
11
9
  /**
12
- * Image resize options
10
+ * Image convert options
13
11
  */
14
- export interface ResizeOptions {
12
+ export interface ConvertOptions {
15
13
  /**
16
14
  * New height
17
15
  */
@@ -30,14 +28,6 @@ export interface ResizeOptions {
30
28
  format?: ImageFormat;
31
29
  }
32
30
 
33
- /**
34
- * Image optimize options
35
- */
36
- export interface OptimizeOptions {
37
- format?: ImageFormat;
38
- }
39
-
40
-
41
31
  type ImageType = Readable | Buffer | ReadableStream;
42
32
 
43
33
  /**
@@ -45,44 +35,50 @@ type ImageType = Readable | Buffer | ReadableStream;
45
35
  */
46
36
  export class ImageUtil {
47
37
 
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
38
  /**
60
39
  * Resize image
61
40
  */
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);
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
+ }
65
48
 
66
49
  const { default: sharp } = await import('sharp');
67
50
 
68
- return this.#sharpReturn(
69
- sharp().resize({
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({
70
56
  width: dims[0],
71
57
  height: dims[1],
72
58
  fit: fluid ? 'inside' : 'fill'
73
- }),
74
- image,
75
- options.optimize,
76
- options.format
77
- );
78
- }
59
+ });
60
+ }
79
61
 
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);
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;
75
+ }
76
+
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);
86
82
  }
87
83
 
88
84
  /**
@@ -96,8 +92,11 @@ export class ImageUtil {
96
92
  /**
97
93
  * Get image type
98
94
  */
99
- static async getFileType(image: Buffer | string): Promise<string> {
95
+ static async getFileType(image: Buffer | string): Promise<ImageFormat> {
100
96
  const { default: sharp } = await import('sharp');
101
- return sharp(image).metadata().then(v => v.format?.replace('heif', 'avif')!);
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
+ );
102
101
  }
103
102
  }