Files
hfb_sys/distribution-admin/src/utils/imageTransparency.ts
T

107 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 图片透明区域探测:用于后台在上传「热区按钮图 / 海报底图」时提示
* 「这张图其实没有透明区域」——最常见的坑是把设计工具里带棋盘格的预览
* 截图/另存下来,棋盘格就成了真实像素,前台会照实显示。
*/
export interface ImageTransparencyProbe {
/** 是否存在透明像素(alpha < 250) */
hasTransparentPixels: boolean
/** 透明像素占比(0-1) */
transparentRatio: number
/** 不透明像素是否只有极少数浅灰/白颜色(棋盘格或纯色底烘焙进图片的典型特征) */
suspectedBakedBackdrop: boolean
}
const SAMPLE_MAX_SIDE = 160
/** alpha 低于该值即视为透明像素 */
const TRANSPARENT_ALPHA = 250
/** 无明显透明像素的判定阈值(透明像素占比) */
const NEGLIGIBLE_TRANSPARENT_RATIO = 0.002
/** 纯像素分析(无 DOM 依赖,便于单测)。 */
export function analyzeImagePixels(data: Uint8ClampedArray | number[]): ImageTransparencyProbe {
const total = Math.floor(data.length / 4)
if (total <= 0) {
return { hasTransparentPixels: false, transparentRatio: 0, suspectedBakedBackdrop: false }
}
let transparent = 0
let opaque = 0
let lightOnly = true
const colors = new Set<number>()
for (let index = 0; index < total; index += 1) {
const offset = index * 4
const r = data[offset] ?? 0
const g = data[offset + 1] ?? 0
const b = data[offset + 2] ?? 0
const a = data[offset + 3] ?? 0
if (a < TRANSPARENT_ALPHA) {
transparent += 1
continue
}
opaque += 1
const min = Math.min(r, g, b)
const max = Math.max(r, g, b)
if (min < 216 || max - min > 28) lightOnly = false
// 量化到 4bit/通道,避免抗锯齿噪点导致颜色数虚高
colors.add(((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4))
}
const transparentRatio = transparent / total
return {
hasTransparentPixels: transparentRatio > NEGLIGIBLE_TRANSPARENT_RATIO,
transparentRatio,
suspectedBakedBackdrop:
opaque > 0 &&
lightOnly &&
transparentRatio <= NEGLIGIBLE_TRANSPARENT_RATIO &&
colors.size <= 4,
}
}
/** 读取本地文件像素并分析;环境不支持时返回 null(不阻塞上传)。 */
export async function probeImageTransparency(file: File): Promise<ImageTransparencyProbe | null> {
const source = await loadImageSource(file)
if (!source) return null
const width = Math.max(1, Math.round(source.width))
const height = Math.max(1, Math.round(source.height))
const scale = Math.min(1, SAMPLE_MAX_SIDE / Math.max(width, height))
const targetWidth = Math.max(1, Math.round(width * scale))
const targetHeight = Math.max(1, Math.round(height * scale))
const canvas = document.createElement('canvas')
canvas.width = targetWidth
canvas.height = targetHeight
const context = canvas.getContext('2d')
if (!context) return null
context.clearRect(0, 0, targetWidth, targetHeight)
context.drawImage(source as CanvasImageSource, 0, 0, targetWidth, targetHeight)
const imageData = context.getImageData(0, 0, targetWidth, targetHeight)
return analyzeImagePixels(imageData.data)
}
async function loadImageSource(file: File): Promise<ImageBitmap | HTMLImageElement | null> {
if (typeof createImageBitmap === 'function') {
try {
return await createImageBitmap(file)
} catch {
// 继续走 <img> 兜底
}
}
return await new Promise(resolve => {
const url = URL.createObjectURL(file)
const image = new Image()
image.onload = () => {
URL.revokeObjectURL(url)
resolve(image)
}
image.onerror = () => {
URL.revokeObjectURL(url)
resolve(null)
}
image.src = url
})
}