1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 17:49:16 +02:00

PngParser: Add support for cHRM chunk

This commit is contained in:
Jeff Schiller 2024-01-17 00:03:18 -08:00
parent f1efe8cd0d
commit 8694b6cad8
4 changed files with 82 additions and 12 deletions

View file

@ -1,6 +1,6 @@
General-purpose, event-based parsers for digital images. General-purpose, event-based parsers for digital images.
Currently only supports GIF and JPEG. Currently supports GIF, JPEG, and PNG.
Some nice implementations of Exif parsing for PNG, HEIF, TIFF here: Some nice implementations of Exif parsing for PNG, HEIF, TIFF here:
https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers

View file

@ -11,10 +11,10 @@
import * as fs from 'node:fs'; // TODO: Remove. import * as fs from 'node:fs'; // TODO: Remove.
import { ByteStream } from '../../io/bytestream.js'; import { ByteStream } from '../../io/bytestream.js';
// https://www.w3.org/TR/2003/REC-PNG-20031110 // https://www.w3.org/TR/png-3/
// https://en.wikipedia.org/wiki/PNG#File_format // https://en.wikipedia.org/wiki/PNG#File_format
// TODO: Ancillary chunks bKGD, cHRM, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt. // TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt.
// let DEBUG = true; // let DEBUG = true;
let DEBUG = false; let DEBUG = false;
@ -25,6 +25,7 @@ export const PngParseEventType = {
IHDR: 'image_header', IHDR: 'image_header',
gAMA: 'image_gamma', gAMA: 'image_gamma',
sBIT: 'significant_bits', sBIT: 'significant_bits',
cHRM: 'chromaticities_white_point',
PLTE: 'palette', PLTE: 'palette',
tRNS: 'transparency', tRNS: 'transparency',
IDAT: 'image_data', IDAT: 'image_data',
@ -92,6 +93,27 @@ export class PngSignificantBitsEvent extends Event {
} }
} }
/**
* @typedef PngChromaticies
* @property {number} whitePointX
* @property {number} whitePointY
* @property {number} redX
* @property {number} redY
* @property {number} greenX
* @property {number} greenY
* @property {number} blueX
* @property {number} blueY
*/
export class PngChromaticitiesEvent extends Event {
/** @param {PngChromaticies} chromaticities */
constructor(chromaticities) {
super(PngParseEventType.cHRM);
/** @type {PngChromaticies} */
this.chromaticities = chromaticities;
}
}
/** /**
* @typedef PngColor * @typedef PngColor
* @property {number} red * @property {number} red
@ -105,7 +127,7 @@ export class PngSignificantBitsEvent extends Event {
*/ */
export class PngPaletteEvent extends Event { export class PngPaletteEvent extends Event {
/** @param {PngPalette} */ /** @param {PngPalette} palette */
constructor(palette) { constructor(palette) {
super(PngParseEventType.PLTE); super(PngParseEventType.PLTE);
/** @type {PngPalette} */ /** @type {PngPalette} */
@ -123,7 +145,7 @@ export class PngPaletteEvent extends Event {
*/ */
export class PngTransparencyEvent extends Event { export class PngTransparencyEvent extends Event {
/** @param {PngTransparency} */ /** @param {PngTransparency} transparency */
constructor(transparency) { constructor(transparency) {
super(PngParseEventType.tRNS); super(PngParseEventType.tRNS);
/** @type {PngTransparency} */ /** @type {PngTransparency} */
@ -137,7 +159,7 @@ export class PngTransparencyEvent extends Event {
*/ */
export class PngImageDataEvent extends Event { export class PngImageDataEvent extends Event {
/** @param {PngImageData} */ /** @param {PngImageData} data */
constructor(data) { constructor(data) {
super(PngParseEventType.IDAT); super(PngParseEventType.IDAT);
/** @type {PngImageData} */ /** @type {PngImageData} */
@ -210,6 +232,16 @@ export class PngParser extends EventTarget {
return this; return this;
} }
/**
* Type-safe way to bind a listener for a PngChromaticiesEvent.
* @param {function(PngChromaticiesEvent): void} listener
* @returns {PngParser} for chaining
*/
onChromaticities(listener) {
super.addEventListener(PngParseEventType.cHRM, listener);
return this;
}
/** /**
* Type-safe way to bind a listener for a PngPaletteEvent. * Type-safe way to bind a listener for a PngPaletteEvent.
* @param {function(PngPaletteEvent): void} listener * @param {function(PngPaletteEvent): void} listener
@ -261,7 +293,7 @@ export class PngParser extends EventTarget {
const chStream = chunk.chunkStream; const chStream = chunk.chunkStream;
switch (chunk.chunkType) { switch (chunk.chunkType) {
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR // https://www.w3.org/TR/png-3/#11IHDR
case 'IHDR': case 'IHDR':
if (this.colorType) throw `Found multiple IHDR chunks`; if (this.colorType) throw `Found multiple IHDR chunks`;
/** @type {PngImageHeader} */ /** @type {PngImageHeader} */
@ -292,13 +324,13 @@ export class PngParser extends EventTarget {
this.dispatchEvent(new PngImageHeaderEvent(header)); this.dispatchEvent(new PngImageHeaderEvent(header));
break; break;
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11gAMA // https://www.w3.org/TR/png-3/#11gAMA
case 'gAMA': case 'gAMA':
if (length !== 4) throw `Bad length for gAMA: ${length}`; if (length !== 4) throw `Bad length for gAMA: ${length}`;
this.dispatchEvent(new PngImageGammaEvent(chStream.readNumber(4))); this.dispatchEvent(new PngImageGammaEvent(chStream.readNumber(4)));
break; break;
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11sBIT // https://www.w3.org/TR/png-3/#11sBIT
case 'sBIT': case 'sBIT':
if (this.colorType === undefined) throw `sBIT before IHDR`; if (this.colorType === undefined) throw `sBIT before IHDR`;
/** @type {PngSignificantBits} */ /** @type {PngSignificantBits} */
@ -329,7 +361,25 @@ export class PngParser extends EventTarget {
this.dispatchEvent(new PngSignificantBitsEvent(sigBits)); this.dispatchEvent(new PngSignificantBitsEvent(sigBits));
break; break;
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11PLTE // https://www.w3.org/TR/png-3/#11cHRM
case 'cHRM':
if (length !== 32) throw `Weird length for cHRM chunk: ${length}`;
/** @type {PngChromaticies} */
const chromaticities = {
whitePointX: chStream.readNumber(4),
whitePointY: chStream.readNumber(4),
redX: chStream.readNumber(4),
redY: chStream.readNumber(4),
greenX: chStream.readNumber(4),
greenY: chStream.readNumber(4),
blueX: chStream.readNumber(4),
blueY: chStream.readNumber(4),
};
this.dispatchEvent(new PngChromaticitiesEvent(chromaticities));
break;
// https://www.w3.org/TR/png-3/#11PLTE
case 'PLTE': case 'PLTE':
if (this.colorType === undefined) throw `PLTE before IHDR`; if (this.colorType === undefined) throw `PLTE before IHDR`;
if (this.colorType === PngColorType.GREYSCALE || if (this.colorType === PngColorType.GREYSCALE ||
@ -354,7 +404,7 @@ export class PngParser extends EventTarget {
this.dispatchEvent(new PngPaletteEvent(this.palette)); this.dispatchEvent(new PngPaletteEvent(this.palette));
break; break;
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11tRNS // https://www.w3.org/TR/png-3/#11tRNS
case 'tRNS': case 'tRNS':
if (this.colorType === undefined) throw `tRNS before IHDR`; if (this.colorType === undefined) throw `tRNS before IHDR`;
if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA || if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA ||
@ -388,7 +438,7 @@ export class PngParser extends EventTarget {
this.dispatchEvent(new PngTransparencyEvent(transparency)); this.dispatchEvent(new PngTransparencyEvent(transparency));
break; break;
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT // https://www.w3.org/TR/png-3/#11IDAT
case 'IDAT': case 'IDAT':
/** @type {PngImageData} */ /** @type {PngImageData} */
const data = { const data = {
@ -445,6 +495,9 @@ async function main() {
parser.onSignificantBits(evt => { parser.onSignificantBits(evt => {
// console.dir(evt.sigBits); // console.dir(evt.sigBits);
}); });
parser.onChromaticities(evt => {
// console.dir(evt.chromaticities);
});
parser.onPalette(evt => { parser.onPalette(evt => {
// console.dir(evt.palette); // console.dir(evt.palette);
}); });

View file

@ -3,6 +3,7 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js'; import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js';
/** @typedef {import('../image/parsers/png.js').PngChromaticies} PngChromaticies */
/** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */ /** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */
/** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */ /** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */
/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ /** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */
@ -72,6 +73,22 @@ describe('bitjs.image.parsers.PngParser', () => {
expect(sBits.significant_alpha).equals(undefined); expect(sBits.significant_alpha).equals(undefined);
}); });
it('extracts cHRM', async () => {
/** @type {PngChromaticies} */
let chromaticities;
await getPngParser('tests/image-testfiles/ccwn2c08.png')
.onChromaticities(evt => chromaticities = evt.chromaticities)
.start();
expect(chromaticities.whitePointX).equals(31270);
expect(chromaticities.whitePointY).equals(32900);
expect(chromaticities.redX).equals(64000);
expect(chromaticities.redY).equals(33000);
expect(chromaticities.greenX).equals(30000);
expect(chromaticities.greenY).equals(60000);
expect(chromaticities.blueX).equals(15000);
expect(chromaticities.blueY).equals(6000);
});
it('extracts PLTE', async () => { it('extracts PLTE', async () => {
/** @type {PngPalette} */ /** @type {PngPalette} */
let palette; let palette;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB