diff --git a/.github/actions/deps/ports/broadcom/action.yml b/.github/actions/deps/ports/broadcom/action.yml
index 99f53064b3f68..f936f8e7edfde 100644
--- a/.github/actions/deps/ports/broadcom/action.yml
+++ b/.github/actions/deps/ports/broadcom/action.yml
@@ -5,8 +5,8 @@ runs:
steps:
- name: Get broadcom toolchain
run: |
- wget --no-verbose https://adafruit-circuit-python.s3.amazonaws.com/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz
- sudo tar -C /usr --strip-components=1 -xaf gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz
+ wget --no-verbose https://adafruit-circuit-python.s3.amazonaws.com/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-elf.tar.xz
+ sudo tar -C /usr --strip-components=1 -xaf arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-elf.tar.xz
sudo apt-get update
sudo apt-get install -y mtools
shell: bash
diff --git a/.gitignore b/.gitignore
index 9679ed5e0e2bc..6725b901b92bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,3 +97,8 @@ TAGS
# clangd cache
##############
.cache
+
+**/CLAUDE.local.md
+
+# windsurf rules
+.windsurfrules
diff --git a/docs/environment.rst b/docs/environment.rst
index 82d7e8790bcd8..442c340f1a4d4 100644
--- a/docs/environment.rst
+++ b/docs/environment.rst
@@ -189,4 +189,18 @@ This feature is not enabled on boards that the CIRCUITPY_OS_GETENV (os CIRCUIPTY
flag has been set to 0. Currently this is primarily boards with limited flash including some
of the Atmel_samd boards based on the SAMD21/M0 microprocessor.
+CIRCUITPY_TERMINAL_FONT
+~~~~~~~~~~~~~~~~~~~~~~~
+Specifies a custom font file path to use for the terminalio console instead of the default
+``/fonts/terminal.lvfontbin``. This allows users to create and use custom fonts for the
+CircuitPython console.
+
+This feature requires both CIRCUITPY_OS_GETENV and CIRCUITPY_LVFONTIO to be enabled.
+
+Example:
+
+.. code-block::
+
+ CIRCUITPY_TERMINAL_FONT="/fonts/myfont.lvfontbin"
+
`boards that the terminalio core module is available on `_
diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot
index 167fb20ff9e80..49920b1549524 100644
--- a/locale/circuitpython.pot
+++ b/locale/circuitpython.pot
@@ -1051,7 +1051,7 @@ msgstr ""
msgid "File exists"
msgstr ""
-#: shared-module/os/getenv.c
+#: shared-module/lvfontio/OnDiskFont.c shared-module/os/getenv.c
msgid "File not found"
msgstr ""
@@ -1247,6 +1247,7 @@ msgstr ""
#: shared-bindings/digitalio/DigitalInOut.c
#: shared-bindings/epaperdisplay/EPaperDisplay.c shared-bindings/pwmio/PWMOut.c
#: shared-module/aurora_epaper/aurora_framebuffer.c
+#: shared-module/lvfontio/OnDiskFont.c
msgid "Invalid %q"
msgstr ""
@@ -1975,6 +1976,7 @@ msgstr ""
#: shared-bindings/displayio/TileGrid.c
#: shared-bindings/memorymonitor/AllocationSize.c
#: shared-bindings/pulseio/PulseIn.c
+#: shared-bindings/tilepalettemapper/TilePaletteMapper.c
msgid "Slices not supported"
msgstr ""
@@ -2042,7 +2044,9 @@ msgstr ""
msgid "Tile height must exactly divide bitmap height"
msgstr ""
-#: shared-bindings/displayio/TileGrid.c shared-module/displayio/TileGrid.c
+#: shared-bindings/displayio/TileGrid.c
+#: shared-bindings/tilepalettemapper/TilePaletteMapper.c
+#: shared-module/displayio/TileGrid.c
msgid "Tile index out of bounds"
msgstr ""
@@ -4277,7 +4281,9 @@ msgstr ""
msgid "unreadable attribute"
msgstr ""
-#: shared-bindings/displayio/TileGrid.c shared-bindings/vectorio/VectorShape.c
+#: shared-bindings/displayio/TileGrid.c shared-bindings/terminalio/Terminal.c
+#: shared-bindings/tilepalettemapper/TilePaletteMapper.c
+#: shared-bindings/vectorio/VectorShape.c
msgid "unsupported %q type"
msgstr ""
diff --git a/ports/atmel-samd/mpconfigport.mk b/ports/atmel-samd/mpconfigport.mk
index 15013b2d831ca..861ef37464633 100644
--- a/ports/atmel-samd/mpconfigport.mk
+++ b/ports/atmel-samd/mpconfigport.mk
@@ -12,6 +12,7 @@ CIRCUITPY_OPTIMIZE_PROPERTY_FLASH_SIZE ?= 1
CIRCUITPY_LTO = 1
CIRCUITPY_KEYPAD_DEMUX ?= 0
+CIRCUITPY_LVFONTIO ?= 0
######################################################################
# Put samd21-only choices here.
diff --git a/ports/raspberrypi/common-hal/picodvi/__init__.c b/ports/raspberrypi/common-hal/picodvi/__init__.c
index 9a77979669463..e8f344852de96 100644
--- a/ports/raspberrypi/common-hal/picodvi/__init__.c
+++ b/ports/raspberrypi/common-hal/picodvi/__init__.c
@@ -74,11 +74,11 @@ static bool picodvi_autoconstruct_enabled(mp_int_t *default_width, mp_int_t *def
if ((established_timings & 0x80) != 0 &&
preferred_width % 1920 == 0 &&
preferred_height % 1080 == 0) {
- *default_width = 720 / 2;
- *default_height = 400 / 2;
+ *default_width = 720;
+ *default_height = 400;
} else {
- *default_width = 640 / 2;
- *default_height = 480 / 2;
+ *default_width = 640;
+ *default_height = 480;
}
}
}
@@ -95,15 +95,15 @@ void picodvi_autoconstruct(void) {
return;
}
- mp_int_t default_width = 320;
- mp_int_t default_height = 240;
+ mp_int_t default_width = 640;
+ mp_int_t default_height = 480;
if (!picodvi_autoconstruct_enabled(&default_width, &default_height)) {
return;
}
mp_int_t width = default_width;
mp_int_t height = 0;
- mp_int_t color_depth = 16;
+ mp_int_t color_depth = 8;
mp_int_t rotation = 0;
(void)common_hal_os_getenv_int("CIRCUITPY_DISPLAY_WIDTH", &width);
@@ -113,6 +113,9 @@ void picodvi_autoconstruct(void) {
if (height == 0) {
switch (width) {
+ case 720:
+ height = 400;
+ break;
case 640:
height = 480;
break;
@@ -134,7 +137,7 @@ void picodvi_autoconstruct(void) {
// invalid configuration, set back to default
width = default_width;
height = default_height;
- color_depth = 16;
+ color_depth = 8;
}
// construct framebuffer and display
diff --git a/ports/stm/boards/meowbit_v121/mpconfigboard.mk b/ports/stm/boards/meowbit_v121/mpconfigboard.mk
index bde49ad13ef49..3a128fb8343cc 100644
--- a/ports/stm/boards/meowbit_v121/mpconfigboard.mk
+++ b/ports/stm/boards/meowbit_v121/mpconfigboard.mk
@@ -27,6 +27,8 @@ CIRCUITPY_BITMAPFILTER = 0
CIRCUITPY_BITMAPTOOLS = 0
CIRCUITPY_BLEIO_HCI = 0
CIRCUITPY_EPAPERDISPLAY = 0
+CIRCUITPY_FRAMEBUFFERIO = 0
+CIRCUITPY_I2CDISPLAYBUS = 0
CIRCUITPY_KEYPAD_DEMUX = 0
CIRCUITPY_SHARPDISPLAY = 0
CIRCUITPY_TILEPALETTEMAPPER = 0
diff --git a/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk b/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
index f34fac4047a72..98d13f592ffcb 100644
--- a/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
+++ b/ports/stm/boards/stm32f411ve_discovery/mpconfigboard.mk
@@ -9,6 +9,8 @@ MCU_SERIES = F4
MCU_VARIANT = STM32F411xE
MCU_PACKAGE = LQFP100_f4
+OPTIMIZATION_FLAGS = -Os
+
LD_COMMON = boards/common_default.ld
LD_FILE = boards/STM32F411_fs.ld
diff --git a/ports/stm/boards/thunderpack_v11/mpconfigboard.mk b/ports/stm/boards/thunderpack_v11/mpconfigboard.mk
index 4fede00dc07ea..0107bbb1c1777 100644
--- a/ports/stm/boards/thunderpack_v11/mpconfigboard.mk
+++ b/ports/stm/boards/thunderpack_v11/mpconfigboard.mk
@@ -13,6 +13,8 @@ MCU_SERIES = F4
MCU_VARIANT = STM32F411xE
MCU_PACKAGE = UFQFPN48
+OPTIMIZATION_FLAGS = -Os
+
LD_COMMON = boards/common_nvm.ld
LD_FILE = boards/STM32F411_nvm.ld
diff --git a/ports/stm/boards/thunderpack_v12/mpconfigboard.mk b/ports/stm/boards/thunderpack_v12/mpconfigboard.mk
index 276cf6d011050..353d0733e6483 100644
--- a/ports/stm/boards/thunderpack_v12/mpconfigboard.mk
+++ b/ports/stm/boards/thunderpack_v12/mpconfigboard.mk
@@ -25,11 +25,7 @@ MCU_SERIES = F4
MCU_VARIANT = STM32F411xE
MCU_PACKAGE = UFQFPN48
+OPTIMIZATION_FLAGS = -Os
+
LD_COMMON = boards/common_nvm.ld
LD_FILE = boards/STM32F411_nvm_nofs.ld
-
-# Disable TERMINALIO on translations with missing characters.
-ifneq (,$(filter $(TRANSLATION),ja ko ru))
-CIRCUITPY_TERMINALIO = 0
-RELEASE_NEEDS_CLEAN_BUILD = $(CIRCUITPY_DISPLAYIO)
-endif
diff --git a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
index ba0291f64300a..624e7e69a3fd1 100644
--- a/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf5340dk/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
index 88178af3b28c2..07ab1f7f4dfc6 100644
--- a/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf54l15dk/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
index 85b5a9c7e0fad..b10bbe69a0a10 100644
--- a/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/nordic/nrf7002dk/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
index 674a19ab107c9..fcfeec097d124 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra6m5/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
index 4f920c59f11d9..69481e904fea4 100644
--- a/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/renesas/ek_ra8d1/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
index 000155eafa744..e7e179e4c2cc9 100644
--- a/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/nucleo_u575zi_q/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
index ac0aff61a59c3..d4fa229b7e67d 100644
--- a/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
+++ b/ports/zephyr-cp/boards/st/stm32h7b3i_dk/autogen_board_info.toml
@@ -58,6 +58,7 @@ jpegio = false
keypad = false
keypad_demux = false
locale = false
+lvfontio = false
math = false
max3421e = false
mdns = false
diff --git a/ports/zephyr-cp/cptools/update_board_info.py b/ports/zephyr-cp/cptools/update_board_info.py
new file mode 100755
index 0000000000000..935fc07c17d3a
--- /dev/null
+++ b/ports/zephyr-cp/cptools/update_board_info.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+import pathlib
+import sys
+import tomlkit
+
+
+def find_modules(top_dir, port_dir):
+ """Find all available modules in shared-bindings and port bindings."""
+ modules = set()
+ for module in sorted(
+ list(top_dir.glob("shared-bindings/*")) + list(port_dir.glob("bindings/*")),
+ key=lambda x: x.name,
+ ):
+ if not module.is_dir():
+ continue
+ modules.add(module.name)
+ return sorted(modules)
+
+
+def find_board_info_files(port_dir):
+ """Find all autogen_board_info.toml files in the port directory."""
+ return list(port_dir.glob("boards/**/autogen_board_info.toml"))
+
+
+def update_board_info(board_info_path, available_modules):
+ """Update board info file with new modules set to false."""
+ if not board_info_path.exists():
+ print(f"Error: Board info file {board_info_path} does not exist", file=sys.stderr)
+ return False
+
+ # Load existing board info
+ with open(board_info_path, "r", encoding="utf-8") as f:
+ board_info = tomlkit.load(f)
+
+ # Get current modules
+ current_modules = set(board_info.get("modules", {}))
+
+ # Find new modules
+ new_modules = set(available_modules) - current_modules
+ if not new_modules:
+ print(
+ f"No new modules found for {board_info_path.relative_to(board_info_path.parents[3])}"
+ )
+ return True
+
+ # Add new modules as disabled in alphabetical order
+ modules_table = board_info["modules"]
+ # Get all modules (existing and new) and sort them
+ all_modules = list(current_modules | new_modules)
+ all_modules.sort()
+
+ # Create a new table with sorted modules
+ sorted_table = tomlkit.table()
+ for module in all_modules:
+ if module in modules_table:
+ # TODO: Use modules_table.item once tomlkit is released with changes from January 2025
+ sorted_table[module] = modules_table._value.item(module)
+ else:
+ sorted_table[module] = tomlkit.item(False)
+
+ # Replace the modules table with the sorted one
+ board_info["modules"] = sorted_table
+
+ # Write updated board info
+ with open(board_info_path, "w", encoding="utf-8") as f:
+ tomlkit.dump(board_info, f)
+
+ print(
+ f"Updated {board_info_path.relative_to(board_info_path.parents[3])} with {len(new_modules)} new modules:"
+ )
+ for module in sorted(new_modules):
+ print(f" - {module}")
+ return True
+
+
+def main():
+ # Get repo paths
+ script_dir = pathlib.Path(__file__).parent
+ top_dir = script_dir.parents[2] # circuitpython root
+ port_dir = script_dir.parent # zephyr-cp directory
+
+ # Get available modules once
+ available_modules = find_modules(top_dir, port_dir)
+
+ # Update all board info files
+ board_info_files = find_board_info_files(port_dir)
+ if not board_info_files:
+ print("No board info files found")
+ sys.exit(1)
+
+ success = True
+ for board_info_path in board_info_files:
+ if not update_board_info(board_info_path, available_modules):
+ success = False
+
+ sys.exit(0 if success else 1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk
index 513f7d2d64d97..8fb0c1c8185f1 100644
--- a/py/circuitpy_defns.mk
+++ b/py/circuitpy_defns.mk
@@ -396,6 +396,9 @@ endif
ifeq ($(CIRCUITPY_FONTIO),1)
SRC_PATTERNS += fontio/%
endif
+ifeq ($(CIRCUITPY_LVFONTIO),1)
+SRC_PATTERNS += lvfontio/%
+endif
ifeq ($(CIRCUITPY_TILEPALETTEMAPPER),1)
SRC_PATTERNS += tilepalettemapper/%
endif
@@ -675,6 +678,8 @@ SRC_SHARED_MODULE_ALL = \
floppyio/__init__.c \
fontio/BuiltinFont.c \
fontio/__init__.c \
+ lvfontio/OnDiskFont.c\
+ lvfontio/__init__.c \
fourwire/__init__.c \
fourwire/FourWire.c \
framebufferio/FramebufferDisplay.c \
diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk
index 831c1ec245fc3..52520215b24d7 100644
--- a/py/circuitpy_mpconfig.mk
+++ b/py/circuitpy_mpconfig.mk
@@ -555,9 +555,12 @@ CFLAGS += -DCIRCUITPY_TERMINALIO=$(CIRCUITPY_TERMINALIO)
CIRCUITPY_TERMINALIO_VT100 ?= $(CIRCUITPY_TERMINALIO)
CFLAGS += -DCIRCUITPY_TERMINALIO_VT100=$(CIRCUITPY_TERMINALIO_VT100)
-CIRCUITPY_FONTIO ?= $(call enable-if-all,$(CIRCUITPY_DISPLAYIO) $(CIRCUITPY_TERMINALIO))
+CIRCUITPY_FONTIO ?= $(CIRCUITPY_TERMINALIO)
CFLAGS += -DCIRCUITPY_FONTIO=$(CIRCUITPY_FONTIO)
+CIRCUITPY_LVFONTIO ?= $(CIRCUITPY_TERMINALIO)
+CFLAGS += -DCIRCUITPY_LVFONTIO=$(CIRCUITPY_LVFONTIO)
+
CIRCUITPY_TILEPALETTEMAPPER ?= $(CIRCUITPY_DISPLAYIO)
CFLAGS += -DCIRCUITPY_TILEPALETTEMAPPER=$(CIRCUITPY_TILEPALETTEMAPPER)
diff --git a/shared-bindings/displayio/TileGrid.c b/shared-bindings/displayio/TileGrid.c
index a8757fbcc17f2..fbc995fc49b89 100644
--- a/shared-bindings/displayio/TileGrid.c
+++ b/shared-bindings/displayio/TileGrid.c
@@ -62,7 +62,8 @@ void displayio_tilegrid_validate_pixel_shader(mp_obj_t pixel_shader) {
//| convert the value and its location to a display native pixel color. This may be a simple color
//| palette lookup, a gradient, a pattern or a color transformer.
//|
-//| To save RAM usage, tile values are only allowed in the range from 0 to 255 inclusive (single byte values).
+//| When the total number of tiles is 256 or less, tile values are stored as single bytes (uint8_t).
+//| When the total number of tiles is more than 256, tile values are stored as double bytes (uint16_t).
//|
//| tile_width and tile_height match the height of the bitmap by default.
//|
@@ -453,7 +454,7 @@ static mp_obj_t tilegrid_subscr(mp_obj_t self_in, mp_obj_t index_obj, mp_obj_t v
return MP_OBJ_NULL; // op not supported
} else {
mp_int_t value = mp_obj_get_int(value_obj);
- mp_arg_validate_int_range(value, 0, 255, MP_QSTR_tile);
+ mp_arg_validate_int_range(value, 0, self->tiles_in_bitmap - 1, MP_QSTR_tile);
common_hal_displayio_tilegrid_set_tile(self, x, y, value);
}
diff --git a/shared-bindings/displayio/TileGrid.h b/shared-bindings/displayio/TileGrid.h
index 19af39fadeb37..35b2b2fd00c9e 100644
--- a/shared-bindings/displayio/TileGrid.h
+++ b/shared-bindings/displayio/TileGrid.h
@@ -13,7 +13,7 @@ extern const mp_obj_type_t displayio_tilegrid_type;
void common_hal_displayio_tilegrid_construct(displayio_tilegrid_t *self, mp_obj_t bitmap,
uint16_t bitmap_width_in_tiles, uint16_t bitmap_height_in_tiles,
mp_obj_t pixel_shader, uint16_t width, uint16_t height,
- uint16_t tile_width, uint16_t tile_height, uint16_t x, uint16_t y, uint8_t default_tile);
+ uint16_t tile_width, uint16_t tile_height, uint16_t x, uint16_t y, uint16_t default_tile);
bool common_hal_displayio_tilegrid_get_hidden(displayio_tilegrid_t *self);
void common_hal_displayio_tilegrid_set_hidden(displayio_tilegrid_t *self, bool hidden);
@@ -43,9 +43,9 @@ uint16_t common_hal_displayio_tilegrid_get_height(displayio_tilegrid_t *self);
uint16_t common_hal_displayio_tilegrid_get_tile_width(displayio_tilegrid_t *self);
uint16_t common_hal_displayio_tilegrid_get_tile_height(displayio_tilegrid_t *self);
-uint8_t common_hal_displayio_tilegrid_get_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y);
-void common_hal_displayio_tilegrid_set_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y, uint8_t tile_index);
+uint16_t common_hal_displayio_tilegrid_get_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y);
+void common_hal_displayio_tilegrid_set_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y, uint16_t tile_index);
// Private API for scrolling the TileGrid.
void common_hal_displayio_tilegrid_set_top_left(displayio_tilegrid_t *self, uint16_t x, uint16_t y);
-void common_hal_displayio_tilegrid_set_all_tiles(displayio_tilegrid_t *self, uint8_t tile_index);
+void common_hal_displayio_tilegrid_set_all_tiles(displayio_tilegrid_t *self, uint16_t tile_index);
diff --git a/shared-bindings/lvfontio/OnDiskFont.c b/shared-bindings/lvfontio/OnDiskFont.c
new file mode 100644
index 0000000000000..3d4df234429ff
--- /dev/null
+++ b/shared-bindings/lvfontio/OnDiskFont.c
@@ -0,0 +1,94 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+
+#include
+
+#include "shared/runtime/context_manager_helpers.h"
+#include "py/binary.h"
+#include "py/objproperty.h"
+#include "py/objstr.h"
+#include "py/runtime.h"
+#include "shared-bindings/microcontroller/Pin.h"
+#include "shared-bindings/util.h"
+
+//| class OnDiskFont:
+//| """A font built into CircuitPython for use with LVGL"""
+//|
+//| def __init__(self, file_path: str, max_glyphs: int = 100) -> None:
+//| """Create a OnDiskFont by loading an LVGL font file from the filesystem.
+//|
+//| :param str file_path: The path to the font file
+//| :param int max_glyphs: Maximum number of glyphs to cache at once
+//| """
+//| ...
+//|
+
+//| bitmap: displayio.Bitmap
+//| """Bitmap containing all font glyphs starting with ASCII and followed by unicode. This is useful for use with LVGL."""
+//|
+static mp_obj_t lvfontio_ondiskfont_obj_get_bitmap(mp_obj_t self_in) {
+ lvfontio_ondiskfont_t *self = MP_OBJ_TO_PTR(self_in);
+ return common_hal_lvfontio_ondiskfont_get_bitmap(self);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(lvfontio_ondiskfont_get_bitmap_obj, lvfontio_ondiskfont_obj_get_bitmap);
+
+MP_PROPERTY_GETTER(lvfontio_ondiskfont_bitmap_obj,
+ (mp_obj_t)&lvfontio_ondiskfont_get_bitmap_obj);
+
+//| def get_bounding_box(self) -> Tuple[int, int]:
+//| """Returns the maximum bounds of all glyphs in the font in a tuple of two values: width, height."""
+//| ...
+//|
+//|
+static mp_obj_t lvfontio_ondiskfont_obj_get_bounding_box(mp_obj_t self_in) {
+ lvfontio_ondiskfont_t *self = MP_OBJ_TO_PTR(self_in);
+
+ return common_hal_lvfontio_ondiskfont_get_bounding_box(self);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(lvfontio_ondiskfont_get_bounding_box_obj, lvfontio_ondiskfont_obj_get_bounding_box);
+
+static const mp_rom_map_elem_t lvfontio_ondiskfont_locals_dict_table[] = {
+ { MP_ROM_QSTR(MP_QSTR_bitmap), MP_ROM_PTR(&lvfontio_ondiskfont_bitmap_obj) },
+ { MP_ROM_QSTR(MP_QSTR_get_bounding_box), MP_ROM_PTR(&lvfontio_ondiskfont_get_bounding_box_obj) },
+};
+static MP_DEFINE_CONST_DICT(lvfontio_ondiskfont_locals_dict, lvfontio_ondiskfont_locals_dict_table);
+
+static mp_obj_t lvfontio_ondiskfont_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
+ enum { ARG_file_path, ARG_max_glyphs };
+ static const mp_arg_t allowed_args[] = {
+ { MP_QSTR_file_path, MP_ARG_OBJ | MP_ARG_REQUIRED },
+ { MP_QSTR_max_glyphs, MP_ARG_INT, {.u_int = 100} },
+ };
+
+ mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
+ mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
+
+ // Allocate the BuiltinFont object
+ lvfontio_ondiskfont_t *self = m_new_obj(lvfontio_ondiskfont_t);
+ self->base.type = &lvfontio_ondiskfont_type;
+
+ // Extract arguments
+ mp_obj_t file_path_obj = args[ARG_file_path].u_obj;
+ mp_uint_t max_glyphs = args[ARG_max_glyphs].u_int;
+
+ // Get the C string from the Python string
+ const char *file_path = mp_obj_str_get_str(file_path_obj);
+
+ // Always use GC allocator for Python-created objects
+ common_hal_lvfontio_ondiskfont_construct(self, file_path, max_glyphs, true);
+
+ return MP_OBJ_FROM_PTR(self);
+}
+
+MP_DEFINE_CONST_OBJ_TYPE(
+ lvfontio_ondiskfont_type,
+ MP_QSTR_OnDiskFont,
+ MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
+ make_new, lvfontio_ondiskfont_make_new,
+ locals_dict, &lvfontio_ondiskfont_locals_dict
+ );
diff --git a/shared-bindings/lvfontio/OnDiskFont.h b/shared-bindings/lvfontio/OnDiskFont.h
new file mode 100644
index 0000000000000..d7dac6ac6787a
--- /dev/null
+++ b/shared-bindings/lvfontio/OnDiskFont.h
@@ -0,0 +1,22 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "shared-module/lvfontio/OnDiskFont.h"
+
+extern const mp_obj_type_t lvfontio_ondiskfont_type;
+
+mp_obj_t common_hal_lvfontio_ondiskfont_get_bitmap(const lvfontio_ondiskfont_t *self);
+mp_obj_t common_hal_lvfontio_ondiskfont_get_bounding_box(const lvfontio_ondiskfont_t *self);
+void common_hal_lvfontio_ondiskfont_get_dimensions(const lvfontio_ondiskfont_t *self, uint16_t *width, uint16_t *height);
+
+// Function prototypes
+void common_hal_lvfontio_ondiskfont_construct(lvfontio_ondiskfont_t *self, const char *file_path, uint16_t max_glyphs, bool use_gc_allocator);
+void common_hal_lvfontio_ondiskfont_deinit(lvfontio_ondiskfont_t *self);
+bool common_hal_lvfontio_ondiskfont_deinited(lvfontio_ondiskfont_t *self);
+int16_t common_hal_lvfontio_ondiskfont_cache_glyph(lvfontio_ondiskfont_t *self, uint32_t codepoint, bool *is_full_width);
+void common_hal_lvfontio_ondiskfont_release_glyph(lvfontio_ondiskfont_t *self, uint32_t slot);
diff --git a/shared-bindings/lvfontio/__init__.c b/shared-bindings/lvfontio/__init__.c
new file mode 100644
index 0000000000000..ec5f352279952
--- /dev/null
+++ b/shared-bindings/lvfontio/__init__.c
@@ -0,0 +1,33 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#include
+
+#include "py/obj.h"
+#include "py/runtime.h"
+
+#include "shared-bindings/lvfontio/__init__.h"
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+
+//| """Core font related data structures for LVGL
+//|
+//| .. note:: This module is intended only for low-level usage with LVGL.
+//|
+//| """
+
+static const mp_rom_map_elem_t lvfontio_module_globals_table[] = {
+ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_lvfontio) },
+ { MP_ROM_QSTR(MP_QSTR_OnDiskFont), MP_ROM_PTR(&lvfontio_ondiskfont_type) },
+};
+
+static MP_DEFINE_CONST_DICT(lvfontio_module_globals, lvfontio_module_globals_table);
+
+const mp_obj_module_t lvfontio_module = {
+ .base = { &mp_type_module },
+ .globals = (mp_obj_dict_t *)&lvfontio_module_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_lvfontio, lvfontio_module);
diff --git a/shared-bindings/lvfontio/__init__.h b/shared-bindings/lvfontio/__init__.h
new file mode 100644
index 0000000000000..b56388fa8ae3a
--- /dev/null
+++ b/shared-bindings/lvfontio/__init__.h
@@ -0,0 +1,7 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
diff --git a/shared-bindings/terminalio/Terminal.c b/shared-bindings/terminalio/Terminal.c
index 570449ad424a4..63cabda7fbed8 100644
--- a/shared-bindings/terminalio/Terminal.c
+++ b/shared-bindings/terminalio/Terminal.c
@@ -16,6 +16,10 @@
#include "py/stream.h"
#include "shared-bindings/fontio/BuiltinFont.h"
+#if CIRCUITPY_LVFONTIO
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+#endif
+
//| class Terminal:
//| """Display a character stream with a TileGrid
//|
@@ -86,7 +90,26 @@ static mp_obj_t terminalio_terminal_make_new(const mp_obj_type_t *type, size_t n
status_bar = mp_arg_validate_type(args[ARG_status_bar].u_obj, &displayio_tilegrid_type, MP_QSTR_status_bar);
}
- fontio_builtinfont_t *font = mp_arg_validate_type(args[ARG_font].u_obj, &fontio_builtinfont_type, MP_QSTR_font);
+ mp_obj_t font = args[ARG_font].u_obj;
+
+ // Ensure the font is one of the supported types
+ bool valid_font = false;
+
+ #if CIRCUITPY_FONTIO
+ if (mp_obj_is_type(font, &fontio_builtinfont_type)) {
+ valid_font = true;
+ }
+ #endif
+
+ #if CIRCUITPY_LVFONTIO
+ if (mp_obj_is_type(font, &lvfontio_ondiskfont_type)) {
+ valid_font = true;
+ }
+ #endif
+
+ if (!valid_font) {
+ mp_raise_TypeError_varg(MP_ERROR_TEXT("unsupported %q type"), MP_QSTR_font);
+ }
mp_arg_validate_int_min(scroll_area->width_in_tiles, 2, MP_QSTR_scroll_area_width);
mp_arg_validate_int_min(scroll_area->height_in_tiles, 2, MP_QSTR_scroll_area_height);
diff --git a/shared-bindings/terminalio/Terminal.h b/shared-bindings/terminalio/Terminal.h
index 893db4d131128..c99cae2b3bb8c 100644
--- a/shared-bindings/terminalio/Terminal.h
+++ b/shared-bindings/terminalio/Terminal.h
@@ -13,7 +13,7 @@
extern const mp_obj_type_t terminalio_terminal_type;
extern void common_hal_terminalio_terminal_construct(terminalio_terminal_obj_t *self,
- displayio_tilegrid_t *scroll_area, const fontio_builtinfont_t *font, displayio_tilegrid_t *status_bar);
+ displayio_tilegrid_t *scroll_area, mp_obj_t font, displayio_tilegrid_t *status_bar);
// Write characters. len is in characters NOT bytes!
extern size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self,
diff --git a/shared-module/displayio/TileGrid.c b/shared-module/displayio/TileGrid.c
index e0d18d0accfb8..9312b3e04748b 100644
--- a/shared-module/displayio/TileGrid.c
+++ b/shared-module/displayio/TileGrid.c
@@ -15,29 +15,53 @@
#include "shared-bindings/tilepalettemapper/TilePaletteMapper.h"
#endif
+#include "supervisor/shared/serial.h"
+
void common_hal_displayio_tilegrid_construct(displayio_tilegrid_t *self, mp_obj_t bitmap,
uint16_t bitmap_width_in_tiles, uint16_t bitmap_height_in_tiles,
mp_obj_t pixel_shader, uint16_t width, uint16_t height,
- uint16_t tile_width, uint16_t tile_height, uint16_t x, uint16_t y, uint8_t default_tile) {
+ uint16_t tile_width, uint16_t tile_height, uint16_t x, uint16_t y, uint16_t default_tile) {
+
uint32_t total_tiles = width * height;
+ self->bitmap_width_in_tiles = bitmap_width_in_tiles;
+ self->tiles_in_bitmap = bitmap_width_in_tiles * bitmap_height_in_tiles;
+
+ // Determine if we need uint16_t or uint8_t for tile indices
+ bool use_uint16 = self->tiles_in_bitmap > 255;
+
// Sprites will only have one tile so save a little memory by inlining values in the pointer.
- uint8_t inline_tiles = sizeof(uint8_t *);
+ uint8_t inline_tiles = sizeof(void *) / (use_uint16 ? sizeof(uint16_t) : sizeof(uint8_t));
+
if (total_tiles <= inline_tiles) {
self->tiles = 0;
// Pack values into the pointer since there are only a few.
- for (uint32_t i = 0; i < inline_tiles; i++) {
- ((uint8_t *)&self->tiles)[i] = default_tile;
+ if (use_uint16) {
+ for (uint32_t i = 0; i < inline_tiles && i < total_tiles; i++) {
+ ((uint16_t *)&self->tiles)[i] = default_tile;
+ }
+ } else {
+ for (uint32_t i = 0; i < inline_tiles && i < total_tiles; i++) {
+ ((uint8_t *)&self->tiles)[i] = (uint8_t)default_tile;
+ }
}
self->inline_tiles = true;
} else {
- self->tiles = (uint8_t *)m_malloc(total_tiles);
- for (uint32_t i = 0; i < total_tiles; i++) {
- self->tiles[i] = default_tile;
+ if (use_uint16) {
+ uint16_t *tiles16 = (uint16_t *)m_malloc(total_tiles * sizeof(uint16_t));
+ for (uint32_t i = 0; i < total_tiles; i++) {
+ tiles16[i] = default_tile;
+ }
+ self->tiles = tiles16;
+ } else {
+ uint8_t *tiles8 = (uint8_t *)m_malloc(total_tiles);
+ for (uint32_t i = 0; i < total_tiles; i++) {
+ tiles8[i] = (uint8_t)default_tile;
+ }
+ self->tiles = tiles8;
}
self->inline_tiles = false;
}
- self->bitmap_width_in_tiles = bitmap_width_in_tiles;
- self->tiles_in_bitmap = bitmap_width_in_tiles * bitmap_height_in_tiles;
+
self->width_in_tiles = width;
self->height_in_tiles = height;
self->x = x;
@@ -234,29 +258,42 @@ uint16_t common_hal_displayio_tilegrid_get_tile_height(displayio_tilegrid_t *sel
return self->tile_height;
}
-uint8_t common_hal_displayio_tilegrid_get_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y) {
- uint8_t *tiles = self->tiles;
+uint16_t common_hal_displayio_tilegrid_get_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y) {
+ void *tiles = self->tiles;
if (self->inline_tiles) {
- tiles = (uint8_t *)&self->tiles;
+ tiles = &self->tiles;
}
if (tiles == NULL) {
return 0;
}
- return tiles[y * self->width_in_tiles + x];
+
+ uint32_t index = y * self->width_in_tiles + x;
+ if (self->tiles_in_bitmap > 255) {
+ return ((uint16_t *)tiles)[index];
+ } else {
+ return ((uint8_t *)tiles)[index];
+ }
}
-void common_hal_displayio_tilegrid_set_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y, uint8_t tile_index) {
+void common_hal_displayio_tilegrid_set_tile(displayio_tilegrid_t *self, uint16_t x, uint16_t y, uint16_t tile_index) {
if (tile_index >= self->tiles_in_bitmap) {
mp_raise_ValueError(MP_ERROR_TEXT("Tile index out of bounds"));
}
- uint8_t *tiles = self->tiles;
+
+ void *tiles = self->tiles;
if (self->inline_tiles) {
- tiles = (uint8_t *)&self->tiles;
+ tiles = &self->tiles;
}
if (tiles == NULL) {
return;
}
- tiles[y * self->width_in_tiles + x] = tile_index;
+
+ uint32_t index = y * self->width_in_tiles + x;
+ if (self->tiles_in_bitmap > 255) {
+ ((uint16_t *)tiles)[index] = tile_index;
+ } else {
+ ((uint8_t *)tiles)[index] = (uint8_t)tile_index;
+ }
displayio_area_t temp_area;
displayio_area_t *tile_area;
if (!self->partial_change) {
@@ -284,21 +321,32 @@ void common_hal_displayio_tilegrid_set_tile(displayio_tilegrid_t *self, uint16_t
self->partial_change = true;
}
-void common_hal_displayio_tilegrid_set_all_tiles(displayio_tilegrid_t *self, uint8_t tile_index) {
+void common_hal_displayio_tilegrid_set_all_tiles(displayio_tilegrid_t *self, uint16_t tile_index) {
if (tile_index >= self->tiles_in_bitmap) {
mp_raise_ValueError(MP_ERROR_TEXT("Tile index out of bounds"));
}
- uint8_t *tiles = self->tiles;
+
+ void *tiles = self->tiles;
if (self->inline_tiles) {
- tiles = (uint8_t *)&self->tiles;
+ tiles = &self->tiles;
}
if (tiles == NULL) {
return;
}
- for (uint16_t x = 0; x < self->width_in_tiles; x++) {
+ if (self->tiles_in_bitmap > 255) {
+ uint16_t *tiles16 = (uint16_t *)tiles;
for (uint16_t y = 0; y < self->height_in_tiles; y++) {
- tiles[y * self->width_in_tiles + x] = tile_index;
+ for (uint16_t x = 0; x < self->width_in_tiles; x++) {
+ tiles16[y * self->width_in_tiles + x] = tile_index;
+ }
+ }
+ } else {
+ uint8_t *tiles8 = (uint8_t *)tiles;
+ for (uint16_t y = 0; y < self->height_in_tiles; y++) {
+ for (uint16_t x = 0; x < self->width_in_tiles; x++) {
+ tiles8[y * self->width_in_tiles + x] = (uint8_t)tile_index;
+ }
}
}
@@ -368,9 +416,9 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
const _displayio_colorspace_t *colorspace, const displayio_area_t *area,
uint32_t *mask, uint32_t *buffer) {
// If no tiles are present we have no impact.
- uint8_t *tiles = self->tiles;
+ void *tiles = self->tiles;
if (self->inline_tiles) {
- tiles = (uint8_t *)&self->tiles;
+ tiles = &self->tiles;
}
if (tiles == NULL) {
return false;
@@ -474,7 +522,12 @@ bool displayio_tilegrid_fill_area(displayio_tilegrid_t *self,
uint16_t x_tile_index = (local_x / self->tile_width + self->top_left_x) % self->width_in_tiles;
uint16_t y_tile_index = (local_y / self->tile_height + self->top_left_y) % self->height_in_tiles;
uint16_t tile_location = y_tile_index * self->width_in_tiles + x_tile_index;
- input_pixel.tile = tiles[tile_location];
+
+ if (self->tiles_in_bitmap > 255) {
+ input_pixel.tile = ((uint16_t *)tiles)[tile_location];
+ } else {
+ input_pixel.tile = ((uint8_t *)tiles)[tile_location];
+ }
input_pixel.tile_x = (input_pixel.tile % self->bitmap_width_in_tiles) * self->tile_width + local_x % self->tile_width;
input_pixel.tile_y = (input_pixel.tile / self->bitmap_width_in_tiles) * self->tile_height + local_y % self->tile_height;
diff --git a/shared-module/displayio/TileGrid.h b/shared-module/displayio/TileGrid.h
index 36d2c9a69b6cb..400ade5359827 100644
--- a/shared-module/displayio/TileGrid.h
+++ b/shared-module/displayio/TileGrid.h
@@ -30,7 +30,7 @@ typedef struct {
uint16_t tile_height;
uint16_t top_left_x;
uint16_t top_left_y;
- uint8_t *tiles;
+ void *tiles; // Can be either uint8_t* or uint16_t* depending on tiles_in_bitmap
const displayio_buffer_transform_t *absolute_transform;
displayio_area_t dirty_area; // Stored as a relative area until the refresh area is fetched.
displayio_area_t previous_area; // Stored as an absolute area.
diff --git a/shared-module/lvfontio/OnDiskFont.c b/shared-module/lvfontio/OnDiskFont.c
new file mode 100644
index 0000000000000..0cf65301d2270
--- /dev/null
+++ b/shared-module/lvfontio/OnDiskFont.c
@@ -0,0 +1,843 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#include
+
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+#include "py/runtime.h"
+#include "py/mperrno.h"
+#include "py/stream.h"
+#include "py/objstr.h"
+#include "py/gc.h"
+#include "shared-bindings/displayio/Bitmap.h"
+#include "extmod/vfs_fat.h"
+#include "lib/oofatfs/ff.h"
+#include "supervisor/shared/translate/translate.h"
+#include "supervisor/port.h"
+#include "supervisor/shared/serial.h"
+#include "supervisor/filesystem.h"
+
+// Helper functions for memory allocation
+static inline void *allocate_memory(lvfontio_ondiskfont_t *self, size_t size) {
+ void *ptr;
+ if (self->use_gc_allocator) {
+ ptr = m_malloc_maybe(size);
+ } else {
+ ptr = port_malloc(size, false);
+ }
+ if (ptr != NULL) {
+ return ptr;
+ }
+ common_hal_lvfontio_ondiskfont_deinit(self);
+ if (self->use_gc_allocator) {
+ m_malloc_fail(size);
+ }
+ return NULL;
+}
+
+static inline void free_memory(lvfontio_ondiskfont_t *self, void *ptr) {
+ if (self->use_gc_allocator) {
+ m_free(ptr);
+ } else {
+ port_free(ptr);
+ }
+}
+
+// Forward declarations for helper functions
+static int16_t find_codepoint_slot(lvfontio_ondiskfont_t *self, uint32_t codepoint);
+static uint16_t find_free_slot(lvfontio_ondiskfont_t *self, uint32_t codepoint);
+static FRESULT read_bits(FIL *file, size_t num_bits, uint8_t *byte_val, uint8_t *remaining_bits, uint32_t *result);
+static FRESULT read_glyph_dimensions(FIL *file, lvfontio_ondiskfont_t *self, uint32_t *advance_width, int32_t *bbox_x, int32_t *bbox_y, uint32_t *bbox_w, uint32_t *bbox_h, uint8_t *byte_val, uint8_t *remaining_bits);
+
+// Load font header data from file
+static bool load_font_header(lvfontio_ondiskfont_t *self, FIL *file, size_t *max_slots) {
+ UINT bytes_read;
+ FRESULT res;
+
+ // Start at the beginning of the file
+ res = f_lseek(file, 0);
+ if (res != FR_OK) {
+ return false;
+ }
+
+ uint8_t buffer[8];
+ bool found_head = false;
+ bool found_cmap = false;
+ bool found_loca = false;
+ bool found_glyf = false;
+
+
+ size_t current_position = 0;
+
+ // Read sections until we find all the sections we need or reach end of file
+ while (true) {
+ // Read section size (4 bytes)
+ res = f_read(file, buffer, 4, &bytes_read);
+ if (res != FR_OK || bytes_read < 4) {
+ break; // Read error or end of file
+ }
+
+ uint32_t section_size = buffer[0] | (buffer[1] << 8) |
+ (buffer[2] << 16) | (buffer[3] << 24);
+
+ if (section_size == 0) {
+ break; // End of sections marker
+ }
+
+ // Read section marker (4 bytes)
+ res = f_read(file, buffer, 4, &bytes_read);
+ if (res != FR_OK || bytes_read < 4) {
+ break; // Read error or unexpected end of file
+ }
+
+
+ // Make a null-terminated copy of the section marker for debug printing
+ char section_marker[5] = {0};
+ memcpy(section_marker, buffer, 4);
+
+ // Process different section types
+ if (memcmp(buffer, "head", 4) == 0) {
+ // Read head section data (35 bytes)
+ uint8_t head_buf[35];
+ res = f_read(file, head_buf, 35, &bytes_read);
+ if (res != FR_OK || bytes_read < 35) {
+ break;
+ }
+
+ // Skip version (4 bytes) and padding (1 byte)
+ // Parse font metrics at offset 6
+ self->header.font_size = head_buf[6] | (head_buf[7] << 8);
+ self->header.ascent = head_buf[8] | (head_buf[9] << 8);
+ self->header.default_advance_width = head_buf[22] | (head_buf[23] << 8);
+
+ // Parse format information
+ self->header.index_to_loc_format = head_buf[26];
+ self->header.bits_per_pixel = head_buf[29];
+ self->header.glyph_bbox_xy_bits = head_buf[30];
+ self->header.glyph_bbox_wh_bits = head_buf[31];
+ self->header.glyph_advance_bits = head_buf[32];
+
+ // Calculate derived values
+ self->header.glyph_header_bits = self->header.glyph_advance_bits +
+ 2 * self->header.glyph_bbox_xy_bits +
+ 2 * self->header.glyph_bbox_wh_bits;
+ self->header.glyph_header_bytes = (self->header.glyph_header_bits + 7) / 8;
+
+ found_head = true;
+ } else if (memcmp(buffer, "cmap", 4) == 0) {
+ // Read subtable count
+ uint8_t cmap_header[4];
+ res = f_read(file, cmap_header, 4, &bytes_read);
+ if (res != FR_OK || bytes_read < 4) {
+ break;
+ }
+
+ uint32_t subtable_count = cmap_header[0] | (cmap_header[1] << 8) |
+ (cmap_header[2] << 16) | (cmap_header[3] << 24);
+
+ // Allocate memory for cmap ranges
+ self->cmap_range_count = subtable_count;
+ self->cmap_ranges = allocate_memory(self, sizeof(lvfontio_cmap_range_t) * subtable_count);
+ if (self->cmap_ranges == NULL) {
+ return false;
+ }
+
+ // Read each subtable
+ for (uint16_t i = 0; i < subtable_count; i++) {
+ uint8_t subtable_buf[16];
+ res = f_read(file, subtable_buf, 16, &bytes_read);
+ if (res != FR_OK || bytes_read < 16) {
+ break;
+ }
+
+ // Read data_offset (4 bytes)
+ uint32_t data_offset = subtable_buf[0] | (subtable_buf[1] << 8) |
+ (subtable_buf[2] << 16) | (subtable_buf[3] << 24);
+
+ // Read range_start, range_length, glyph_offset
+ uint32_t range_start = subtable_buf[4] | (subtable_buf[5] << 8) |
+ (subtable_buf[6] << 16) | (subtable_buf[7] << 24);
+ uint16_t range_length = subtable_buf[8] | (subtable_buf[9] << 8);
+ uint16_t glyph_offset = subtable_buf[10] | (subtable_buf[11] << 8);
+ uint16_t entries_count = subtable_buf[12] | (subtable_buf[13] << 8);
+
+ // Get format type (0=sparse mapping, 1=range mapping, 2=range to range, 3=direct mapping)
+ uint8_t format_type = subtable_buf[14];
+ // Check for supported format types (0, 2, and 3)
+ if (format_type != 0 && format_type != 2 && format_type != 3) {
+ continue;
+ }
+
+ // Store the range information
+ self->cmap_ranges[i].range_start = range_start;
+ self->cmap_ranges[i].range_end = range_start + range_length;
+ self->cmap_ranges[i].glyph_offset = glyph_offset;
+ self->cmap_ranges[i].format_type = format_type;
+ self->cmap_ranges[i].data_offset = current_position + data_offset;
+ self->cmap_ranges[i].entries_count = entries_count;
+ }
+
+ found_cmap = true;
+ } else if (memcmp(buffer, "loca", 4) == 0) {
+ // Read max_cid
+ uint8_t loca_header[4];
+ res = f_read(file, loca_header, 4, &bytes_read);
+ if (res != FR_OK || bytes_read < 4) {
+ break;
+ }
+
+ // Store max_cid value
+ self->max_cid = loca_header[0] | (loca_header[1] << 8) |
+ (loca_header[2] << 16) | (loca_header[3] << 24);
+
+ // Store location of the loca table offset data
+ self->loca_table_offset = current_position + 12;
+
+ found_loca = true;
+ } else if (memcmp(buffer, "glyf", 4) == 0) {
+ // Store start of glyf table
+ self->glyf_table_offset = current_position;
+ size_t advances[2] = {0, 0};
+ size_t advance_count[2] = {0, 0};
+
+ if (self->header.default_advance_width != 0) {
+ advances[0] = self->header.default_advance_width;
+ }
+
+ // Set the default advance width based on the first character in the
+ // file.
+ size_t cid = 0;
+ while (cid < self->max_cid - 1) {
+ // Read glyph header fields
+ uint32_t glyph_advance;
+ int32_t bbox_x, bbox_y;
+ uint32_t bbox_w, bbox_h;
+
+ uint8_t byte_val = 0;
+ uint8_t remaining_bits = 0;
+
+ // Use the helper function to read glyph dimensions
+ read_glyph_dimensions(file, self, &glyph_advance, &bbox_x, &bbox_y, &bbox_w, &bbox_h, &byte_val, &remaining_bits);
+
+ // Throw away the bitmap bits.
+ read_bits(file, self->header.bits_per_pixel * bbox_w * bbox_h, &byte_val, &remaining_bits, NULL);
+ if (advances[0] == glyph_advance) {
+ advance_count[0]++;
+ } else if (advances[1] == glyph_advance) {
+ advance_count[1]++;
+ } else if (advance_count[0] == 0) {
+ advances[0] = glyph_advance;
+ advance_count[0] = 1;
+ } else if (advance_count[1] == 0) {
+ advances[1] = glyph_advance;
+ advance_count[1] = 1;
+ } else {
+ break;
+ }
+ cid++;
+ }
+
+ if (self->header.default_advance_width == 0) {
+ if (advance_count[1] == 0) {
+ self->header.default_advance_width = advances[0];
+ *max_slots = advance_count[0];
+ } else {
+ if (advances[0] > advances[1]) {
+ self->header.default_advance_width = advances[0] / 2;
+ *max_slots = advance_count[0] * 2 + advance_count[1];
+ } else {
+ self->header.default_advance_width = advances[1] / 2;
+ *max_slots = advance_count[1] * 2 + advance_count[0];
+ }
+ }
+ }
+
+
+ found_glyf = true;
+ }
+
+ current_position += section_size;
+
+ // Skip to the end of the section
+ res = f_lseek(file, current_position);
+ if (res != FR_OK) {
+ break;
+ }
+
+ // If we found all needed sections, we can stop
+ if (found_head && found_cmap && found_loca && found_glyf) {
+ break;
+ }
+ }
+
+ // Check if we found all required sections
+ if (!found_head || !found_cmap || !found_loca || !found_glyf) {
+ return false;
+ }
+
+ return true;
+}
+
+// Get character ID (glyph index) for a codepoint
+static int32_t get_char_id(lvfontio_ondiskfont_t *self, uint32_t codepoint) {
+ // Find codepoint in cmap ranges
+ for (uint16_t i = 0; i < self->cmap_range_count; i++) {
+ // Check if codepoint is in range for this subtable
+ if (codepoint >= self->cmap_ranges[i].range_start &&
+ codepoint < self->cmap_ranges[i].range_end) {
+
+ // Handle according to format type
+ switch (self->cmap_ranges[i].format_type) {
+ case 0: { // Sparse mapping - need to look up in a sparse table
+ if (!self->file_is_open) {
+ return -1;
+ }
+
+ // Calculate the relative position within the range
+ uint32_t idx = codepoint - self->cmap_ranges[i].range_start;
+
+ if (idx >= self->cmap_ranges[i].entries_count) {
+ return -1;
+ }
+
+ // Calculate the absolute data position in the file
+ uint32_t data_pos = self->cmap_ranges[i].data_offset + idx; // 1 byte per entry
+ FRESULT res = f_lseek(&self->file, data_pos);
+ if (res != FR_OK) {
+ return -1;
+ }
+
+ // Read the glyph ID (1 byte)
+ uint8_t glyph_id;
+ UINT bytes_read;
+ res = f_read(&self->file, &glyph_id, 1, &bytes_read);
+
+ if (res != FR_OK || bytes_read < 1) {
+ return -1;
+ }
+
+
+ return self->cmap_ranges[i].glyph_offset + glyph_id;
+ }
+
+ case 2: // Range to range - calculate based on offset within range
+ uint16_t idx = codepoint - self->cmap_ranges[i].range_start;
+ uint16_t glyph_id = self->cmap_ranges[i].glyph_offset + idx;
+ return glyph_id;
+
+ case 3: { // Direct mapping - need to look up in the table
+ if (!self->file_is_open) {
+ return -1;
+ }
+
+ FRESULT res;
+ res = f_lseek(&self->file, self->cmap_ranges[i].data_offset);
+ if (res != FR_OK) {
+ return -1;
+ }
+ uint16_t codepoint_delta = codepoint - self->cmap_ranges[i].range_start;
+
+ for (size_t j = 0; j < self->cmap_ranges[i].entries_count; j++) {
+ // Read code point at the index
+ uint16_t candidate_codepoint_delta;
+ res = f_read(&self->file, &candidate_codepoint_delta, 2, NULL);
+ if (res != FR_OK) {
+ return -1;
+ }
+
+ if (candidate_codepoint_delta == codepoint_delta) {
+ return self->cmap_ranges[i].glyph_offset + j;
+ }
+ }
+ return -1;
+ }
+
+ default:
+ return -1;
+ }
+ }
+ }
+
+ return -1; // Not found
+}
+
+// Load glyph bitmap data into a slot
+// This function assumes the file is already open and positioned after reading the glyph dimensions
+static bool load_glyph_bitmap(FIL *file, lvfontio_ondiskfont_t *self, uint32_t codepoint, uint16_t slot,
+ uint32_t glyph_advance, int32_t bbox_x, int32_t bbox_y, uint32_t bbox_w, uint32_t bbox_h,
+ uint8_t *byte_val, uint8_t *remaining_bits) {
+ // Store codepoint at slot
+ self->codepoints[slot] = codepoint;
+ self->reference_counts[slot] = 1;
+
+ // Read bitmap data pixel by pixel
+ uint16_t x_offset = slot * self->header.default_advance_width;
+ uint16_t y_offset = self->header.ascent - bbox_y - bbox_h;
+ for (uint16_t y = 0; y < bbox_h; y++) {
+ for (uint16_t x = 0; x < bbox_w; x++) {
+ uint32_t pixel_value;
+ FRESULT res = read_bits(file, self->header.bits_per_pixel, byte_val, remaining_bits, &pixel_value);
+ if (res != FR_OK) {
+ return false;
+ }
+
+ // Adjust for bbox position within the glyph bounding box
+ int16_t bitmap_x = x_offset + x + bbox_x;
+ int16_t bitmap_y = y_offset + y;
+
+ // Make sure we're in bounds
+ if (bitmap_x >= 0 &&
+ bitmap_x < self->header.default_advance_width * self->max_glyphs &&
+ bitmap_y >= 0 &&
+ bitmap_y < self->header.font_size) {
+ common_hal_displayio_bitmap_set_pixel(
+ self->bitmap,
+ bitmap_x,
+ bitmap_y,
+ pixel_value
+ );
+ }
+ }
+ }
+
+ return true;
+}
+
+// Constructor
+void common_hal_lvfontio_ondiskfont_construct(lvfontio_ondiskfont_t *self,
+ const char *file_path,
+ uint16_t max_glyphs,
+ bool use_gc_allocator) {
+
+ // Store the allocation mode
+ self->use_gc_allocator = use_gc_allocator;
+ // Store parameters
+ self->file_path = file_path; // Store the provided path string directly
+ self->max_glyphs = max_glyphs;
+ self->cmap_ranges = NULL;
+ self->file_is_open = false;
+
+ // Determine which filesystem to use based on the path
+ const char *path_under_mount;
+ fs_user_mount_t *vfs = filesystem_for_path(file_path, &path_under_mount);
+
+ if (vfs == NULL) {
+ if (self->use_gc_allocator) {
+ mp_raise_ValueError(MP_ERROR_TEXT("File not found"));
+ }
+ return;
+ }
+
+ // Open the file and keep it open for the lifetime of the object
+ FRESULT res = f_open(&vfs->fatfs, &self->file, path_under_mount, FA_READ);
+
+ if (res != FR_OK) {
+ if (self->use_gc_allocator) {
+ mp_raise_ValueError(MP_ERROR_TEXT("File not found"));
+ }
+ return;
+ }
+
+ self->file_is_open = true;
+
+ // Load font headers
+ size_t max_slots;
+ if (!load_font_header(self, &self->file, &max_slots)) {
+ f_close(&self->file);
+ self->file_is_open = false;
+ if (self->use_gc_allocator) {
+ mp_raise_ValueError_varg(MP_ERROR_TEXT("Invalid %q"), MP_QSTR_file);
+ }
+ return;
+ }
+ // Cap the number of slots to the number of slots needed by the font. That way
+ // small font files don't need a bunch of extra cache space.
+ max_glyphs = MIN(max_glyphs, max_slots);
+
+ // Allocate codepoints array. allocate_memory will raise an exception if
+ // allocation fails and the VM is active.
+ self->codepoints = allocate_memory(self, sizeof(uint32_t) * max_glyphs);
+ if (self->codepoints == NULL) {
+ return;
+ }
+
+ // Initialize codepoints to invalid
+ for (uint16_t i = 0; i < max_glyphs; i++) {
+ self->codepoints[i] = LVFONTIO_INVALID_CODEPOINT;
+ }
+
+ // Allocate reference counts
+ self->reference_counts = allocate_memory(self, sizeof(uint16_t) * max_glyphs);
+ if (self->reference_counts == NULL) {
+ return;
+ }
+
+ // Initialize reference counts to 0
+ memset(self->reference_counts, 0, sizeof(uint16_t) * max_glyphs);
+
+ self->half_width_px = self->header.default_advance_width;
+
+ // Create bitmap for glyph cache
+ displayio_bitmap_t *bitmap = allocate_memory(self, sizeof(displayio_bitmap_t));
+ bitmap->base.type = &displayio_bitmap_type;
+ if (bitmap == NULL) {
+ return;
+ }
+
+ // Calculate bitmap stride
+ uint32_t bits_per_pixel = 1 << self->header.bits_per_pixel;
+ uint32_t width = self->header.default_advance_width * max_glyphs;
+ uint32_t row_width = width * bits_per_pixel;
+ uint16_t stride = (row_width + 31) / 32; // Align to uint32_t (32 bits)
+
+ // Allocate buffer for bitmap data
+ uint32_t buffer_size = stride * self->header.font_size * sizeof(uint32_t);
+ uint32_t *bitmap_buffer = allocate_memory(self, buffer_size);
+ if (bitmap_buffer == NULL) {
+ return;
+ }
+
+ // Zero out bitmap buffer
+ memset(bitmap_buffer, 0, buffer_size);
+
+ // Construct bitmap with allocated buffer
+ common_hal_displayio_bitmap_construct_from_buffer(bitmap,
+ self->header.default_advance_width * max_glyphs,
+ self->header.font_size,
+ 1 << self->header.bits_per_pixel,
+ bitmap_buffer,
+ false);
+ self->bitmap = bitmap;
+}
+
+void common_hal_lvfontio_ondiskfont_deinit(lvfontio_ondiskfont_t *self) {
+ if (!self->file_is_open) {
+ return;
+ }
+
+ if (self->bitmap != NULL) {
+ common_hal_displayio_bitmap_deinit(self->bitmap);
+ self->bitmap = NULL;
+ }
+
+ if (self->codepoints != NULL) {
+ free_memory(self, self->codepoints);
+ self->codepoints = NULL;
+ }
+
+ if (self->reference_counts != NULL) {
+ free_memory(self, self->reference_counts);
+ self->reference_counts = NULL;
+ }
+
+
+
+ if (self->cmap_ranges != NULL) {
+ free_memory(self, self->cmap_ranges);
+ self->cmap_ranges = NULL;
+ }
+
+ f_close(&self->file);
+ self->file_is_open = false;
+}
+
+bool common_hal_lvfontio_ondiskfont_deinited(lvfontio_ondiskfont_t *self) {
+ return !self->file_is_open;
+}
+
+mp_obj_t common_hal_lvfontio_ondiskfont_get_bitmap(const lvfontio_ondiskfont_t *self) {
+ return MP_OBJ_FROM_PTR(self->bitmap);
+}
+
+mp_obj_t common_hal_lvfontio_ondiskfont_get_bounding_box(const lvfontio_ondiskfont_t *self) {
+ mp_obj_t bbox[2];
+ bbox[0] = MP_OBJ_NEW_SMALL_INT(self->header.default_advance_width);
+ bbox[1] = MP_OBJ_NEW_SMALL_INT(self->header.font_size);
+ return mp_obj_new_tuple(2, bbox);
+}
+
+void common_hal_lvfontio_ondiskfont_get_dimensions(const lvfontio_ondiskfont_t *self,
+ uint16_t *width, uint16_t *height) {
+ if (width != NULL) {
+ *width = self->header.default_advance_width;
+ }
+ if (height != NULL) {
+ *height = self->header.font_size;
+ }
+}
+
+int16_t common_hal_lvfontio_ondiskfont_cache_glyph(lvfontio_ondiskfont_t *self, uint32_t codepoint, bool *is_full_width) {
+ // Check if already cached
+ int16_t existing_slot = find_codepoint_slot(self, codepoint);
+ if (existing_slot >= 0) {
+ // Glyph is already cached, increment reference count
+ self->reference_counts[existing_slot]++;
+
+ // Check if this is a full-width character by looking for a second slot
+ // with the same codepoint right after this one
+ if (is_full_width != NULL) {
+ if (existing_slot + 1 < self->max_glyphs &&
+ self->codepoints[existing_slot + 1] == codepoint) {
+ *is_full_width = true;
+ } else {
+ *is_full_width = false;
+ }
+ }
+
+ return existing_slot;
+ }
+
+ // First check if the glyph is full-width before allocating slots
+ // This way we know if we need one or two slots before committing
+ bool is_full_width_glyph = false;
+
+ // Check if file is already open
+ if (!self->file_is_open) {
+
+ return -1;
+ }
+
+ // Find character ID from codepoint
+ int32_t char_id = get_char_id(self, codepoint);
+ if (char_id < 0 || (uint32_t)char_id >= self->max_cid) {
+ return -1; // Invalid character
+ }
+
+ // Get glyph offset from location table
+ uint32_t glyph_offset = 0;
+ uint32_t loca_offset = self->loca_table_offset + char_id *
+ (self->header.index_to_loc_format == 1 ? 4 : 2);
+
+ FRESULT res = f_lseek(&self->file, loca_offset);
+ if (res != FR_OK) {
+ return -1;
+ }
+
+ UINT bytes_read;
+ if (self->header.index_to_loc_format == 1) {
+ // 4-byte offset
+ uint8_t offset_buf[4];
+ res = f_read(&self->file, offset_buf, 4, &bytes_read);
+ if (res != FR_OK || bytes_read < 4) {
+ return -1;
+ }
+ glyph_offset = offset_buf[0] | (offset_buf[1] << 8) |
+ (offset_buf[2] << 16) | (offset_buf[3] << 24);
+ } else {
+ // 2-byte offset
+ uint8_t offset_buf[2];
+ res = f_read(&self->file, offset_buf, 2, &bytes_read);
+ if (res != FR_OK || bytes_read < 2) {
+ return -1;
+ }
+ glyph_offset = offset_buf[0] | (offset_buf[1] << 8);
+ }
+ // Seek to glyph data
+ res = f_lseek(&self->file, self->glyf_table_offset + glyph_offset);
+ if (res != FR_OK) {
+ return -1;
+ }
+
+ // Read glyph header fields to determine width
+ uint32_t glyph_advance;
+ int32_t bbox_x, bbox_y;
+ uint32_t bbox_w, bbox_h;
+
+ // Initialize bit reading state
+ uint8_t byte_val = 0;
+ uint8_t remaining_bits = 0;
+
+ // Use the helper function to read glyph dimensions
+ res = read_glyph_dimensions(&self->file, self, &glyph_advance, &bbox_x, &bbox_y, &bbox_w, &bbox_h, &byte_val, &remaining_bits);
+ if (res != FR_OK) {
+ return -1;
+ }
+
+ // Check if the glyph is full-width based on its advance width
+ // Full-width characters typically have an advance width close to or greater than the font height
+ is_full_width_glyph = glyph_advance > self->half_width_px;
+
+ // Now we know if we need one or two slots
+ uint16_t slots_needed = is_full_width_glyph ? 2 : 1;
+
+ // Find an appropriate slot (or consecutive slots for full-width)
+ uint16_t slot = UINT16_MAX;
+
+ if (slots_needed == 1) {
+ // For regular width, find a free slot starting at codepoint's position
+ slot = find_free_slot(self, codepoint);
+ } else {
+ // For full-width, find two consecutive free slots
+ for (uint16_t i = 0; i < self->max_glyphs - 1; i++) {
+ if (self->codepoints[i] == LVFONTIO_INVALID_CODEPOINT &&
+ self->reference_counts[i] == 0 &&
+ self->codepoints[i + 1] == LVFONTIO_INVALID_CODEPOINT &&
+ self->reference_counts[i + 1] == 0) {
+ slot = i;
+ break;
+ }
+ }
+ }
+
+ // Check if we found appropriate slot(s)
+ if (slot == UINT16_MAX) {
+ return -1; // No slots available
+ }
+
+ // Load glyph into the slot
+ if (!load_glyph_bitmap(&self->file, self, codepoint, slot, glyph_advance,
+ bbox_x, bbox_y, bbox_w, bbox_h, &byte_val, &remaining_bits)) {
+ return -1; // Failed to load glyph
+ }
+
+ // For full-width characters, mark both slots with the same codepoint
+ if (is_full_width_glyph && slot + 1 < self->max_glyphs) {
+ self->codepoints[slot + 1] = codepoint;
+ self->reference_counts[slot + 1] = 1;
+ }
+
+ if (is_full_width != NULL) {
+ *is_full_width = is_full_width_glyph;
+ }
+
+ return slot;
+}
+
+void common_hal_lvfontio_ondiskfont_release_glyph(lvfontio_ondiskfont_t *self, uint32_t slot) {
+ if (slot >= self->max_glyphs) {
+ return;
+ }
+
+ if (self->reference_counts[slot] > 0) {
+ self->reference_counts[slot]--;
+ }
+}
+
+static int16_t find_codepoint_slot(lvfontio_ondiskfont_t *self, uint32_t codepoint) {
+ size_t offset = codepoint % self->max_glyphs;
+ for (uint16_t i = 0; i < self->max_glyphs; i++) {
+ int16_t slot = (i + offset) % self->max_glyphs;
+ if (self->codepoints[slot] == codepoint) {
+ return slot;
+ }
+ }
+ return -1;
+}
+
+static uint16_t find_free_slot(lvfontio_ondiskfont_t *self, uint32_t codepoint) {
+ size_t offset = codepoint % self->max_glyphs;
+
+ // First look for completely unused slots, starting at the offset
+ for (uint16_t i = 0; i < self->max_glyphs; i++) {
+ int16_t slot = (i + offset) % self->max_glyphs;
+ if (self->codepoints[slot] == LVFONTIO_INVALID_CODEPOINT && self->reference_counts[slot] == 0) {
+ return slot;
+ }
+ }
+
+ // If none found, look for slots with zero reference count, starting at the offset
+ for (uint16_t i = 0; i < self->max_glyphs; i++) {
+ int16_t slot = (i + offset) % self->max_glyphs;
+ if (self->reference_counts[slot] == 0) {
+ return slot;
+ }
+ }
+
+ // No slots available
+ return UINT16_MAX;
+}
+
+static FRESULT read_glyph_dimensions(FIL *file, lvfontio_ondiskfont_t *self,
+ uint32_t *advance_width, int32_t *bbox_x, int32_t *bbox_y,
+ uint32_t *bbox_w, uint32_t *bbox_h,
+ uint8_t *byte_val, uint8_t *remaining_bits) {
+ FRESULT res;
+ uint32_t temp_value;
+
+ // Read glyph_advance
+ res = read_bits(file, self->header.glyph_advance_bits, byte_val, remaining_bits, &temp_value);
+ if (res != FR_OK) {
+ return res;
+ }
+ *advance_width = temp_value;
+
+ // Read bbox_x (signed)
+ res = read_bits(file, self->header.glyph_bbox_xy_bits, byte_val, remaining_bits, &temp_value);
+ if (res != FR_OK) {
+ return res;
+ }
+ // Convert to signed value if needed
+ if (temp_value & (1 << (self->header.glyph_bbox_xy_bits - 1))) {
+ *bbox_x = temp_value - (1 << self->header.glyph_bbox_xy_bits);
+ } else {
+ *bbox_x = temp_value;
+ }
+
+ // Read bbox_y (signed)
+ res = read_bits(file, self->header.glyph_bbox_xy_bits, byte_val, remaining_bits, &temp_value);
+ if (res != FR_OK) {
+ return res;
+ }
+ // Convert to signed value if needed
+ if (temp_value & (1 << (self->header.glyph_bbox_xy_bits - 1))) {
+ *bbox_y = temp_value - (1 << self->header.glyph_bbox_xy_bits);
+ } else {
+ *bbox_y = temp_value;
+ }
+
+ // Read bbox_w
+ res = read_bits(file, self->header.glyph_bbox_wh_bits, byte_val, remaining_bits, &temp_value);
+ if (res != FR_OK) {
+ return res;
+ }
+ *bbox_w = temp_value;
+
+ // Read bbox_h
+ res = read_bits(file, self->header.glyph_bbox_wh_bits, byte_val, remaining_bits, &temp_value);
+ if (res != FR_OK) {
+ return res;
+ }
+ *bbox_h = temp_value;
+
+ return FR_OK;
+}
+
+static FRESULT read_bits(FIL *file, size_t num_bits, uint8_t *byte_val, uint8_t *remaining_bits, uint32_t *result) {
+ FRESULT res = FR_OK;
+ UINT bytes_read;
+
+ uint32_t value = 0;
+ // Bits will be lost when num_bits > 32. However, this is good for skipping bits.
+ size_t bits_needed = num_bits;
+
+ while (bits_needed > 0) {
+ // If no bits remaining, read a new byte
+ if (*remaining_bits == 0) {
+ res = f_read(file, byte_val, 1, &bytes_read);
+ if (res != FR_OK || bytes_read < 1) {
+ return FR_DISK_ERR;
+ }
+ *remaining_bits = 8;
+ }
+
+ // Calculate how many bits to take from current byte
+ uint8_t bits_to_take = (*remaining_bits < bits_needed) ? *remaining_bits : bits_needed;
+ value = (value << bits_to_take) | (*byte_val >> (8 - bits_to_take));
+
+ // Update state
+ *remaining_bits -= bits_to_take;
+ bits_needed -= bits_to_take;
+
+ // Shift byte for next read
+ *byte_val <<= bits_to_take;
+ *byte_val &= 0xFF;
+ }
+
+ if (result != NULL) {
+ *result = value;
+ }
+ return FR_OK;
+}
diff --git a/shared-module/lvfontio/OnDiskFont.h b/shared-module/lvfontio/OnDiskFont.h
new file mode 100644
index 0000000000000..db68126e9f459
--- /dev/null
+++ b/shared-module/lvfontio/OnDiskFont.h
@@ -0,0 +1,75 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "py/obj.h"
+#include "shared-module/displayio/Bitmap.h"
+
+#include "lib/oofatfs/ff.h"
+
+#define LVFONTIO_INVALID_CODEPOINT 0xFFFFFFFF
+
+// LV Font header information
+typedef struct {
+ // Font size and metrics
+ uint16_t font_size;
+ uint16_t ascent;
+ uint16_t default_advance_width;
+
+ // Encoding formats
+ uint8_t index_to_loc_format;
+ uint8_t bits_per_pixel;
+ uint8_t glyph_bbox_xy_bits;
+ uint8_t glyph_bbox_wh_bits;
+ uint8_t glyph_advance_bits;
+
+ // Calculated values
+ uint8_t glyph_header_bits;
+ uint8_t glyph_header_bytes;
+} lvfontio_header_t;
+
+// Mapping of codepoint ranges to glyph IDs
+typedef struct {
+ uint32_t range_start; // Start of codepoint range
+ uint32_t range_end; // End of codepoint range (exclusive)
+ uint16_t glyph_offset; // Offset to apply to codepoint
+ uint8_t format_type; // Format type: 0=sparse mapping, 2=range to range, 3=direct mapping
+ uint16_t entries_count; // Number of entries in sparse data
+ uint32_t data_offset; // File offset to the cmap data
+} lvfontio_cmap_range_t;
+
+typedef struct {
+ mp_obj_base_t base;
+ // Bitmap containing cached glyphs
+ displayio_bitmap_t *bitmap;
+ // Source of font file path (either a const char* or a copied string)
+ const char *file_path;
+ // Array mapping glyph indices to codepoints
+ uint32_t *codepoints;
+ // Array of reference counts for each glyph slot
+ uint16_t *reference_counts; // Use uint16_t to handle higher reference counts
+ // Maximum number of glyphs to cache at once
+ uint16_t max_glyphs;
+ // Flag indicating whether to use m_malloc (true) or port_malloc (false)
+ bool use_gc_allocator;
+ uint8_t half_width_px;
+
+ FIL file;
+ bool file_is_open;
+
+ // Font metrics information loaded from file
+ lvfontio_header_t header;
+
+ // CMAP information
+ lvfontio_cmap_range_t *cmap_ranges;
+ uint16_t cmap_range_count;
+
+ // Offsets for tables in the file
+ uint32_t loca_table_offset;
+ uint32_t glyf_table_offset;
+ uint32_t max_cid;
+} lvfontio_ondiskfont_t;
diff --git a/shared-module/lvfontio/__init__.c b/shared-module/lvfontio/__init__.c
new file mode 100644
index 0000000000000..d0eee6d0a521a
--- /dev/null
+++ b/shared-module/lvfontio/__init__.c
@@ -0,0 +1,5 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: Copyright (c) 2024 Scott Shawcroft for Adafruit Industries
+//
+// SPDX-License-Identifier: MIT
diff --git a/shared-module/terminalio/Terminal.c b/shared-module/terminalio/Terminal.c
index 2828d0d3800c5..02bcffb30c5f2 100644
--- a/shared-module/terminalio/Terminal.c
+++ b/shared-module/terminalio/Terminal.c
@@ -10,20 +10,150 @@
#include "shared-bindings/displayio/TileGrid.h"
#include "shared-bindings/displayio/Palette.h"
#include "shared-bindings/terminalio/Terminal.h"
+#include "shared-bindings/fontio/BuiltinFont.h"
+#if CIRCUITPY_LVFONTIO
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+#endif
#if CIRCUITPY_STATUS_BAR
#include "shared-bindings/supervisor/__init__.h"
#include "shared-bindings/supervisor/StatusBar.h"
#endif
+#include "supervisor/shared/serial.h"
+
+uint16_t terminalio_terminal_get_glyph_index(mp_obj_t font, mp_uint_t codepoint, bool *is_full_width) {
+ if (is_full_width != NULL) {
+ *is_full_width = false; // Default to not full width
+ }
+
+ #if CIRCUITPY_LVFONTIO
+ if (mp_obj_is_type(font, &lvfontio_ondiskfont_type)) {
+ // For LV fonts, we need to cache the glyph first
+ lvfontio_ondiskfont_t *lv_font = MP_OBJ_TO_PTR(font);
+ bool full_width = false;
+ int16_t slot = common_hal_lvfontio_ondiskfont_cache_glyph(lv_font, codepoint, &full_width);
+
+ if (is_full_width != NULL) {
+ *is_full_width = full_width;
+ }
+
+ if (slot == -1) {
+ // Not found or couldn't cache
+ return 0xffff;
+ }
+ return (uint16_t)slot;
+ }
+ #endif
+
+ #if CIRCUITPY_FONTIO
+ if (mp_obj_is_type(font, &fontio_builtinfont_type)) {
+ // Use the standard fontio function
+ fontio_builtinfont_t *fontio_font = MP_OBJ_TO_PTR(font);
+ uint8_t index = fontio_builtinfont_get_glyph_index(fontio_font, codepoint);
+ if (index == 0xff) {
+ return 0xffff;
+ }
+ return index;
+ }
+ #endif
+
+ // Unsupported font type
+ return 0xffff;
+}
+
+static void wrap_cursor(uint16_t width, uint16_t height, uint16_t *cursor_x, uint16_t *cursor_y) {
+ if (*cursor_x >= width) {
+ *cursor_y = *cursor_y + 1;
+ *cursor_x %= width;
+ }
+ if (*cursor_y >= height) {
+ *cursor_y %= height;
+ }
+}
+
+static void release_current_glyph(displayio_tilegrid_t *tilegrid, mp_obj_t font, uint16_t x, uint16_t y) {
+ #if CIRCUITPY_LVFONTIO
+ if (!mp_obj_is_type(font, &lvfontio_ondiskfont_type)) {
+ return;
+ }
+ uint16_t current_tile = common_hal_displayio_tilegrid_get_tile(tilegrid, x, y);
+ if (current_tile == 0) {
+ }
+ common_hal_lvfontio_ondiskfont_release_glyph(MP_OBJ_TO_PTR(font), current_tile);
+ #endif
+}
+
+static void terminalio_terminal_set_tile(terminalio_terminal_obj_t *self, bool status_bar, mp_uint_t character, bool release_glyphs) {
+ displayio_tilegrid_t *tilegrid = self->scroll_area;
+ uint16_t *x = &self->cursor_x;
+ uint16_t *y = &self->cursor_y;
+ uint16_t w = self->scroll_area->width_in_tiles;
+ uint16_t h = self->scroll_area->height_in_tiles;
+ if (status_bar) {
+ tilegrid = self->status_bar;
+ x = &self->status_x;
+ y = &self->status_y;
+ w = self->status_bar->width_in_tiles;
+ h = self->status_bar->height_in_tiles;
+ }
+ if (release_glyphs) {
+ release_current_glyph(tilegrid, self->font, *x, *y);
+ }
+ bool is_full_width;
+ uint16_t new_tile = terminalio_terminal_get_glyph_index(self->font, character, &is_full_width);
+ if (new_tile == 0xffff) {
+ // Missing glyph.
+ return;
+ }
+ // If there is only half width left, then fill it with a space and wrap to the next line.
+ if (is_full_width && *x == w - 1) {
+ uint16_t space = terminalio_terminal_get_glyph_index(self->font, ' ', NULL);
+ common_hal_displayio_tilegrid_set_tile(tilegrid, *x, *y, space);
+ *x = *x + 1;
+ wrap_cursor(w, h, x, y);
+ if (release_glyphs) {
+ release_current_glyph(tilegrid, self->font, *x, *y);
+ }
+ }
+ common_hal_displayio_tilegrid_set_tile(tilegrid, *x, *y, new_tile);
+ *x = *x + 1;
+ wrap_cursor(w, h, x, y);
+ if (is_full_width) {
+ if (release_glyphs) {
+ release_current_glyph(tilegrid, self->font, *x, *y);
+ }
+ common_hal_displayio_tilegrid_set_tile(tilegrid, *x, *y, new_tile + 1);
+ *x = *x + 1;
+ wrap_cursor(w, h, x, y);
+ }
+}
+
+// Helper function to set all tiles in a tilegrid with optional glyph release
+static void terminalio_terminal_set_all_tiles(terminalio_terminal_obj_t *self, bool status_bar, mp_uint_t character, bool release_glyphs) {
+ uint16_t *x = &self->cursor_x;
+ uint16_t *y = &self->cursor_y;
+ if (status_bar) {
+ x = &self->status_x;
+ y = &self->status_y;
+ }
+ *x = 0;
+ *y = 0;
+ terminalio_terminal_set_tile(self, status_bar, character, release_glyphs);
+ while (*x != 0 || *y != 0) {
+ terminalio_terminal_set_tile(self, status_bar, character, release_glyphs);
+ }
+}
+
void terminalio_terminal_clear_status_bar(terminalio_terminal_obj_t *self) {
if (self->status_bar) {
- common_hal_displayio_tilegrid_set_all_tiles(self->status_bar, 0);
+ terminalio_terminal_set_all_tiles(self, true, ' ', true);
}
}
+
void common_hal_terminalio_terminal_construct(terminalio_terminal_obj_t *self,
- displayio_tilegrid_t *scroll_area, const fontio_builtinfont_t *font,
+ displayio_tilegrid_t *scroll_area, mp_obj_t font,
displayio_tilegrid_t *status_bar) {
self->cursor_x = 0;
self->cursor_y = 0;
@@ -35,9 +165,9 @@ void common_hal_terminalio_terminal_construct(terminalio_terminal_obj_t *self,
self->first_row = 0;
self->vt_scroll_top = 0;
self->vt_scroll_end = self->scroll_area->height_in_tiles - 1;
- common_hal_displayio_tilegrid_set_all_tiles(self->scroll_area, 0);
+ terminalio_terminal_set_all_tiles(self, false, ' ', false);
if (self->status_bar) {
- common_hal_displayio_tilegrid_set_all_tiles(self->status_bar, 0);
+ terminalio_terminal_set_all_tiles(self, true, ' ', false);
}
common_hal_displayio_tilegrid_set_top_left(self->scroll_area, 0, 1);
@@ -85,29 +215,16 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
self->osc_command == 0 &&
self->status_bar != NULL &&
self->status_y < self->status_bar->height_in_tiles) {
- uint8_t tile_index = fontio_builtinfont_get_glyph_index(self->font, c);
- if (tile_index != 0xff) {
- // Clear the tile grid before we start putting new info.
- if (self->status_x == 0 && self->status_y == 0) {
- common_hal_displayio_tilegrid_set_all_tiles(self->status_bar, 0);
- }
- common_hal_displayio_tilegrid_set_tile(self->status_bar, self->status_x, self->status_y, tile_index);
- self->status_x++;
- if (self->status_x >= self->status_bar->width_in_tiles) {
- self->status_y++;
- self->status_x %= self->status_bar->width_in_tiles;
- }
+ // Clear the tile grid before we start putting new info.
+ if (self->status_x == 0 && self->status_y == 0) {
+ terminalio_terminal_set_all_tiles(self, true, ' ', true);
}
+ terminalio_terminal_set_tile(self, true, c, true);
}
continue;
}
- // Always handle ASCII.
- if (c < 128) {
- if (c >= 0x20 && c <= 0x7e) {
- uint8_t tile_index = fontio_builtinfont_get_glyph_index(self->font, c);
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, self->cursor_x, self->cursor_y, tile_index);
- self->cursor_x++;
- } else if (c == '\r') {
+ if (c < 0x20) {
+ if (c == '\r') {
self->cursor_x = 0;
} else if (c == '\n') {
self->cursor_y++;
@@ -162,6 +279,8 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
#endif
} else {
if (c == 'K') {
+ int16_t original_cursor_x = self->cursor_x;
+ int16_t original_cursor_y = self->cursor_y;
int16_t clr_start = self->cursor_x;
int16_t clr_end = self->scroll_area->width_in_tiles;
#if CIRCUITPY_TERMINALIO_VT100
@@ -171,11 +290,14 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
} else if (vt_args[0] == 2) {
clr_start = 0;
}
+ self->cursor_x = clr_start;
#endif
// Clear the (start/rest/all) of the line.
for (uint16_t k = clr_start; k < clr_end; k++) {
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, k, self->cursor_y, 0);
+ terminalio_terminal_set_tile(self, false, ' ', true);
}
+ self->cursor_x = original_cursor_x;
+ self->cursor_y = original_cursor_y;
} else if (c == 'D') {
if (vt_args[0] > self->cursor_x) {
self->cursor_x = 0;
@@ -186,7 +308,7 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
if (vt_args[0] == 2) {
common_hal_displayio_tilegrid_set_top_left(self->scroll_area, 0, 0);
self->cursor_x = self->cursor_y = start_y = 0;
- common_hal_displayio_tilegrid_set_all_tiles(self->scroll_area, 0);
+ terminalio_terminal_set_all_tiles(self, false, ' ', true);
}
} else if (c == 'H') {
if (vt_args[0] > 0) {
@@ -240,16 +362,20 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
self->cursor_y = self->scroll_area->height_in_tiles - 1;
}
} else {
- if (self->vt_scroll_top != 0 || self->vt_scroll_end != self->scroll_area->height_in_tiles) {
+ if (self->vt_scroll_top != 0 || self->vt_scroll_end != self->scroll_area->height_in_tiles - 1) {
// Scroll range defined, manually move tiles to perform scroll
for (int16_t irow = self->vt_scroll_end - 1; irow >= self->vt_scroll_top; irow--) {
for (int16_t icol = 0; icol < self->scroll_area->width_in_tiles; icol++) {
common_hal_displayio_tilegrid_set_tile(self->scroll_area, icol, SCRNMOD(irow + 1), common_hal_displayio_tilegrid_get_tile(self->scroll_area, icol, SCRNMOD(irow)));
}
}
+ self->cursor_x = 0;
+ int16_t old_y = self->cursor_y;
+ // Fill the row with spaces.
for (int16_t icol = 0; icol < self->scroll_area->width_in_tiles; icol++) {
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, icol, self->cursor_y, 0);
+ terminalio_terminal_set_tile(self, false, ' ', true);
}
+ self->cursor_y = old_y;
} else {
// Full screen scroll, just set new top_y pointer and clear row
if (self->cursor_y > 0) {
@@ -257,8 +383,12 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
} else {
common_hal_displayio_tilegrid_set_top_left(self->scroll_area, 0, self->scroll_area->height_in_tiles - 1);
}
- for (uint16_t icol = 0; icol < self->scroll_area->width_in_tiles; icol++) {
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, icol, self->scroll_area->top_left_y, 0);
+
+ self->cursor_x = 0;
+ self->cursor_y = self->scroll_area->top_left_y;
+ // Fill the row with spaces.
+ for (int16_t icol = 0; icol < self->scroll_area->width_in_tiles; icol++) {
+ terminalio_terminal_set_tile(self, false, ' ', true);
}
self->cursor_y = self->scroll_area->top_left_y;
}
@@ -277,12 +407,7 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
}
}
} else {
- uint8_t tile_index = fontio_builtinfont_get_glyph_index(self->font, c);
- if (tile_index != 0xff) {
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, self->cursor_x, self->cursor_y, tile_index);
- self->cursor_x++;
-
- }
+ terminalio_terminal_set_tile(self, false, c, true);
}
if (self->cursor_x >= self->scroll_area->width_in_tiles) {
self->cursor_y++;
@@ -294,7 +419,7 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
if (self->cursor_y != start_y) {
if (((self->cursor_y + self->scroll_area->height_in_tiles) - 1) % self->scroll_area->height_in_tiles == SCRNMOD(self->vt_scroll_end)) {
#if CIRCUITPY_TERMINALIO_VT100
- if (self->vt_scroll_top != 0 || self->vt_scroll_end != self->scroll_area->height_in_tiles) {
+ if (self->vt_scroll_top != 0 || self->vt_scroll_end != self->scroll_area->height_in_tiles - 1) {
// Scroll range defined, manually move tiles to perform scroll
self->cursor_y = SCRNMOD(self->vt_scroll_end);
@@ -305,15 +430,18 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con
}
}
#endif
- if (self->vt_scroll_top == 0 && self->vt_scroll_end == self->scroll_area->height_in_tiles) {
+ if (self->vt_scroll_top == 0 && self->vt_scroll_end == self->scroll_area->height_in_tiles - 1) {
// Full screen scroll, just set new top_y pointer
common_hal_displayio_tilegrid_set_top_left(self->scroll_area, 0, (self->cursor_y + self->scroll_area->height_in_tiles + 1) % self->scroll_area->height_in_tiles);
}
// clear the new row in case of scroll up
+ self->cursor_x = 0;
+ int16_t old_y = self->cursor_y;
for (int16_t icol = 0; icol < self->scroll_area->width_in_tiles; icol++) {
- common_hal_displayio_tilegrid_set_tile(self->scroll_area, icol, self->cursor_y, 0);
+ terminalio_terminal_set_tile(self, false, ' ', true);
}
self->cursor_x = 0;
+ self->cursor_y = old_y;
}
start_y = self->cursor_y;
}
diff --git a/shared-module/terminalio/Terminal.h b/shared-module/terminalio/Terminal.h
index d617f571aed7d..8bf294cb27ff7 100644
--- a/shared-module/terminalio/Terminal.h
+++ b/shared-module/terminalio/Terminal.h
@@ -15,7 +15,7 @@
typedef struct {
mp_obj_base_t base;
- const fontio_builtinfont_t *font;
+ mp_obj_t font; // Can be fontio_builtinfont_t or lvfontio_ondiskfont_t
uint16_t cursor_x;
uint16_t cursor_y;
displayio_tilegrid_t *scroll_area;
@@ -30,3 +30,4 @@ typedef struct {
} terminalio_terminal_obj_t;
extern void terminalio_terminal_clear_status_bar(terminalio_terminal_obj_t *self);
+uint16_t terminalio_terminal_get_glyph_index(mp_obj_t font, mp_uint_t codepoint, bool *is_full_width);
diff --git a/shared/runtime/pyexec.c b/shared/runtime/pyexec.c
index 050a14ef82e34..14e5e51a49aa5 100644
--- a/shared/runtime/pyexec.c
+++ b/shared/runtime/pyexec.c
@@ -91,7 +91,7 @@ static int parse_compile_execute(const void *source, mp_parse_input_kind_t input
nlr_buf_t nlr;
nlr.ret_val = NULL;
if (nlr_push(&nlr) == 0) {
- mp_obj_t module_fun;
+ mp_obj_t module_fun = mp_const_none;
// CIRCUITPY-CHANGE
#if CIRCUITPY_ATEXIT
if (!(exec_flags & EXEC_FLAG_SOURCE_IS_ATEXIT))
@@ -157,7 +157,7 @@ static int parse_compile_execute(const void *source, mp_parse_input_kind_t input
mp_call_function_n_kw(callback->func, callback->n_pos, callback->n_kw, callback->args);
} else
#endif
- {
+ if (module_fun != mp_const_none) {
mp_call_function_0(module_fun);
}
mp_hal_set_interrupt_char(-1); // disable interrupt
diff --git a/supervisor/shared/display.c b/supervisor/shared/display.c
index 32df5be74efce..87af13d64688b 100644
--- a/supervisor/shared/display.c
+++ b/supervisor/shared/display.c
@@ -7,8 +7,10 @@
#include "supervisor/shared/display.h"
#include
+#include "supervisor/port.h"
#include "py/mpstate.h"
+#include "py/gc.h"
#include "shared-bindings/displayio/Bitmap.h"
#include "shared-bindings/displayio/Group.h"
#include "shared-bindings/displayio/Palette.h"
@@ -39,6 +41,77 @@
#if CIRCUITPY_OS_GETENV
#include "shared-module/os/__init__.h"
#endif
+#if CIRCUITPY_LVFONTIO
+#include "shared-bindings/lvfontio/OnDiskFont.h"
+#include "supervisor/filesystem.h"
+#include "extmod/vfs_fat.h"
+#include "lib/oofatfs/ff.h"
+
+#include "supervisor/shared/serial.h"
+
+// Check if a custom font file exists and return its path if found
+// Returns true if font file exists, false otherwise
+static bool check_for_custom_font(const char **font_path_out) {
+ if (!filesystem_present()) {
+ return false;
+ }
+
+ fs_user_mount_t *vfs = filesystem_circuitpy();
+ if (vfs == NULL) {
+ return false;
+ }
+
+ // Use FATFS directly to check if file exists
+ FILINFO file_info;
+ const char *default_font_path = "/fonts/terminal.lvfontbin";
+ const char *font_path = default_font_path;
+
+ #if CIRCUITPY_OS_GETENV
+ // Buffer for storing custom font path
+ static char custom_font_path[128];
+ if (common_hal_os_getenv_str("CIRCUITPY_TERMINAL_FONT", custom_font_path, sizeof(custom_font_path)) == GETENV_OK) {
+ // Use custom font path from environment variable
+ font_path = custom_font_path;
+ }
+ #endif
+
+ FRESULT result = f_stat(&vfs->fatfs, font_path, &file_info);
+ if (result == FR_OK) {
+ if (font_path_out != NULL) {
+ *font_path_out = font_path;
+ }
+ return true;
+ }
+
+ // If custom font path doesn't exist, use default font
+ font_path = default_font_path;
+ result = f_stat(&vfs->fatfs, font_path, &file_info);
+
+ if (result == FR_OK) {
+ if (font_path_out != NULL) {
+ *font_path_out = font_path;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+// Initialize a BuiltinFont object with the specified font file and max_slots
+// Returns true on success, false on failure
+static bool init_lvfont(lvfontio_ondiskfont_t *font, const char *font_path, uint16_t max_slots) {
+ if (font == NULL) {
+ return false;
+ }
+
+ font->base.type = &lvfontio_ondiskfont_type;
+
+ // Pass false for use_gc_allocator during startup when garbage collector isn't fully initialized
+ common_hal_lvfontio_ondiskfont_construct(font, font_path, max_slots, false);
+
+ return !common_hal_lvfontio_ondiskfont_deinited(font);
+}
+#endif
#endif
#if CIRCUITPY_REPL_LOGO
@@ -52,29 +125,66 @@ static uint8_t *tilegrid_tiles = NULL;
static size_t tilegrid_tiles_size = 0;
#endif
+#if CIRCUITPY_LVFONTIO
+static lvfontio_ondiskfont_t *lvfont = NULL;
+#endif
+
void supervisor_start_terminal(uint16_t width_px, uint16_t height_px) {
if (supervisor_terminal_started()) {
return;
}
+
+ #if CIRCUITPY_TERMINALIO
// Default the scale to 2 because we may show blinka without the terminal for
// languages that don't have font support.
mp_int_t scale = 2;
- #if CIRCUITPY_TERMINALIO
displayio_tilegrid_t *scroll_area = &supervisor_terminal_scroll_area_text_grid;
displayio_tilegrid_t *status_bar = &supervisor_terminal_status_bar_text_grid;
bool reset_tiles = false;
- uint16_t width_in_tiles = width_px / scroll_area->tile_width;
+
+ uint16_t glyph_width = 0;
+ uint16_t glyph_height = 0;
+
+ #if CIRCUITPY_LVFONTIO
+ // Check if we have a custom terminal font in the filesystem
+ bool use_lv_font = false;
+ const char *font_path = NULL;
+
+ if (check_for_custom_font(&font_path)) {
+ // Initialize a temporary font just to get dimensions
+ lvfontio_ondiskfont_t temp_font;
+ if (init_lvfont(&temp_font, font_path, 1)) {
+ // Get the font dimensions
+ common_hal_lvfontio_ondiskfont_get_dimensions(&temp_font, &glyph_width, &glyph_height);
+
+ // Clean up the temp font - we'll create a proper one later
+ common_hal_lvfontio_ondiskfont_deinit(&temp_font);
+ use_lv_font = true;
+ reset_tiles = true;
+ // TODO: We may want to detect when the files modified time hasn't changed.
+ }
+ }
+ #endif
+ #if CIRCUITPY_FONTIO
+ if (glyph_width == 0) {
+ glyph_width = supervisor_terminal_font.width;
+ glyph_height = supervisor_terminal_font.height;
+ }
+ #endif
+
+ uint16_t width_in_tiles = width_px / glyph_width;
+
// determine scale based on width
- if (width_in_tiles <= 80) {
+ if (width_in_tiles <= 120) {
scale = 1;
}
#if CIRCUITPY_OS_GETENV
(void)common_hal_os_getenv_int("CIRCUITPY_TERMINAL_SCALE", &scale);
#endif
- width_in_tiles = MAX(1, width_px / (scroll_area->tile_width * scale));
- uint16_t height_in_tiles = MAX(2, height_px / (scroll_area->tile_height * scale));
+ width_in_tiles = MAX(1, width_px / (glyph_width * scale));
+ uint16_t height_in_tiles = MAX(2, height_px / (glyph_height * scale));
uint16_t total_tiles = width_in_tiles * height_in_tiles;
@@ -83,69 +193,121 @@ void supervisor_start_terminal(uint16_t width_px, uint16_t height_px) {
(scroll_area->height_in_tiles != height_in_tiles - 1)) {
reset_tiles = true;
}
- // Reuse the previous allocation if possible
- if (tilegrid_tiles) {
- if (tilegrid_tiles_size != total_tiles) {
+
+ circuitpython_splash.scale = scale;
+ if (!reset_tiles) {
+ return;
+ }
+
+ // Adjust the display dimensions to account for scale of the outer group.
+ width_px /= scale;
+ height_px /= scale;
+
+ // Number of tiles from the left edge to inset the status bar.
+ size_t min_left_padding = 0;
+ status_bar->tile_width = glyph_width;
+ status_bar->tile_height = glyph_height;
+ #if CIRCUITPY_REPL_LOGO
+ // Blinka + 1 px padding minimum
+ min_left_padding = supervisor_blinka_sprite.pixel_width + 1;
+ // Align the status bar to the bottom of the logo.
+ status_bar->y = supervisor_blinka_sprite.pixel_height - status_bar->tile_height;
+ #else
+ status_bar->y = 0;
+ #endif
+ status_bar->width_in_tiles = (width_px - min_left_padding) / status_bar->tile_width;
+ status_bar->height_in_tiles = 1;
+ status_bar->pixel_width = status_bar->width_in_tiles * status_bar->tile_width;
+ status_bar->pixel_height = status_bar->tile_height;
+ // Right align the status bar.
+ status_bar->x = width_px - status_bar->pixel_width;
+ status_bar->top_left_y = 0;
+ status_bar->full_change = true;
+
+ scroll_area->tile_width = glyph_width;
+ scroll_area->tile_height = glyph_height;
+ scroll_area->width_in_tiles = width_in_tiles;
+ // Leave space for the status bar, no matter if we have logo or not.
+ scroll_area->height_in_tiles = height_in_tiles - 1;
+ scroll_area->pixel_width = scroll_area->width_in_tiles * scroll_area->tile_width;
+ scroll_area->pixel_height = scroll_area->height_in_tiles * scroll_area->tile_height;
+ // Right align the scroll area to give margin to the start of each line.
+ scroll_area->x = width_px - scroll_area->pixel_width;
+ scroll_area->top_left_y = 0;
+ // Align the scroll area to the bottom so that the newest line isn't cutoff. The top line
+ // may be clipped by the status bar and that's ok.
+ scroll_area->y = height_px - scroll_area->pixel_height;
+ scroll_area->full_change = true;
+
+ mp_obj_t new_bitmap = mp_const_none;
+ mp_obj_t new_font = mp_const_none;
+
+ #if CIRCUITPY_LVFONTIO
+ if (lvfont != NULL) {
+ common_hal_lvfontio_ondiskfont_deinit(lvfont);
+ // This will also free internal buffers that may change size.
+ port_free(lvfont);
+ lvfont = NULL;
+ }
+
+ if (use_lv_font) {
+ // We found a custom terminal font file, use it instead of the built-in font
+
+ lvfont = port_malloc(sizeof(lvfontio_ondiskfont_t), false);
+ if (lvfont != NULL) {
+ // Use the number of tiles in the terminal and status bar for the number of slots
+ // This ensures we have enough slots to display all characters that could appear on screen
+ uint16_t num_slots = width_in_tiles * height_in_tiles;
+
+ // Initialize the font with our helper function
+ if (init_lvfont(lvfont, font_path, num_slots)) {
+ // Get the bitmap from the font
+ new_bitmap = common_hal_lvfontio_ondiskfont_get_bitmap(lvfont);
+ new_font = MP_OBJ_FROM_PTR(lvfont);
+ } else {
+ // If font initialization failed, free the memory and fall back to built-in font
+ port_free(lvfont);
+ lvfont = NULL;
+ use_lv_font = false;
+ }
+ }
+ }
+ #endif
+ #if CIRCUITPY_FONTIO
+ if (new_font == mp_const_none) {
+ new_bitmap = MP_OBJ_FROM_PTR(supervisor_terminal_font.bitmap);
+ new_font = MP_OBJ_FROM_PTR(&supervisor_terminal_font);
+ }
+ #endif
+
+ if (new_font != mp_const_none) {
+ size_t total_values = common_hal_displayio_bitmap_get_width(new_bitmap) / glyph_width;
+ if (tilegrid_tiles) {
port_free(tilegrid_tiles);
tilegrid_tiles = NULL;
- tilegrid_tiles_size = 0;
- reset_tiles = true;
}
- }
- if (!tilegrid_tiles) {
- tilegrid_tiles = port_malloc(total_tiles, false);
- reset_tiles = true;
+ size_t bytes_per_tile = 1;
+ if (total_tiles > 255) {
+ // Two bytes per tile.
+ bytes_per_tile = 2;
+ }
+ tilegrid_tiles = port_malloc(total_tiles * bytes_per_tile, false);
if (!tilegrid_tiles) {
return;
}
- }
-
- if (reset_tiles) {
- // Adjust the display dimensions to account for scale of the outer group.
- width_px /= scale;
- height_px /= scale;
-
- // Number of tiles from the left edge to inset the status bar.
- size_t min_left_padding = 0;
- #if CIRCUITPY_REPL_LOGO
- // Blinka + 1 px padding minimum
- min_left_padding = supervisor_blinka_sprite.pixel_width + 1;
- // Align the status bar to the bottom of the logo.
- status_bar->y = supervisor_blinka_sprite.pixel_height - status_bar->tile_height;
- #else
- status_bar->y = 0;
- #endif
- status_bar->width_in_tiles = (width_px - min_left_padding) / status_bar->tile_width;
- status_bar->height_in_tiles = 1;
- status_bar->pixel_width = status_bar->width_in_tiles * status_bar->tile_width;
- status_bar->pixel_height = status_bar->tile_height;
- // Right align the status bar.
- status_bar->x = width_px - status_bar->pixel_width;
- status_bar->top_left_y = 0;
status_bar->tiles = tilegrid_tiles;
- status_bar->full_change = true;
-
- scroll_area->width_in_tiles = width_in_tiles;
- // Leave space for the status bar, no matter if we have logo or not.
- scroll_area->height_in_tiles = height_in_tiles - 1;
- scroll_area->pixel_width = scroll_area->width_in_tiles * scroll_area->tile_width;
- scroll_area->pixel_height = scroll_area->height_in_tiles * scroll_area->tile_height;
- // Right align the scroll area to give margin to the start of each line.
- scroll_area->x = width_px - scroll_area->pixel_width;
- scroll_area->top_left_y = 0;
- // Align the scroll area to the bottom so that the newest line isn't cutoff. The top line
- // may be clipped by the status bar and that's ok.
- scroll_area->y = height_px - scroll_area->pixel_height;
- scroll_area->tiles = tilegrid_tiles + width_in_tiles;
- scroll_area->full_change = true;
-
- common_hal_terminalio_terminal_construct(&supervisor_terminal, scroll_area, &supervisor_terminal_font, status_bar);
-
- // Do not update status bar until after boot.py has run, in case it is disabled.
+ status_bar->tiles_in_bitmap = total_values;
+ status_bar->bitmap_width_in_tiles = total_values;
+ scroll_area->tiles = tilegrid_tiles + width_in_tiles * bytes_per_tile;
+ scroll_area->tiles_in_bitmap = total_values;
+ scroll_area->bitmap_width_in_tiles = total_values;
+
+ common_hal_displayio_tilegrid_set_bitmap(scroll_area, new_bitmap);
+ common_hal_displayio_tilegrid_set_bitmap(status_bar, new_bitmap);
+ common_hal_terminalio_terminal_construct(&supervisor_terminal, scroll_area,
+ new_font, status_bar);
}
#endif
-
- circuitpython_splash.scale = scale;
}
void supervisor_stop_terminal(void) {
diff --git a/tools/gen_display_resources.py b/tools/gen_display_resources.py
index 2219b4e86da50..7b6de6d95d445 100644
--- a/tools/gen_display_resources.py
+++ b/tools/gen_display_resources.py
@@ -366,7 +366,7 @@ def _load_row(self, y, row):
"""\
terminalio_terminal_obj_t supervisor_terminal = {
.base = { .type = &terminalio_terminal_type },
- .font = &supervisor_terminal_font,
+ .font = MP_OBJ_FROM_PTR(&supervisor_terminal_font),
.cursor_x = 0,
.cursor_y = 0,
.scroll_area = NULL,