custom icons

Topics (not sure which fora)
when not sure where to post, post here and mods will move it to right forum.

Moderators: leecollings, remb0

User avatar
gizmocuz
Posts: 3072
Joined: Thursday 11 July 2013 18:59
Target OS: Raspberry Pi / ODroid
Domoticz version: beta
Location: Top of the world
Contact:

Re: custom icons

Post by gizmocuz »

Seems the original site is down.

I created a temporarily site for the icon generator here

https://www.domoticz.com/icon-creator/
Quality outlives Quantity!
User avatar
waltervl
Posts: 7030
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2026.3
Location: NL
Contact:

Re: custom icons

Post by waltervl »

McMelloW wrote: Friday 26 January 2024 12:20 Anybody know what is wrong with the link for the Icon-generator?

https://domoticz-icon.aurelien-loyer.fr/

The site is not reachable
Site is brought back online again.
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
User avatar
McMelloW
Posts: 435
Joined: Monday 20 November 2017 17:01
Target OS: Raspberry Pi / ODroid
Domoticz version: V2024.1
Location: Harderwijk, NL
Contact:

Re: custom icons

Post by McMelloW »

waltervl wrote: Sunday 17 March 2024 17:26
McMelloW wrote: Friday 26 January 2024 12:20 Anybody know what is wrong with the link for the Icon-generator?

https://domoticz-icon.aurelien-loyer.fr/

The site is not reachable
Site is brought back online again.
Thanks for the update.
Greetings McMelloW
User avatar
habahabahaba
Posts: 286
Joined: Saturday 18 March 2023 14:44
Target OS: Windows
Domoticz version: 2024.4
Contact:

Re: custom icons

Post by habahabahaba »

All links are broken ... again.
So... here is the html code of fully offline generator
Spoiler: show

