forked from MikeKovarik/exifr
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathheif.mjs
217 lines (195 loc) · 6.85 KB
/
heif.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
213
214
215
216
217
import {fileParsers} from '../plugins.mjs'
import {FileParserBase} from '../parser.mjs'
// Only HEIC uses BufferView.getUint64
import '../util/BufferView-get64.mjs'
import {BufferView} from '../util/BufferView.mjs'
// 4 length + 4 kind + 8 (not always) for additional 64b length field
const boxHeaderLength = 16
// boxes with full head: meta, iinf, iref
export class IsoBmffParser extends FileParserBase {
parseBoxes(offset = 0) {
let boxes = []
while (offset < this.file.byteLength - 4) {
let box = this.parseBoxHead(offset)
boxes.push(box)
if (box.length === 0) break
offset += box.length
}
return boxes
}
parseSubBoxes(box) {
box.boxes = this.parseBoxes(box.start)
}
findBox(box, kind) {
if (box.boxes === undefined) this.parseSubBoxes(box)
return box.boxes.find(box => box.kind === kind)
}
parseBoxHead(offset) {
let length = this.file.getUint32(offset)
let kind = this.file.getString(offset + 4, 4)
let start = offset + 8 // 4+4 bytes
// length can be larger than 32b number in which case it is the first 64bits after header
if (length === 1) {
length = this.file.getUint64(offset + 8)
start += 8
}
return {offset, length, kind, start}
}
parseBoxFullHead(box) {
// ISO boxes come in 'old' and 'full' variants.
// The 'full' variant also contains version and flags information.
if (box.version !== undefined) return
let vflags = this.file.getUint32(box.start)
box.version = vflags >> 24
box.start += 4
}
}
export class HeifFileParser extends IsoBmffParser {
// NOTE: most parsers check if bytes 4-8 are 'ftyp' and then if 8-12 is one of heic/heix/hevc/hevx/heim/heis/hevm/hevs/mif1/msf1
// but bytes 20-24 are actually always 'heic' for all of these formats
static canHandle(file, firstTwoBytes) {
// The file starts with 4 byte FTYP number. The value is unlikely more than 30, let alone 2^32 FTYPs.
// So it's safe to assume that if first two bytes are 0, then this is HEIC.
if (firstTwoBytes !== 0) return false
let ftypLength = file.getUint16(2)
if (ftypLength > 50) return false
let offset = 16
let compatibleBrands = []
while (offset < ftypLength) {
compatibleBrands.push(file.getString(offset, 4))
offset += 4
}
return compatibleBrands.includes(this.type)
}
async parse() {
let nextBoxOffset = this.file.getUint32(0)
let meta = this.parseBoxHead(nextBoxOffset)
while (meta.kind !== 'meta') {
nextBoxOffset += meta.length
await this.file.ensureChunk(nextBoxOffset, boxHeaderLength)
meta = this.parseBoxHead(nextBoxOffset)
}
await this.file.ensureChunk(meta.offset, meta.length)
this.parseBoxFullHead(meta)
this.parseSubBoxes(meta)
//await this.findThumb(meta)
if (this.options.icc.enabled) await this.findIcc(meta)
if (this.options.tiff.enabled) await this.findExif(meta)
}
async registerSegment(key, offset, length) {
await this.file.ensureChunk(offset, length)
let chunk = this.file.subarray(offset, length)
this.createParser(key, chunk)
}
/*
async findThumb(meta) {
let iref = this.findBox(meta, 'iref')
if (iref === undefined) return
this.parseBoxFullHead(iref)
let thmb = this.findBox(iref, 'thmb')
if (thmb === undefined) return
let thumbLocId = this.file.getUint16(thmb.offset + 8)
let iloc = this.findBox(meta, 'iloc')
if (iloc === undefined) return
let extent = this.findExtentInIloc(iloc, thumbLocId)
if (extent === undefined) return
let [thumbOffset, thumbLength] = extent
console.log('thumbOffset', thumbOffset)
console.log('thumbLength', thumbLength)
await this.file.ensureChunk(thumbOffset, thumbLength)
let chunk = this.file.subarray(thumbOffset, thumbLength)
return chunk.toUint8()
}
*/
async findIcc(meta) {
let iprp = this.findBox(meta, 'iprp')
if (iprp === undefined) return
let ipco = this.findBox(iprp, 'ipco')
if (ipco === undefined) return
let colr = this.findBox(ipco, 'colr')
if (colr === undefined) return
await this.registerSegment('icc', colr.offset + 12, colr.length)
}
async findExif(meta) {
let iinf = this.findBox(meta, 'iinf')
if (iinf === undefined) return
let iloc = this.findBox(meta, 'iloc')
if (iloc === undefined) return
let exifLocId = this.findExifLocIdInIinf(iinf)
let extent = this.findExtentInIloc(iloc, exifLocId)
if (extent === undefined) return
let [exifOffset, exifLength] = extent
await this.file.ensureChunk(exifOffset, exifLength)
let nameSize = this.file.getUint32(exifOffset)
//let name = this.file.getString(exifOffset + 4, nameSize)
let extentContentShift = 4 + nameSize
exifOffset += extentContentShift
exifLength -= extentContentShift
await this.registerSegment('tiff', exifOffset, exifLength)
}
findExifLocIdInIinf(box) {
this.parseBoxFullHead(box)
let offset = box.start
let count = this.file.getUint16(offset)
let infe, infeOffset, idSize, name
offset += 2
while (count--) {
infe = this.parseBoxHead(offset)
this.parseBoxFullHead(infe)
infeOffset = infe.start
if (infe.version >= 2) {
idSize = infe.version === 3 ? 4 : 2
name = this.file.getString(infeOffset + idSize + 2, 4)
if (name === 'Exif')
return this.file.getUintBytes(infeOffset, idSize)
}
offset += infe.length
}
}
get8bits(offset) {
let n = this.file.getUint8(offset)
let n0 = n >> 4
let n1 = n & 0xf
return [n0, n1]
}
findExtentInIloc(box, wantedLocId) {
this.parseBoxFullHead(box)
let offset = box.start
let [offsetSize, lengthSize] = this.get8bits(offset++)
let [baseOffsetSize, indexSize] = this.get8bits(offset++)
let itemIdSize = box.version === 2 ? 4 : 2
let constMethodSize = box.version === 1 || box.version === 2 ? 2 : 0
let extentSize = indexSize + offsetSize + lengthSize
let itemCountSize = box.version === 2 ? 4 : 2
let itemCount = this.file.getUintBytes(offset, itemCountSize)
offset += itemCountSize
while (itemCount--) {
let itemId = this.file.getUintBytes(offset, itemIdSize)
offset += itemIdSize + constMethodSize + 2 + baseOffsetSize // itemId + construction_method + data_reference_index + base_offset
let extentCount = this.file.getUint16(offset)
offset += 2
if (itemId === wantedLocId) {
if (extentCount > 1) {
console.warn(`ILOC box has more than one extent but we're only processing one\nPlease create an issue at https://github.com/MikeKovarik/exifr with this file`)
// iloc contains items array, each of which contains extents.
// theres usually only one extent in each item but if there's more
}
return [
// extent offset
this.file.getUintBytes(offset + indexSize, offsetSize),
// extent length
this.file.getUintBytes(offset + indexSize + offsetSize, lengthSize),
]
}
offset += extentCount * extentSize
}
}
}
export class HeicFileParser extends HeifFileParser {
static type = 'heic'
}
export class AvifFileParser extends HeifFileParser {
static type = 'avif'
}
fileParsers.set('heic', HeicFileParser)
fileParsers.set('avif', AvifFileParser)