fix(media): preserve GIF animation, skip JPEG optimization
- Skip JPEG optimization for image/gif content type (both local and URL) - Preserves animation in uploaded GIFs to Discord/other providers - Added tests for GIF preservation from local files and URLs - Updated changelog
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
## 2.0.0-beta5 — Unreleased
|
||||
|
||||
### Fixed
|
||||
- Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).
|
||||
|
||||
### Breaking
|
||||
- Skills config schema moved under `skills.*`:
|
||||
- `skillsLoad.extraDirs` → `skills.load.extraDirs`
|
||||
|
||||
@@ -75,4 +75,58 @@ describe("web media loading", () => {
|
||||
|
||||
fetchMock.mockRestore();
|
||||
});
|
||||
|
||||
it("preserves GIF animation by skipping JPEG optimization", async () => {
|
||||
// Create a minimal valid GIF (1x1 pixel)
|
||||
// GIF89a header + minimal image data
|
||||
const gifBuffer = Buffer.from([
|
||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // GIF89a
|
||||
0x01, 0x00, 0x01, 0x00, // 1x1 dimensions
|
||||
0x00, 0x00, 0x00, // no global color table
|
||||
0x2c, 0x00, 0x00, 0x00, 0x00, // image descriptor
|
||||
0x01, 0x00, 0x01, 0x00, 0x00, // 1x1 image
|
||||
0x02, 0x01, 0x44, 0x00, 0x3b, // minimal LZW data + trailer
|
||||
]);
|
||||
|
||||
const file = path.join(os.tmpdir(), `clawdis-media-${Date.now()}.gif`);
|
||||
tmpFiles.push(file);
|
||||
await fs.writeFile(file, gifBuffer);
|
||||
|
||||
const result = await loadWebMedia(file, 1024 * 1024);
|
||||
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.contentType).toBe("image/gif");
|
||||
// GIF should NOT be converted to JPEG
|
||||
expect(result.buffer.slice(0, 3).toString()).toBe("GIF");
|
||||
});
|
||||
|
||||
it("preserves GIF from URL without JPEG conversion", async () => {
|
||||
const gifBytes = new Uint8Array([
|
||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61,
|
||||
0x01, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00,
|
||||
0x2c, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x01, 0x00, 0x00,
|
||||
0x02, 0x01, 0x44, 0x00, 0x3b,
|
||||
]);
|
||||
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
|
||||
ok: true,
|
||||
body: true,
|
||||
arrayBuffer: async () => gifBytes.buffer.slice(gifBytes.byteOffset, gifBytes.byteOffset + gifBytes.byteLength),
|
||||
headers: { get: () => "image/gif" },
|
||||
status: 200,
|
||||
} as Response);
|
||||
|
||||
const result = await loadWebMedia(
|
||||
"https://example.com/animation.gif",
|
||||
1024 * 1024,
|
||||
);
|
||||
|
||||
expect(result.kind).toBe("image");
|
||||
expect(result.contentType).toBe("image/gif");
|
||||
expect(result.buffer.slice(0, 3).toString()).toBe("GIF");
|
||||
|
||||
fetchMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,17 @@ export async function loadWebMedia(
|
||||
maxBytesForKind(kind),
|
||||
);
|
||||
if (kind === "image") {
|
||||
// Skip optimization for GIFs to preserve animation
|
||||
if (contentType === "image/gif") {
|
||||
if (array.length > cap) {
|
||||
throw new Error(
|
||||
`GIF exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
||||
array.length / (1024 * 1024)
|
||||
).toFixed(2)}MB)`,
|
||||
);
|
||||
}
|
||||
return { buffer: array, contentType, kind, fileName };
|
||||
}
|
||||
return { ...(await optimizeAndClampImage(array, cap)), fileName };
|
||||
}
|
||||
if (array.length > cap) {
|
||||
@@ -105,6 +116,17 @@ export async function loadWebMedia(
|
||||
maxBytesForKind(kind),
|
||||
);
|
||||
if (kind === "image") {
|
||||
// Skip optimization for GIFs to preserve animation
|
||||
if (mime === "image/gif") {
|
||||
if (data.length > cap) {
|
||||
throw new Error(
|
||||
`GIF exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
||||
data.length / (1024 * 1024)
|
||||
).toFixed(2)}MB)`,
|
||||
);
|
||||
}
|
||||
return { buffer: data, contentType: mime, kind, fileName };
|
||||
}
|
||||
return { ...(await optimizeAndClampImage(data, cap)), fileName };
|
||||
}
|
||||
if (data.length > cap) {
|
||||
|
||||
Reference in New Issue
Block a user