From ae6148a9e20f3ac98796ad80085578824039c325 Mon Sep 17 00:00:00 2001 From: Oleksandr Polishchuk Date: Wed, 13 May 2026 21:13:24 +0100 Subject: [PATCH 1/3] micropython/bfu_ua_display: Add Ukrainian text library. Add bfu_ua_display package for rendering Ukrainian text on MicroPython displays. Features: * Full Ukrainian alphabet support. * 5x7 bitmap font optimized for ESP32. * Works with displays supporting pixel() method. * Tested with SSD1306 OLED displays. Package provides ua_text, ua_text_center, and ua_text_scaled. Signed-off-by: Oleksandr Polishchuk --- micropython/bfu_ua_display/LICENSE | 21 ++ micropython/bfu_ua_display/README.md | 182 ++++++++++++ .../bfu_ua_display/bfu_ua_display/__init__.py | 24 ++ .../bfu_ua_display/bfu_ua_display/font5x7.py | 235 +++++++++++++++ .../bfu_ua_display/text_engine.py | 260 ++++++++++++++++ .../bfu_ua_display/bfu_ua_display/utils.py | 278 ++++++++++++++++++ micropython/bfu_ua_display/manifest.py | 6 + 7 files changed, 1006 insertions(+) create mode 100644 micropython/bfu_ua_display/LICENSE create mode 100644 micropython/bfu_ua_display/README.md create mode 100644 micropython/bfu_ua_display/bfu_ua_display/__init__.py create mode 100644 micropython/bfu_ua_display/bfu_ua_display/font5x7.py create mode 100644 micropython/bfu_ua_display/bfu_ua_display/text_engine.py create mode 100644 micropython/bfu_ua_display/bfu_ua_display/utils.py create mode 100644 micropython/bfu_ua_display/manifest.py diff --git a/micropython/bfu_ua_display/LICENSE b/micropython/bfu_ua_display/LICENSE new file mode 100644 index 000000000..e6cd09f65 --- /dev/null +++ b/micropython/bfu_ua_display/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BFU Electronics + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/micropython/bfu_ua_display/README.md b/micropython/bfu_ua_display/README.md new file mode 100644 index 000000000..997a77540 --- /dev/null +++ b/micropython/bfu_ua_display/README.md @@ -0,0 +1,182 @@ +# BFU UA Display + +Ukrainian text rendering library for MicroPython displays. + +## Description + +A lightweight library for rendering Ukrainian text on displays commonly used with ESP32 and MicroPython projects. Standard MicroPython display libraries do not include Ukrainian characters (А, Б, В, Г, Ґ, Д, Е, Є, Ж, З, И, І, Ї, Й, etc.), making it impossible to display Ukrainian text properly. This library solves that problem with a custom 5x7 bitmap font containing all 33 Ukrainian letters (uppercase and lowercase). + +## Features + +- **Full Ukrainian Alphabet Support** - All 33 Ukrainian letters (uppercase and lowercase) +- **Lightweight** - Optimized for ESP32 memory constraints (~2-3 KB) +- **Display Agnostic** - Works with any display supporting `pixel()` method +- **Simple API** - Three main functions for text rendering +- **5x7 Bitmap Font** - Compact and readable on small displays + +## Installation + +```python +import mip +mip.install("bfu_ua_display") +``` + +Or using mpremote: + +```bash +mpremote connect COM3 mip install bfu_ua_display +``` + +## Quick Start + +```python +from machine import I2C, Pin +from ssd1306 import SSD1306_I2C +from bfu_ua_display import ua_text, ua_text_center + +# Initialize display +i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +oled = SSD1306_I2C(128, 64, i2c) + +# Draw Ukrainian text +ua_text(oled, "ПРИВІТ УКРАЇНО!", 0, 0) +ua_text_center(oled, "BFU Electronics", 28) + +# Update display +oled.show() +``` + +## API Reference + +### ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False) + +Render text at specified position with Ukrainian character support. + +**Parameters:** +- `display` - Display object with `pixel()` method +- `text` - String to render (Ukrainian, English, numbers, symbols) +- `x` - X coordinate (left edge) +- `y` - Y coordinate (top edge) +- `color` - Foreground color (default: 1) +- `bg_color` - Background color (default: 0) +- `clear_bg` - Clear background behind text (default: False) + +**Returns:** Total width of rendered text in pixels + +**Example:** +```python +ua_text(oled, "Температура: 25°C", 0, 0) +``` + +--- + +### ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, display_width=128) + +Render text centered horizontally on the display. + +**Parameters:** +- `display` - Display object +- `text` - String to render +- `y` - Y coordinate (top edge) +- `color` - Foreground color (default: 1) +- `bg_color` - Background color (default: 0) +- `clear_bg` - Clear background (default: False) +- `display_width` - Display width in pixels (default: 128) + +**Returns:** X coordinate where text was rendered + +**Example:** +```python +ua_text_center(oled, "УКРАЇНА", 28) +``` + +--- + +### ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=False) + +Render text with scaling (2x, 3x, etc.). + +**Parameters:** +- `display` - Display object +- `text` - String to render +- `x` - X coordinate (left edge) +- `y` - Y coordinate (top edge) +- `scale` - Scaling factor (1=normal, 2=double, etc.) +- `color` - Foreground color (default: 1) +- `bg_color` - Background color (default: 0) +- `clear_bg` - Clear background (default: False) + +**Returns:** Total width of rendered text in pixels + +**Example:** +```python +ua_text_scaled(oled, "ПРИВІТ", 0, 0, scale=2) +``` + +## Supported Characters + +- **Ukrainian Alphabet**: А Б В Г Ґ Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ь Ю Я (uppercase and lowercase) +- **English Alphabet**: A-Z, a-z +- **Numbers**: 0-9 +- **Symbols**: Common punctuation and special characters + +## Display Requirements + +The library works with any display object that implements: + +- `pixel(x, y, color)` - Set individual pixel (required) +- `show()` - Update display (optional, for buffered displays) +- `fill_rect(x, y, width, height, color)` - Fill rectangle (optional, for optimization) + +## Compatibility + +**Tested with:** +- ESP32 with MicroPython v1.19+ +- SSD1306 OLED displays (128x64, 128x32) via I2C/SPI + +**Compatible with:** +- Any MicroPython-compatible board +- Any display supporting the `pixel()` method + +## Examples + +### Multi-line Text + +```python +oled.fill(0) +ua_text(oled, "Рядок 1", 0, 0) +ua_text(oled, "Рядок 2", 0, 10) +ua_text(oled, "Рядок 3", 0, 20) +oled.show() +``` + +### Scaled Text + +```python +oled.fill(0) +ua_text_scaled(oled, "ВЕЛИКИЙ", 0, 0, scale=2) +oled.show() +``` + +### Centered Text + +```python +oled.fill(0) +ua_text_center(oled, "УКРАЇНА", 10) +ua_text_center(oled, "2026", 28) +oled.show() +``` + +## License + +MIT License + +## Documentation + +For complete documentation, examples, and troubleshooting, visit: + +**https://github.com/BrainFromUkraine/bfu_ua_display** + +## Author + +BFU Electronics diff --git a/micropython/bfu_ua_display/bfu_ua_display/__init__.py b/micropython/bfu_ua_display/bfu_ua_display/__init__.py new file mode 100644 index 000000000..e2d67f190 --- /dev/null +++ b/micropython/bfu_ua_display/bfu_ua_display/__init__.py @@ -0,0 +1,24 @@ +""" +BFU UA Display - Ukrainian Text Rendering Library for MicroPython +================================================================== + +A professional, lightweight library for rendering Ukrainian text on displays +commonly used with ESP32 and MicroPython projects. + +Features: +- Full Ukrainian alphabet support (33 letters) +- Optimized for ESP32 memory constraints +- Clean, modular architecture +- Easy to use API +- Extensible for multiple display types + +Author: BFU Electronics +License: MIT +Version: 0.1.0 +""" + +from .text_engine import ua_text, ua_text_center, ua_text_scaled + +__version__ = "0.1.0" +__author__ = "BFU Electronics" +__all__ = ["ua_text", "ua_text_center", "ua_text_scaled"] diff --git a/micropython/bfu_ua_display/bfu_ua_display/font5x7.py b/micropython/bfu_ua_display/bfu_ua_display/font5x7.py new file mode 100644 index 000000000..7ca1851f7 --- /dev/null +++ b/micropython/bfu_ua_display/bfu_ua_display/font5x7.py @@ -0,0 +1,235 @@ +# ruff: noqa: RUF001, RUF003 +""" +5x7 Bitmap Font with Ukrainian Character Support +================================================= + +Compact bitmap font optimized for small displays and ESP32 memory constraints. +Each character is 5 pixels wide and 7 pixels tall. + +Character encoding uses bytearrays where each byte represents a column of pixels. +""" + +# Font dimensions +FONT_WIDTH = 5 +FONT_HEIGHT = 7 + +# Basic ASCII characters (32-126) +# Each character is represented as 5 bytes (columns), 7 bits per byte (rows) +_ASCII_FONT = { + " ": bytearray([0x00, 0x00, 0x00, 0x00, 0x00]), + "!": bytearray([0x00, 0x00, 0x5F, 0x00, 0x00]), + '"': bytearray([0x00, 0x07, 0x00, 0x07, 0x00]), + "#": bytearray([0x14, 0x7F, 0x14, 0x7F, 0x14]), + "$": bytearray([0x24, 0x2A, 0x7F, 0x2A, 0x12]), + "%": bytearray([0x23, 0x13, 0x08, 0x64, 0x62]), + "&": bytearray([0x36, 0x49, 0x55, 0x22, 0x50]), + "'": bytearray([0x00, 0x05, 0x03, 0x00, 0x00]), + "(": bytearray([0x00, 0x1C, 0x22, 0x41, 0x00]), + ")": bytearray([0x00, 0x41, 0x22, 0x1C, 0x00]), + "*": bytearray([0x14, 0x08, 0x3E, 0x08, 0x14]), + "+": bytearray([0x08, 0x08, 0x3E, 0x08, 0x08]), + ",": bytearray([0x00, 0x50, 0x30, 0x00, 0x00]), + "-": bytearray([0x08, 0x08, 0x08, 0x08, 0x08]), + ".": bytearray([0x00, 0x60, 0x60, 0x00, 0x00]), + "/": bytearray([0x20, 0x10, 0x08, 0x04, 0x02]), + "0": bytearray([0x3E, 0x51, 0x49, 0x45, 0x3E]), + "1": bytearray([0x00, 0x42, 0x7F, 0x40, 0x00]), + "2": bytearray([0x42, 0x61, 0x51, 0x49, 0x46]), + "3": bytearray([0x21, 0x41, 0x45, 0x4B, 0x31]), + "4": bytearray([0x18, 0x14, 0x12, 0x7F, 0x10]), + "5": bytearray([0x27, 0x45, 0x45, 0x45, 0x39]), + "6": bytearray([0x3C, 0x4A, 0x49, 0x49, 0x30]), + "7": bytearray([0x01, 0x71, 0x09, 0x05, 0x03]), + "8": bytearray([0x36, 0x49, 0x49, 0x49, 0x36]), + "9": bytearray([0x06, 0x49, 0x49, 0x29, 0x1E]), + ":": bytearray([0x00, 0x36, 0x36, 0x00, 0x00]), + ";": bytearray([0x00, 0x56, 0x36, 0x00, 0x00]), + "<": bytearray([0x08, 0x14, 0x22, 0x41, 0x00]), + "=": bytearray([0x14, 0x14, 0x14, 0x14, 0x14]), + ">": bytearray([0x00, 0x41, 0x22, 0x14, 0x08]), + "?": bytearray([0x02, 0x01, 0x51, 0x09, 0x06]), + "@": bytearray([0x32, 0x49, 0x79, 0x41, 0x3E]), + "A": bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), + "B": bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), + "C": bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), + "D": bytearray([0x7F, 0x41, 0x41, 0x22, 0x1C]), + "E": bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), + "F": bytearray([0x7F, 0x09, 0x09, 0x09, 0x01]), + "G": bytearray([0x3E, 0x41, 0x49, 0x49, 0x7A]), + "H": bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), + "I": bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), + "J": bytearray([0x20, 0x40, 0x41, 0x3F, 0x01]), + "K": bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), + "L": bytearray([0x7F, 0x40, 0x40, 0x40, 0x40]), + "M": bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), + "N": bytearray([0x7F, 0x04, 0x08, 0x10, 0x7F]), + "O": bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), + "P": bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), + "Q": bytearray([0x3E, 0x41, 0x51, 0x21, 0x5E]), + "R": bytearray([0x7F, 0x09, 0x19, 0x29, 0x46]), + "S": bytearray([0x46, 0x49, 0x49, 0x49, 0x31]), + "T": bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), + "U": bytearray([0x3F, 0x40, 0x40, 0x40, 0x3F]), + "V": bytearray([0x1F, 0x20, 0x40, 0x20, 0x1F]), + "W": bytearray([0x3F, 0x40, 0x38, 0x40, 0x3F]), + "X": bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), + "Y": bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), + "Z": bytearray([0x61, 0x51, 0x49, 0x45, 0x43]), + "[": bytearray([0x00, 0x7F, 0x41, 0x41, 0x00]), + "\\": bytearray([0x02, 0x04, 0x08, 0x10, 0x20]), + "]": bytearray([0x00, 0x41, 0x41, 0x7F, 0x00]), + "^": bytearray([0x04, 0x02, 0x01, 0x02, 0x04]), + "_": bytearray([0x40, 0x40, 0x40, 0x40, 0x40]), + "`": bytearray([0x00, 0x01, 0x02, 0x04, 0x00]), + "a": bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), + "b": bytearray([0x7F, 0x48, 0x44, 0x44, 0x38]), + "c": bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), + "d": bytearray([0x38, 0x44, 0x44, 0x48, 0x7F]), + "e": bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), + "f": bytearray([0x08, 0x7E, 0x09, 0x01, 0x02]), + "g": bytearray([0x0C, 0x52, 0x52, 0x52, 0x3E]), + "h": bytearray([0x7F, 0x08, 0x04, 0x04, 0x78]), + "i": bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), + "j": bytearray([0x20, 0x40, 0x44, 0x3D, 0x00]), + "k": bytearray([0x7F, 0x10, 0x28, 0x44, 0x00]), + "l": bytearray([0x00, 0x41, 0x7F, 0x40, 0x00]), + "m": bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), + "n": bytearray([0x7C, 0x08, 0x04, 0x04, 0x78]), + "o": bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), + "p": bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), + "q": bytearray([0x08, 0x14, 0x14, 0x18, 0x7C]), + "r": bytearray([0x7C, 0x08, 0x04, 0x04, 0x08]), + "s": bytearray([0x48, 0x54, 0x54, 0x54, 0x20]), + "t": bytearray([0x04, 0x3F, 0x44, 0x40, 0x20]), + "u": bytearray([0x3C, 0x40, 0x40, 0x20, 0x7C]), + "v": bytearray([0x1C, 0x20, 0x40, 0x20, 0x1C]), + "w": bytearray([0x3C, 0x40, 0x30, 0x40, 0x3C]), + "x": bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), + "y": bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), + "z": bytearray([0x44, 0x64, 0x54, 0x4C, 0x44]), + "{": bytearray([0x00, 0x08, 0x36, 0x41, 0x00]), + "|": bytearray([0x00, 0x00, 0x7F, 0x00, 0x00]), + "}": bytearray([0x00, 0x41, 0x36, 0x08, 0x00]), + "~": bytearray([0x10, 0x08, 0x08, 0x10, 0x08]), +} + +# Ukrainian Cyrillic characters +# Uppercase: А Б В Г Ґ Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ь Ю Я +# Lowercase: а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ь ю я +_UKRAINIAN_FONT = { + # Uppercase Ukrainian + "А": bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), # A (same as Latin A) + "Б": bytearray([0x7F, 0x49, 0x49, 0x49, 0x31]), # B + "В": bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), # V (same as Latin B) + "Г": bytearray([0x7F, 0x09, 0x09, 0x01, 0x01]), # H (Cyrillic) + "Ґ": bytearray([0x7F, 0x09, 0x09, 0x01, 0x03]), # G with upturn + "Д": bytearray([0x60, 0x3E, 0x21, 0x3E, 0x60]), # D + "Е": bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), # E (same as Latin E) + "Є": bytearray([0x3E, 0x49, 0x49, 0x49, 0x22]), # Ye + "Ж": bytearray([0x63, 0x14, 0x7F, 0x14, 0x63]), # Zh + "З": bytearray([0x22, 0x41, 0x49, 0x49, 0x36]), # Z + "И": bytearray([0x7F, 0x20, 0x10, 0x08, 0x7F]), # Y + "І": bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), # I (same as Latin I) + "Ї": bytearray([0x22, 0x41, 0x7F, 0x41, 0x22]), # Yi (I with dots) + "Й": bytearray([0x7E, 0x20, 0x11, 0x08, 0x7E]), # Y with breve + "К": bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), # K (same as Latin K) + "Л": bytearray([0x60, 0x30, 0x0F, 0x30, 0x60]), # L + "М": bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), # M (same as Latin M) + "Н": bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), # N (same as Latin H) + "О": bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), # O (same as Latin O) + "П": bytearray([0x7F, 0x01, 0x01, 0x01, 0x7F]), # P + "Р": bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), # R (same as Latin P) + "С": bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), # S (same as Latin C) + "Т": bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), # T (same as Latin T) + "У": bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), # U (same as Latin Y) + "Ф": bytearray([0x38, 0x54, 0x7F, 0x54, 0x38]), # F + "Х": bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), # Kh (same as Latin X) + "Ц": bytearray([0x3F, 0x40, 0x40, 0x7F, 0x40]), # Ts + "Ч": bytearray([0x07, 0x04, 0x04, 0x04, 0x7F]), # Ch + "Ш": bytearray([0x7F, 0x40, 0x7F, 0x40, 0x7F]), # Sh + "Щ": bytearray([0x7F, 0x40, 0x7F, 0x40, 0xFF]), # Shch + "Ь": bytearray([0x7F, 0x48, 0x48, 0x48, 0x30]), # Soft sign + "Ю": bytearray([0x7F, 0x38, 0x44, 0x44, 0x38]), # Yu + "Я": bytearray([0x32, 0x49, 0x49, 0x49, 0x7F]), # Ya + # Lowercase Ukrainian + "а": bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), # a (same as Latin a) + "б": bytearray([0x3F, 0x44, 0x44, 0x44, 0x38]), # b + "в": bytearray([0x7C, 0x54, 0x54, 0x54, 0x28]), # v + "г": bytearray([0x7C, 0x04, 0x04, 0x04, 0x00]), # h + "ґ": bytearray([0x7C, 0x04, 0x04, 0x04, 0x06]), # g with upturn + "д": bytearray([0x30, 0x28, 0x24, 0x7C, 0x60]), # d + "е": bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), # e (same as Latin e) + "є": bytearray([0x38, 0x54, 0x54, 0x54, 0x44]), # ye + "ж": bytearray([0x44, 0x28, 0x7C, 0x28, 0x44]), # zh + "з": bytearray([0x28, 0x44, 0x54, 0x54, 0x28]), # z + "и": bytearray([0x7C, 0x20, 0x10, 0x08, 0x7C]), # y + "і": bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), # i (same as Latin i) + "ї": bytearray([0x28, 0x44, 0x7D, 0x40, 0x28]), # yi (i with dots) + "й": bytearray([0x7C, 0x21, 0x12, 0x09, 0x7C]), # y with breve + "к": bytearray([0x7C, 0x10, 0x28, 0x44, 0x00]), # k + "л": bytearray([0x30, 0x28, 0x24, 0x28, 0x30]), # l + "м": bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), # m (same as Latin m) + "н": bytearray([0x7C, 0x08, 0x08, 0x08, 0x7C]), # n + "о": bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), # o (same as Latin o) + "п": bytearray([0x7C, 0x04, 0x04, 0x04, 0x7C]), # p + "р": bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), # r (same as Latin p) + "с": bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), # s (same as Latin c) + "т": bytearray([0x04, 0x04, 0x7F, 0x04, 0x04]), # t + "у": bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), # u (same as Latin y) + "ф": bytearray([0x38, 0x54, 0x7C, 0x54, 0x38]), # f + "х": bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), # kh (same as Latin x) + "ц": bytearray([0x3C, 0x40, 0x40, 0x7C, 0x40]), # ts + "ч": bytearray([0x0C, 0x10, 0x10, 0x10, 0x7C]), # ch + "ш": bytearray([0x7C, 0x40, 0x7C, 0x40, 0x7C]), # sh + "щ": bytearray([0x7C, 0x40, 0x7C, 0x40, 0xFC]), # shch + "ь": bytearray([0x7C, 0x48, 0x48, 0x48, 0x30]), # soft sign + "ю": bytearray([0x7C, 0x38, 0x44, 0x44, 0x38]), # yu + "я": bytearray([0x28, 0x54, 0x54, 0x54, 0x7C]), # ya +} + +# Combine all fonts into one dictionary +FONT_DATA = {} +FONT_DATA.update(_ASCII_FONT) +FONT_DATA.update(_UKRAINIAN_FONT) + + +def get_char_bitmap(char): + """ + Get bitmap data for a character. + + Args: + char: Single character to get bitmap for + + Returns: + bytearray: 5-byte bitmap data, or None if character not found + """ + return FONT_DATA.get(char, None) + + +def char_width(char): + """ + Get the width of a character in pixels. + + Args: + char: Single character + + Returns: + int: Width in pixels (always 5 for this font) + """ + return FONT_WIDTH if char in FONT_DATA else 0 + + +def text_width(text): + """ + Calculate the total width of a text string in pixels. + + Args: + text: String to measure + + Returns: + int: Total width in pixels (including 1px spacing between chars) + """ + if not text: + return 0 + # Each character is FONT_WIDTH pixels, plus 1 pixel spacing between characters + return len(text) * (FONT_WIDTH + 1) - 1 diff --git a/micropython/bfu_ua_display/bfu_ua_display/text_engine.py b/micropython/bfu_ua_display/bfu_ua_display/text_engine.py new file mode 100644 index 000000000..c7cae67e5 --- /dev/null +++ b/micropython/bfu_ua_display/bfu_ua_display/text_engine.py @@ -0,0 +1,260 @@ +""" +Text Rendering Engine for Ukrainian Display Library +==================================================== + +Core rendering functions for drawing text on displays. +Optimized for MicroPython and ESP32 memory constraints. + +The engine is designed to work with any display object that supports: +- pixel(x, y, color) - Set individual pixel +- fill_rect(x, y, width, height, color) - Fill rectangle (optional, for optimization) +- show() - Update display (optional, for buffered displays) +""" + +from . import font5x7 + + +def ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False): + """ + Render text at specified position with Ukrainian character support. + + This is the core rendering function that draws text pixel-by-pixel + using the bitmap font data. + + Args: + display: Display object with pixel() method + text: String to render (supports English, numbers, symbols, Ukrainian) + x: X coordinate (left edge) + y: Y coordinate (top edge) + color: Foreground color (default: 1 for white/on) + bg_color: Background color (default: 0 for black/off) + clear_bg: If True, clear background behind text (default: False) + + Returns: + int: Total width of rendered text in pixels + + Example: + >>> from machine import I2C, Pin + >>> from ssd1306 import SSD1306_I2C + >>> from bfu_ua_display import ua_text + >>> + >>> i2c = I2C(0, scl=Pin(22), sda=Pin(21)) + >>> oled = SSD1306_I2C(128, 64, i2c) + >>> + >>> ua_text(oled, "ПРИВІТ", 0, 0) + >>> oled.show() + """ + if not text: + return 0 + + cursor_x = x + total_width = 0 + + for char in text: + bitmap = font5x7.get_char_bitmap(char) + + if bitmap is None: + # Character not found, skip it + continue + + # Clear background if requested + if clear_bg and hasattr(display, "fill_rect"): + display.fill_rect(cursor_x, y, font5x7.FONT_WIDTH, font5x7.FONT_HEIGHT, bg_color) + + # Render character bitmap + for col in range(font5x7.FONT_WIDTH): + column_data = bitmap[col] + for row in range(font5x7.FONT_HEIGHT): + # Check if pixel should be set (bit is 1) + if column_data & (1 << row): + display.pixel(cursor_x + col, y + row, color) + elif clear_bg: + # Clear pixel if background clearing is enabled + display.pixel(cursor_x + col, y + row, bg_color) + + # Move cursor to next character position (with 1px spacing) + cursor_x += font5x7.FONT_WIDTH + 1 + total_width += font5x7.FONT_WIDTH + 1 + + # Remove trailing spacing from total width + if total_width > 0: + total_width -= 1 + + return total_width + + +def ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, display_width=128): + """ + Render text centered horizontally on the display. + + Calculates the text width and centers it automatically. + + Args: + display: Display object with pixel() method + text: String to render + y: Y coordinate (top edge) + color: Foreground color (default: 1) + bg_color: Background color (default: 0) + clear_bg: If True, clear background behind text (default: False) + display_width: Display width in pixels (default: 128 for SSD1306) + + Returns: + int: X coordinate where text was rendered + + Example: + >>> ua_text_center(oled, "УКРАЇНА", 28) + >>> oled.show() + """ + text_w = font5x7.text_width(text) + x = (display_width - text_w) // 2 + + # Ensure x is not negative + x = max(x, 0) + + ua_text(display, text, x, y, color, bg_color, clear_bg) + return x + + +def ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=False): + """ + Render text with scaling (2x, 3x, etc.). + + Each pixel in the original font is rendered as a scale x scale block. + Note: This is memory-intensive for large scale values. + + Args: + display: Display object with pixel() or fill_rect() method + text: String to render + x: X coordinate (left edge) + y: Y coordinate (top edge) + scale: Scaling factor (1=normal, 2=double size, etc.) + color: Foreground color (default: 1) + bg_color: Background color (default: 0) + clear_bg: If True, clear background behind text (default: False) + + Returns: + int: Total width of rendered text in pixels + + Example: + >>> ua_text_scaled(oled, "ПРИВІТ", 0, 0, scale=2) + >>> oled.show() + """ + if not text or scale < 1: + return 0 + + # For scale=1, use regular rendering for efficiency + if scale == 1: + return ua_text(display, text, x, y, color, bg_color, clear_bg) + + cursor_x = x + total_width = 0 + scaled_width = font5x7.FONT_WIDTH * scale + scaled_height = font5x7.FONT_HEIGHT * scale + spacing = scale # Scaled spacing between characters + + # Check if display supports fill_rect for optimization + has_fill_rect = hasattr(display, "fill_rect") + + for char in text: + bitmap = font5x7.get_char_bitmap(char) + + if bitmap is None: + continue + + # Clear background if requested + if clear_bg and has_fill_rect: + display.fill_rect(cursor_x, y, scaled_width, scaled_height, bg_color) + + # Render scaled character + for col in range(font5x7.FONT_WIDTH): + column_data = bitmap[col] + for row in range(font5x7.FONT_HEIGHT): + pixel_on = column_data & (1 << row) + + # Draw scaled pixel as a block + if pixel_on or clear_bg: + pixel_color = color if pixel_on else bg_color + + if has_fill_rect: + # Use fill_rect for efficiency + display.fill_rect( + cursor_x + col * scale, y + row * scale, scale, scale, pixel_color + ) + else: + # Fall back to individual pixels + for sx in range(scale): + for sy in range(scale): + display.pixel( + cursor_x + col * scale + sx, y + row * scale + sy, pixel_color + ) + + # Move cursor + cursor_x += scaled_width + spacing + total_width += scaled_width + spacing + + # Remove trailing spacing + if total_width > 0: + total_width -= spacing + + return total_width + + +def ua_text_right(display, text, x, y, color=1, bg_color=0, clear_bg=False): + """ + Render text right-aligned at specified position. + + The x coordinate represents the right edge of the text. + + Args: + display: Display object with pixel() method + text: String to render + x: X coordinate (right edge) + y: Y coordinate (top edge) + color: Foreground color (default: 1) + bg_color: Background color (default: 0) + clear_bg: If True, clear background behind text (default: False) + + Returns: + int: X coordinate where text starts (left edge) + + Example: + >>> ua_text_right(oled, "100%", 127, 0) + >>> oled.show() + """ + text_w = font5x7.text_width(text) + start_x = x - text_w + + # Ensure start_x is not negative + start_x = max(start_x, 0) + + ua_text(display, text, start_x, y, color, bg_color, clear_bg) + return start_x + + +def clear_text_area(display, x, y, width, height, color=0): + """ + Clear a rectangular area on the display. + + Useful for clearing text before redrawing. + + Args: + display: Display object + x: X coordinate (left edge) + y: Y coordinate (top edge) + width: Width in pixels + height: Height in pixels + color: Fill color (default: 0 for black) + + Example: + >>> # Clear area before updating text + >>> clear_text_area(oled, 0, 0, 128, 8) + >>> ua_text(oled, "Updated", 0, 0) + >>> oled.show() + """ + if hasattr(display, "fill_rect"): + display.fill_rect(x, y, width, height, color) + else: + # Fall back to pixel-by-pixel clearing + for px in range(x, x + width): + for py in range(y, y + height): + display.pixel(px, py, color) diff --git a/micropython/bfu_ua_display/bfu_ua_display/utils.py b/micropython/bfu_ua_display/bfu_ua_display/utils.py new file mode 100644 index 000000000..3d4fc826f --- /dev/null +++ b/micropython/bfu_ua_display/bfu_ua_display/utils.py @@ -0,0 +1,278 @@ +""" +Utility Functions for BFU UA Display Library +============================================= + +Helper functions for text measurement, display detection, and common operations. +""" + +from . import font5x7 + + +def measure_text(text): + """ + Measure the dimensions of a text string. + + Args: + text: String to measure + + Returns: + tuple: (width, height) in pixels + + Example: + >>> width, height = measure_text("ПРИВІТ") + >>> print(f"Text size: {width}x{height}") + """ + width = font5x7.text_width(text) + height = font5x7.FONT_HEIGHT + return (width, height) + + +def measure_text_scaled(text, scale=2): + """ + Measure the dimensions of scaled text. + + Args: + text: String to measure + scale: Scaling factor + + Returns: + tuple: (width, height) in pixels + + Example: + >>> width, height = measure_text_scaled("ПРИВІТ", scale=2) + """ + base_width = font5x7.text_width(text) + width = base_width * scale + (len(text) - 1) * scale if text else 0 + height = font5x7.FONT_HEIGHT * scale + return (width, height) + + +def wrap_text(text, max_width, char_spacing=1): + """ + Wrap text to fit within a maximum width. + + Breaks text into lines that fit within the specified width. + Tries to break at spaces when possible. + + Args: + text: String to wrap + max_width: Maximum width in pixels + char_spacing: Spacing between characters (default: 1) + + Returns: + list: List of text lines + + Example: + >>> lines = wrap_text("ПРИВІТ УКРАЇНО", 40) + >>> for i, line in enumerate(lines): + >>> ua_text(oled, line, 0, i * 8) + """ + if not text: + return [] + + lines = [] + words = text.split(" ") + current_line = "" + + for word in words: + test_line = current_line + (" " if current_line else "") + word + test_width = len(test_line) * (font5x7.FONT_WIDTH + char_spacing) + + if test_width <= max_width: + current_line = test_line + else: + # Current line is full, start new line + if current_line: + lines.append(current_line) + + # Check if single word is too long + word_width = len(word) * (font5x7.FONT_WIDTH + char_spacing) + if word_width > max_width: + # Break word into chunks + chars_per_line = max_width // (font5x7.FONT_WIDTH + char_spacing) + for i in range(0, len(word), chars_per_line): + lines.append(word[i : i + chars_per_line]) + current_line = "" + else: + current_line = word + + # Add remaining text + if current_line: + lines.append(current_line) + + return lines + + +def truncate_text(text, max_width, suffix="..."): + """ + Truncate text to fit within maximum width, adding suffix if truncated. + + Args: + text: String to truncate + max_width: Maximum width in pixels + suffix: String to append if truncated (default: "...") + + Returns: + str: Truncated text + + Example: + >>> short = truncate_text("VERY LONG TEXT", 50) + >>> ua_text(oled, short, 0, 0) + """ + if not text: + return "" + + text_w = font5x7.text_width(text) + if text_w <= max_width: + return text + + suffix_w = font5x7.text_width(suffix) + available_width = max_width - suffix_w + + if available_width <= 0: + return suffix[: max_width // (font5x7.FONT_WIDTH + 1)] + + # Binary search for optimal length + left, right = 0, len(text) + result = "" + + while left <= right: + mid = (left + right) // 2 + test_text = text[:mid] + test_width = font5x7.text_width(test_text) + + if test_width <= available_width: + result = test_text + left = mid + 1 + else: + right = mid - 1 + + return result + suffix + + +def get_display_info(display): + """ + Get information about the display object. + + Attempts to detect display type and capabilities. + + Args: + display: Display object + + Returns: + dict: Display information + + Example: + >>> info = get_display_info(oled) + >>> print(f"Display: {info['type']}, Size: {info['width']}x{info['height']}") + """ + info = { + "type": "unknown", + "width": 128, # Default assumption + "height": 64, # Default assumption + "has_pixel": hasattr(display, "pixel"), + "has_fill_rect": hasattr(display, "fill_rect"), + "has_show": hasattr(display, "show"), + "has_fill": hasattr(display, "fill"), + } + + # Try to detect display type from class name + class_name = type(display).__name__ + info["class"] = class_name + + if "SSD1306" in class_name: + info["type"] = "SSD1306" + elif "ST7789" in class_name: + info["type"] = "ST7789" + info["width"] = 240 + info["height"] = 240 + elif "ILI9341" in class_name: + info["type"] = "ILI9341" + info["width"] = 320 + info["height"] = 240 + elif "GC9A01" in class_name: + info["type"] = "GC9A01" + info["width"] = 240 + info["height"] = 240 + + # Try to get actual dimensions + if hasattr(display, "width"): + info["width"] = display.width + if hasattr(display, "height"): + info["height"] = display.height + + return info + + +def center_position(text, display_width=128, display_height=64, scale=1): + """ + Calculate position to center text both horizontally and vertically. + + Args: + text: String to center + display_width: Display width in pixels (default: 128) + display_height: Display height in pixels (default: 64) + scale: Text scale factor (default: 1) + + Returns: + tuple: (x, y) coordinates for centered text + + Example: + >>> x, y = center_position("ПРИВІТ", 128, 64) + >>> ua_text(oled, "ПРИВІТ", x, y) + """ + if scale == 1: + text_w, text_h = measure_text(text) + else: + text_w, text_h = measure_text_scaled(text, scale) + + x = (display_width - text_w) // 2 + y = (display_height - text_h) // 2 + + # Ensure coordinates are not negative + x = max(0, x) + y = max(0, y) + + return (x, y) + + +def supports_ukrainian(text): + """ + Check if text contains Ukrainian characters. + + Args: + text: String to check + + Returns: + bool: True if text contains Ukrainian characters + + Example: + >>> if supports_ukrainian("ПРИВІТ"): + >>> print("Ukrainian text detected") + """ + ukrainian_chars = set("АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя") + return any(char in ukrainian_chars for char in text) + + +def validate_text(text): + """ + Validate if all characters in text are supported by the font. + + Args: + text: String to validate + + Returns: + tuple: (is_valid, unsupported_chars) + + Example: + >>> valid, unsupported = validate_text("ПРИВІТ 123") + >>> if not valid: + >>> print(f"Unsupported characters: {unsupported}") + """ + unsupported = [] + for char in text: + if font5x7.get_char_bitmap(char) is None: + if char not in unsupported: + unsupported.append(char) + + return (len(unsupported) == 0, unsupported) diff --git a/micropython/bfu_ua_display/manifest.py b/micropython/bfu_ua_display/manifest.py new file mode 100644 index 000000000..3855b6c5d --- /dev/null +++ b/micropython/bfu_ua_display/manifest.py @@ -0,0 +1,6 @@ +metadata( + description="Ukrainian text rendering library for MicroPython displays", + version="0.1.0", +) + +package("bfu_ua_display") From 99b5b841fe6a0fa01bb6e8b8cd7da40f09529522 Mon Sep 17 00:00:00 2001 From: Oleksandr Polishchuk Date: Fri, 15 May 2026 11:19:26 +0100 Subject: [PATCH 2/3] python-ecosys/bfu_ua_display: Move package to python-ecosys. Signed-off-by: Oleksandr Polishchuk --- {micropython => python-ecosys}/bfu_ua_display/LICENSE | 0 {micropython => python-ecosys}/bfu_ua_display/README.md | 0 .../bfu_ua_display/bfu_ua_display/__init__.py | 0 .../bfu_ua_display/bfu_ua_display/font5x7.py | 0 .../bfu_ua_display/bfu_ua_display/text_engine.py | 0 .../bfu_ua_display/bfu_ua_display/utils.py | 0 {micropython => python-ecosys}/bfu_ua_display/manifest.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {micropython => python-ecosys}/bfu_ua_display/LICENSE (100%) rename {micropython => python-ecosys}/bfu_ua_display/README.md (100%) rename {micropython => python-ecosys}/bfu_ua_display/bfu_ua_display/__init__.py (100%) rename {micropython => python-ecosys}/bfu_ua_display/bfu_ua_display/font5x7.py (100%) rename {micropython => python-ecosys}/bfu_ua_display/bfu_ua_display/text_engine.py (100%) rename {micropython => python-ecosys}/bfu_ua_display/bfu_ua_display/utils.py (100%) rename {micropython => python-ecosys}/bfu_ua_display/manifest.py (100%) diff --git a/micropython/bfu_ua_display/LICENSE b/python-ecosys/bfu_ua_display/LICENSE similarity index 100% rename from micropython/bfu_ua_display/LICENSE rename to python-ecosys/bfu_ua_display/LICENSE diff --git a/micropython/bfu_ua_display/README.md b/python-ecosys/bfu_ua_display/README.md similarity index 100% rename from micropython/bfu_ua_display/README.md rename to python-ecosys/bfu_ua_display/README.md diff --git a/micropython/bfu_ua_display/bfu_ua_display/__init__.py b/python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py similarity index 100% rename from micropython/bfu_ua_display/bfu_ua_display/__init__.py rename to python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py diff --git a/micropython/bfu_ua_display/bfu_ua_display/font5x7.py b/python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py similarity index 100% rename from micropython/bfu_ua_display/bfu_ua_display/font5x7.py rename to python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py diff --git a/micropython/bfu_ua_display/bfu_ua_display/text_engine.py b/python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py similarity index 100% rename from micropython/bfu_ua_display/bfu_ua_display/text_engine.py rename to python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py diff --git a/micropython/bfu_ua_display/bfu_ua_display/utils.py b/python-ecosys/bfu_ua_display/bfu_ua_display/utils.py similarity index 100% rename from micropython/bfu_ua_display/bfu_ua_display/utils.py rename to python-ecosys/bfu_ua_display/bfu_ua_display/utils.py diff --git a/micropython/bfu_ua_display/manifest.py b/python-ecosys/bfu_ua_display/manifest.py similarity index 100% rename from micropython/bfu_ua_display/manifest.py rename to python-ecosys/bfu_ua_display/manifest.py From 476c3c229d1b48db363b64498584636ee72b56ba Mon Sep 17 00:00:00 2001 From: Oleksandr Polishchuk Date: Thu, 21 May 2026 23:56:17 +0100 Subject: [PATCH 3/3] python-ecosys/bfu_ua_display: Improve Ukrainian glyph rendering. Signed-off-by: Oleksandr Polishchuk --- .ruff.toml | 6 + python-ecosys/bfu_ua_display/README.md | 473 +++++++++++++++--- .../bfu_ua_display/bfu_ua_display/__init__.py | 22 +- .../bfu_ua_display/bfu_ua_display/font5x7.py | 353 ++++++------- .../bfu_ua_display/text_engine.py | 140 +++--- .../bfu_ua_display/bfu_ua_display/utils.py | 161 +++--- python-ecosys/bfu_ua_display/manifest.py | 4 +- python-ecosys/bfu_ua_display/package.json | 10 + 8 files changed, 782 insertions(+), 387 deletions(-) create mode 100644 .ruff.toml create mode 100644 python-ecosys/bfu_ua_display/package.json diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..102f17a55 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,6 @@ +[lint] +ignore = [ + "RUF001", + "RUF002", + "RUF003", +] \ No newline at end of file diff --git a/python-ecosys/bfu_ua_display/README.md b/python-ecosys/bfu_ua_display/README.md index 997a77540..beeb3f850 100644 --- a/python-ecosys/bfu_ua_display/README.md +++ b/python-ecosys/bfu_ua_display/README.md @@ -1,33 +1,208 @@ # BFU UA Display -Ukrainian text rendering library for MicroPython displays. +**Professional Ukrainian Text Rendering Library for MicroPython** -## Description +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![MicroPython](https://img.shields.io/badge/MicroPython-1.19+-blue.svg)](https://micropython.org/) +[![ESP32](https://img.shields.io/badge/Platform-ESP32-green.svg)](https://www.espressif.com/en/products/socs/esp32) -A lightweight library for rendering Ukrainian text on displays commonly used with ESP32 and MicroPython projects. Standard MicroPython display libraries do not include Ukrainian characters (А, Б, В, Г, Ґ, Д, Е, Є, Ж, З, И, І, Ї, Й, etc.), making it impossible to display Ukrainian text properly. This library solves that problem with a custom 5x7 bitmap font containing all 33 Ukrainian letters (uppercase and lowercase). +A lightweight, optimized library for rendering Ukrainian text on displays commonly used with ESP32 and MicroPython projects. Solves the problem of missing Ukrainian character support in standard display libraries. -## Features +## 🌟 Features -- **Full Ukrainian Alphabet Support** - All 33 Ukrainian letters (uppercase and lowercase) -- **Lightweight** - Optimized for ESP32 memory constraints (~2-3 KB) -- **Display Agnostic** - Works with any display supporting `pixel()` method -- **Simple API** - Three main functions for text rendering -- **5x7 Bitmap Font** - Compact and readable on small displays +- ✅ **Full Ukrainian Alphabet Support** - All 33 Ukrainian letters (uppercase and lowercase) +- ✅ **Lightweight & Optimized** - Designed for ESP32 memory constraints +- ✅ **Easy to Use** - Simple, intuitive API +- ✅ **Multiple Text Functions** - Basic, centered, scaled, and right-aligned text +- ✅ **Display Agnostic** - Works with any display supporting `pixel()` method +- ✅ **5x7 Bitmap Font** - Compact and readable on small displays +- ✅ **Production Ready** - Clean, modular, professional code +- ✅ **Extensible Architecture** - Easy to add new display drivers and fonts -## Installation +## 📦 Installation + +### ⭐ Official Method: Using mip (Recommended) + +**Note:** This package is prepared for submission to the official MicroPython package index. Once accepted, you will be able to install it directly using: ```python import mip mip.install("bfu_ua_display") ``` -Or using mpremote: +Or using mpremote from your PC: ```bash mpremote connect COM3 mip install bfu_ua_display ``` -## Quick Start +**Status:** Pending submission to micropython-lib. Until then, use one of the alternative methods below. + +--- + +### Alternative Method 1: Using mpremote (Most Reliable) + +**mpremote** is the official MicroPython tool that works reliably across all firmware versions and avoids network/TLS issues. + +#### Step 1: Install mpremote on your PC + +```bash +pip install mpremote +``` + +#### Step 2: Download the library + +```bash +git clone https://github.com/BrainFromUkraine/bfu_ua_display.git +cd bfu_ua_display +``` + +Or download ZIP from GitHub and extract it. + +#### Step 3: Install to ESP32 + +**Windows:** +```bash +mpremote connect COM3 fs mkdir :/lib/bfu_ua_display +mpremote connect COM3 fs cp bfu_ua_display/__init__.py :/lib/bfu_ua_display/__init__.py +mpremote connect COM3 fs cp bfu_ua_display/font5x7.py :/lib/bfu_ua_display/font5x7.py +mpremote connect COM3 fs cp bfu_ua_display/text_engine.py :/lib/bfu_ua_display/text_engine.py +mpremote connect COM3 fs cp bfu_ua_display/utils.py :/lib/bfu_ua_display/utils.py +``` + +**Linux/Mac:** +```bash +mpremote connect /dev/ttyUSB0 fs mkdir :/lib/bfu_ua_display +mpremote connect /dev/ttyUSB0 fs cp bfu_ua_display/__init__.py :/lib/bfu_ua_display/__init__.py +mpremote connect /dev/ttyUSB0 fs cp bfu_ua_display/font5x7.py :/lib/bfu_ua_display/font5x7.py +mpremote connect /dev/ttyUSB0 fs cp bfu_ua_display/text_engine.py :/lib/bfu_ua_display/text_engine.py +mpremote connect /dev/ttyUSB0 fs cp bfu_ua_display/utils.py :/lib/bfu_ua_display/utils.py +``` + +**Note:** Replace `COM3` or `/dev/ttyUSB0` with your actual port. + +### Alternative Method 1: Using Thonny IDE (Easiest for Beginners) + +1. Download the library from GitHub: + - Go to https://github.com/BrainFromUkraine/bfu_ua_display + - Click "Code" → "Download ZIP" + - Extract the ZIP file + +2. Install using Thonny: + - Open Thonny IDE + - Connect your ESP32 + - View → Files + - On your ESP32, create a `lib` folder if it doesn't exist + - Drag the `bfu_ua_display` folder into the `lib` folder + +### Alternative Method 2: GitHub mip Installation (Experimental - May Fail) + +**⚠️ WARNING:** This method is **experimental** and **frequently fails** on many ESP32 MicroPython firmware versions due to HTTPS/TLS/DNS limitations. **Use mpremote or Thonny instead** for reliable installation. + +If you want to try on-device installation (not recommended): + +```python +import os +import mip + +# Create directories +try: + os.mkdir("/lib") +except: + pass + +try: + os.mkdir("/lib/bfu_ua_display") +except: + pass + +# Attempt to install files (may fail with OSError: -202) +files = [ + "__init__.py", + "font5x7.py", + "text_engine.py", + "utils.py" +] + +base = "https://raw.githubusercontent.com/BrainFromUkraine/bfu_ua_display/main/bfu_ua_display/" + +for file in files: + print(f"Installing {file}...") + try: + mip.install(base + file, target="/lib/bfu_ua_display") + except OSError as e: + print(f"Failed: {e}") + print("Use mpremote or Thonny installation instead!") + break + +print("✓ BFU UA Display installed successfully!") +``` + +**Common Failure:** `OSError: -202` when downloading from raw.githubusercontent.com means the ESP32 MicroPython firmware cannot complete HTTPS/TLS/DNS requests. This is a **firmware limitation**, not a library issue. **Use mpremote or Thonny instead.** + +### Alternative Method 3: GitHub Package mip (Experimental) + +```python +import mip +mip.install("github:BrainFromUkraine/bfu_ua_display") +``` + +**⚠️ Warning:** This method is experimental and may fail on some ESP32 firmware versions due to GitHub HTTPS/chunked transfer limitations. Use the mpremote method for reliable installation. + +### Verify Installation + +Test that the library is installed correctly: + +```python +# Test import +from bfu_ua_display import ua_text, ua_text_center, ua_text_scaled +print("✓ BFU UA Display imported successfully!") + +# Check version +import bfu_ua_display +print(f"Version: {bfu_ua_display.__version__}") +``` + +### Troubleshooting + +**Problem:** `ImportError: no module named 'bfu_ua_display'` + +**Solution:** +1. Verify the library is in `/lib/bfu_ua_display/` on your ESP32 +2. Check that the folder contains `__init__.py` +3. List files using mpremote: `mpremote connect COM3 fs ls :/lib/bfu_ua_display` +4. Try resetting your ESP32: `import machine; machine.reset()` + +**Problem:** `OSError: -202` when using mip + +**Solution:** This is a network/DNS error on ESP32. The on-device mip installation is failing due to TLS or network issues. Use the **mpremote method** (recommended) or **Thonny IDE method** instead. + +**Problem:** `ValueError: Unsupported Transfer-Encoding: chunked` + +**Solution:** This error occurs with some MicroPython firmware versions when using on-device mip. Use the **mpremote method** (recommended) or **Thonny IDE method** instead. + +**Problem:** Nested folders like `bfu_ua_display/font5x7.py/font5x7.py` + +**Solution:** This was caused by an older installation method. Clean up and reinstall: + +```bash +# Remove incorrect installation +mpremote connect COM3 fs rm -r :/lib/bfu_ua_display + +# Reinstall using mpremote method above +``` + +**Problem:** How do I find my COM port? + +**Solution:** +- **Windows:** Check Device Manager → Ports (COM & LPT) → Look for "USB-SERIAL CH340" or similar +- **Linux:** Run `ls /dev/ttyUSB*` or `ls /dev/ttyACM*` +- **Mac:** Run `ls /dev/tty.usb*` or `ls /dev/cu.usb*` +- **Thonny:** Bottom-right corner shows the port when connected + +## 🚀 Quick Start + +### Basic Example ```python from machine import I2C, Pin @@ -42,13 +217,24 @@ oled = SSD1306_I2C(128, 64, i2c) ua_text(oled, "ПРИВІТ УКРАЇНО!", 0, 0) ua_text_center(oled, "BFU Electronics", 28) -# Update display oled.show() ``` -## API Reference +### Scaled Text + +```python +from bfu_ua_display import ua_text_scaled + +# Draw 2x scaled text +ua_text_scaled(oled, "ПРИВІТ", 10, 10, scale=2) +oled.show() +``` + +## 📖 API Reference -### ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False) +### Core Functions + +#### `ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False)` Render text at specified position with Ukrainian character support. @@ -63,14 +249,9 @@ Render text at specified position with Ukrainian character support. **Returns:** Total width of rendered text in pixels -**Example:** -```python -ua_text(oled, "Температура: 25°C", 0, 0) -``` - --- -### ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, display_width=128) +#### `ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, display_width=128)` Render text centered horizontally on the display. @@ -85,14 +266,9 @@ Render text centered horizontally on the display. **Returns:** X coordinate where text was rendered -**Example:** -```python -ua_text_center(oled, "УКРАЇНА", 28) -``` - --- -### ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=False) +#### `ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=False)` Render text with scaling (2x, 3x, etc.). @@ -108,75 +284,234 @@ Render text with scaling (2x, 3x, etc.). **Returns:** Total width of rendered text in pixels -**Example:** -```python -ua_text_scaled(oled, "ПРИВІТ", 0, 0, scale=2) -``` +--- -## Supported Characters +### Utility Functions -- **Ukrainian Alphabet**: А Б В Г Ґ Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ь Ю Я (uppercase and lowercase) -- **English Alphabet**: A-Z, a-z -- **Numbers**: 0-9 -- **Symbols**: Common punctuation and special characters +The library also includes utility functions in `bfu_ua_display.utils`: -## Display Requirements +- `measure_text(text)` - Get text dimensions +- `wrap_text(text, max_width)` - Wrap text to fit width +- `truncate_text(text, max_width)` - Truncate with ellipsis +- `center_position(text, display_width, display_height)` - Calculate center position +- `supports_ukrainian(text)` - Check if text contains Ukrainian characters +- `validate_text(text)` - Validate character support -The library works with any display object that implements: +## 🎯 Supported Characters -- `pixel(x, y, color)` - Set individual pixel (required) -- `show()` - Update display (optional, for buffered displays) +### Ukrainian Alphabet + +**Uppercase:** А Б В Г Ґ Д Е Є Ж З И І Ї Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ь Ю Я + +**Lowercase:** а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ь ю я + +### Additional Characters + +- English alphabet (A-Z, a-z) +- Numbers (0-9) +- Common symbols (!, @, #, $, %, etc.) +- Punctuation marks + +## 🖥️ Supported Displays + +Currently tested and working with: + +- **SSD1306** - OLED 128x64, 128x32 (I2C/SPI) + +The library is designed to work with any display that supports: +- `pixel(x, y, color)` - Set individual pixel - `fill_rect(x, y, width, height, color)` - Fill rectangle (optional, for optimization) +- `show()` - Update display (optional, for buffered displays) + +## 🔬 Tested on Real Hardware + +All Ukrainian glyphs have been **manually refined and tested on real SSD1306 OLED hardware** to ensure optimal readability and visual quality. + +### Hardware Testing Setup -## Compatibility +- **Display:** SSD1306 128x64 OLED (I2C) +- **Controller:** ESP32 DevKit +- **Testing:** Full Ukrainian alphabet + scaled rendering + variable-width glyphs -**Tested with:** -- ESP32 with MicroPython v1.19+ -- SSD1306 OLED displays (128x64, 128x32) via I2C/SPI +### Gallery -**Compatible with:** -- Any MicroPython-compatible board -- Any display supporting the `pixel()` method +
-## Examples +![Ukrainian Alphabet on SSD1306](assets/ukrainian-alphabet-oled.jpg) +*Complete Ukrainian alphabet displayed on real SSD1306 OLED hardware* -### Multi-line Text +
+ +**Key Features Verified:** +- ✅ All 33 Ukrainian letters render correctly +- ✅ Variable-width glyph rendering works properly +- ✅ Scaled text (2x, 3x) displays clearly +- ✅ Mixed Ukrainian/English text alignment +- ✅ Proper spacing and kerning +- ✅ Readable at standard 5x7 pixel size + +> **Note:** Glyphs were iteratively refined directly on hardware, not just simulated. This ensures real-world readability on actual OLED displays. + +## 🎨 Demo & Screenshots + +### Full Alphabet Display + +The library supports the complete Ukrainian alphabet with carefully designed glyphs: ```python -oled.fill(0) -ua_text(oled, "Рядок 1", 0, 0) -ua_text(oled, "Рядок 2", 0, 10) -ua_text(oled, "Рядок 3", 0, 20) +# Display full Ukrainian alphabet +from bfu_ua_display import ua_text + +ua_text(oled, "АБВГҐДЕЄЖЗИІЇЙКЛМН", 0, 0) +ua_text(oled, "ОПРСТУФХЦЧШЩЬЮЯ", 0, 10) +ua_text(oled, "абвгґдеєжзиіїйклмн", 0, 20) +ua_text(oled, "опрстуфхцчшщьюя", 0, 30) oled.show() ``` -### Scaled Text +### Scaled Text Example ```python -oled.fill(0) -ua_text_scaled(oled, "ВЕЛИКИЙ", 0, 0, scale=2) +# Large 2x scaled Ukrainian text +ua_text_scaled(oled, "УКРАЇНА", 10, 10, scale=2) oled.show() ``` -### Centered Text +## 📁 Project Structure -```python -oled.fill(0) -ua_text_center(oled, "УКРАЇНА", 10) -ua_text_center(oled, "2026", 28) -oled.show() ``` +bfu_ua_display/ +│ +├── bfu_ua_display/ +│ ├── __init__.py # Package initialization +│ ├── font5x7.py # 5x7 bitmap font with Ukrainian characters +│ ├── text_engine.py # Core rendering functions +│ └── utils.py # Utility functions +│ +├── examples/ +│ └── oled_i2c_example.py # Complete usage examples +│ +├── README.md # This file +├── LICENSE # MIT License +├── package.json # MicroPython package metadata +└── .gitignore # Git ignore rules +``` + +## 💡 Examples + +See the `examples/` folder for complete working examples: + +- **oled_i2c_example.py** - Comprehensive examples including: + - Basic text rendering + - Centered text + - Scaled text + - Full alphabet display + - Mixed Ukrainian/English content + - Scrolling text animation + - Multi-line text + - Background clearing + +## 🛠️ Hardware Requirements + +- **ESP32** board (or compatible MicroPython device) +- **Display** (SSD1306 OLED recommended for testing) +- **I2C or SPI connection** (depending on display) + +### Typical Wiring (SSD1306 I2C) + +``` +ESP32 SSD1306 +----- ------- +GPIO 22 ---> SCL +GPIO 21 ---> SDA +3.3V ---> VCC +GND ---> GND +``` + +## 🔮 Future Roadmap + +### Version 0.2.0 +- ST7789 TFT display support +- GC9A01 round display support +- Additional font sizes (8x8, 10x14) + +### Version 0.3.0 +- ILI9341 display support +- Font scaling improvements +- Word wrapping helper + +### Version 1.0.0 +- UI widgets (buttons, progress bars) +- Menu system +- Notification system +- Animation helpers + +## 🤝 Contributing + +Contributions are welcome! This is an open-source project aimed at improving Ukrainian language support in MicroPython projects. -## License +### How to Contribute -MIT License +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly on real hardware +5. Submit a pull request -## Documentation +### Areas for Contribution -For complete documentation, examples, and troubleshooting, visit: +- Additional display driver support +- New font sizes and styles +- Performance optimizations +- Documentation improvements +- Example projects +- Bug fixes -**https://github.com/BrainFromUkraine/bfu_ua_display** +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 👥 Authors + +**BFU Electronics** + +- GitHub: [@BFU-Electronics](https://github.com/BFU-Electronics) + +## 🙏 Acknowledgments + +- MicroPython community +- Ukrainian maker community +- All contributors and testers + +## 📞 Support + +- **Issues:** [GitHub Issues](https://github.com/BFU-Electronics/bfu_ua_display/issues) +- **Discussions:** [GitHub Discussions](https://github.com/BFU-Electronics/bfu_ua_display/discussions) + +## 🎓 Community & Tutorials + +**🇺🇦 Ukrainian Educational Content:** + +This library is actively used in educational YouTube lessons and tutorials about ESP32, MicroPython, and embedded systems. + +**YouTube Channel:** [Brain From Ukraine](https://www.youtube.com/@BrainFromUkraine) + +Watch tutorials covering: +- Getting started with BFU UA Display +- ESP32 and MicroPython projects +- Display programming +- Ukrainian language support in embedded systems +- Real-world IoT projects + +## Links + +- [MicroPython Official Site](https://micropython.org/) +- [ESP32 Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/) +- [SSD1306 Driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py) +- [Brain From Ukraine YouTube](https://www.youtube.com/@BrainFromUkraine) + +--- -## Author +**Made with ❤️ in Ukraine 🇺🇦** -BFU Electronics +*Зроблено з любов'ю в Україні* diff --git a/python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py b/python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py index e2d67f190..5736152fa 100644 --- a/python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py +++ b/python-ecosys/bfu_ua_display/bfu_ua_display/__init__.py @@ -1,20 +1,24 @@ """ BFU UA Display - Ukrainian Text Rendering Library for MicroPython ================================================================== +Бібліотека для відображення українського тексту на дисплеях MicroPython A professional, lightweight library for rendering Ukrainian text on displays commonly used with ESP32 and MicroPython projects. -Features: -- Full Ukrainian alphabet support (33 letters) -- Optimized for ESP32 memory constraints -- Clean, modular architecture -- Easy to use API -- Extensible for multiple display types +Професійна, легка бібліотека для відображення українського тексту на дисплеях, +які зазвичай використовуються з ESP32 та MicroPython проєктами. -Author: BFU Electronics -License: MIT -Version: 0.1.0 +Features / Можливості: +- Full Ukrainian alphabet support / Повна підтримка української абетки +- Optimized for ESP32 memory constraints / Оптимізовано для обмежень пам'яті ESP32 +- Clean, modular architecture / Чиста, модульна архітектура +- Easy to use API / Простий у використанні API +- Extensible for multiple display types / Розширюваний для різних типів дисплеїв + +Author / Автор: BFU Electronics +License / Ліцензія: MIT +Version / Версія: 0.1.0 """ from .text_engine import ua_text, ua_text_center, ua_text_scaled diff --git a/python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py b/python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py index 7ca1851f7..712b51a6a 100644 --- a/python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py +++ b/python-ecosys/bfu_ua_display/bfu_ua_display/font5x7.py @@ -1,4 +1,3 @@ -# ruff: noqa: RUF001, RUF003 """ 5x7 Bitmap Font with Ukrainian Character Support ================================================= @@ -11,106 +10,106 @@ # Font dimensions FONT_WIDTH = 5 -FONT_HEIGHT = 7 +FONT_HEIGHT = 8 # Basic ASCII characters (32-126) # Each character is represented as 5 bytes (columns), 7 bits per byte (rows) _ASCII_FONT = { - " ": bytearray([0x00, 0x00, 0x00, 0x00, 0x00]), - "!": bytearray([0x00, 0x00, 0x5F, 0x00, 0x00]), + ' ': bytearray([0x00, 0x00, 0x00, 0x00, 0x00]), + '!': bytearray([0x00, 0x00, 0x5F, 0x00, 0x00]), '"': bytearray([0x00, 0x07, 0x00, 0x07, 0x00]), - "#": bytearray([0x14, 0x7F, 0x14, 0x7F, 0x14]), - "$": bytearray([0x24, 0x2A, 0x7F, 0x2A, 0x12]), - "%": bytearray([0x23, 0x13, 0x08, 0x64, 0x62]), - "&": bytearray([0x36, 0x49, 0x55, 0x22, 0x50]), + '#': bytearray([0x14, 0x7F, 0x14, 0x7F, 0x14]), + '$': bytearray([0x24, 0x2A, 0x7F, 0x2A, 0x12]), + '%': bytearray([0x23, 0x13, 0x08, 0x64, 0x62]), + '&': bytearray([0x36, 0x49, 0x55, 0x22, 0x50]), "'": bytearray([0x00, 0x05, 0x03, 0x00, 0x00]), - "(": bytearray([0x00, 0x1C, 0x22, 0x41, 0x00]), - ")": bytearray([0x00, 0x41, 0x22, 0x1C, 0x00]), - "*": bytearray([0x14, 0x08, 0x3E, 0x08, 0x14]), - "+": bytearray([0x08, 0x08, 0x3E, 0x08, 0x08]), - ",": bytearray([0x00, 0x50, 0x30, 0x00, 0x00]), - "-": bytearray([0x08, 0x08, 0x08, 0x08, 0x08]), - ".": bytearray([0x00, 0x60, 0x60, 0x00, 0x00]), - "/": bytearray([0x20, 0x10, 0x08, 0x04, 0x02]), - "0": bytearray([0x3E, 0x51, 0x49, 0x45, 0x3E]), - "1": bytearray([0x00, 0x42, 0x7F, 0x40, 0x00]), - "2": bytearray([0x42, 0x61, 0x51, 0x49, 0x46]), - "3": bytearray([0x21, 0x41, 0x45, 0x4B, 0x31]), - "4": bytearray([0x18, 0x14, 0x12, 0x7F, 0x10]), - "5": bytearray([0x27, 0x45, 0x45, 0x45, 0x39]), - "6": bytearray([0x3C, 0x4A, 0x49, 0x49, 0x30]), - "7": bytearray([0x01, 0x71, 0x09, 0x05, 0x03]), - "8": bytearray([0x36, 0x49, 0x49, 0x49, 0x36]), - "9": bytearray([0x06, 0x49, 0x49, 0x29, 0x1E]), - ":": bytearray([0x00, 0x36, 0x36, 0x00, 0x00]), - ";": bytearray([0x00, 0x56, 0x36, 0x00, 0x00]), - "<": bytearray([0x08, 0x14, 0x22, 0x41, 0x00]), - "=": bytearray([0x14, 0x14, 0x14, 0x14, 0x14]), - ">": bytearray([0x00, 0x41, 0x22, 0x14, 0x08]), - "?": bytearray([0x02, 0x01, 0x51, 0x09, 0x06]), - "@": bytearray([0x32, 0x49, 0x79, 0x41, 0x3E]), - "A": bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), - "B": bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), - "C": bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), - "D": bytearray([0x7F, 0x41, 0x41, 0x22, 0x1C]), - "E": bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), - "F": bytearray([0x7F, 0x09, 0x09, 0x09, 0x01]), - "G": bytearray([0x3E, 0x41, 0x49, 0x49, 0x7A]), - "H": bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), - "I": bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), - "J": bytearray([0x20, 0x40, 0x41, 0x3F, 0x01]), - "K": bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), - "L": bytearray([0x7F, 0x40, 0x40, 0x40, 0x40]), - "M": bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), - "N": bytearray([0x7F, 0x04, 0x08, 0x10, 0x7F]), - "O": bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), - "P": bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), - "Q": bytearray([0x3E, 0x41, 0x51, 0x21, 0x5E]), - "R": bytearray([0x7F, 0x09, 0x19, 0x29, 0x46]), - "S": bytearray([0x46, 0x49, 0x49, 0x49, 0x31]), - "T": bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), - "U": bytearray([0x3F, 0x40, 0x40, 0x40, 0x3F]), - "V": bytearray([0x1F, 0x20, 0x40, 0x20, 0x1F]), - "W": bytearray([0x3F, 0x40, 0x38, 0x40, 0x3F]), - "X": bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), - "Y": bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), - "Z": bytearray([0x61, 0x51, 0x49, 0x45, 0x43]), - "[": bytearray([0x00, 0x7F, 0x41, 0x41, 0x00]), - "\\": bytearray([0x02, 0x04, 0x08, 0x10, 0x20]), - "]": bytearray([0x00, 0x41, 0x41, 0x7F, 0x00]), - "^": bytearray([0x04, 0x02, 0x01, 0x02, 0x04]), - "_": bytearray([0x40, 0x40, 0x40, 0x40, 0x40]), - "`": bytearray([0x00, 0x01, 0x02, 0x04, 0x00]), - "a": bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), - "b": bytearray([0x7F, 0x48, 0x44, 0x44, 0x38]), - "c": bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), - "d": bytearray([0x38, 0x44, 0x44, 0x48, 0x7F]), - "e": bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), - "f": bytearray([0x08, 0x7E, 0x09, 0x01, 0x02]), - "g": bytearray([0x0C, 0x52, 0x52, 0x52, 0x3E]), - "h": bytearray([0x7F, 0x08, 0x04, 0x04, 0x78]), - "i": bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), - "j": bytearray([0x20, 0x40, 0x44, 0x3D, 0x00]), - "k": bytearray([0x7F, 0x10, 0x28, 0x44, 0x00]), - "l": bytearray([0x00, 0x41, 0x7F, 0x40, 0x00]), - "m": bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), - "n": bytearray([0x7C, 0x08, 0x04, 0x04, 0x78]), - "o": bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), - "p": bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), - "q": bytearray([0x08, 0x14, 0x14, 0x18, 0x7C]), - "r": bytearray([0x7C, 0x08, 0x04, 0x04, 0x08]), - "s": bytearray([0x48, 0x54, 0x54, 0x54, 0x20]), - "t": bytearray([0x04, 0x3F, 0x44, 0x40, 0x20]), - "u": bytearray([0x3C, 0x40, 0x40, 0x20, 0x7C]), - "v": bytearray([0x1C, 0x20, 0x40, 0x20, 0x1C]), - "w": bytearray([0x3C, 0x40, 0x30, 0x40, 0x3C]), - "x": bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), - "y": bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), - "z": bytearray([0x44, 0x64, 0x54, 0x4C, 0x44]), - "{": bytearray([0x00, 0x08, 0x36, 0x41, 0x00]), - "|": bytearray([0x00, 0x00, 0x7F, 0x00, 0x00]), - "}": bytearray([0x00, 0x41, 0x36, 0x08, 0x00]), - "~": bytearray([0x10, 0x08, 0x08, 0x10, 0x08]), + '(': bytearray([0x00, 0x1C, 0x22, 0x41, 0x00]), + ')': bytearray([0x00, 0x41, 0x22, 0x1C, 0x00]), + '*': bytearray([0x14, 0x08, 0x3E, 0x08, 0x14]), + '+': bytearray([0x08, 0x08, 0x3E, 0x08, 0x08]), + ',': bytearray([0x00, 0x50, 0x30, 0x00, 0x00]), + '-': bytearray([0x08, 0x08, 0x08, 0x08, 0x08]), + '.': bytearray([0x00, 0x60, 0x60, 0x00, 0x00]), + '/': bytearray([0x20, 0x10, 0x08, 0x04, 0x02]), + '0': bytearray([0x3E, 0x51, 0x49, 0x45, 0x3E]), + '1': bytearray([0x00, 0x42, 0x7F, 0x40, 0x00]), + '2': bytearray([0x42, 0x61, 0x51, 0x49, 0x46]), + '3': bytearray([0x21, 0x41, 0x45, 0x4B, 0x31]), + '4': bytearray([0x18, 0x14, 0x12, 0x7F, 0x10]), + '5': bytearray([0x27, 0x45, 0x45, 0x45, 0x39]), + '6': bytearray([0x3C, 0x4A, 0x49, 0x49, 0x30]), + '7': bytearray([0x01, 0x71, 0x09, 0x05, 0x03]), + '8': bytearray([0x36, 0x49, 0x49, 0x49, 0x36]), + '9': bytearray([0x06, 0x49, 0x49, 0x29, 0x1E]), + ':': bytearray([0x00, 0x36, 0x36, 0x00, 0x00]), + ';': bytearray([0x00, 0x56, 0x36, 0x00, 0x00]), + '<': bytearray([0x08, 0x14, 0x22, 0x41, 0x00]), + '=': bytearray([0x14, 0x14, 0x14, 0x14, 0x14]), + '>': bytearray([0x00, 0x41, 0x22, 0x14, 0x08]), + '?': bytearray([0x02, 0x01, 0x51, 0x09, 0x06]), + '@': bytearray([0x32, 0x49, 0x79, 0x41, 0x3E]), + 'A': bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), + 'B': bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), + 'C': bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), + 'D': bytearray([0x7F, 0x41, 0x41, 0x22, 0x1C]), + 'E': bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), + 'F': bytearray([0x7F, 0x09, 0x09, 0x09, 0x01]), + 'G': bytearray([0x3E, 0x41, 0x49, 0x49, 0x7A]), + 'H': bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), + 'I': bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), + 'J': bytearray([0x20, 0x40, 0x41, 0x3F, 0x01]), + 'K': bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), + 'L': bytearray([0x7F, 0x40, 0x40, 0x40, 0x40]), + 'M': bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), + 'N': bytearray([0x7F, 0x04, 0x08, 0x10, 0x7F]), + 'O': bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), + 'P': bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), + 'Q': bytearray([0x3E, 0x41, 0x51, 0x21, 0x5E]), + 'R': bytearray([0x7F, 0x09, 0x19, 0x29, 0x46]), + 'S': bytearray([0x46, 0x49, 0x49, 0x49, 0x31]), + 'T': bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), + 'U': bytearray([0x3F, 0x40, 0x40, 0x40, 0x3F]), + 'V': bytearray([0x1F, 0x20, 0x40, 0x20, 0x1F]), + 'W': bytearray([0x3F, 0x40, 0x38, 0x40, 0x3F]), + 'X': bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), + 'Y': bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), + 'Z': bytearray([0x61, 0x51, 0x49, 0x45, 0x43]), + '[': bytearray([0x00, 0x7F, 0x41, 0x41, 0x00]), + '\\': bytearray([0x02, 0x04, 0x08, 0x10, 0x20]), + ']': bytearray([0x00, 0x41, 0x41, 0x7F, 0x00]), + '^': bytearray([0x04, 0x02, 0x01, 0x02, 0x04]), + '_': bytearray([0x40, 0x40, 0x40, 0x40, 0x40]), + '`': bytearray([0x00, 0x01, 0x02, 0x04, 0x00]), + 'a': bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), + 'b': bytearray([0x7F, 0x48, 0x44, 0x44, 0x38]), + 'c': bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), + 'd': bytearray([0x38, 0x44, 0x44, 0x48, 0x7F]), + 'e': bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), + 'f': bytearray([0x08, 0x7E, 0x09, 0x01, 0x02]), + 'g': bytearray([0x0C, 0x52, 0x52, 0x52, 0x3E]), + 'h': bytearray([0x7F, 0x08, 0x04, 0x04, 0x78]), + 'i': bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), + 'j': bytearray([0x20, 0x40, 0x44, 0x3D, 0x00]), + 'k': bytearray([0x7F, 0x10, 0x28, 0x44, 0x00]), + 'l': bytearray([0x00, 0x41, 0x7F, 0x40, 0x00]), + 'm': bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), + 'n': bytearray([0x7C, 0x08, 0x04, 0x04, 0x78]), + 'o': bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), + 'p': bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), + 'q': bytearray([0x08, 0x14, 0x14, 0x18, 0x7C]), + 'r': bytearray([0x7C, 0x08, 0x04, 0x04, 0x08]), + 's': bytearray([0x48, 0x54, 0x54, 0x54, 0x20]), + 't': bytearray([0x04, 0x3F, 0x44, 0x40, 0x20]), + 'u': bytearray([0x3C, 0x40, 0x40, 0x20, 0x7C]), + 'v': bytearray([0x1C, 0x20, 0x40, 0x20, 0x1C]), + 'w': bytearray([0x3C, 0x40, 0x30, 0x40, 0x3C]), + 'x': bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), + 'y': bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), + 'z': bytearray([0x44, 0x64, 0x54, 0x4C, 0x44]), + '{': bytearray([0x00, 0x08, 0x36, 0x41, 0x00]), + '|': bytearray([0x00, 0x00, 0x7F, 0x00, 0x00]), + '}': bytearray([0x00, 0x41, 0x36, 0x08, 0x00]), + '~': bytearray([0x10, 0x08, 0x08, 0x10, 0x08]), } # Ukrainian Cyrillic characters @@ -118,73 +117,74 @@ # Lowercase: а б в г ґ д е є ж з и і ї й к л м н о п р с т у ф х ц ч ш щ ь ю я _UKRAINIAN_FONT = { # Uppercase Ukrainian - "А": bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), # A (same as Latin A) - "Б": bytearray([0x7F, 0x49, 0x49, 0x49, 0x31]), # B - "В": bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), # V (same as Latin B) - "Г": bytearray([0x7F, 0x09, 0x09, 0x01, 0x01]), # H (Cyrillic) - "Ґ": bytearray([0x7F, 0x09, 0x09, 0x01, 0x03]), # G with upturn - "Д": bytearray([0x60, 0x3E, 0x21, 0x3E, 0x60]), # D - "Е": bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), # E (same as Latin E) - "Є": bytearray([0x3E, 0x49, 0x49, 0x49, 0x22]), # Ye - "Ж": bytearray([0x63, 0x14, 0x7F, 0x14, 0x63]), # Zh - "З": bytearray([0x22, 0x41, 0x49, 0x49, 0x36]), # Z - "И": bytearray([0x7F, 0x20, 0x10, 0x08, 0x7F]), # Y - "І": bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), # I (same as Latin I) - "Ї": bytearray([0x22, 0x41, 0x7F, 0x41, 0x22]), # Yi (I with dots) - "Й": bytearray([0x7E, 0x20, 0x11, 0x08, 0x7E]), # Y with breve - "К": bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), # K (same as Latin K) - "Л": bytearray([0x60, 0x30, 0x0F, 0x30, 0x60]), # L - "М": bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), # M (same as Latin M) - "Н": bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), # N (same as Latin H) - "О": bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), # O (same as Latin O) - "П": bytearray([0x7F, 0x01, 0x01, 0x01, 0x7F]), # P - "Р": bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), # R (same as Latin P) - "С": bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), # S (same as Latin C) - "Т": bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), # T (same as Latin T) - "У": bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), # U (same as Latin Y) - "Ф": bytearray([0x38, 0x54, 0x7F, 0x54, 0x38]), # F - "Х": bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), # Kh (same as Latin X) - "Ц": bytearray([0x3F, 0x40, 0x40, 0x7F, 0x40]), # Ts - "Ч": bytearray([0x07, 0x04, 0x04, 0x04, 0x7F]), # Ch - "Ш": bytearray([0x7F, 0x40, 0x7F, 0x40, 0x7F]), # Sh - "Щ": bytearray([0x7F, 0x40, 0x7F, 0x40, 0xFF]), # Shch - "Ь": bytearray([0x7F, 0x48, 0x48, 0x48, 0x30]), # Soft sign - "Ю": bytearray([0x7F, 0x38, 0x44, 0x44, 0x38]), # Yu - "Я": bytearray([0x32, 0x49, 0x49, 0x49, 0x7F]), # Ya + 'А': bytearray([0x7E, 0x11, 0x11, 0x11, 0x7E]), # A (same as Latin A) + 'Б': bytearray([0x7F, 0x49, 0x49, 0x49, 0x31]), # B + 'В': bytearray([0x7F, 0x49, 0x49, 0x49, 0x36]), # V (same as Latin B) + 'Г': bytearray([0x7F, 0x01, 0x01, 0x01, 0x00]), # H (Cyrillic) + 'Ґ': bytearray([0x7E, 0x02, 0x02, 0x03, 0x00]), # G with upturn + 'Д': bytearray([0x60, 0x3E, 0x21, 0x3E, 0x60]), # D + 'Е': bytearray([0x7F, 0x49, 0x49, 0x49, 0x41]), # E (same as Latin E) + 'Є': bytearray([0x3E, 0x49, 0x49, 0x49, 0x22]), # Ye + 'Ж': bytearray([0x63, 0x14, 0x7F, 0x14, 0x63]), # Zh + 'З': bytearray([0x22, 0x41, 0x49, 0x49, 0x36]), # Z + 'И': bytearray([0x7F, 0x20, 0x10, 0x08, 0x7F]), # Y + 'І': bytearray([0x00, 0x41, 0x7F, 0x41, 0x00]), # I (same as Latin I) + 'Ї': bytearray([0x01, 0x00, 0x7E, 0x00, 0x01]), # Yi (I with dots) + 'Й': bytearray([0x7E, 0x20, 0x11, 0x08, 0x7E]), # Y with breve + 'К': bytearray([0x7F, 0x08, 0x14, 0x22, 0x41]), # K (same as Latin K) + 'Л': bytearray([0x40, 0x60, 0x3F, 0x01, 0x7F]), # L + 'М': bytearray([0x7F, 0x02, 0x0C, 0x02, 0x7F]), # M (same as Latin M) + 'Н': bytearray([0x7F, 0x08, 0x08, 0x08, 0x7F]), # N (same as Latin H) + 'О': bytearray([0x3E, 0x41, 0x41, 0x41, 0x3E]), # O (same as Latin O) + 'П': bytearray([0x7F, 0x01, 0x01, 0x01, 0x7F]), # P + 'Р': bytearray([0x7F, 0x09, 0x09, 0x09, 0x06]), # R (same as Latin P) + 'С': bytearray([0x3E, 0x41, 0x41, 0x41, 0x22]), # S (same as Latin C) + 'Т': bytearray([0x01, 0x01, 0x7F, 0x01, 0x01]), # T (same as Latin T) + 'У': bytearray([0x07, 0x08, 0x70, 0x08, 0x07]), # U (same as Latin Y) + 'Ф': bytearray([0x1C, 0x22, 0x7F, 0x22, 0x1C]), # F + 'Х': bytearray([0x63, 0x14, 0x08, 0x14, 0x63]), # Kh (same as Latin X) + 'Ц': bytearray([0x3F, 0x40, 0x40, 0x7F, 0x40]), # Ts + 'Ч': bytearray([0x07, 0x04, 0x04, 0x04, 0x7F]), # Ch + 'Ш': bytearray([0x7F, 0x40, 0x7F, 0x40, 0x7F]), # Sh + 'Щ': bytearray([0x7F, 0x40, 0x7F, 0x40, 0xFF]), # Shch + 'Ь': bytearray([0x7F, 0x48, 0x48, 0x48, 0x30]), # Soft sign + 'Ю': bytearray([0x7F, 0x08, 0x3E, 0x41, 0x3E]), # Yu + 'Я': bytearray([0x32, 0x49, 0x49, 0x49, 0x7F]), # Ya + # Lowercase Ukrainian - "а": bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), # a (same as Latin a) - "б": bytearray([0x3F, 0x44, 0x44, 0x44, 0x38]), # b - "в": bytearray([0x7C, 0x54, 0x54, 0x54, 0x28]), # v - "г": bytearray([0x7C, 0x04, 0x04, 0x04, 0x00]), # h - "ґ": bytearray([0x7C, 0x04, 0x04, 0x04, 0x06]), # g with upturn - "д": bytearray([0x30, 0x28, 0x24, 0x7C, 0x60]), # d - "е": bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), # e (same as Latin e) - "є": bytearray([0x38, 0x54, 0x54, 0x54, 0x44]), # ye - "ж": bytearray([0x44, 0x28, 0x7C, 0x28, 0x44]), # zh - "з": bytearray([0x28, 0x44, 0x54, 0x54, 0x28]), # z - "и": bytearray([0x7C, 0x20, 0x10, 0x08, 0x7C]), # y - "і": bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), # i (same as Latin i) - "ї": bytearray([0x28, 0x44, 0x7D, 0x40, 0x28]), # yi (i with dots) - "й": bytearray([0x7C, 0x21, 0x12, 0x09, 0x7C]), # y with breve - "к": bytearray([0x7C, 0x10, 0x28, 0x44, 0x00]), # k - "л": bytearray([0x30, 0x28, 0x24, 0x28, 0x30]), # l - "м": bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), # m (same as Latin m) - "н": bytearray([0x7C, 0x08, 0x08, 0x08, 0x7C]), # n - "о": bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), # o (same as Latin o) - "п": bytearray([0x7C, 0x04, 0x04, 0x04, 0x7C]), # p - "р": bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), # r (same as Latin p) - "с": bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), # s (same as Latin c) - "т": bytearray([0x04, 0x04, 0x7F, 0x04, 0x04]), # t - "у": bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), # u (same as Latin y) - "ф": bytearray([0x38, 0x54, 0x7C, 0x54, 0x38]), # f - "х": bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), # kh (same as Latin x) - "ц": bytearray([0x3C, 0x40, 0x40, 0x7C, 0x40]), # ts - "ч": bytearray([0x0C, 0x10, 0x10, 0x10, 0x7C]), # ch - "ш": bytearray([0x7C, 0x40, 0x7C, 0x40, 0x7C]), # sh - "щ": bytearray([0x7C, 0x40, 0x7C, 0x40, 0xFC]), # shch - "ь": bytearray([0x7C, 0x48, 0x48, 0x48, 0x30]), # soft sign - "ю": bytearray([0x7C, 0x38, 0x44, 0x44, 0x38]), # yu - "я": bytearray([0x28, 0x54, 0x54, 0x54, 0x7C]), # ya + 'а': bytearray([0x20, 0x54, 0x54, 0x54, 0x78]), # a (same as Latin a) + 'б': bytearray([0x38, 0x54, 0x54, 0x54, 0x24]), # b + 'в': bytearray([0x7C, 0x54, 0x54, 0x54, 0x28]), # v + 'г': bytearray([0x7C, 0x04, 0x04, 0x04, 0x00]), # h + 'ґ': bytearray([0x7C, 0x04, 0x04, 0x04, 0x06]), # g with upturn + 'д': bytearray([0x30, 0x28, 0x24, 0x7C, 0x60]), # d + 'е': bytearray([0x38, 0x54, 0x54, 0x54, 0x18]), # e (same as Latin e) + 'є': bytearray([0x38, 0x54, 0x54, 0x54, 0x44]), # ye + 'ж': bytearray([0x44, 0x28, 0x7C, 0x28, 0x44]), # zh + 'з': bytearray([0x28, 0x44, 0x54, 0x54, 0x28]), # z + 'и': bytearray([0x7C, 0x20, 0x10, 0x08, 0x7C]), # y + 'і': bytearray([0x00, 0x44, 0x7D, 0x40, 0x00]), # i (same as Latin i) + 'ї': bytearray([0x01, 0x00, 0x7C, 0x00, 0x01]), # yi (i with dots) + 'й': bytearray([0x7C, 0x21, 0x12, 0x09, 0x7C]), # y with breve + 'к': bytearray([0x7C, 0x10, 0x28, 0x44, 0x00]), # k + 'л': bytearray([0x40, 0x60, 0x3C, 0x04, 0x7C]), # l + 'м': bytearray([0x7C, 0x04, 0x18, 0x04, 0x78]), # m (same as Latin m) + 'н': bytearray([0x7C, 0x08, 0x08, 0x08, 0x7C]), # n + 'о': bytearray([0x38, 0x44, 0x44, 0x44, 0x38]), # o (same as Latin o) + 'п': bytearray([0x7C, 0x04, 0x04, 0x04, 0x7C]), # p + 'р': bytearray([0x7C, 0x14, 0x14, 0x14, 0x08]), # r (same as Latin p) + 'с': bytearray([0x38, 0x44, 0x44, 0x44, 0x20]), # s (same as Latin c) + 'т': bytearray([0x04, 0x04, 0x7F, 0x04, 0x04]), # t + 'у': bytearray([0x0C, 0x50, 0x50, 0x50, 0x3C]), # u (same as Latin y) + 'ф': bytearray([0x38, 0x54, 0x7C, 0x54, 0x38]), # f + 'х': bytearray([0x44, 0x28, 0x10, 0x28, 0x44]), # kh (same as Latin x) + 'ц': bytearray([0x3C, 0x40, 0x40, 0x7C, 0x40]), # ts + 'ч': bytearray([0x0C, 0x10, 0x10, 0x10, 0x7C]), # ch + 'ш': bytearray([0x7C, 0x40, 0x7C, 0x40, 0x7C]), # sh + 'щ': bytearray([0x7C, 0x40, 0x7C, 0x40, 0xFC]), # shch + 'ь': bytearray([0x7C, 0x48, 0x48, 0x48, 0x30]), # soft sign + 'ю': bytearray([0x7C, 0x38, 0x44, 0x44, 0x38]), # yu + 'я': bytearray([0x28, 0x54, 0x54, 0x54, 0x7C]), # ya } # Combine all fonts into one dictionary @@ -196,10 +196,10 @@ def get_char_bitmap(char): """ Get bitmap data for a character. - + Args: char: Single character to get bitmap for - + Returns: bytearray: 5-byte bitmap data, or None if character not found """ @@ -209,27 +209,40 @@ def get_char_bitmap(char): def char_width(char): """ Get the width of a character in pixels. - + Args: char: Single character - + Returns: int: Width in pixels (always 5 for this font) """ - return FONT_WIDTH if char in FONT_DATA else 0 + bitmap = FONT_DATA.get(char, None) + if bitmap is None: + return 0 + return len(bitmap) def text_width(text): """ Calculate the total width of a text string in pixels. - + Args: text: String to measure - + Returns: int: Total width in pixels (including 1px spacing between chars) """ if not text: return 0 - # Each character is FONT_WIDTH pixels, plus 1 pixel spacing between characters - return len(text) * (FONT_WIDTH + 1) - 1 + + width = 0 + for char in text: + bitmap = FONT_DATA.get(char, None) + if bitmap is None: + continue + width += len(bitmap) + 1 + + if width > 0: + width -= 1 + + return width diff --git a/python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py b/python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py index c7cae67e5..76608dd42 100644 --- a/python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py +++ b/python-ecosys/bfu_ua_display/bfu_ua_display/text_engine.py @@ -17,10 +17,10 @@ def ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False): """ Render text at specified position with Ukrainian character support. - + This is the core rendering function that draws text pixel-by-pixel using the bitmap font data. - + Args: display: Display object with pixel() method text: String to render (supports English, numbers, symbols, Ukrainian) @@ -29,40 +29,42 @@ def ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False): color: Foreground color (default: 1 for white/on) bg_color: Background color (default: 0 for black/off) clear_bg: If True, clear background behind text (default: False) - + Returns: int: Total width of rendered text in pixels - + Example: >>> from machine import I2C, Pin >>> from ssd1306 import SSD1306_I2C >>> from bfu_ua_display import ua_text - >>> + >>> >>> i2c = I2C(0, scl=Pin(22), sda=Pin(21)) >>> oled = SSD1306_I2C(128, 64, i2c) - >>> + >>> >>> ua_text(oled, "ПРИВІТ", 0, 0) >>> oled.show() """ if not text: return 0 - + cursor_x = x total_width = 0 - + for char in text: bitmap = font5x7.get_char_bitmap(char) - + if bitmap is None: # Character not found, skip it continue - + + char_width = len(bitmap) + # Clear background if requested - if clear_bg and hasattr(display, "fill_rect"): - display.fill_rect(cursor_x, y, font5x7.FONT_WIDTH, font5x7.FONT_HEIGHT, bg_color) - + if clear_bg and hasattr(display, 'fill_rect'): + display.fill_rect(cursor_x, y, char_width, font5x7.FONT_HEIGHT, bg_color) + # Render character bitmap - for col in range(font5x7.FONT_WIDTH): + for col in range(char_width): column_data = bitmap[col] for row in range(font5x7.FONT_HEIGHT): # Check if pixel should be set (bit is 1) @@ -71,24 +73,32 @@ def ua_text(display, text, x, y, color=1, bg_color=0, clear_bg=False): elif clear_bg: # Clear pixel if background clearing is enabled display.pixel(cursor_x + col, y + row, bg_color) - + # Move cursor to next character position (with 1px spacing) - cursor_x += font5x7.FONT_WIDTH + 1 - total_width += font5x7.FONT_WIDTH + 1 - + cursor_x += char_width + 1 + total_width += char_width + 1 + # Remove trailing spacing from total width if total_width > 0: total_width -= 1 - + return total_width -def ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, display_width=128): +def ua_text_center( + display, + text, + y, + color=1, + bg_color=0, + clear_bg=False, + display_width=128, +): """ Render text centered horizontally on the display. - + Calculates the text width and centers it automatically. - + Args: display: Display object with pixel() method text: String to render @@ -97,20 +107,21 @@ def ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, displa bg_color: Background color (default: 0) clear_bg: If True, clear background behind text (default: False) display_width: Display width in pixels (default: 128 for SSD1306) - + Returns: int: X coordinate where text was rendered - + Example: >>> ua_text_center(oled, "УКРАЇНА", 28) >>> oled.show() """ text_w = font5x7.text_width(text) x = (display_width - text_w) // 2 - + # Ensure x is not negative - x = max(x, 0) - + if x < 0: + x = 0 + ua_text(display, text, x, y, color, bg_color, clear_bg) return x @@ -118,10 +129,10 @@ def ua_text_center(display, text, y, color=1, bg_color=0, clear_bg=False, displa def ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=False): """ Render text with scaling (2x, 3x, etc.). - - Each pixel in the original font is rendered as a scale x scale block. + + Each pixel in the original font is rendered as a scale×scale block. Note: This is memory-intensive for large scale values. - + Args: display: Display object with pixel() or fill_rect() method text: String to render @@ -131,80 +142,90 @@ def ua_text_scaled(display, text, x, y, scale=2, color=1, bg_color=0, clear_bg=F color: Foreground color (default: 1) bg_color: Background color (default: 0) clear_bg: If True, clear background behind text (default: False) - + Returns: int: Total width of rendered text in pixels - + Example: >>> ua_text_scaled(oled, "ПРИВІТ", 0, 0, scale=2) >>> oled.show() """ if not text or scale < 1: return 0 - + # For scale=1, use regular rendering for efficiency if scale == 1: return ua_text(display, text, x, y, color, bg_color, clear_bg) - + cursor_x = x total_width = 0 scaled_width = font5x7.FONT_WIDTH * scale scaled_height = font5x7.FONT_HEIGHT * scale spacing = scale # Scaled spacing between characters - + # Check if display supports fill_rect for optimization - has_fill_rect = hasattr(display, "fill_rect") - + has_fill_rect = hasattr(display, 'fill_rect') + for char in text: bitmap = font5x7.get_char_bitmap(char) - + if bitmap is None: continue - + + char_width = len(bitmap) + scaled_width = char_width * scale + scaled_height = font5x7.FONT_HEIGHT * scale + # Clear background if requested if clear_bg and has_fill_rect: display.fill_rect(cursor_x, y, scaled_width, scaled_height, bg_color) - + # Render scaled character - for col in range(font5x7.FONT_WIDTH): + for col in range(char_width): column_data = bitmap[col] for row in range(font5x7.FONT_HEIGHT): pixel_on = column_data & (1 << row) - + # Draw scaled pixel as a block if pixel_on or clear_bg: pixel_color = color if pixel_on else bg_color - + if has_fill_rect: # Use fill_rect for efficiency display.fill_rect( - cursor_x + col * scale, y + row * scale, scale, scale, pixel_color + cursor_x + col * scale, + y + row * scale, + scale, + scale, + pixel_color ) else: # Fall back to individual pixels for sx in range(scale): for sy in range(scale): display.pixel( - cursor_x + col * scale + sx, y + row * scale + sy, pixel_color + cursor_x + col * scale + sx, + y + row * scale + sy, + pixel_color ) - + # Move cursor cursor_x += scaled_width + spacing total_width += scaled_width + spacing - + # Remove trailing spacing if total_width > 0: total_width -= spacing - + return total_width def ua_text_right(display, text, x, y, color=1, bg_color=0, clear_bg=False): """ Render text right-aligned at specified position. - + The x coordinate represents the right edge of the text. - + Args: display: Display object with pixel() method text: String to render @@ -213,20 +234,21 @@ def ua_text_right(display, text, x, y, color=1, bg_color=0, clear_bg=False): color: Foreground color (default: 1) bg_color: Background color (default: 0) clear_bg: If True, clear background behind text (default: False) - + Returns: int: X coordinate where text starts (left edge) - + Example: >>> ua_text_right(oled, "100%", 127, 0) >>> oled.show() """ text_w = font5x7.text_width(text) start_x = x - text_w - + # Ensure start_x is not negative - start_x = max(start_x, 0) - + if start_x < 0: + start_x = 0 + ua_text(display, text, start_x, y, color, bg_color, clear_bg) return start_x @@ -234,9 +256,9 @@ def ua_text_right(display, text, x, y, color=1, bg_color=0, clear_bg=False): def clear_text_area(display, x, y, width, height, color=0): """ Clear a rectangular area on the display. - + Useful for clearing text before redrawing. - + Args: display: Display object x: X coordinate (left edge) @@ -244,14 +266,14 @@ def clear_text_area(display, x, y, width, height, color=0): width: Width in pixels height: Height in pixels color: Fill color (default: 0 for black) - + Example: >>> # Clear area before updating text >>> clear_text_area(oled, 0, 0, 128, 8) >>> ua_text(oled, "Updated", 0, 0) >>> oled.show() """ - if hasattr(display, "fill_rect"): + if hasattr(display, 'fill_rect'): display.fill_rect(x, y, width, height, color) else: # Fall back to pixel-by-pixel clearing diff --git a/python-ecosys/bfu_ua_display/bfu_ua_display/utils.py b/python-ecosys/bfu_ua_display/bfu_ua_display/utils.py index 3d4fc826f..3b4473a7b 100644 --- a/python-ecosys/bfu_ua_display/bfu_ua_display/utils.py +++ b/python-ecosys/bfu_ua_display/bfu_ua_display/utils.py @@ -11,13 +11,13 @@ def measure_text(text): """ Measure the dimensions of a text string. - + Args: text: String to measure - + Returns: tuple: (width, height) in pixels - + Example: >>> width, height = measure_text("ПРИВІТ") >>> print(f"Text size: {width}x{height}") @@ -30,14 +30,14 @@ def measure_text(text): def measure_text_scaled(text, scale=2): """ Measure the dimensions of scaled text. - + Args: text: String to measure scale: Scaling factor - + Returns: tuple: (width, height) in pixels - + Example: >>> width, height = measure_text_scaled("ПРИВІТ", scale=2) """ @@ -50,18 +50,18 @@ def measure_text_scaled(text, scale=2): def wrap_text(text, max_width, char_spacing=1): """ Wrap text to fit within a maximum width. - + Breaks text into lines that fit within the specified width. Tries to break at spaces when possible. - + Args: text: String to wrap max_width: Maximum width in pixels char_spacing: Spacing between characters (default: 1) - + Returns: list: List of text lines - + Example: >>> lines = wrap_text("ПРИВІТ УКРАЇНО", 40) >>> for i, line in enumerate(lines): @@ -69,154 +69,154 @@ def wrap_text(text, max_width, char_spacing=1): """ if not text: return [] - + lines = [] - words = text.split(" ") + words = text.split(' ') current_line = "" - + for word in words: - test_line = current_line + (" " if current_line else "") + word + test_line = current_line + (' ' if current_line else '') + word test_width = len(test_line) * (font5x7.FONT_WIDTH + char_spacing) - + if test_width <= max_width: current_line = test_line else: # Current line is full, start new line if current_line: lines.append(current_line) - + # Check if single word is too long word_width = len(word) * (font5x7.FONT_WIDTH + char_spacing) if word_width > max_width: # Break word into chunks chars_per_line = max_width // (font5x7.FONT_WIDTH + char_spacing) for i in range(0, len(word), chars_per_line): - lines.append(word[i : i + chars_per_line]) + lines.append(word[i:i + chars_per_line]) current_line = "" else: current_line = word - + # Add remaining text if current_line: lines.append(current_line) - + return lines def truncate_text(text, max_width, suffix="..."): """ Truncate text to fit within maximum width, adding suffix if truncated. - + Args: text: String to truncate max_width: Maximum width in pixels suffix: String to append if truncated (default: "...") - + Returns: str: Truncated text - + Example: - >>> short = truncate_text("VERY LONG TEXT", 50) + >>> short = truncate_text("ДУЖЕ ДОВГИЙ ТЕКСТ", 50) >>> ua_text(oled, short, 0, 0) """ if not text: return "" - + text_w = font5x7.text_width(text) if text_w <= max_width: return text - + suffix_w = font5x7.text_width(suffix) available_width = max_width - suffix_w - + if available_width <= 0: - return suffix[: max_width // (font5x7.FONT_WIDTH + 1)] - + return suffix[:max_width // (font5x7.FONT_WIDTH + 1)] + # Binary search for optimal length left, right = 0, len(text) result = "" - + while left <= right: mid = (left + right) // 2 test_text = text[:mid] test_width = font5x7.text_width(test_text) - + if test_width <= available_width: result = test_text left = mid + 1 else: right = mid - 1 - + return result + suffix def get_display_info(display): """ Get information about the display object. - + Attempts to detect display type and capabilities. - + Args: display: Display object - + Returns: dict: Display information - + Example: >>> info = get_display_info(oled) >>> print(f"Display: {info['type']}, Size: {info['width']}x{info['height']}") """ info = { - "type": "unknown", - "width": 128, # Default assumption - "height": 64, # Default assumption - "has_pixel": hasattr(display, "pixel"), - "has_fill_rect": hasattr(display, "fill_rect"), - "has_show": hasattr(display, "show"), - "has_fill": hasattr(display, "fill"), + 'type': 'unknown', + 'width': 128, # Default assumption + 'height': 64, # Default assumption + 'has_pixel': hasattr(display, 'pixel'), + 'has_fill_rect': hasattr(display, 'fill_rect'), + 'has_show': hasattr(display, 'show'), + 'has_fill': hasattr(display, 'fill'), } - + # Try to detect display type from class name class_name = type(display).__name__ - info["class"] = class_name - - if "SSD1306" in class_name: - info["type"] = "SSD1306" - elif "ST7789" in class_name: - info["type"] = "ST7789" - info["width"] = 240 - info["height"] = 240 - elif "ILI9341" in class_name: - info["type"] = "ILI9341" - info["width"] = 320 - info["height"] = 240 - elif "GC9A01" in class_name: - info["type"] = "GC9A01" - info["width"] = 240 - info["height"] = 240 - + info['class'] = class_name + + if 'SSD1306' in class_name: + info['type'] = 'SSD1306' + elif 'ST7789' in class_name: + info['type'] = 'ST7789' + info['width'] = 240 + info['height'] = 240 + elif 'ILI9341' in class_name: + info['type'] = 'ILI9341' + info['width'] = 320 + info['height'] = 240 + elif 'GC9A01' in class_name: + info['type'] = 'GC9A01' + info['width'] = 240 + info['height'] = 240 + # Try to get actual dimensions - if hasattr(display, "width"): - info["width"] = display.width - if hasattr(display, "height"): - info["height"] = display.height - + if hasattr(display, 'width'): + info['width'] = display.width + if hasattr(display, 'height'): + info['height'] = display.height + return info def center_position(text, display_width=128, display_height=64, scale=1): """ Calculate position to center text both horizontally and vertically. - + Args: text: String to center display_width: Display width in pixels (default: 128) display_height: Display height in pixels (default: 64) scale: Text scale factor (default: 1) - + Returns: tuple: (x, y) coordinates for centered text - + Example: >>> x, y = center_position("ПРИВІТ", 128, 64) >>> ua_text(oled, "ПРИВІТ", x, y) @@ -225,45 +225,48 @@ def center_position(text, display_width=128, display_height=64, scale=1): text_w, text_h = measure_text(text) else: text_w, text_h = measure_text_scaled(text, scale) - + x = (display_width - text_w) // 2 y = (display_height - text_h) // 2 - + # Ensure coordinates are not negative x = max(0, x) y = max(0, y) - + return (x, y) def supports_ukrainian(text): """ Check if text contains Ukrainian characters. - + Args: text: String to check - + Returns: bool: True if text contains Ukrainian characters - + Example: >>> if supports_ukrainian("ПРИВІТ"): >>> print("Ukrainian text detected") """ - ukrainian_chars = set("АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯабвгґдеєжзиіїйклмнопрстуфхцчшщьюя") + ukrainian_chars = set( + "АБВГҐДЕЄЖЗИІЇЙКЛМНОПРСТУФХЦЧШЩЬЮЯ" + "абвгґдеєжзиіїйклмнопрстуфхцчшщьюя" + ) return any(char in ukrainian_chars for char in text) def validate_text(text): """ Validate if all characters in text are supported by the font. - + Args: text: String to validate - + Returns: tuple: (is_valid, unsupported_chars) - + Example: >>> valid, unsupported = validate_text("ПРИВІТ 123") >>> if not valid: @@ -274,5 +277,5 @@ def validate_text(text): if font5x7.get_char_bitmap(char) is None: if char not in unsupported: unsupported.append(char) - + return (len(unsupported) == 0, unsupported) diff --git a/python-ecosys/bfu_ua_display/manifest.py b/python-ecosys/bfu_ua_display/manifest.py index 3855b6c5d..63e831640 100644 --- a/python-ecosys/bfu_ua_display/manifest.py +++ b/python-ecosys/bfu_ua_display/manifest.py @@ -1,6 +1,8 @@ +# ruff: noqa: F821 + metadata( description="Ukrainian text rendering library for MicroPython displays", version="0.1.0", ) -package("bfu_ua_display") +package("bfu_ua_display") \ No newline at end of file diff --git a/python-ecosys/bfu_ua_display/package.json b/python-ecosys/bfu_ua_display/package.json new file mode 100644 index 000000000..ec1603d65 --- /dev/null +++ b/python-ecosys/bfu_ua_display/package.json @@ -0,0 +1,10 @@ +{ + "urls": [ + ["__init__.py", "github:BrainFromUkraine/bfu_ua_display/bfu_ua_display/__init__.py"], + ["font5x7.py", "github:BrainFromUkraine/bfu_ua_display/bfu_ua_display/font5x7.py"], + ["text_engine.py", "github:BrainFromUkraine/bfu_ua_display/bfu_ua_display/text_engine.py"], + ["utils.py", "github:BrainFromUkraine/bfu_ua_display/bfu_ua_display/utils.py"] + ], + "deps": [], + "version": "0.1.0" +}