Skip to content

Commit

Permalink
PngParser: Add support for tRNS chunk.
Browse files Browse the repository at this point in the history
  • Loading branch information
codedread committed Jan 17, 2024
1 parent 6ff9333 commit f1efe8c
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 9 deletions.
93 changes: 84 additions & 9 deletions image/parsers/png.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import * as fs from 'node:fs'; // TODO: Remove.
import { ByteStream } from '../../io/bytestream.js';

// https://en.wikipedia.org/wiki/PNG#File_format
// https://www.w3.org/TR/2003/REC-PNG-20031110
// https://en.wikipedia.org/wiki/PNG#File_format

// TODO: Ancillary chunks bKGD, cHRM, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt.

// let DEBUG = true;
let DEBUG = false;
Expand All @@ -24,6 +26,7 @@ export const PngParseEventType = {
gAMA: 'image_gamma',
sBIT: 'significant_bits',
PLTE: 'palette',
tRNS: 'transparency',
IDAT: 'image_data',
};

Expand Down Expand Up @@ -110,6 +113,24 @@ export class PngPaletteEvent extends Event {
}
}

/**
* @typedef PngTransparency
* @property {number=} greySampleValue Populated for color type 0.
* @property {number=} redSampleValue Populated for color type 2.
* @property {number=} blueSampleValue Populated for color type 2.
* @property {number=} greenSampleValue Populated for color type 2.
* @property {number[]=} alphaPalette Populated for color type 3.
*/

export class PngTransparencyEvent extends Event {
/** @param {PngTransparency} */
constructor(transparency) {
super(PngParseEventType.tRNS);
/** @type {PngTransparency} */
this.transparency = transparency;
}
}

/**
* @typedef PngImageData
* @property {Uint8Array} rawImageData
Expand Down Expand Up @@ -145,6 +166,13 @@ export class PngParser extends EventTarget {
*/
colorType;

/**
* @type {PngPalette}
* @private
*/
palette;


/** @param {ArrayBuffer} ab */
constructor(ab) {
super();
Expand Down Expand Up @@ -192,6 +220,16 @@ export class PngParser extends EventTarget {
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;
}

/**
* Type-safe way to bind a listener for a PngImageDataEvent.
* @param {function(PngImageDataEvent): void} listener
Expand Down Expand Up @@ -266,22 +304,22 @@ export class PngParser extends EventTarget {
/** @type {PngSignificantBits} */
const sigBits = {};

const badLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
const sbitBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
if (this.colorType === PngColorType.GREYSCALE) {
if (length !== 1) throw badLengthErr;
if (length !== 1) throw sbitBadLengthErr;
sigBits.significant_greyscale = chStream.readNumber(1);
} else if (this.colorType === PngColorType.TRUE_COLOR ||
this.colorType === PngColorType.INDEXED_COLOR) {
if (length !== 3) throw badLengthErr;
if (length !== 3) throw sbitBadLengthErr;
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;
if (length !== 2) throw sbitBadLengthErr;
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;
if (length !== 4) throw sbitBadLengthErr;
sigBits.significant_red = chStream.readNumber(1);
sigBits.significant_green = chStream.readNumber(1);
sigBits.significant_blue = chStream.readNumber(1);
Expand Down Expand Up @@ -309,11 +347,45 @@ export class PngParser extends EventTarget {
}

/** @type {PngPalette} */
const palette = {
this.palette = {
entries: paletteEntries,
};

this.dispatchEvent(new PngPaletteEvent(palette));
this.dispatchEvent(new PngPaletteEvent(this.palette));
break;

// https://www.w3.org/TR/2003/REC-PNG-20031110/#11tRNS
case 'tRNS':
if (this.colorType === undefined) throw `tRNS before IHDR`;
if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA ||
this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) {
throw `tRNS with color type ${this.colorType}`;
}

/** @type {PngTransparency} */
const transparency = {};

const trnsBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
if (this.colorType === PngColorType.GREYSCALE) {
if (length !== 2) throw trnsBadLengthErr;
transparency.greySampleValue = chStream.readNumber(2);
} else if (this.colorType === PngColorType.TRUE_COLOR) {
if (length !== 6) throw trnsBadLengthErr;
// Oddly the order is RBG instead of RGB :-/
transparency.redSampleValue = chStream.readNumber(2);
transparency.blueSampleValue = chStream.readNumber(2);
transparency.greenSampleValue = chStream.readNumber(2);
} else if (this.colorType === PngColorType.INDEXED_COLOR) {
if (!this.palette) throw `tRNS before PLTE`;
if (length > this.palette.entries.length) throw `More tRNS entries than palette`;

transparency.alphaPalette = [];
for (let a = 0; a < length; ++a) {
transparency.alphaPalette.push(chStream.readNumber(1));
}
}

this.dispatchEvent(new PngTransparencyEvent(transparency));
break;

// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT
Expand Down Expand Up @@ -371,11 +443,14 @@ async function main() {
// console.dir(evt.imageGamma);
});
parser.onSignificantBits(evt => {
console.dir(evt.sigBits);
// console.dir(evt.sigBits);
});
parser.onPalette(evt => {
// console.dir(evt.palette);
});
parser.onTransparency(evt => {
// console.dir(evt.transparency);
});
parser.onImageData(evt => {
// console.dir(evt);
});
Expand Down
38 changes: 38 additions & 0 deletions tests/image-parsers-png.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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').PngTransparency} PngTransparency */

function getPngParser(fileName) {
const nodeBuf = fs.readFileSync(fileName);
Expand Down Expand Up @@ -84,6 +85,43 @@ describe('bitjs.image.parsers.PngParser', () => {
expect(entry.blue).equals(86);
});

describe('tRNS', () => {
it('extracts alpha palette', async () => {
/** @type {PngTransparency} */
let transparency;
await getPngParser('tests/image-testfiles/tbbn3p08.png')
.onTransparency(evt => transparency = evt.transparency)
.start();

expect(transparency.alphaPalette.length).equals(1);
expect(transparency.alphaPalette[0]).equals(0);
});

it('extracts 8-bit RGB transparency', async () => {
/** @type {PngTransparency} */
let transparency;
await getPngParser('tests/image-testfiles/tbrn2c08.png')
.onTransparency(evt => transparency = evt.transparency)
.start();

expect(transparency.redSampleValue).equals(255);
expect(transparency.blueSampleValue).equals(255);
expect(transparency.greenSampleValue).equals(255);
});

it('extracts 16-bit RGB transparency', async () => {
/** @type {PngTransparency} */
let transparency;
await getPngParser('tests/image-testfiles/tbgn2c16.png')
.onTransparency(evt => transparency = evt.transparency)
.start();

expect(transparency.redSampleValue).equals(65535);
expect(transparency.blueSampleValue).equals(65535);
expect(transparency.greenSampleValue).equals(65535);
});
});

it('extracts IDAT', async () => {
/** @type {PngImageData} */
let data;
Expand Down
Binary file added tests/image-testfiles/tbgn2c16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/image-testfiles/tbrn2c08.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 f1efe8c

Please sign in to comment.