From ae6148a9e20f3ac98796ad80085578824039c325 Mon Sep 17 00:00:00 2001 From: Oleksandr Polishchuk Date: Wed, 13 May 2026 21:13:24 +0100 Subject: [PATCH 1/2] 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/2] 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