Skip to content

[New Feature] Multi-resolution screenshot generator #747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b91d105
rough prototype
kdmukai Jul 23, 2024
6a4a03a
Tweaks to SeedMnemonicEntryScreen layout
kdmukai Jul 24, 2024
f19d7e7
Expand xpub in SeedExportXpubDetailsScreen
kdmukai Jul 25, 2024
84c0109
new `ToolsAddressExplorerAddressListScreen`
kdmukai Jul 25, 2024
cfcfb4a
Screensaver relative positioning
kdmukai Aug 16, 2024
87638e7
basic ili9341 working, but no portrait only
kdmukai Aug 17, 2024
1e6dd1b
Support for new IPS display
kdmukai Aug 20, 2024
e33fac8
Starting to generalize support for the different displays
kdmukai Aug 25, 2024
75542ec
Update settings_definition.py
kdmukai Aug 25, 2024
f843c62
on-the-fly display driver changes via SettingsQR
kdmukai Aug 25, 2024
14ca507
Update settings_definition.py
kdmukai Aug 25, 2024
61f1655
Update st7789_mpy.py
kdmukai Aug 25, 2024
6bfd7c4
compatibility for st7789 240x320; re-init display on settings change
kdmukai Aug 25, 2024
166c7c8
Bugfixes
kdmukai Apr 18, 2025
7a0bff7
Restore original ST7789 driver for 240x240
kdmukai Apr 18, 2025
11d0969
Fix centering on OpeningSplashScreen for 320x240
kdmukai Apr 19, 2025
639afc2
Aspect ratio-savvy camera frame resizing for non-square displays
kdmukai Apr 19, 2025
c0cb701
Bugfix: SeedQR transcription UI navigation; cleanup
kdmukai Apr 20, 2025
9500059
simplified, optimized SeedQR transcription mask overlay rendering
kdmukai Apr 20, 2025
1116c9e
Finish integrating ToolsAddressExplorerAddressListScreen
kdmukai Apr 20, 2025
499c82d
standardize on our landscape orientation (widest dim first)
kdmukai Apr 20, 2025
052035d
Fix up one last value
kdmukai Apr 20, 2025
7f53c7c
bugfix after swapping st7789 dims
kdmukai Apr 20, 2025
6887c4f
Add `invert` to `ST7789` driver
kdmukai Apr 22, 2025
d8c2833
Add "beta" designation to ili9341 driver
kdmukai Apr 22, 2025
e00f0a2
Reorder Settings screenshots, remove 320x240 hard coding
kdmukai Apr 22, 2025
95c9234
Initial multiresolution screenshot generator
kdmukai Apr 28, 2025
fb174e0
`--no-clean` added; slight refactor
kdmukai Apr 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/seedsigner/gui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -1986,3 +1986,47 @@ def reflow_text_into_pages(text: str,
pages.append("\n".join(lines[i:i+lines_per_page]))

return pages


def resize_image_to_fill(img: Image, target_size_x: int, target_size_y: int, sampling_method=Image.Resampling.NEAREST) -> Image:
"""
Resizes the image to fill the target size, cropping the image if necessary.
"""
if img.width == target_size_x and img.height == target_size_y:
# No need to resize
return img

# if the image aspect ratio doesn't match the render area, we
# need to provide an aspect ratio-aware crop box.
render_aspect_ratio = target_size_x / target_size_y
source_frame_aspect_ratio = img.width / img.height
if render_aspect_ratio > source_frame_aspect_ratio:
# Render surface is wider than the source frame; preserve
# the width but crop the height
cropped_height = (img.width * target_size_y / target_size_x)
box = (
0,
int((img.height - cropped_height)/2),
img.width,
img.height - int((img.height - cropped_height)/2),
)

elif render_aspect_ratio < source_frame_aspect_ratio:
# Render surface is taller than the source frame; preserve
# the height but crop the width
box = (
int((img.width - img.height * target_size_x / target_size_y) / 2),
0,
int(img.width - (img.width - img.height * target_size_x / target_size_y) / 2),
img.height,
)

else:
# Render surface and source frame are the same aspect ratio
box = None

return img.resize(
(target_size_x, target_size_y),
resample=sampling_method,
box=box,
)
47 changes: 37 additions & 10 deletions src/seedsigner/gui/renderer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from PIL import Image, ImageDraw
from threading import Lock

from seedsigner.hardware.ST7789 import ST7789
from seedsigner.hardware.displays.display_driver import ALL_DISPLAY_TYPES, DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486, DISPLAY_TYPE__ST7789, DisplayDriver
from seedsigner.models.settings import Settings
from seedsigner.models.settings_definition import SettingsConstants
from seedsigner.models.singleton import ConfigurableSingleton


Expand All @@ -22,19 +24,44 @@ def configure_instance(cls):
renderer = cls.__new__(cls)
cls._instance = renderer

# Eventually we'll be able to plug in other display controllers
renderer.disp = ST7789()
renderer.canvas_width = renderer.disp.width
renderer.canvas_height = renderer.disp.height
renderer.initialize_display()

renderer.canvas = Image.new('RGB', (renderer.canvas_width, renderer.canvas_height))
renderer.draw = ImageDraw.Draw(renderer.canvas)

def initialize_display(self):
# May be called while already running with a previous display driver; must
# prevent any other screen writes while we're changing the display driver.
self.lock.acquire()

display_config = Settings.get_instance().get_value(SettingsConstants.SETTING__DISPLAY_CONFIGURATION, default_if_none=True)
self.display_type = display_config.split("_")[0]
if self.display_type not in ALL_DISPLAY_TYPES:
raise Exception(f"Invalid display type: {self.display_type}")

width, height = display_config.split("_")[1].split("x")
self.disp = DisplayDriver(self.display_type, width=int(width), height=int(height))

if Settings.get_instance().get_value(SettingsConstants.SETTING__DISPLAY_COLOR_INVERTED, default_if_none=True) == SettingsConstants.OPTION__ENABLED:
self.disp.invert()

if self.display_type == DISPLAY_TYPE__ST7789:
self.canvas_width = self.disp.width
self.canvas_height = self.disp.height

elif self.display_type in [DISPLAY_TYPE__ILI9341, DISPLAY_TYPE__ILI9486]:
# Swap for the natively portrait-oriented displays
self.canvas_width = self.disp.height
self.canvas_height = self.disp.width

self.canvas = Image.new('RGB', (self.canvas_width, self.canvas_height))
self.draw = ImageDraw.Draw(self.canvas)

self.lock.release()


def show_image(self, image=None, alpha_overlay=None, show_direct=False):
if show_direct:
# Use the incoming image as the canvas and immediately render
self.disp.ShowImage(image, 0, 0)
self.disp.show_image(image, 0, 0)
return

if alpha_overlay:
Expand All @@ -46,7 +73,7 @@ def show_image(self, image=None, alpha_overlay=None, show_direct=False):
# Always write to the current canvas, rather than trying to replace it
self.canvas.paste(image)

self.disp.ShowImage(self.canvas, 0, 0)
self.disp.show_image(self.canvas, 0, 0)


def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None):
Expand Down Expand Up @@ -80,7 +107,7 @@ def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_over
# Always keep a copy of the current display in the canvas
self.canvas.paste(crop)

