From dee7fcfd49b27a84eb1b86a5c6f042366660caa5 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Mon, 26 May 2025 11:19:43 -0500 Subject: [PATCH] 1px black outline on text in sports displays --- src/mlb_manager.py | 50 ++++++++++--- src/ncaa_baseball_managers.py | 48 ++++++++---- src/ncaam_basketball_managers.py | 124 ++++++++++++++++++++----------- src/nhl_managers.py | 4 +- 4 files changed, 155 insertions(+), 71 deletions(-) diff --git a/src/mlb_manager.py b/src/mlb_manager.py index b20f9c5e..1228ad62 100644 --- a/src/mlb_manager.py +++ b/src/mlb_manager.py @@ -63,6 +63,27 @@ class BaseMLBManager: logger.error(f"Error loading logo for team {team_abbr}: {e}") return None + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """ + Draw text with a black outline for better readability. + + Args: + draw: ImageDraw object + text: Text to draw + position: (x, y) position to draw the text + font: Font to use + fill: Text color (default: white) + outline_color: Outline color (default: black) + """ + x, y = position + + # Draw the outline by drawing the text in black at 8 positions around the text + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + + # Draw the text in the specified color + draw.text((x, y), text, font=font, fill=fill) + def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: """Draw base indicators on the display.""" base_size = 8 # Increased from 6 to 8 for better visibility @@ -165,13 +186,15 @@ class BaseMLBManager: date_width = draw.textlength(game_date, font=date_font) date_x = (width - date_width) // 2 date_y = (height - date_font.size) // 2 - 3 - draw.text((date_x, date_y), game_date, font=date_font, fill=(255, 255, 255)) + # draw.text((date_x, date_y), game_date, font=date_font, fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_date, (date_x, date_y), date_font) # Draw time below date time_width = draw.textlength(game_time_str, font=time_font) time_x = (width - time_width) // 2 time_y = date_y + 10 - draw.text((time_x, time_y), game_time_str, font=time_font, fill=(255, 255, 255)) + # draw.text((time_x, time_y), game_time_str, font=time_font, fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_time_str, (time_x, time_y), time_font) # For recent/final games, show scores and status elif game_data['status'] in ['status_final', 'final', 'completed']: @@ -198,7 +221,8 @@ class BaseMLBManager: score_width = draw.textlength(score_text, font=score_font) score_x = (width - score_width) // 2 score_y = height - score_font.size - 2 - draw.text((score_x, score_y), score_text, font=score_font, fill=(255, 255, 255)) + # draw.text((score_x, score_y), score_text, font=score_font, fill=(255, 255, 255)) + self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font) return image @@ -615,7 +639,8 @@ class MLBLiveManager(BaseMLBManager): inning_width = inning_bbox[2] - inning_bbox[0] inning_x = (width - inning_width) // 2 inning_y = 0 # Position near top center - draw.text((inning_x, inning_y), inning_text, fill=(255, 255, 255), font=self.display_manager.font) + # draw.text((inning_x, inning_y), inning_text, fill=(255, 255, 255), font=self.display_manager.font) + self._draw_text_with_outline(draw, inning_text, (inning_x, inning_y), self.display_manager.font) # --- REVISED BASES AND OUTS DRAWING --- bases_occupied = game_data['bases_occupied'] # [1st, 2nd, 3rd] @@ -718,7 +743,9 @@ class MLBLiveManager(BaseMLBManager): # Ensure draw object is set and draw text self.display_manager.draw = draw - self.display_manager._draw_bdf_text(count_text, count_x, count_y, text_color, font=bdf_font) + # self.display_manager._draw_bdf_text(count_text, count_x, count_y, text_color, font=bdf_font) + # Use _draw_text_with_outline for count text + self._draw_text_with_outline(draw, count_text, (count_x, count_y), bdf_font, fill=text_color) # Draw Team:Score at the bottom score_font = self.display_manager.font # Use PressStart2P @@ -728,12 +755,13 @@ class MLBLiveManager(BaseMLBManager): # Helper function for outlined text def draw_bottom_outlined_text(x, y, text): # Draw outline - draw.text((x-1, y), text, font=score_font, fill=outline_color) - draw.text((x+1, y), text, font=score_font, fill=outline_color) - draw.text((x, y-1), text, font=score_font, fill=outline_color) - draw.text((x, y+1), text, font=score_font, fill=outline_color) - # Draw main text - draw.text((x, y), text, font=score_font, fill=score_text_color) + # draw.text((x-1, y), text, font=score_font, fill=outline_color) + # draw.text((x+1, y), text, font=score_font, fill=outline_color) + # draw.text((x, y-1), text, font=score_font, fill=outline_color) + # draw.text((x, y+1), text, font=score_font, fill=outline_color) + # # Draw main text + # draw.text((x, y), text, font=score_font, fill=score_text_color) + self._draw_text_with_outline(draw, text, (x,y), score_font, fill=score_text_color, outline_color=outline_color) away_abbr = game_data['away_team'] home_abbr = game_data['home_team'] diff --git a/src/ncaa_baseball_managers.py b/src/ncaa_baseball_managers.py index 0d8b789f..ce4eb2a6 100644 --- a/src/ncaa_baseball_managers.py +++ b/src/ncaa_baseball_managers.py @@ -66,6 +66,27 @@ class BaseNCAABaseballManager: logger.error(f"[NCAABaseball] Error loading logo for team {team_abbr}: {e}") return None + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """ + Draw text with a black outline for better readability. + + Args: + draw: ImageDraw object + text: Text to draw + position: (x, y) position to draw the text + font: Font to use + fill: Text color (default: white) + outline_color: Outline color (default: black) + """ + x, y = position + + # Draw the outline by drawing the text in black at 8 positions around the text + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + + # Draw the text in the specified color + draw.text((x, y), text, font=font, fill=fill) + def _draw_base_indicators(self, draw: ImageDraw.Draw, bases_occupied: List[bool], center_x: int, y: int) -> None: """Draw base indicators on the display.""" base_size = 8 @@ -120,7 +141,8 @@ class BaseNCAABaseballManager: status_x = (width - status_width) // 2 status_y = 2 self.display_manager.draw = draw - self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font) + status_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Using a default small font + self._draw_text_with_outline(draw, status_text, (status_x, status_y), status_font) game_time = datetime.fromisoformat(game_data['start_time'].replace('Z', '+00:00')) timezone_str = self.config.get('timezone', 'UTC') @@ -140,23 +162,23 @@ class BaseNCAABaseballManager: date_width = draw.textlength(game_date, font=date_font) date_x = (width - date_width) // 2 - date_y = (height - date_font.size) // 2 - 3 - draw.text((date_x, date_y), game_date, font=date_font, fill=(255, 255, 255)) + date_y = (height - date_font.getmetrics()[0]) // 2 - 3 # Adjusted for font metrics + self._draw_text_with_outline(draw, game_date, (date_x, date_y), date_font) time_width = draw.textlength(game_time_str, font=time_font) time_x = (width - time_width) // 2 time_y = date_y + 10 - draw.text((time_x, time_y), game_time_str, font=time_font, fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_time_str, (time_x, time_y), time_font) # For recent/final games, show scores and status elif game_data['status'] in ['status_final', 'final', 'completed']: status_text = "Final" - self.display_manager.calendar_font.set_char_size(height=7*64) - status_width = self.display_manager.get_text_width(status_text, self.display_manager.calendar_font) + status_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) # Using a default small font + status_width = draw.textlength(status_text, font=status_font) status_x = (width - status_width) // 2 status_y = 2 self.display_manager.draw = draw - self.display_manager._draw_bdf_text(status_text, status_x, status_y, color=(255, 255, 255), font=self.display_manager.calendar_font) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), status_font) away_score = str(game_data['away_score']) home_score = str(game_data['home_score']) @@ -165,8 +187,8 @@ class BaseNCAABaseballManager: score_width = draw.textlength(score_text, font=score_font) score_x = (width - score_width) // 2 - score_y = height - score_font.size - 2 - draw.text((score_x, score_y), score_text, font=score_font, fill=(255, 255, 255)) + score_y = height - score_font.getmetrics()[0] - 2 # Adjusted for font metrics + self._draw_text_with_outline(draw, score_text, (score_x, score_y), score_font) return image @@ -494,7 +516,7 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager): inning_width = inning_bbox[2] - inning_bbox[0] inning_x = (width - inning_width) // 2 inning_y = 0 - draw.text((inning_x, inning_y), inning_text, fill=(255, 255, 255), font=self.display_manager.font) + self._draw_text_with_outline(draw, inning_text, (inning_x, inning_y), self.display_manager.font) bases_occupied = game_data['bases_occupied'] outs = game_data.get('outs', 0) @@ -555,13 +577,11 @@ class NCAABaseballLiveManager(BaseNCAABaseballManager): count_y = cluster_bottom_y + 2 count_x = bases_origin_x + (base_cluster_width - count_text_width) // 2 self.display_manager.draw = draw - self.display_manager._draw_bdf_text(count_text, count_x, count_y, text_color, font=bdf_font) + self._draw_text_with_outline(draw, count_text, (count_x, count_y), bdf_font, fill=text_color) score_font = self.display_manager.font; outline_color = (0, 0, 0); score_text_color = (255, 255, 255) def draw_bottom_outlined_text(x, y, text): - draw.text((x-1, y), text, font=score_font, fill=outline_color); draw.text((x+1, y), text, font=score_font, fill=outline_color) - draw.text((x, y-1), text, font=score_font, fill=outline_color); draw.text((x, y+1), text, font=score_font, fill=outline_color) - draw.text((x, y), text, font=score_font, fill=score_text_color) + self._draw_text_with_outline(draw, text, (x,y), score_font, fill=score_text_color, outline_color=outline_color) away_abbr = game_data['away_team']; home_abbr = game_data['home_team'] away_score_str = str(game_data['away_score']); home_score_str = str(game_data['home_score']) away_text = f"{away_abbr}:{away_score_str}"; home_text = f"{home_abbr}:{home_score_str}" diff --git a/src/ncaam_basketball_managers.py b/src/ncaam_basketball_managers.py index 28579952..cec348cb 100644 --- a/src/ncaam_basketball_managers.py +++ b/src/ncaam_basketball_managers.py @@ -249,6 +249,27 @@ class BaseNCAAMBasketballManager: self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True) return None + def _draw_text_with_outline(self, draw, text, position, font, fill=(255, 255, 255), outline_color=(0, 0, 0)): + """ + Draw text with a black outline for better readability. + + Args: + draw: ImageDraw object + text: Text to draw + position: (x, y) position to draw the text + font: Font to use + fill: Text color (default: white) + outline_color: Outline color (default: black) + """ + x, y = position + + # Draw the outline by drawing the text in black at 8 positions around the text + for dx, dy in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]: + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + + # Draw the text in the specified color + draw.text((x, y), text, font=font, fill=fill) + @classmethod def _fetch_shared_data(cls, date_str: str = None) -> Optional[Dict]: """Fetch and cache data for all managers to share.""" @@ -472,19 +493,19 @@ class BaseNCAAMBasketballManager: status_width = draw.textlength(status_text, font=self.fonts['status']) status_x = (self.display_width - status_width) // 2 status_y = 2 - draw.text((status_x, status_y), status_text, font=self.fonts['status'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['status']) # Calculate position for the date text (centered horizontally, below "Next Game") date_width = draw.textlength(game_date, font=self.fonts['time']) date_x = (self.display_width - date_width) // 2 date_y = center_y - 5 # Position in center - draw.text((date_x, date_y), game_date, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_date, (date_x, date_y), self.fonts['time']) # Calculate position for the time text (centered horizontally, in center) time_width = draw.textlength(game_time, font=self.fonts['time']) time_x = (self.display_width - time_width) // 2 time_y = date_y + 10 # Position below date - draw.text((time_x, time_y), game_time, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, game_time, (time_x, time_y), self.fonts['time']) else: # For live/final games, show scores and period/time home_score = str(game.get("home_score", "0")) @@ -495,7 +516,7 @@ class BaseNCAAMBasketballManager: score_width = draw.textlength(score_text, font=self.fonts['score']) score_x = (self.display_width - score_width) // 2 score_y = self.display_height - 10 - draw.text((score_x, score_y), score_text, font=self.fonts['score'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, score_text, (score_x, score_y), self.fonts['score']) # Draw period and time or Final if game.get("is_final", False): @@ -503,13 +524,13 @@ class BaseNCAAMBasketballManager: status_width = draw.textlength(status_text, font=self.fonts['time']) status_x = (self.display_width - status_width) // 2 status_y = 5 - draw.text((status_x, status_y), status_text, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time']) elif game.get("is_halftime", False): status_text = "Halftime" status_width = draw.textlength(status_text, font=self.fonts['time']) status_x = (self.display_width - status_width) // 2 status_y = 5 - draw.text((status_x, status_y), status_text, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, status_text, (status_x, status_y), self.fonts['time']) else: period = game.get("period", 0) clock = game.get("clock", "0:00") @@ -530,13 +551,13 @@ class BaseNCAAMBasketballManager: period_width = draw.textlength(period_text, font=self.fonts['time']) period_x = (self.display_width - period_width) // 2 period_y = 1 - draw.text((period_x, period_y), period_text, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, period_text, (period_x, period_y), self.fonts['time']) # Draw clock below period clock_width = draw.textlength(clock, font=self.fonts['time']) clock_x = (self.display_width - clock_width) // 2 clock_y = period_y + 10 # Position below period - draw.text((clock_x, clock_y), clock, font=self.fonts['time'], fill=(255, 255, 255)) + self._draw_text_with_outline(draw, clock, (clock_x, clock_y), self.fonts['time']) # Display the image self.display_manager.image.paste(main_img, (0, 0)) @@ -606,29 +627,33 @@ class NCAAMBasketballLiveManager(BaseNCAAMBasketballManager): if self.test_mode: # For testing, update the clock and maybe period if self.current_game: - minutes = int(self.current_game["clock"].split(":")[0]) - seconds = int(self.current_game["clock"].split(":")[1]) - seconds -= 1 - if seconds < 0: - seconds = 59 - minutes -= 1 - if minutes < 0: - # Simulate moving from H1 to H2 or H2 to OT - if self.current_game["period"] == 1: - self.current_game["period"] = 2 - minutes = 19 # Reset clock for H2 - seconds = 59 - elif self.current_game["period"] == 2: - self.current_game["period"] = 3 # Go to OT - minutes = 4 # Reset clock for OT - seconds = 59 - elif self.current_game["period"] >= 3: # OT+ - self.current_game["period"] += 1 - minutes = 4 - seconds = 59 - self.current_game["clock"] = f"{minutes:02d}:{seconds:02d}" - # Always update display in test mode - self.display(force_clear=True) + try: # Add try-except for robust clock parsing + minutes_str, seconds_str = self.current_game["clock"].split(":") + minutes = int(minutes_str) + seconds = int(seconds_str) + seconds -= 1 + if seconds < 0: + seconds = 59 + minutes -= 1 + if minutes < 0: + # Simulate moving from H1 to H2 or H2 to OT + if self.current_game["period"] == 1: + self.current_game["period"] = 2 + minutes = 19 # Reset clock for H2 + seconds = 59 + elif self.current_game["period"] == 2: + self.current_game["period"] = 3 # Go to OT + minutes = 4 # Reset clock for OT + seconds = 59 + elif self.current_game["period"] >= 3: # OT+ + self.current_game["period"] += 1 + minutes = 4 + seconds = 59 + self.current_game["clock"] = f"{minutes:02d}:{seconds:02d}" + # Always update display in test mode + self.display(force_clear=True) + except ValueError: + self.logger.warning(f"[NCAAMBasketball] Could not parse clock in test mode: {self.current_game.get('clock')}") else: # Fetch live game data from ESPN API data = self._fetch_data() @@ -665,7 +690,19 @@ class NCAAMBasketballLiveManager(BaseNCAAMBasketballManager): if new_live_games: self.logger.info(f"[NCAAMBasketball] Found {len(new_live_games)} live games") for game in new_live_games: - status_str = f"H{game['period']}" if game['period'] <=2 else f"OT{game['period']-2 if game['period'] > 3 else ''}" + period = game.get('period', 0) + if game.get('is_halftime'): + status_str = "Halftime" + elif period == 1: + status_str = "H1" + elif period == 2: + status_str = "H2" + elif period == 3: + status_str = "OT" + elif period > 3: + status_str = f"{period-2}OT" + else: + status_str = f"P{period}" # Fallback self.logger.info(f"[NCAAMBasketball] Live game: {game['away_abbr']} vs {game['home_abbr']} - {status_str}, {game['clock']}") if has_favorite_team: self.logger.info("[NCAAMBasketball] Found live game(s) for favorite team(s)") @@ -676,24 +713,23 @@ class NCAAMBasketballLiveManager(BaseNCAAMBasketballManager): if new_live_games: # Update the current game with the latest data if it matches current_game_updated = False - for new_game in new_live_games: - if self.current_game and ( - (new_game["home_abbr"] == self.current_game["home_abbr"] and - new_game["away_abbr"] == self.current_game["away_abbr"]) or - (new_game["home_abbr"] == self.current_game["away_abbr"] and - new_game["away_abbr"] == self.current_game["home_abbr"]) - ): - self.current_game = new_game - current_game_updated = True - break + if self.current_game: # Ensure current_game is not None + for new_game in new_live_games: + if (new_game["home_abbr"] == self.current_game["home_abbr"] and + new_game["away_abbr"] == self.current_game["away_abbr"]) or \ + (new_game["home_abbr"] == self.current_game["away_abbr"] and + new_game["away_abbr"] == self.current_game["home_abbr"]): + self.current_game = new_game + current_game_updated = True + break # Only update the games list if there's a structural change if not self.live_games or set(game["away_abbr"] + game["home_abbr"] for game in new_live_games) != set(game["away_abbr"] + game["home_abbr"] for game in self.live_games): self.live_games = new_live_games # If we don't have a current game, it's not in the new list, or the list was empty, reset - if not self.current_game or not current_game_updated or not self.live_games: + if not self.current_game or not current_game_updated or not self.live_games: # Check self.live_games is not empty self.current_game_index = 0 - self.current_game = self.live_games[0] if self.live_games else None + self.current_game = self.live_games[0] if self.live_games else None # Handle empty self.live_games self.last_game_switch = current_time # Cycle through games if multiple are present diff --git a/src/nhl_managers.py b/src/nhl_managers.py index 1e15f437..ceed5e6c 100644 --- a/src/nhl_managers.py +++ b/src/nhl_managers.py @@ -426,14 +426,14 @@ class BaseNHLManager: center_y = self.display_height // 2 # Draw home team logo (far right, extending beyond screen) - home_x = self.display_width - home_logo.width + home_x = self.display_width - home_logo.width + 2 home_y = center_y - (home_logo.height // 2) # Paste the home logo onto the overlay overlay.paste(home_logo, (home_x, home_y), home_logo) # Draw away team logo (far left, extending beyond screen) - away_x = 0 + away_x = -2 away_y = center_y - (away_logo.height // 2) # Paste the away logo onto the overlay