diff --git a/README.md b/README.md index 43cbcab..55590ff 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ const mimeType = findMimeType(someArrayBuffer); ### bitjs.image This package includes code for dealing with binary images. It includes general event-based parsers -for images (GIF and JPEG only, at the moment). It also includes a module for converting WebP images +for images (GIF, JPEG, PNG). It also includes a module for converting WebP images into alternative raster graphics formats (PNG/JPG). This latter module is deprecated, now that WebP images are well-supported in all browsers. diff --git a/image/parsers/png.js b/image/parsers/png.js index b9c326c..e43dd4c 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, tEXt, tIME, zTXt. +// TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tIME, zTXt. // let DEBUG = true; let DEBUG = false; @@ -22,13 +22,17 @@ const SIG = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); /** @enum {string} */ export const PngParseEventType = { + // Critical chunks. + IDAT: 'image_data', IHDR: 'image_header', + PLTE: 'palette', + + // Ancillary chunks. + cHRM: 'chromaticities_white_point', gAMA: 'image_gamma', sBIT: 'significant_bits', - cHRM: 'chromaticities_white_point', - PLTE: 'palette', + tEXt: 'textual_data', tRNS: 'transparency', - IDAT: 'image_data', }; /** @enum {number} */ @@ -167,6 +171,21 @@ export class PngImageDataEvent extends Event { } } +/** + * @typedef PngTextualData + * @property {string} keyword + * @property {string=} textString + */ + +export class PngTextualDataEvent extends Event { + /** @param {PngTextualData} textualData */ + constructor(textualData) { + super(PngParseEventType.tEXt); + /** @type {PngTextualData} */ + this.textualData = textualData; + } +} + /** * @typedef PngChunk Internal use only. * @property {number} length @@ -203,12 +222,12 @@ export class PngParser extends EventTarget { } /** - * Type-safe way to bind a listener for a PngImageHeaderEvent. - * @param {function(PngImageHeaderEvent): void} listener + * Type-safe way to bind a listener for a PngChromaticiesEvent. + * @param {function(PngChromaticiesEvent): void} listener * @returns {PngParser} for chaining */ - onImageHeader(listener) { - super.addEventListener(PngParseEventType.IHDR, listener); + onChromaticities(listener) { + super.addEventListener(PngParseEventType.cHRM, listener); return this; } @@ -223,22 +242,22 @@ export class PngParser extends EventTarget { } /** - * Type-safe way to bind a listener for a PngSignificantBitsEvent. - * @param {function(PngSignificantBitsEvent): void} listener + * Type-safe way to bind a listener for a PngImageDataEvent. + * @param {function(PngImageDataEvent): void} listener * @returns {PngParser} for chaining */ - onSignificantBits(listener) { - super.addEventListener(PngParseEventType.sBIT, listener); + onImageData(listener) { + super.addEventListener(PngParseEventType.IDAT, listener); return this; } /** - * Type-safe way to bind a listener for a PngChromaticiesEvent. - * @param {function(PngChromaticiesEvent): void} listener + * Type-safe way to bind a listener for a PngImageHeaderEvent. + * @param {function(PngImageHeaderEvent): void} listener * @returns {PngParser} for chaining */ - onChromaticities(listener) { - super.addEventListener(PngParseEventType.cHRM, listener); + onImageHeader(listener) { + super.addEventListener(PngParseEventType.IHDR, listener); return this; } @@ -252,6 +271,26 @@ 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 PngTextualDataEvent. + * @param {function(PngTextualDataEvent): void} listener + * @returns {PngParser} for chaining + */ + onTextualData(listener) { + super.addEventListener(PngParseEventType.tEXt, listener); + return this; + } + /** * Type-safe way to bind a listener for a PngTransparencyEvent. * @param {function(PngTransparencyEvent): void} listener @@ -262,16 +301,6 @@ export class PngParser extends EventTarget { return this; } - /** - * Type-safe way to bind a listener for a PngImageDataEvent. - * @param {function(PngImageDataEvent): void} listener - * @returns {PngParser} for chaining - */ - onImageData(listener) { - super.addEventListener(PngParseEventType.IDAT, listener); - return this; - } - /** @returns {Promise} A Promise that resolves when the parsing is complete. */ async start() { const sigLength = SIG.byteLength; @@ -404,6 +433,18 @@ export class PngParser extends EventTarget { this.dispatchEvent(new PngPaletteEvent(this.palette)); break; + // https://www.w3.org/TR/png-3/#11tEXt + case 'tEXt': + const byteArr = chStream.peekBytes(length); + const nullIndex = byteArr.indexOf(0); + /** @type {PngTextualData} */ + const textualData = { + keyword: chStream.readString(nullIndex), + textString: chStream.skip(1).readString(length - nullIndex - 1), + }; + this.dispatchEvent(new PngTextualDataEvent(textualData)); + break; + // https://www.w3.org/TR/png-3/#11tRNS case 'tRNS': if (this.colorType === undefined) throw `tRNS before IHDR`; @@ -507,6 +548,9 @@ async function main() { parser.onImageData(evt => { // console.dir(evt); }); + parser.onTextualData(evt => { + // console.dir(evt.textualData); + }); try { await parser.start(); diff --git a/tests/image-parsers-png.spec.js b/tests/image-parsers-png.spec.js index 1e63c3b..508ae66 100644 --- a/tests/image-parsers-png.spec.js +++ b/tests/image-parsers-png.spec.js @@ -9,6 +9,7 @@ import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/pn /** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */ /** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */ /** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */ +/** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */ /** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */ function getPngParser(fileName) { @@ -150,4 +151,19 @@ describe('bitjs.image.parsers.PngParser', () => { expect(data.rawImageData.byteLength).equals(2205); expect(data.rawImageData[0]).equals(120); }); + + it('extracts tEXt', async () => { + /** @type {PngTextualData[]} */ + let textualDataArr = []; + + await getPngParser('tests/image-testfiles/ctzn0g04.png') + .onTextualData(evt => { textualDataArr.push(evt.textualData) }) + .start(); + + expect(textualDataArr.length).equals(2); + expect(textualDataArr[0].keyword).equals('Title'); + expect(textualDataArr[0].textString).equals('PngSuite'); + expect(textualDataArr[1].keyword).equals('Author'); + expect(textualDataArr[1].textString).equals('Willem A.J. van Schaik\n(willem@schaik.com)'); + }); }); diff --git a/tests/image-testfiles/ctzn0g04.png b/tests/image-testfiles/ctzn0g04.png new file mode 100644 index 0000000..b4401c9 Binary files /dev/null and b/tests/image-testfiles/ctzn0g04.png differ