feat: Lyra Icons (#725)

/!\ This PR depends on
https://github.com/crosspoint-reader/crosspoint-reader/pull/732 being
merged first

Also requires the
https://github.com/open-x4-epaper/community-sdk/pull/18 PR

## Summary

Lyra theme icons on the home menu, in the file browser and on empty book
covers

![IMG_8023
Medium](https://github.com/user-attachments/assets/ba7c1407-94d2-4353-80ff-d5b800c6ac5b)
![IMG_8024
Medium](https://github.com/user-attachments/assets/edb59e13-b1c9-4c86-bef3-c61cc8134e64)
![IMG_7958
Medium](https://github.com/user-attachments/assets/d3079ce1-95f0-43f4-bbc7-1f747cc70203)
![IMG_8033
Medium](https://github.com/user-attachments/assets/f3e2e03b-0fa8-47b7-8717-c0b71361b7a8)


## Additional Context

- Added a function to the open-x4-sdk renderer to draw transparent
images
- Added a scripts/convert_icon.py script to convert svg/png icons into a
C array that can be directly imported into the project. Usage:
```bash
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32
```
This will create a components/icons/cover.h file with a C array called
CoverIcon, of size 32x32px. Lyra uses icons from
https://lucide.dev/icons with a stroke width of 2px, that can be
downloaded with any desired size on the site.

> The file browser is noticeably slower with the addition of icons, and
using an image buffer like on the home page doesn't help very much. Any
suggestions to optimize this are welcome.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.


Did you use AI tools to help write this code? _**PARTIALLY**_
The icon conversion python script was generated by Copilot as I am not a
python dev.

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
CaptainFrito
2026-02-19 17:38:09 +07:00
committed by GitHub
parent e7ee6ff05e
commit fdcd71e94d
31 changed files with 434 additions and 34 deletions

80
scripts/convert_icon.py Normal file
View File

@@ -0,0 +1,80 @@
import sys
import os
from PIL import Image
import cairosvg
import io
threshold = 128
def svg_to_png_bytes(svg_path, width, height):
with open(svg_path, 'rb') as f:
svg_data = f.read()
png_bytes = cairosvg.svg2png(bytestring=svg_data, output_width=width, output_height=height)
return png_bytes
def load_image(path, width, height):
ext = os.path.splitext(path)[1].lower()
if ext == '.svg':
png_bytes = svg_to_png_bytes(path, width, height)
img = Image.open(io.BytesIO(png_bytes))
else:
img = Image.open(path)
img = img.convert('RGBA')
img = img.resize((width, height), Image.LANCZOS)
# Flatten alpha: paste on white background
background = Image.new('RGBA', img.size, (255, 255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
# Rotate 90 degrees counterclockwise
img = img.rotate(90, expand=True)
return img
def image_to_c_array(img, array_name):
# Convert to grayscale, then threshold to get white=1, black=0
# Convert to grayscale
img = img.convert('L')
width, height = img.size
pixels = list(img.getdata())
packed = []
for y in range(height):
for x in range(0, width, 8):
byte = 0
for b in range(8):
if x + b < width:
v = pixels[y * width + x + b]
# 1 for white, 0 for black
bit = 1 if v >= threshold else 0
byte |= (bit << (7 - b))
packed.append(byte)
# Format as C array
c = f'#pragma once\n#include <cstdint>\n\n'
c += f'// size: {width}x{height}\n'
c += f'static const uint8_t {array_name}[] = {{\n '
for i, v in enumerate(packed):
c += f'0x{v:02X}, '
if (i + 1) % 16 == 0:
c += '\n '
c = c.rstrip(', \n') + '\n};\n'
return c
def main():
if len(sys.argv) < 5:
print('Usage: python convert_image.py input.png output_name width height')
sys.exit(1)
input_path, output_name, width, height = sys.argv[1:5]
array_name = output_name.capitalize() + 'Icon'
width, height = int(width), int(height)
img = load_image(input_path, width, height)
c_array = image_to_c_array(img, array_name)
# Always save to src/components/icons/[output_name].h relative to project root
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
output_dir = os.path.join(project_root, 'src', 'components', 'icons')
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, f'{output_name}.h')
with open(output_path, 'w') as f:
f.write(c_array)
print(f'Wrote {output_path}')
if __name__ == '__main__':
main()