Skip to content

Commit

Permalink
fix: correctly decode non-ASCII tEXt chunks
Browse files Browse the repository at this point in the history
Refs: #26
  • Loading branch information
targos committed Feb 13, 2024
1 parent 395ecee commit 76391df
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 13 deletions.
Binary file added img/text-ascii.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 img/text-excalidraw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 4 additions & 13 deletions src/PngDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { inflate, Inflate as Inflator } from 'pako';
import { crc } from './common';
import { decodeInterlaceNull } from './helpers/decodeInterlaceNull';
import { checkSignature } from './helpers/signature';
import { readKeyword, readLatin1 } from './helpers/text';
import {
ColorType,
CompressionMethod,
Expand All @@ -18,8 +19,6 @@ import {
PngDecoderOptions,
} from './types';

const NULL = '\0';

export default class PngDecoder extends IOBuffer {
private readonly _checkCrc: boolean;
private readonly _inflator: Inflator;
Expand Down Expand Up @@ -246,11 +245,7 @@ export default class PngDecoder extends IOBuffer {

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

// https://www.w3.org/TR/PNG/#11tEXt
private decodetEXt(length: number): void {
let keyword = '';
let char;
while ((char = this.readChar()) !== NULL) {
keyword += char;
}
this._png.text[keyword] = this.readChars(length - keyword.length - 1);
const keyword = readKeyword(this);
this._png.text[keyword] = readLatin1(this, length - keyword.length - 1);
}

// https://www.w3.org/TR/PNG/#11pHYs
Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,32 @@ describe('decode', () => {
expect(img.iccEmbeddedProfile.name).toBe('ICC profile');
expect(img.iccEmbeddedProfile.profile).toHaveLength(672);
});

it('tEXt chunk - ASCII', () => {
const { text } = loadAndDecode('text-ascii.png');
expect(text).toStrictEqual({
Smiles: 'CCCC',
'date:create': '2024-02-12T15:56:01+00:00',
'date:modify': '2024-02-12T15:55:48+00:00',
'date:timestamp': '2024-02-12T16:04:26+00:00',
'exif:ExifOffset': '78, ',
'exif:PixelXDimension': '175, ',
'exif:PixelYDimension': '51, ',
});
});

it('tEXt chunk - latin1', () => {
const { text } = loadAndDecode('text-excalidraw.png');
expect(text).toHaveProperty(['application/vnd.excalidraw+json']);
const json = JSON.parse(text['application/vnd.excalidraw+json']);
expect(json).toMatchObject({
version: '1',
encoding: 'bstring',
compressed: true,
});
// Binary string that we don't know how to interpret.
expect(json.encoded).toHaveLength(654);
});
});

function loadAndDecode(img: string, options?: PngDecoderOptions): DecodedPng {
Expand Down
32 changes: 32 additions & 0 deletions src/helpers/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IOBuffer } from 'iobuffer';

const latin1Decoder = new TextDecoder('latin1');

const NULL = 0;

const keywordRegex = /^[\x20-\x7e\xa1-\xff]+$/;

// https://www.w3.org/TR/png/#11keywords
export function readKeyword(buffer: IOBuffer): string {
buffer.mark();
while (buffer.readByte() !== NULL) {
/* advance */
}
const end = buffer.offset;
buffer.reset();
const keyword = latin1Decoder.decode(
buffer.readBytes(end - buffer.offset - 1),
);
// NULL
buffer.skip(1);

if (!keywordRegex.test(keyword)) {
throw new Error(`keyword contains invalid characters: ${keyword}`);
}

return keyword;
}

export function readLatin1(buffer: IOBuffer, length: number): string {
return latin1Decoder.decode(buffer.readBytes(length));
}

0 comments on commit 76391df

Please sign in to comment.