UNPKG

8.58 kBJavaScriptView Raw
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 'use strict'
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 'use strict'
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})