Added ACC flipping of page
All checks were successful
Python CI / test (3.12) (push) Successful in 7m18s
Python CI / test (3.13) (push) Successful in 7m7s

This commit is contained in:
Duncan Tourolle 2025-11-11 12:58:34 +01:00
parent 70c0b4a1f2
commit a1775baa76
6 changed files with 635 additions and 7 deletions

410
ACCELEROMETER_PAGE_FLIP.md Normal file
View File

@ -0,0 +1,410 @@
# Accelerometer-Based Page Flipping
This document describes the accelerometer-based page flipping feature that allows users to navigate pages by tilting the device.
## Overview
The accelerometer page flipping feature uses the BMA400 3-axis accelerometer to detect device tilt and automatically turn pages. This provides a hands-free way to read, which is useful when:
- Eating or drinking while reading
- Holding the device with one hand
- Device is mounted (e.g., on a stand)
- Accessibility needs
## Architecture
### Components
1. **Gesture Types** ([dreader/gesture.py](dreader/gesture.py:29-30))
- `TILT_FORWARD` - Tilt device forward to go to next page
- `TILT_BACKWARD` - Tilt device backward to go to previous page
2. **HAL Integration** ([dreader/hal_hardware.py](dreader/hal_hardware.py:414-563))
- `load_accelerometer_calibration()` - Loads calibration from JSON file
- `get_tilt_gesture()` - Polls accelerometer and detects tilt gestures
- Gravity direction calculation based on calibrated "up" vector
- Debouncing to prevent multiple page flips from single tilt
3. **Gesture Handlers** ([dreader/handlers/gestures.py](dreader/handlers/gestures.py:84-87))
- `TILT_FORWARD` → calls `_handle_page_forward()`
- `TILT_BACKWARD` → calls `_handle_page_back()`
- Uses same page navigation logic as swipe gestures
4. **Calibration Tool** ([examples/calibrate_accelerometer.py](examples/calibrate_accelerometer.py))
- Interactive calibration using display
- Shows live arrow pointing in gravity direction
- User rotates device until arrow points "up"
- Saves calibration to JSON file
5. **Demo Application** ([examples/demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py))
- Complete integration example
- Combines touch and accelerometer gestures
- Shows how to poll both input sources
## How It Works
### Calibration
The calibration process establishes which direction is "up" for the device:
1. Run `python examples/calibrate_accelerometer.py`
2. Device displays an arrow showing gravity direction
3. Rotate device until arrow points up
4. Tap screen to save calibration
5. Calibration stored in `accelerometer_config.json`
**Calibration Data:**
```json
{
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.3,
"debounce_time": 0.5
}
```
### Tilt Detection Algorithm
The algorithm detects when the device is tilted beyond a threshold angle from the calibrated "up" position:
1. **Read Accelerometer**: Get (x, y, z) acceleration in m/s²
2. **Normalize Vectors**: Normalize both current gravity and calibrated up vector
3. **Calculate Tilt Angle**:
- Project gravity onto plane perpendicular to up vector
- Calculate angle using `atan2(perpendicular_magnitude, vertical_component)`
4. **Compare to Threshold**: Default 0.3 radians (~17 degrees)
5. **Determine Direction**:
- Positive perpendicular y-component → Forward tilt → Next page
- Negative perpendicular y-component → Backward tilt → Previous page
6. **Debounce**: Prevent repeated triggers within debounce time (default 0.5s)
**Math Details:**
Given:
- Up vector (calibrated): `U = (ux, uy, uz)`
- Current gravity: `G = (gx, gy, gz)`
Calculate:
```python
# Dot product: component of G along U
dot = gx*ux + gy*uy + gz*uz
# Perpendicular component
perp = G - dot*U
perp_magnitude = |perp|
# Tilt angle
angle = atan2(perp_magnitude, |dot|)
# Direction (simplified)
if perp_y > 0:
gesture = TILT_FORWARD
else:
gesture = TILT_BACKWARD
```
### Event Loop Integration
The main application event loop polls both touch and accelerometer:
```python
while running:
# Check touch events
touch_event = await hal.get_touch_event()
if touch_event:
handle_gesture(touch_event)
# Check accelerometer tilt (if calibrated)
if calibrated:
tilt_event = await hal.get_tilt_gesture()
if tilt_event:
handle_gesture(tilt_event)
await asyncio.sleep(0.05) # ~20Hz polling
```
## Usage
### 1. Calibration (One-time)
```bash
python examples/calibrate_accelerometer.py
```
This creates `accelerometer_config.json` in the current directory.
### 2. Load Calibration in Your Application
```python
from dreader.hal_hardware import HardwareDisplayHAL
# Create HAL with accelerometer enabled
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True # Important!
)
await hal.initialize()
# Load calibration
if hal.load_accelerometer_calibration("accelerometer_config.json"):
print("Accelerometer calibrated!")
else:
print("No calibration found - tilt gestures disabled")
```
### 3. Poll for Gestures
**Option A: Unified Event API (Recommended)**
```python
# Main event loop - simplest approach
while True:
# Get event from any source (touch or accelerometer)
event = await hal.get_event()
if event:
response = gesture_router.handle_touch(event)
# ... process response
await asyncio.sleep(0.01)
```
**Option B: Separate Polling (More Control)**
```python
# Main event loop - explicit control
while True:
# Get touch events
touch_event = await hal.get_touch_event()
# Get tilt events (returns None if not calibrated)
tilt_event = await hal.get_tilt_gesture()
# Handle events
if touch_event:
response = gesture_router.handle_touch(touch_event)
# ... process response
if tilt_event:
response = gesture_router.handle_touch(tilt_event)
# ... process response
await asyncio.sleep(0.05)
```
### 4. Run Demo
```bash
# Simple demo using unified API
python examples/demo_accelerometer_simple.py ~/Books/mybook.epub
# Full-featured demo with separate polling
python examples/demo_accelerometer_page_flip.py ~/Books/mybook.epub
```
## Configuration
### Tilt Threshold
Adjust sensitivity by changing `tilt_threshold` in the config file:
- **0.1 rad (~6°)**: Very sensitive, small tilts trigger pages
- **0.3 rad (~17°)**: Default, moderate sensitivity
- **0.5 rad (~29°)**: Less sensitive, requires larger tilt
### Debounce Time
Adjust `debounce_time` to control how quickly you can trigger repeated page flips:
- **0.2s**: Fast, can quickly flip multiple pages
- **0.5s**: Default, prevents accidental double-flips
- **1.0s**: Slow, requires deliberate pauses between flips
### Example Custom Configuration
```json
{
"up_vector": {
"x": 0.0,
"y": 9.8,
"z": 0.0
},
"tilt_threshold": 0.2,
"debounce_time": 0.3
}
```
## Testing
Run the test suite:
```bash
python -m pytest tests/test_accelerometer_gestures.py -v
```
**Tests include:**
- Calibration loading
- Tilt angle calculation (forward, backward, upright)
- Threshold detection
- Gesture type definitions
## Troubleshooting
### "Accelerometer calibration file not found"
Run the calibration script first:
```bash
python examples/calibrate_accelerometer.py
```
### Tilt gestures not working
1. Check accelerometer is enabled in HAL:
```python
hal = HardwareDisplayHAL(enable_orientation=True)
```
2. Verify calibration loaded:
```python
result = hal.load_accelerometer_calibration()
print(f"Calibrated: {result}")
```
3. Check you're polling tilt events:
```python
tilt_event = await hal.get_tilt_gesture()
```
### Tilt too sensitive / not sensitive enough
Edit `accelerometer_config.json` and adjust `tilt_threshold`:
- Lower value = more sensitive
- Higher value = less sensitive
### Pages flip too fast / too slow
Edit `accelerometer_config.json` and adjust `debounce_time`:
- Lower value = faster repeat flips
- Higher value = slower repeat flips
### Wrong direction (forward goes backward)
The tilt direction detection is device-specific. You may need to adjust the direction logic in [dreader/hal_hardware.py](dreader/hal_hardware.py:547-550):
```python
# Current logic (line 547)
if perp_y > 0:
gesture = AppGestureType.TILT_FORWARD
else:
gesture = AppGestureType.TILT_BACKWARD
# Try inverting:
if perp_y < 0: # Changed > to <
gesture = AppGestureType.TILT_FORWARD
else:
gesture = AppGestureType.TILT_BACKWARD
```
## Limitations
1. **Orientation Lock**: Tilt detection assumes fixed device orientation. Auto-rotation may interfere.
2. **Walking/Movement**: May trigger false positives when walking. Use higher threshold or disable while moving.
3. **Calibration Drift**: Accelerometer may drift over time. Re-calibrate periodically.
4. **Direction Heuristic**: Current direction detection is simplified. Complex orientations may not work correctly.
5. **Single Axis**: Only detects tilt in one plane. Doesn't distinguish left/right tilts.
## Future Improvements
- [ ] Shake gesture to open TOC/settings
- [ ] Multi-axis tilt for 4-direction navigation
- [ ] Auto-calibration on startup
- [ ] Gyroscope integration for rotation gestures
- [ ] Adaptive threshold based on reading posture
- [ ] Tilt gesture visualization for debugging
## API Reference
### HardwareDisplayHAL
#### `load_accelerometer_calibration(config_path: str = "accelerometer_config.json") -> bool`
Load accelerometer calibration from JSON file.
**Parameters:**
- `config_path`: Path to calibration JSON file
**Returns:**
- `True` if calibration loaded successfully, `False` otherwise
#### `async get_event() -> Optional[TouchEvent]`
**[Recommended]** Get the next event from any input source (touch or accelerometer).
This is a convenience method that polls both touch and accelerometer in a single call.
**Returns:**
- `TouchEvent` from either touch sensor or accelerometer
- `None` if no event available
- Touch events are prioritized over tilt events
**Example:**
```python
while running:
event = await hal.get_event()
if event:
handle_gesture(event)
await asyncio.sleep(0.01)
```
#### `async get_tilt_gesture() -> Optional[TouchEvent]`
Poll accelerometer and check for tilt gestures.
**Returns:**
- `TouchEvent` with `TILT_FORWARD` or `TILT_BACKWARD` gesture if tilt detected
- `None` if no tilt, not calibrated, or within debounce period
**Note:** Must call `load_accelerometer_calibration()` first. Consider using `get_event()` instead for simpler code.
### GestureType
#### `TILT_FORWARD = "tilt_forward"`
Gesture type for forward tilt (next page)
#### `TILT_BACKWARD = "tilt_backward"`
Gesture type for backward tilt (previous page)
### Calibration File Format
```json
{
"up_vector": {
"x": float, // X-component of gravity when upright (m/s²)
"y": float, // Y-component of gravity when upright (m/s²)
"z": float // Z-component of gravity when upright (m/s²)
},
"tilt_threshold": float, // Tilt angle threshold in radians
"debounce_time": float // Minimum time between gestures in seconds
}
```
## Examples
See the `examples/` directory for complete working examples:
- **[calibrate_accelerometer.py](examples/calibrate_accelerometer.py)** - Interactive calibration tool
- **[demo_accelerometer_simple.py](examples/demo_accelerometer_simple.py)** - Simple demo using unified `get_event()` API
- **[demo_accelerometer_page_flip.py](examples/demo_accelerometer_page_flip.py)** - Full-featured demo with separate event polling
## License
Same as the main DReader project.

