Signal-Desktop/ts/util/getAnimatedPngDataIfExists.ts

86 lines
2.6 KiB
TypeScript

// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const PNG_SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
const ACTL_CHUNK_BYTES = new TextEncoder().encode('acTL');
const IDAT_CHUNK_BYTES = new TextEncoder().encode('IDAT');
const MAX_BYTES_TO_READ = 1024 * 1024;
type AnimatedPngData = {
numPlays: number;
};
/**
* This is a naïve implementation. It only performs two checks:
*
* 1. Do the bytes start with the [PNG signature][0]?
* 2. If so, does it contain the [`acTL` chunk][1] before the [`IDAT` chunk][2], in the
* first megabyte?
*
* Though we _could_ only check for the presence of the `acTL` chunk anywhere, we make
* sure it's before the `IDAT` chunk and within the first megabyte. This adds a small
* amount of validity checking and helps us avoid problems with large PNGs.
*
* It doesn't make sure the PNG is valid. It doesn't verify [the CRC code][3] of each PNG
* chunk; it doesn't verify any of the chunk's data; it doesn't verify that the chunks are
* in the right order; etc.
*
* [0]: https://www.w3.org/TR/PNG/#5PNG-file-signature
* [1]: https://wiki.mozilla.org/APNG_Specification#.60acTL.60:_The_Animation_Control_Chunk
* [2]: https://www.w3.org/TR/PNG/#11IDAT
* [3]: https://www.w3.org/TR/PNG/#5Chunk-layout
*/
export function getAnimatedPngDataIfExists(
bytes: Uint8Array
): null | AnimatedPngData {
if (!hasPngSignature(bytes)) {
return null;
}
let numPlays: void | number;
const dataView = new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
);
let i = PNG_SIGNATURE.length;
while (i < bytes.byteLength && i <= MAX_BYTES_TO_READ) {
const chunkTypeBytes = bytes.slice(i + 4, i + 8);
if (areBytesEqual(chunkTypeBytes, ACTL_CHUNK_BYTES)) {
// 4 bytes for the length; 4 bytes for the type; 4 bytes for the number of frames.
numPlays = dataView.getUint32(i + 12);
if (numPlays === 0) {
numPlays = Infinity;
}
return { numPlays };
}
if (areBytesEqual(chunkTypeBytes, IDAT_CHUNK_BYTES)) {
return null;
}
// Jump over the length (4 bytes), the type (4 bytes), the data, and the CRC checksum
// (4 bytes).
i += 12 + dataView.getUint32(i);
}
return null;
}
function hasPngSignature(bytes: Uint8Array): boolean {
return areBytesEqual(bytes.slice(0, 8), PNG_SIGNATURE);
}
function areBytesEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.byteLength !== b.byteLength) {
return false;
}
for (let i = 0; i < a.byteLength; i += 1) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}