Skip to content

fix(steami_screen): Improve text scaling using pixel-by-pixel framebuf.#398

Merged
nedseb merged 2 commits intomainfrom
fix/steami-screen-text-scaling
Apr 16, 2026
Merged

fix(steami_screen): Improve text scaling using pixel-by-pixel framebuf.#398
nedseb merged 2 commits intomainfrom
fix/steami-screen-text-scaling

Conversation

@MatteoCnda1
Copy link
Copy Markdown
Contributor

Summary

Improve _draw_scaled_text() to produce a true pixel-scale zoom instead of a bold offset effect. Closes #369

Changes

  • Implemented approach 3 (backend-specific): added draw_scaled_text() directly in SSD1327Display (steami_screen/ssd1327.py)
  • The method renders each character into a temporary 8x8 MONO_HLSB framebuf, reads each lit pixel with framebuf.pixel(), and draws a scale x scale filled rectangle for each lit pixel
  • _draw_scaled_text() in device.py already checks hasattr(self._d, 'draw_scaled_text') and delegates to the backend — no changes needed in device.py
  • This approach allows each backend (SSD1327, GC9A01, etc.) to implement its own optimized scaling strategy

Checklist

  • ruff check passes
  • python -m pytest tests/ -k mock -v passes (no mock test broken)
  • Tested on hardware (STM32WB55 / STeaMi board)
  • README updated (if adding/changing public API) — N/A, internal method only
  • Examples added/updated — N/A, existing examples benefit automatically
  • Commit messages follow <scope>: <Description.> format