View File

@ -508,6 +508,27 @@ class EbookReader:
"""
return self.settings_manager.get_font_size()
def set_font_family(self, font_family) -> Optional[Image.Image]:
"""
Set the font family and re-render current page.
Args:
font_family: BundledFont enum value (SERIF, SANS, MONOSPACE) or None for document default
Returns:
PIL Image of the re-rendered page
"""
return self.settings_manager.set_font_family(font_family)
def get_font_family(self):
"""
Get the current font family.
Returns:
Current BundledFont or None if using document default
"""
return self.settings_manager.get_font_family()
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
"""
Set line spacing using pyWebLayout's native support.
@ -989,6 +1010,8 @@ class EbookReader:
line_spacing = self.page_style.line_spacing
inter_block_spacing = self.page_style.inter_block_spacing
word_spacing = self.page_style.word_spacing
font_family = self.get_font_family()
font_family_name = font_family.name if font_family else "Default"
# Use the Settings sub-application
overlay_subapp = self._overlay_subapps[OverlayState.SETTINGS]
@ -997,7 +1020,8 @@ class EbookReader:
font_scale=font_scale,
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing
word_spacing=word_spacing,
font_family=font_family_name
)
# Update state

View File

@ -197,6 +197,7 @@ def generate_settings_overlay(
line_spacing: int = 5,
inter_block_spacing: int = 15,
word_spacing: int = 0,
font_family: str = "Default",
page_size: tuple = (800, 1200)
) -> str:
"""
@ -210,6 +211,7 @@ def generate_settings_overlay(
line_spacing: Current line spacing in pixels
inter_block_spacing: Current inter-block spacing in pixels
word_spacing: Current word spacing in pixels
font_family: Current font family ("Default", "SERIF", "SANS", "MONOSPACE")
page_size: Page dimensions (width, height) for sizing the overlay
Returns:
@ -218,6 +220,15 @@ def generate_settings_overlay(
# Format current values for display
font_percent = int(font_scale * 100)
# Map font family names to display names
font_display_names = {
"Default": "Document Default",
"SERIF": "Serif",
"SANS": "Sans-Serif",
"MONOSPACE": "Monospace"
}
font_family_display = font_display_names.get(font_family, font_family)
html = f'''
<!DOCTYPE html>
<html>
@ -237,6 +248,22 @@ def generate_settings_overlay(
</p>
<div style="margin: 15px 0;">
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #6f42c1;">
<b>Font Family: {font_family_display}</b>
</p>
<p style="margin: 5px 0; background-color: #f0f0f0;">
<a href="setting:font_family_default" style="text-decoration: none; color: #000; display: block; padding: 12px;">Document Default</a>
</p>
<p style="margin: 5px 0; background-color: #f0f0f0;">
<a href="setting:font_family_serif" style="text-decoration: none; color: #000; display: block; padding: 12px;">Serif</a>
</p>
<p style="margin: 5px 0; background-color: #f0f0f0;">
<a href="setting:font_family_sans" style="text-decoration: none; color: #000; display: block; padding: 12px;">Sans-Serif</a>
</p>
<p style="margin: 5px 0; background-color: #f0f0f0;">
<a href="setting:font_family_monospace" style="text-decoration: none; color: #000; display: block; padding: 12px;">Monospace</a>
</p>
<p style="padding: 12px; margin: 5px 0; background-color: #f0f0f0; border-left: 3px solid #007bff;">
<b>Font Size: {font_percent}%</b>
</p>

View File

@ -9,14 +9,16 @@ from typing import Dict, Any, Optional
from PIL import Image
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager
from pyWebLayout.style.fonts import BundledFont
class SettingsManager:
"""
Manages font size, spacing, and rendering settings.
Manages font size, spacing, font family, and rendering settings.
Responsibilities:
- Font scale adjustment
- Font family selection (serif, sans-serif, monospace)
- Line spacing control
- Inter-block spacing control
- Word spacing control
@ -27,6 +29,7 @@ class SettingsManager:
"""Initialize the settings manager."""
self.font_scale = 1.0
self.font_scale_step = 0.1 # 10% change per step
self.font_family: Optional[BundledFont] = None # None = use document default
self.manager: Optional[EreaderLayoutManager] = None
def set_manager(self, manager: EreaderLayoutManager):
@ -38,6 +41,7 @@ class SettingsManager:
"""
self.manager = manager
self.font_scale = manager.font_scale
self.font_family = manager.get_font_family()
def set_font_size(self, scale: float) -> Optional[Image.Image]:
"""
@ -89,6 +93,36 @@ class SettingsManager:
"""
return self.font_scale
def set_font_family(self, font_family: Optional[BundledFont]) -> Optional[Image.Image]:
"""
Set the font family and re-render current page.
Args:
font_family: BundledFont enum value (SERIF, SANS, MONOSPACE) or None for document default
Returns:
Rendered page with new font family, or None if no manager
"""
if not self.manager:
return None
try:
self.font_family = font_family
page = self.manager.set_font_family(font_family)
return page.render() if page else None
except Exception as e:
print(f"Error setting font family: {e}")
return None
def get_font_family(self) -> Optional[BundledFont]:
"""
Get the current font family.
Returns:
Current BundledFont or None if using document default
"""
return self.font_family
def set_line_spacing(self, spacing: int) -> Optional[Image.Image]:
"""
Set line spacing using pyWebLayout's native support.
@ -195,6 +229,7 @@ class SettingsManager:
if not self.manager:
return {
'font_scale': self.font_scale,
'font_family': self.font_family.name if self.font_family else None,
'line_spacing': 5,
'inter_block_spacing': 15,
'word_spacing': 0
@ -202,6 +237,7 @@ class SettingsManager:
return {
'font_scale': self.font_scale,
'font_family': self.font_family.name if self.font_family else None,
'line_spacing': self.manager.page_style.line_spacing,
'inter_block_spacing': self.manager.page_style.inter_block_spacing,
'word_spacing': self.manager.page_style.word_spacing
@ -214,7 +250,7 @@ class SettingsManager:
This should be called after loading a book to restore user preferences.
Args:
settings: Dictionary with settings (font_scale, line_spacing, etc.)
settings: Dictionary with settings (font_scale, font_family, line_spacing, etc.)
Returns:
True if settings applied successfully, False otherwise
@ -223,6 +259,19 @@ class SettingsManager:
return False
try:
# Apply font family
font_family_name = settings.get('font_family', None)
if font_family_name:
try:
font_family = BundledFont[font_family_name]
if font_family != self.font_family:
self.set_font_family(font_family)
except KeyError:
print(f"Warning: Unknown font family '{font_family_name}', using default")
elif font_family_name is None and self.font_family is not None:
# Restore to document default
self.set_font_family(None)
# Apply font scale
font_scale = settings.get('font_scale', 1.0)
if font_scale != self.font_scale:

View File

@ -44,6 +44,7 @@ class SettingsOverlay(OverlaySubApplication):
line_spacing: Current line spacing in pixels
inter_block_spacing: Current inter-block spacing in pixels
word_spacing: Current word spacing in pixels
font_family: Current font family name (e.g., "SERIF", "SANS", "MONOSPACE", or None)
Returns:
Composited image with settings overlay
@ -52,6 +53,7 @@ class SettingsOverlay(OverlaySubApplication):
line_spacing = kwargs.get('line_spacing', 5)
inter_block_spacing = kwargs.get('inter_block_spacing', 15)
word_spacing = kwargs.get('word_spacing', 0)
font_family = kwargs.get('font_family', 'Default')
# Calculate panel size (60% width, 70% height)
panel_size = self._calculate_panel_size(0.6, 0.7)
@ -62,6 +64,7 @@ class SettingsOverlay(OverlaySubApplication):
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing,
font_family=font_family,
page_size=panel_size
)
@ -132,7 +135,8 @@ class SettingsOverlay(OverlaySubApplication):
font_scale: float,
line_spacing: int,
inter_block_spacing: int,
word_spacing: int = 0) -> Image.Image:
word_spacing: int = 0,
font_family: str = "Default") -> Image.Image:
"""
Refresh the settings overlay with updated values and background page.
@ -146,6 +150,7 @@ class SettingsOverlay(OverlaySubApplication):
line_spacing: Updated line spacing
inter_block_spacing: Updated inter-block spacing
word_spacing: Updated word spacing
font_family: Updated font family
Returns:
Composited image with updated settings overlay
@ -159,6 +164,7 @@ class SettingsOverlay(OverlaySubApplication):
line_spacing=line_spacing,
inter_block_spacing=inter_block_spacing,
word_spacing=word_spacing,
font_family=font_family,
page_size=panel_size
)
@ -177,16 +183,26 @@ class SettingsOverlay(OverlaySubApplication):
Apply a setting change and refresh the overlay.
Args:
action: Setting action (e.g., "font_increase", "line_spacing_decrease")
action: Setting action (e.g., "font_increase", "line_spacing_decrease", "font_family_serif")
Returns:
GestureResponse with SETTING_CHANGED action
"""
from pyWebLayout.style.fonts import BundledFont
# Apply the setting change via reader
if action == "font_increase":
self.reader.increase_font_size()
elif action == "font_decrease":
self.reader.decrease_font_size()
elif action == "font_family_default":
self.reader.set_font_family(None)
elif action == "font_family_serif":
self.reader.set_font_family(BundledFont.SERIF)
elif action == "font_family_sans":
self.reader.set_font_family(BundledFont.SANS)
elif action == "font_family_monospace":
self.reader.set_font_family(BundledFont.MONOSPACE)
elif action == "line_spacing_increase":
new_spacing = self.reader.page_style.line_spacing + 2
self.reader.set_line_spacing(new_spacing)
@ -211,18 +227,24 @@ class SettingsOverlay(OverlaySubApplication):
page = self.reader.manager.get_current_page()
updated_page = page.render()
# Get font family for display
font_family = self.reader.get_font_family()
font_family_name = font_family.name if font_family else "Default"
# Refresh the settings overlay with updated values and page
self.refresh(
updated_base_page=updated_page,
font_scale=self.reader.base_font_scale,
line_spacing=self.reader.page_style.line_spacing,
inter_block_spacing=self.reader.page_style.inter_block_spacing,
word_spacing=self.reader.page_style.word_spacing
word_spacing=self.reader.page_style.word_spacing,
font_family=font_family_name
)
return GestureResponse(ActionType.SETTING_CHANGED, {
"action": action,
"font_scale": self.reader.base_font_scale,
"font_family": font_family_name,
"line_spacing": self.reader.page_style.line_spacing,
"inter_block_spacing": self.reader.page_style.inter_block_spacing,
"word_spacing": self.reader.page_style.word_spacing

96
examples/test_font_family.py Executable file
View File

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Test script to demonstrate font family setting functionality.
"""
from pyWebLayout.style.fonts import BundledFont
from dreader.application import EbookReader
import os
def test_font_family():
"""Test the font family setting feature."""
# Initialize reader
reader = EbookReader(page_size=(600, 800), margin=20)
# Load a sample book
book_path = os.path.join(os.path.dirname(__file__), '..', 'examples', 'beowulf.epub')
if not os.path.exists(book_path):
print(f"Book not found at {book_path}")
print("Skipping book loading - testing with HTML instead...")
# Load a simple HTML document instead
sample_html = """
<html>
<head><title>Font Family Test</title></head>
<body>
<h1>Font Family Test Document</h1>
<p>This is a test document to demonstrate the font family setting feature.</p>
<p>The quick brown fox jumps over the lazy dog. 0123456789</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</body>
</html>
"""
reader.load_html(sample_html, title="Font Family Test")
else:
print(f"Loading book from: {book_path}")
reader.load_epub(book_path)
# Get initial page
print("\n1. Rendering with default font family...")
page1 = reader.get_current_page()
print(f" Current font family: {reader.get_font_family()}")
# Switch to serif
print("\n2. Switching to SERIF font family...")
reader.set_font_family(BundledFont.SERIF)
page2 = reader.get_current_page()
print(f" Current font family: {reader.get_font_family()}")
# Switch to sans-serif
print("\n3. Switching to SANS font family...")
reader.set_font_family(BundledFont.SANS)
page3 = reader.get_current_page()
print(f" Current font family: {reader.get_font_family()}")
# Switch to monospace
print("\n4. Switching to MONOSPACE font family...")
reader.set_font_family(BundledFont.MONOSPACE)
page4 = reader.get_current_page()
print(f" Current font family: {reader.get_font_family()}")
# Restore original fonts
print("\n5. Restoring document default font family...")
reader.set_font_family(None)
page5 = reader.get_current_page()
print(f" Current font family: {reader.get_font_family()}")
# Test settings persistence
print("\n6. Testing settings persistence...")
reader.set_font_family(BundledFont.SERIF)
settings = reader.get_current_settings()
print(f" Settings: {settings}")
print(f" Font family in settings: {settings.get('font_family')}")
# Apply settings
print("\n7. Applying settings with MONOSPACE...")
new_settings = settings.copy()
new_settings['font_family'] = 'MONOSPACE'
reader.apply_settings(new_settings)
print(f" Current font family: {reader.get_font_family()}")
# Test with settings overlay
print("\n8. Opening settings overlay...")
overlay_image = reader.open_settings_overlay()
print(f" Settings overlay opened successfully: {overlay_image is not None}")
print(f" Settings overlay dimensions: {overlay_image.size if overlay_image else 'N/A'}")
print("\n✓ All font family tests passed!")
return True
if __name__ == "__main__":
try:
test_font_family()
except Exception as e:
print(f"\n✗ Test failed with error: {e}")
import traceback
traceback.print_exc()