forked from MikeKovarik/exifr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathparser.mjs
212 lines (182 loc) · 6.74 KB
/
parser.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import {BufferView} from './util/BufferView.mjs'
import {Options} from './options.mjs'
import {tagKeys, tagValues, tagRevivers} from './tags.mjs'
import {throwError} from './util/helpers.mjs'
import {segmentParsers} from './plugins.mjs'
const MAX_APP_SIZE = 65536 // 64kb
const DEFAULT = 'DEFAULT'
export class FileParserBase {
constructor(options, file, parsers) {
if (this.extendOptions)
this.extendOptions(options)
this.options = options
this.file = file
this.parsers = parsers
}
errors = []
injectSegment(type, chunk) {
if (this.options[type].enabled)
this.createParser(type, chunk)
}
createParser(type, chunk) {
let Parser = segmentParsers.get(type)
let parser = new Parser(chunk, this.options, this.file)
return this.parsers[type] = parser
}
// NOTE: This method was created to be reusable and not just one off. Mainly due to parsing ifd0 before thumbnail extraction.
// But also because we want to enable advanced users selectively add and execute parser on the fly.
createParsers(segments) {
// IDEA: dynamic loading through import(parser.type) ???
// We would need to know the type of segment, but we dont since its implemented in parser itself.
// I.E. Unless we first load apropriate parser, the segment is of unknown type.
for (let segment of segments) {
let {type, chunk} = segment
let segOpts = this.options[type]
if (segOpts && segOpts.enabled) {
let parser = this.parsers[type]
if (parser && parser.append) {
// TODO multisegment: to be implemented. or deleted. some types of data may be split into multiple APP segments (FLIR, maybe ICC)
//parser.append(chunk)
} else if (!parser) {
this.createParser(type, chunk)
}
}
}
}
async readSegments(segments) {
//let ranges = new Ranges(this.appSegments)
//await Promise.all(ranges.list.map(range => this.file.ensureChunk(range.offset, range.length)))
let promises = segments.map(this.ensureSegmentChunk)
await Promise.all(promises)
}
// TODO: deprecate
ensureSegmentChunk = async seg => {
let start = seg.start
let size = seg.size || MAX_APP_SIZE
if (this.file.chunked) {
if (this.file.available(start, size)) {
seg.chunk = this.file.subarray(start, size)
} else {
try {
seg.chunk = await this.file.readChunk(start, size)
} catch (err) {
throwError(`Couldn't read segment: ${JSON.stringify(seg)}. ${err.message}`)
}
}
} else if (this.file.byteLength > start + size) {
seg.chunk = this.file.subarray(start, size)
} else if (seg.size === undefined) {
// we dont know the length of segment and the file is much smaller than the fallback size of 64kbs (MAX_APP_SIZE)
seg.chunk = this.file.subarray(start)
} else {
throwError(`Segment unreachable: ` + JSON.stringify(seg))
}
return seg.chunk
}
}
/*
offset = where FF En starts
length = size of the whole APPn segment (header/marker + content)
start = start of the content
size = size of the content. i.e. from start till end
end = end of the content (as well as the APPn segment)
*/
export class AppSegmentParserBase {
static headerLength = 4
// name. Couldn't use static name property because it is used by contructor name
static type = undefined
// The data may span multiple APP segments.
static multiSegment = false
static canHandle = () => false
// offset + length === end | begining and end of the whole segment, including the segment header 0xFF 0xEn + two lenght bytes.
// start + size === end | begining and end of parseable content
static findPosition(buffer, offset) {
// length at offset+2 is the size of APPn content plus the two appN length bytes. it does not include te appN 0xFF 0xEn marker.
let length = buffer.getUint16(offset + 2) + 2
let headerLength = typeof this.headerLength === 'function'
? this.headerLength(buffer, offset, length)
: this.headerLength
let start = offset + headerLength
let size = length - headerLength
let end = start + size
return {offset, length, headerLength, start, size, end}
}
static parse(input, segOptions = {}) {
let options = new Options({[this.type]: segOptions})
let instance = new this(input, options, input)
return instance.parse()
}
normalizeInput(input) {
return input instanceof BufferView
? input
: new BufferView(input)
}
errors = []
// raw parsed tags
raw = new Map
constructor(chunk, options = {}, file) {
// BufferView instance of the segment chunk. Possibly a subview of the same memory shared with this.file
this.chunk = this.normalizeInput(chunk)
// BufferView instance of the whole file.
this.file = file
this.type = this.constructor.type
this.globalOptions = this.options = options // todo: rename to fileOptions ???
this.localOptions = options[this.type] // todo: rename to this.options
this.canTranslate = this.localOptions && this.localOptions.translate
}
// can be overriden by parses (namely TIFF) that inherits from this base class.
translate() {
if (this.canTranslate)
this.translated = this.translateBlock(this.raw, this.type)
}
get output() {
if (this.translated)
return this.translated
else if (this.raw)
return Object.fromEntries(this.raw)
}
// split into separate function so that it can be used by TIFF but shared with other parsers.
translateBlock(rawTags, blockKey) {
let revivers = tagRevivers.get(blockKey)
let valDict = tagValues.get(blockKey)
let keyDict = tagKeys.get(blockKey)
let blockOptions = this.options[blockKey] // todo: refactor tiff so this isn't needed anymore (in favor of segOptions & options)
let canRevive = blockOptions.reviveValues && !!revivers
let canTranslateVal = blockOptions.translateValues && !!valDict
let canTranslateKey = blockOptions.translateKeys && !!keyDict
let output = {}
for (let [key, val] of rawTags) {
if (canRevive && revivers.has(key))
val = revivers.get(key)(val)
else if (canTranslateVal && valDict.has(key))
val = this.translateValue(val, valDict.get(key))
if (canTranslateKey && keyDict.has(key))
key = keyDict.get(key) || key
output[key] = val
}
return output
}
// can be overriden by parses (namely ICC) that inherits from this base class.
translateValue(val, tagEnum) {
return tagEnum[val] || tagEnum[DEFAULT] || val
}
handleError = error => {
if (this.options.silentErrors)
this.errors.push(error.message)
else
throw error
}
assignToOutput(root, parserOutput) {
this.assignObjectToOutput(root, this.constructor.type, parserOutput)
}
assignObjectToOutput(root, key, parserOutput) {
if (this.globalOptions.mergeOutput)
return Object.assign(root, parserOutput)
if (root[key])
Object.assign(root[key], parserOutput)
else
root[key] = parserOutput
}
}
const isDefined = val => val !== undefined
const findDefined = (...values) => values.find(isDefined)