@MatteoCnda1 MatteoCnda1 changed the title fix(steami_screen): Improve text scaling using pixel-by-pixel framebu… fix(steami_screen): Improve text scaling using pixel-by-pixel framebuf rendering. Apr 15, 2026
@MatteoCnda1 MatteoCnda1 force-pushed the fix/steami-screen-text-scaling branch from c0e596f to 28c7d80 Compare April 15, 2026 08:07
@MatteoCnda1 MatteoCnda1 changed the title fix(steami_screen): Improve text scaling using pixel-by-pixel framebuf rendering. fix(steami_screen): Improve text scaling using pixel-by-pixel framebuf. Apr 15, 2026
@MatteoCnda1 MatteoCnda1 requested review from Charly-sketch, Kaanoz-en and nedseb and removed request for Kaanoz-en April 15, 2026 08:09
@nedseb nedseb requested review from Copilot and removed request for Charly-sketch April 16, 2026 19:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves scaled text rendering for the SSD1327 backend in steami_screen by implementing backend-specific pixel-accurate scaling, and updates documentation to reflect the improved behavior (closes #369).

Changes:

  • Added SSD1327Display.draw_scaled_text() that scales glyphs by rendering to a temporary mono framebuffer and expanding lit pixels into scale x scale blocks.
  • Updated SSD1327 wrapper internals (quotes/hasattr calls) to support the new scaling path.
  • Updated lib/steami_screen/README.md note to describe true pixel-scale zoom behavior on SSD1327.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
lib/steami_screen/steami_screen/ssd1327.py Adds backend-specific scaled-text rendering via per-pixel expansion from a temporary mono framebuffer.
lib/steami_screen/README.md Updates user-facing note about text scaling behavior on SSD1327.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 13 to 15
import framebuf

from steami_screen.colors import rgb_to_gray4
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing framebuf at module import time can make steami_screen fail to import in CPython environments where framebuf isn’t available (unless callers pre-inject a stub). Consider moving this import inside draw_scaled_text() (or guarding it with a try/except) so only the scaled-text path requires framebuf.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
gray = rgb_to_gray4(color)

char_buf = bytearray(8)
fb = framebuf.FrameBuffer(char_buf, 8, 8, framebuf.MONO_HLSB)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

draw_scaled_text() allocates a new bytearray and FrameBuffer on every call. On MicroPython this can add GC pressure and jitter during UI updates. Consider caching the buffer/framebuf on self (lazy-init once) and reusing it across calls.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +77
for py in range(8):
for px in range(8):
if fb.pixel(px, py):
if hasattr(self._raw, "fill_rect"):
self._raw.fill_rect(
cx + px * scale,
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner pixel loops repeatedly call hasattr(self._raw, "fill_rect") for every lit pixel. Hoist this check once (e.g., pick a local fill_rect function) before the loops to reduce overhead in tight rendering code.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +90
reads each lit pixel, and draws a scale x scale filled rectangle.
This produces a true pixel-scale zoom instead of a bold offset effect.
"""

gray = rgb_to_gray4(color)

char_buf = bytearray(8)
fb = framebuf.FrameBuffer(char_buf, 8, 8, framebuf.MONO_HLSB)

cx = x
for char in text:
fb.fill(0)
fb.text(char, 0, 0, 1)
for py in range(8):
for px in range(8):
if fb.pixel(px, py):
if hasattr(self._raw, "fill_rect"):
self._raw.fill_rect(
cx + px * scale,
y + py * scale,
scale,
scale,
gray,
)
else:
self._raw.framebuf.fill_rect(
cx + px * scale,
y + py * scale,
scale,
scale,
gray,
)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation can issue up to 64 fill_rect() calls per character (one per set pixel), which is typically slow on MicroPython displays. A common optimization is to coalesce consecutive lit pixels into longer horizontal runs (one fill_rect per run) or build a scaled glyph buffer and blit it, to drastically cut per-character draw calls.

Suggested change
reads each lit pixel, and draws a scale x scale filled rectangle.
This produces a true pixel-scale zoom instead of a bold offset effect.
"""
gray = rgb_to_gray4(color)
char_buf = bytearray(8)
fb = framebuf.FrameBuffer(char_buf, 8, 8, framebuf.MONO_HLSB)
cx = x
for char in text:
fb.fill(0)
fb.text(char, 0, 0, 1)
for py in range(8):
for px in range(8):
if fb.pixel(px, py):
if hasattr(self._raw, "fill_rect"):
self._raw.fill_rect(
cx + px * scale,
y + py * scale,
scale,
scale,
gray,
)
else:
self._raw.framebuf.fill_rect(
cx + px * scale,
y + py * scale,
scale,
scale,
gray,
)
reads each lit pixel, and draws scaled filled rectangles per
horizontal run of lit pixels. This produces a true pixel-scale zoom
instead of a bold offset effect while reducing display draw calls.
"""
gray = rgb_to_gray4(color)
char_buf = bytearray(8)
fb = framebuf.FrameBuffer(char_buf, 8, 8, framebuf.MONO_HLSB)
if hasattr(self._raw, "fill_rect"):
fill_rect = self._raw.fill_rect
else:
fill_rect = self._raw.framebuf.fill_rect
cx = x
for char in text:
fb.fill(0)
fb.text(char, 0, 0, 1)
for py in range(8):
px = 0
while px < 8:
while px < 8 and not fb.pixel(px, py):
px += 1
if px >= 8:
break
run_start = px
while px < 8 and fb.pixel(px, py):
px += 1
fill_rect(
cx + run_start * scale,
y + py * scale,
(px - run_start) * scale,
scale,
gray,
)

Copilot uses AI. Check for mistakes.
Comment thread lib/steami_screen/README.md Outdated
Cardinal positions: `"N"`, `"NE"`, `"E"`, `"SE"`, `"S"`, `"SW"`, `"W"`, `"NW"`, `"CENTER"`.

Note: `scale=2` produces a bold effect (text drawn with 1px offset), not a true pixel-scale zoom. Backends can provide `draw_scaled_text()` for true scaling.
Note: `scale=2` produces a true pixel-scale zoom on SSD1327 displays via pixel-by-pixel
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note refers specifically to scale=2, but the SSD1327 backend implementation supports arbitrary scale > 1. Consider rewording to scale>1 to avoid implying other scale factors won’t render as true pixel scaling.

Suggested change
Note: `scale=2` produces a true pixel-scale zoom on SSD1327 displays via pixel-by-pixel
Note: `scale>1` produces a true pixel-scale zoom on SSD1327 displays via pixel-by-pixel

Copilot uses AI. Check for mistakes.
nedseb added a commit that referenced this pull request Apr 16, 2026
Address Copilot review comments on #398:

1. Defer `import framebuf` into draw_scaled_text() so steami_screen
   remains importable in CPython environments (tests, stubs, IDE).

2. Resolve fill_rect dispatch once at __init__ (self._fill_rect_raw)
   instead of calling hasattr() on every lit pixel in the inner loop.
   Also simplifies the public fill_rect() method.

3. Cache the dispatch as a local `blit` variable inside draw_scaled_text
   to avoid attribute lookups in the hot pixel loop.

4. Fix README note: scaling works for any scale > 1, not just scale=2.
   Mention the bold offset fallback for backends without draw_scaled_text.
MatteoCnda1 and others added 2 commits April 16, 2026 21:37
Address Copilot review comments on #398:

1. Defer `import framebuf` into draw_scaled_text() so steami_screen
   remains importable in CPython environments (tests, stubs, IDE).

2. Resolve fill_rect dispatch once at __init__ (self._fill_rect_raw)
   instead of calling hasattr() on every lit pixel in the inner loop.
   Also simplifies the public fill_rect() method.

3. Cache the dispatch as a local `blit` variable inside draw_scaled_text
   to avoid attribute lookups in the hot pixel loop.

4. Fix README note: scaling works for any scale > 1, not just scale=2.
   Mention the bold offset fallback for backends without draw_scaled_text.
@nedseb nedseb force-pushed the fix/steami-screen-text-scaling branch from 88ff2bc to 716acdb Compare April 16, 2026 19:37
Copy link
Copy Markdown
Contributor

@nedseb nedseb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bonne implémentation Matteo. L'approche framebuf 8x8 par caractère est la bonne — c'est la façon standard de faire du scaling sans accès direct au bitmap de la font MicroPython.

J'ai poussé un commit de review (716acdb) qui adresse les 5 commentaires Copilot :

1. import framebuf déféré — déplacé du top-level dans draw_scaled_text() pour que steami_screen reste importable en CPython (tests, stubs IDE, screenshots Pillow).

2. Dispatch fill_rect résolu une seule foishasattr(self._raw, 'fill_rect') était appelé à chaque pixel allumé (jusqu'à 64× par caractère × nombre de caractères). Maintenant résolu une fois au __init__ via self._fill_rect_raw, et caché en local blit dans la boucle.

3. Simplification de fill_rect() — la méthode publique utilise directement self._fill_rect_raw, supprimant la duplication du dispatch.

4. Note README — corrigée : le scaling fonctionne pour tout scale > 1, pas seulement scale=2. Mention du fallback bold offset pour les backends sans draw_scaled_text().

Branche rebasée sur main. Prêt à merger.

@nedseb nedseb merged commit 16386ea into main Apr 16, 2026
9 checks passed
@nedseb nedseb deleted the fix/steami-screen-text-scaling branch April 16, 2026 19:38
semantic-release-updater Bot pushed a commit that referenced this pull request Apr 16, 2026
## [0.20.2](v0.20.1...v0.20.2) (2026-04-16)

### Bug Fixes

* **steami_screen:** Improve text scaling using pixel-by-pixel framebuf. ([#398](#398)) ([16386ea](16386ea))
@semantic-release-updater
Copy link
Copy Markdown

🎉 This PR is included in version 0.20.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(steami_screen): Improve text scaling beyond bold offset effect.

3 participants