Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.1.10"
"latest_version": "1.2.0"
},
{
"id": "of-the-day",
Expand Down Expand Up @@ -806,10 +806,10 @@
"plugin_path": "plugins/stock-news",
"stars": 0,
"downloads": 0,
"last_updated": "2026-07-17",
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.4.6"
"latest_version": "2.5.0"
},
{
"id": "stocks",
Expand All @@ -829,10 +829,10 @@
"plugin_path": "plugins/ledmatrix-stocks",
"stars": 0,
"downloads": 0,
"last_updated": "2026-07-31",
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.6.0",
"latest_version": "2.7.0",
"icon": "fas fa-chart-line"
},
{
Expand Down
17 changes: 16 additions & 1 deletion plugins/ledmatrix-stocks/chart_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
from src.common import TextHelper


def _pixel_draw(image):
"""ImageDraw that renders text crisply on the LED grid.

PIL anti-aliases by default, blending glyph edges into dim partial-lit
pixels. On a 1:1 LED matrix those read as blur rather than smoothing, so
every draw surface here -- including scratch canvases used only to measure
text -- sets fontmode "1" for 1-bit glyph rendering. Measuring through the
same helper keeps metrics and rendering in agreement.
"""
draw = ImageDraw.Draw(image)
draw.fontmode = "1"
return draw



class StockChartRenderer:
"""Handles rendering of stock and cryptocurrency charts."""

Expand Down Expand Up @@ -67,7 +82,7 @@ def _create_chart_image(self, symbol: str, data: Dict[str, Any]) -> Optional[Ima
try:
# Create image
image = Image.new('RGB', (self.display_width, self.display_height), self.chart_colors['background'])
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# Get price history
price_history = data['price_history']
Expand Down
23 changes: 19 additions & 4 deletions plugins/ledmatrix-stocks/display_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@
from src.common import ScrollHelper, LogoHelper, TextHelper


def _pixel_draw(image):
"""ImageDraw that renders text crisply on the LED grid.

PIL anti-aliases by default, blending glyph edges into dim partial-lit
pixels. On a 1:1 LED matrix those read as blur rather than smoothing, so
every draw surface here -- including scratch canvases used only to measure
text -- sets fontmode "1" for 1-bit glyph rendering. Measuring through the
same helper keeps metrics and rendering in agreement.
"""
draw = ImageDraw.Draw(image)
draw.fontmode = "1"
return draw



class StockDisplayRenderer:
"""Handles rendering of stock and cryptocurrency displays."""

Expand Down Expand Up @@ -232,7 +247,7 @@ def create_stock_display(self, symbol: str, data: Dict[str, Any]) -> Image.Image

# Measure the text on a scratch canvas so the real canvas can be sized to
# the content (rather than a display-scaled guess that then needs clamping).
mdraw = ImageDraw.Draw(Image.new('RGB', (1, 1)))
mdraw = _pixel_draw(Image.new('RGB', (1, 1)))
symbol_bbox = mdraw.textbbox((0, 0), symbol_text, font=symbol_font)
price_bbox = mdraw.textbbox((0, 0), price_text, font=price_font)
if change_text:
Expand Down Expand Up @@ -270,7 +285,7 @@ def create_stock_display(self, symbol: str, data: Dict[str, Any]) -> Image.Image

# Real canvas, sized to the content
image = Image.new('RGB', (width, height), (0, 0, 0))
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# Draw the logo on the left, vertically centered
if logo:
Expand Down Expand Up @@ -312,7 +327,7 @@ def create_static_display(self, symbol: str, data: Dict[str, Any]) -> Image.Imag
"""Create a static display for one stock/crypto (no scrolling)."""
# Ensure dimensions are integers
image = Image.new('RGB', (int(self.display_width), int(self.display_height)), (0, 0, 0))
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

is_crypto = data.get('is_crypto', False)

Expand Down Expand Up @@ -512,7 +527,7 @@ def _create_error_display(self) -> Image.Image:
"""Create an error display when no data is available."""
# Ensure dimensions are integers
image = Image.new('RGB', (int(self.display_width), int(self.display_height)), (0, 0, 0))
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# Use symbol font for error display
error_font = self.symbol_font
Expand Down
10 changes: 8 additions & 2 deletions plugins/ledmatrix-stocks/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ledmatrix-stocks",
"name": "Stock & Crypto Ticker",
"version": "2.6.0",
"version": "2.7.0",
"description": "Displays stock tickers with prices, changes, and optional charts for stocks and cryptocurrencies. Supports scroll and switch display modes.",
"author": "LEDMatrix Team",
"homepage": "https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/ledmatrix-stocks",
Expand Down Expand Up @@ -39,6 +39,12 @@
}
],
"versions": [
{
"version": "2.7.0",
"released": "2026-08-05",
"ledmatrix_min_version": "2.0.0",
"notes": "Pixel-perfect text: all draw surfaces in the display and chart renderers now render with anti-aliasing off, so custom font sizes stay hard-edged on the LED grid. Default sizes were already on the font's pixel grid, so the default look is unchanged."
},
{
"version": "2.6.0",
"released": "2026-07-31",
Expand Down Expand Up @@ -104,7 +110,7 @@
"ledmatrix_min_version": "2.0.0"
}
],
"last_updated": "2026-07-31",
"last_updated": "2026-08-05",
"stars": 0,
"downloads": 0,
"verified": true,
Expand Down
33 changes: 26 additions & 7 deletions plugins/odds-ticker/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@
from pathlib import Path
import numpy as np


