88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
import { planAssetPreviewDecode } from "../../../protocol/asset-preview-decode";
|
|
import type { AssetPreviewIdentityIR } from "../../../protocol/asset-preview";
|
|
|
|
export type AssetPreviewDisplayBackend = "MAIN_THREAD_CANVAS_2D" | "OFFSCREEN_CANVAS_2D";
|
|
|
|
export interface AssetPreviewDisplayReceiptIR {
|
|
schemaVersion: 1;
|
|
status: "READY";
|
|
backend: AssetPreviewDisplayBackend;
|
|
identitySha256: string;
|
|
contentSha256: string;
|
|
width: number;
|
|
height: number;
|
|
pixelByteLength: number;
|
|
pixelSha256: string;
|
|
nonTransparentPixels: number;
|
|
bitmapClosed: true;
|
|
}
|
|
|
|
export interface AssetPreviewDisplayResultIR {
|
|
receipt: AssetPreviewDisplayReceiptIR;
|
|
pixels: Uint8Array;
|
|
}
|
|
|
|
type PreviewCanvas = HTMLCanvasElement | OffscreenCanvas;
|
|
|
|
async function sha256Bytes(value: Uint8Array): Promise<string> {
|
|
const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(value).buffer);
|
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
export async function displayAssetPreview(
|
|
canvas: PreviewCanvas,
|
|
backend: AssetPreviewDisplayBackend,
|
|
identityValue: unknown,
|
|
encodedBytes: ArrayBuffer,
|
|
): Promise<AssetPreviewDisplayResultIR> {
|
|
const plan = await planAssetPreviewDecode(identityValue, encodedBytes);
|
|
if (typeof createImageBitmap !== "function") throw new Error("CAPABILITY_MISSING: createImageBitmap is unavailable");
|
|
const identity = identityValue as AssetPreviewIdentityIR;
|
|
const bitmap = await createImageBitmap(new Blob([encodedBytes], { type: plan.mimeType }), {
|
|
colorSpaceConversion: "none",
|
|
premultiplyAlpha: "none",
|
|
imageOrientation: "none",
|
|
});
|
|
try {
|
|
if (bitmap.width !== plan.width || bitmap.height !== plan.height) {
|
|
throw new Error("ASSET_PREVIEW_IDENTITY_MISMATCH: decoded preview dimensions changed");
|
|
}
|
|
canvas.width = plan.width;
|
|
canvas.height = plan.height;
|
|
const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true }) as
|
|
| CanvasRenderingContext2D
|
|
| OffscreenCanvasRenderingContext2D
|
|
| null;
|
|
if (!context) throw new Error("CAPABILITY_MISSING: preview Canvas2D context is unavailable");
|
|
context.clearRect(0, 0, plan.width, plan.height);
|
|
context.imageSmoothingEnabled = false;
|
|
context.globalCompositeOperation = "copy";
|
|
context.drawImage(bitmap, 0, 0, plan.width, plan.height);
|
|
const pixels = new Uint8Array(context.getImageData(0, 0, plan.width, plan.height).data);
|
|
let nonTransparentPixels = 0;
|
|
for (let offset = 3; offset < pixels.byteLength; offset += 4) if (pixels[offset] !== 0) nonTransparentPixels++;
|
|
const receipt: AssetPreviewDisplayReceiptIR = {
|
|
schemaVersion: 1,
|
|
status: "READY",
|
|
backend,
|
|
identitySha256: identity.identitySha256,
|
|
contentSha256: identity.content.sha256,
|
|
width: plan.width,
|
|
height: plan.height,
|
|
pixelByteLength: pixels.byteLength,
|
|
pixelSha256: await sha256Bytes(pixels),
|
|
nonTransparentPixels,
|
|
bitmapClosed: true,
|
|
};
|
|
if (typeof HTMLCanvasElement !== "undefined" && canvas instanceof HTMLCanvasElement) {
|
|
canvas.dataset.assetPreviewStatus = "ready";
|
|
canvas.dataset.assetPreviewBackend = backend;
|
|
canvas.dataset.assetPreviewPixelSha256 = receipt.pixelSha256;
|
|
}
|
|
return { receipt, pixels };
|
|
}
|
|
finally {
|
|
bitmap.close();
|
|
}
|
|
}
|