-
Notifications
You must be signed in to change notification settings - Fork 7
/
image.ts
137 lines (117 loc) · 3.38 KB
/
image.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import gm, { State } from 'gm';
import sharp = require('sharp');
import { logger } from '../../common/logger';
const PngQuant = require('pngquant');
/**
* Holds a physical image that can be processed by GLTFExporter.
*
* It uses Sharp for reprocessing, crushes PNGs with PngQuant and converts
* PNGs to JPEGs when no alpha channel is found.
*/
export class Image {
private static readonly jpegQuality = 65;
public readonly src: string;
public height: number;
public width: number;
private format: string;
private sharp: sharp.Sharp;
private data: Buffer;
private readonly optimize: boolean;
private gm: State;
private stats: sharp.Stats;
constructor(src: string, data: Buffer | sharp.Sharp, optimize: boolean) {
this.src = src;
this.optimize = optimize;
if (data instanceof Buffer) {
this.sharp = sharp(data);
this.data = data;
} else {
this.sharp = data;
}
}
public async init(): Promise<this> {
try {
const metadata = await this.sharp.metadata();
this.width = metadata.width;
this.height = metadata.height;
this.format = metadata.format;
} catch (err) {
logger.warn(null, '[Image.init] Could not read metadata from buffer (%s), using GM to read image.', err.message);
this.gm = gm(this.data);
const metadata = await this.gmIdentify();
this.format = metadata.format.toLowerCase();
this.width = metadata.size.width;
this.height = metadata.size.height;
const data = await new Promise((resolve, reject) => {
const buffers: Buffer[] = [];
this.gm.setFormat('jpeg').stream().on('error', reject)
.on('data', (buf: Buffer) => buffers.push(buf as Buffer))
.on('end', () => resolve(Buffer.concat(buffers)))
.on('error', reject);
});
this.data = undefined;
this.sharp = sharp(data);
}
this.stats = await this.sharp.stats();
return this;
}
public resize(width: number, height: number): this {
this.sharp.resize(width, height, { fit: 'fill' });
this.width = width;
this.height = height;
return this;
}
public flipY(): this {
this.sharp.flip();
return this;
}
public getFormat(): string {
return this.format;
}
public getMimeType(): string {
return !this.stats.isOpaque ? 'image/png' : 'image/jpeg';
}
public async getImage(): Promise<Buffer> {
if (this.stats.isOpaque) {
if (this.format === 'png') {
logger.debug(null, '[Image.getImage]: Converting opaque png to jpeg.');
}
return this.sharp.jpeg({ quality: Image.jpegQuality }).toBuffer();
}
switch (this.format) {
case 'png': {
if (this.optimize) {
const quanter = new PngQuant([128]);
return new Promise((resolve, reject) => {
const buffers: Buffer[] = [];
this.sharp.on('error', reject)
.pipe(quanter).on('error', reject)
.on('data', (buf: Buffer) => buffers.push(buf as Buffer))
.on('end', () => resolve(Buffer.concat(buffers)))
.on('error', reject);
});
}
return this.sharp.toBuffer();
}
default: {
return this.sharp.jpeg({ quality: Image.jpegQuality }).toBuffer();
}
}
}
public hasTransparency(): boolean {
return ['png', 'webp', 'gif'].includes(this.format);
}
public containsTransparency(): boolean {
return !this.stats.isOpaque;
}
private async gmIdentify(): Promise<any> {
return new Promise((resolve, reject) => {
this.gm.identify((err, value) => {
if (err) {
return reject(err);
}
resolve(value);
});
});
}
}