Open History to see the last commit
1import { describe, expect, it } from "vite-plus/test";
2
3import { readImageDimensions } from "./imageDimensions.ts";
4
5function bytes(...parts: ReadonlyArray<number | string | ReadonlyArray<number>>): Uint8Array {
6 const out: number[] = [];
7 for (const part of parts) {
8 if (typeof part === "string") for (const c of part) out.push(c.charCodeAt(0));
9 else if (typeof part === "number") out.push(part);
10 else out.push(...part);
11 }
12 return Uint8Array.from(out);
13}
14
15const u32 = (n: number) => [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff];
16const u16 = (n: number) => [(n >>> 8) & 0xff, n & 0xff];
17const u16le = (n: number) => [n & 0xff, (n >>> 8) & 0xff];
18
19describe("readImageDimensions", () => {
20 it("reads a PNG IHDR", () => {
21 const png = bytes(
22 [0x89],
23 "PNG",
24 [0x0d, 0x0a, 0x1a, 0x0a],
25 u32(13),
26 "IHDR",
27 u32(1600),
28 u32(900),
29 );
30 expect(readImageDimensions(png)).toEqual({ width: 1600, height: 900 });
31 });
32
33 it("reads a GIF logical screen", () => {
34 expect(readImageDimensions(bytes("GIF89a", u16le(320), u16le(240)))).toEqual({
35 width: 320,
36 height: 240,
37 });
38 });
39
40 it("reads a JPEG start-of-frame after an APP segment", () => {
41 const app1 = bytes([0xff, 0xe1], u16(2 + 6), "Exif\0\0");
42 const sof0 = bytes([0xff, 0xc0], u16(17), [8], u16(1400), u16(720));
43 expect(readImageDimensions(bytes([0xff, 0xd8], [...app1], [...sof0]))).toEqual({
44 width: 720,
45 height: 1400,
46 });
47 });
48
49 it("swaps the axes for a JPEG whose EXIF orientation rotates it 90 degrees", () => {
50 const tiff = [
51 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00,
52 0x00, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
53 ];
54 const exif = ["Exif\0\0", tiff] as const;
55 const app1 = bytes([0xff, 0xe1], u16(2 + 6 + tiff.length), ...exif);
56 const sof0 = bytes([0xff, 0xc0], u16(17), [8], u16(3024), u16(4032));
57 expect(readImageDimensions(bytes([0xff, 0xd8], [...app1], [...sof0]))).toEqual({
58 width: 3024,
59 height: 4032,
60 });
61 const xmp = bytes([0xff, 0xe1], u16(2 + 29), "http://ns.adobe.com/xap/1.0/\0");
62 expect(readImageDimensions(bytes([0xff, 0xd8], [...app1], [...xmp], [...sof0]))).toEqual({
63 width: 3024,
64 height: 4032,
65 });
66 const upright = [...tiff];
67 upright[19] = 0x01;
68 const app1Upright = bytes([0xff, 0xe1], u16(2 + 6 + upright.length), "Exif\0\0", upright);
69 expect(readImageDimensions(bytes([0xff, 0xd8], [...app1Upright], [...sof0]))).toEqual({
70 width: 4032,
71 height: 3024,
72 });
73 });
74
75 it("steps over standalone TEM and restart markers", () => {
76 const sof0 = bytes([0xff, 0xc0], u16(17), [8], u16(10), u16(20));
77 expect(readImageDimensions(bytes([0xff, 0xd8], [0xff, 0x01], [0xff, 0xd3], [...sof0]))).toEqual(
78 { width: 20, height: 10 },
79 );
80 });
81
82 it("does not mistake a Huffman table marker for a frame", () => {
83 const dht = bytes([0xff, 0xc4], u16(4), [0, 0]);
84 const sof2 = bytes([0xff, 0xc2], u16(17), [8], u16(10), u16(20));
85 expect(readImageDimensions(bytes([0xff, 0xd8], [...dht], [...sof2]))).toEqual({
86 width: 20,
87 height: 10,
88 });
89 });
90
91 it("reads each WebP container flavour", () => {
92 const riff = (chunk: string, body: ReadonlyArray<number>) =>
93 bytes("RIFF", u32(0), "WEBP", chunk, u32(body.length), body);
94 expect(
95 readImageDimensions(riff("VP8 ", [0, 0, 0, 0x9d, 0x01, 0x2a, ...u16le(800), ...u16le(600)])),
96 ).toEqual({ width: 800, height: 600 });
97 const packed = (800 - 1) | ((600 - 1) << 14);
98 expect(
99 readImageDimensions(
100 riff("VP8L", [
101 0x2f,
102 packed & 0xff,
103 (packed >>> 8) & 0xff,
104 (packed >>> 16) & 0xff,
105 (packed >>> 24) & 0xff,
106 ]),
107 ),
108 ).toEqual({ width: 800, height: 600 });
109 expect(
110 readImageDimensions(
111 riff("VP8X", [
112 0,
113 0,
114 0,
115 0,
116 799 & 0xff,
117 (799 >> 8) & 0xff,
118 0,
119 599 & 0xff,
120 (599 >> 8) & 0xff,
121 0,
122 ]),
123 ),
124 ).toEqual({ width: 800, height: 600 });
125 });
126
127 it("returns null for unsupported, truncated, or zero-sized input", () => {
128 expect(readImageDimensions(bytes("<svg xmlns='http://www.w3.org/2000/svg'/>"))).toBeNull();
129 expect(readImageDimensions(bytes([0x89], "PNG"))).toBeNull();
130 expect(readImageDimensions(bytes("GIF89a", u16le(0), u16le(240)))).toBeNull();
131 expect(readImageDimensions(bytes([0xff, 0xd8], [0xff, 0xd9]))).toBeNull();
132 expect(readImageDimensions(new Uint8Array())).toBeNull();
133 });
134});