File size: 3,499 Bytes
fab7cea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Image size from the file header, without decoding: PNG, JPEG (with EXIF orientation) and WebP.
// Anything else (GIF, BMP, HEIC, AVIF, …) returns null and is refused before any decoding.
// Returns { width, height, format, rotated } with width/height as displayed (after EXIF rotation), or null.

const HEAD_BYTES = 512 * 1024;

export async function readImageInfo(file) {
  const buf = new Uint8Array(await file.slice(0, HEAD_BYTES).arrayBuffer());
  return png(buf) ?? jpeg(buf) ?? webp(buf);
}

const u16be = (b, i) => (b[i] << 8) | b[i + 1];
const u16le = (b, i) => b[i] | (b[i + 1] << 8);
const u32be = (b, i) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0;
const u32le = (b, i) => (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0;
const ascii = (b, i, n) => String.fromCharCode(...b.subarray(i, i + n));

function result(width, height, format, orientation = 1) {
  if (!width || !height) return null;
  const rotated = orientation >= 5 && orientation <= 8;
  return { width: rotated ? height : width, height: rotated ? width : height, format, rotated };
}

function png(b) {
  if (b.length < 24 || u32be(b, 0) !== 0x89504e47 || ascii(b, 12, 4) !== "IHDR") return null;
  let at = 8;
  while (at + 8 <= b.length) {
    const len = u32be(b, at);
    const type = ascii(b, at + 4, 4);
    if (type === "acTL") return { ...result(u32be(b, 16), u32be(b, 20), "png"), animated: true };
    if (type === "IDAT") return result(u32be(b, 16), u32be(b, 20), "png");
    at += 12 + len;
  }
  return null;
}

function exifOrientation(b, start, len) {
  if (ascii(b, start, 4) !== "Exif") return 1;
  const tiff = start + 6;
  if (tiff + 8 > b.length) return 1;
  const little = ascii(b, tiff, 2) === "II";
  const r16 = (i) => (little ? u16le(b, i) : u16be(b, i));
  const r32 = (i) => (little ? u32le(b, i) : u32be(b, i));
  const ifd = tiff + r32(tiff + 4);
  if (ifd + 2 > Math.min(b.length, start + len)) return 1;
  const count = r16(ifd);
  for (let k = 0; k < count; k++) {
    const e = ifd + 2 + k * 12;
    if (e + 12 > b.length) break;
    if (r16(e) === 0x0112) return r16(e + 8) || 1;
  }
  return 1;
}

function jpeg(b) {
  if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
  let at = 2;
  let orientation = 1;
  while (at + 4 <= b.length) {
    if (b[at] !== 0xff) return null;
    const marker = b[at + 1];
    if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) {
      at += 2;
      continue;
    }
    const len = u16be(b, at + 2);
    if (marker === 0xe1) orientation = exifOrientation(b, at + 4, len - 2);
    const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
    if (isSof && at + 9 <= b.length) return result(u16be(b, at + 7), u16be(b, at + 5), "jpeg", orientation);
    at += 2 + len;
  }
  return null;
}

function webp(b) {
  if (b.length < 30 || ascii(b, 0, 4) !== "RIFF" || ascii(b, 8, 4) !== "WEBP") return null;
  const kind = ascii(b, 12, 4);
  if (kind === "VP8 ") return result(u16le(b, 26) & 0x3fff, u16le(b, 28) & 0x3fff, "webp");
  if (kind === "VP8L") {
    const bits = u32le(b, 21);
    return result((bits & 0x3fff) + 1, ((bits >> 14) & 0x3fff) + 1, "webp");
  }
  if (kind === "VP8X") {
    const animated = (b[20] & 0x02) !== 0;
    const w = 1 + (b[24] | (b[25] << 8) | (b[26] << 16));
    const h = 1 + (b[27] | (b[28] << 8) | (b[29] << 16));
    return { ...result(w, h, "webp"), animated };
  }
  return null;
}