1 | const CustomError = require('./customerror.js')
|
2 | const rotateBuffer = require('./transform.js').rotateBuffer
|
3 | const fs = require('fs')
|
4 | const piexif = require('piexifjs')
|
5 | const promisify = require('util').promisify
|
6 |
|
7 | const m = {}
|
8 |
|
9 | m.errors = {
|
10 | read_file: 'read_file',
|
11 | read_exif: 'read_exif',
|
12 | no_orientation: 'no_orientation',
|
13 | unknown_orientation: 'unknown_orientation',
|
14 | correct_orientation: 'correct_orientation',
|
15 | rotate_file: 'rotate_file',
|
16 | }
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | m.rotate = function (pathOrBuffer, options, callback) {
|
22 | const hasCallback = typeof callback === 'function'
|
23 | const quality =
|
24 | typeof options.quality === 'number' && options.quality > 0 && options.quality <= 100 ? options.quality : 100
|
25 | const maxResolutionInMP =
|
26 | typeof options.jpegjsMaxResolutionInMP === 'number' && options.jpegjsMaxResolutionInMP > 0
|
27 | ? options.jpegjsMaxResolutionInMP
|
28 | : null
|
29 | const maxMemoryUsageInMB =
|
30 | typeof options.jpegjsMaxMemoryUsageInMB === 'number' && options.jpegjsMaxMemoryUsageInMB > 0
|
31 | ? options.jpegjsMaxMemoryUsageInMB
|
32 | : null
|
33 | const promise = readBuffer(pathOrBuffer)
|
34 | .then(readExifFromBuffer)
|
35 | .then(({buffer, exifData}) => {
|
36 | const orientation = parseOrientationTag({buffer, exifData})
|
37 | return Promise.all([
|
38 | rotateImage(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB),
|
39 | rotateThumbnail(buffer, exifData, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB),
|
40 | ]).then(([image, thumbnail]) => {
|
41 | return computeFinalBuffer(image, thumbnail, exifData, orientation)
|
42 | })
|
43 | })
|
44 | .then(({updatedBuffer, orientation, updatedDimensions}) => {
|
45 | if (!hasCallback) {
|
46 | return {buffer: updatedBuffer, orientation, dimensions: updatedDimensions, quality}
|
47 | }
|
48 | callback(null, updatedBuffer, orientation, updatedDimensions, quality)
|
49 | })
|
50 | .catch((customError) => {
|
51 | const buffer = customError.buffer
|
52 | delete customError.buffer
|
53 | if (!hasCallback) {
|
54 | throw customError
|
55 | }
|
56 | callback(customError, buffer, null, null, null)
|
57 | })
|
58 | if (!hasCallback) {
|
59 | return promise
|
60 | }
|
61 | }
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 | function readBuffer(pathOrBuffer) {
|
68 | if (typeof pathOrBuffer === 'string') {
|
69 | return promisify(fs.readFile)(pathOrBuffer).catch((error) => {
|
70 | throw new CustomError(m.errors.read_file, 'Could not read file (' + error.message + ')')
|
71 | })
|
72 | }
|
73 | if (typeof pathOrBuffer === 'object' && Buffer.isBuffer(pathOrBuffer)) {
|
74 | return Promise.resolve(pathOrBuffer)
|
75 | }
|
76 | return Promise.reject(new CustomError(m.errors.read_file, 'Not a file path or buffer'))
|
77 | }
|
78 |
|
79 | function readExifFromBuffer(buffer) {
|
80 | let exifData = null
|
81 | try {
|
82 | exifData = piexif.load(buffer.toString('binary'))
|
83 | } catch (error) {
|
84 | return Promise.reject(new CustomError(m.errors.read_exif, 'Could not read EXIF data (' + error + ')'))
|
85 | }
|
86 | return Promise.resolve({buffer, exifData})
|
87 | }
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | function parseOrientationTag({buffer, exifData}) {
|
93 | let orientation = null
|
94 | if (exifData['0th'] && exifData['0th'][piexif.ImageIFD.Orientation]) {
|
95 | orientation = parseInt(exifData['0th'][piexif.ImageIFD.Orientation])
|
96 | }
|
97 | if (orientation === null) {
|
98 | throw new CustomError(m.errors.no_orientation, 'No orientation tag found in EXIF', buffer)
|
99 | }
|
100 | if (isNaN(orientation) || orientation < 1 || orientation > 8) {
|
101 | throw new CustomError(m.errors.unknown_orientation, 'Unknown orientation (' + orientation + ')', buffer)
|
102 | }
|
103 | if (orientation === 1) {
|
104 | throw new CustomError(m.errors.correct_orientation, 'Orientation already correct', buffer)
|
105 | }
|
106 | return orientation
|
107 | }
|
108 |
|
109 | function rotateImage(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB) {
|
110 | return rotateBuffer(buffer, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB).catch((error) => {
|
111 | throw new CustomError(m.errors.rotate_file, 'Could not rotate image (' + error.message + ')', buffer)
|
112 | })
|
113 | }
|
114 |
|
115 | function rotateThumbnail(buffer, exifData, orientation, quality, maxResolutionInMP, maxMemoryUsageInMB) {
|
116 | if (typeof exifData['thumbnail'] === 'undefined' || exifData['thumbnail'] === null) {
|
117 | return Promise.resolve({})
|
118 | }
|
119 | return rotateBuffer(
|
120 | Buffer.from(exifData['thumbnail'], 'binary'),
|
121 | orientation,
|
122 | quality,
|
123 | maxResolutionInMP,
|
124 | maxMemoryUsageInMB
|
125 | ).catch((error) => {
|
126 | throw new CustomError(m.errors.rotate_file, 'Could not rotate thumbnail (' + error.message + ')', buffer)
|
127 | })
|
128 | }
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | function computeFinalBuffer(image, thumbnail, exifData, orientation) {
|
134 | exifData['0th'][piexif.ImageIFD.Orientation] = 1
|
135 | if (typeof exifData['Exif'][piexif.ExifIFD.PixelXDimension] !== 'undefined') {
|
136 | exifData['Exif'][piexif.ExifIFD.PixelXDimension] = image.width
|
137 | }
|
138 | if (typeof exifData['Exif'][piexif.ExifIFD.PixelYDimension] !== 'undefined') {
|
139 | exifData['Exif'][piexif.ExifIFD.PixelYDimension] = image.height
|
140 | }
|
141 | if (thumbnail.buffer) {
|
142 | exifData['thumbnail'] = thumbnail.buffer.toString('binary')
|
143 | }
|
144 | const exifBytes = piexif.dump(exifData)
|
145 | const updatedBuffer = Buffer.from(piexif.insert(exifBytes, image.buffer.toString('binary')), 'binary')
|
146 | const updatedDimensions = {
|
147 | height: image.height,
|
148 | width: image.width,
|
149 | }
|
150 | return Promise.resolve({updatedBuffer, orientation, updatedDimensions})
|
151 | }
|
152 |
|
153 | module.exports = m
|