mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 09:39:16 +02:00
PngParser: Add support for cHRM chunk
This commit is contained in:
parent
f1efe8cd0d
commit
8694b6cad8
4 changed files with 82 additions and 12 deletions
|
@ -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
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
BIN
tests/image-testfiles/ccwn2c08.png
Normal file
BIN
tests/image-testfiles/ccwn2c08.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
Loading…
Add table
Add a link
Reference in a new issue