1- import time
1+ import html
22import logging
3- import requests
4- import xml .etree .ElementTree as ET
5- import json
6- import random
7- from typing import Dict , Any , List , Tuple , Optional
8- from datetime import datetime , timedelta
93import os
10- import urllib .parse
114import re
12- import html
5+ import requests
6+ import time
7+ import xml .etree .ElementTree as ET
8+ from typing import Dict , Any , List
9+ from datetime import datetime
10+ from src .image_utils import scale_to_max_dimensions
1311from src .config_manager import ConfigManager
1412from PIL import Image , ImageDraw , ImageFont
1513from src .cache_manager import CacheManager
@@ -37,10 +35,11 @@ def __init__(self, config: Dict[str, Any], display_manager, config_manager=None)
3735 self .news_config = config .get ('news_manager' , {})
3836 self .last_update = time .time () # Initialize to current time
3937 self .news_data = {}
38+ self .favicons = {}
4039 self .current_headline_index = 0
4140 self .scroll_position = 0
4241 self .scrolling_image = None # Pre-rendered image for smooth scrolling
43- self .cached_text = None
42+ self .cached_images = []
4443 self .cache_manager = CacheManager ()
4544 self .current_headlines = []
4645 self .headline_start_times = []
@@ -101,6 +100,13 @@ def __init__(self, config: Dict[str, Any], display_manager, config_manager=None)
101100 adapter = HTTPAdapter (max_retries = retry_strategy )
102101 self .session .mount ("http://" , adapter )
103102 self .session .mount ("https://" , adapter )
103+
104+ try :
105+ self .font = ImageFont .truetype (self .font_path , self .font_size )
106+ logger .debug (f"Successfully loaded custom font: { self .font_path } " )
107+ except Exception as e :
108+ logger .warning (f"Failed to load custom font '{ self .font_path } ': { e } . Using default font." )
109+ self .font = ImageFont .load_default ()
104110
105111 logger .debug (f"NewsManager initialized with feeds: { self .enabled_feeds } " )
106112 logger .debug (f"Headlines per feed: { self .headlines_per_feed } " )
@@ -112,7 +118,6 @@ def parse_rss_feed(self, url: str, feed_name: str) -> List[Dict[str, Any]]:
112118 headers = {
113119 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
114120 }
115-
116121 response = self .session .get (url , headers = headers , timeout = 10 )
117122 response .raise_for_status ()
118123
@@ -156,6 +161,16 @@ def parse_rss_feed(self, url: str, feed_name: str) -> List[Dict[str, Any]]:
156161 except Exception as e :
157162 logger .error (f"Error parsing RSS feed { feed_name } ({ url } ): { e } " )
158163 return []
164+
165+ def load_favicon (self , feed_name ):
166+ try :
167+ img_path = os .path .join ('assets' , 'news_logos' , f"{ feed_name .lower ()} .png" )
168+ with Image .open (img_path ) as img :
169+ img = scale_to_max_dimensions (img , 32 , int (self .display_manager .height * 0.8 )).convert ('RGBA' )
170+ self .favicons [feed_name ] = img .copy ()
171+ except Exception as e :
172+ logger .error (f"Error loading favicon for { feed_name } : { e } " )
173+ return
159174
160175 def fetch_news_data (self ):
161176 """Fetch news from all enabled feeds"""
@@ -170,6 +185,7 @@ def fetch_news_data(self):
170185 url = all_feeds [feed_name ]
171186 headlines = self .parse_rss_feed (url , feed_name )
172187 all_headlines .extend (headlines )
188+ self .load_favicon (feed_name )
173189 else :
174190 logger .warning (f"Feed '{ feed_name } ' not found in available feeds" )
175191
@@ -215,81 +231,96 @@ def prepare_headlines_for_display(self):
215231
216232 # Create scrolling text with separators
217233 if display_headlines :
218- text_parts = []
234+ self . cached_images = []
219235 for i , headline in enumerate (display_headlines ):
220- feed_prefix = f"[{ headline ['feed' ]} ] "
221- text_parts .append (feed_prefix + headline ['title' ])
236+ favicon = self .favicons .get (headline ['feed' ])
237+
238+ # Use backup separator and prefix if no logo for feed
239+ separator = " • " if not favicon and i > 0 else ''
240+ feed_prefix = f"[{ headline ['feed' ]} ] " if not favicon else ''
241+ text = separator + feed_prefix + headline ['title' ]
242+
243+ # Calculate text width and X value
244+ text_width = self ._get_text_width (text , self .font )
245+ headline_width = text_width
246+ text_x_pos = 0
247+ if favicon :
248+ text_x_pos = favicon .width + 16
249+ headline_width += text_x_pos
250+
251+ # Draw Image
252+ img = Image .new ('RGB' , (headline_width , self .display_manager .height ), (0 , 0 , 0 ))
253+ draw = ImageDraw .Draw (img )
254+ if favicon :
255+ logo_x = 10
256+ logo_y = (self .display_manager .height - favicon .height ) // 2
257+ img .paste (favicon , (logo_x , logo_y ), favicon )
222258
223- # Join with separators and add spacing
224- separator = " • "
225- self .cached_text = separator .join (text_parts ) + " • " # Add separator at end for smooth loop
259+ # Draw text
260+ text_height = self .font_size
261+ y_pos = (self .display_manager .height - text_height ) // 2
262+ draw .text ((text_x_pos , y_pos ), text , font = self .font , fill = self .text_color )
263+
264+ # Append to cached images for rendering in `create_scrolling_image()`
265+ self .cached_images .append (img )
226266
267+ self .current_headlines = display_headlines
268+
227269 # Calculate text dimensions for perfect scrolling
228270 self .calculate_scroll_dimensions ()
229271 self .create_scrolling_image ()
230272
231- self .current_headlines = display_headlines
232273 logger .debug (f"Prepared { len (display_headlines )} headlines for display" )
233274
234275 def calculate_scroll_dimensions (self ):
235276 """Calculate exact dimensions needed for smooth scrolling"""
236- if not self .cached_text :
277+ if not self .cached_images :
237278 return
238279
239280 try :
240- # Load font
241- try :
242- font = ImageFont .truetype (self .font_path , self .font_size )
243- logger .debug (f"Successfully loaded custom font: { self .font_path } " )
244- except Exception as e :
245- logger .warning (f"Failed to load custom font '{ self .font_path } ': { e } . Using default font." )
246- font = ImageFont .load_default ()
247-
248- # Calculate text width
249- temp_img = Image .new ('RGB' , (1 , 1 ))
250- temp_draw = ImageDraw .Draw (temp_img )
251-
252- # Get text dimensions
253- bbox = temp_draw .textbbox ((0 , 0 ), self .cached_text , font = font )
254- text_width = bbox [2 ] - bbox [0 ]
255- # Add display width gap at the beginning (simulates blank screen)
256281 display_width = self .display_manager .width
257- self .total_scroll_width = display_width + text_width
282+ self .total_scroll_width = display_width
283+ for img in self .cached_images :
284+ self .total_scroll_width += img .width
258285
259286 # Calculate dynamic display duration
260287 self .calculate_dynamic_duration ()
261288
262- logger .debug (f"Text width calculated: { self .total_scroll_width } pixels" )
289+ logger .debug (f"Image width calculated: { self .total_scroll_width } pixels" )
263290 logger .debug (f"Dynamic duration calculated: { self .dynamic_duration } seconds" )
264291
265292 except Exception as e :
266293 logger .error (f"Error calculating scroll dimensions: { e } " )
267- self .total_scroll_width = len (self .cached_text ) * 8 # Fallback estimate
294+ self .total_scroll_width = sum ( len (x [ 'title' ]) for x in self .current_headlines ) * 8 # Fallback estimate
268295 self .calculate_dynamic_duration ()
269296
297+ def _get_text_width (self , text , font ):
298+ temp_img = Image .new ('RGB' , (1 , 1 ))
299+ temp_draw = ImageDraw .Draw (temp_img )
300+
301+ # Get text dimensions
302+ bbox = temp_draw .textbbox ((0 , 0 ), text , font = font )
303+ return bbox [2 ] - bbox [0 ]
304+
305+
270306 def create_scrolling_image (self ):
271307 """Create a pre-rendered image for smooth scrolling."""
272- if not self .cached_text :
308+ if not self .cached_images :
273309 self .scrolling_image = None
274310 return
275311
276- try :
277- font = ImageFont .truetype (self .font_path , self .font_size )
278- except Exception as e :
279- logger .warning (f"Failed to load custom font for pre-rendering: { e } . Using default." )
280- font = ImageFont .load_default ()
281-
282312 height = self .display_manager .height
283313 width = self .total_scroll_width
284314
285315 self .scrolling_image = Image .new ('RGB' , (width , height ), (0 , 0 , 0 ))
286- draw = ImageDraw .Draw (self .scrolling_image )
287316
288- text_height = self .font_size
289- y_pos = (height - text_height ) // 2
290317 # Draw text starting after display width gap (simulates blank screen)
291- display_width = self .display_manager .width
292- draw .text ((display_width , y_pos ), self .cached_text , font = font , fill = self .text_color )
318+ x_pos = self .display_manager .width
319+ for img in self .cached_images :
320+ # Render each cached image and advance the cursor by the width of the image
321+ self .scrolling_image .paste (img , (x_pos , 0 ))
322+ x_pos += img .width
323+
293324 logger .debug ("Pre-rendered scrolling news image created." )
294325
295326 def calculate_dynamic_duration (self ):
@@ -405,22 +436,15 @@ def create_no_news_image(self) -> Image.Image:
405436 img = Image .new ('RGB' , (width , height ), (0 , 0 , 0 ))
406437 draw = ImageDraw .Draw (img )
407438
408- try :
409- font = ImageFont .truetype (self .font_path , self .font_size )
410- logger .debug (f"Successfully loaded custom font: { self .font_path } " )
411- except Exception as e :
412- logger .warning (f"Failed to load custom font '{ self .font_path } ': { e } . Using default font." )
413- font = ImageFont .load_default ()
414-
415439 text = "Loading news..."
416- bbox = draw .textbbox ((0 , 0 ), text , font = font )
440+ bbox = draw .textbbox ((0 , 0 ), text , font = self . font )
417441 text_width = bbox [2 ] - bbox [0 ]
418442 text_height = bbox [3 ] - bbox [1 ]
419443
420444 x = (width - text_width ) // 2
421445 y = (height - text_height ) // 2
422446
423- draw .text ((x , y ), text , font = font , fill = self .text_color )
447+ draw .text ((x , y ), text , font = self . font , fill = self .text_color )
424448 return img
425449
426450 def create_error_image (self , error_msg : str ) -> Image .Image :
@@ -431,22 +455,15 @@ def create_error_image(self, error_msg: str) -> Image.Image:
431455 img = Image .new ('RGB' , (width , height ), (0 , 0 , 0 ))
432456 draw = ImageDraw .Draw (img )
433457
434- try :
435- font = ImageFont .truetype (self .font_path , max (8 , self .font_size - 2 ))
436- logger .debug (f"Successfully loaded custom font: { self .font_path } " )
437- except Exception as e :
438- logger .warning (f"Failed to load custom font '{ self .font_path } ': { e } . Using default font." )
439- font = ImageFont .load_default ()
440-
441458 text = f"News Error: { error_msg [:50 ]} ..."
442- bbox = draw .textbbox ((0 , 0 ), text , font = font )
459+ bbox = draw .textbbox ((0 , 0 ), text , font = self . font )
443460 text_width = bbox [2 ] - bbox [0 ]
444461 text_height = bbox [3 ] - bbox [1 ]
445462
446463 x = max (0 , (width - text_width ) // 2 )
447464 y = (height - text_height ) // 2
448465
449- draw .text ((x , y ), text , font = font , fill = (255 , 0 , 0 ))
466+ draw .text ((x , y ), text , font = self . font , fill = (255 , 0 , 0 ))
450467 return img
451468
452469 def display_news (self , force_clear : bool = False ):
0 commit comments