Page 2 of 2

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Tuesday 10 June 2025 15:25
by frank666
Same problem domoticz2025.1 (Build 16675) in container, I tried to import icons created with https://domoticz- icon.aurelien-ve.fr but it gives me error :
error in loading the icon set: icon file: tower.png is to small or issue with extraction.

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Wednesday 11 June 2025 8:25
by gizmocuz
RonkA wrote: Tuesday 10 June 2025 13:30 The icon in question is a custom icon i created called 'domoticz_custom_icon_Inverter.zip' ,a picture of my Solar-Edge inverter, but the icon that's being shown is not mine anymore. It looks like some sort of Victron gizmo..
Ahh, sorry about that, but I added an 'Inverter' icon a long time ago... I think your Icons had the same name (Inveter, Inverter48_On/Off)
It's an Inverter, does it really matter what brand it is? You got a much better one now :mrgreen:

All custom icons are stored inside the database.

They are reloaded/stored on disk at Domoticz startup

It will check if the file exists, and if it does, it will not overwrite it

It will probably also find that the file exists when it is 0 bytes

I have changed this in beta xx, it will not try to overwrite 0 length files

When uploading a new custom image

But this is not an error, but for you an unfortunate event... You can create a new custom icon with the names SolarEdge

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Wednesday 11 June 2025 8:28
by gizmocuz
frank666 wrote: Tuesday 10 June 2025 15:25 Same problem domoticz2025.1 (Build 16675) in container, I tried to import icons created with https://domoticz- icon.aurelien-ve.fr but it gives me error :
error in loading the icon set: icon file: tower.png is to small or issue with extraction.
That's great, but you really have to attach the zip file with the custom icon so we can have a look at it...

It is always easy to say it's a bug

Try it with this nice flour.... No issues here at all, also running Domoticz inside a docker compose container
flour.zip
(6.66 KiB) Downloaded 141 times

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Wednesday 11 June 2025 16:32
by frank666
gizmo thanks for the example zip, I have now succeeded; the problem is https://domoticz- icon.aurelien-ve.fr there seems to be some problem as it creates a file in the zip with the extension 0 .
I enclose the files the first constructed manually
towerpower.zip
(8.45 KiB) Downloaded 120 times

the second built via the site
domoticz_custom_icon_towerpowerfailed.zip
(7.66 KiB) Downloaded 119 times
result :
image_2025-06-11_163841412.png
image_2025-06-11_163841412.png (62.68 KiB) Viewed 344 times
👍👍👍

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Wednesday 11 June 2025 20:48
by RonkA
Here is a python-script that generates the right icons from the generated ones by domoticz- icon.aurelien-ve.fr:

Code: Select all

import sys
import os
import zipfile
from PIL import Image
from io import BytesIO

def crop_icon(image_data):
    with Image.open(BytesIO(image_data)) as img:
        if img.size == (50, 50):
            cropped = img.crop((1, 1, 49, 49))  # Remove 1 pixel from each edge
            output = BytesIO()
            cropped.save(output, format="PNG")
            return output.getvalue(), cropped
        return image_data, img.copy()

def resize_to_16x16(image: Image.Image):
    resized = image.resize((16, 16), Image.LANCZOS)
    output = BytesIO()
    resized.save(output, format="PNG")
    return output.getvalue()

def process_zip(zip_path):
    zip_dir = os.path.dirname(zip_path)
    zip_name = os.path.basename(zip_path)
    new_zip_path = os.path.join(zip_dir, f"processed_{zip_name}")

    icons_txt = None
    cropped_icons = {}
    resized_icons = {}

    with zipfile.ZipFile(zip_path, 'r') as zin:
        for item in zin.infolist():
            data = zin.read(item.filename)

            # Keep only icons.txt
            if item.filename.lower() == "icons.txt":
                icons_txt = (item.filename, data)

            # Process only 48_Off and 48_On icons
            elif item.filename.endswith("48_Off.png") or item.filename.endswith("48_On.png"):
                cropped_data, cropped_img = crop_icon(data)
                cropped_icons[item.filename] = cropped_data

                # Create 16x16 icon from *_48_On.png only
                if item.filename.endswith("48_On.png"):
                    base_name = item.filename.replace("48_On", "")
                    resized_data = resize_to_16x16(cropped_img)
                    resized_icons[base_name] = resized_data

    # Create the new ZIP file
    with zipfile.ZipFile(new_zip_path, 'w') as zout:
        if icons_txt:
            zout.writestr(icons_txt[0], icons_txt[1])

        for filename, data in cropped_icons.items():
            zout.writestr(filename, data)

        for filename, data in resized_icons.items():
            zout.writestr(filename, data)

    print(f"Processed ZIP file saved as: {new_zip_path}")

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: python Iconcrop.py <file.zip>")
        sys.exit(1)

    zip_file_path = sys.argv[1]
    if not os.path.isfile(zip_file_path) or not zip_file_path.endswith(".zip"):
        print("Please provide a valid .zip file.")
        sys.exit(1)

    process_zip(zip_file_path)

