diff --git a/image/parsers/png.js b/image/parsers/png.js index e43dd4c..ca9d20c 100644 --- a/image/parsers/png.js +++ b/image/parsers/png.js @@ -14,7 +14,7 @@ import { ByteStream } from '../../io/bytestream.js'; // https://www.w3.org/TR/png-3/ // https://en.wikipedia.org/wiki/PNG#File_format -// TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tIME, zTXt. +// TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tIME. // let DEBUG = true; let DEBUG = false; @@ -33,6 +33,7 @@ export const PngParseEventType = { sBIT: 'significant_bits', tEXt: 'textual_data', tRNS: 'transparency', + zTXt: 'compressed_textual_data', }; /** @enum {number} */ @@ -186,6 +187,22 @@ export class PngTextualDataEvent extends Event { } } +/** + * @typedef PngCompressedTextualData + * @property {string} keyword + * @property {number} compressionMethod Only value supported is 0 for deflate compression. + * @property {Uint8Array=} compressedText + */ + +export class PngCompressedTextualDataEvent extends Event { + /** @param {PngCompressedTextualData} compressedTextualData */ + constructor(compressedTextualData) { + super(PngParseEventType.zTXt); + /** @type {PngCompressedTextualData} */ + this.compressedTextualData = compressedTextualData; + } +} + /** * @typedef PngChunk Internal use only. * @property {number} length @@ -231,6 +248,16 @@ export class PngParser extends EventTarget { return this; } + /** + * Type-safe way to bind a listener for a PngCompressedTextualDataEvent. + * @param {function(PngCompressedTextualDataEvent): void} listener + * @returns {PngParser} for chaining + */ + onCompressedTextualData(listener) { + super.addEventListener(PngParseEventType.zTXt, listener); + return this; + } + /** * Type-safe way to bind a listener for a PngImageGammaEvent. * @param {function(PngImageGammaEvent): void} listener @@ -479,6 +506,20 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngTransparencyEvent(transparency)); break; + // https://www.w3.org/TR/png-3/#11zTXt + case 'zTXt': + const compressedByteArr = chStream.peekBytes(length); + const compressedNullIndex = compressedByteArr.indexOf(0); + + /** @type {PngCompressedTextualData} */ + const compressedTextualData = { + keyword: chStream.readString(compressedNullIndex), + compressionMethod: chStream.skip(1).readNumber(1), + compressedText: chStream.readBytes(length - compressedNullIndex - 2), + }; + this.dispatchEvent(new PngCompressedTextualDataEvent(compressedTextualData)); + break; + // https://www.w3.org/TR/png-3/#11IDAT case 'IDAT': /** @type {PngImageData} */ @@ -551,6 +592,9 @@ async function main() { parser.onTextualData(evt => { // console.dir(evt.textualData); }); + parser.onCompressedTextualData(evt => { + // console.dir(evt.compressedTextualData); + }); try { await parser.start(); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index 508ae66..5e98ae4 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -4,6 +4,7 @@ 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').PngCompressedTextualData} PngCompressedTextualData */ /** @typedef {import('../image/parsers/png.js').PngImageData} PngImageData */ /** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */ /** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ @@ -166,4 +167,17 @@ describe('bitjs.image.parsers.PngParser', () => { expect(textualDataArr[1].keyword).equals('Author'); expect(textualDataArr[1].textString).equals('Willem A.J. van Schaik\n(willem@schaik.com)'); }); + + it('extracts zTXt', async () => { + /** @type {PngCompressedTextualData} */ + let data; + + await getPngParser('tests/image-testfiles/ctzn0g04.png') + .onCompressedTextualData(evt => { data = evt.compressedTextualData }) + .start(); + + expect(data.keyword).equals('Disclaimer'); + expect(data.compressionMethod).equals(0); + expect(data.compressedText.byteLength).equals(17); + }); });