Skip to content

Commit af72e19

Browse files
committed
PngParser: Add support for iTXt chunk
1 parent 153405a commit af72e19

File tree

4 files changed

+76
-1
lines changed

4 files changed

+76
-1
lines changed

image/parsers/png.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { ByteStream } from '../../io/bytestream.js';
1414
// https://www.w3.org/TR/png-3/
1515
// https://en.wikipedia.org/wiki/PNG#File_format
1616

17-
// TODO: Ancillary chunks bKGD, eXIf, hIST, iTXt, pHYs, sPLT, tIME.
17+
// TODO: Ancillary chunks bKGD, eXIf, hIST, pHYs, sPLT, tIME.
1818

1919
// let DEBUG = true;
2020
let DEBUG = false;
@@ -30,6 +30,7 @@ export const PngParseEventType = {
3030
// Ancillary chunks.
3131
cHRM: 'chromaticities_white_point',
3232
gAMA: 'image_gamma',
33+
iTXt: 'intl_text_data',
3334
sBIT: 'significant_bits',
3435
tEXt: 'textual_data',
3536
tRNS: 'transparency',
@@ -203,6 +204,25 @@ export class PngCompressedTextualDataEvent extends Event {
203204
}
204205
}
205206

207+
/**
208+
* @typedef PngIntlTextualData
209+
* @property {string} keyword
210+
* @property {number} compressionFlag 0 for uncompressed, 1 for compressed.
211+
* @property {number} compressionMethod 0 means zlib defalt when compressionFlag is 1.
212+
* @property {string=} languageTag
213+
* @property {string=} translatedKeyword
214+
* @property {Uint8Array} text The raw UTF-8 text (may be compressed).
215+
*/
216+
217+
export class PngIntlTextualDataEvent extends Event {
218+
/** @param {PngIntlTextualData} intlTextualdata */
219+
constructor(intlTextualdata) {
220+
super(PngParseEventType.iTXt);
221+
/** @type {PngIntlTextualData} */
222+
this.intlTextualdata = intlTextualdata;
223+
}
224+
}
225+
206226
/**
207227
* @typedef PngChunk Internal use only.
208228
* @property {number} length
@@ -288,6 +308,16 @@ export class PngParser extends EventTarget {
288308
return this;
289309
}
290310

311+
/**
312+
* Type-safe way to bind a listener for a PngIntlTextualDataEvent.
313+
* @param {function(PngIntlTextualDataEvent): void} listener
314+
* @returns {PngParser} for chaining
315+
*/
316+
onIntlTextualData(listener) {
317+
super.addEventListener(PngParseEventType.iTXt, listener);
318+
return this;
319+
}
320+
291321
/**
292322
* Type-safe way to bind a listener for a PngPaletteEvent.
293323
* @param {function(PngPaletteEvent): void} listener
@@ -520,6 +550,29 @@ export class PngParser extends EventTarget {
520550
this.dispatchEvent(new PngCompressedTextualDataEvent(compressedTextualData));
521551
break;
522552

553+
// https://www.w3.org/TR/png-3/#11iTXt
554+
case 'iTXt':
555+
const intlByteArr = chStream.peekBytes(length);
556+
const intlNull0 = intlByteArr.indexOf(0);
557+
const intlNull1 = intlByteArr.indexOf(0, intlNull0 + 1);
558+
const intlNull2 = intlByteArr.indexOf(0, intlNull1 + 1);
559+
if (intlNull0 === -1) throw `iTXt: Did not have one null`;
560+
if (intlNull1 === -1) throw `iTXt: Did not have two nulls`;
561+
if (intlNull2 === -1) throw `iTXt: Did not have three nulls`;
562+
563+
/** @type {PngIntlTextualData} */
564+
const intlTextData = {
565+
keyword: chStream.readString(intlNull0),
566+
compressionFlag: chStream.skip(1).readNumber(1),
567+
compressionMethod: chStream.readNumber(1),
568+
languageTag: (intlNull1 - intlNull0 > 1) ? chStream.readString(intlNull1 - intlNull0 - 1) : undefined,
569+
translatedKeyword: (intlNull2 - intlNull1 > 1) ? chStream.skip(1).readString(intlNull2 - intlNull1 - 1) : undefined,
570+
text: chStream.skip(1).readBytes(length - intlNull2 - 1),
571+
};
572+
573+
this.dispatchEvent(new PngIntlTextualDataEvent(intlTextData));
574+
break;
575+
523576
// https://www.w3.org/TR/png-3/#11IDAT
524577
case 'IDAT':
525578
/** @type {PngImageData} */
@@ -595,6 +648,9 @@ async function main() {
595648
parser.onCompressedTextualData(evt => {
596649
// console.dir(evt.compressedTextualData);
597650
});
651+
parser.onIntlTextualData(evt => {
652+
// console.dir(evt.intlTextualdata);
653+
});
598654

599655
try {
600656
await parser.start();

tests/image-parsers-png.spec.js

Lines changed: 16 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').PngImageData} PngImageData */
99
/** @typedef {import('../image/parsers/png.js').PngImageGamma} PngImageGamma */
1010
/** @typedef {import('../image/parsers/png.js').PngImageHeader} PngImageHeader */
11+
/** @typedef {import('../image/parsers/png.js').PngIntlTextualData} PngIntlTextualData */
1112
/** @typedef {import('../image/parsers/png.js').PngPalette} PngPalette */
1213
/** @typedef {import('../image/parsers/png.js').PngSignificantBits} PngSignificantBits */
1314
/** @typedef {import('../image/parsers/png.js').PngTextualData} PngTextualData */
@@ -185,4 +186,19 @@ describe('bitjs.image.parsers.PngParser', () => {
185186
const decompressedText = await new Response(decompressedStream).text();
186187
expect(decompressedText).equals('Freeware.');
187188
});
189+
190+
it('extracts iTXt', async () => {
191+
/** @type {PngIntlTextualData[]} */
192+
let data = [];
193+
194+
await getPngParser('tests/image-testfiles/ctjn0g04.png')
195+
.onIntlTextualData(evt => { data.push(evt.intlTextualdata) })
196+
.start();
197+
198+
expect(data.length).equals(6);
199+
expect(data[1].keyword).equals('Author');
200+
expect(data[1].compressionFlag).equals(0)
201+
expect(data[5].keyword).equals('Disclaimer');
202+
// TODO: Test this better!
203+
});
188204
});

tests/image-testfiles/ctjn0g04.png

941 Bytes
Loading

tests/io-bytestream.spec.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ describe('bitjs.io.ByteStream', () => {
214214

215215
const str = stream.readString(12);
216216
expect(str).equals('ABCDEFGHIJKL');
217+
218+
const str2 = stream.readString(0);
219+
expect(str2).equals('');
217220
});
218221

219222
describe('skip()', () => {

0 commit comments

Comments
 (0)