@tldraw/utils
Version:
tldraw infinite canvas SDK (private utilities).
151 lines (128 loc) • 4.09 kB
text/typescript
/*!
* MIT License: https://github.com/vHeemstra/is-apng/blob/main/license
* Copyright (c) Philip van Heemstra
*/
export function isApngAnimated(buffer: ArrayBuffer): boolean {
const view = new Uint8Array(buffer)
if (
!view ||
!((typeof Buffer !== 'undefined' && Buffer.isBuffer(view)) || view instanceof Uint8Array) ||
view.length < 16
) {
return false
}
const isPNG =
view[0] === 0x89 &&
view[1] === 0x50 &&
view[2] === 0x4e &&
view[3] === 0x47 &&
view[4] === 0x0d &&
view[5] === 0x0a &&
view[6] === 0x1a &&
view[7] === 0x0a
if (!isPNG) {
return false
}
/**
* Returns the index of the first occurrence of a sequence in an typed array, or -1 if it is not present.
*
* Works similar to `Array.prototype.indexOf()`, but it searches for a sequence of array values (bytes).
* The bytes in the `haystack` array are decoded (UTF-8) and then used to search for `needle`.
*
* @param haystack `Uint8Array`
* Array to search in.
*
* @param needle `string | RegExp`
* The value to locate in the array.
*
* @param fromIndex `number`
* The array index at which to begin the search.
*
* @param upToIndex `number`
* The array index up to which to search.
* If omitted, search until the end.
*
* @param chunksize `number`
* Size of the chunks used when searching (default 1024).
*
* @returns boolean
* Whether the array holds Animated PNG data.
*/
function indexOfSubstring(
haystack: Uint8Array,
needle: string | RegExp,
fromIndex: number,
upToIndex?: number,
chunksize = 1024 /* Bytes */
) {
/**
* Adopted from: https://stackoverflow.com/a/67771214/2142071
*/
if (!needle) {
return -1
}
needle = new RegExp(needle, 'g')
// The needle could get split over two chunks.
// So, at every chunk we prepend the last few characters
// of the last chunk.
const needle_length = needle.source.length
const decoder = new TextDecoder()
// Handle search offset in line with
// `Array.prototype.indexOf()` and `TypedArray.prototype.subarray()`.
const full_haystack_length = haystack.length
if (typeof upToIndex === 'undefined') {
upToIndex = full_haystack_length
}
if (fromIndex >= full_haystack_length || upToIndex <= 0 || fromIndex >= upToIndex) {
return -1
}
haystack = haystack.subarray(fromIndex, upToIndex)
let position = -1
let current_index = 0
let full_length = 0
let needle_buffer = ''
outer: while (current_index < haystack.length) {
const next_index = current_index + chunksize
// subarray doesn't copy
const chunk = haystack.subarray(current_index, next_index)
const decoded = decoder.decode(chunk, { stream: true })
const text = needle_buffer + decoded
let match: RegExpExecArray | null
let last_index = -1
while ((match = needle.exec(text)) !== null) {
last_index = match.index - needle_buffer.length
position = full_length + last_index
break outer
}
current_index = next_index
full_length += decoded.length
// Check that the buffer doesn't itself include the needle
// this would cause duplicate finds (we could also use a Set to avoid that).
const needle_index =
last_index > -1 ? last_index + needle_length : decoded.length - needle_length
needle_buffer = decoded.slice(needle_index)
}
// Correct for search offset.
if (position >= 0) {
position += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex
}
return position
}
// APNGs have an animation control chunk ('acTL') preceding the IDATs.
// See: https://en.wikipedia.org/wiki/APNG#File_format
const idatIdx = indexOfSubstring(view, 'IDAT', 12)
if (idatIdx >= 12) {
const actlIdx = indexOfSubstring(view, 'acTL', 8, idatIdx)
return actlIdx >= 8
}
return false
}
// globalThis.isApng = isApng
// (new TextEncoder()).encode('IDAT')
// Decimal: [73, 68, 65, 84]
// Hex: [0x49, 0x44, 0x41, 0x54]
// (new TextEncoder()).encode('acTL')
// Decimal: [97, 99, 84, 76]
// Hex: [0x61, 0x63, 0x54, 0x4C]
// const idatIdx = buffer.indexOf('IDAT')
// const actlIdx = buffer.indexOf('acTL')