def _pixel_draw(image):
"""ImageDraw that renders text crisply on the LED grid.

PIL anti-aliases by default, blending glyph edges into dim partial-lit
pixels. On a 1:1 LED matrix those read as blur rather than smoothing. The
ticker's default element sizes are on Press Start 2P's 8px grid and so were
already crisp, but any size a user picks in the customization UI was not --
fontmode "1" makes 1-bit rendering unconditional. Scratch canvases used only
to measure text go through here too, so metrics match what gets drawn.
"""
draw = ImageDraw.Draw(image)
draw.fontmode = "1"
return draw


# Import will be handled by the plugin system
try:
from src.plugin_system.base_plugin import BasePlugin
Expand Down Expand Up @@ -1644,8 +1660,11 @@ def _create_game_display(self, game: Dict[str, Any]) -> Image.Image:
width = self.display_manager.matrix.width
height = self.display_manager.matrix.height

# Make logos use most of the display height, with a small margin
logo_size = int(height * 1.2)
# Fit logos inside the panel. This used to be int(height * 1.2), which
# with the (height - logo_size) // 2 centering below resolved to a
# negative y -- so the top and bottom of every team logo was cropped off
# the panel.
logo_size = height
h_padding = 4 # Use a consistent horizontal padding

# Fonts - use custom fonts from config
Expand Down Expand Up @@ -1822,7 +1841,7 @@ def _create_game_display(self, game: Dict[str, Any]) -> Image.Image:
time_text = "TBD"

# Datetime column width
temp_draw = ImageDraw.Draw(Image.new('RGB', (1, 1)))
temp_draw = _pixel_draw(Image.new('RGB', (1, 1)))
day_width = int(temp_draw.textlength(day_text, font=datetime_font))
date_width = int(temp_draw.textlength(date_text, font=datetime_font))
time_width = int(temp_draw.textlength(time_text, font=datetime_font))
Expand Down Expand Up @@ -2032,7 +2051,7 @@ def _create_game_display(self, game: Dict[str, Any]) -> Image.Image:

# --- Create final image ---
image = Image.new('RGB', (int(total_width), height), color=(0, 0, 0))
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# --- Draw elements ---
current_x = 0
Expand Down Expand Up @@ -2202,7 +2221,7 @@ def _create_ticker_image(self):
if idx < len(game_images) - 1:
bar_x = current_x + gap_width // 2
# Use ImageDraw for more efficient drawing
draw = ImageDraw.Draw(self.ticker_image)
draw = _pixel_draw(self.ticker_image)
draw.line([(bar_x, 0), (bar_x, height - 1)], fill=(255, 255, 255), width=1)
current_x += gap_width

Expand Down Expand Up @@ -2804,7 +2823,7 @@ def _display_fallback_message(self):

# Create a simple fallback image with a brighter background
image = Image.new('RGB', (width, height), color=(50, 50, 50)) # Dark gray instead of black
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# Draw a simple message with larger font
message = "No odds data"
Expand All @@ -2820,7 +2839,7 @@ def _display_fallback_message(self):

# Display the fallback image
self.display_manager.image = image
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
self.display_manager.draw = _pixel_draw(self.display_manager.image)
self.display_manager.update_display()

