All files / qoi / decode.ts

100.00% Branches 20/20
100.00% Lines 46/46
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
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
x1
 
x15
x16
x16
x155
x16
x16
x15
x16
x16
x15
x16
x16
x25
x25
x25
x25
x15
x15
x15
 
x15
x15
x15
x15
x15
x16
x16
x16
x16
 
x25
x25
 
x25
x25
x15
x24
x25
x25
x25
x25
 
x25
x128
x32
x16
x15












































































import { createDecoder } from "./_decoder.ts";
import type { QOIOptions } from "./types.ts";

/**
 * decodeQOI is a function that decodes a QOI image into raw image data. The raw
 * image data is a sequence of `[ r, g, b, a ]` numbers.
 *
 * ```ts
 * import { decodeQOI, encodeQOI } from "@img/qoi";
 *
 * const encodedData = encodeQOI(
 *   await new Response(ReadableStream.from(async function* () {
 *     for (let r = 0; r < 256; ++r) {
 *       for (let c = 0; c < 256; ++c) {
 *         yield new Uint8Array([255 - r, c, r, 255]);
 *       }
 *     }
 *   }())).bytes(),
 *   { width: 256, height: 256, channels: "rgb", colorspace: 0 },
 * );
 *
 * console.log(decodeQOI(encodedData).header)
 * ```
 *
 * @param input The encoded image data to be decoded.
 * @returns The raw/decoded image data.
 *
 * @module
 */
export function decodeQOI(
  input: Uint8Array | Uint8ClampedArray,
): { header: QOIOptions; body: Uint8Array } {
  if (input.length < 14 + 8) {
    throw new RangeError("QOI input is too short to be valid");
  }
  if (![113, 111, 105, 102].every((x, i) => x === input[i])) {
    throw new TypeError("QOI input had invalid magic number");
  }
  if (input[12] !== 3 && input[12] !== 4) {
    throw new TypeError("QOI input had invalid channels");
  }
  if (input[13] !== 0 && input[13] !== 1) {
    throw new TypeError("QOI input had invalid colorspace");
  }
  const view = new DataView(input.buffer);
  const header: QOIOptions = {
    width: view.getUint32(4 + input.byteOffset),
    height: view.getUint32(8 + input.byteOffset),
    channels: input[12] === 3 ? "rgb" : "rgba",
    colorspace: input[13],
  };

  const originalSize = input.length;
  const maxSize = 14 +
    4 * header.width * header.height * (header.channels === "rgb" ? 1 : 1.25) +
    8;
  if (input.byteOffset) {
    const buffer = new Uint8Array(input.buffer);
    buffer.set(input);
    input = buffer.subarray(0, input.length);
  }
  // deno-lint-ignore no-explicit-any
  const output = new Uint8Array((input.buffer as any).transfer(maxSize));
  output.set(output.subarray(0, originalSize), maxSize - originalSize);

  const decoder = createDecoder();
  const { o, c, isEnd } = decoder(output, maxSize - originalSize + 14, 0);
  if (isEnd) {
    if (c !== header.width * header.height) {
      throw new RangeError(
        `QOI input received exit code, but pixels (${c}) decoded does not match width * height (${
          header.width * header.height
        }) in header`,
      );
    }
    return { header, body: output.subarray(0, o) };
  }
  throw new RangeError("Expected more bytes from input");
}