Icons: small images of their own size for pages

Bitmaps that only pages use (backgrounds, images by state) are now icons:
own width and height, stored in /icons, not in the page lists or the Home
Assistant dropdowns. Page backgrounds and image elements use icons or
bitmaps; the renderer gets icons expanded at the top left, so pages look
the same. Web UI: Icons tab with an editor, Make icon / Make bitmap, Save
as icon on the Draw tab, icons in the backup.

A one-time migration in time-boxed steps moves the uploaded bitmaps that
only pages use, after the new firmware is marked good.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-16 22:03:18 +02:00
1 parent 1704cbf17b
commit ab001e5d18
37 files changed
+955 -78

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
#include "logic/icon_format.h"
#include <string.h>
bool validIconSize(int width, int height)
{
return width >= 1 && height >= 1 && width <= ICON_MAX_WIDTH && height <= ICON_MAX_HEIGHT;
}
bool iconPixel(const Icon &icon, int x, int y)
{
if (x < 0 || y < 0 || x >= icon.width || y >= icon.height)
return false;
return icon.data[y * iconRowBytes(icon.width) + x / 8] & (0x80 >> (x & 7));
}
void setIconPixel(Icon &icon, int x, int y, bool on)
{
if (x < 0 || y < 0 || x >= icon.width || y >= icon.height)
return;
uint8_t &byte = icon.data[y * iconRowBytes(icon.width) + x / 8];
if (on)
byte |= 0x80 >> (x & 7);
else
byte &= ~(0x80 >> (x & 7));
}
static int hexValue(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
bool iconFromHex(int width, int height, const char *hex, Icon &icon)
{
if (!validIconSize(width, height) || hex == nullptr)
return false;
int bytes = iconBytes(width, height);
if (strlen(hex) != (size_t)bytes * 2)
return false;
Icon result;
result.width = width;
result.height = height;
for (int i = 0; i < bytes; i++)
{
int high = hexValue(hex[2 * i]);
int low = hexValue(hex[2 * i + 1]);
if (high < 0 || low < 0)
return false;
result.data[i] = (high << 4) | low;
}
// The unused bits at the end of each row stay off
for (int y = 0; y < height; y++)
for (int x = width; x < iconRowBytes(width) * 8; x++)
result.data[y * iconRowBytes(width) + x / 8] &= ~(0x80 >> (x & 7));
icon = result;
return true;
}
void iconToHex(const Icon &icon, char *out)
{
static const char digits[] = "0123456789abcdef";
int bytes = iconBytes(icon.width, icon.height);
for (int i = 0; i < bytes; i++)
{
out[2 * i] = digits[icon.data[i] >> 4];
out[2 * i + 1] = digits[icon.data[i] & 0x0F];
}
out[bytes * 2] = '\0';
}
void iconToBitmap(const Icon &icon, uint8_t *bitmap)
{
memset(bitmap, 0, BITMAP_BYTES);
for (int y = 0; y < icon.height; y++)
for (int x = 0; x < icon.width; x++)
if (iconPixel(icon, x, y))
setBitmapPixel(bitmap, x, y, true);
}
void iconFromBitmap(const uint8_t *bitmap, Icon &icon)
{
int right = 0, bottom = 0;
for (int y = 0; y < BITMAP_HEIGHT; y++)
for (int x = 0; x < BITMAP_WIDTH; x++)
if (bitmapPixel(bitmap, x, y))
{
right = x > right ? x : right;
bottom = y > bottom ? y : bottom;
}
icon = Icon();
icon.width = right + 1;
icon.height = bottom + 1;
for (int y = 0; y < icon.height; y++)
for (int x = 0; x < icon.width; x++)
if (bitmapPixel(bitmap, x, y))
setIconPixel(icon, x, y, true);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
#include "logic/bitmap_format.h"
// Icons: small bitmaps of their own size (1 x 1 up to 84 x 19) that pages place as images
// or backgrounds; they are never shown on their own. Rows from top to bottom, each
// (width + 7) / 8 bytes, the most significant bit is the leftmost dot (like the full
// screen bitmaps of logic/bitmap_format.h). Icons share the names of bitmaps and pages.
// Pure logic, tested in test/test_esp32.
const int ICON_MAX_WIDTH = BITMAP_WIDTH;
const int ICON_MAX_HEIGHT = BITMAP_HEIGHT;
struct Icon
{
uint8_t width = 0;
uint8_t height = 0;
uint8_t data[BITMAP_BYTES] = {}; // iconBytes(width, height) are used
};
inline int iconRowBytes(int width)
{
return (width + 7) / 8;
}
inline int iconBytes(int width, int height)
{
return iconRowBytes(width) * height;
}
bool validIconSize(int width, int height);
bool iconPixel(const Icon &icon, int x, int y);
void setIconPixel(Icon &icon, int x, int y, bool on);
// Exactly iconBytes(width, height) * 2 hex digits. Returns false on anything else.
bool iconFromHex(int width, int height, const char *hex, Icon &icon);
// out must hold BITMAP_HEX_LENGTH + 1 characters
void iconToHex(const Icon &icon, char *out);
// The icon in a full screen bitmap, with its top left corner at 0, 0
void iconToBitmap(const Icon &icon, uint8_t *bitmap);
// A full screen bitmap as an icon that reaches from 0, 0 to its last dot on the right and
// at the bottom, so it is drawn on the same dots (1 x 1 when it has no dots)
void iconFromBitmap(const uint8_t *bitmap, Icon &icon);
+26
View File
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
static const uint16_t MINUTES_PER_DAY = 24 * 60;
static const uint32_t MAX_TARGET = 36525UL * MINUTES_PER_DAY; // 2100-01-01
@@ -70,6 +71,31 @@ bool elementUsesHa(const PageElement &element)
}
}
bool pageUsesImage(const DynamicPage &page, const char *name, bool by_state)
{
for (int i = 0; i < page.slot_count; i++)
if (sameName(page.slots[i].bitmap, name))
return true;
for (int i = 0; i < page.element_count; i++)
{
const PageElement &e = page.elements[i];
if (e.type != ElementType::Image)
continue;
if (!e.entity[0])
{
if (sameName(e.text, name))
return true;
continue;
}
size_t start = strlen(e.text);
if (strncasecmp(name, e.text, start) != 0)
continue;
if (by_state ? name[start] != '\0' : strcasecmp(name + start, "default") == 0)
return true;
}
return false;
}
bool validEntityId(const char *entity)
{
if (entity == nullptr)
+5
View File
@@ -223,6 +223,11 @@ void plexUserSlug(const char *name, char *out, size_t out_size);
// The element shows a Home Assistant value (HaValue; Image, Bar with an entity)
bool elementUsesHa(const PageElement &element);
// Whether the page uses this bitmap or icon (compared without case): as a background, as
// a fixed image, or as the "default" image of an image with an entity. by_state: also the
// names an image with an entity may show (its name start + any state).
bool pageUsesImage(const DynamicPage &page, const char *name, bool by_state = false);
// Entity ids look like "sensor.outside_temperature" (lower case, digits, '_', one '.');
// {plex_user} may be in them
bool validEntityId(const char *entity);