logger.info("Fallback message display completed")
Expand Down
10 changes: 8 additions & 2 deletions plugins/odds-ticker/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "odds-ticker",
"name": "Odds Ticker",
"version": "1.1.10",
"version": "1.2.0",
"description": "Displays scrolling odds and betting lines for upcoming games across multiple sports leagues including NFL, NBA, MLB, NCAA Football, and more",
"author": "ChuckBuilds",
"category": "sports",
Expand All @@ -20,6 +20,12 @@
"branch": "main",
"plugin_path": "plugins/odds-ticker",
"versions": [
{
"version": "1.2.0",
"released": "2026-08-05",
"ledmatrix_min_version": "2.0.0",
"notes": "Team logos now fit inside the panel instead of being scaled to 120% of the display height and cropped top and bottom. Text also renders with anti-aliasing off across all draw surfaces, so custom font sizes stay hard-edged; the default sizes were already on the font's pixel grid."
},
{
"version": "1.1.10",
"released": "2026-08-05",
Expand All @@ -40,7 +46,7 @@
{
"released": "2026-07-18",
"version": "1.1.7",
"notes": "Fix config save failing with a 400 validation error. The per-league favorite-team pickers hardcoded several wrong team abbreviations that didn't match ESPN's (Rams LA\u2192LAR, White Sox CWS\u2192CHW, A's OAK\u2192ATH, Warriors GSW\u2192GS, Pelicans NOP\u2192NO, Knicks NYK\u2192NY, Spurs SAS\u2192SA, Jazz UTA\u2192UTAH, Wizards WAS\u2192WSH, NHL Kings LAK\u2192LA, Capitals WAS\u2192WSH), so valid favorites were rejected on save and never matched games. Also removed the overly strict regex on the NCAA/MiLB free-text team fields so documented values like AP_TOP_25 are accepted.",
"notes": "Fix config save failing with a 400 validation error. The per-league favorite-team pickers hardcoded several wrong team abbreviations that didn't match ESPN's (Rams LA→LAR, White Sox CWS→CHW, A's OAK→ATH, Warriors GSW→GS, Pelicans NOP→NO, Knicks NYK→NY, Spurs SAS→SA, Jazz UTA→UTAH, Wizards WAS→WSH, NHL Kings LAK→LA, Capitals WAS→WSH), so valid favorites were rejected on save and never matched games. Also removed the overly strict regex on the NCAA/MiLB free-text team fields so documented values like AP_TOP_25 are accepted.",
"ledmatrix_min": "2.0.0"
},
{
Expand Down
37 changes: 28 additions & 9 deletions plugins/odds-ticker/odds_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path


def _pixel_draw(image):
"""ImageDraw that renders text crisply on the LED grid.

PIL anti-aliases by default, blending glyph edges into dim partial-lit
pixels. On a 1:1 LED matrix those read as blur rather than smoothing. The
ticker's default element sizes are on Press Start 2P's 8px grid and so were
already crisp, but any size a user picks in the customization UI was not --
fontmode "1" makes 1-bit rendering unconditional. Scratch canvases used only
to measure text go through here too, so metrics match what gets drawn.
"""
draw = ImageDraw.Draw(image)
draw.fontmode = "1"
return draw


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -253,8 +269,11 @@ def _create_game_display(self, game: Dict) -> Image.Image:
width = self.display_manager.matrix.width
height = self.display_manager.matrix.height

# Make logos use most of the display height, with a small margin
logo_size = int(height * 1.2)
# Fit logos inside the panel. This used to be int(height * 1.2), which
# with the (height - logo_size) // 2 centering below resolved to a
# negative y -- so the top and bottom of every team logo was cropped off
# the panel.
logo_size = height
h_padding = 4 # Use a consistent horizontal padding

# Fonts - use custom fonts from config
Expand Down Expand Up @@ -331,7 +350,7 @@ def _create_game_display(self, game: Dict) -> Image.Image:
time_text = "TBD"

# Calculate column widths (like original)
temp_draw = ImageDraw.Draw(Image.new('RGB', (1, 1)))
temp_draw = _pixel_draw(Image.new('RGB', (1, 1)))
day_width = int(temp_draw.textlength(day_text, font=datetime_font))
date_width = int(temp_draw.textlength(date_text, font=datetime_font))
time_width = int(temp_draw.textlength(time_text, font=datetime_font))
Expand Down Expand Up @@ -385,7 +404,7 @@ def _create_game_display(self, game: Dict) -> Image.Image:

# Create the image
image = Image.new('RGB', (int(total_width), height), color=(0, 0, 0))
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# --- Draw elements (exactly like original) ---
current_x = 0
Expand Down Expand Up @@ -563,7 +582,7 @@ def _create_no_data_image(self) -> Image.Image:
matrix_height = self.display_manager.matrix.height

img = Image.new('RGB', (matrix_width, matrix_height), (0, 0, 0))
draw = ImageDraw.Draw(img)
draw = _pixel_draw(img)
draw.text((10, 12), "No Odds Available",
font=self.team_font, fill=(150, 150, 150))

Expand All @@ -579,7 +598,7 @@ def _display_fallback_message(self):

# Create a simple fallback image with a brighter background
image = Image.new('RGB', (width, height), color=(50, 50, 50)) # Dark gray instead of black
draw = ImageDraw.Draw(image)
draw = _pixel_draw(image)

# Draw a simple message with larger font
message = "No odds data"
Expand All @@ -596,7 +615,7 @@ def _display_fallback_message(self):
# Display the fallback image
self.display_manager.image = image
if hasattr(self.display_manager, 'draw'):
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
self.display_manager.draw = _pixel_draw(self.display_manager.image)
self.display_manager.update_display()

except Exception as e:
Expand Down Expand Up @@ -627,7 +646,7 @@ def _create_error_image(self, message: str) -> Image.Image:
matrix_height = self.display_manager.matrix.height

img = Image.new('RGB', (matrix_width, matrix_height), (0, 0, 0))
draw = ImageDraw.Draw(img)
draw = _pixel_draw(img)
draw.text((10, 12), message,
font=self.team_font, fill=(255, 0, 0))

Expand Down Expand Up @@ -760,7 +779,7 @@ def render_scrolling_ticker(self, ticker_image: Image.Image) -> bool:
# Display the cropped image
self.display_manager.image = visible_image
if hasattr(self.display_manager, 'draw'):
self.display_manager.draw = ImageDraw.Draw(self.display_manager.image)
self.display_manager.draw = _pixel_draw(self.display_manager.image)

# Add timeout protection for display update to prevent hanging
try:
Expand Down
Loading
Loading