From 8694b6cad84d2886fea81b417a4c8642c26a24f4 Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 17 Jan 2024 00:03:18 -0800 Subject: [PATCH] PngParser: Add support for cHRM chunk --- image/parsers/README.md | 2 +- image/parsers/png.js | 75 ++++++++++++++++++++++++----- tests/image-parsers-png.spec.js | 17 +++++++ tests/image-testfiles/ccwn2c08.png | Bin 0 -> 1514 bytes 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 tests/image-testfiles/ccwn2c08.png diff --git a/image/parsers/README.md b/image/parsers/README.md index 8583ca4..7ea5ff0 100644 --- a/image/parsers/README.md +++ b/image/parsers/README.md @@ -1,6 +1,6 @@ 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: https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers \ No newline at end of file diff --git a/image/parsers/png.js b/image/parsers/png.js index 62fa306..b9c326c 100644 --- a/image/parsers/png.js +++ b/image/parsers/png.js @@ -11,10 +11,10 @@ import * as fs from 'node:fs'; // TODO: Remove. 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 -// 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 = false; @@ -25,6 +25,7 @@ export const PngParseEventType = { IHDR: 'image_header', gAMA: 'image_gamma', sBIT: 'significant_bits', + cHRM: 'chromaticities_white_point', PLTE: 'palette', tRNS: 'transparency', 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 * @property {number} red @@ -105,7 +127,7 @@ export class PngSignificantBitsEvent extends Event { */ export class PngPaletteEvent extends Event { - /** @param {PngPalette} */ + /** @param {PngPalette} palette */ constructor(palette) { super(PngParseEventType.PLTE); /** @type {PngPalette} */ @@ -123,7 +145,7 @@ export class PngPaletteEvent extends Event { */ export class PngTransparencyEvent extends Event { - /** @param {PngTransparency} */ + /** @param {PngTransparency} transparency */ constructor(transparency) { super(PngParseEventType.tRNS); /** @type {PngTransparency} */ @@ -137,7 +159,7 @@ export class PngTransparencyEvent extends Event { */ export class PngImageDataEvent extends Event { - /** @param {PngImageData} */ + /** @param {PngImageData} data */ constructor(data) { super(PngParseEventType.IDAT); /** @type {PngImageData} */ @@ -210,6 +232,16 @@ export class PngParser extends EventTarget { 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. * @param {function(PngPaletteEvent): void} listener @@ -261,7 +293,7 @@ export class PngParser extends EventTarget { const chStream = chunk.chunkStream; switch (chunk.chunkType) { - // https://www.w3.org/TR/2003/REC-PNG-20031110/#11IHDR + // https://www.w3.org/TR/png-3/#11IHDR case 'IHDR': if (this.colorType) throw `Found multiple IHDR chunks`; /** @type {PngImageHeader} */ @@ -292,13 +324,13 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngImageHeaderEvent(header)); break; - // https://www.w3.org/TR/2003/REC-PNG-20031110/#11gAMA + // https://www.w3.org/TR/png-3/#11gAMA case 'gAMA': if (length !== 4) throw `Bad length for gAMA: ${length}`; this.dispatchEvent(new PngImageGammaEvent(chStream.readNumber(4))); break; - // https://www.w3.org/TR/2003/REC-PNG-20031110/#11sBIT + // https://www.w3.org/TR/png-3/#11sBIT case 'sBIT': if (this.colorType === undefined) throw `sBIT before IHDR`; /** @type {PngSignificantBits} */ @@ -329,7 +361,25 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngSignificantBitsEvent(sigBits)); 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': if (this.colorType === undefined) throw `PLTE before IHDR`; if (this.colorType === PngColorType.GREYSCALE || @@ -354,7 +404,7 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngPaletteEvent(this.palette)); break; - // https://www.w3.org/TR/2003/REC-PNG-20031110/#11tRNS + // https://www.w3.org/TR/png-3/#11tRNS case 'tRNS': if (this.colorType === undefined) throw `tRNS before IHDR`; if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA || @@ -388,7 +438,7 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngTransparencyEvent(transparency)); break; - // https://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT + // https://www.w3.org/TR/png-3/#11IDAT case 'IDAT': /** @type {PngImageData} */ const data = { @@ -445,6 +495,9 @@ async function main() { parser.onSignificantBits(evt => { // console.dir(evt.sigBits); }); + parser.onChromaticities(evt => { + // console.dir(evt.chromaticities); + }); parser.onPalette(evt => { // console.dir(evt.palette); }); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index 7c01cff..1e63c3b 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -3,6 +3,7 @@ import 'mocha'; import { expect } from 'chai'; 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').PngImageGamma} PngImageGamma */ /** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ @@ -72,6 +73,22 @@ describe('bitjs.image.parsers.PngParser', () => { 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 () => { /** @type {PngPalette} */ let palette; diff --git a/tests/image-testfiles/ccwn2c08.png b/tests/image-testfiles/ccwn2c08.png new file mode 100644 index 0000000000000000000000000000000000000000..47c24817b79c1e2df8eb3f4fe43798f249a063d6 GIT binary patch literal 1514 zcmV004R>004l5008;`004mK004C`008P>0026e000+ooVrmw000Gb zNkl1!`#jIX%gn1pggd$P9Zd(E8@>1o|NOaq`y>7F!|ZsTJzwK126G#Ly6%XM z@b-^5>QWhU%dqD1!_57RBTumVe+8~Hq9eTb8gGRJAwh?*OV}a2z_~^C|CZMU`+s$C zf=f^GMjIPqJ*EzYFsfNGT)Lap?PPm7_!7VJ=-g%pukx;sAJU$c-=j0zF+ofa5q!)a ze#6rhquT+FvGf6bpLReH((V#=343&VbR#+uVMHe)7;@qr4%Ccq1-RxEz0QX|?KUbv zg-D3?a5PfW@#w_lF{X>|p$bm?jc2~d`wI6~f#a;6WITuOqXKjXI#{2ULDi%_otUzR z>7gTt5je5J^Sd~Dle2ye@C#0R_#V2AZlgm~2OH2hsG43tH)a~4d#D*O2R?ta&G)tW z>8Ar@roZFTCSclIO*Ag1ArI+AG!Z7+1hzr17QOyU)Mwc7+NS`==$DuP*T?vqO>|IQ ztcPnUI`m>BLib<}*@i@om~~+l){c| zVM7=SBVi=G`3mjN^Uzb*0scX@ir!A!MZ4%0+DG}A03D)xh@t5bN9Y)dtvG|t!e{A2 zVJr-VH(rqM{a!t^`;)+*NnHXL-9oj{tt~?bm>$Z7h&)1VrgJrYXc=0@ma%1G8KuoD z&+_AUoChDj1~`crIu6P~xu_ZAq5Z4pRfm`!Wn^(jTpi2MGMYsT(bw1_m z-{DWcW?RoW2Sx1%q6`?p5#3d|Bp`vxK|FMT+rjJ7PN`Fx6q_I^ zHsSO@o_kdt`-!tWMURz18H)xjXp0u5EYvg!dKD%@(vVc71xZelk*1^>Sw>ormt<42 zhFmB#g+t{FiXi4}-?><#$4U#@DlI4r4PvoVssT2+X;jvbP04Gr zio7B#DN2fhGApO!Ash15FNBNxItO+uXX|?4s1n(L1}xIhR7?x<%j`IhKSXD!3`rZZ zhRl*T6jO>R>MU|4RaMv=y0F$9} zR4$TPn>VItC~Z@fNxj~;XU>`>Z{Pj*Eq?Q{1ADY{!yAorm%^@LYwChBA?vgK9NhuV zEWJQVYm2(9vPpCPqMTgf?}Pc|ffx6F>Cg}2g?nz4y9M0qW3N!>6l2^z@iOrMH$#`G zQl@pW(kx$*<@%1VVyS645OM(au{s2+Lb!Cm*v-(Kz~fjhVU4fw3PM64Z& QC;$Ke07*qoM6N<$g6Hha`2YX_ literal 0 HcmV?d00001