compress.ts

src/core/compress.ts · raw · all examples · site home

Exports: BytesInputpipeByteReaderCompression

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import type { TypedArray } from "./formats";

// Bytes of a file: given as bytes, an ArrayBuffer or a stream
export type BytesInput = Uint8Array<ArrayBuffer> | ArrayBuffer | ReadableStream<Uint8Array>;

function transposeBytes(data: Uint8Array, stride: number) {
  if (data.byteLength % stride !== 0) {
    throw new Error("Data length must be divisible by the byte stride");
  }

  const output = new Uint8Array(data.byteLength);
  let out = 0;
  for (let offset = 0; offset < stride; offset += 1) {
    for (let i = offset; i < data.byteLength; i += stride) {
      output[out++] = data[i];
    }
  }
  return output;
}

export async function pipe(data: Uint8Array<ArrayBuffer>, transform: CompressionStream | DecompressionStream) {
  const stream = new Blob([data]).stream().pipeThrough(transform);
  return new Uint8Array(await new Response(stream).arrayBuffer());
}

// Bytes taken in order from bytes or a stream that may split anywhere:
// read(n) resolves with exactly n bytes (a view when they sit in one chunk,
// else a copy), peek(n) without taking them, skip(n) without copying,
// rest() the remainder, stream() the remainder as a stream; cancel() stops
// the source. Bytes given whole are never copied.
export class ByteReader {
  // Bytes taken so far
  position = 0;
  // Unread chunks from `head` on, the first from `offset`
  private chunks: Uint8Array[] = [];
  private head = 0;
  private offset = 0;
  private buffered = 0;
  private reader?: ReadableStreamDefaultReader<Uint8Array>;
  private done = false;
  private label: string;

  // `label` names the data in errors
  constructor(input: BytesInput, label = "Data") {
    this.label = label;
    if (input instanceof ReadableStream) {
      this.reader = input.getReader();
      return;
    }
    const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input;
    if (bytes.byteLength > 0) {
      this.chunks.push(bytes);
    }
    this.buffered = bytes.byteLength;
    this.done = true;
  }

  // Buffers at least n bytes, or all that remain; returns how many are
  private async fill(n: number): Promise<number> {
    while (this.buffered < n && !this.done) {
      const { done, value } = await this.reader!.read();
      if (done) {
        this.done = true;
      } else if (value.byteLength > 0) {
        this.chunks.push(value);
        this.buffered += value.byteLength;
      }
    }
    return this.buffered;
  }

  private take(n: number, consume: boolean): Uint8Array<ArrayBuffer> {
    const first = this.chunks[this.head];
    let out: Uint8Array<ArrayBuffer>;
    if (n === 0) {
      out = new Uint8Array(0);
    } else if (first.byteLength - this.offset >= n) {
      out = first.subarray(this.offset, this.offset + n) as Uint8Array<ArrayBuffer>;
    } else {
      out = new Uint8Array(n);
      let at = 0;
      for (let k = this.head, from = this.offset; at < n; k += 1, from = 0) {
        const part = this.chunks[k].subarray(from, from + n - at);
        out.set(part, at);
        at += part.byteLength;
      }
    }
    if (consume) {
      this.drop(n);
    }
    return out;
  }

  private drop(n: number) {
    this.position += n;
    this.buffered -= n;
    while (n > 0) {
      const left = this.chunks[this.head].byteLength - this.offset;
      if (n < left) {
        this.offset += n;
        break;
      }
      n -= left;
      this.head += 1;
      this.offset = 0;
    }
    if (this.head > 64 && this.head * 2 > this.chunks.length) {
      this.chunks = this.chunks.slice(this.head);
      this.head = 0;
    }
  }

  async read(n: number): Promise<Uint8Array<ArrayBuffer>> {
    if ((await this.fill(n)) < n) {
      throw new Error(`${this.label} ended unexpectedly`);
    }
    return this.take(n, true);
  }

  // Up to n bytes, fewer at the end
  async peek(n: number): Promise<Uint8Array<ArrayBuffer>> {
    return this.take(Math.min(n, await this.fill(n)), false);
  }

  async skip(n: number) {
    while (n > 0) {
      const have = await this.fill(1);
      if (have === 0) {
        throw new Error(`${this.label} ended unexpectedly`);
      }
      const step = Math.min(n, have);
      this.drop(step);
      n -= step;
    }
  }

  async rest(): Promise<Uint8Array<ArrayBuffer>> {
    return this.take(await this.fill(Infinity), true);
  }

  async atEnd(): Promise<boolean> {
    return (await this.fill(1)) === 0;
  }

  // The bytes not taken yet, buffered ones first
  stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
    return new ReadableStream({
      pull: async (controller) => {
        if ((await this.fill(1)) === 0) {
          controller.close();
          return;
        }
        controller.enqueue(this.take(this.chunks[this.head].byteLength - this.offset, true));
      },
      cancel: () => this.cancel(),
    });
  }

  cancel() {
    void this.reader?.cancel().catch(() => {});
    this.done = true;
    this.chunks = [];
    this.head = 0;
    this.buffered = 0;
    this.offset = 0;
  }
}

// Plain gzip and reading bytes, for the file formats, and byte-transposed
// gzip: byte k of every element is stored together, which compresses
// element data (float exponents, packed fields) much better.
export class Compression {
  static gzip(bytes: Uint8Array<ArrayBuffer>) {
    return pipe(bytes, new CompressionStream("gzip"));
  }

  static gunzip(bytes: Uint8Array<ArrayBuffer>) {
    return pipe(bytes, new DecompressionStream("gzip"));
  }

  static async readBytes(input: BytesInput): Promise<Uint8Array<ArrayBuffer>> {
    if (input instanceof ArrayBuffer) {
      return new Uint8Array(input);
    }
    if (input instanceof Uint8Array) {
      return input;
    }
    return new Uint8Array(await new Response(input as ReadableStream<Uint8Array<ArrayBuffer>>).arrayBuffer());
  }

  // `values` holds whole elements of `elementBytes` bytes
  static async compressTransposed(values: TypedArray, elementBytes: number): Promise<Uint8Array<ArrayBuffer>> {
    const bytes = new Uint8Array(values.buffer, values.byteOffset, values.byteLength);
    return pipe(transposeBytes(bytes, elementBytes), new CompressionStream("gzip"));
  }

  static async decompressTransposed(bytes: Uint8Array<ArrayBuffer>, elementBytes: number): Promise<Uint8Array<ArrayBuffer>> {
    const transposed = await pipe(bytes, new DecompressionStream("gzip"));
    if (transposed.byteLength % elementBytes !== 0) {
      throw new Error("Decompressed length must be divisible by the element size");
    }
    // Transposing back is a transpose with the element count as the stride
    return transposed.byteLength === 0 ? transposed : transposeBytes(transposed, transposed.byteLength / elementBytes);
  }
}