File size: 3,983 Bytes
9de7b70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 图像取样:超采样 + 区域平均,得到干净的格子颜色(旧版一次性缩放会产生噪点和锯齿色)
// 所有结果都用 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 }