Code: Select all

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Domoticz icon generator</title>
<style>
*{box-sizing:border-box}body{margin:0;background:#18212e;color:#edf2fa;font:16px system-ui,sans-serif;padding:24px}main{max-width:700px;margin:24px auto;background:#253346;padding:28px;border-radius:18px}h1{font-size:25px;margin-top:0}label{display:block;margin:18px 0 8px}input{max-width:100%;font:inherit}input[type=text]{width:100%;padding:10px;border-radius:7px;border:1px solid #8090a0}button{margin-top:22px;width:100%;padding:14px;border:0;border-radius:8px;background:#64d7af;color:#10251c;font:bold 17px system-ui;cursor:pointer}button:disabled{opacity:.5;cursor:wait}.previews{display:flex;gap:22px;margin-top:20px;flex-wrap:wrap}canvas{width:96px;height:96px;background:repeating-conic-gradient(#eee 0% 25%,#bbb 0% 50%) 0/16px 16px;border-radius:6px}.hint{color:#c0ccda;font-size:14px;line-height:1.6}#status{white-space:pre-wrap;line-height:1.5}a{color:#8ee6c5}#download{display:none;margin-top:16px;padding:10px}code{color:#b5e5ff}
</style>
</head>
<body><main>
<h1>Domoticz icon generator</h1>
<p class="hint">Runs locally without libraries, CDNs, or uploading files to the internet. Original PNG transparency is preserved; white backgrounds are not removed automatically.</p>
<label for="base">Icon set name (Latin letters, digits, _ and -)</label>
<input id="base" type="text" value="floodlight" maxlength="64" pattern="[A-Za-z][A-Za-z0-9_-]{0,63}">
<label for="title">Display name in Domoticz</label><input id="title" type="text" value="Floodlight" maxlength="100">
<label for="on">On icon (PNG):</label><input id="on" type="file" accept="image/png,.png">
<label for="off">Off icon (PNG):</label><input id="off" type="file" accept="image/png,.png">
<div class="previews"><div><p>On · 48×48</p><canvas id="previewOn" width="48" height="48"></canvas></div><div><p>Off · 48×48</p><canvas id="previewOff" width="48" height="48"></canvas></div></div>
<p class="hint">Images fit within a square without stretching or cropping. Archive contents: <code>name48_On.png</code>, <code>name48_Off.png</code>, <code>name.png</code> (16×16, generated from On), and <code>icons.txt</code>.</p>
<button id="generate" type="button">Create ZIP</button>
<p id="status" role="status" aria-live="polite"></p><a id="download">Download ZIP</a>
<noscript>JavaScript must be enabled to use this tool.</noscript>
</main>
<script>
'use strict';
const $=id=>document.getElementById(id);
const encoder=new TextEncoder();
let downloadURL=null;
function report(message){$('status').textContent=message;}
async function loadPNG(file){
 if(!file)throw new Error('Select both files: On and Off.');
 if(file.size>20*1024*1024)throw new Error('The file exceeds 20 MB. Use a smaller image.');
 const signature=new Uint8Array(await file.slice(0,8).arrayBuffer());
 const png=[137,80,78,71,13,10,26,10];
 if(signature.length!==8||!png.every((b,i)=>signature[i]===b))throw new Error('File "'+file.name+'" is not a PNG.');
 const url=URL.createObjectURL(file);
 try{
  return await new Promise((resolve,reject)=>{
   const img=new Image();
   img.onload=()=>resolve(img);
   img.onerror=()=>reject(new Error('Unable to load image "'+file.name+'".'));
   img.src=url;
  });
 }finally{URL.revokeObjectURL(url);}
}
function draw(img,canvas){
 const ctx=canvas.getContext('2d');
 if(!ctx)throw new Error('Your browser does not support Canvas 2D.');
 ctx.clearRect(0,0,canvas.width,canvas.height);
 ctx.imageSmoothingEnabled=true;ctx.imageSmoothingQuality='high';
 const scale=Math.min(canvas.width/img.naturalWidth,canvas.height/img.naturalHeight);
 const w=img.naturalWidth*scale,h=img.naturalHeight*scale;
 ctx.drawImage(img,(canvas.width-w)/2,(canvas.height-h)/2,w,h);
}
async function pngBytes(img,size){
 const c=document.createElement('canvas');c.width=c.height=size;draw(img,c);
 const blob=await new Promise((resolve,reject)=>c.toBlob(b=>b?resolve(b):reject(new Error('Failed to create PNG.')),'image/png'));
 return new Uint8Array(await blob.arrayBuffer());
}
const crcTable=Uint32Array.from({length:256},(_,n)=>{
 for(let k=0;k<8;k++)n=n&1?0xedb88320^(n>>>1):n>>>1;
 return n>>>0;
});
function crc32(data){let crc=0xffffffff;for(const b of data)crc=crcTable[(crc^b)&255]^(crc>>>8);return (crc^0xffffffff)>>>0;}
// ZIP STORE: PNG is already compressed; no external library is needed.
function makeZip(entries){
 const chunks=[],central=[];let offset=0,centralSize=0;
 const now=new Date(),year=Math.max(1980,Math.min(2107,now.getFullYear()));
 const time=(now.getHours()<<11)|(now.getMinutes()<<5)|(now.getSeconds()>>1);
 const date=((year-1980)<<9)|((now.getMonth()+1)<<5)|now.getDate();
 for(const [name,data] of entries){
  const filename=encoder.encode(name),crc=crc32(data);
  const local=new Uint8Array(30+filename.length),v=new DataView(local.buffer);
  v.setUint32(0,0x04034b50,true);v.setUint16(4,20,true);v.setUint16(6,0x800,true);
  v.setUint16(10,time,true);v.setUint16(12,date,true);v.setUint32(14,crc,true);
  v.setUint32(18,data.length,true);v.setUint32(22,data.length,true);v.setUint16(26,filename.length,true);local.set(filename,30);
  chunks.push(local,data);
  const c=new Uint8Array(46+filename.length),w=new DataView(c.buffer);
  w.setUint32(0,0x02014b50,true);w.setUint16(4,20,true);w.setUint16(6,20,true);w.setUint16(8,0x800,true);
  w.setUint16(12,time,true);w.setUint16(14,date,true);w.setUint32(16,crc,true);
  w.setUint32(20,data.length,true);w.setUint32(24,data.length,true);w.setUint16(28,filename.length,true);w.setUint32(42,offset,true);c.set(filename,46);
  central.push(c);centralSize+=c.length;offset+=local.length+data.length;
 }
 const end=new Uint8Array(22),e=new DataView(end.buffer);
 e.setUint32(0,0x06054b50,true);e.setUint16(8,entries.length,true);e.setUint16(10,entries.length,true);e.setUint32(12,centralSize,true);e.setUint32(16,offset,true);
 return new Blob([...chunks,...central,end],{type:'application/zip'});
}
function clearDownload(){
 if(downloadURL){URL.revokeObjectURL(downloadURL);downloadURL=null;}
 $('download').style.display='none';$('download').removeAttribute('href');
}
let revision=0;
for(const [input,preview] of [['on','previewOn'],['off','previewOff']]){
 $(input).addEventListener('change',async()=>{
  const current=++revision;clearDownload();report('');
  const file=$(input).files[0];
  const canvas=$(preview);canvas.getContext('2d').clearRect(0,0,48,48);
  if(!file)return;
  try{const img=await loadPNG(file);if($(input).files[0]===file)draw(img,canvas);}
  catch(error){if(current===revision)report(error.message);}
 });
}
for(const id of ['base','title'])$(id).addEventListener('input',()=>{revision++;clearDownload();});
$('generate').addEventListener('click',async()=>{
 const button=$('generate');button.disabled=true;clearDownload();report('Processing...');
 const current=revision;
 try{
  const base=$('base').value.trim(),title=$('title').value.trim()||base;
  if(!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(base))throw new Error('The name must contain 1 to 64 characters, start with a Latin letter, and contain only Latin letters, digits, _ or -.');
  if(/[;\r\n]/.test(title))throw new Error('The display name must not contain semicolons or line breaks.');
  const [on,off]=await Promise.all([loadPNG($('on').files[0]),loadPNG($('off').files[0])]);
  const [on48,off48,small]=await Promise.all([pngBytes(on,48),pngBytes(off,48),pngBytes(on,16)]);
  if(current!==revision)throw new Error('The settings changed during processing. Click "Create ZIP" again.');
  const blob=makeZip([[base+'48_On.png',on48],[base+'48_Off.png',off48],[base+'.png',small],['icons.txt',encoder.encode(base+';'+title+';'+title+'\n')]]);
  downloadURL=URL.createObjectURL(blob);
  const link=$('download');link.href=downloadURL;link.download=base+'_icons.zip';link.style.display='inline-block';
  draw(on,$('previewOn'));draw(off,$('previewOff'));
  report('The archive is ready. Click "Download ZIP".');
 }catch(error){report('Error: '+(error.message||String(error)));console.error(error);}
 finally{button.disabled=false;}
});
window.addEventListener('pagehide',()=>{if(downloadURL)URL.revokeObjectURL(downloadURL);});
</script></body></html>
You can use it as separate html page or build-in in Domoticz Custom menu
User avatar
waltervl
Posts: 7030
Joined: Monday 28 January 2019 18:48
Target OS: Linux
Domoticz version: 2026.3
Location: NL
Contact:

Re: custom icons

Post by waltervl »

Gizmocuz made his own implentation on https://www.domoticz.com/domoticz_icon_generator.html
It has also more functionality.
Domoticz running on Udoo X86 (on Ubuntu)
Devices/plugins: ZigbeeforDomoticz (with Xiaomi, Ikea, Tuya devices), Nefit Easy, Midea Airco, Omnik Solar, Goodwe Solar
User avatar
imautohuttraeger
Posts: 458
Joined: Saturday 14 March 2020 13:40
Target OS: Raspberry Pi / ODroid
Domoticz version: latest
Location: Germany
Contact:

Re: custom icons

Post by imautohuttraeger »

waltervl wrote: Saturday 14 November 2026 15:23 Gizmocuz made his own implentation on https://www.domoticz.com/domoticz_icon_generator.html
It has also more functionality.
It is even part of the top naviagion menu of domoticz.com
Stop, look and listen.
Post Reply