check-icon.js 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. "use strict";
  2. /** Verify rendered icons: decode PNG (8-bit RGBA, non-interlaced) and check
  3. * color composition + whale centering. Usage: node tools/check-icon.js a.png */
  4. const fs = require("node:fs");
  5. const zlib = require("node:zlib");
  6. function decodePng(buf) {
  7. if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error("not a PNG");
  8. let pos = 8;
  9. let width = 0;
  10. let height = 0;
  11. let bitDepth = 0;
  12. let colorType = 0;
  13. const idat = [];
  14. while (pos < buf.length) {
  15. const len = buf.readUInt32BE(pos);
  16. const type = buf.toString("ascii", pos + 4, pos + 8);
  17. const data = buf.subarray(pos + 8, pos + 8 + len);
  18. if (type === "IHDR") {
  19. width = data.readUInt32BE(0);
  20. height = data.readUInt32BE(4);
  21. bitDepth = data[8];
  22. colorType = data[9];
  23. } else if (type === "IDAT") idat.push(data);
  24. else if (type === "IEND") break;
  25. pos += 12 + len;
  26. }
  27. if (bitDepth !== 8 || colorType !== 6) throw new Error(`unsupported PNG: depth=${bitDepth} color=${colorType}`);
  28. const raw = zlib.inflateSync(Buffer.concat(idat));
  29. const stride = width * 4;
  30. const out = Buffer.alloc(width * height * 4);
  31. const paeth = (a, b, c) => {
  32. const p = a + b - c;
  33. const pa = Math.abs(p - a);
  34. const pb = Math.abs(p - b);
  35. const pc = Math.abs(p - c);
  36. return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
  37. };
  38. for (let y = 0; y < height; y++) {
  39. const filter = raw[y * (stride + 1)];
  40. const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
  41. const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null;
  42. for (let x = 0; x < stride; x++) {
  43. const i = y * stride + x;
  44. const left = x >= 4 ? out[i - 4] : 0;
  45. const up = prev ? prev[x] : 0;
  46. const ul = prev && x >= 4 ? prev[x - 4] : 0;
  47. let v = row[x];
  48. if (filter === 1) v = (v + left) & 255;
  49. else if (filter === 2) v = (v + up) & 255;
  50. else if (filter === 3) v = (v + ((left + up) >> 1)) & 255;
  51. else if (filter === 4) v = (v + paeth(left, up, ul)) & 255;
  52. out[i] = v;
  53. }
  54. }
  55. return { width, height, pixels: out };
  56. }
  57. const file = process.argv[2];
  58. const { width, height, pixels } = decodePng(fs.readFileSync(file));
  59. let transparent = 0;
  60. let white = 0;
  61. let blue = 0;
  62. let black = 0;
  63. let other = 0;
  64. let minX = Infinity;
  65. let maxX = -Infinity;
  66. let minY = Infinity;
  67. let maxY = -Infinity;
  68. for (let y = 0; y < height; y++) {
  69. for (let x = 0; x < width; x++) {
  70. const i = (y * width + x) * 4;
  71. const r = pixels[i];
  72. const g = pixels[i + 1];
  73. const b = pixels[i + 2];
  74. const a = pixels[i + 3];
  75. if (a < 16) { transparent++; continue; }
  76. if (r > 220 && g > 220 && b > 220) {
  77. white++;
  78. if (x < minX) minX = x;
  79. if (x > maxX) maxX = x;
  80. if (y < minY) minY = y;
  81. if (y > maxY) maxY = y;
  82. } else if (b > 180 && b > r + 40 && g > r) blue++;
  83. else if (r < 60 && g < 60 && b < 60) black++;
  84. else other++;
  85. }
  86. }
  87. const total = width * height;
  88. const pct = (n) => `${((n / total) * 100).toFixed(1)}%`;
  89. console.log(`${file}: ${width}×${height}`);
  90. console.log(` transparent=${pct(transparent)} white=${pct(white)} blue=${pct(blue)} black=${pct(black)} other=${pct(other)}`);
  91. if (white > 0) {
  92. const cx = (minX + maxX) / 2 / width;
  93. const cy = (minY + maxY) / 2 / height;
  94. console.log(` white mass bbox: x ${minX}..${maxX}, y ${minY}..${maxY}; center=(${cx.toFixed(2)}, ${cy.toFixed(2)}) of canvas`);
  95. }