Skip to content

Commit

Permalink
PngParser: Add support for tEXt chunk
Browse files Browse the repository at this point in the history
  • Loading branch information
codedread committed Jan 17, 2024
1 parent 8694b6c commit afb1a67
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
92 changes: 68 additions & 24 deletions image/parsers/png.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@ 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;
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} */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -253,22 +272,32 @@ export class PngParser extends EventTarget {
}

/**
* Type-safe way to bind a listener for a PngTransparencyEvent.
* @param {function(PngTransparencyEvent): void} listener
* Type-safe way to bind a listener for a PngSignificantBitsEvent.
* @param {function(PngSignificantBitsEvent): void} listener
* @returns {PngParser} for chaining
*/
onTransparency(listener) {
super.addEventListener(PngParseEventType.tRNS, listener);
onSignificantBits(listener) {
super.addEventListener(PngParseEventType.sBIT, listener);
return this;
}

/**
* Type-safe way to bind a listener for a PngImageDataEvent.
* @param {function(PngImageDataEvent): void} listener
* Type-safe way to bind a listener for a PngTextualDataEvent.
* @param {function(PngTextualDataEvent): void} listener
* @returns {PngParser} for chaining
*/
onImageData(listener) {
super.addEventListener(PngParseEventType.IDAT, listener);
onTextualData(listener) {
super.addEventListener(PngParseEventType.tEXt, listener);
return this;
}

/**
* Type-safe way to bind a listener for a PngTransparencyEvent.
* @param {function(PngTransparencyEvent): void} listener
* @returns {PngParser} for chaining
*/
onTransparency(listener) {
super.addEventListener(PngParseEventType.tRNS, listener);
return this;
}

Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -507,6 +548,9 @@ async function main() {
parser.onImageData(evt => {
// console.dir(evt);
});
parser.onTextualData(evt => {
// console.dir(evt.textualData);
});

try {
await parser.start();
Expand Down
16 changes: 16 additions & 0 deletions tests/image-parsers-png.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)');
});
});
Binary file added tests/image-testfiles/ctzn0g04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit afb1a67

Please # to comment.