1 | /*
|
2 | * JavaScript Load Image IPTC Parser
|
3 | * https://github.com/blueimp/JavaScript-Load-Image
|
4 | *
|
5 | * Copyright 2013, Sebastian Tschan
|
6 | * Copyright 2018, Dave Bevan
|
7 | * https://blueimp.net
|
8 | *
|
9 | * Licensed under the MIT license:
|
10 | * https://opensource.org/licenses/MIT
|
11 | */
|
12 |
|
13 | /* global define, module, require, DataView */
|
14 |
|
15 | ;(function (factory) {
|
16 |
|
17 | if (typeof define === 'function' && define.amd) {
|
18 | // Register as an anonymous AMD module:
|
19 | define(['./load-image', './load-image-meta'], factory)
|
20 | } else if (typeof module === 'object' && module.exports) {
|
21 | factory(require('./load-image'), require('./load-image-meta'))
|
22 | } else {
|
23 | // Browser globals:
|
24 | factory(window.loadImage)
|
25 | }
|
26 | })(function (loadImage) {
|
27 |
|
28 |
|
29 | /**
|
30 | * IPTC tag map
|
31 | *
|
32 | * @name IptcMap
|
33 | * @class
|
34 | */
|
35 | function IptcMap() {}
|
36 |
|
37 | IptcMap.prototype.map = {
|
38 | ObjectName: 5
|
39 | }
|
40 |
|
41 | IptcMap.prototype.types = {
|
42 | 0: 'Uint16', // ApplicationRecordVersion
|
43 | 200: 'Uint16', // ObjectPreviewFileFormat
|
44 | 201: 'Uint16', // ObjectPreviewFileVersion
|
45 | 202: 'binary' // ObjectPreviewData
|
46 | }
|
47 |
|
48 | /**
|
49 | * Retrieves IPTC tag value
|
50 | *
|
51 | * @param {number|string} id IPTC tag code or name
|
52 | * @returns {object} IPTC tag value
|
53 | */
|
54 | IptcMap.prototype.get = function (id) {
|
55 | return this[id] || this[this.map[id]]
|
56 | }
|
57 |
|
58 | /**
|
59 | * Retrieves string for the given DataView and range
|
60 | *
|
61 | * @param {DataView} dataView Data view interface
|
62 | * @param {number} offset Offset start
|
63 | * @param {number} length Offset length
|
64 | * @returns {string} String value
|
65 | */
|
66 | function getStringValue(dataView, offset, length) {
|
67 | var outstr = ''
|
68 | var end = offset + length
|
69 | for (var n = offset; n < end; n += 1) {
|
70 | outstr += String.fromCharCode(dataView.getUint8(n))
|
71 | }
|
72 | return outstr
|
73 | }
|
74 |
|
75 | /**
|
76 | * Retrieves tag value for the given DataView and range
|
77 | *
|
78 | * @param {number} tagCode tag code
|
79 | * @param {IptcMap} map IPTC tag map
|
80 | * @param {DataView} dataView Data view interface
|
81 | * @param {number} offset Range start
|
82 | * @param {number} length Range length
|
83 | * @returns {object} Tag value
|
84 | */
|
85 | function getTagValue(tagCode, map, dataView, offset, length) {
|
86 | if (map.types[tagCode] === 'binary') {
|
87 | return new Blob([dataView.buffer.slice(offset, offset + length)])
|
88 | }
|
89 | if (map.types[tagCode] === 'Uint16') {
|
90 | return dataView.getUint16(offset)
|
91 | }
|
92 | return getStringValue(dataView, offset, length)
|
93 | }
|
94 |
|
95 | /**
|
96 | * Combines IPTC value with existing ones.
|
97 | *
|
98 | * @param {object} value Existing IPTC field value
|
99 | * @param {object} newValue New IPTC field value
|
100 | * @returns {object} Resulting IPTC field value
|
101 | */
|
102 | function combineTagValues(value, newValue) {
|
103 | if (value === undefined) return newValue
|
104 | if (value instanceof Array) {
|
105 | value.push(newValue)
|
106 | return value
|
107 | }
|
108 | return [value, newValue]
|
109 | }
|
110 |
|
111 | /**
|
112 | * Parses IPTC tags.
|
113 | *
|
114 | * @param {DataView} dataView Data view interface
|
115 | * @param {number} segmentOffset Segment offset
|
116 | * @param {number} segmentLength Segment length
|
117 | * @param {object} data Data export object
|
118 | * @param {object} includeTags Map of tags to include
|
119 | * @param {object} excludeTags Map of tags to exclude
|
120 | */
|
121 | function parseIptcTags(
|
122 | dataView,
|
123 | segmentOffset,
|
124 | segmentLength,
|
125 | data,
|
126 | includeTags,
|
127 | excludeTags
|
128 | ) {
|
129 | var value, tagSize, tagCode
|
130 | var segmentEnd = segmentOffset + segmentLength
|
131 | var offset = segmentOffset
|
132 | while (offset < segmentEnd) {
|
133 | if (
|
134 | dataView.getUint8(offset) === 0x1c && // tag marker
|
135 | dataView.getUint8(offset + 1) === 0x02 // record number, only handles v2
|
136 | ) {
|
137 | tagCode = dataView.getUint8(offset + 2)
|
138 | if (
|
139 | (!includeTags || includeTags[tagCode]) &&
|
140 | (!excludeTags || !excludeTags[tagCode])
|
141 | ) {
|
142 | tagSize = dataView.getInt16(offset + 3)
|
143 | value = getTagValue(tagCode, data.iptc, dataView, offset + 5, tagSize)
|
144 | data.iptc[tagCode] = combineTagValues(data.iptc[tagCode], value)
|
145 | if (data.iptcOffsets) {
|
146 | data.iptcOffsets[tagCode] = offset
|
147 | }
|
148 | }
|
149 | }
|
150 | offset += 1
|
151 | }
|
152 | }
|
153 |
|
154 | /**
|
155 | * Tests if field segment starts at offset.
|
156 | *
|
157 | * @param {DataView} dataView Data view interface
|
158 | * @param {number} offset Segment offset
|
159 | * @returns {boolean} True if '8BIM<EOT><EOT>' exists at offset
|
160 | */
|
161 | function isSegmentStart(dataView, offset) {
|
162 | return (
|
163 | dataView.getUint32(offset) === 0x3842494d && // Photoshop segment start
|
164 | dataView.getUint16(offset + 4) === 0x0404 // IPTC segment start
|
165 | )
|
166 | }
|
167 |
|
168 | /**
|
169 | * Returns header length.
|
170 | *
|
171 | * @param {DataView} dataView Data view interface
|
172 | * @param {number} offset Segment offset
|
173 | * @returns {number} Header length
|
174 | */
|
175 | function getHeaderLength(dataView, offset) {
|
176 | var length = dataView.getUint8(offset + 7)
|
177 | if (length % 2 !== 0) length += 1
|
178 | // Check for pre photoshop 6 format
|
179 | if (length === 0) {
|
180 | // Always 4
|
181 | length = 4
|
182 | }
|
183 | return length
|
184 | }
|
185 |
|
186 | loadImage.parseIptcData = function (dataView, offset, length, data, options) {
|
187 | if (options.disableIptc) {
|
188 | return
|
189 | }
|
190 | var markerLength = offset + length
|
191 | while (offset + 8 < markerLength) {
|
192 | if (isSegmentStart(dataView, offset)) {
|
193 | var headerLength = getHeaderLength(dataView, offset)
|
194 | var segmentOffset = offset + 8 + headerLength
|
195 | if (segmentOffset > markerLength) {
|
196 | // eslint-disable-next-line no-console
|
197 | console.log('Invalid IPTC data: Invalid segment offset.')
|
198 | break
|
199 | }
|
200 | var segmentLength = dataView.getUint16(offset + 6 + headerLength)
|
201 | if (offset + segmentLength > markerLength) {
|
202 | // eslint-disable-next-line no-console
|
203 | console.log('Invalid IPTC data: Invalid segment size.')
|
204 | break
|
205 | }
|
206 | // Create the iptc object to store the tags:
|
207 | data.iptc = new IptcMap()
|
208 | if (!options.disableIptcOffsets) {
|
209 | data.iptcOffsets = new IptcMap()
|
210 | }
|
211 | parseIptcTags(
|
212 | dataView,
|
213 | segmentOffset,
|
214 | segmentLength,
|
215 | data,
|
216 | options.includeIptcTags,
|
217 | options.excludeIptcTags || { 202: true } // ObjectPreviewData
|
218 | )
|
219 | return
|
220 | }
|
221 | // eslint-disable-next-line no-param-reassign
|
222 | offset += 1
|
223 | }
|
224 | }
|
225 |
|
226 | // Registers this IPTC parser for the APP13 JPEG metadata segment:
|
227 | loadImage.metaDataParsers.jpeg[0xffed].push(loadImage.parseIptcData)
|
228 |
|
229 | loadImage.IptcMap = IptcMap
|
230 |
|
231 | // Adds the following properties to the parseMetaData callback data:
|
232 | // - iptc: The iptc tags, parsed by the parseIptcData method
|
233 |
|
234 | // Adds the following options to the parseMetaData method:
|
235 | // - disableIptc: Disables IPTC parsing when true.
|
236 | // - disableIptcOffsets: Disables storing IPTC tag offsets when true.
|
237 | // - includeIptcTags: A map of IPTC tags to include for parsing.
|
238 | // - excludeIptcTags: A map of IPTC tags to exclude from parsing.
|
239 | })
|