diff --git a/image/parsers/png.js b/image/parsers/png.js index 453b398..2e040da 100644 --- a/image/parsers/png.js +++ b/image/parsers/png.js @@ -22,6 +22,7 @@ const SIG = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); export const PngParseEventType = { IHDR: 'image_header', gAMA: 'image_gamma', + sBIT: 'significant_bits', PLTE: 'palette', IDAT: 'image_data', }; @@ -70,6 +71,24 @@ export class PngImageGammaEvent extends Event { } } +/** + * @typedef PngSignificantBits + * @property {number=} significant_greyscale Populated for color types 0, 4. + * @property {number=} significant_red Populated for color types 2, 3, 6. + * @property {number=} significant_green Populated for color types 2, 3, 6. + * @property {number=} significant_blue Populated for color types 2, 3, 6. + * @property {number=} significant_alpha Populated for color types 4, 6. + */ + +export class PngSignificantBitsEvent extends Event { + /** @param {PngSignificantBits} */ + constructor(sigBits) { + super(PngParseEventType.sBIT); + /** @type {PngSignificantBits} */ + this.sigBits = sigBits; + } +} + /** * @typedef PngColor * @property {number} red @@ -153,6 +172,16 @@ export class PngParser extends EventTarget { return this; } + /** + * Type-safe way to bind a listener for a PngSignificantBitsEvent. + * @param {function(PngSignificantBitsEvent): void} listener + * @returns {PngParser} for chaining + */ + onSignificantBits(listener) { + super.addEventListener(PngParseEventType.sBIT, listener); + return this; + } + /** * Type-safe way to bind a listener for a PngPaletteEvent. * @param {function(PngPaletteEvent): void} listener @@ -231,6 +260,37 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngImageGammaEvent(chStream.readNumber(4))); break; + // https://www.w3.org/TR/2003/REC-PNG-20031110/#11sBIT + case 'sBIT': + if (this.colorType === undefined) throw `sBIT before IHDR`; + /** @type {PngSignificantBits} */ + const sigBits = {}; + + const badLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`; + if (this.colorType === PngColorType.GREYSCALE) { + if (length !== 1) throw badLengthErr; + sigBits.significant_greyscale = chStream.readNumber(1); + } else if (this.colorType === PngColorType.TRUE_COLOR || + this.colorType === PngColorType.INDEXED_COLOR) { + if (length !== 3) throw badLengthErr; + sigBits.significant_red = chStream.readNumber(1); + sigBits.significant_green = chStream.readNumber(1); + sigBits.significant_blue = chStream.readNumber(1); + } else if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA) { + if (length !== 2) throw badLengthErr; + sigBits.significant_greyscale = chStream.readNumber(1); + sigBits.significant_alpha = chStream.readNumber(1); + } else if (this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) { + if (length !== 4) throw badLengthErr; + sigBits.significant_red = chStream.readNumber(1); + sigBits.significant_green = chStream.readNumber(1); + sigBits.significant_blue = chStream.readNumber(1); + sigBits.significant_alpha = chStream.readNumber(1); + } + + this.dispatchEvent(new PngSignificantBitsEvent(sigBits)); + break; + // https://www.w3.org/TR/2003/REC-PNG-20031110/#11PLTE case 'PLTE': if (this.colorType === undefined) throw `PLTE before IHDR`; @@ -300,8 +360,6 @@ basn0g02.png bgbn4a08.png cs5n3p08.png f03n0g08.png g10n2c08.png ps2n2c16.png s0 async function main() { for (const fileName of FILES) { - if (!fileName.includes('3p')) continue; - console.log(`file: ${fileName}`); const nodeBuf = fs.readFileSync(fileName); const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length); @@ -312,6 +370,9 @@ async function main() { parser.onGamma(evt => { // console.dir(evt.imageGamma); }); + parser.onSignificantBits(evt => { + console.dir(evt.sigBits); + }); parser.onPalette(evt => { // console.dir(evt.palette); }); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index ca2cd55..9a21fc3 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -3,10 +3,11 @@ import 'mocha'; import { expect } from 'chai'; import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/png.js'; -/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ /** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */ /** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */ +/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ /** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */ +/** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */ function getPngParser(fileName) { const nodeBuf = fs.readFileSync(fileName); @@ -57,6 +58,19 @@ describe('bitjs.image.parsers.PngParser', () => { expect(gamma).equals(55000); }); + it('extracts sBIT', async () => { + /** @type {PngSignificantBits} */ + let sBits; + await getPngParser('tests/image-testfiles/cs3n2c16.png') + .onSignificantBits(evt => sBits = evt.sigBits) + .start(); + expect(sBits.significant_red).equals(13); + expect(sBits.significant_green).equals(13); + expect(sBits.significant_blue).equals(13); + expect(sBits.significant_greyscale).equals(undefined); + expect(sBits.significant_alpha).equals(undefined); + }); + it('extracts PLTE', async () => { /** @type {PngPalette} */ let palette; diff --git a/tests/image-testfiles/cs3n2c16.png b/tests/image-testfiles/cs3n2c16.png new file mode 100644 index 0000000..bf5fd20 Binary files /dev/null and b/tests/image-testfiles/cs3n2c16.png differ