All files / qoi / decoder_stream.ts

100.00% Branches 24/24
100.00% Lines 93/93
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
x1
 
x1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x1
 
x1
x1
x1
x15
 
 
 
x15
x15
x29
x29
 
 
x129
x43
x129
x43
x43
x44
x44
x323
x44
x44
x43
x44
x44
x43
x44
x44
x43
x47
x47
x47
x53
x53
x53
x53
x43
x43
x43
x43
x43
x43
x43
x43
x43
x29
 
x39
x39
x39
x39
x29
x47
x47
x47
x49
x49
x49
x49
 
x47
x47
x47
x55
x55
x55
 
x55
 
x47
x47
x47
x47
 
x47
x47
x47
x47
x47
x56
x57
x57
x57
x57
 
x57
x64
x64
x47
x30
x15
 
x15
x15
 
x1
x15
x15
 
x1
x15
x15
x1


























































































































import { toByteStream } from "@std/streams/unstable-to-byte-stream";
import type { QOIOptions } from "./types.ts";
import { createDecoder } from "./_decoder.ts";

/**
 * The QOIDecoderStream is a TransformStream that decodes qoi image format into
 * raw image data. The raw data is a sequence of `[ r, g, b, a ]` numbers.
 *
 * @example
 * ```ts ignore
 * import { QOIDecoderStream } from "img/qoi";
 *
 * const rawStream = (await Deno.open("image.qoi"))
 *   .readable
 *   .pipeThrough(new QOIDecoderStream(header => console.log(header)));
 * ```
 *
 * @module
 */
export class QOIDecoderStream
  implements TransformStream<Uint8Array, Uint8Array> {
  #readable: ReadableStream<Uint8Array>;
  #writable: WritableStream<Uint8Array>;
  constructor(cb: (header: QOIOptions) => unknown = () => {}) {
    const { readable, writable } = new TransformStream<
      Uint8Array,
      Uint8Array
    >();
    this.#readable = ReadableStream.from(
      async function* (): AsyncGenerator<Uint8Array> {
        const byteStream = toByteStream(readable);
        const { width, height, isRGB } = await async function (): Promise<
          { width: number; height: number; isRGB: boolean }
        > {
          const reader = byteStream.getReader({ mode: "byob" });
          const { done, value } = await reader
            .read(new Uint8Array(14), { min: 14 });
          try {
            if (done || value.length !== 14) {
              throw new RangeError("QOI stream is too short to be valid");
            }
            if (![113, 111, 105, 102].every((x, i) => x === value[i])) {
              throw new TypeError("QOI stream had invalid magic number");
            }
            if (value[12] !== 3 && value[12] !== 4) {
              throw new TypeError("QOI stream had invalid channels");
            }
            if (value[13] !== 0 && value[13] !== 1) {
              throw new TypeError("QOI stream had invalid colorspace");
            }
          } catch (e) {
            reader.cancel(e);
            throw e;
          }
          const view = new DataView(value.buffer);
          cb({
            width: view.getUint32(4),
            height: view.getUint32(8),
            channels: value[12] === 3 ? "rgb" : "rgba",
            colorspace: value[13],
          });
          reader.releaseLock();
          return {
            width: view.getUint32(4),
            height: view.getUint32(8),
            isRGB: value[12] === 3,
          };
        }();

        const buffer = new Uint8Array(8);
        let offset = 0;
        const decoder = createDecoder();
        let count = 0;
        for await (let chunk of byteStream) {
          const originalSize = chunk.length;
          const maxSize = (offset + originalSize) * 63 * (isRGB ? 4 : 5);
          if (chunk.byteOffset) {
            const buffer = new Uint8Array(chunk.buffer);
            buffer.set(chunk);
            chunk = buffer.subarray(0, chunk.length);
          }
          // deno-lint-ignore no-explicit-any
          chunk = new Uint8Array((chunk.buffer as any).transfer(maxSize));
          chunk.set(chunk.subarray(0, originalSize), maxSize - originalSize);
          if (offset) {
            chunk.set(
              buffer.subarray(0, offset),
              maxSize - originalSize - offset,
            );
          }

          const { i, o, c, isEnd } = decoder(
            chunk,
            maxSize - originalSize - offset,
            0,
          );
          count += c;
          offset = chunk.length - i;
          if (offset) buffer.set(chunk.subarray(i));
          yield chunk.subarray(0, o);
          if (isEnd) {
            if (count !== width * height) {
              throw new RangeError(
                `QOI stream received exit code, but pixels (${count}) decoded does not match width * height (${
                  width * height
                }) in header`,
              );
            }
            return;
          }
        }
        throw new RangeError("Expected more bytes from stream");
      }(),
    );
    this.#writable = writable;
  }

  get readable(): ReadableStream<Uint8Array> {
    return this.#readable;
  }

  get writable(): WritableStream<Uint8Array> {
    return this.#writable;
  }
}