tint-status-icons.mjs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. // 状态标识图标(地图图例行):
  2. // 从 Texture/Icon 取 I 系列,按各状态的语义色染色(--lv1/lv2/lv3/lv4/neutral),
  3. // 输出 public/cockpit/assets/icons/status/*.png。
  4. // 语义色来自 tokens.css:报警 #ff3b52 / 预警 #ffb020 / 工单 #01abea / 正常 #27d17a / 中性 #5680b0。
  5. import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
  6. import { inflateSync, deflateSync } from 'node:zlib'
  7. const SRC = 'E:/&项目模型文件等/UI图片/Texture/Icon'
  8. const OUT = 'public/cockpit/assets/icons/status'
  9. mkdirSync(OUT, { recursive: true })
  10. const STATUS = [
  11. { key: 'alarm', src: 'I图标报警产生.png', hex: '#ff3b52', meaning: '报警(铃铛警示)' },
  12. { key: 'warning', src: 'I图标报警等级.png', hex: '#ffb020', meaning: '预警(警灯+等级)' },
  13. { key: 'workOrder', src: 'I图标报警解除.png', hex: '#01abea', meaning: '工单处置(解除对勾)' },
  14. { key: 'normal', src: 'I图标在线.png', hex: '#27d17a', meaning: '正常(在线信号)' },
  15. { key: 'offline', src: 'I图标离线.png', hex: '#5680b0', meaning: '离线(断线信号)' },
  16. { key: 'empty', src: 'I图标设备.png', hex: '#5680b0', meaning: '无设备(设备机柜,中性色)' },
  17. { key: 'unknown', src: 'I图标报警误报.png', hex: '#5680b0', meaning: '状态未发布(问号铃,中性色)' },
  18. { key: 'unassociated', src: 'I图标设备开发.png', hex: '#5680b0', meaning: '无关联设备(中性色)' },
  19. ]
  20. const hexToRgb = (h) => [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16)]
  21. function tintPng(buf, tint) {
  22. if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('非 PNG')
  23. let pos = 8
  24. const idat = []
  25. let width = 0
  26. let height = 0
  27. let bitDepth = 0
  28. let colorType = 0
  29. let palette = null
  30. let trns = null
  31. while (pos < buf.length) {
  32. const len = buf.readUInt32BE(pos)
  33. const type = buf.toString('ascii', pos + 4, pos + 8)
  34. const data = buf.subarray(pos + 8, pos + 8 + len)
  35. if (type === 'IHDR') {
  36. width = data.readUInt32BE(0)
  37. height = data.readUInt32BE(4)
  38. bitDepth = data[8]
  39. colorType = data[9]
  40. if (bitDepth !== 8 || ![2, 3, 6].includes(colorType)) throw new Error(`不支持的 PNG: bit=${bitDepth} color=${colorType}`)
  41. } else if (type === 'PLTE') palette = Buffer.from(data)
  42. else if (type === 'tRNS') trns = Buffer.from(data)
  43. else if (type === 'IDAT') idat.push(data)
  44. else if (type === 'IEND') break
  45. pos += 12 + len
  46. }
  47. const raw = inflateSync(Buffer.concat(idat))
  48. const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
  49. const stride = width * channels
  50. const bpp = channels
  51. const img = Buffer.alloc(height * stride)
  52. for (let y = 0; y < height; y++) {
  53. const filter = raw[y * (stride + 1)]
  54. const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1))
  55. const prev = y > 0 ? img.subarray((y - 1) * stride, y * stride) : null
  56. const out = img.subarray(y * stride, (y + 1) * stride)
  57. for (let x = 0; x < stride; x++) {
  58. const a = x >= bpp ? out[x - bpp] : 0
  59. const b = prev ? prev[x] : 0
  60. const c = prev && x >= bpp ? prev[x - bpp] : 0
  61. let v = row[x]
  62. if (filter === 1) v += a
  63. else if (filter === 2) v += b
  64. else if (filter === 3) v += (a + b) >> 1
  65. else if (filter === 4) {
  66. const p = a + b - c
  67. const pa = Math.abs(p - a)
  68. const pb = Math.abs(p - b)
  69. const pc = Math.abs(p - c)
  70. v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
  71. }
  72. out[x] = v & 0xff
  73. }
  74. }
  75. const outRows = []
  76. for (let y = 0; y < height; y++) {
  77. const rowOut = Buffer.alloc(1 + stride)
  78. rowOut[0] = 0
  79. for (let x = 0; x < width; x++) {
  80. let r, g, b, alpha = 255
  81. if (colorType === 6) {
  82. const o = x * 4
  83. r = img[y * stride + o]; g = img[y * stride + o + 1]; b = img[y * stride + o + 2]; alpha = img[y * stride + o + 3]
  84. } else if (colorType === 2) {
  85. const o = x * 3
  86. r = img[y * stride + o]; g = img[y * stride + o + 1]; b = img[y * stride + o + 2]
  87. } else {
  88. const idx = img[y * stride + x]
  89. const p = palette ? idx * 3 : 0
  90. r = palette ? palette[p] : 0; g = palette ? palette[p + 1] : 0; b = palette ? palette[p + 2] : 0
  91. if (trns && idx < trns.length) alpha = trns[idx]
  92. }
  93. const lum = (r * 0.299 + g * 0.587 + b * 0.114) / 255
  94. const o = x * channels
  95. rowOut[1 + o] = Math.round(tint[0] * lum)
  96. rowOut[1 + o + 1] = Math.round(tint[1] * lum)
  97. rowOut[1 + o + 2] = Math.round(tint[2] * lum)
  98. if (channels === 4) rowOut[1 + o + 3] = alpha
  99. }
  100. outRows.push(rowOut)
  101. }
  102. const crc32Table = (() => {
  103. const t = new Int32Array(256)
  104. for (let n = 0; n < 256; n++) {
  105. let c = n
  106. for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
  107. t[n] = c
  108. }
  109. return t
  110. })()
  111. const crc32 = (b) => {
  112. let c = -1
  113. for (const byte of b) c = crc32Table[(c ^ byte) & 0xff] ^ (c >>> 8)
  114. return (c ^ -1) >>> 0
  115. }
  116. const chunk = (type, data) => {
  117. const head = Buffer.alloc(4)
  118. head.writeUInt32BE(data.length)
  119. const body = Buffer.concat([Buffer.from(type, 'ascii'), data])
  120. const crc = Buffer.alloc(4)
  121. crc.writeUInt32BE(crc32(body))
  122. return Buffer.concat([head, body, crc])
  123. }
  124. const ihdr = Buffer.alloc(13)
  125. ihdr.writeUInt32BE(width, 0)
  126. ihdr.writeUInt32BE(height, 4)
  127. ihdr[8] = 8
  128. ihdr[9] = colorType === 6 ? 6 : 2
  129. const compressed = deflateSync(Buffer.concat(outRows), { level: 9 })
  130. return Buffer.concat([
  131. Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
  132. chunk('IHDR', ihdr),
  133. chunk('IDAT', compressed),
  134. chunk('IEND', Buffer.alloc(0)),
  135. ])
  136. }
  137. for (const cfg of STATUS) {
  138. const srcPath = `${SRC}/${cfg.src}`
  139. if (!existsSync(srcPath)) {
  140. console.log(` ✗ 缺源图: ${cfg.src}`)
  141. continue
  142. }
  143. const out = tintPng(readFileSync(srcPath), hexToRgb(cfg.hex))
  144. writeFileSync(`${OUT}/status-${cfg.key}.png`, out)
  145. console.log(` ✓ status-${cfg.key}.png <- ${cfg.src} @ ${cfg.hex} (${out.length}B)`)
  146. }