mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-02 09:18:06 +00:00
Compare commits
3
Commits
main
...
8d96ea5dd4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d96ea5dd4 | ||
|
|
43faf10fea | ||
|
|
9813e47837 |
@@ -279,6 +279,19 @@ class PluginAdapter:
|
|||||||
# Copy the image to prevent modification
|
# Copy the image to prevent modification
|
||||||
img = cached_image.copy()
|
img = cached_image.copy()
|
||||||
|
|
||||||
|
# Plugins that build their own ticker image via this shared
|
||||||
|
# ScrollHelper's create_scrolling_image() get a solid-black
|
||||||
|
# leading margin exactly `display_width` columns wide baked in
|
||||||
|
# (scroll_helper.py's "initial gap before first item"). Vegas mode
|
||||||
|
# adds its own leading gap/separator around every item already,
|
||||||
|
# so leaving this in stacks a second, uncontrolled blank margin on
|
||||||
|
# top of vegas_scroll.separator_width — making this plugin's
|
||||||
|
# transitions look inconsistent with plugins that provide content
|
||||||
|
# via get_vegas_content() (which carries no such margin). Strip it
|
||||||
|
# here so every plugin contributes only its real content and the
|
||||||
|
# gap between items is governed solely by separator_width.
|
||||||
|
img = self._strip_scroll_padding(img, scroll_helper, plugin_id)
|
||||||
|
|
||||||
# Ensure correct height
|
# Ensure correct height
|
||||||
if img.height != self.display_height:
|
if img.height != self.display_height:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -306,6 +319,69 @@ class PluginAdapter:
|
|||||||
logger.exception("[%s] Error getting scroll_helper content", plugin_id)
|
logger.exception("[%s] Error getting scroll_helper content", plugin_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _strip_scroll_padding(
|
||||||
|
self, img: Image.Image, scroll_helper: Any, plugin_id: str
|
||||||
|
) -> Image.Image:
|
||||||
|
"""
|
||||||
|
Crop off a plugin's own leading/trailing blank margins, if present.
|
||||||
|
|
||||||
|
create_scrolling_image() always pads the *start* of its cached image
|
||||||
|
with exactly `scroll_helper.display_width` columns of solid black
|
||||||
|
(0, 0, 0) ("initial gap before first item"). Some ticker-style plugins
|
||||||
|
also pad the *end* of their own cached image (e.g. so their standalone
|
||||||
|
display exits cleanly before looping). Vegas mode already adds its own
|
||||||
|
gap/separator around every item, so either margin left in place stacks
|
||||||
|
an extra, uncontrolled blank stretch on top of `separator_width` —
|
||||||
|
only when running inside Vegas mode does this matter, since the
|
||||||
|
plugin's own standalone display still wants that margin. Detect solid
|
||||||
|
black margins up to `scroll_helper.display_width` wide on each edge and
|
||||||
|
crop them here. Images built via set_scrolling_image() (no such
|
||||||
|
margins) are left untouched.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
img: Captured scroll_helper.cached_image (already copied)
|
||||||
|
scroll_helper: The plugin's ScrollHelper instance
|
||||||
|
plugin_id: Plugin identifier for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
img, cropped on whichever edge(s) had a matching blank margin
|
||||||
|
"""
|
||||||
|
pad_width = getattr(scroll_helper, 'display_width', None)
|
||||||
|
if not isinstance(pad_width, int) or pad_width <= 0 or pad_width >= img.width:
|
||||||
|
return img
|
||||||
|
|
||||||
|
def is_solid_black(strip: Image.Image) -> bool:
|
||||||
|
return strip.convert('RGB').getextrema() == ((0, 0), (0, 0), (0, 0))
|
||||||
|
|
||||||
|
left = pad_width if is_solid_black(img.crop((0, 0, pad_width, img.height))) else 0
|
||||||
|
right = (
|
||||||
|
pad_width
|
||||||
|
if is_solid_black(img.crop((img.width - pad_width, 0, img.width, img.height)))
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if not left and not right:
|
||||||
|
return img
|
||||||
|
|
||||||
|
# Degenerate case (e.g. an all-black cached image): don't crop past
|
||||||
|
# zero width, just leave the image as-is.
|
||||||
|
if left + right >= img.width:
|
||||||
|
return img
|
||||||
|
|
||||||
|
cropped = img.crop((left, 0, img.width - right, img.height))
|
||||||
|
|
||||||
|
# Both edges matching at once is a much stronger signal of genuine
|
||||||
|
# baked-in padding than a single edge (which has a small chance of
|
||||||
|
# coinciding with real all-black content, e.g. a dark logo touching
|
||||||
|
# one boundary). Log that case at warning level so an unexpected
|
||||||
|
# double-edge crop is easy to spot in the field.
|
||||||
|
log = logger.warning if (left and right) else logger.info
|
||||||
|
log(
|
||||||
|
"[%s] Stripping scroll_helper padding (left=%dpx, right=%dpx): %dpx -> %dpx",
|
||||||
|
plugin_id, left, right, img.width, cropped.width
|
||||||
|
)
|
||||||
|
return cropped
|
||||||
|
|
||||||
def _trigger_scroll_content_generation(
|
def _trigger_scroll_content_generation(
|
||||||
self, plugin: 'BasePlugin', plugin_id: str, scroll_helper: Any
|
self, plugin: 'BasePlugin', plugin_id: str, scroll_helper: Any
|
||||||
) -> Optional[Image.Image]:
|
) -> Optional[Image.Image]:
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
Tests for src/vegas_mode/plugin_adapter.py
|
||||||
|
|
||||||
|
Covers PluginAdapter._strip_scroll_padding(): the heuristic that crops a
|
||||||
|
plugin's own baked-in leading/trailing blank margins before Vegas mode
|
||||||
|
composites the content, so vegas_scroll.separator_width is the only gap
|
||||||
|
applied between items.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from src.common.scroll_helper import ScrollHelper
|
||||||
|
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDisplayManager:
|
||||||
|
width = 64
|
||||||
|
height = 32
|
||||||
|
|
||||||
|
|
||||||
|
class FakePlugin:
|
||||||
|
def __init__(self, scroll_helper):
|
||||||
|
self.scroll_helper = scroll_helper
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def adapter():
|
||||||
|
return PluginAdapter(FakeDisplayManager())
|
||||||
|
|
||||||
|
|
||||||
|
def _solid(width: int, height: int, color: tuple) -> Image.Image:
|
||||||
|
"""Create a solid-color RGB image of the given dimensions."""
|
||||||
|
return Image.new('RGB', (width, height), color)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStripScrollPadding:
|
||||||
|
def test_leading_pad_from_create_scrolling_image_is_stripped(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
item = _solid(40, 32, (200, 50, 50))
|
||||||
|
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
assert images[0].width == 40
|
||||||
|
assert images[0].getpixel((0, 0)) == (200, 50, 50)
|
||||||
|
|
||||||
|
def test_leading_and_trailing_pad_both_stripped(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
content_w = 80
|
||||||
|
full = _solid(64 + content_w + 64, 32, (0, 0, 0))
|
||||||
|
full.paste(_solid(content_w, 32, (10, 220, 30)), (64, 0))
|
||||||
|
sh.set_scrolling_image(full)
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
assert images[0].width == content_w
|
||||||
|
assert images[0].getpixel((0, 0)) == (10, 220, 30)
|
||||||
|
assert images[0].getpixel((content_w - 1, 0)) == (10, 220, 30)
|
||||||
|
|
||||||
|
def test_leading_only_pad_stripped_trailing_content_kept(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
content_w = 80
|
||||||
|
full = _solid(64 + content_w, 32, (0, 0, 0))
|
||||||
|
full.paste(_solid(content_w, 32, (5, 5, 250)), (64, 0))
|
||||||
|
sh.set_scrolling_image(full)
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
assert images[0].width == content_w
|
||||||
|
assert images[0].getpixel((0, 0)) == (5, 5, 250)
|
||||||
|
|
||||||
|
def test_trailing_only_pad_stripped_leading_content_kept(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
content_w = 80
|
||||||
|
full = _solid(content_w + 64, 32, (0, 0, 0))
|
||||||
|
full.paste(_solid(content_w, 32, (5, 5, 250)), (0, 0))
|
||||||
|
sh.set_scrolling_image(full)
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
assert images[0].width == content_w
|
||||||
|
assert images[0].getpixel((0, 0)) == (5, 5, 250)
|
||||||
|
|
||||||
|
def test_no_margin_image_left_untouched(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
raw = _solid(150, 32, (5, 5, 5))
|
||||||
|
raw.paste(_solid(50, 32, (123, 45, 67)), (0, 0))
|
||||||
|
sh.set_scrolling_image(raw)
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "no_margin")
|
||||||
|
assert images[0].width == 150
|
||||||
|
|
||||||
|
def test_degenerate_all_black_image_left_untouched(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
sh.set_scrolling_image(_solid(50, 32, (0, 0, 0)))
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "all_black")
|
||||||
|
assert images[0].width == 50
|
||||||
|
|
||||||
|
def test_missing_display_width_attribute_left_untouched(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
item = _solid(40, 32, (200, 50, 50))
|
||||||
|
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||||
|
original_width = sh.cached_image.width
|
||||||
|
del sh.display_width
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
assert images[0].width == original_width
|
||||||
|
|
||||||
|
def test_pad_width_not_smaller_than_image_left_untouched(self, adapter):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
sh.set_scrolling_image(_solid(64, 32, (0, 0, 0)))
|
||||||
|
|
||||||
|
images = adapter._get_scroll_helper_content(FakePlugin(sh), "narrow")
|
||||||
|
assert images[0].width == 64
|
||||||
|
|
||||||
|
def test_both_edges_matching_logs_warning(self, adapter, caplog):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
content_w = 80
|
||||||
|
full = _solid(64 + content_w + 64, 32, (0, 0, 0))
|
||||||
|
full.paste(_solid(content_w, 32, (10, 220, 30)), (64, 0))
|
||||||
|
sh.set_scrolling_image(full)
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING, logger="src.vegas_mode.plugin_adapter"):
|
||||||
|
adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
|
||||||
|
assert any("Stripping scroll_helper padding" in r.message for r in caplog.records)
|
||||||
|
|
||||||
|
def test_single_edge_match_logs_info_not_warning(self, adapter, caplog):
|
||||||
|
sh = ScrollHelper(64, 32)
|
||||||
|
item = _solid(40, 32, (200, 50, 50))
|
||||||
|
sh.create_scrolling_image([item], item_gap=10, element_gap=0)
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO, logger="src.vegas_mode.plugin_adapter"):
|
||||||
|
adapter._get_scroll_helper_content(FakePlugin(sh), "ticker")
|
||||||
|
|
||||||
|
strip_records = [r for r in caplog.records if "Stripping scroll_helper padding" in r.message]
|
||||||
|
assert len(strip_records) == 1
|
||||||
|
assert strip_records[0].levelno == logging.INFO
|
||||||
Reference in New Issue
Block a user