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