// 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色) // 所有结果都用 Typed Array 表示,不再生成 HEX 字符串再反解析 const PAPER = [247, 241, 230] function getImageDimensions(image) { const width = Number(image.width) || Number(image.naturalWidth) || Number(image._w) const height = Number(image.height) || Number(image.naturalHeight) || Number(image._h) if (!width || !height) throw new Error('IMAGE_DIMENSIONS_UNAVAILABLE') return { width, height } } // 把图片按裁切参数画到 context 上(cover 语义,zoom≥1) function drawCropped(context, image, width, height, crop) { const source = getImageDimensions(image) const scale = Math.max(width / source.width, height / source.height) * (crop.zoom || 1) const w = source.width * scale const h = source.height * scale const offsetX = (width - w) * ((crop.focusX == null ? 50 : crop.focusX) / 100) const offsetY = (height - h) * ((crop.focusY == null ? 50 : crop.focusY) / 100) context.fillStyle = `rgb(${PAPER[0]},${PAPER[1]},${PAPER[2]})` context.fillRect(0, 0, width, height) context.save() context.translate(crop.flipH ? width : 0, crop.flipV ? height : 0) context.scale(crop.flipH ? -1 : 1, crop.flipV ? -1 : 1) context.drawImage(image, offsetX, offsetY, w, h) context.restore() } // 返回 { size, rgb: Uint8Array(size*size*3) } function sampleImage(canvas, image, size, crop) { // 超采样到约 700px 再按块平均,等价于高质量面积缩放 const factor = Math.max(1, Math.min(24, Math.floor(720 / size))) const big = size * factor canvas.width = big canvas.height = big const context = canvas.getContext('2d') context.imageSmoothingEnabled = true try { context.imageSmoothingQuality = 'high' } catch (e) { /* 部分基础库不支持 */ } drawCropped(context, image, big, big, crop || {}) const pixels = context.getImageData(0, 0, big, big).data const rgb = new Uint8Array(size * size * 3) const area = factor * factor for (let gy = 0; gy < size; gy += 1) { for (let gx = 0; gx < size; gx += 1) { let r = 0; let g = 0; let b = 0 for (let sy = 0; sy < factor; sy += 1) { let p = ((gy * factor + sy) * big + gx * factor) * 4 for (let sx = 0; sx < factor; sx += 1) { r += pixels[p]; g += pixels[p + 1]; b += pixels[p + 2] p += 4 } } const o = (gy * size + gx) * 3 rgb[o] = (r / area + 0.5) | 0 rgb[o + 1] = (g / area + 0.5) | 0 rgb[o + 2] = (b / area + 0.5) | 0 } } return { size, rgb } } // 去除简单纯色背景:四角均色为背景色,从边缘洪泛填充相近颜色(主体内部不受影响) // 返回 Uint8Array 掩码:1 = 背景(空钉位,不放豆) function removeSimpleBackground(rgb, size, tolerance) { const corners = [0, size - 1, (size - 1) * size, size * size - 1] let br = 0; let bg = 0; let bb = 0 corners.forEach((i) => { br += rgb[i * 3]; bg += rgb[i * 3 + 1]; bb += rgb[i * 3 + 2] }) br /= 4; bg /= 4; bb /= 4 const threshold = (tolerance / 100) * 441.673 const limit = threshold * threshold const mask = new Uint8Array(size * size) const isBg = (i) => { const dr = rgb[i * 3] - br; const dg = rgb[i * 3 + 1] - bg; const db = rgb[i * 3 + 2] - bb return dr * dr + dg * dg + db * db <= limit } const stack = [] const push = (x, y) => { if (x < 0 || y < 0 || x >= size || y >= size) return const i = y * size + x if (mask[i] || !isBg(i)) return mask[i] = 1 stack.push(i) } for (let i = 0; i < size; i += 1) { push(i, 0); push(i, size - 1); push(0, i); push(size - 1, i) } while (stack.length) { const i = stack.pop() const x = i % size const y = (i - x) / size push(x + 1, y); push(x - 1, y); push(x, y + 1); push(x, y - 1) } return mask } module.exports = { PAPER, getImageDimensions, drawCropped, sampleImage, removeSimpleBackground }