81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
import sharp from "sharp";
|
|
|
|
/** Real encoded images, generated rather than checked in, so a fixture can never drift from what the
|
|
* pipeline actually accepts. */
|
|
export async function makeImage(options: {
|
|
readonly width: number;
|
|
readonly height: number;
|
|
readonly format: "png" | "jpeg" | "webp";
|
|
readonly alpha?: boolean;
|
|
readonly colour?: { r: number; g: number; b: number };
|
|
}): Promise<Uint8Array> {
|
|
const colour = options.colour ?? { b: 40, g: 120, r: 200 };
|
|
const pipeline = sharp({
|
|
create: {
|
|
background: { ...colour, alpha: options.alpha === true ? 0.5 : 1 },
|
|
channels: options.alpha === true ? 4 : 3,
|
|
height: options.height,
|
|
width: options.width,
|
|
},
|
|
});
|
|
const encoded =
|
|
options.format === "png"
|
|
? await pipeline.png().toBuffer()
|
|
: options.format === "jpeg"
|
|
? await pipeline.jpeg().toBuffer()
|
|
: await pipeline.webp().toBuffer();
|
|
return new Uint8Array(encoded);
|
|
}
|
|
|
|
/** A genuine two-frame animated WebP. Signage refuses animation: a signage slide is a still frame,
|
|
* and an animated one would loop out of step with the authoritative slot deadline. */
|
|
export async function makeAnimatedWebp(): Promise<Uint8Array> {
|
|
const frame = (red: number) =>
|
|
sharp({
|
|
create: { background: { alpha: 1, b: 0, g: 0, r: red }, channels: 4, height: 8, width: 8 },
|
|
})
|
|
.raw()
|
|
.toBuffer();
|
|
const frames = Buffer.concat([await frame(255), await frame(0)]);
|
|
const encoded = await sharp(frames, {
|
|
raw: { channels: 4, height: 16, pageHeight: 8, width: 8 },
|
|
})
|
|
.webp({ delay: [100, 100], loop: 0 })
|
|
.toBuffer();
|
|
return new Uint8Array(encoded);
|
|
}
|
|
|
|
/** A genuine two-frame animated GIF. GIF is refused outright, but animation is detected first, so
|
|
* this proves the *animation* rule rather than the format rule. */
|
|
export async function makeAnimatedGif(): Promise<Uint8Array> {
|
|
const frame = (red: number) =>
|
|
sharp({
|
|
create: { background: { alpha: 1, b: 0, g: 0, r: red }, channels: 4, height: 8, width: 8 },
|
|
})
|
|
.raw()
|
|
.toBuffer();
|
|
const frames = Buffer.concat([await frame(255), await frame(0)]);
|
|
const encoded = await sharp(frames, {
|
|
raw: { channels: 4, height: 16, pageHeight: 8, width: 8 },
|
|
})
|
|
.gif({ delay: [100, 100], loop: 0 })
|
|
.toBuffer();
|
|
return new Uint8Array(encoded);
|
|
}
|
|
|
|
export function bytesOf(value: string): Uint8Array {
|
|
return new TextEncoder().encode(value);
|
|
}
|
|
|
|
/** A minimal, genuinely well-formed SVG. Signage refuses it: an SVG is a document with script and
|
|
* external-reference capability, not a raster image. */
|
|
export const SVG_DOCUMENT =
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10"/></svg>';
|
|
|
|
/** The first bytes of a PDF. Refused for the same reason as everything else that is not a still
|
|
* raster image. */
|
|
export const PDF_DOCUMENT = "%PDF-1.7\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>";
|
|
|
|
/** A GIF89a header with one tiny frame. */
|
|
export const GIF_DOCUMENT_BASE64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|