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