Skip to content

Commit f1efe8c

Browse files
committed
PngParser: Add support for tRNS chunk.
1 parent 6ff9333 commit f1efe8c

File tree

4 files changed

+122
-9
lines changed

4 files changed

+122
-9
lines changed

image/parsers/png.js

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import * as fs from 'node:fs'; // TODO: Remove.
1212
import { ByteStream } from '../../io/bytestream.js';
1313

14-
// https://en.wikipedia.org/wiki/PNG#File_format
1514
// https://www.w3.org/TR/2003/REC-PNG-20031110
15+
// https://en.wikipedia.org/wiki/PNG#File_format
16+
17+
// TODO: Ancillary chunks bKGD, cHRM, hIST, iTXt, pHYs, sPLT, tEXt, tIME, zTXt.
1618

1719
// let DEBUG = true;
1820
let DEBUG = false;
@@ -24,6 +26,7 @@ export const PngParseEventType = {
2426
gAMA: 'image_gamma',
2527
sBIT: 'significant_bits',
2628
PLTE: 'palette',
29+
tRNS: 'transparency',
2730
IDAT: 'image_data',
2831
};
2932

@@ -110,6 +113,24 @@ export class PngPaletteEvent extends Event {
110113
}
111114
}
112115

116+
/**
117+
* @typedef PngTransparency
118+
* @property {number=} greySampleValue Populated for color type 0.
119+
* @property {number=} redSampleValue Populated for color type 2.
120+
* @property {number=} blueSampleValue Populated for color type 2.
121+
* @property {number=} greenSampleValue Populated for color type 2.
122+
* @property {number[]=} alphaPalette Populated for color type 3.
123+
*/
124+
125+
export class PngTransparencyEvent extends Event {
126+
/** @param {PngTransparency} */
127+
constructor(transparency) {
128+
super(PngParseEventType.tRNS);
129+
/** @type {PngTransparency} */
130+
this.transparency = transparency;
131+
}
132+
}
133+
113134
/**
114135
* @typedef PngImageData
115136
* @property {Uint8Array} rawImageData
@@ -145,6 +166,13 @@ export class PngParser extends EventTarget {
145166
*/
146167
colorType;
147168

