-
Notifications
You must be signed in to change notification settings - Fork 70
/
png.mjs
139 lines (117 loc) · 4.57 KB
/
png.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
import {FileParserBase} from '../parser.mjs'
import {fileParsers, segmentParsers} from '../plugins.mjs'
import * as platform from '../util/platform.mjs'
import dynamicImport from '../util/import.mjs'
let zlibPromise = dynamicImport('zlib')
// https://dev.exiv2.org/projects/exiv2/wiki/The_Metadata_in_PNG_files
// http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
const PNG_MAGIC_BYTES = '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a'
const PNG_XMP_PREFIX = 'XML:com.adobe.xmp'
const LENGTH_SIZE = 4
const TYPE_SIZE = 4
const CRC_SIZE = 4
const IHDR = 'ihdr'
const ICCP = 'iccp'
const TEXT = 'text'
const ITXT = 'itxt'
const EXIF = 'exif' // eXIf
const pngMetaChunks = [IHDR, ICCP, TEXT, ITXT, EXIF]
export class PngFileParser extends FileParserBase {
static type = 'png'
static canHandle(file, firstTwoBytes) {
return firstTwoBytes === 0x8950
&& file.getUint32(0) === 0x89504e47
&& file.getUint32(4) === 0x0d0a1a0a
}
async parse() {
let {file} = this
await this.findPngChunksInRange(PNG_MAGIC_BYTES.length, file.byteLength)
await this.readSegments(this.metaChunks)
this.findIhdr()
this.parseTextChunks()
await this.findExif().catch(this.catchError)
await this.findXmp().catch(this.catchError)
await this.findIcc().catch(this.catchError)
}
catchError = err => this.errors.push(err)
metaChunks = []
unknownChunks = []
async findPngChunksInRange(offset, end) {
let {file} = this
while (offset < end) {
let size = file.getUint32(offset) // size without crc
let marker = file.getUint32(offset + LENGTH_SIZE)
let name = file.getString(offset + LENGTH_SIZE, 4)
let type = name.toLowerCase()
let start = offset + LENGTH_SIZE + TYPE_SIZE
let length = size + LENGTH_SIZE + TYPE_SIZE + CRC_SIZE
let seg = {type, offset, length, start, size, marker}
if (pngMetaChunks.includes(type))
this.metaChunks.push(seg)
else
this.unknownChunks.push(seg)
offset += length
}
}
// PNG additionally stores simple string key:value pairs each in separate tEXt chunks.
// There can be many of them, the format is simple enough to not mandate custom segment-parser class.
// For simplicity's and performance's sake. And these chunks do not specifically belong (like for example into iptc, exif, etc...)
// So we're just parse it all here and merge the output into ihdr (rest of PNG header data).
parseTextChunks() {
let textChunks = this.metaChunks.filter(info => info.type === TEXT)
for (let seg of textChunks) {
let [key, val] = this.file.getString(seg.start, seg.size).split('\0')
this.injectKeyValToIhdr(key, val)
}
}
injectKeyValToIhdr(key, val) {
let parser = this.parsers.ihdr
if (parser) parser.raw.set(key, val)
}
findIhdr() {
let seg = this.metaChunks.find(seg => seg.type === IHDR)
if (!seg) return
// ihdr option is undefined by default (because we don't want jpegs and heic files to pick it up)
// so here we create it for every png file. But only if user didn't explicitly disabled it.
if (this.options[IHDR].enabled !== false)
this.createParser(IHDR, seg.chunk)
}
async findExif() {
let seg = this.metaChunks.find(info => info.type === 'exif')
if (!seg) return
this.injectSegment('tiff', seg.chunk)
}
// http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.iTXt
// iTXt chunk header is slightly complicated. It contains multiple null-terminator-separated info.
// The XMP data is present after third null-terminator.
async findXmp() {
let itxtChunks = this.metaChunks.filter(info => info.type === ITXT)
for (let seg of itxtChunks) {
let prefix = seg.chunk.getString(0, PNG_XMP_PREFIX.length)
if (prefix === PNG_XMP_PREFIX)
this.injectSegment('xmp', seg.chunk)
}
}
async findIcc() {
let seg = this.metaChunks.find(info => info.type === ICCP)
if (!seg) return
let {chunk} = seg
let chunkHead = chunk.getUint8Array(0, 81)
// icc profile has variable length (up to 80B) followed by null terminator.
let nameLength = 0
// Get length of the profile name by looking for the null terminator.
while (nameLength < 80 && chunkHead[nameLength] !== 0) nameLength++
// Recalculate actual ICC data position.
let iccpHeaderLength = nameLength + 2 // 1 byte null terminator, + 1 byte compression
let profileName = chunk.getString(0, nameLength)
this.injectKeyValToIhdr('ProfileName', profileName)
// ICC data is zlib compressed by default. Spec doesn't even allow raw data.
if (platform.node) {
let zlib = await zlibPromise
let dataChunk = chunk.getUint8Array(iccpHeaderLength)
dataChunk = zlib.inflateSync(dataChunk)
this.injectSegment('icc', dataChunk)
}
}
}
fileParsers.set('png', PngFileParser)