save code as Iconcrop.py

I copied all custom icon zipfiles from the download-folder from windows into a new folder, also placed the python script and from commandprompt do for example

Code: Select all

C:\Users\Ron\domoticz icons>python Iconcrop.py domoticz_custom_icon_Kliko.zip
the reply should be:

Code: Select all

Nieuw ZIP-file created as: processed_domoticz_custom_icon_Kliko.zip

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Wednesday 11 June 2025 20:54
by frank666
tnx Ronka 👍

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Domoticz Icon Generator</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {
            background: #f3f4f6; /* Soft light gray (grigio tenue) */
        }
        .container {
            transition: transform 0.3s ease-in-out;
        }
        .container:hover {
            transform: scale(1.02);
        }
        .glow {
            box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
        }
    </style>
</head>
<body class="flex flex-col items-center justify-center min-h-screen">
    <div class="container bg-white p-10 rounded-2xl shadow-md w-full max-w-lg mx-4">
        <h1 class="text-3xl font-extrabold text-center text-indigo-700 mb-6">Domoticz Icon Generator</h1>
        <div class="mb-6">
            <label class="block text-sm font-medium text-gray-900 mb-2">Upload Icon (Any PNG)</label>
            <input type="file" id="iconInput" accept="image/png" class="block w-full text-sm text-gray-900 file:mr-4 file:py-3 file:px-5 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 transition duration-200">
        </div>
        <button id="generateZip" class="glow w-full py-3 px-4 bg-green-500 text-white text-lg font-semibold rounded-lg hover:bg-green-600 focus:ring-4 focus:ring-green-300 transition duration-200">Generate ZIP</button>
        <p id="status" class="mt-4 text-sm text-gray-900 text-center"></p>
    </div>

    <script>
        const iconInput = document.getElementById('iconInput');
        const generateZip = document.getElementById('generateZip');
        const status = document.getElementById('status');

        // Image processing functions
        function resizeTo48x48(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function cropIcon(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 1, 1, 48, 48, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function resizeTo16x16(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 16;
            canvas.height = 16;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 16, 16);
            canvas.toBlob(callback, 'image/png');
        }

        function createOffIcon(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.filter = 'grayscale(100%) opacity(50%)';
            ctx.drawImage(img, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        // ZIP generation
        generateZip.addEventListener('click', async () => {
            if (!iconInput.files[0]) {
                status.textContent = 'Please upload a PNG file.';
                return;
            }

            status.textContent = 'Processing...';
            const zip = new JSZip();

            // Determine icon name
            let iconName = iconInput.files[0].name.replace(/\.[^/.]+$/, '');
            const iconNameCapitalized = iconName.charAt(0).toUpperCase() + iconName.slice(1);

            let img = new Image();
            let source = URL.createObjectURL(iconInput.files[0]);

            img.onload = async () => {
                // Resize input image to 48x48
                const resizedBlob = await new Promise(resolve => resizeTo48x48(img, resolve));
                const resizedImg = new Image();
                resizedImg.src = URL.createObjectURL(resizedBlob);

                resizedImg.onload = async () => {
                    // Crop the "On" icon
                    const croppedOnBlob = await new Promise(resolve => cropIcon(resizedImg, resolve));
                    zip.file(`${iconName}48_On.png`, croppedOnBlob);

                    // Create and crop the "Off" icon
                    const offImg = await new Promise(resolve => {
                        createOffIcon(resizedImg, blob => {
                            const offImg = new Image();
                            offImg.onload = () => resolve(offImg);
                            offImg.src = URL.createObjectURL(blob);
                        });
                    });
                    const croppedOffBlob = await new Promise(resolve => cropIcon(offImg, resolve));
                    zip.file(`${iconName}48_Off.png`, croppedOffBlob);

                    // Resize cropped "On" icon to 16x16
                    const croppedOnImg = new Image();
                    croppedOnImg.src = URL.createObjectURL(croppedOnBlob);
                    croppedOnImg.onload = async () => {
                        const resizedBlob = await new Promise(resolve => resizeTo16x16(croppedOnImg, resolve));
                        zip.file(`${iconName}.png`, resizedBlob);

                        // Add icons.txt with name;Name;Name format
                        zip.file('icons.txt', `${iconName};${iconNameCapitalized};${iconNameCapitalized}`);

                        // Generate and download ZIP
                        const content = await zip.generateAsync({ type: 'blob' });
                        const link = document.createElement('a');
                        link.href = URL.createObjectURL(content);
                        link.download = `${iconName}_icons.zip`;
                        link.click();
                        status.textContent = 'ZIP file downloaded!';
                    };
                };
            };
            img.onerror = () => status.textContent = 'Error loading image.';
            img.src = source;
        });
    </script>
</body>
</html>
image_2025-06-12_165737833.png
image_2025-06-12_165737833.png (17.64 KiB) Viewed 264 times
I made this , it is to be saved in .html and used on a server works well .

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Saturday 14 June 2025 14:16
by gizmocuz
Thanks for the feedback. Maybe you could let the author of this tool know that it sometimes generates icons with 0 byte length

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Saturday 14 June 2025 14:44
by HvdW
Which reminds me to say thank you to the Domoticz developers for the new icons in the latest release.

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Saturday 14 June 2025 15:49
by RonkA
Maybe you could let the author of this tool know that it sometimes generates icons with 0 byte length
I couldn't reach him, i don't have twitter/X, his GitHub is 404, and i don't have LinkedIn..
I made this , it is to be saved in .html and used on a server works well
Also Nice, saves a lot of time making new icons!!

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Tuesday 17 June 2025 13:22
by Filip
frank666 wrote: Wednesday 11 June 2025 20:54 tnx Ronka 👍

Code: Select all

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Domoticz Icon Generator</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {
            background: #f3f4f6; /* Soft light gray (grigio tenue) */
        }
        .container {
            transition: transform 0.3s ease-in-out;
        }
        .container:hover {
            transform: scale(1.02);
        }
        .glow {
            box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
        }
    </style>
</head>
<body class="flex flex-col items-center justify-center min-h-screen">
    <div class="container bg-white p-10 rounded-2xl shadow-md w-full max-w-lg mx-4">
        <h1 class="text-3xl font-extrabold text-center text-indigo-700 mb-6">Domoticz Icon Generator</h1>
        <div class="mb-6">
            <label class="block text-sm font-medium text-gray-900 mb-2">Upload Icon (Any PNG)</label>
            <input type="file" id="iconInput" accept="image/png" class="block w-full text-sm text-gray-900 file:mr-4 file:py-3 file:px-5 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 transition duration-200">
        </div>
        <button id="generateZip" class="glow w-full py-3 px-4 bg-green-500 text-white text-lg font-semibold rounded-lg hover:bg-green-600 focus:ring-4 focus:ring-green-300 transition duration-200">Generate ZIP</button>
        <p id="status" class="mt-4 text-sm text-gray-900 text-center"></p>
    </div>

    <script>
        const iconInput = document.getElementById('iconInput');
        const generateZip = document.getElementById('generateZip');
        const status = document.getElementById('status');

        // Image processing functions
        function resizeTo48x48(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function cropIcon(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 1, 1, 48, 48, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function resizeTo16x16(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 16;
            canvas.height = 16;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 16, 16);
            canvas.toBlob(callback, 'image/png');
        }

        function createOffIcon(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.filter = 'grayscale(100%) opacity(50%)';
            ctx.drawImage(img, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        // ZIP generation
        generateZip.addEventListener('click', async () => {
            if (!iconInput.files[0]) {
                status.textContent = 'Please upload a PNG file.';
                return;
            }

            status.textContent = 'Processing...';
            const zip = new JSZip();

            // Determine icon name
            let iconName = iconInput.files[0].name.replace(/\.[^/.]+$/, '');
            const iconNameCapitalized = iconName.charAt(0).toUpperCase() + iconName.slice(1);

            let img = new Image();
            let source = URL.createObjectURL(iconInput.files[0]);

            img.onload = async () => {
                // Resize input image to 48x48
                const resizedBlob = await new Promise(resolve => resizeTo48x48(img, resolve));
                const resizedImg = new Image();
                resizedImg.src = URL.createObjectURL(resizedBlob);

                resizedImg.onload = async () => {
                    // Crop the "On" icon
                    const croppedOnBlob = await new Promise(resolve => cropIcon(resizedImg, resolve));
                    zip.file(`${iconName}48_On.png`, croppedOnBlob);

                    // Create and crop the "Off" icon
                    const offImg = await new Promise(resolve => {
                        createOffIcon(resizedImg, blob => {
                            const offImg = new Image();
                            offImg.onload = () => resolve(offImg);
                            offImg.src = URL.createObjectURL(blob);
                        });
                    });
                    const croppedOffBlob = await new Promise(resolve => cropIcon(offImg, resolve));
                    zip.file(`${iconName}48_Off.png`, croppedOffBlob);

                    // Resize cropped "On" icon to 16x16
                    const croppedOnImg = new Image();
                    croppedOnImg.src = URL.createObjectURL(croppedOnBlob);
                    croppedOnImg.onload = async () => {
                        const resizedBlob = await new Promise(resolve => resizeTo16x16(croppedOnImg, resolve));
                        zip.file(`${iconName}.png`, resizedBlob);

                        // Add icons.txt with name;Name;Name format
                        zip.file('icons.txt', `${iconName};${iconNameCapitalized};${iconNameCapitalized}`);

                        // Generate and download ZIP
                        const content = await zip.generateAsync({ type: 'blob' });
                        const link = document.createElement('a');
                        link.href = URL.createObjectURL(content);
                        link.download = `${iconName}_icons.zip`;
                        link.click();
                        status.textContent = 'ZIP file downloaded!';
                    };
                };
            };
            img.onerror = () => status.textContent = 'Error loading image.';
            img.src = source;
        });
    </script>
</body>
</html>
image_2025-06-12_165737833.png
I made this , it is to be saved in .html and used on a server works well .
I like this tool? Just some suggestions: I would prefer the possibility to upload 2 different files; one for ON and one for OFF. Because sometime the OFF icon is not just a "gray" version of the ON one...
And would be good to have it somewhere in the domoticz git...
Thanks!

Re: Custom icons do not shown anymore / custom icons upload fails

Posted: Thursday 19 June 2025 8:40
by frank666
@Filip I have modified as suggested here :

Code: Select all

<script type="text/javascript">
        var gk_isXlsx = false;
        var gk_xlsxFileLookup = {};
        var gk_fileData = {};
        function filledCell(cell) {
          return cell !== '' && cell != null;
        }
        function loadFileData(filename) {
        if (gk_isXlsx && gk_xlsxFileLookup[filename]) {
            try {
                var workbook = XLSX.read(gk_fileData[filename], { type: 'base64' });
                var firstSheetName = workbook.SheetNames[0];
                var worksheet = workbook.Sheets[firstSheetName];

                // Convert sheet to JSON to filter blank rows
                var jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, blankrows: false, defval: '' });
                // Filter out blank rows (rows where all cells are empty, null, or undefined)
                var filteredData = jsonData.filter(row => row.some(filledCell));

                // Heuristic to find the header row by ignoring rows with fewer filled cells than the next row
                var headerRowIndex = filteredData.findIndex((row, index) =>
                  row.filter(filledCell).length >= filteredData[index + 1]?.filter(filledCell).length
                );
                // Fallback
                if (headerRowIndex === -1 || headerRowIndex > 25) {
                  headerRowIndex = 0;
                }

                // Convert filtered JSON back to CSV
                var csv = XLSX.utils.aoa_to_sheet(filteredData.slice(headerRowIndex)); // Create a new sheet from filtered array of arrays
                csv = XLSX.utils.sheet_to_csv(csv, { header: 1 });
                return csv;
            } catch (e) {
                console.error(e);
                return "";
            }
        }
        return gk_fileData[filename] || "";
        }
        </script><!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Domoticz Icon Generator</title>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body {
            background: #1f2937; /* Dark gray background */
        }
        .container {
            transition: transform 0.3s ease-in-out;
        }
        .container:hover {
            transform: scale(1.02);
        }
        .glow {
            box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
        }
    </style>
</head>
<body class="flex flex-col items-center justify-center min-h-screen">
    <div class="container bg-gray-300 p-10 rounded-2xl shadow-md w-full max-w-lg mx-4">
        <h1 class="text-3xl font-extrabold text-center text-indigo-700 mb-6">Domoticz Icon Generator</h1>
        <div class="mb-6">
            <label class="block text-sm font-medium text-gray-900 mb-2">Upload On Icon (PNG)</label>
            <input type="file" id="onIconInput" accept="image/png" class="block w-full text-sm text-gray-900 file:mr-4 file:py-3 file:px-5 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 transition duration-200">
        </div>
        <div class="mb-6">
            <label class="block text-sm font-medium text-gray-900 mb-2">Upload Off Icon (PNG)</label>
            <input type="file" id="offIconInput" accept="image/png" class="block w-full text-sm text-gray-900 file:mr-4 file:py-3 file:px-5 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 transition duration-200">
        </div>
        <button id="generateZip" class="glow w-full py-3 px-4 bg-green-500 text-white text-lg font-semibold rounded-lg hover:bg-green-600 focus:ring-4 focus:ring-green-300 transition duration-200">Generate ZIP</button>
        <p id="status" class="mt-4 text-sm text-gray-900 text-center"></p>
    </div>

    <script>
        const onIconInput = document.getElementById('onIconInput');
        const offIconInput = document.getElementById('offIconInput');
        const generateZip = document.getElementById('generateZip');
        const status = document.getElementById('status');

        // Image processing functions
        function resizeTo48x48(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function cropIcon(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 48;
            canvas.height = 48;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 1, 1, 48, 48, 0, 0, 48, 48);
            canvas.toBlob(callback, 'image/png');
        }

        function resizeTo16x16(img, callback) {
            const canvas = document.createElement('canvas');
            canvas.width = 16;
            canvas.height = 16;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0, 16, 16);
            canvas.toBlob(callback, 'image/png');
        }

        // ZIP generation
        generateZip.addEventListener('click', async () => {
            if (!onIconInput.files[0] || !offIconInput.files[0]) {
                status.textContent = 'Please upload both On and Off PNG files.';
                return;
            }

            status.textContent = 'Processing...';
            const zip = new JSZip();

            // Determine icon name from On icon
            let iconName = onIconInput.files[0].name.replace(/\.[^/.]+$/, '');
            const iconNameCapitalized = iconName.charAt(0).toUpperCase() + iconName.slice(1);

            // Process On icon
            let onImg = new Image();
            onImg.src = URL.createObjectURL(onIconInput.files[0]);

            onImg.onload = async () => {
                // Resize On image to 48x48
                const resizedOnBlob = await new Promise(resolve => resizeTo48x48(onImg, resolve));
                const resizedOnImg = new Image();
                resizedOnImg.src = URL.createObjectURL(resizedOnBlob);

                resizedOnImg.onload = async () => {
                    // Crop the On icon
                    const croppedOnBlob = await new Promise(resolve => cropIcon(resizedOnImg, resolve));
                    zip.file(`${iconName}48_On.png`, croppedOnBlob);

                    // Resize cropped On icon to 16x16
                    const croppedOnImg = new Image();
                    croppedOnImg.src = URL.createObjectURL(croppedOnBlob);
                    croppedOnImg.onload = async () => {
                        const resized16Blob = await new Promise(resolve => resizeTo16x16(croppedOnImg, resolve));
                        zip.file(`${iconName}.png`, resized16Blob);

                        // Process Off icon
                        let offImg = new Image();
                        offImg.src = URL.createObjectURL(offIconInput.files[0]);

                        offImg.onload = async () => {
                            // Resize Off image to 48x48
                            const resizedOffBlob = await new Promise(resolve => resizeTo48x48(offImg, resolve));
                            const resizedOffImg = new Image();
                            resizedOffImg.src = URL.createObjectURL(resizedOffBlob);

                            resizedOffImg.onload = async () => {
                                // Crop the Off icon
                                const croppedOffBlob = await new Promise(resolve => cropIcon(resizedOffImg, resolve));
                                zip.file(`${iconName}48_Off.png`, croppedOffBlob);

                                // Add icons.txt with name;Name;Name format
                                zip.file('icons.txt', `${iconName};${iconNameCapitalized};${iconNameCapitalized}`);

                                // Generate and download ZIP
                                const content = await zip.generateAsync({ type: 'blob' });
                                const link = document.createElement('a');
                                link.href = URL.createObjectURL(content);
                                link.download = `${iconName}_icons.zip`;
                                link.click();
                                status.textContent = 'ZIP file downloaded!';
                            };
                        };
                        offImg.onerror = () => status.textContent = 'Error loading Off image.';
                    };
                };
            };
            onImg.onerror = () => status.textContent = 'Error loading On image.';
        });
    </script>
</body>
</html>