From a1775baa764f14d912f9df9137dd41de466af93c Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 11 Nov 2025 12:58:34 +0100 Subject: [PATCH] Added ACC flipping of page --- ACCELEROMETER_PAGE_FLIP.md | 410 +++++++++++++++++++++++++++++++++++ dreader/application.py | 28 ++- dreader/html_generator.py | 27 +++ dreader/managers/settings.py | 53 ++++- dreader/overlays/settings.py | 28 ++- examples/test_font_family.py | 96 ++++++++ 6 files changed, 635 insertions(+), 7 deletions(-) create mode 100644 ACCELEROMETER_PAGE_FLIP.md create mode 100755 examples/test_font_family.py diff --git a/ACCELEROMETER_PAGE_FLIP.md b/ACCELEROMETER_PAGE_FLIP.md new file mode 100644 index 0000000..364ebcd --- /dev/null +++ b/ACCELEROMETER_PAGE_FLIP.md @@ -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. diff --git a/dreader/application.py b/dreader/application.py index 9a606c3..dfe01ce 100644 --- a/dreader/application.py +++ b/dreader/application.py @@ -507,7 +507,28 @@ class EbookReader: Current font scale factor """ 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 diff --git a/dreader/html_generator.py b/dreader/html_generator.py index 3444e0b..0820ba6 100644 --- a/dreader/html_generator.py +++ b/dreader/html_generator.py @@ -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''' @@ -237,6 +248,22 @@ def generate_settings_overlay(

+

+ Font Family: {font_family_display} +

+

+ Document Default +

+

+ Serif +

+

+ Sans-Serif +

+

+ Monospace +

+

Font Size: {font_percent}%

diff --git a/dreader/managers/settings.py b/dreader/managers/settings.py index ef461da..ac39a43 100644 --- a/dreader/managers/settings.py +++ b/dreader/managers/settings.py @@ -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: diff --git a/dreader/overlays/settings.py b/dreader/overlays/settings.py index 29703e7..c644a07 100644 --- a/dreader/overlays/settings.py +++ b/dreader/overlays/settings.py @@ -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 diff --git a/examples/test_font_family.py b/examples/test_font_family.py new file mode 100755 index 0000000..6a69cce --- /dev/null +++ b/examples/test_font_family.py @@ -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 = """ + + Font Family Test + +

Font Family Test Document

+

This is a test document to demonstrate the font family setting feature.

+

The quick brown fox jumps over the lazy dog. 0123456789

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

+ + + """ + 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()