169+
/**
170+
* @type {PngPalette}
171+
* @private
172+
*/
173+
palette;
174+
175+
148176
/** @param {ArrayBuffer} ab */
149177
constructor(ab) {
150178
super();
@@ -192,6 +220,16 @@ export class PngParser extends EventTarget {
192220
return this;
193221
}
194222

223+
/**
224+
* Type-safe way to bind a listener for a PngTransparencyEvent.
225+
* @param {function(PngTransparencyEvent): void} listener
226+
* @returns {PngParser} for chaining
227+
*/
228+
onTransparency(listener) {
229+
super.addEventListener(PngParseEventType.tRNS, listener);
230+
return this;
231+
}
232+
195233
/**
196234
* Type-safe way to bind a listener for a PngImageDataEvent.
197235
* @param {function(PngImageDataEvent): void} listener
@@ -266,22 +304,22 @@ export class PngParser extends EventTarget {
266304
/** @type {PngSignificantBits} */
267305
const sigBits = {};
268306

269-
const badLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
307+
const sbitBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
270308
if (this.colorType === PngColorType.GREYSCALE) {
271-
if (length !== 1) throw badLengthErr;
309+
if (length !== 1) throw sbitBadLengthErr;
272310
sigBits.significant_greyscale = chStream.readNumber(1);
273311
} else if (this.colorType === PngColorType.TRUE_COLOR ||
274312
this.colorType === PngColorType.INDEXED_COLOR) {
275-
if (length !== 3) throw badLengthErr;
313+
if (length !== 3) throw sbitBadLengthErr;
276314
sigBits.significant_red = chStream.readNumber(1);
277315
sigBits.significant_green = chStream.readNumber(1);
278316
sigBits.significant_blue = chStream.readNumber(1);
279317
} else if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA) {
280-
if (length !== 2) throw badLengthErr;
318+
if (length !== 2) throw sbitBadLengthErr;
281319
sigBits.significant_greyscale = chStream.readNumber(1);
282320
sigBits.significant_alpha = chStream.readNumber(1);
283321
} else if (this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) {
284-
if (length !== 4) throw badLengthErr;
322+
if (length !== 4) throw sbitBadLengthErr;
285323
sigBits.significant_red = chStream.readNumber(1);
286324
sigBits.significant_green = chStream.readNumber(1);
287325
sigBits.significant_blue = chStream.readNumber(1);
@@ -309,11 +347,45 @@ export class PngParser extends EventTarget {
309347
}
310348

311349
/** @type {PngPalette} */
312-
const palette = {
350+
this.palette = {
313351
entries: paletteEntries,
314352
};
315353

316-
this.dispatchEvent(new PngPaletteEvent(palette));
354+
this.dispatchEvent(new PngPaletteEvent(this.palette));
355+
break;
356+
357+
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11tRNS
358+
case 'tRNS':
359+
if (this.colorType === undefined) throw `tRNS before IHDR`;
360+
if (this.colorType === PngColorType.GREYSCALE_WITH_ALPHA ||
361+
this.colorType === PngColorType.TRUE_COLOR_WITH_ALPHA) {
362+
throw `tRNS with color type ${this.colorType}`;
363+
}
364+
365+
/** @type {PngTransparency} */
366+
const transparency = {};
367+
368+
const trnsBadLengthErr = `Weird sBIT length for color type ${this.colorType}: ${length}`;
369+
if (this.colorType === PngColorType.GREYSCALE) {
370+
if (length !== 2) throw trnsBadLengthErr;
371+
transparency.greySampleValue = chStream.readNumber(2);
372+
} else if (this.colorType === PngColorType.TRUE_COLOR) {
373+
if (length !== 6) throw trnsBadLengthErr;
374+
// Oddly the order is RBG instead of RGB :-/
375+
transparency.redSampleValue = chStream.readNumber(2);
376+
transparency.blueSampleValue = chStream.readNumber(2);
377+
transparency.greenSampleValue = chStream.readNumber(2);
378+
} else if (this.colorType === PngColorType.INDEXED_COLOR) {
379+
if (!this.palette) throw `tRNS before PLTE`;
380+
if (length > this.palette.entries.length) throw `More tRNS entries than palette`;
381+
382+
transparency.alphaPalette = [];
383+
for (let a = 0; a < length; ++a) {
384+
transparency.alphaPalette.push(chStream.readNumber(1));
385+
}
386+
}
387+
388+
this.dispatchEvent(new PngTransparencyEvent(transparency));
317389
break;
318390

319391
// https://www.w3.org/TR/2003/REC-PNG-20031110/#11IDAT
@@ -371,11 +443,14 @@ async function main() {
371443
// console.dir(evt.imageGamma);
372444
});
373445
parser.onSignificantBits(evt => {
374-
console.dir(evt.sigBits);
446+
// console.dir(evt.sigBits);
375447
});
376448
parser.onPalette(evt => {
377449
// console.dir(evt.palette);
378450
});
451+
parser.onTransparency(evt => {
452+
// console.dir(evt.transparency);
453+
});
379454
parser.onImageData(evt => {
380455
// console.dir(evt);
381456
});

tests/image-parsers-png.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PngColorType, PngInterlaceMethod, PngParser } from '../image/parsers/pn
88
/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */
99
/** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */
1010
/** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */
11+
/** @typedef {import('../image/parsers/png.js').PngTransparency} PngTransparency */
1112

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

88+
describe('tRNS', () => {
89+
it('extracts alpha palette', async () => {
90+
/** @type {PngTransparency} */
91+
let transparency;
92+
await getPngParser('tests/image-testfiles/tbbn3p08.png')
93+
.onTransparency(evt => transparency = evt.transparency)
94+
.start();
95+
96+
expect(transparency.alphaPalette.length).equals(1);
97+
expect(transparency.alphaPalette[0]).equals(0);
98+
});
99+
100+
it('extracts 8-bit RGB transparency', async () => {
101+
/** @type {PngTransparency} */
102+
let transparency;
103+
await getPngParser('tests/image-testfiles/tbrn2c08.png')
104+
.onTransparency(evt => transparency = evt.transparency)
105+
.start();
106+
107+
expect(transparency.redSampleValue).equals(255);
108+
expect(transparency.blueSampleValue).equals(255);
109+
expect(transparency.greenSampleValue).equals(255);
110+
});
111+
112+
it('extracts 16-bit RGB transparency', async () => {
113+
/** @type {PngTransparency} */
114+
let transparency;
115+
await getPngParser('tests/image-testfiles/tbgn2c16.png')
116+
.onTransparency(evt => transparency = evt.transparency)
117+
.start();
118+
119+
expect(transparency.redSampleValue).equals(65535);
120+
expect(transparency.blueSampleValue).equals(65535);
121+
expect(transparency.greenSampleValue).equals(65535);
122+
});
123+
});
124+
87125
it('extracts IDAT', async () => {
88126
/** @type {PngImageData} */
89127
let data;

tests/image-testfiles/tbgn2c16.png

1.99 KB
Loading

tests/image-testfiles/tbrn2c08.png

1.59 KB
Loading

0 commit comments

Comments
 (0)