Skip to content

Commit 8694b6c

Browse files
committed
PngParser: Add support for cHRM chunk
1 parent f1efe8c commit 8694b6c

File tree

4 files changed

+82
-12
lines changed

4 files changed

+82
-12
lines changed

image/parsers/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
General-purpose, event-based parsers for digital images.
22

3-
Currently only supports GIF and JPEG.
3+
Currently supports GIF, JPEG, and PNG.
44

55
Some nice implementations of Exif parsing for PNG, HEIF, TIFF here:
66
https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers

image/parsers/png.js

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
import * as fs from 'node:fs'; // TODO: Remove.
1212
import { ByteStream } from '../../io/bytestream.js';
1313

14-
// https://www.w3.org/TR/2003/REC-PNG-20031110
14+
// https://www.w3.org/TR/png-3/
1515
// https://en.wikipedia.org/wiki/PNG#File_format
1616

17-
// TODO: Ancillary chunks bKGD, cHRM, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt.
17+
// TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt.
1818

1919
// let DEBUG = true;
2020
let DEBUG = false;
@@ -25,6 +25,7 @@ export const PngParseEventType = {
2525
IHDR: 'image_header',
2626
gAMA: 'image_gamma',
2727
sBIT: 'significant_bits',
28+
cHRM: 'chromaticities_white_point',
2829
PLTE: 'palette',
2930
tRNS: 'transparency',
3031
IDAT: 'image_data',
@@ -92,6 +93,27 @@ export class PngSignificantBitsEvent extends Event {
9293
}
9394
}
9495

96+
/**
97+
* @typedef PngChromaticies
98+
* @property {number} whitePointX
99+
* @property {number} whitePointY
100+
* @property {number} redX
101+
* @property {number} redY
102+
* @property {number} greenX
103+
* @property {number} greenY
104+
* @property {number} blueX
105+
* @property {number} blueY
106+
*/
107+
108+
export class PngChromaticitiesEvent extends Event {
109+
/** @param {PngChromaticies} chromaticities */
110+
constructor(chromaticities) {
111+
super(PngParseEventType.cHRM);
112+
/** @type {PngChromaticies} */
113+
this.chromaticities = chromaticities;
114+
}
115+
}
116+
95117
/**
96118
* @typedef PngColor
97119
* @property {number} red
@@ -105,7 +127,7 @@ export class PngSignificantBitsEvent extends Event {
105127
*/
106128