self.disp.ShowImage(crop, 0, 0)
self.disp.show_image(crop, 0, 0)



Expand Down
9 changes: 3 additions & 6 deletions src/seedsigner/gui/screens/scan_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from PIL import Image, ImageDraw

from seedsigner.gui import renderer
from seedsigner.gui.components import GUIConstants, Fonts
from seedsigner.gui.components import GUIConstants, Fonts, resize_image_to_fill
from seedsigner.models.decode_qr import DecodeQR
from seedsigner.models.threads import BaseThread, ThreadsafeCounter

Expand Down Expand Up @@ -132,11 +132,8 @@ def run(self):
scan_text += f" {cur_fps:0.2f} | {self.decoder_fps}"

with self.renderer.lock:
if frame.width > self.render_width or frame.height > self.render_height:
frame = frame.resize(
(self.render_width, self.render_height),
resample=Image.NEAREST # Use nearest neighbor for max speed
)
# Use nearest neighbor resizing for max speed
frame = resize_image_to_fill(frame, self.render_width, self.render_height, sampling_method=Image.Resampling.NEAREST)

if scan_text:
# Note: shadowed text (adding a 'stroke' outline) can
Expand Down
6 changes: 4 additions & 2 deletions src/seedsigner/gui/screens/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,11 @@ def __post_init__(self):
if len(self.button_data) not in [2, 4]:
raise Exception("LargeButtonScreen only supports 2 or 4 buttons")

# Maximize 2-across width; calc height with a 4:3 aspect ratio
# Maximize 2-across width
button_width = int((self.canvas_width - (2 * GUIConstants.EDGE_PADDING) - GUIConstants.COMPONENT_PADDING) / 2)
button_height = int(button_width * (3.0 / 4.0))

# Maximize 2-row height
button_height = int((self.canvas_height - self.top_nav.height - (2 * GUIConstants.COMPONENT_PADDING) - GUIConstants.EDGE_PADDING) / 2)

# Vertically center the buttons
if len(self.button_data) == 2:
Expand Down
Loading