81 lines
2.5 KiB
TypeScript
81 lines
2.5 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 precense 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);
|
|
|
|
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;
|
|
}
|