@tldraw/utils
Version:
tldraw infinite canvas SDK (private utilities).
8 lines (7 loc) • 6.9 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../../src/lib/media/apng.ts"],
"sourcesContent": ["/*!\n * MIT License: https://github.com/vHeemstra/is-apng/blob/main/license\n * Copyright (c) Philip van Heemstra\n */\n\n/**\n * Determines whether an ArrayBuffer contains an animated PNG (APNG) image.\n *\n * This function checks if the provided buffer contains a valid PNG file with animation\n * control chunks (acTL) that precede the image data chunks (IDAT), which indicates\n * it's an animated PNG rather than a static PNG.\n *\n * @param buffer - The ArrayBuffer containing the image data to analyze\n * @returns True if the buffer contains an animated PNG, false otherwise\n *\n * @example\n * ```typescript\n * // Check if an uploaded file contains an animated PNG\n * if (file.type === 'image/apng') {\n * const isAnimated = isApngAnimated(await file.arrayBuffer())\n * console.log(isAnimated ? 'Animated PNG' : 'Static PNG')\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Use with fetch to check remote images\n * const response = await fetch('image.png')\n * const buffer = await response.arrayBuffer()\n * const hasAnimation = isApngAnimated(buffer)\n * ```\n *\n * @public\n */\nexport function isApngAnimated(buffer: ArrayBuffer): boolean {\n\tconst view = new Uint8Array(buffer)\n\n\tif (\n\t\t!view ||\n\t\t!((typeof Buffer !== 'undefined' && Buffer.isBuffer(view)) || view instanceof Uint8Array) ||\n\t\tview.length < 16\n\t) {\n\t\treturn false\n\t}\n\n\tconst isPNG =\n\t\tview[0] === 0x89 &&\n\t\tview[1] === 0x50 &&\n\t\tview[2] === 0x4e &&\n\t\tview[3] === 0x47 &&\n\t\tview[4] === 0x0d &&\n\t\tview[5] === 0x0a &&\n\t\tview[6] === 0x1a &&\n\t\tview[7] === 0x0a\n\n\tif (!isPNG) {\n\t\treturn false\n\t}\n\n\t/**\n\t * Returns the index of the first occurrence of a string pattern in a Uint8Array, or -1 if not found.\n\t *\n\t * Searches for a string pattern by decoding chunks of the byte array to UTF-8 text and using\n\t * regular expression matching. Handles cases where the pattern might be split across chunk boundaries.\n\t *\n\t * @param haystack - The Uint8Array to search in\n\t * @param needle - The string or RegExp pattern to locate\n\t * @param fromIndex - The array index at which to begin the search\n\t * @param upToIndex - The array index up to which to search (optional, defaults to array end)\n\t * @param chunksize - Size of the chunks used when searching (default 1024 bytes)\n\t * @returns The index position of the first match, or -1 if not found\n\t */\n\tfunction indexOfSubstring(\n\t\thaystack: Uint8Array,\n\t\tneedle: string | RegExp,\n\t\tfromIndex: number,\n\t\tupToIndex?: number,\n\t\tchunksize = 1024 /* Bytes */\n\t) {\n\t\t/**\n\t\t * Adopted from: https://stackoverflow.com/a/67771214/2142071\n\t\t */\n\n\t\tif (!needle) {\n\t\t\treturn -1\n\t\t}\n\t\tneedle = new RegExp(needle, 'g')\n\n\t\t// The needle could get split over two chunks.\n\t\t// So, at every chunk we prepend the last few characters\n\t\t// of the last chunk.\n\t\tconst needle_length = needle.source.length\n\t\tconst decoder = new TextDecoder()\n\n\t\t// Handle search offset in line with\n\t\t// `Array.prototype.indexOf()` and `TypedArray.prototype.subarray()`.\n\t\tconst full_haystack_length = haystack.length\n\t\tif (typeof upToIndex === 'undefined') {\n\t\t\tupToIndex = full_haystack_length\n\t\t}\n\t\tif (fromIndex >= full_haystack_length || upToIndex <= 0 || fromIndex >= upToIndex) {\n\t\t\treturn -1\n\t\t}\n\t\thaystack = haystack.subarray(fromIndex, upToIndex)\n\n\t\tlet position = -1\n\t\tlet current_index = 0\n\t\tlet full_length = 0\n\t\tlet needle_buffer = ''\n\n\t\touter: while (current_index < haystack.length) {\n\t\t\tconst next_index = current_index + chunksize\n\t\t\t// subarray doesn't copy\n\t\t\tconst chunk = haystack.subarray(current_index, next_index)\n\t\t\tconst decoded = decoder.decode(chunk, { stream: true })\n\n\t\t\tconst text = needle_buffer + decoded\n\n\t\t\tlet match: RegExpExecArray | null\n\t\t\tlet last_index = -1\n\t\t\twhile ((match = needle.exec(text)) !== null) {\n\t\t\t\tlast_index = match.index - needle_buffer.length\n\t\t\t\tposition = full_length + last_index\n\t\t\t\tbreak outer\n\t\t\t}\n\n\t\t\tcurrent_index = next_index\n\t\t\tfull_length += decoded.length\n\n\t\t\t// Check that the buffer doesn't itself include the needle\n\t\t\t// this would cause duplicate finds (we could also use a Set to avoid that).\n\t\t\tconst needle_index =\n\t\t\t\tlast_index > -1 ? last_index + needle_length : decoded.length - needle_length\n\t\t\tneedle_buffer = decoded.slice(needle_index)\n\t\t}\n\n\t\t// Correct for search offset.\n\t\tif (position >= 0) {\n\t\t\tposition += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex\n\t\t}\n\n\t\treturn position\n\t}\n\n\t// APNGs have an animation control chunk ('acTL') preceding the IDATs.\n\t// See: https://en.wikipedia.org/wiki/APNG#File_format\n\tconst idatIdx = indexOfSubstring(view, 'IDAT', 12)\n\tif (idatIdx >= 12) {\n\t\tconst actlIdx = indexOfSubstring(view, 'acTL', 8, idatIdx)\n\t\treturn actlIdx >= 8\n\t}\n\n\treturn false\n}\n\n// globalThis.isApng = isApng\n\n// (new TextEncoder()).encode('IDAT')\n// Decimal: [73, 68, 65, 84]\n// Hex: [0x49, 0x44, 0x41, 0x54]\n\n// (new TextEncoder()).encode('acTL')\n// Decimal: [97, 99, 84, 76]\n// Hex: [0x61, 0x63, 0x54, 0x4C]\n\n// const idatIdx = buffer.indexOf('IDAT')\n// const actlIdx = buffer.indexOf('acTL')\n"],
"mappings": "AAAA;AAAA;AAAA;AAAA;AAkCO,SAAS,eAAe,QAA8B;AAC5D,QAAM,OAAO,IAAI,WAAW,MAAM;AAElC,MACC,CAAC,QACD,EAAG,OAAO,WAAW,eAAe,OAAO,SAAS,IAAI,KAAM,gBAAgB,eAC9E,KAAK,SAAS,IACb;AACD,WAAO;AAAA,EACR;AAEA,QAAM,QACL,KAAK,CAAC,MAAM,OACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM,MACZ,KAAK,CAAC,MAAM;AAEb,MAAI,CAAC,OAAO;AACX,WAAO;AAAA,EACR;AAeA,WAAS,iBACR,UACA,QACA,WACA,WACA,YAAY,MACX;AAKD,QAAI,CAAC,QAAQ;AACZ,aAAO;AAAA,IACR;AACA,aAAS,IAAI,OAAO,QAAQ,GAAG;AAK/B,UAAM,gBAAgB,OAAO,OAAO;AACpC,UAAM,UAAU,IAAI,YAAY;AAIhC,UAAM,uBAAuB,SAAS;AACtC,QAAI,OAAO,cAAc,aAAa;AACrC,kBAAY;AAAA,IACb;AACA,QAAI,aAAa,wBAAwB,aAAa,KAAK,aAAa,WAAW;AAClF,aAAO;AAAA,IACR;AACA,eAAW,SAAS,SAAS,WAAW,SAAS;AAEjD,QAAI,WAAW;AACf,QAAI,gBAAgB;AACpB,QAAI,cAAc;AAClB,QAAI,gBAAgB;AAEpB,UAAO,QAAO,gBAAgB,SAAS,QAAQ;AAC9C,YAAM,aAAa,gBAAgB;AAEnC,YAAM,QAAQ,SAAS,SAAS,eAAe,UAAU;AACzD,YAAM,UAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAEtD,YAAM,OAAO,gBAAgB;AAE7B,UAAI;AACJ,UAAI,aAAa;AACjB,cAAQ,QAAQ,OAAO,KAAK,IAAI,OAAO,MAAM;AAC5C,qBAAa,MAAM,QAAQ,cAAc;AACzC,mBAAW,cAAc;AACzB,cAAM;AAAA,MACP;AAEA,sBAAgB;AAChB,qBAAe,QAAQ;AAIvB,YAAM,eACL,aAAa,KAAK,aAAa,gBAAgB,QAAQ,SAAS;AACjE,sBAAgB,QAAQ,MAAM,YAAY;AAAA,IAC3C;AAGA,QAAI,YAAY,GAAG;AAClB,kBAAY,aAAa,IAAI,YAAY,uBAAuB;AAAA,IACjE;AAEA,WAAO;AAAA,EACR;AAIA,QAAM,UAAU,iBAAiB,MAAM,QAAQ,EAAE;AACjD,MAAI,WAAW,IAAI;AAClB,UAAM,UAAU,iBAAiB,MAAM,QAAQ,GAAG,OAAO;AACzD,WAAO,WAAW;AAAA,EACnB;AAEA,SAAO;AACR;",
"names": []
}