(src, bgHex, targetW, targetH, tintHex)
| 2729 | } |
| 2730 | |
| 2731 | function loadPngToRgb565(src, bgHex, targetW, targetH, tintHex) { |
| 2732 | return new Promise((resolve, reject) => { |
| 2733 | const img = new Image(); |
| 2734 | // Same-origin typically; set anyway for hosted builds |
| 2735 | img.crossOrigin = "anonymous"; |
| 2736 | img.onload = () => { |
| 2737 | try { |
| 2738 | const naturalW = img.naturalWidth || img.width; |
| 2739 | const naturalH = img.naturalHeight || img.height; |
| 2740 | const w = targetW || naturalW; |
| 2741 | const h = targetH || naturalH; |
| 2742 | if (!w || !h) throw new Error("Invalid icon dimensions."); |
| 2743 | imgCanvas.width = w; |
| 2744 | imgCanvas.height = h; |
| 2745 | imgCtx.clearRect(0, 0, w, h); |
| 2746 | imgCtx.drawImage(img, 0, 0, w, h); |
| 2747 | const imageData = imgCtx.getImageData(0, 0, w, h); |
| 2748 | |
| 2749 | // If tint enabled, recolor non-transparent pixels before converting |
| 2750 | if (tintHex) { |
| 2751 | const t = hexToRgb(tintHex) || { r: 255, g: 255, b: 255 }; |
| 2752 | const d = imageData.data; |
| 2753 | for (let i = 0; i < d.length; i += 4) { |
| 2754 | const a = d[i + 3]; |
| 2755 | if (a >= 16) { |
| 2756 | d[i] = t.r; |
| 2757 | d[i + 1] = t.g; |
| 2758 | d[i + 2] = t.b; |
| 2759 | } |
| 2760 | } |
| 2761 | } |
| 2762 | |
| 2763 | const rgb565 = imageDataToRgb565(imageData.data, w, h, bgHex); |
| 2764 | const monoBitmap = buildMonoBitmapFromImageData(imageData, w, h); |
| 2765 | resolve({ w, h, rgb565, monoBitmap }); |
| 2766 | } catch (e) { |
| 2767 | reject(e); |
| 2768 | } |
| 2769 | }; |
| 2770 | img.onerror = () => reject(new Error("Failed to load icon image.")); |
| 2771 | img.src = src; |
| 2772 | }); |
| 2773 | } |
| 2774 | |
| 2775 | function schedule(fn, delayMs = 150) { |
| 2776 | let t = null; |
no test coverage detected