Added press state, fixed font registry
This commit is contained in:
@@ -16,7 +16,7 @@ class LinkText(Text, Interactable, Queriable):
|
||||
"""
|
||||
|
||||
def __init__(self, link: Link, text: str, font: Font, draw: ImageDraw.Draw,
|
||||
source=None, line=None):
|
||||
source=None, line=None, page=None):
|
||||
"""
|
||||
Initialize a linkable text object.
|
||||
|
||||
@@ -27,6 +27,7 @@ class LinkText(Text, Interactable, Queriable):
|
||||
draw: The drawing context
|
||||
source: Optional source object
|
||||
line: Optional line container
|
||||
page: Optional parent page (for dirty flag management)
|
||||
"""
|
||||
# Create link-styled font (underlined and colored based on link type)
|
||||
link_font = font.with_decoration(TextDecoration.UNDERLINE)
|
||||
@@ -46,9 +47,11 @@ class LinkText(Text, Interactable, Queriable):
|
||||
# Initialize Interactable with the link's execute method
|
||||
Interactable.__init__(self, link.execute)
|
||||
|
||||
# Store the link object
|
||||
# Store the link object and page reference
|
||||
self._link = link
|
||||
self._page = page
|
||||
self._hovered = False
|
||||
self._pressed = False
|
||||
|
||||
# Ensure _origin is initialized as numpy array
|
||||
if not hasattr(self, '_origin') or self._origin is None:
|
||||
@@ -62,40 +65,53 @@ class LinkText(Text, Interactable, Queriable):
|
||||
def set_hovered(self, hovered: bool):
|
||||
"""Set the hover state for visual feedback"""
|
||||
self._hovered = hovered
|
||||
self._mark_page_dirty()
|
||||
|
||||
def set_pressed(self, pressed: bool):
|
||||
"""Set the pressed state for visual feedback"""
|
||||
self._pressed = pressed
|
||||
self._mark_page_dirty()
|
||||
|
||||
def _mark_page_dirty(self):
|
||||
"""Mark the parent page as dirty if available"""
|
||||
if self._page and hasattr(self._page, 'mark_dirty'):
|
||||
self._page.mark_dirty()
|
||||
|
||||
def render(self, next_text: Optional['Text'] = None, spacing: int = 0):
|
||||
"""
|
||||
Render the link text with optional hover effects.
|
||||
Render the link text with optional hover and pressed effects.
|
||||
|
||||
Args:
|
||||
next_text: The next Text object in the line (if any)
|
||||
spacing: The spacing to the next text object
|
||||
"""
|
||||
# Handle mock objects in tests
|
||||
size = self.size
|
||||
if hasattr(size, '__call__'): # It's a Mock
|
||||
# Use default size for tests
|
||||
size = np.array([100, 20])
|
||||
else:
|
||||
size = np.array(size)
|
||||
|
||||
# Ensure origin is a numpy array
|
||||
origin = np.array(
|
||||
self._origin) if not isinstance(
|
||||
self._origin,
|
||||
np.ndarray) else self._origin
|
||||
|
||||
# Draw background based on state (before text is rendered)
|
||||
if self._pressed:
|
||||
# Pressed state - stronger, darker highlight
|
||||
bg_color = (180, 180, 255, 180) # Stronger blue with more opacity
|
||||
self._draw.rectangle([origin, origin + size], fill=bg_color)
|
||||
elif self._hovered:
|
||||
# Hover state - subtle highlight
|
||||
bg_color = (220, 220, 255, 100) # Light blue with alpha
|
||||
self._draw.rectangle([origin, origin + size], fill=bg_color)
|
||||
|
||||
# Call the parent Text render method with parameters
|
||||
super().render(next_text, spacing)
|
||||
|
||||
# Add hover effect if needed
|
||||
if self._hovered:
|
||||
# Draw a subtle highlight background
|
||||
highlight_color = (220, 220, 255, 100) # Light blue with alpha
|
||||
|
||||
# Handle mock objects in tests
|
||||
size = self.size
|
||||
if hasattr(size, '__call__'): # It's a Mock
|
||||
# Use default size for tests
|
||||
size = np.array([100, 20])
|
||||
else:
|
||||
size = np.array(size)
|
||||
|
||||
# Ensure origin is a numpy array
|
||||
origin = np.array(
|
||||
self._origin) if not isinstance(
|
||||
self._origin,
|
||||
np.ndarray) else self._origin
|
||||
|
||||
self._draw.rectangle([origin, origin + size],
|
||||
fill=highlight_color)
|
||||
|
||||
|
||||
class ButtonText(Text, Interactable, Queriable):
|
||||
"""
|
||||
@@ -105,7 +121,7 @@ class ButtonText(Text, Interactable, Queriable):
|
||||
|
||||
def __init__(self, button: Button, font: Font, draw: ImageDraw.Draw,
|
||||
padding: Tuple[int, int, int, int] = (4, 8, 4, 8),
|
||||
source=None, line=None):
|
||||
source=None, line=None, page=None):
|
||||
"""
|
||||
Initialize a button text object.
|
||||
|
||||
@@ -116,6 +132,7 @@ class ButtonText(Text, Interactable, Queriable):
|
||||
padding: Padding around the button text (top, right, bottom, left)
|
||||
source: Optional source object
|
||||
line: Optional line container
|
||||
page: Optional parent page (for dirty flag management)
|
||||
"""
|
||||
# Initialize Text with the button label
|
||||
Text.__init__(self, button.label, font, draw, source, line)
|
||||
@@ -126,6 +143,7 @@ class ButtonText(Text, Interactable, Queriable):
|
||||
# Store button properties
|
||||
self._button = button
|
||||
self._padding = padding
|
||||
self._page = page
|
||||
self._pressed = False
|
||||
self._hovered = False
|
||||
|
||||
@@ -150,10 +168,26 @@ class ButtonText(Text, Interactable, Queriable):
|
||||
def set_pressed(self, pressed: bool):
|
||||
"""Set the pressed state"""
|
||||
self._pressed = pressed
|
||||
self._mark_page_dirty()
|
||||
|
||||
def set_hovered(self, hovered: bool):
|
||||
"""Set the hover state"""
|
||||
self._hovered = hovered
|
||||
self._mark_page_dirty()
|
||||
|
||||
def set_page(self, page):
|
||||
"""
|
||||
Set the parent page reference for dirty flag management.
|
||||
|
||||
Args:
|
||||
page: The Page object containing this element
|
||||
"""
|
||||
self._page = page
|
||||
|
||||
def _mark_page_dirty(self):
|
||||
"""Mark the parent page as dirty if available"""
|
||||
if self._page and hasattr(self._page, 'mark_dirty'):
|
||||
self._page.mark_dirty()
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Interaction handler for managing button/link press-release lifecycle with visual feedback.
|
||||
|
||||
This module provides utilities for handling interactive element states and rendering
|
||||
frames at different stages of interaction (pressed, released).
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple, Callable, Any
|
||||
from PIL import Image
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from pyWebLayout.concrete.functional import LinkText, ButtonText
|
||||
from pyWebLayout.concrete.page import Page
|
||||
|
||||
|
||||
class InteractionHandler:
|
||||
"""
|
||||
Manages the press-release lifecycle for interactive elements.
|
||||
|
||||
This class handles the timing and state management needed to show
|
||||
visual feedback when buttons or links are clicked. It can generate
|
||||
multiple rendered frames showing the pressed and released states.
|
||||
|
||||
Usage patterns:
|
||||
|
||||
Pattern A - Simple one-shot with automatic frames:
|
||||
handler = InteractionHandler(page)
|
||||
frames = handler.execute_with_feedback(button_element, point)
|
||||
# Returns: [pressed_frame, released_frame]
|
||||
# Show frames in sequence with brief delay
|
||||
|
||||
Pattern B - Manual state management for custom event loops:
|
||||
handler = InteractionHandler(page)
|
||||
handler.set_pressed_state(button_element, True)
|
||||
pressed_frame = handler.render_current_state()
|
||||
# ... show frame, wait, execute action ...
|
||||
handler.set_pressed_state(button_element, False)
|
||||
released_frame = handler.render_current_state()
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page, press_duration_ms: int = 150):
|
||||
"""
|
||||
Initialize the interaction handler.
|
||||
|
||||
Args:
|
||||
page: The Page object containing the interactive elements
|
||||
press_duration_ms: How long to show the pressed state (default: 150ms)
|
||||
"""
|
||||
self._page = page
|
||||
self._press_duration_ms = press_duration_ms
|
||||
|
||||
def set_pressed_state(self, element, pressed: bool):
|
||||
"""
|
||||
Set the pressed state of an interactive element.
|
||||
|
||||
Args:
|
||||
element: A LinkText or ButtonText object
|
||||
pressed: True to show pressed, False to show released
|
||||
"""
|
||||
if isinstance(element, (LinkText, ButtonText)):
|
||||
# Ensure element has page reference for dirty flag
|
||||
if not hasattr(element, '_page') or element._page is None:
|
||||
element.set_page(self._page)
|
||||
element.set_pressed(pressed)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Element must be LinkText or ButtonText, got {type(element)}")
|
||||
|
||||
def set_hovered_state(self, element, hovered: bool):
|
||||
"""
|
||||
Set the hovered state of an interactive element.
|
||||
|
||||
Args:
|
||||
element: A LinkText or ButtonText object
|
||||
hovered: True to show hovered, False for normal
|
||||
"""
|
||||
if isinstance(element, (LinkText, ButtonText)):
|
||||
# Ensure element has page reference for dirty flag
|
||||
if not hasattr(element, '_page') or element._page is None:
|
||||
element.set_page(self._page)
|
||||
element.set_hovered(hovered)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Element must be LinkText or ButtonText, got {type(element)}")
|
||||
|
||||
def render_current_state(self) -> Image.Image:
|
||||
"""
|
||||
Render the page with current element states.
|
||||
|
||||
Returns:
|
||||
PIL Image of the rendered page
|
||||
"""
|
||||
return self._page.render()
|
||||
|
||||
def execute_with_feedback(
|
||||
self,
|
||||
element,
|
||||
point: Optional[np.ndarray] = None,
|
||||
callback: Optional[Callable] = None) -> Tuple[Image.Image, Image.Image, Any]:
|
||||
"""
|
||||
Execute an interaction with visual feedback at each stage.
|
||||
|
||||
This is the high-level "all-in-one" method that:
|
||||
1. Sets pressed state and renders
|
||||
2. Waits for press_duration_ms
|
||||
3. Executes the element's callback (or provided callback)
|
||||
4. Sets released state and renders
|
||||
|
||||
Args:
|
||||
element: A LinkText or ButtonText object
|
||||
point: Optional point where interaction occurred
|
||||
callback: Optional custom callback (overrides element's callback)
|
||||
|
||||
Returns:
|
||||
Tuple of (pressed_frame, released_frame, callback_result)
|
||||
"""
|
||||
# Step 1: Render pressed state
|
||||
self.set_pressed_state(element, True)
|
||||
pressed_frame = self.render_current_state()
|
||||
|
||||
# Step 2: Wait for visual feedback duration
|
||||
time.sleep(self._press_duration_ms / 1000.0)
|
||||
|
||||
# Step 3: Execute callback
|
||||
callback_result = None
|
||||
if callback:
|
||||
callback_result = callback(point) if point is not None else callback()
|
||||
elif hasattr(element, 'interact'):
|
||||
callback_result = element.interact(point)
|
||||
|
||||
# Step 4: Render released state
|
||||
self.set_pressed_state(element, False)
|
||||
released_frame = self.render_current_state()
|
||||
|
||||
return pressed_frame, released_frame, callback_result
|
||||
|
||||
def execute_async_with_feedback(
|
||||
self,
|
||||
element,
|
||||
point: Optional[np.ndarray] = None) -> Tuple[Image.Image, Callable, Image.Image]:
|
||||
"""
|
||||
Execute an interaction with visual feedback, returning frames immediately
|
||||
without blocking.
|
||||
|
||||
This method returns the frames and a callback to execute later, allowing
|
||||
the caller to control when the action actually happens.
|
||||
|
||||
Args:
|
||||
element: A LinkText or ButtonText object
|
||||
point: Optional point where interaction occurred
|
||||
|
||||
Returns:
|
||||
Tuple of (pressed_frame, execute_callback, released_frame)
|
||||
where execute_callback is a function that will execute the interaction
|
||||
"""
|
||||
# Render pressed state
|
||||
self.set_pressed_state(element, True)
|
||||
pressed_frame = self.render_current_state()
|
||||
|
||||
# Create callback that will execute the interaction and reset state
|
||||
def execute_callback():
|
||||
result = None
|
||||
if hasattr(element, 'interact'):
|
||||
result = element.interact(point)
|
||||
self.set_pressed_state(element, False)
|
||||
return result
|
||||
|
||||
# Pre-render the released state (element state is still pressed)
|
||||
# We'll return this frame but the caller controls when to show it
|
||||
self.set_pressed_state(element, False)
|
||||
released_frame = self.render_current_state()
|
||||
|
||||
# Reset back to pressed for consistency
|
||||
# (caller will call execute_callback which sets to False)
|
||||
self.set_pressed_state(element, True)
|
||||
|
||||
return pressed_frame, execute_callback, released_frame
|
||||
|
||||
|
||||
class InteractionStateManager:
|
||||
"""
|
||||
Manages interaction states for multiple elements on a page.
|
||||
|
||||
Useful for applications that need to track hover/press states
|
||||
across many interactive elements simultaneously.
|
||||
"""
|
||||
|
||||
def __init__(self, page: Page):
|
||||
"""
|
||||
Initialize the state manager.
|
||||
|
||||
Args:
|
||||
page: The Page object containing interactive elements
|
||||
"""
|
||||
self._page = page
|
||||
self._hovered_element = None
|
||||
self._pressed_element = None
|
||||
|
||||
def update_hover(self, point: Tuple[int, int]) -> Optional[Image.Image]:
|
||||
"""
|
||||
Update hover state based on cursor position.
|
||||
|
||||
Queries the page to find what's under the cursor and updates
|
||||
hover states accordingly.
|
||||
|
||||
Args:
|
||||
point: Cursor position (x, y)
|
||||
|
||||
Returns:
|
||||
New rendered frame if hover state changed, None otherwise
|
||||
"""
|
||||
# Query what's at this point
|
||||
result = self._page.query_point(point)
|
||||
|
||||
if not result or not result.is_interactive:
|
||||
# Nothing interactive under cursor
|
||||
if self._hovered_element:
|
||||
# Clear previous hover
|
||||
if isinstance(self._hovered_element, (LinkText, ButtonText)):
|
||||
self._hovered_element.set_hovered(False)
|
||||
self._hovered_element = None
|
||||
return self._page.render()
|
||||
return None
|
||||
|
||||
# Something interactive is under cursor
|
||||
element = result.object
|
||||
if element != self._hovered_element:
|
||||
# Hover changed
|
||||
# Clear old hover
|
||||
if self._hovered_element and isinstance(
|
||||
self._hovered_element, (LinkText, ButtonText)):
|
||||
self._hovered_element.set_hovered(False)
|
||||
|
||||
# Set new hover
|
||||
if isinstance(element, (LinkText, ButtonText)):
|
||||
element.set_hovered(True)
|
||||
|
||||
self._hovered_element = element
|
||||
return self._page.render()
|
||||
|
||||
return None
|
||||
|
||||
def handle_mouse_down(self, point: Tuple[int, int]) -> Optional[Image.Image]:
|
||||
"""
|
||||
Handle mouse button press at a point.
|
||||
|
||||
Args:
|
||||
point: Click position (x, y)
|
||||
|
||||
Returns:
|
||||
New rendered frame showing pressed state, or None if nothing interactive
|
||||
"""
|
||||
result = self._page.query_point(point)
|
||||
|
||||
if not result or not result.is_interactive:
|
||||
return None
|
||||
|
||||
element = result.object
|
||||
if isinstance(element, (LinkText, ButtonText)):
|
||||
element.set_pressed(True)
|
||||
self._pressed_element = element
|
||||
return self._page.render()
|
||||
|
||||
return None
|
||||
|
||||
def handle_mouse_up(
|
||||
self,
|
||||
point: Tuple[int,
|
||||
int]) -> Tuple[Optional[Image.Image],
|
||||
Any]:
|
||||
"""
|
||||
Handle mouse button release at a point.
|
||||
|
||||
Args:
|
||||
point: Release position (x, y)
|
||||
|
||||
Returns:
|
||||
Tuple of (rendered_frame, callback_result)
|
||||
Frame shows released state, result is from executing the callback
|
||||
"""
|
||||
if not self._pressed_element:
|
||||
return None, None
|
||||
|
||||
# Execute the interaction
|
||||
callback_result = None
|
||||
if hasattr(self._pressed_element, 'interact'):
|
||||
callback_result = self._pressed_element.interact(
|
||||
np.array(point))
|
||||
|
||||
# Release the pressed state
|
||||
if isinstance(self._pressed_element, (LinkText, ButtonText)):
|
||||
self._pressed_element.set_pressed(False)
|
||||
|
||||
self._pressed_element = None
|
||||
|
||||
return self._page.render(), callback_result
|
||||
|
||||
def reset(self):
|
||||
"""Reset all interaction states."""
|
||||
if self._hovered_element and isinstance(
|
||||
self._hovered_element, (LinkText, ButtonText)):
|
||||
self._hovered_element.set_hovered(False)
|
||||
|
||||
if self._pressed_element and isinstance(
|
||||
self._pressed_element, (LinkText, ButtonText)):
|
||||
self._pressed_element.set_pressed(False)
|
||||
|
||||
self._hovered_element = None
|
||||
self._pressed_element = None
|
||||
@@ -35,6 +35,8 @@ class Page(Renderable, Queriable):
|
||||
self._is_first_line = True # Track if we're placing the first line
|
||||
# Callback registry for managing interactable elements
|
||||
self._callbacks = CallbackRegistry()
|
||||
# Dirty flag to track if page needs re-rendering due to state changes
|
||||
self._dirty = True
|
||||
|
||||
def free_space(self) -> Tuple[int, int]:
|
||||
"""Get the remaining space on the page"""
|
||||
@@ -113,6 +115,19 @@ class Page(Renderable, Queriable):
|
||||
"""Get the callback registry for managing interactable elements"""
|
||||
return self._callbacks
|
||||
|
||||
@property
|
||||
def is_dirty(self) -> bool:
|
||||
"""Check if the page needs re-rendering due to state changes"""
|
||||
return self._dirty
|
||||
|
||||
def mark_dirty(self):
|
||||
"""Mark the page as needing re-rendering"""
|
||||
self._dirty = True
|
||||
|
||||
def mark_clean(self):
|
||||
"""Mark the page as clean (up-to-date render)"""
|
||||
self._dirty = False
|
||||
|
||||
@property
|
||||
def draw(self) -> Optional[ImageDraw.Draw]:
|
||||
"""Get the ImageDraw object for drawing on this page's canvas"""
|
||||
@@ -232,6 +247,9 @@ class Page(Renderable, Queriable):
|
||||
# Render all children - they draw directly onto the canvas
|
||||
self.render_children()
|
||||
|
||||
# Mark as clean after rendering
|
||||
self._dirty = False
|
||||
|
||||
return self._canvas
|
||||
|
||||
def _create_canvas(self) -> Image.Image:
|
||||
|
||||
@@ -82,8 +82,15 @@ def create_base_context(
|
||||
Returns:
|
||||
StyleContext with default values
|
||||
"""
|
||||
# Use document's font registry if available, otherwise create default font
|
||||
if base_font is None:
|
||||
if document and hasattr(document, 'get_or_create_font'):
|
||||
base_font = document.get_or_create_font()
|
||||
else:
|
||||
base_font = Font()
|
||||
|
||||
return StyleContext(
|
||||
font=base_font or Font(),
|
||||
font=base_font,
|
||||
background=None,
|
||||
css_classes=set(),
|
||||
css_styles={},
|
||||
|
||||
@@ -100,15 +100,28 @@ def paragraph_layouter(paragraph: Paragraph,
|
||||
|
||||
# Cap font size to page maximum if needed
|
||||
if font.font_size > page.style.max_font_size:
|
||||
font = Font(
|
||||
font_path=font._font_path,
|
||||
font_size=page.style.max_font_size,
|
||||
colour=font.colour,
|
||||
weight=font.weight,
|
||||
style=font.style,
|
||||
decoration=font.decoration,
|
||||
background=font.background
|
||||
)
|
||||
# Use paragraph's font registry to create the capped font
|
||||
if hasattr(paragraph, 'get_or_create_font'):
|
||||
font = paragraph.get_or_create_font(
|
||||
font_path=font._font_path,
|
||||
font_size=page.style.max_font_size,
|
||||
colour=font.colour,
|
||||
weight=font.weight,
|
||||
style=font.style,
|
||||
decoration=font.decoration,
|
||||
background=font.background
|
||||
)
|
||||
else:
|
||||
# Fallback to direct creation (will still use global cache)
|
||||
font = Font(
|
||||
font_path=font._font_path,
|
||||
font_size=page.style.max_font_size,
|
||||
colour=font.colour,
|
||||
weight=font.weight,
|
||||
style=font.style,
|
||||
decoration=font.decoration,
|
||||
background=font.background
|
||||
)
|
||||
|
||||
# Calculate baseline-to-baseline spacing: font size + additional line spacing
|
||||
# This is the vertical distance between baselines of consecutive lines
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
# e.g. bold, italic, regular
|
||||
from PIL import ImageFont
|
||||
from enum import Enum
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, Dict
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Set up logging for font loading
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global cache for PIL ImageFont objects to avoid reloading fonts from disk
|
||||
# Key: (font_path, font_size), Value: PIL ImageFont object
|
||||
_FONT_CACHE: Dict[Tuple[Optional[str], int], ImageFont.FreeTypeFont] = {}
|
||||
|
||||
# Cache for bundled font path to avoid repeated filesystem lookups
|
||||
_BUNDLED_FONT_PATH: Optional[str] = None
|
||||
|
||||
|
||||
class FontWeight(Enum):
|
||||
NORMAL = "normal"
|
||||
@@ -70,7 +77,14 @@ class Font:
|
||||
self._load_font()
|
||||
|
||||
def _get_bundled_font_path(self):
|
||||
"""Get the path to the bundled font"""
|
||||
"""Get the path to the bundled font (cached)"""
|
||||
global _BUNDLED_FONT_PATH
|
||||
|
||||
# Return cached path if available
|
||||
if _BUNDLED_FONT_PATH is not None:
|
||||
return _BUNDLED_FONT_PATH
|
||||
|
||||
# First time - determine the path and cache it
|
||||
# Get the directory containing this module
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# Navigate to the assets/fonts directory
|
||||
@@ -86,13 +100,31 @@ class Font:
|
||||
|
||||
if os.path.exists(bundled_font_path):
|
||||
logger.info(f"Found bundled font at: {bundled_font_path}")
|
||||
_BUNDLED_FONT_PATH = bundled_font_path
|
||||
return bundled_font_path
|
||||
else:
|
||||
logger.warning(f"Bundled font not found at: {bundled_font_path}")
|
||||
# Cache None to indicate bundled font is not available
|
||||
_BUNDLED_FONT_PATH = "" # Use empty string instead of None to differentiate from "not checked yet"
|
||||
return None
|
||||
|
||||
def _load_font(self):
|
||||
"""Load the font using PIL's ImageFont with consistent bundled font"""
|
||||
"""Load the font using PIL's ImageFont with consistent bundled font and caching"""
|
||||
# Determine the actual font path to use
|
||||
font_path_to_use = self._font_path
|
||||
if not font_path_to_use:
|
||||
font_path_to_use = self._get_bundled_font_path()
|
||||
|
||||
# Create cache key
|
||||
cache_key = (font_path_to_use, self._font_size)
|
||||
|
||||
# Check if font is already cached
|
||||
if cache_key in _FONT_CACHE:
|
||||
self._font = _FONT_CACHE[cache_key]
|
||||
logger.debug(f"Reusing cached font: {font_path_to_use} at size {self._font_size}")
|
||||
return
|
||||
|
||||
# Font not cached, need to load it
|
||||
try:
|
||||
if self._font_path:
|
||||
# Use specified font path
|
||||
@@ -119,10 +151,15 @@ class Font:
|
||||
"Bundled font not available, falling back to PIL default font")
|
||||
self._font = ImageFont.load_default()
|
||||
|
||||
# Cache the loaded font
|
||||
_FONT_CACHE[cache_key] = self._font
|
||||
logger.debug(f"Cached font: {font_path_to_use} at size {self._font_size}")
|
||||
|
||||
except Exception as e:
|
||||
# Ultimate fallback to default font
|
||||
logger.error(f"Failed to load font: {e}, falling back to PIL default font")
|
||||
self._font = ImageFont.load_default()
|
||||
# Don't cache the default font as it doesn't have a path
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
|
||||
Reference in New Issue
Block a user