107129
export class PngPaletteEvent extends Event {
108-
/** @param {PngPalette} */
130+
/** @param {PngPalette} palette */
109131
constructor(palette) {
110132
super(PngParseEventType.PLTE);
111133
/** @type {PngPalette} */
@@ -123,7 +145,7 @@ export class PngPaletteEvent extends Event {
123145
*/
124146

125147
export class PngTransparencyEvent extends Event {
126-
/** @param {PngTransparency} */
148+
/** @param {PngTransparency} transparency */
127149
constructor(transparency) {
128150
super(PngParseEventType.tRNS);
129151
/** @type {PngTransparency} */
@@ -137,7 +159,7 @@ export class PngTransparencyEvent extends Event {
137159
*/
138160

139161
export class PngImageDataEvent extends Event {
140-
/** @param {PngImageData} */
162+
/** @param {PngImageData} data */
141163
constructor(data) {
142164
super(PngParseEventType.IDAT);
143165
/** @type {PngImageData} */
@@ -210,6 +232,16 @@ export class PngParser extends EventTarget {
210232
return this;
211233
}
212234

235+
/**
236+
* Type-safe way to bind a listener for a PngChromaticiesEvent.
237+
* @param {function(PngChromaticiesEvent): void} listener
238+
* @returns {PngParser} for chaining
239+
*/
240+
onChromaticities(listener) {
241+
super.addEventListener(PngParseEventType.cHRM, listener);
242+
return this;
243+
}
244+
213245
/**
214246
* Type-safe way to bind a listener for a PngPaletteEvent.
215247
* @param {function(PngPaletteEvent): void} listener
@@ -261,7 +293,7 @@ export class PngParser extends EventTarget {
261293

262294
const chStream = chunk.chunkStream;
263295
switch (chunk.chunkType) {
264-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR
296+
// https://www.w3.org/TR/png-3/#11IHDR
265297
case 'IHDR':
266298
if (this.colorType) throw `Found multiple IHDR chunks`;
267299
/** @type {PngImageHeader} */
@@ -292,13 +324,13 @@ export class PngParser extends EventTarget {
292324
this.dispatchEvent(new PngImageHeaderEvent(header));
293325
break;
294326

295-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11gAMA
327+
// https://www.w3.org/TR/png-3/#11gAMA
296328
case 'gAMA':
297329
if (length !== 4) throw `Bad length for gAMA: ${length}`;
298330
this.dispatchEvent(new PngImageGammaEvent(chStream.readNumber(4)));
299331
break;
300332

301-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11sBIT
333+
// https://www.w3.org/TR/png-3/#11sBIT
302334
case 'sBIT':
303335
if (this.colorType === undefined) throw `sBIT before IHDR`;
304336
/** @type {PngSignificantBits} */
@@ -329,7 +361,25 @@ export class PngParser extends EventTarget {
329361
this.dispatchEvent(new PngSignificantBitsEvent(sigBits));
330362
break;
331363

332-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11PLTE
364+
// https://www.w3.org/TR/png-3/#11cHRM
365+
case 'cHRM':
366+
if (length !== 32) throw `Weird length for cHRM chunk: ${length}`;
367+
368+
/** @type {PngChromaticies} */
369+
const chromaticities = {
370+
whitePointX: chStream.readNumber(4),
371+
whitePointY: chStream.readNumber(4),
372+
redX: chStream.readNumber(4),
373+
redY: chStream.readNumber(4),
374+
greenX: chStream.readNumber(4),
375+
greenY: chStream.readNumber(4),
376+
blueX: chStream.readNumber(4),
377+
blueY: chStream.readNumber(4),
378+
};
379+
this.dispatchEvent(new PngChromaticitiesEvent(chromaticities));
380+
break;
381+
382+
// https://www.w3.org/TR/png-3/#11PLTE
333383
case 'PLTE':
334384
if (this.colorType === undefined) throw `PLTE before IHDR`;
335385
if (this.colorType === PngColorType.GREYSCALE ||
@@ -354,7 +404,7 @@ export class PngParser extends EventTarget {
354404
this.dispatchEvent(new PngPaletteEvent(this.palette));
355405
break;
356406

357-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11tRNS
407+
// https://www.w3.org/TR/png-3/#11tRNS
358408
case 'tRNS':
359409
if (this.colorType === undefined) throw `tRNS before IHDR`;
360410
if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA ||
@@ -388,7 +438,7 @@ export class PngParser extends EventTarget {
388438
this.dispatchEvent(new PngTransparencyEvent(transparency));
389439
break;
390440

391-
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT
441+
// https://www.w3.org/TR/png-3/#11IDAT
392442
case 'IDAT':
393443
/** @type {PngImageData} */
394444
const data = {
@@ -445,6 +495,9 @@ async function main() {
445495
parser.onSignificantBits(evt => {
446496
// console.dir(evt.sigBits);
447497
});
498+
parser.onChromaticities(evt => {
499+
// console.dir(evt.chromaticities);
500+
});
448501
parser.onPalette(evt => {
449502
// console.dir(evt.palette);
450503
});

tests/image-parsers-png.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'mocha';
33
import { expect } from 'chai';
44
import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js';
55

6+
/** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */
67
/** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */
78
/** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */
89
/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */
@@ -72,6 +73,22 @@ describe('bitjs.image.parsers.PngParser', () => {
7273
expect(sBits.significant_alpha).equals(undefined);
7374
});
7475

76+
it('extracts cHRM', async () => {
77+
/** @type {PngChromaticies} */
78+
let chromaticities;
79+
await getPngParser('tests/image-testfiles/ccwn2c08.png')
80+
.onChromaticities(evt => chromaticities = evt.chromaticities)
81+
.start();
82+
expect(chromaticities.whitePointX).equals(31270);
83+
expect(chromaticities.whitePointY).equals(32900);
84+
expect(chromaticities.redX).equals(64000);
85+
expect(chromaticities.redY).equals(33000);
86+
expect(chromaticities.greenX).equals(30000);
87+
expect(chromaticities.greenY).equals(60000);
88+
expect(chromaticities.blueX).equals(15000);
89+
expect(chromaticities.blueY).equals(6000);
90+
});
91+
7592
it('extracts PLTE', async () => {
7693
/** @type {PngPalette} */
7794
let palette;

tests/image-testfiles/ccwn2c08.png

1.48 KB
Loading

0 commit comments

Comments
 (0)