All files / qoi / _encoder.ts

100.00% Branches 32/32
100.00% Lines 83/83
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
92
93
94
95
96
97
98
99
100
101
x4
 
x4
x4
x4
x4
 
x337
x1166
x1166
x580
x337
 
x4
 
 
 
 
x55
x330
x55
x55
x13386
x55
x55
x55
x55
 
x107
x392
x392
x629
 
x629
x631
x631
x631
x392
 
x440
x442
x442
x442
 
x440
x440
 
x446
x446
x440
x482
 
x482
x482
x482
x482
x482
x482
x482
x482
x482
x482
 
x486
x486
x486
x486
x482
x520
x520
x520
x520
x520
x520
x520
x520
 
x524
x524
x520
 
x584
x584
x584
x554
 
x558
x558
x558
x558
x520
x482
x440
x392
x107
 
x126
x126
x428
x55
x55



































































































import { calcIndex } from "./_common.ts";

export function isEqual(
  previousPixel: Uint8Array,
  currentPixel: Uint8Array,
  isRGB: boolean,
): boolean {
  for (let i = 0; i < (isRGB ? 3 : 4); ++i) {
    if (previousPixel[i] !== currentPixel[i]) return false;
  }
  return true;
}

export function createEncoder(isRGB: boolean): (
  data: Uint8Array,
  i: number,
  o: number,
) => { i: number; o: number } {
  let run = 0;
  const previousPixel = new Uint8Array([0, 0, 0, 255]);
  const seenPixels: Uint8Array[] = new Array(64)
    .fill(0)
    .map((_) => new Uint8Array([0, 0, 0, 0]));
  return function (
    data: Uint8Array,
    i: number,
    o: number,
  ): { i: number; o: number } {
    for (; i <= data.length - 4; i += 4) {
      const currentPixel = data.subarray(i, i + 4);
      if (isEqual(previousPixel, currentPixel, isRGB)) {
        ++run;
        // QOI_OP_RUN
        if (run === 62) {
          data[o++] = 0b11_111101;
          run = 0;
        }
      } else {
        // QOI_OP_RUN
        if (run) {
          data[o++] = (0b11 << 6) + run - 1;
          run = 0;
        }

        const index = calcIndex(currentPixel, isRGB);
        if (isEqual(seenPixels[index], currentPixel, isRGB)) {
          // QOI_OP_INDEX
          data[o++] = (0b00 << 6) + index;
          previousPixel.set(currentPixel);
        } else {
          seenPixels[index].set(currentPixel);

          const diff = new Array(isRGB ? 3 : 4)
            .fill(0)
            .map((_, i) => currentPixel[i] - previousPixel[i]);
          previousPixel.set(currentPixel);
          if (
            -2 <= diff[0] && diff[0] <= 1 &&
            -2 <= diff[1] && diff[1] <= 1 &&
            -2 <= diff[2] && diff[2] <= 1 &&
            !diff[3]
          ) {
            // QOI_OP_DIFF
            data[o++] = (0b01 << 6) +
              (diff[0] + 2 << 4) +
              (diff[1] + 2 << 2) +
              diff[2] + 2;
          } else {
            diff[0] -= diff[1];
            diff[2] -= diff[1];
            if (
              -8 <= diff[0] && diff[0] <= 7 &&
              -32 <= diff[1] && diff[1] <= 31 &&
              -8 <= diff[2] && diff[2] <= 7 &&
              !diff[3]
            ) {
              // QOI_OP_LUMA
              data[o++] = (0b10 << 6) + diff[1] + 32;
              data[o++] = (diff[0] + 8 << 4) + diff[2] + 8;
            } else if (isRGB) {
              // QOI_OP_RGB
              data.set(currentPixel.subarray(0, 3), o + 1);
              data[o] = 0b11111110;
              o += 4;
            } else {
              // QOI_OP_RGBA
              data.set(currentPixel, o + 1);
              data[o] = 0b11111111;
              o += 5;
            }
          }
        }
      }
    }
    if (run) {
      // QOI_OP_RUN
      data[o++] = (0b11 << 6) + run - 1;
    }
    return { i, o };
  };
}