All files / qoi / encode.ts

100.00% Branches 16/16
100.00% Lines 45/45
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
x2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x2
x2
x2
 
x30
x31
x31
x30
x31
x31
x30
x31
x31
x55
 
x55
x30
x30
x31
x31
x31
x31
 
x55
x55
 
x330
x55
x55
x55
x55
x55
x55
x55
x30
x30
 
x30
x30
x30
x30
x31
x31
x31
x31
 
x31
 
x540
x54
x30




















































































import { createEncoder } from "./_encoder.ts";
import type { QOIOptions } from "./types.ts";

/**
 * encodeQOI is a function that encodes raw image data into the QOI image
 * format. The raw image data is expected to be in a sequence of
 * `[ r, g, b, a ]` numbers.
 *
 * @example
 * ```ts
 * import { encodeQOI } from "@img/qoi";
 *
 * await Deno.mkdir(".output/", { recursive: true });
 *
 * const rawData = await new Response(ReadableStream.from(async function* () {
 *   for (let r = 0; r < 256; ++r) {
 *     for (let c = 0; c < 256; ++c) {
 *       yield Uint8Array.from([255 - r, c, r, 255]);
 *     }
 *   }
 * }())).bytes();
 *
 * await Deno.writeFile(".output/image.qoi", encodeQOI(rawData, {
 *   width: 256,
 *   height: 256,
 *   channels: "rgb",
 *   colorspace: 0,
 * }));
 * ```
 *
 * @param input The raw image data to be encoded.
 * @returns The encoded image data.
 *
 * @module
 */
export function encodeQOI(
  input: Uint8Array | Uint8ClampedArray,
  options: QOIOptions,
): Uint8Array {
  if (options.width < 0 || Number.isNaN(options.width)) {
    throw new RangeError("Width cannot be a negative number or NaN");
  }
  if (options.height < 0 || Number.isNaN(options.height)) {
    throw new RangeError("Height cannot be a negative number or NaN");
  }
  if (input.length % 4 !== 0) {
    throw new RangeError("Unexpected number of bytes from input");
  }
  const isRGB = options.channels === "rgb";

  const originalSize = input.length;
  const maxSize = 14 + originalSize + (isRGB ? 0 : originalSize / 4) + 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);

  output.set([113, 111, 105, 102]);
  {
    const view = new DataView(new ArrayBuffer(4));
    view.setUint32(0, options.width);
    output.set(new Uint8Array(view.buffer), 4);
    view.setUint32(0, options.height);
    output.set(new Uint8Array(view.buffer), 8);
  }
  output[12] = isRGB ? 3 : 4;
  output[13] = options.colorspace;

  const encoder = createEncoder(isRGB);
  const { i, o } = encoder(output, maxSize - originalSize, 14);
  const count = (i - (maxSize - originalSize)) / 4;
  if (options.width * options.height !== count) {
    throw new RangeError(
      `Width * height (${
        options.width * options.height
      }) does not equal pixels encoded (${count}))`,
    );
  }

  output.set([0, 0, 0, 0, 0, 0, 0, 1], o);
  return output.subarray(0, o + 8);
}