85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { base64 } from "./archive.ts";
|
||
import { uid, type Asset } from "./schema.ts";
|
||
import { sliceImage } from "./two-d.ts";
|
||
/** Read image dimensions without DOM, so browser and MCP import share validation. */
|
||
export function imageInfo(bytes: Uint8Array) {
|
||
if (bytes.length > 25 * 1024 * 1024)
|
||
throw Error("Изображение превышает 25 МБ");
|
||
const d = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||
let width = 0,
|
||
height = 0,
|
||
mime = "",
|
||
ext = "";
|
||
if (
|
||
bytes.length >= 24 &&
|
||
d.getUint32(0) === 0x89504e47 &&
|
||
d.getUint32(4) === 0x0d0a1a0a
|
||
) {
|
||
width = d.getUint32(16);
|
||
height = d.getUint32(20);
|
||
mime = "image/png";
|
||
ext = "png";
|
||
} else if (
|
||
bytes.length >= 30 &&
|
||
d.getUint32(0) === 0x52494646 &&
|
||
d.getUint32(8) === 0x57454250
|
||
) {
|
||
const kind = d.getUint32(12);
|
||
if (kind === 0x56503858) {
|
||
width = 1 + bytes[24] + bytes[25] * 256 + bytes[26] * 65536;
|
||
height = 1 + bytes[27] + bytes[28] * 256 + bytes[29] * 65536;
|
||
} else if (kind === 0x5650384c && bytes[20] === 0x2f) {
|
||
const bits = d.getUint32(21, true);
|
||
width = (bits & 0x3fff) + 1;
|
||
height = ((bits >>> 14) & 0x3fff) + 1;
|
||
} else if (
|
||
kind === 0x56503820 &&
|
||
bytes[23] === 0x9d &&
|
||
bytes[24] === 1 &&
|
||
bytes[25] === 0x2a
|
||
) {
|
||
width = d.getUint16(26, true) & 0x3fff;
|
||
height = d.getUint16(28, true) & 0x3fff;
|
||
}
|
||
mime = "image/webp";
|
||
ext = "webp";
|
||
} else if (bytes.length > 4 && d.getUint16(0) === 0xffd8) {
|
||
let offset = 2;
|
||
while (offset + 4 <= bytes.length) {
|
||
if (bytes[offset++] !== 255) break;
|
||
while (bytes[offset] === 255) offset++;
|
||
const marker = bytes[offset++];
|
||
if (marker === 0xda || marker === 0xd9) break;
|
||
const length = d.getUint16(offset);
|
||
if (length < 2 || offset + length > bytes.length) break;
|
||
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 7) {
|
||
height = d.getUint16(offset + 3);
|
||
width = d.getUint16(offset + 5);
|
||
break;
|
||
}
|
||
offset += length;
|
||
}
|
||
mime = "image/jpeg";
|
||
ext = "jpg";
|
||
}
|
||
if (!width || !height || width > 16384 || height > 16384)
|
||
throw Error("Ожидалось PNG, JPEG или WebP размером до 16384×16384");
|
||
return { width, height, mime, ext };
|
||
}
|
||
export function imageAsset(bytes: Uint8Array, name: string): Asset {
|
||
const info = imageInfo(bytes);
|
||
return {
|
||
id: uid("image"),
|
||
name: name.replace(/\.[^.]+$/, "") + "." + info.ext,
|
||
kind: "image",
|
||
uri: `data:${info.mime};base64,${base64(bytes)}`,
|
||
image: {
|
||
width: info.width,
|
||
height: info.height,
|
||
pixelsPerUnit: 100,
|
||
filter: "nearest",
|
||
frames: sliceImage(info.width, info.height, info.width, info.height),
|
||
},
|
||
};
|
||
}
|