1 | /*
|
2 | * JavaScript Load Image Meta
|
3 | * https://github.com/blueimp/JavaScript-Load-Image
|
4 | *
|
5 | * Copyright 2013, Sebastian Tschan
|
6 | * https://blueimp.net
|
7 | *
|
8 | * Image metadata handling implementation
|
9 | * based on the help and contribution of
|
10 | * Achim Stöhr.
|
11 | *
|
12 | * Licensed under the MIT license:
|
13 | * https://opensource.org/licenses/MIT
|
14 | */
|
15 |
|
16 | /* global define, module, require, Promise, DataView, Uint8Array, ArrayBuffer */
|
17 |
|
18 | ;(function (factory) {
|
19 |
|
20 | if (typeof define === 'function' && define.amd) {
|
21 | // Register as an anonymous AMD module:
|
22 | define(['./load-image'], factory)
|
23 | } else if (typeof module === 'object' && module.exports) {
|
24 | factory(require('./load-image'))
|
25 | } else {
|
26 | // Browser globals:
|
27 | factory(window.loadImage)
|
28 | }
|
29 | })(function (loadImage) {
|
30 |
|
31 |
|
32 | var global = loadImage.global
|
33 | var originalTransform = loadImage.transform
|
34 |
|
35 | var blobSlice =
|
36 | global.Blob &&
|
37 | (Blob.prototype.slice ||
|
38 | Blob.prototype.webkitSlice ||
|
39 | Blob.prototype.mozSlice)
|
40 |
|
41 | var bufferSlice =
|
42 | (global.ArrayBuffer && ArrayBuffer.prototype.slice) ||
|
43 | function (begin, end) {
|
44 | // Polyfill for IE10, which does not support ArrayBuffer.slice
|
45 | // eslint-disable-next-line no-param-reassign
|
46 | end = end || this.byteLength - begin
|
47 | var arr1 = new Uint8Array(this, begin, end)
|
48 | var arr2 = new Uint8Array(end)
|
49 | arr2.set(arr1)
|
50 | return arr2.buffer
|
51 | }
|
52 |
|
53 | var metaDataParsers = {
|
54 | jpeg: {
|
55 | 0xffe1: [], // APP1 marker
|
56 | 0xffed: [] // APP13 marker
|
57 | }
|
58 | }
|
59 |
|
60 | /**
|
61 | * Parses image metadata and calls the callback with an object argument
|
62 | * with the following property:
|
63 | * - imageHead: The complete image head as ArrayBuffer
|
64 | * The options argument accepts an object and supports the following
|
65 | * properties:
|
66 | * - maxMetaDataSize: Defines the maximum number of bytes to parse.
|
67 | * - disableImageHead: Disables creating the imageHead property.
|
68 | *
|
69 | * @param {Blob} file Blob object
|
70 | * @param {Function} [callback] Callback function
|
71 | * @param {object} [options] Parsing options
|
72 | * @param {object} [data] Result data object
|
73 | * @returns {Promise<object>|undefined} Returns Promise if no callback given.
|
74 | */
|
75 | function parseMetaData(file, callback, options, data) {
|
76 | var that = this
|
77 | /**
|
78 | * Promise executor
|
79 | *
|
80 | * @param {Function} resolve Resolution function
|
81 | * @param {Function} reject Rejection function
|
82 | * @returns {undefined} Undefined
|
83 | */
|
84 | function executor(resolve, reject) {
|
85 | if (
|
86 | !(
|
87 | global.DataView &&
|
88 | blobSlice &&
|
89 | file &&
|
90 | file.size >= 12 &&
|
91 | file.type === 'image/jpeg'
|
92 | )
|
93 | ) {
|
94 | // Nothing to parse
|
95 | return resolve(data)
|
96 | }
|
97 | // 256 KiB should contain all EXIF/ICC/IPTC segments:
|
98 | var maxMetaDataSize = options.maxMetaDataSize || 262144
|
99 | if (
|
100 | !loadImage.readFile(
|
101 | blobSlice.call(file, 0, maxMetaDataSize),
|
102 | function (buffer) {
|
103 | // Note on endianness:
|
104 | // Since the marker and length bytes in JPEG files are always
|
105 | // stored in big endian order, we can leave the endian parameter
|
106 | // of the DataView methods undefined, defaulting to big endian.
|
107 | var dataView = new DataView(buffer)
|
108 | // Check for the JPEG marker (0xffd8):
|
109 | if (dataView.getUint16(0) !== 0xffd8) {
|
110 | return reject(
|
111 | new Error('Invalid JPEG file: Missing JPEG marker.')
|
112 | )
|
113 | }
|
114 | var offset = 2
|
115 | var maxOffset = dataView.byteLength - 4
|
116 | var headLength = offset
|
117 | var markerBytes
|
118 | var markerLength
|
119 | var parsers
|
120 | var i
|
121 | while (offset < maxOffset) {
|
122 | markerBytes = dataView.getUint16(offset)
|
123 | // Search for APPn (0xffeN) and COM (0xfffe) markers,
|
124 | // which contain application-specific metadata like
|
125 | // Exif, ICC and IPTC data and text comments:
|
126 | if (
|
127 | (markerBytes >= 0xffe0 && markerBytes <= 0xffef) ||
|
128 | markerBytes === 0xfffe
|
129 | ) {
|
130 | // The marker bytes (2) are always followed by
|
131 | // the length bytes (2), indicating the length of the
|
132 | // marker segment, which includes the length bytes,
|
133 | // but not the marker bytes, so we add 2:
|
134 | markerLength = dataView.getUint16(offset + 2) + 2
|
135 | if (offset + markerLength > dataView.byteLength) {
|
136 | // eslint-disable-next-line no-console
|
137 | console.log('Invalid JPEG metadata: Invalid segment size.')
|
138 | break
|
139 | }
|
140 | parsers = metaDataParsers.jpeg[markerBytes]
|
141 | if (parsers && !options.disableMetaDataParsers) {
|
142 | for (i = 0; i < parsers.length; i += 1) {
|
143 | parsers[i].call(
|
144 | that,
|
145 | dataView,
|
146 | offset,
|
147 | markerLength,
|
148 | data,
|
149 | options
|
150 | )
|
151 | }
|
152 | }
|
153 | offset += markerLength
|
154 | headLength = offset
|
155 | } else {
|
156 | // Not an APPn or COM marker, probably safe to
|
157 | // assume that this is the end of the metadata
|
158 | break
|
159 | }
|
160 | }
|
161 | // Meta length must be longer than JPEG marker (2)
|
162 | // plus APPn marker (2), followed by length bytes (2):
|
163 | if (!options.disableImageHead && headLength > 6) {
|
164 | data.imageHead = bufferSlice.call(buffer, 0, headLength)
|
165 | }
|
166 | resolve(data)
|
167 | },
|
168 | reject,
|
169 | 'readAsArrayBuffer'
|
170 | )
|
171 | ) {
|
172 | // No support for the FileReader interface, nothing to parse
|
173 | resolve(data)
|
174 | }
|
175 | }
|
176 | options = options || {} // eslint-disable-line no-param-reassign
|
177 | if (global.Promise && typeof callback !== 'function') {
|
178 | options = callback || {} // eslint-disable-line no-param-reassign
|
179 | data = options // eslint-disable-line no-param-reassign
|
180 | return new Promise(executor)
|
181 | }
|
182 | data = data || {} // eslint-disable-line no-param-reassign
|
183 | return executor(callback, callback)
|
184 | }
|
185 |
|
186 | /**
|
187 | * Replaces the head of a JPEG Blob
|
188 | *
|
189 | * @param {Blob} blob Blob object
|
190 | * @param {ArrayBuffer} oldHead Old JPEG head
|
191 | * @param {ArrayBuffer} newHead New JPEG head
|
192 | * @returns {Blob} Combined Blob
|
193 | */
|
194 | function replaceJPEGHead(blob, oldHead, newHead) {
|
195 | if (!blob || !oldHead || !newHead) return null
|
196 | return new Blob([newHead, blobSlice.call(blob, oldHead.byteLength)], {
|
197 | type: 'image/jpeg'
|
198 | })
|
199 | }
|
200 |
|
201 | /**
|
202 | * Replaces the image head of a JPEG blob with the given one.
|
203 | * Returns a Promise or calls the callback with the new Blob.
|
204 | *
|
205 | * @param {Blob} blob Blob object
|
206 | * @param {ArrayBuffer} head New JPEG head
|
207 | * @param {Function} [callback] Callback function
|
208 | * @returns {Promise<Blob|null>|undefined} Combined Blob
|
209 | */
|
210 | function replaceHead(blob, head, callback) {
|
211 | var options = { maxMetaDataSize: 256, disableMetaDataParsers: true }
|
212 | if (!callback && global.Promise) {
|
213 | return parseMetaData(blob, options).then(function (data) {
|
214 | return replaceJPEGHead(blob, data.imageHead, head)
|
215 | })
|
216 | }
|
217 | parseMetaData(
|
218 | blob,
|
219 | function (data) {
|
220 | callback(replaceJPEGHead(blob, data.imageHead, head))
|
221 | },
|
222 | options
|
223 | )
|
224 | }
|
225 |
|
226 | loadImage.transform = function (img, options, callback, file, data) {
|
227 | if (loadImage.requiresMetaData(options)) {
|
228 | data = data || {} // eslint-disable-line no-param-reassign
|
229 | parseMetaData(
|
230 | file,
|
231 | function (result) {
|
232 | if (result !== data) {
|
233 | // eslint-disable-next-line no-console
|
234 | if (global.console) console.log(result)
|
235 | result = data // eslint-disable-line no-param-reassign
|
236 | }
|
237 | originalTransform.call(
|
238 | loadImage,
|
239 | img,
|
240 | options,
|
241 | callback,
|
242 | file,
|
243 | result
|
244 | )
|
245 | },
|
246 | options,
|
247 | data
|
248 | )
|
249 | } else {
|
250 | originalTransform.apply(loadImage, arguments)
|
251 | }
|
252 | }
|
253 |
|
254 | loadImage.blobSlice = blobSlice
|
255 | loadImage.bufferSlice = bufferSlice
|
256 | loadImage.replaceHead = replaceHead
|
257 | loadImage.parseMetaData = parseMetaData
|
258 | loadImage.metaDataParsers = metaDataParsers
|
259 | })
|