UNPKG

5.61 kBJavaScriptView Raw
1const CustomError = require('./customerror.js')
2const rotateBuffer = require('./transform.js').rotateBuffer
3const fs = require('fs')
4const piexif = require('piexifjs')
5const promisify = require('util').promisify
6
7const m = {}
8
9m.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 * Read the input, rotate the image, return the result (updated buffer, dimensions, etc)
20 */
21m.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 * Transform the given input to a buffer
65 * (May be a string or a buffer)
66 */
67function 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
79function 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 * Extract the orientation tag from the given EXIF data
91 */
92function 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
109function 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
115function 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 * Compute the final buffer by updating the original EXIF data and linking it to the rotated buffer
132 */
133function 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
153module.exports = m