Files
EpicNext-Cms/src/lib/imager-png.ts
T
openhands f3407ebfdf
CI / check (push) Successful in 37s
CI / tests-integration (push) Successful in 2m2s
CI / tests-unit (push) Successful in 2m9s
CI / tests-ui (push) Successful in 2m44s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 3m12s
fix(imaging): support .hab assets, cache avatars/badges, harden routes
The hotel moved from .nitro to .hab bundles, so the imager must treat .hab
like .nitro and the public avatar endpoints must accept the figure strings
the client sends (including extra trailing segments such as `.0`, which the
old FIGURE_RE rejected with a 400).

- imager-upstream: relax FIGURE_RE, extend parseAvatarQuery test coverage
- api/imaging/avatar + /imaging: shared validation, placeholder and ETag/304
- api/imaging/badge: single-flight upstream fetch and conditional responses
- imager-http/imager-png: strong ETag helpers and APNG passthrough guard
- csp + next/image: unoptimized external images, tightened CSP
- deps: override postcss-selector-parser to ^7.1.6
- also fold in the pending drizzle draft snapshot and stray test cleanup
2026-10-14 20:40:50 +02:00

44 lines
1.4 KiB
TypeScript

import "server-only";
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
/**
* Whether a buffer is an animated PNG (APNG).
*
* APNG is a PNG carrying an `acTL` control chunk before `IDAT`; a plain PNG has
* none. `sharp`/libvips in this build cannot decode APNG *animation* — it reads
* only the first frame and re-encodes that — so a WebP/AVIF transcode of an
* APNG silently turns a moving avatar into a still one. Callers that care about
* quality use this to leave animated renders untouched and only re-encode the
* static ones. Chunk walking stops at `IDAT`/`IEND` because the spec requires
* `acTL` to precede them.
*/
export function isAnimatedPng(bytes: Uint8Array): boolean {
if (bytes.length < 8) return false;
for (let i = 0; i < 8; i++) {
if (bytes[i] !== PNG_SIGNATURE[i]) return false;
}
let offset = 8;
while (offset + 8 <= bytes.length) {
const length =
((bytes[offset] << 24) |
(bytes[offset + 1] << 16) |
(bytes[offset + 2] << 8) |
bytes[offset + 3]) >>>
0;
const type = String.fromCharCode(
bytes[offset + 4],
bytes[offset + 5],
bytes[offset + 6],
bytes[offset + 7],
);
if (type === "acTL") return true;
if (type === "IDAT" || type === "IEND") return false;
// A corrupt length would loop forever; bail instead.
if (length > bytes.length) return false;
offset += 12 + length;
}
return false;
}