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);
}
}