main.js
5.62 KB
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
const CustomError = require('./customerror.js')
const rotateBuffer = require('./transform.js').rotateBuffer
const fs = require('fs')
const piexif = require('piexifjs')
const promisify = require('util').promisify
const m = {}
m.errors = {
read_file: 'read_file',
read_exif: 'read_exif',
no_orientation: 'no_orientation',
unknown_orientation: 'unknown_orientation',
correct_orientation: 'correct_orientation',
rotate_file: 'rotate_file',
}
/**
* Read the input, rotate the image, return the result (updated buffer, dimensions, etc)
*/
m.rotate = function (pathOrBuffer, opts, callback) {
const hasCallback = typeof callback === 'function'
const quality =
typeof opts === 'object' &&
opts !== null &&
typeof opts.quality === 'number' &&
opts.quality > 0 &&
opts.quality <= 100
? opts.quality
: 100
const maxResolutionInMP =
typeof opts === 'object' &&
opts !== null &&
typeof opts.jpegjsMaxResolutionInMP === 'number' &&
opts.jpegjsMaxResolutionInMP > 0
? opts.jpegjsMaxResolutionInMP
: null
const maxMemoryUsageInMB =
typeof opts === 'object' &&
opts !== null &&
typeof opts.jpegjsMaxMemoryUsageInMB === 'number' &&
opts.jpegjsMaxMemoryUsageInMB > 0
? opts.jpegjsMaxMemoryUsageInMB
: null
const promise = readBuffer(pathOrBuffer)
.then(readExifFromBuffer)
.then(({buffer, exifData}) => {
const orientation = parseOrientationTag({buffer, exifData})
return Promise.all([
rotateImage(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB),
rotateThumbnail(buffer, exifData, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB),
]).then(([image, thumbnail]) => {
return computeFinalBuffer(image, thumbnail, exifData, orientation)
})
})
.then(({updatedBuffer, orientation, updatedDimensions}) => {
if (!hasCallback) {
return {buffer: updatedBuffer, orientation, dimensions: updatedDimensions, quality}
}
callback(null, updatedBuffer, orientation, updatedDimensions, quality)
})
.catch((customError) => {
const buffer = customError.buffer
delete customError.buffer
if (!hasCallback) {
throw customError
}
callback(customError, buffer, null, null, null)
})
if (!hasCallback) {
return promise
}
}
/**
* Transform the given input to a buffer
* (May be a string or a buffer)
*/
function readBuffer(pathOrBuffer) {
if (typeof pathOrBuffer === 'string') {
return promisify(fs.readFile)(pathOrBuffer).catch((error) => {
throw new CustomError(m.errors.read_file, 'Could not read file (' + error.message + ')')
})
}
if (typeof pathOrBuffer === 'object' && Buffer.isBuffer(pathOrBuffer)) {
return Promise.resolve(pathOrBuffer)
}
return Promise.reject(new CustomError(m.errors.read_file, 'Not a file path or buffer'))
}
function readExifFromBuffer(buffer) {
let exifData = null
try {
exifData = piexif.load(buffer.toString('binary'))
} catch (error) {
return Promise.reject(new CustomError(m.errors.read_exif, 'Could not read EXIF data (' + error + ')'))
}
return Promise.resolve({buffer, exifData})
}
/**
* Extract the orientation tag from the given EXIF data
*/
function parseOrientationTag({buffer, exifData}) {
let orientation = null
if (exifData['0th'] && exifData['0th'][piexif.ImageIFD.Orientation]) {
orientation = parseInt(exifData['0th'][piexif.ImageIFD.Orientation])
}
if (orientation === null) {
throw new CustomError(m.errors.no_orientation, 'No orientation tag found in EXIF', buffer)
}
if (isNaN(orientation) || orientation < 1 || orientation > 8) {
throw new CustomError(m.errors.unknown_orientation, 'Unknown orientation (' + orientation + ')', buffer)
}
if (orientation === 1) {
throw new CustomError(m.errors.correct_orientation, 'Orientation already correct', buffer)
}
return orientation
}
function rotateImage(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB) {
return rotateBuffer(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB).catch((error) => {
throw new CustomError(m.errors.rotate_file, 'Could not rotate image (' + error.message + ')', buffer)
})
}
function rotateThumbnail(buffer, exifData, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB) {
if (typeof exifData['thumbnail'] === 'undefined' || exifData['thumbnail'] === null) {
return Promise.resolve({})
}
return rotateBuffer(
Buffer.from(exifData['thumbnail'], 'binary'),
orientation,
quality,
maxResolutionInMP,
maxMemoryUsageInMB
).catch((error) => {
throw new CustomError(m.errors.rotate_file, 'Could not rotate thumbnail (' + error.message + ')', buffer)
})
}
/**
* Compute the final buffer by updating the original EXIF data and linking it to the rotated buffer
*/
function computeFinalBuffer(image, thumbnail, exifData, orientation) {
exifData['0th'][piexif.ImageIFD.Orientation] = 1
if (typeof exifData['Exif'][piexif.ExifIFD.PixelXDimension] !== 'undefined') {
exifData['Exif'][piexif.ExifIFD.PixelXDimension] = image.width
}
if (typeof exifData['Exif'][piexif.ExifIFD.PixelYDimension] !== 'undefined') {
exifData['Exif'][piexif.ExifIFD.PixelYDimension] = image.height
}
if (thumbnail.buffer) {
exifData['thumbnail'] = thumbnail.buffer.toString('binary')
}
const exifBytes = piexif.dump(exifData)
const updatedBuffer = Buffer.from(piexif.insert(exifBytes, image.buffer.toString('binary')), 'binary')
const updatedDimensions = {
height: image.height,
width: image.width,
}
return Promise.resolve({updatedBuffer, orientation, updatedDimensions})
}
module.exports = m