Skip to content

Commit 76391df

Browse files
committed
fix: correctly decode non-ASCII tEXt chunks
Refs: #26
1 parent 395ecee commit 76391df

File tree

5 files changed

+62
-13
lines changed

5 files changed

+62
-13
lines changed

‎img/text-ascii.png

1.58 KB
Loading

‎img/text-excalidraw.png

4.06 KB
Loading

‎src/PngDecoder.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { inflate, Inflate as Inflator } from 'pako';
44
import { crc } from './common';
55
import { decodeInterlaceNull } from './helpers/decodeInterlaceNull';
66
import { checkSignature } from './helpers/signature';
7+
import { readKeyword, readLatin1 } from './helpers/text';
78
import {
89
ColorType,
910
CompressionMethod,
@@ -18,8 +19,6 @@ import {
1819
PngDecoderOptions,
1920
} from './types';
2021

21-
const NULL = '\0';
22-
2322
export default class PngDecoder extends IOBuffer {
2423
private readonly _checkCrc: boolean;
2524
private readonly _inflator: Inflator;
@@ -246,11 +245,7 @@ export default class PngDecoder extends IOBuffer {
246245

247246
// https://www.w3.org/TR/PNG/#11iCCP
248247
private decodeiCCP(length: number): void {
249-
let name = '';
250-
let char;
251-
while ((char = this.readChar()) !== NULL) {
252-
name += char;
253-
}
248+
const name = readKeyword(this);
254249
const compressionMethod = this.readUint8();
255250
if (compressionMethod !== CompressionMethod.DEFLATE) {
256251
throw new Error(
@@ -266,12 +261,8 @@ export default class PngDecoder extends IOBuffer {
266261

267262
// https://www.w3.org/TR/PNG/#11tEXt
268263
private decodetEXt(length: number): void {
269-
let keyword = '';
270-
let char;
271-
while ((char = this.readChar()) !== NULL) {
272-
keyword += char;
273-
}
274-
this._png.text[keyword] = this.readChars(length - keyword.length - 1);
264+
const keyword = readKeyword(this);
265+
this._png.text[keyword] = readLatin1(this, length - keyword.length - 1);
275266
}
276267

277268
// https://www.w3.org/TR/PNG/#11pHYs

‎src/__tests__/decode.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,32 @@ describe('decode', () => {
109109
expect(img.iccEmbeddedProfile.name).toBe('ICC profile');
110110
expect(img.iccEmbeddedProfile.profile).toHaveLength(672);
111111
});
112+
113+
it('tEXt chunk - ASCII', () => {
114+
const { text } = loadAndDecode('text-ascii.png');
115+
expect(text).toStrictEqual({
116+
Smiles: 'CCCC',
117+
'date:create': '2024-02-12T15:56:01+00:00',
118+
'date:modify': '2024-02-12T15:55:48+00:00',
119+
'date:timestamp': '2024-02-12T16:04:26+00:00',
120+
'exif:ExifOffset': '78, ',
121+
'exif:PixelXDimension': '175, ',
122+
'exif:PixelYDimension': '51, ',
123+
});
124+
});
125+
126+
it('tEXt chunk - latin1', () => {
127+
const { text } = loadAndDecode('text-excalidraw.png');
128+
expect(text).toHaveProperty(['application/vnd.excalidraw+json']);
129+
const json = JSON.parse(text['application/vnd.excalidraw+json']);
130+
expect(json).toMatchObject({
131+
version: '1',
132+
encoding: 'bstring',
133+
compressed: true,
134+
});
135+
// Binary string that we don't know how to interpret.
136+
expect(json.encoded).toHaveLength(654);
137+
});
112138
});
113139

114140
function loadAndDecode(img: string, options?: PngDecoderOptions): DecodedPng {

‎src/helpers/text.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IOBuffer } from 'iobuffer';
2+
3+
const latin1Decoder = new TextDecoder('latin1');
4+
5+
const NULL = 0;
6+
7+
const keywordRegex = /^[\x20-\x7e\xa1-\xff]+$/;
8+
9+
// https://www.w3.org/TR/png/#11keywords
10+
export function readKeyword(buffer: IOBuffer): string {
11+
buffer.mark();
12+
while (buffer.readByte() !== NULL) {
13+
/* advance */
14+
}
15+
const end = buffer.offset;
16+
buffer.reset();
17+
const keyword = latin1Decoder.decode(
18+
buffer.readBytes(end - buffer.offset - 1),
19+
);
20+
// NULL
21+
buffer.skip(1);
22+
23+
if (!keywordRegex.test(keyword)) {
24+
throw new Error(`keyword contains invalid characters: ${keyword}`);
25+
}
26+
27+
return keyword;
28+
}
29+
30+
export function readLatin1(buffer: IOBuffer, length: number): string {
31+
return latin1Decoder.decode(buffer.readBytes(length));
32+
}

0 commit comments

Comments
 (0)