Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

103 lines
3.0 KiB
TypeScript

/*
* @Author : PureWhite
* @Date : 2026-02-22 19:43:35
* @LastEditors : PureWhite
* @LastEditTime: 2026-02-28 03:36:57
* @Description :
*/
export type NinePatchInsets = {
top: number;
right: number;
bottom: number;
left: number;
};
type DecodeResult = {
insets: NinePatchInsets;
trimmedBlob: Blob;
};
function isBlack(r: number, g: number, b: number, a: number): boolean {
return a > 0 && r < 16 && g < 16 && b < 16;
}
function findMarkedRangeTopBorder(data: Uint8ClampedArray, w: number): { start: number; end: number } | null {
let start = -1;
let end = -1;
for (let x = 1; x < w - 1; x++) {
const i = (0 * w + x) * 4;
if (isBlack(data[i], data[i + 1], data[i + 2], data[i + 3])) {
if (start === -1) start = x;
end = x;
}
}
if (start === -1 || end === -1) return null;
return { start, end };
}
function findMarkedRangeLeftBorder(data: Uint8ClampedArray, w: number, h: number): { start: number; end: number } | null {
let start = -1;
let end = -1;
for (let y = 1; y < h - 1; y++) {
const i = (y * w + 0) * 4;
if (isBlack(data[i], data[i + 1], data[i + 2], data[i + 3])) {
if (start === -1) start = y;
end = y;
}
}
if (start === -1 || end === -1) return null;
return { start, end };
}
export async function decodeNinePatchFromUrl(src: string): Promise<DecodeResult | null> {
const res = await fetch(src);
if (!res.ok) return null;
const blob = await res.blob();
const bitmap = await createImageBitmap(blob).catch(() => null);
if (!bitmap) return null;
const w = bitmap.width;
const h = bitmap.height;
if (w < 3 || h < 3) return null;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
ctx.drawImage(bitmap, 0, 0);
const img = ctx.getImageData(0, 0, w, h);
const data = img.data;
const xRange = findMarkedRangeTopBorder(data, w);
const yRange = findMarkedRangeLeftBorder(data, w, h);
if (!xRange || !yRange) return null;
const contentW = w - 2;
const contentH = h - 2;
const stretchStartX = xRange.start - 1;
const stretchEndX = xRange.end - 1;
const stretchStartY = yRange.start - 1;
const stretchEndY = yRange.end - 1;
const left = Math.max(0, Math.min(contentW, stretchStartX));
const right = Math.max(0, Math.min(contentW, contentW - (stretchEndX + 1)));
const top = Math.max(0, Math.min(contentH, stretchStartY));
const bottom = Math.max(0, Math.min(contentH, contentH - (stretchEndY + 1)));
const trimmedCanvas = document.createElement('canvas');
trimmedCanvas.width = contentW;
trimmedCanvas.height = contentH;
const tctx = trimmedCanvas.getContext('2d');
if (!tctx) return null;
tctx.drawImage(canvas, 1, 1, contentW, contentH, 0, 0, contentW, contentH);
const trimmedBlob: Blob | null = await new Promise((resolve) => {
trimmedCanvas.toBlob((b) => resolve(b), 'image/png');
});
if (!trimmedBlob) return null;
return { insets: { top, right, bottom, left }, trimmedBlob };
}