UNPKG

5.23 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 = 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 * Parse the target JPEG quality
56 * (Got from the CLI or the public API)
57 */
58function 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 * Transform the given input to a buffer
69 * (May be a string or a buffer)
70 */
71function 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
83function 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 * Extract the orientation tag from the given EXIF data
95 */
96function 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
113function 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
119function 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 * Compute the final buffer by updating the original EXIF data and linking it to the rotated buffer
130 */
131function 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
151module.exports = m