310 lines
7.3 KiB
TypeScript
310 lines
7.3 KiB
TypeScript
import { vi } from "vitest";
|
|
|
|
import type {
|
|
ImageProbeScheduler,
|
|
} from "../../src/adapters/browser-transfer/image-cdn/browser-image-probe.ts";
|
|
import type {
|
|
ImageCapabilityVerificationScheduler,
|
|
} from "../../src/adapters/browser-transfer/image-cdn/image-cdn-runtime.ts";
|
|
|
|
export function responseAt(
|
|
url: string,
|
|
body: Uint8Array,
|
|
init: ResponseInit,
|
|
): Response {
|
|
const responseBytes = new Uint8Array(body.byteLength);
|
|
responseBytes.set(body);
|
|
const response = new Response(responseBytes.buffer, init);
|
|
Object.defineProperty(response, "url", {
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: url,
|
|
});
|
|
return response;
|
|
}
|
|
|
|
export function publicImageHeaders(
|
|
mediaType: string,
|
|
contentLength?: number,
|
|
): Headers {
|
|
const headers = new Headers({
|
|
"cache-control":
|
|
"public, max-age=31536000, s-maxage=31536000, immutable",
|
|
"content-type": mediaType,
|
|
vary: "Accept-Encoding",
|
|
});
|
|
if (contentLength !== undefined) {
|
|
headers.set("content-length", String(contentLength));
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
export function pngBytes(
|
|
width: number,
|
|
height: number,
|
|
animated = false,
|
|
): Uint8Array {
|
|
const header = new Uint8Array(13);
|
|
const headerView = new DataView(header.buffer);
|
|
headerView.setUint32(0, width);
|
|
headerView.setUint32(4, height);
|
|
header[8] = 8;
|
|
header[9] = 6;
|
|
return concatenateBytes([
|
|
Uint8Array.from([
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
]),
|
|
pngChunk("IHDR", header),
|
|
...(animated
|
|
? [pngChunk("acTL", new Uint8Array(8))]
|
|
: []),
|
|
pngChunk("IDAT", new Uint8Array()),
|
|
pngChunk("IEND", new Uint8Array()),
|
|
]);
|
|
}
|
|
|
|
function pngChunk(type: string, payload: Uint8Array): Uint8Array {
|
|
const chunk = new Uint8Array(12 + payload.byteLength);
|
|
const view = new DataView(chunk.buffer);
|
|
view.setUint32(0, payload.byteLength);
|
|
writeAscii(chunk, 4, type);
|
|
chunk.set(payload, 8);
|
|
return chunk;
|
|
}
|
|
|
|
export function jpegBytes(width: number, height: number): Uint8Array {
|
|
return Uint8Array.from([
|
|
0xff,
|
|
0xd8,
|
|
0xff,
|
|
0xc0,
|
|
0x00,
|
|
0x11,
|
|
0x08,
|
|
(height >>> 8) & 0xff,
|
|
height & 0xff,
|
|
(width >>> 8) & 0xff,
|
|
width & 0xff,
|
|
0x03,
|
|
0x01,
|
|
0x11,
|
|
0x00,
|
|
0x02,
|
|
0x11,
|
|
0x00,
|
|
0x03,
|
|
0x11,
|
|
0x00,
|
|
0xff,
|
|
0xda,
|
|
]);
|
|
}
|
|
|
|
export function webpBytes(
|
|
width: number,
|
|
height: number,
|
|
animated = false,
|
|
): Uint8Array {
|
|
const chunkType = animated ? "VP8X" : "VP8 ";
|
|
const payload = new Uint8Array(10);
|
|
if (animated) {
|
|
payload[0] = 0x02;
|
|
writeUint24LittleEndian(payload, 4, width - 1);
|
|
writeUint24LittleEndian(payload, 7, height - 1);
|
|
} else {
|
|
payload.set([0x9d, 0x01, 0x2a], 3);
|
|
const view = new DataView(payload.buffer);
|
|
view.setUint16(6, width, true);
|
|
view.setUint16(8, height, true);
|
|
}
|
|
const chunk = concatenateBytes([
|
|
asciiBytes(chunkType),
|
|
littleEndianUint32(payload.byteLength),
|
|
payload,
|
|
]);
|
|
return concatenateBytes([
|
|
asciiBytes("RIFF"),
|
|
littleEndianUint32(4 + chunk.byteLength),
|
|
asciiBytes("WEBP"),
|
|
chunk,
|
|
]);
|
|
}
|
|
|
|
export function avifBytes(
|
|
width: number,
|
|
height: number,
|
|
brand = "avif",
|
|
): Uint8Array {
|
|
const fileType = isoBox(
|
|
"ftyp",
|
|
concatenateBytes([
|
|
asciiBytes(brand),
|
|
new Uint8Array(4),
|
|
asciiBytes(brand),
|
|
]),
|
|
);
|
|
const spatialExtent = new Uint8Array(12);
|
|
const extentView = new DataView(spatialExtent.buffer);
|
|
extentView.setUint32(4, width);
|
|
extentView.setUint32(8, height);
|
|
const primaryItem = new Uint8Array(6);
|
|
new DataView(primaryItem.buffer).setUint16(4, 1);
|
|
const itemInfoEntry = new Uint8Array(13);
|
|
itemInfoEntry[0] = 2;
|
|
const itemInfoView = new DataView(itemInfoEntry.buffer);
|
|
itemInfoView.setUint16(4, 1);
|
|
writeAscii(itemInfoEntry, 8, "av01");
|
|
const itemInfo = new Uint8Array(6);
|
|
new DataView(itemInfo.buffer).setUint16(4, 1);
|
|
const propertyAssociation = new Uint8Array(12);
|
|
const associationView = new DataView(
|
|
propertyAssociation.buffer,
|
|
);
|
|
associationView.setUint32(4, 1);
|
|
associationView.setUint16(8, 1);
|
|
propertyAssociation[10] = 1;
|
|
propertyAssociation[11] = 0x81;
|
|
const properties = isoBox(
|
|
"iprp",
|
|
concatenateBytes([
|
|
isoBox("ipco", isoBox("ispe", spatialExtent)),
|
|
isoBox("ipma", propertyAssociation),
|
|
]),
|
|
);
|
|
const metadata = isoBox(
|
|
"meta",
|
|
concatenateBytes([
|
|
new Uint8Array(4),
|
|
isoBox("pitm", primaryItem),
|
|
isoBox(
|
|
"iinf",
|
|
concatenateBytes([
|
|
itemInfo,
|
|
isoBox("infe", itemInfoEntry),
|
|
]),
|
|
),
|
|
properties,
|
|
]),
|
|
);
|
|
return concatenateBytes([
|
|
fileType,
|
|
metadata,
|
|
isoBox("mdat", Uint8Array.of(0)),
|
|
]);
|
|
}
|
|
|
|
export function isoBox(type: string, payload: Uint8Array): Uint8Array {
|
|
const box = new Uint8Array(8 + payload.byteLength);
|
|
const view = new DataView(box.buffer);
|
|
view.setUint32(0, box.byteLength);
|
|
writeAscii(box, 4, type);
|
|
box.set(payload, 8);
|
|
return box;
|
|
}
|
|
|
|
export function littleEndianUint32(value: number): Uint8Array {
|
|
const bytes = new Uint8Array(4);
|
|
new DataView(bytes.buffer).setUint32(0, value, true);
|
|
return bytes;
|
|
}
|
|
|
|
export function writeUint24LittleEndian(
|
|
bytes: Uint8Array,
|
|
offset: number,
|
|
value: number,
|
|
): void {
|
|
bytes[offset] = value & 0xff;
|
|
bytes[offset + 1] = (value >>> 8) & 0xff;
|
|
bytes[offset + 2] = (value >>> 16) & 0xff;
|
|
}
|
|
|
|
export function asciiBytes(value: string): Uint8Array {
|
|
return Uint8Array.from(
|
|
[...value].map((character) => character.charCodeAt(0)),
|
|
);
|
|
}
|
|
|
|
export function writeAscii(
|
|
target: Uint8Array,
|
|
offset: number,
|
|
value: string,
|
|
): void {
|
|
target.set(asciiBytes(value), offset);
|
|
}
|
|
|
|
export function concatenateBytes(
|
|
chunks: readonly Uint8Array[],
|
|
): Uint8Array {
|
|
const combined = new Uint8Array(
|
|
chunks.reduce((total, chunk) => total + chunk.byteLength, 0),
|
|
);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
combined.set(chunk, offset);
|
|
offset += chunk.byteLength;
|
|
}
|
|
return combined;
|
|
}
|
|
|
|
export function manualImageProbeScheduler(): Readonly<{
|
|
scheduler: ImageProbeScheduler;
|
|
fire(): void;
|
|
}> {
|
|
let callback: (() => void) | undefined;
|
|
return {
|
|
scheduler: {
|
|
setTimeout(nextCallback) {
|
|
callback = nextCallback;
|
|
return 1;
|
|
},
|
|
clearTimeout: vi.fn(),
|
|
},
|
|
fire() {
|
|
if (!callback) {
|
|
throw new TypeError("No image probe timeout is scheduled.");
|
|
}
|
|
callback();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function manualCapabilityVerificationScheduler(): Readonly<{
|
|
scheduler: ImageCapabilityVerificationScheduler;
|
|
delays: readonly number[];
|
|
clearTimeout: ReturnType<typeof vi.fn>;
|
|
fire(): void;
|
|
}> {
|
|
let callback: (() => void) | undefined;
|
|
const delays: number[] = [];
|
|
const clearTimeout = vi.fn();
|
|
return {
|
|
scheduler: {
|
|
setTimeout(nextCallback, milliseconds) {
|
|
callback = nextCallback;
|
|
delays.push(milliseconds);
|
|
return 1;
|
|
},
|
|
clearTimeout,
|
|
},
|
|
delays,
|
|
clearTimeout,
|
|
fire() {
|
|
if (!callback) {
|
|
throw new TypeError(
|
|
"No capability verification timeout is scheduled.",
|
|
);
|
|
}
|
|
callback();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function base64Url(bytes: Uint8Array): string {
|
|
let binary = "";
|
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
return btoa(binary)
|
|
.replace(/\+/gu, "-")
|
|
.replace(/\//gu, "_")
|
|
.replace(/=+$/gu, "");
|
|
}
|