exifr
Version:
📷 The fastest and most versatile JavaScript EXIF reading library.
203 lines (177 loc) • 5.86 kB
JavaScript
import {AppSegmentParserBase} from '../parser.mjs'
import {segmentParsers} from '../plugins.mjs'
import {throwError, normalizeString} from '../util/helpers.mjs'
import {BufferView} from '../util/BufferView.mjs'
const PROFILE_HEADER_LENGTH = 84
const TAG_TYPE_DESC = 'desc'
const TAG_TYPE_MLUC = 'mluc'
const TAG_TYPE_TEXT = 'text'
const TAG_TYPE_SIG = 'sig '
// TODO: other types 'mft2', 'XYZ '
const EMPTY_VALUE = '\x00\x00\x00\x00'
export default class Icc extends AppSegmentParserBase {
static type = 'icc'
static multiSegment = true
static headerLength = 18
static canHandle(chunk, offset) {
return chunk.getUint8(offset + 1) === 0xE2
&& chunk.getUint32(offset + 4) === 0x4943435f // ICC_
// full header is: ICC_PROFILE
}
static findPosition(chunk, offset) {
let seg = super.findPosition(chunk, offset)
seg.chunkNumber = chunk.getUint8(offset + 16)
seg.chunkCount = chunk.getUint8(offset + 17)
seg.multiSegment = seg.chunkCount > 1
return seg
}
static handleMultiSegments(segments) {
return concatChunks(segments)
}
parse() {
this.raw = new Map
this.parseHeader()
this.parseTags()
this.translate()
return this.output
}
parseHeader() {
let {raw} = this
if (this.chunk.byteLength < PROFILE_HEADER_LENGTH)
throwError('ICC header is too short')
for (let [offset, parse] of Object.entries(headerParsers)) {
offset = parseInt(offset, 10)
let val = parse(this.chunk, offset)
if (val === EMPTY_VALUE) continue
raw.set(offset, val)
}
}
parseTags() {
let {raw} = this
let tagCount = this.chunk.getUint32(128)
let offset = 132
let chunkLength = this.chunk.byteLength
let code, valueOffset, valueLength, type, value
while (tagCount--) {
code = this.chunk.getString(offset, 4)
valueOffset = this.chunk.getUint32(offset + 4)
valueLength = this.chunk.getUint32(offset + 8)
type = this.chunk.getString(valueOffset, 4)
if (valueOffset + valueLength > chunkLength) {
console.warn('reached the end of the first ICC chunk. Enable options.tiff.multiSegment to read all ICC segments.')
return
}
value = this.parseTag(type, valueOffset, valueLength)
// Not all the type parsers are implemented.
if (value !== undefined && value !== EMPTY_VALUE)
raw.set(code, value)
offset += 12
}
}
parseTag(type, offset, length) {
switch (type) {
case TAG_TYPE_DESC: return this.parseDesc(offset)
case TAG_TYPE_MLUC: return this.parseMluc(offset)
case TAG_TYPE_TEXT: return this.parseText(offset, length)
case TAG_TYPE_SIG: return this.parseSig(offset)
// TODO: implement more types
}
if (offset + length > this.chunk.byteLength) {
// TODO: handle these when multi-segment ICC is being implemented
// look out for issue-metadata-extractor-65.jpg
} else {
return this.chunk.getUint8Array(offset, length)
}
}
parseDesc(offset) {
let length = this.chunk.getUint32(offset + 8) - 1 // last byte is null termination
return normalizeString(this.chunk.getString(offset + 12, length))
}
parseText(offset, length) {
return normalizeString(this.chunk.getString(offset + 8, length - 8))
}
// NOTE: some tags end with empty space. TODO: investigate. maybe add .trim()
parseSig(offset) {
return normalizeString(this.chunk.getString(offset + 8, 4))
}
// Multi Localized Unicode Type
parseMluc(tagOffset) {
let {chunk} = this
let entryCount = chunk.getUint32(tagOffset + 8)
let entrySize = chunk.getUint32(tagOffset + 12)
let entryOffset = tagOffset + 16
let values = []
for (let i = 0; i < entryCount; i++) {
let lang = chunk.getString(entryOffset + 0, 2)
let country = chunk.getString(entryOffset + 2, 2)
let length = chunk.getUint32(entryOffset + 4)
let offset = chunk.getUint32(entryOffset + 8) + tagOffset
let text = normalizeString(chunk.getUnicodeString(offset, length))
values.push({lang, country, text})
entryOffset += entrySize
}
if (entryCount === 1)
return values[0].text
else
return values
}
translateValue(val, tagEnum) {
if (typeof val === 'string')
return tagEnum[val] || tagEnum[val.toLowerCase()] || val
else
return tagEnum[val] || val
}
}
const headerParsers = {
4: parseString,
8: parseVersion,
12: parseString,
16: parseString,
20: parseString,
24: parseDate,
36: parseString,
40: parseString,
48: parseString,
52: parseString,
64: (view, offset) => view.getUint32(offset),
80: parseString
}
function parseString(view, offset) {
return normalizeString(view.getString(offset, 4))
}
function parseVersion(view, offset) {
return [
view.getUint8(offset),
view.getUint8(offset + 1) >> 4,
view.getUint8(offset + 1) % 16,
]
.map(num => num.toString(10))
.join('.')
}
function parseDate(view, offset) {
const year = view.getUint16(offset)
const month = view.getUint16(offset + 2) - 1
const day = view.getUint16(offset + 4)
const hours = view.getUint16(offset + 6)
const minutes = view.getUint16(offset + 8)
const seconds = view.getUint16(offset + 10)
return new Date(Date.UTC(year, month, day, hours, minutes, seconds))
}
function concatChunks(chunks) {
let buffers = chunks.map(s => s.chunk.toUint8())
let combined = concatBuffers(buffers)
return new BufferView(combined)
}
function concatBuffers(buffers) {
let ArrayType = buffers[0].constructor
let totalLength = 0
for (let buffer of buffers) totalLength += buffer.length
let result = new ArrayType(totalLength)
let offset = 0
for (let buffer of buffers) {
result.set(buffer, offset)
offset += buffer.length
}
return result
}
segmentParsers.set('icc', Icc)