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