現(xiàn) API 限流:X-Forwarded-For 的坑我替你踩完了)
前言給網(wǎng)站做 favicon常規(guī)路子是找在線生成器或者npm install sharp/png-to-ico一把梭。但我的場景比較刁部署在一臺零依賴的 Node 服務(wù)器上整個(gè)項(xiàng)目連package.json都沒有為一個(gè) 4KB 的圖標(biāo)引入圖像庫有點(diǎn)殺雞用牛刀。于是我研究了一下 PNG 和 ICO 的文件格式發(fā)現(xiàn)這倆格式簡單得出奇——純 Node 內(nèi)置模塊Buffer zlib就能手寫一個(gè) PNG 編碼器配上一個(gè)用有向距離場SDF畫圓角矩形的小渲染器150 行代碼搞定。最終效果輸入三個(gè)參數(shù)尺寸、兩種漸變色輸出一個(gè)漸變底 白色圓角工具塊圖案的.ico瀏覽器、搜索引擎站點(diǎn)圖標(biāo)Google 搜索結(jié)果里的 favicon都能正常識別。我的五個(gè)站點(diǎn)用了同一套代碼、不同主色生成了一組家族臉圖標(biāo)。先看成品node gen_favicon.js out.ico 55,138,221 91,157,249生成 32×32 的 ICO藍(lán)橙漸變底四個(gè)白色圓角小塊 2×2 排列。改兩個(gè) RGB 參數(shù)就能換配色。第一步理解 PNG 文件結(jié)構(gòu)PNG 一個(gè) 8 字節(jié)簽名 若干個(gè)塊chunk。我們只需要三塊塊作用IHDR寬、高、位深、顏色類型等頭信息13 字節(jié)定長IDAT像素?cái)?shù)據(jù)zlib 壓縮后的 RGBAIEND結(jié)束標(biāo)記空塊每個(gè) chunk 的結(jié)構(gòu)[4字節(jié)長度][4字節(jié)類型][數(shù)據(jù)][4字節(jié)CRC32校驗(yàn)]難點(diǎn)只有一個(gè)CRC32。好消息是 zlib 內(nèi)置了constzlibrequire(zlib);constcrc32(buf)zlib.crc32?zlib.crc32(buf):softCrc32(buf);// Node 22 自帶 zlib.crc32低版本用查表法兜底第二步手寫 PNG 編碼器constzlibrequire(zlib);functionchunk(type,data){constlenBuffer.alloc(4);len.writeUInt32BE(data.length);consttypeBufBuffer.from(type,ascii);constcrcBuffer.alloc(4);crc.writeUInt32BE(zlib.crc32(Buffer.concat([typeBuf,data]))0);returnBuffer.concat([len,typeBuf,data,crc]);}functionencodePNG(w,h,rgba){// IHDR: 位深8 顏色類型6 RGBAconstihdrBuffer.alloc(13);ihdr.writeUInt32BE(w,0);ihdr.writeUInt32BE(h,4);ihdr[8]8;// bit depthihdr[9]6;// color type RGBA// IDAT: 每行前面加 1 字節(jié)過濾器 0None再整塊 zlib 壓縮conststridew*4;constrawBuffer.alloc((stride1)*h);for(lety0;yh;y){raw[y*(stride1)]0;// filter: Nonergba.copy(raw,y*(stride1)1,y*stride,(y1)*stride);}constidatzlib.deflateSync(raw,{level:9});returnBuffer.concat([Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]),// 簽名chunk(IHDR,ihdr),chunk(IDAT,idat),chunk(IEND,Buffer.alloc(0)),]);}就這么幾十行一個(gè)合規(guī)的 PNG 編碼器完成了。踩坑提醒IDAT 的像素?cái)?shù)據(jù)每行開頭必須有一個(gè) filter 字節(jié)哪怕你不用任何過濾器也要寫 0漏了它 zlib 能壓過去、瀏覽器卻解碼出一條條斜紋——這是我調(diào)了半小時(shí)才發(fā)現(xiàn)的特色。第三步用 SDF 畫圓角矩形位圖繪制最樸素的思路是逐像素判斷我在不在形狀里。圓角矩形用有向距離場來做非常優(yōu)雅// 點(diǎn) (px,py) 到圓角矩形中心距離場r 為圓角半徑// 返回值 0 表示在矩形內(nèi)部0 表示在外部數(shù)值≈到邊緣的距離functionsdfRoundRect(px,py,hw,hh,r){constqxMath.abs(px)-hwr;constqyMath.abs(py)-hhr;constaxMath.max(qx,0),ayMath.max(qy,0);returnMath.hypot(ax,ay)Math.min(Math.max(qx,qy),0)-r;}有了它邊緣抗鋸齒也順手解決了像素中心距離在[-1, 1]之間的按距離線性混合透明度邊緣就是平滑漸變而不是鋸齒樓梯。渲染主體先畫一個(gè)漸變底對角線線性插值兩色再疊四塊白色圓角小矩形functionrender(size,c1,c2){constbufBuffer.alloc(size*size*4);constpadsize*0.14;// 外邊距constgapsize*0.06;// 塊間距constcell(size-pad*2-gap)/2;consthalfcell/2;constrcell*0.28;// 圓角半徑constcenters[[-1,-1],[1,-1],[-1,1],[1,1],].map(([sx,sy])[size/2sx*(halfgap/2),size/2sy*(halfgap/2)]);for(lety0;ysize;y){for(letx0;xsize;x){consti(y*sizex)*4;// 漸變底對角線constt(xy)/(2*size-2);letRc1[0](c2[0]-c1[0])*t;letGc1[1](c2[1]-c1[1])*t;letBc1[2](c2[2]-c1[2])*t;letA255;// 疊加白色塊任一塊命中即白lethit1e9;for(const[cx,cy]ofcenters){hitMath.min(hit,sdfRoundRect(x-cx,y-cy,half,half,r));}if(hit1){constalphaMath.min(1,Math.max(0,(1-hit)/2));// 抗鋸齒R255*alphaR*(1-alpha);G255*alphaG*(1-alpha);B255*alphaB*(1-alpha);}buf[i]R|0;buf[i1]G|0;buf[i2]B|0;buf[i3]A;}}returnbuf;}第四步包一層 ICO 殼ICO 格式比想象中還簡單一個(gè) 6 字節(jié)頭 一條 16 字節(jié)目錄項(xiàng) 原始 PNG 數(shù)據(jù)。現(xiàn)代 ICO 支持內(nèi)嵌 PNG256×256 內(nèi)都合法不用再寫 BMP 行序轉(zhuǎn)換functionwrapICO(png){constheadBuffer.alloc(6);head.writeUInt16LE(0,0);// 保留位head.writeUInt16LE(1,2);// 類型1 圖標(biāo)head.writeUInt16LE(1,4);// 圖像數(shù)量constdirBuffer.alloc(16);dir[0]32;dir[1]32;// 寬高0 表示 256dir[2]0;dir[3]0;// 調(diào)色板dir.writeUInt16LE(1,4);// 色彩平面dir.writeUInt16LE(32,6);// 位深dir.writeUInt32LE(png.length,8);// 數(shù)據(jù)大小dir.writeUInt32LE(22,12);// 數(shù)據(jù)偏移 6 16returnBuffer.concat([head,dir,png]);}組裝入口constoutprocess.argv[2]||favicon.ico;constparse(s)s.split(,).map(Number);constc1parse(process.argv[3]||55,138,221);constc2parse(process.argv[4]||91,157,249);require(fs).writeFileSync(out,wrapICO(encodePNG(32,32,render(32,c1,c2))));console.log(OK -,out);實(shí)用建議多尺寸怎么辦嚴(yán)格的 favicon 規(guī)范建議提供 16/32/48 三檔。做法是把三個(gè)尺寸各自encodePNG后在 ICO 里放三條目錄項(xiàng)把wrapICO里的數(shù)量改成 3、循環(huán)寫目錄項(xiàng)即可代碼模式完全一樣。我的實(shí)踐里單 32×32 在 Chrome/Edge/Google 搜索結(jié)果里顯示都正常48 以上的場景PWA、桌面快捷方式才需要補(bǔ)??偨Y(jié)PNG 編碼沒有想象中神秘簽名 IHDR/IDAT/IEND 三個(gè) chunk CRC32zlib 自帶IDAT 每行的 filter 字節(jié)是最隱蔽的坑SDF 是位圖繪制圓角形狀 抗鋸齒的優(yōu)雅解法ICO 現(xiàn)代格式直接內(nèi)嵌 PNG外殼只有 22 字節(jié)完整代碼約 130 行零依賴Node 18 直接跑。適合的場景CI/CD 里動(dòng)態(tài)生成圖標(biāo)、給批量站點(diǎn)生成同款不同色的圖標(biāo)家族、或者單純想搞懂圖片文件格式。完整可運(yùn)行源碼我整理在文末附件改配色只需要?jiǎng)觾蓚€(gè) RGB 參數(shù)。有問題歡迎評論區(qū)交流。