@evshiron/exifr
Version:
📷 The fastest and most versatile JavaScript EXIF reading library.
212 lines (182 loc) • 7.12 kB
JavaScript
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)