Update coverage badges [skip ci]

This commit is contained in:
Gitea Action
2025-11-23 14:10:14 +00:00
commit f83411929c
107 changed files with 23106 additions and 0 deletions
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
Accelerometer Calibration Script
This script helps calibrate the accelerometer for gravity-based page flipping.
It displays visual instructions on the e-ink display to guide the user through
aligning the device with the "up" direction.
The calibration process:
1. Shows an arrow pointing up
2. User rotates device until arrow aligns with desired "up" direction
3. User confirms by tapping screen
4. Script saves calibration offset to config file
Usage:
python examples/calibrate_accelerometer.py
"""
import asyncio
import sys
import json
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.gesture import GestureType
class AccelerometerCalibrator:
"""Interactive accelerometer calibration tool"""
def __init__(self, hal: HardwareDisplayHAL, config_path: str = "accelerometer_config.json"):
self.hal = hal
self.config_path = Path(config_path)
self.width = hal.width
self.height = hal.height
self.calibrated = False
# Calibration data
self.up_vector = None # (x, y, z) when device is in "up" position
async def run(self):
"""Run the calibration process"""
print("Starting accelerometer calibration...")
print(f"Display: {self.width}x{self.height}")
await self.hal.initialize()
try:
# Show welcome screen
await self.show_welcome()
await self.wait_for_tap()
# Calibration loop
await self.calibration_loop()
# Show completion screen
await self.show_completion()
await asyncio.sleep(3)
finally:
await self.hal.cleanup()
async def show_welcome(self):
"""Display welcome/instruction screen"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
# Try to load a font, fall back to default
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
except:
title_font = ImageFont.load_default()
body_font = ImageFont.load_default()
# Title
title = "Accelerometer Calibration"
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_width = title_bbox[2] - title_bbox[0]
draw.text(((self.width - title_width) // 2, 100), title, fill=(0, 0, 0), font=title_font)
# Instructions
instructions = [
"This will calibrate the accelerometer",
"for gravity-based page flipping.",
"",
"You will:",
"1. See an arrow on screen",
"2. Rotate device until arrow points UP",
"3. Tap screen to confirm",
"",
"Tap anywhere to begin..."
]
y = 250
for line in instructions:
line_bbox = draw.textbbox((0, 0), line, font=body_font)
line_width = line_bbox[2] - line_bbox[0]
draw.text(((self.width - line_width) // 2, y), line, fill=(0, 0, 0), font=body_font)
y += 50
await self.hal.show_image(img)
async def calibration_loop(self):
"""Main calibration loop - show live arrow and accelerometer reading"""
print("\nCalibration mode:")
print("Rotate device until arrow points UP, then tap screen.")
last_display_time = 0
display_interval = 0.2 # Update display every 200ms
while not self.calibrated:
# Get current acceleration
x, y, z = await self.hal.hal.orientation.get_acceleration()
# Update display if enough time has passed
current_time = asyncio.get_event_loop().time()
if current_time - last_display_time >= display_interval:
await self.show_calibration_screen(x, y, z)
last_display_time = current_time
# Check for touch event
event = await self.hal.get_touch_event()
if event and event.gesture == GestureType.TAP:
# Save current orientation as "up"
self.up_vector = (x, y, z)
self.calibrated = True
print(f"\nCalibration saved: up_vector = ({x:.2f}, {y:.2f}, {z:.2f})")
break
await asyncio.sleep(0.05) # Poll at ~20Hz
async def show_calibration_screen(self, ax: float, ay: float, az: float):
"""
Show arrow pointing in direction of gravity
Args:
ax, ay, az: Acceleration components in m/s²
"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
# Calculate gravity direction (normalized)
magnitude = math.sqrt(ax**2 + ay**2 + az**2)
if magnitude < 0.1: # Avoid division by zero
magnitude = 1.0
gx = ax / magnitude
gy = ay / magnitude
gz = az / magnitude
# Project gravity onto screen plane (assuming z is out of screen)
# We want to show which way is "down" on the device
# Arrow should point opposite to gravity (toward "up")
arrow_dx = -gx
arrow_dy = -gy
# Normalize for display
arrow_length = min(self.width, self.height) * 0.3
arrow_magnitude = math.sqrt(arrow_dx**2 + arrow_dy**2)
if arrow_magnitude < 0.1:
arrow_magnitude = 1.0
arrow_dx = (arrow_dx / arrow_magnitude) * arrow_length
arrow_dy = (arrow_dy / arrow_magnitude) * arrow_length
# Center point
cx = self.width // 2
cy = self.height // 2
# Arrow endpoint
end_x = cx + int(arrow_dx)
end_y = cy + int(arrow_dy)
# Draw large arrow
self.draw_arrow(draw, cx, cy, end_x, end_y, width=10)
# Draw circle at center
circle_radius = 30
draw.ellipse(
[(cx - circle_radius, cy - circle_radius),
(cx + circle_radius, cy + circle_radius)],
outline=(0, 0, 0),
width=5
)
# Draw text with acceleration values
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28)
except:
font = ImageFont.load_default()
text = f"X: {ax:6.2f} m/s²"
draw.text((50, 50), text, fill=(0, 0, 0), font=font)
text = f"Y: {ay:6.2f} m/s²"
draw.text((50, 100), text, fill=(0, 0, 0), font=font)
text = f"Z: {az:6.2f} m/s²"
draw.text((50, 150), text, fill=(0, 0, 0), font=font)
text = "Rotate device until arrow points UP"
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
draw.text(((self.width - text_width) // 2, self.height - 150),
text, fill=(0, 0, 0), font=font)
text = "Then TAP screen to save"
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
draw.text(((self.width - text_width) // 2, self.height - 100),
text, fill=(0, 0, 0), font=font)
await self.hal.show_image(img)
def draw_arrow(self, draw: ImageDraw.Draw, x1: int, y1: int, x2: int, y2: int, width: int = 5):
"""Draw an arrow from (x1, y1) to (x2, y2)"""
# Main line
draw.line([(x1, y1), (x2, y2)], fill=(0, 0, 0), width=width)
# Arrow head
dx = x2 - x1
dy = y2 - y1
length = math.sqrt(dx**2 + dy**2)
if length < 0.1:
return
# Normalize
dx /= length
dy /= length
# Arrow head size
head_length = 40
head_width = 30
# Perpendicular vector
px = -dy
py = dx
# Arrow head points
p1_x = x2 - dx * head_length + px * head_width
p1_y = y2 - dy * head_length + py * head_width
p2_x = x2 - dx * head_length - px * head_width
p2_y = y2 - dy * head_length - py * head_width
# Draw arrow head
draw.polygon([(x2, y2), (p1_x, p1_y), (p2_x, p2_y)], fill=(0, 0, 0))
async def show_completion(self):
"""Show calibration complete screen"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
except:
title_font = ImageFont.load_default()
body_font = ImageFont.load_default()
# Title
title = "Calibration Complete!"
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_width = title_bbox[2] - title_bbox[0]
draw.text(((self.width - title_width) // 2, 200), title, fill=(0, 0, 0), font=title_font)
# Details
if self.up_vector:
x, y, z = self.up_vector
details = [
f"Up vector saved:",
f"X: {x:.3f} m/s²",
f"Y: {y:.3f} m/s²",
f"Z: {z:.3f} m/s²",
"",
f"Saved to: {self.config_path}"
]
y_pos = 350
for line in details:
line_bbox = draw.textbbox((0, 0), line, font=body_font)
line_width = line_bbox[2] - line_bbox[0]
draw.text(((self.width - line_width) // 2, y_pos), line, fill=(0, 0, 0), font=body_font)
y_pos += 50
await self.hal.show_image(img)
# Save calibration to file
self.save_calibration()
def save_calibration(self):
"""Save calibration data to JSON file"""
if not self.up_vector:
print("Warning: No calibration data to save")
return
x, y, z = self.up_vector
config = {
"up_vector": {
"x": x,
"y": y,
"z": z
},
"tilt_threshold": 0.3, # Radians (~17 degrees)
"debounce_time": 0.5, # Seconds between tilt gestures
}
with open(self.config_path, 'w') as f:
json.dump(config, f, indent=2)
print(f"Calibration saved to {self.config_path}")
async def wait_for_tap(self):
"""Wait for user to tap screen"""
while True:
event = await self.hal.get_touch_event()
if event and event.gesture == GestureType.TAP:
break
await asyncio.sleep(0.05)
async def main():
"""Main entry point"""
# Create HAL with accelerometer enabled
print("Initializing hardware...")
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True,
enable_rtc=False,
enable_power_monitor=False,
virtual_display=False # Set to True for testing without hardware
)
# Create calibrator
calibrator = AccelerometerCalibrator(hal)
# Run calibration
await calibrator.run()
print("\nCalibration complete!")
print("You can now use accelerometer-based page flipping.")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nCalibration cancelled by user")
sys.exit(0)
except Exception as e:
print(f"\nError during calibration: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Demo: Accelerometer-based Page Flipping
This example demonstrates how to use the accelerometer for hands-free
page turning by tilting the device forward or backward.
Features:
- Tilt device forward to advance to next page
- Tilt device backward to go to previous page
- Touch gestures still work normally
- Configurable tilt threshold and debounce time
Prerequisites:
1. Run calibration first: python examples/calibrate_accelerometer.py
2. This creates accelerometer_config.json with calibration data
Usage:
python examples/demo_accelerometer_page_flip.py <epub_file>
"""
import asyncio
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.application import EbookReader
from dreader.gesture import GestureType
class AccelerometerPageFlipDemo:
"""Demo application with accelerometer-based page flipping"""
def __init__(self, epub_path: str):
self.epub_path = epub_path
# Create HAL with accelerometer enabled
print("Initializing hardware HAL...")
self.hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True,
enable_rtc=False,
enable_power_monitor=False,
virtual_display=False # Set to True for testing without hardware
)
# Create reader
print("Creating ebook reader...")
self.reader = EbookReader(
page_size=(self.hal.width, self.hal.height),
margin=60
)
self.running = False
async def run(self):
"""Run the demo application"""
print("\n" + "="*60)
print("Accelerometer Page Flip Demo")
print("="*60)
# Initialize HAL
await self.hal.initialize()
# Load accelerometer calibration
print("\nLoading accelerometer calibration...")
calibrated = self.hal.load_accelerometer_calibration("accelerometer_config.json")
if not calibrated:
print("\nWARNING: Accelerometer not calibrated!")
print("Please run: python examples/calibrate_accelerometer.py")
print("\nProceeding with touch gestures only...\n")
else:
print("Accelerometer calibration loaded successfully!")
print(f" Up vector: {self.hal.accel_up_vector}")
print(f" Tilt threshold: {self.hal.accel_tilt_threshold:.2f} rad")
print(f" Debounce time: {self.hal.accel_debounce_time:.2f}s")
# Load EPUB
print(f"\nLoading EPUB: {self.epub_path}")
success = self.reader.load_epub(self.epub_path)
if not success:
print(f"ERROR: Failed to load {self.epub_path}")
await self.hal.cleanup()
return
print(f"Loaded: {self.reader.book_title}")
print(f"Author: {self.reader.book_author}")
# Display first page
print("\nDisplaying first page...")
img = self.reader.get_current_page()
await self.hal.show_image(img)
# Instructions
print("\n" + "="*60)
print("Controls:")
print(" - Tilt FORWARD to go to next page")
print(" - Tilt BACKWARD to go to previous page")
print(" - Swipe LEFT for next page (touch)")
print(" - Swipe RIGHT for previous page (touch)")
print(" - Long press to exit")
print("="*60 + "\n")
# Main event loop
self.running = True
try:
await self.event_loop()
finally:
await self.hal.cleanup()
print("\nDemo finished!")
async def event_loop(self):
"""Main event loop - poll for touch and accelerometer events"""
accel_poll_interval = 0.05 # Check accelerometer every 50ms
while self.running:
# Check for touch events
touch_event = await self.hal.get_touch_event()
if touch_event:
await self.handle_event(touch_event)
# Check for accelerometer tilt events (if calibrated)
if hasattr(self.hal, 'accel_up_vector'):
tilt_event = await self.hal.get_tilt_gesture()
if tilt_event:
await self.handle_event(tilt_event)
# Small delay to avoid busy-waiting
await asyncio.sleep(accel_poll_interval)
async def handle_event(self, event):
"""Handle a gesture event (touch or accelerometer)"""
gesture = event.gesture
print(f"Gesture: {gesture.value}")
# Navigation gestures
if gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
await self.next_page()
elif gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
await self.previous_page()
# Exit on long press
elif gesture == GestureType.LONG_PRESS:
print("\nLong press detected - exiting...")
self.running = False
# Word tap
elif gesture == GestureType.TAP:
# You could implement word selection here
print(f" Tap at ({event.x}, {event.y})")
async def next_page(self):
"""Go to next page"""
img = self.reader.next_page()
if img:
progress = self.reader.get_reading_progress()
chapter = self.reader.get_current_chapter_info()
print(f" -> Next page ({progress['percent']:.1f}% - {chapter['title']})")
await self.hal.show_image(img)
else:
print(" -> At end of book")
async def previous_page(self):
"""Go to previous page"""
img = self.reader.previous_page()
if img:
progress = self.reader.get_reading_progress()
chapter = self.reader.get_current_chapter_info()
print(f" -> Previous page ({progress['percent']:.1f}% - {chapter['title']})")
await self.hal.show_image(img)
else:
print(" -> At start of book")
async def main():
"""Main entry point"""
if len(sys.argv) < 2:
print("Usage: python demo_accelerometer_page_flip.py <epub_file>")
print("\nExample:")
print(" python demo_accelerometer_page_flip.py ~/Books/mybook.epub")
sys.exit(1)
epub_path = sys.argv[1]
# Check if file exists
if not Path(epub_path).exists():
print(f"ERROR: File not found: {epub_path}")
sys.exit(1)
# Run demo
demo = AccelerometerPageFlipDemo(epub_path)
await demo.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nDemo interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Simple Accelerometer Demo - Using Unified Event API
This is a simplified version of the accelerometer demo that uses
the HAL's get_event() convenience method to poll both touch and
accelerometer in a single call.
Usage:
python examples/demo_accelerometer_simple.py <epub_file>
"""
import asyncio
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.application import EbookReader
from dreader.gesture import GestureType
async def main():
"""Simple demo using unified event API"""
if len(sys.argv) < 2:
print("Usage: python demo_accelerometer_simple.py <epub_file>")
sys.exit(1)
epub_path = sys.argv[1]
# Create HAL with accelerometer enabled
print("Initializing hardware...")
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True
)
await hal.initialize()
# Load accelerometer calibration (optional)
if hal.load_accelerometer_calibration("accelerometer_config.json"):
print("✓ Accelerometer calibrated - tilt gestures enabled")
else:
print("✗ No accelerometer calibration - touch only")
# Create reader and load book
print(f"\nLoading: {epub_path}")
reader = EbookReader(page_size=(hal.width, hal.height), margin=60)
if not reader.load_epub(epub_path):
print(f"ERROR: Failed to load {epub_path}")
await hal.cleanup()
return
print(f"Loaded: {reader.book_title}")
# Display first page
img = reader.get_current_page()
await hal.show_image(img)
print("\nControls:")
print(" Swipe LEFT or Tilt FORWARD → Next page")
print(" Swipe RIGHT or Tilt BACKWARD → Previous page")
print(" Long press → Exit\n")
# Main event loop - simple unified API!
running = True
while running:
# Get event from any source (touch or accelerometer)
event = await hal.get_event()
if event:
print(f"Gesture: {event.gesture.value}")
# Page navigation
if event.gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
img = reader.next_page()
if img:
progress = reader.get_reading_progress()
print(f" → Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
await hal.show_image(img)
else:
print(" → End of book")
elif event.gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
img = reader.previous_page()
if img:
progress = reader.get_reading_progress()
print(f" ← Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
await hal.show_image(img)
else:
print(" ← Start of book")
# Exit
elif event.gesture == GestureType.LONG_PRESS:
print("\nExiting...")
running = False
await asyncio.sleep(0.01)
await hal.cleanup()
print("Done!")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nInterrupted")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
Demo script showing TOC overlay pagination functionality.
This demonstrates:
1. Opening a navigation overlay with many chapters
2. Navigating through pages using Next/Previous buttons
3. Switching between Contents and Bookmarks tabs with pagination
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from dreader import EbookReader, TouchEvent, GestureType
def main():
print("=" * 60)
print("TOC Pagination Demo")
print("=" * 60)
# Create reader
reader = EbookReader(page_size=(800, 1200))
# Create a mock book with many chapters for demonstration
from dreader.html_generator import generate_navigation_overlay
# Generate test data: 35 chapters and 20 bookmarks
chapters = [{"index": i, "title": f"Chapter {i+1}: The Adventure Continues"} for i in range(35)]
bookmarks = [{"name": f"Bookmark {i+1}", "position": f"Page {i*10}"} for i in range(20)]
print("\nTest Data:")
print(f" - {len(chapters)} chapters")
print(f" - {len(bookmarks)} bookmarks")
print(f" - Items per page: 10")
print()
# Demonstrate pagination on Contents tab
print("Contents Tab Pagination:")
print("-" * 60)
# Page 1 of TOC (chapters 1-10)
print("\n[Page 1/4] Chapters 1-10:")
html_page1 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=0,
toc_items_per_page=10
)
# Extract chapter titles for display
for i in range(10):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 1 of 4 [Next →]")
# Page 2 of TOC (chapters 11-20)
print("\n[Page 2/4] Chapters 11-20:")
html_page2 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=1,
toc_items_per_page=10
)
for i in range(10, 20):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 2 of 4 [Next →]")
# Page 3 of TOC (chapters 21-30)
print("\n[Page 3/4] Chapters 21-30:")
html_page3 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=2,
toc_items_per_page=10
)
for i in range(20, 30):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 3 of 4 [Next →]")
# Page 4 of TOC (chapters 31-35)
print("\n[Page 4/4] Chapters 31-35:")
html_page4 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="contents",
page_size=(800, 1200),
toc_page=3,
toc_items_per_page=10
)
for i in range(30, 35):
print(f" {i+1}. {chapters[i]['title']}")
print(" [← Prev] Page 4 of 4 [Next →]")
# Demonstrate pagination on Bookmarks tab
print("\n" + "=" * 60)
print("Bookmarks Tab Pagination:")
print("-" * 60)
# Page 1 of Bookmarks (1-10)
print("\n[Page 1/2] Bookmarks 1-10:")
html_bm1 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="bookmarks",
page_size=(800, 1200),
toc_page=0,
bookmarks_page=0,
toc_items_per_page=10
)
for i in range(10):
print(f" {bookmarks[i]['name']} - {bookmarks[i]['position']}")
print(" [← Prev] Page 1 of 2 [Next →]")
# Page 2 of Bookmarks (11-20)
print("\n[Page 2/2] Bookmarks 11-20:")
html_bm2 = generate_navigation_overlay(
chapters=chapters,
bookmarks=bookmarks,
active_tab="bookmarks",
page_size=(800, 1200),
toc_page=0,
bookmarks_page=1,
toc_items_per_page=10
)
for i in range(10, 20):
print(f" {bookmarks[i]['name']} - {bookmarks[i]['position']}")
print(" [← Prev] Page 2 of 2 [Next →]")
print("\n" + "=" * 60)
print("Pagination Controls:")
print("-" * 60)
print(" - Click 'Next →' to go to next page")
print(" - Click '← Prev' to go to previous page")
print(" - Page indicator shows: 'Page X of Y'")
print(" - Buttons are disabled at boundaries:")
print("'← Prev' disabled on page 1")
print("'Next →' disabled on last page")
print()
print("=" * 60)
print("Interactive Gesture Flow:")
print("-" * 60)
print("1. User swipes up → Opens navigation overlay (page 1)")
print("2. User taps 'Next →' → Shows page 2")
print("3. User taps 'Next →' → Shows page 3")
print("4. User taps chapter → Navigates to chapter & closes overlay")
print("5. OR taps '← Prev' → Goes back to page 2")
print()
print("HTML Features Implemented:")
print("-" * 60)
print("✓ Pagination links: <a href='page:next'> and <a href='page:prev'>")
print("✓ Page indicator: 'Page X of Y' text")
print("✓ Disabled styling: opacity 0.3 + pointer-events: none")
print("✓ Separate pagination for Contents and Bookmarks tabs")
print("✓ Automatic page calculation based on total items")
print("✓ Graceful handling of empty lists")
print()
print("=" * 60)
print("Demo Complete!")
print("=" * 60)
if __name__ == "__main__":
main()
+470
View File
@@ -0,0 +1,470 @@
#!/usr/bin/env python3
"""
Demo script for Settings overlay feature.
This script demonstrates the complete settings overlay workflow:
1. Display reading page
2. Swipe down from top to open settings overlay
3. Display settings overlay with controls
4. Tap on font size increase button
5. Show live preview update (background page changes)
6. Tap on line spacing increase button
7. Show another live preview update
8. Close overlay and show final page with new settings
Generates a GIF showing all these interactions.
"""
from pathlib import Path
from dreader import EbookReader, TouchEvent, GestureType
from PIL import Image, ImageDraw, ImageFont
def add_gesture_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
"""
Add a text annotation to an image showing what gesture is being performed.
Args:
image: Base image
text: Annotation text
position: "top" or "bottom"
Returns:
Image with annotation
"""
# Create a copy
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Try to use a nice font, fall back to default
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except:
font = ImageFont.load_default()
# Calculate text position
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (image.width - text_width) // 2
if position == "top":
y = 20
else:
y = image.height - text_height - 20
# Draw background rectangle
padding = 10
draw.rectangle(
[x - padding, y - padding, x + text_width + padding, y + text_height + padding],
fill=(0, 0, 0, 200)
)
# Draw text
draw.text((x, y), text, fill=(255, 255, 255), font=font)
return annotated
def add_swipe_arrow(image: Image.Image, start_y: int, end_y: int) -> Image.Image:
"""
Add a visual swipe arrow to show gesture direction.
Args:
image: Base image
start_y: Starting Y position
end_y: Ending Y position
Returns:
Image with arrow overlay
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw arrow in center of screen
x = image.width // 2
# Draw line
draw.line([(x, start_y), (x, end_y)], fill=(255, 100, 100), width=5)
# Draw arrowhead
arrow_size = 20
if end_y < start_y: # Upward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y + arrow_size),
(x + arrow_size, end_y + arrow_size)
], fill=(255, 100, 100))
else: # Downward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y - arrow_size),
(x + arrow_size, end_y - arrow_size)
], fill=(255, 100, 100))
return annotated
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "") -> Image.Image:
"""
Add a visual tap indicator to show where user tapped.
Args:
image: Base image
x, y: Tap coordinates
label: Optional label for the tap
Returns:
Image with tap indicator
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw circle at tap location
radius = 30
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline=(255, 100, 100),
width=5
)
# Draw crosshair
draw.line([(x - radius - 10, y), (x + radius + 10, y)], fill=(255, 100, 100), width=3)
draw.line([(x, y - radius - 10), (x, y + radius + 10)], fill=(255, 100, 100), width=3)
# Add label if provided
if label:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
# Position label above tap point
label_x = x - text_width // 2
label_y = y - radius - 40
draw.text((label_x, label_y), label, fill=(255, 100, 100), font=font)
return annotated
def main():
"""Generate Settings overlay demo GIF"""
print("=== Settings Overlay Demo ===")
print()
# Use Alice in Wonderland test book (has actual content)
epub_path = Path(__file__).parent.parent / 'tests' / 'data' / 'test.epub'
if not epub_path.exists():
print("Error: test.epub not found!")
print(f"Looked in: {epub_path}")
return
print(f"Using book: {epub_path.name}")
# Create reader
reader = EbookReader(page_size=(800, 1200))
# Load book
print("Loading book...")
success = reader.load_epub(str(epub_path))
if not success:
print("Error: Failed to load EPUB!")
return
print(f"Loaded: {reader.book_title} by {reader.book_author}")
print()
# Skip to a page with actual content (past cover/title pages)
for _ in range(3):
reader.next_page()
# Prepare frames for GIF
frames = []
frame_duration = [] # Duration in milliseconds for each frame
# Frame 1: Initial reading page
print("Frame 1: Initial reading page...")
page1 = reader.get_current_page()
annotated1 = add_gesture_annotation(page1, f"Reading: {reader.book_title}", "top")
frames.append(annotated1)
frame_duration.append(2000) # 2 seconds
# Frame 2: Show swipe down gesture
print("Frame 2: Swipe down gesture...")
swipe_visual = add_swipe_arrow(page1, 100, 300)
annotated2 = add_gesture_annotation(swipe_visual, "Swipe down from top", "top")
frames.append(annotated2)
frame_duration.append(1000) # 1 second
# Frame 3: Settings overlay appears
print("Frame 3: Settings overlay opens...")
event_swipe_down = TouchEvent(gesture=GestureType.SWIPE_DOWN, x=400, y=100)
response = reader.handle_touch(event_swipe_down)
print(f" Response: {response.action}")
# Get the overlay image by calling open_settings_overlay again
overlay_image = reader.open_settings_overlay()
annotated3 = add_gesture_annotation(overlay_image, "Settings", "top")
frames.append(annotated3)
frame_duration.append(3000) # 3 seconds to read
# Find actual button coordinates by querying the overlay
print("Querying overlay for button positions...")
link_positions = {}
if reader._active_overlay and reader._active_overlay._overlay_reader:
page = reader._active_overlay._overlay_reader.manager.get_current_page()
# Scan for all links with very fine granularity to catch all buttons
for y in range(0, 840, 3):
for x in range(0, 480, 3):
result = page.query_point((x, y))
if result and result.link_target:
if result.link_target not in link_positions:
# Translate to screen coordinates
panel_x_offset = int((800 - 480) / 2)
panel_y_offset = int((1200 - 840) / 2)
screen_x = x + panel_x_offset
screen_y = y + panel_y_offset
link_positions[result.link_target] = (screen_x, screen_y)
for link, (x, y) in sorted(link_positions.items()):
print(f" Found: {link} at ({x}, {y})")
# Frame 4: Tap on font size increase button
print("Frame 4: Tap on font size increase...")
if 'setting:font_increase' in link_positions:
tap_x, tap_y = link_positions['setting:font_increase']
print(f" Using coordinates: ({tap_x}, {tap_y})")
tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "Increase")
annotated4 = add_gesture_annotation(tap_visual, "Tap to increase font size", "bottom")
frames.append(annotated4)
frame_duration.append(1500) # 1.5 seconds
# Frames 5-9: Font size increased (live preview) - show each tap individually
print("Frames 5-9: Font size increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_font = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y)
response = reader.handle_touch(event_tap_font)
print(f" Tap {i+1}: {response.action} - Font scale: {response.data.get('font_scale', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay = reader.get_current_page() # This gets the composited overlay
annotated = add_gesture_annotation(
updated_overlay,
f"Font: {int(reader.base_font_scale * 100)}% (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final font size for a bit longer
final_font_overlay = reader.get_current_page()
annotated_final = add_gesture_annotation(
final_font_overlay,
f"Font: {int(reader.base_font_scale * 100)}% (complete)",
"top"
)
frames.append(annotated_final)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
updated_overlay = overlay_image
# Get current overlay state for line spacing section
current_overlay = reader.get_current_page()
# Frame N: Tap on line spacing increase button
print("Frame N: Tap on line spacing increase...")
if 'setting:line_spacing_increase' in link_positions:
tap_x2, tap_y2 = link_positions['setting:line_spacing_increase']
print(f" Using coordinates: ({tap_x2}, {tap_y2})")
tap_visual2 = add_tap_indicator(current_overlay, tap_x2, tap_y2, "Increase")
annotated_ls_tap = add_gesture_annotation(tap_visual2, "Tap to increase line spacing", "bottom")
frames.append(annotated_ls_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames N+1 to N+5: Line spacing increased (live preview) - show each tap individually
print("Frames N+1 to N+5: Line spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_spacing = TouchEvent(gesture=GestureType.TAP, x=tap_x2, y=tap_y2)
response = reader.handle_touch(event_tap_spacing)
print(f" Tap {i+1}: {response.action} - Line spacing: {response.data.get('line_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay2 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay2,
f"Line Spacing: {reader.page_style.line_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final line spacing for a bit longer
final_spacing_overlay = reader.get_current_page()
annotated_final_ls = add_gesture_annotation(
final_spacing_overlay,
f"Line Spacing: {reader.page_style.line_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ls)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
# Get current overlay state for paragraph spacing section
current_overlay2 = reader.get_current_page()
# Frame M: Tap on paragraph spacing increase button
print("Frame M: Tap on paragraph spacing increase...")
if 'setting:block_spacing_increase' in link_positions:
tap_x3, tap_y3 = link_positions['setting:block_spacing_increase']
print(f" Using coordinates: ({tap_x3}, {tap_y3})")
tap_visual3 = add_tap_indicator(current_overlay2, tap_x3, tap_y3, "Increase")
annotated_ps_tap = add_gesture_annotation(tap_visual3, "Tap to increase paragraph spacing", "bottom")
frames.append(annotated_ps_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames M+1 to M+5: Paragraph spacing increased (live preview) - show each tap individually
print("Frames M+1 to M+5: Paragraph spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_para = TouchEvent(gesture=GestureType.TAP, x=tap_x3, y=tap_y3)
response = reader.handle_touch(event_tap_para)
print(f" Tap {i+1}: {response.action} - Paragraph spacing: {response.data.get('inter_block_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay3 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay3,
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final paragraph spacing for a bit longer
final_para_overlay = reader.get_current_page()
annotated_final_ps = add_gesture_annotation(
final_para_overlay,
f"Paragraph Spacing: {reader.page_style.inter_block_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ps)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
# Get current overlay state for word spacing section
current_overlay3 = reader.get_current_page()
# Frame W: Tap on word spacing increase button
print("Frame W: Tap on word spacing increase...")
if 'setting:word_spacing_increase' in link_positions:
tap_x4, tap_y4 = link_positions['setting:word_spacing_increase']
print(f" Using coordinates: ({tap_x4}, {tap_y4})")
tap_visual4 = add_tap_indicator(current_overlay3, tap_x4, tap_y4, "Increase")
annotated_ws_tap = add_gesture_annotation(tap_visual4, "Tap to increase word spacing", "bottom")
frames.append(annotated_ws_tap)
frame_duration.append(1500) # 1.5 seconds
# Frames W+1 to W+5: Word spacing increased (live preview) - show each tap individually
print("Frames W+1 to W+5: Word spacing increased with live preview (5 taps, showing each)...")
for i in range(5):
event_tap_word = TouchEvent(gesture=GestureType.TAP, x=tap_x4, y=tap_y4)
response = reader.handle_touch(event_tap_word)
print(f" Tap {i+1}: {response.action} - Word spacing: {response.data.get('word_spacing', 'N/A')}")
# Get updated overlay image after each tap
updated_overlay4 = reader.get_current_page()
annotated = add_gesture_annotation(
updated_overlay4,
f"Word Spacing: {reader.page_style.word_spacing}px (tap {i+1}/5)",
"top"
)
frames.append(annotated)
frame_duration.append(800) # 0.8 seconds per tap
# Hold on final word spacing for a bit longer
final_word_overlay = reader.get_current_page()
annotated_final_ws = add_gesture_annotation(
final_word_overlay,
f"Word Spacing: {reader.page_style.word_spacing}px (complete)",
"top"
)
frames.append(annotated_final_ws)
frame_duration.append(1500) # 1.5 seconds to see the final result
else:
print(" Skipping - button not found")
# Frame Z: Tap outside to close
print("Frame Z: Close overlay...")
final_overlay_state = reader.get_current_page()
tap_visual_close = add_tap_indicator(final_overlay_state, 100, 600, "Close")
annotated_close = add_gesture_annotation(tap_visual_close, "Tap outside to close", "bottom")
frames.append(annotated_close)
frame_duration.append(1500) # 1.5 seconds
# Final Frame: Back to reading with new settings applied
print("Final Frame: Back to reading with new settings...")
event_close = TouchEvent(gesture=GestureType.TAP, x=100, y=600)
response = reader.handle_touch(event_close)
print(f" Response: {response.action}")
final_page = reader.get_current_page()
annotated_final = add_gesture_annotation(
final_page,
f"Settings Applied: {int(reader.base_font_scale * 100)}% font, {reader.page_style.line_spacing}px line, {reader.page_style.inter_block_spacing}px para, {reader.page_style.word_spacing}px word",
"top"
)
frames.append(annotated_final)
frame_duration.append(3000) # 3 seconds
# Save as GIF
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'settings_overlay_demo.gif'
output_path.parent.mkdir(parents=True, exist_ok=True)
print()
print(f"Saving GIF with {len(frames)} frames...")
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=frame_duration,
loop=0,
optimize=False
)
print(f"✓ GIF saved to: {output_path}")
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
print(f" Frames: {len(frames)}")
print(f" Total duration: {sum(frame_duration) / 1000:.1f}s")
# Also save individual frames for documentation
frames_dir = output_path.parent / 'settings_overlay_frames'
frames_dir.mkdir(exist_ok=True)
for i, frame in enumerate(frames):
frame_path = frames_dir / f'frame_{i+1:02d}.png'
frame.save(frame_path)
print(f"✓ Individual frames saved to: {frames_dir}")
# Cleanup
reader.close()
print()
print("=== Demo Complete ===")
if __name__ == '__main__':
main()
+296
View File
@@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""
Demo script for TOC overlay feature.
This script demonstrates the complete TOC overlay workflow:
1. Display reading page
2. Swipe up from bottom to open TOC overlay
3. Display TOC overlay with chapter list
4. Tap on a chapter to navigate
5. Close overlay and show new page
Generates a GIF showing all these interactions.
"""
from pathlib import Path
from dreader import EbookReader, TouchEvent, GestureType
from PIL import Image, ImageDraw, ImageFont
import time
def add_gesture_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
"""
Add a text annotation to an image showing what gesture is being performed.
Args:
image: Base image
text: Annotation text
position: "top" or "bottom"
Returns:
Image with annotation
"""
# Create a copy
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Try to use a nice font, fall back to default
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except:
font = ImageFont.load_default()
# Calculate text position
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (image.width - text_width) // 2
if position == "top":
y = 20
else:
y = image.height - text_height - 20
# Draw background rectangle
padding = 10
draw.rectangle(
[x - padding, y - padding, x + text_width + padding, y + text_height + padding],
fill=(0, 0, 0, 200)
)
# Draw text
draw.text((x, y), text, fill=(255, 255, 255), font=font)
return annotated
def add_swipe_arrow(image: Image.Image, start_y: int, end_y: int) -> Image.Image:
"""
Add a visual swipe arrow to show gesture direction.
Args:
image: Base image
start_y: Starting Y position
end_y: Ending Y position
Returns:
Image with arrow overlay
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw arrow in center of screen
x = image.width // 2
# Draw line
draw.line([(x, start_y), (x, end_y)], fill=(255, 100, 100), width=5)
# Draw arrowhead
arrow_size = 20
if end_y < start_y: # Upward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y + arrow_size),
(x + arrow_size, end_y + arrow_size)
], fill=(255, 100, 100))
else: # Downward arrow
draw.polygon([
(x, end_y),
(x - arrow_size, end_y - arrow_size),
(x + arrow_size, end_y - arrow_size)
], fill=(255, 100, 100))
return annotated
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "") -> Image.Image:
"""
Add a visual tap indicator to show where user tapped.
Args:
image: Base image
x, y: Tap coordinates
label: Optional label for the tap
Returns:
Image with tap indicator
"""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
# Draw circle at tap location
radius = 30
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline=(255, 100, 100),
width=5
)
# Draw crosshair
draw.line([(x - radius - 10, y), (x + radius + 10, y)], fill=(255, 100, 100), width=3)
draw.line([(x, y - radius - 10), (x, y + radius + 10)], fill=(255, 100, 100), width=3)
# Add label if provided
if label:
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
# Position label above tap point
label_x = x - text_width // 2
label_y = y - radius - 40
draw.text((label_x, label_y), label, fill=(255, 100, 100), font=font)
return annotated
def main():
"""Generate TOC overlay demo GIF"""
print("=== TOC Overlay Demo ===")
print()
# Find a test EPUB
epub_dir = Path(__file__).parent.parent / 'tests' / 'data' / 'library-epub'
epubs = list(epub_dir.glob('*.epub'))
if not epubs:
print("Error: No test EPUB files found!")
print(f"Looked in: {epub_dir}")
return
epub_path = epubs[0]
print(f"Using book: {epub_path.name}")
# Create reader
reader = EbookReader(page_size=(800, 1200))
# Load book
print("Loading book...")
success = reader.load_epub(str(epub_path))
if not success:
print("Error: Failed to load EPUB!")
return
print(f"Loaded: {reader.book_title} by {reader.book_author}")
print(f"Chapters: {len(reader.get_chapters())}")
print()
# Prepare frames for GIF
frames = []
frame_duration = [] # Duration in milliseconds for each frame
# Frame 1: Initial reading page
print("Frame 1: Initial reading page...")
page1 = reader.get_current_page()
annotated1 = add_gesture_annotation(page1, f"Reading: {reader.book_title}", "top")
frames.append(annotated1)
frame_duration.append(2000) # 2 seconds
# Frame 2: Show swipe up gesture
print("Frame 2: Swipe up gesture...")
swipe_visual = add_swipe_arrow(page1, 1100, 900)
annotated2 = add_gesture_annotation(swipe_visual, "Swipe up from bottom", "bottom")
frames.append(annotated2)
frame_duration.append(1000) # 1 second
# Frame 3: TOC overlay appears
print("Frame 3: TOC overlay opens...")
event_swipe_up = TouchEvent(gesture=GestureType.SWIPE_UP, x=400, y=1100)
response = reader.handle_touch(event_swipe_up)
print(f" Response: {response.action}")
# Get the overlay image by calling open_toc_overlay again
# (handle_touch already opened it, but we need the image)
overlay_image = reader.open_toc_overlay()
annotated3 = add_gesture_annotation(overlay_image, "Table of Contents", "top")
frames.append(annotated3)
frame_duration.append(3000) # 3 seconds to read
# Frame 4: Show tap on chapter III (index 6)
print("Frame 4: Tap on chapter III...")
chapters = reader.get_chapters()
if len(chapters) >= 7:
# Calculate tap position for chapter III (7th in list, index 6)
# Based on actual measurements from pyWebLayout link query:
# Chapter 6 "III" link is clickable at screen position (200, 378)
tap_x = 200
tap_y = 378
tap_visual = add_tap_indicator(overlay_image, tap_x, tap_y, "III")
annotated4 = add_gesture_annotation(tap_visual, "Tap chapter to navigate", "bottom")
frames.append(annotated4)
frame_duration.append(1500) # 1.5 seconds
# Frame 5: Navigate to chapter III
print(f"Frame 5: Jump to chapter III (tapping at {tap_x}, {tap_y})...")
event_tap = TouchEvent(gesture=GestureType.TAP, x=tap_x, y=tap_y)
response = reader.handle_touch(event_tap)
print(f" Response: {response.action}")
new_page = reader.get_current_page()
# Use the chapter title from the response data (more accurate)
if response.action == "chapter_selected" and "chapter_title" in response.data:
chapter_title = response.data['chapter_title']
else:
chapter_title = "Chapter"
annotated5 = add_gesture_annotation(new_page, f"Navigated to: {chapter_title}", "top")
frames.append(annotated5)
frame_duration.append(2000) # 2 seconds
else:
print(" Skipping chapter selection (not enough chapters)")
# Frame 6: Another page for context
print("Frame 6: Next page...")
reader.next_page()
page_final = reader.get_current_page()
annotated6 = add_gesture_annotation(page_final, "Reading continues...", "top")
frames.append(annotated6)
frame_duration.append(2000) # 2 seconds
# Save as GIF
output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'toc_overlay_demo.gif'
output_path.parent.mkdir(parents=True, exist_ok=True)
print()
print(f"Saving GIF with {len(frames)} frames...")
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=frame_duration,
loop=0,
optimize=False
)
print(f"✓ GIF saved to: {output_path}")
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
print(f" Frames: {len(frames)}")
print(f" Total duration: {sum(frame_duration) / 1000:.1f}s")
# Also save individual frames for documentation
frames_dir = output_path.parent / 'toc_overlay_frames'
frames_dir.mkdir(exist_ok=True)
for i, frame in enumerate(frames):
frame_path = frames_dir / f'frame_{i+1:02d}.png'
frame.save(frame_path)
print(f"✓ Individual frames saved to: {frames_dir}")
# Cleanup
reader.close()
print()
print("=== Demo Complete ===")
if __name__ == '__main__':
main()
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env python3
"""
Comprehensive demo of the EbookReader functionality.
This script demonstrates all features of the pyWebLayout EbookReader:
- Loading EPUB files
- Page navigation (forward/backward)
- Position saving/loading
- Chapter navigation
- Font size and spacing adjustments
- Getting book and position information
Usage:
python ereader_demo.py path/to/book.epub
"""
import sys
import os
from pathlib import Path
# Add parent directory to path to import pyWebLayout
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.application import EbookReader
def print_separator():
"""Print a visual separator."""
print("\n" + "="*70 + "\n")
def demo_basic_navigation(reader: EbookReader):
"""Demonstrate basic page navigation."""
print("DEMO: Basic Navigation")
print_separator()
# Get current page
print("Getting first page...")
page = reader.get_current_page()
if page:
print(f"✓ Current page rendered: {page.size}")
reader.render_to_file("demo_page_001.png")
print(" Saved to: demo_page_001.png")
# Navigate forward
print("\nNavigating to next page...")
page = reader.next_page()
if page:
print(f"✓ Next page rendered: {page.size}")
reader.render_to_file("demo_page_002.png")
print(" Saved to: demo_page_002.png")
# Navigate backward
print("\nNavigating to previous page...")
page = reader.previous_page()
if page:
print(f"✓ Previous page rendered: {page.size}")
print_separator()
def demo_position_management(reader: EbookReader):
"""Demonstrate position save/load functionality."""
print("DEMO: Position Management")
print_separator()
# Navigate a few pages forward
print("Navigating forward 3 pages...")
for i in range(3):
reader.next_page()
# Save position
print("Saving current position as 'demo_bookmark'...")
success = reader.save_position("demo_bookmark")
if success:
print("✓ Position saved successfully")
# Get position info
pos_info = reader.get_position_info()
print(f"\nCurrent position info:")
print(f" Chapter: {pos_info.get('chapter', {}).get('title', 'N/A')}")
print(f" Block index: {pos_info['position']['block_index']}")
print(f" Word index: {pos_info['position']['word_index']}")
print(f" Progress: {pos_info['progress']*100:.1f}%")
# Navigate away
print("\nNavigating forward 5 more pages...")
for i in range(5):
reader.next_page()
# Load saved position
print("Loading saved position 'demo_bookmark'...")
page = reader.load_position("demo_bookmark")
if page:
print("✓ Position restored successfully")
reader.render_to_file("demo_restored_position.png")
print(" Saved to: demo_restored_position.png")
# List all saved positions
positions = reader.list_saved_positions()
print(f"\nAll saved positions: {positions}")
print_separator()
def demo_chapter_navigation(reader: EbookReader):
"""Demonstrate chapter navigation."""
print("DEMO: Chapter Navigation")
print_separator()
# Get all chapters
chapters = reader.get_chapters()
print(f"Found {len(chapters)} chapters:")
for title, idx in chapters[:5]: # Show first 5
print(f" [{idx}] {title}")
if len(chapters) > 5:
print(f" ... and {len(chapters) - 5} more")
# Jump to a chapter by index
if len(chapters) > 1:
print(f"\nJumping to chapter 1...")
page = reader.jump_to_chapter(1)
if page:
print("✓ Jumped to chapter successfully")
reader.render_to_file("demo_chapter_1.png")
print(" Saved to: demo_chapter_1.png")
# Get current chapter info
chapter_info = reader.get_current_chapter_info()
if chapter_info:
print(f" Current chapter: {chapter_info['title']}")
# Jump to a chapter by title (if we have chapters)
if len(chapters) > 0:
first_chapter_title = chapters[0][0]
print(f"\nJumping to chapter by title: '{first_chapter_title}'...")
page = reader.jump_to_chapter(first_chapter_title)
if page:
print("✓ Jumped to chapter by title successfully")
print_separator()
def demo_font_size_adjustment(reader: EbookReader):
"""Demonstrate font size adjustments."""
print("DEMO: Font Size Adjustment")
print_separator()
# Save current page for comparison
print("Rendering page at normal font size (1.0x)...")
page = reader.get_current_page()
if page:
reader.render_to_file("demo_font_normal.png")
print("✓ Saved to: demo_font_normal.png")
# Increase font size
print("\nIncreasing font size...")
page = reader.increase_font_size()
if page:
print(f"✓ Font size increased to {reader.get_font_size():.1f}x")
reader.render_to_file("demo_font_larger.png")
print(" Saved to: demo_font_larger.png")
# Increase again
print("\nIncreasing font size again...")
page = reader.increase_font_size()
if page:
print(f"✓ Font size increased to {reader.get_font_size():.1f}x")
reader.render_to_file("demo_font_largest.png")
print(" Saved to: demo_font_largest.png")
# Decrease font size
print("\nDecreasing font size...")
page = reader.decrease_font_size()
if page:
print(f"✓ Font size decreased to {reader.get_font_size():.1f}x")
# Set specific font size
print("\nResetting to normal font size (1.0x)...")
page = reader.set_font_size(1.0)
if page:
print("✓ Font size reset to 1.0x")
print_separator()
def demo_spacing_adjustment(reader: EbookReader):
"""Demonstrate line and block spacing adjustments."""
print("DEMO: Spacing Adjustment")
print_separator()
# Save current page
print("Rendering page with default spacing...")
page = reader.get_current_page()
if page:
reader.render_to_file("demo_spacing_default.png")
print("✓ Saved to: demo_spacing_default.png")
# Increase line spacing
print("\nIncreasing line spacing to 10px...")
page = reader.set_line_spacing(10)
if page:
print("✓ Line spacing increased")
reader.render_to_file("demo_spacing_lines_10.png")
print(" Saved to: demo_spacing_lines_10.png")
# Increase inter-block spacing
print("\nIncreasing inter-block spacing to 25px...")
page = reader.set_inter_block_spacing(25)
if page:
print("✓ Inter-block spacing increased")
reader.render_to_file("demo_spacing_blocks_25.png")
print(" Saved to: demo_spacing_blocks_25.png")
# Reset to defaults
print("\nResetting spacing to defaults (line: 5px, block: 15px)...")
reader.set_line_spacing(5)
page = reader.set_inter_block_spacing(15)
if page:
print("✓ Spacing reset to defaults")
print_separator()
def demo_book_information(reader: EbookReader):
"""Demonstrate getting book information."""
print("DEMO: Book Information")
print_separator()
# Get book info
book_info = reader.get_book_info()
print("Book Information:")
print(f" Title: {book_info['title']}")
print(f" Author: {book_info['author']}")
print(f" Document ID: {book_info['document_id']}")
print(f" Total blocks: {book_info['total_blocks']}")
print(f" Total chapters: {book_info['total_chapters']}")
print(f" Page size: {book_info['page_size']}")
print(f" Font scale: {book_info['font_scale']}")
# Get reading progress
progress = reader.get_reading_progress()
print(f"\nReading Progress: {progress*100:.1f}%")
# Get detailed position info
pos_info = reader.get_position_info()
print("\nDetailed Position:")
print(f" Chapter index: {pos_info['position']['chapter_index']}")
print(f" Block index: {pos_info['position']['block_index']}")
print(f" Word index: {pos_info['position']['word_index']}")
chapter = pos_info.get('chapter', {})
if chapter.get('title'):
print(f" Current chapter: {chapter['title']}")
print_separator()
def main():
"""Main function to run all demos."""
if len(sys.argv) < 2:
print("Usage: python ereader_demo.py path/to/book.epub")
print("\nExample EPUBs to try:")
print(" - tests/data/test.epub")
print(" - tests/data/test2.epub")
sys.exit(1)
epub_path = sys.argv[1]
if not os.path.exists(epub_path):
print(f"Error: File not found: {epub_path}")
sys.exit(1)
print("="*70)
print(" EbookReader Demo - pyWebLayout")
print("="*70)
print(f"\nLoading EPUB: {epub_path}")
# Create reader with context manager
with EbookReader(page_size=(800, 1000)) as reader:
# Load the EPUB
if not reader.load_epub(epub_path):
print("Error: Failed to load EPUB file")
sys.exit(1)
print("✓ EPUB loaded successfully")
# Run all demos
try:
demo_basic_navigation(reader)
demo_position_management(reader)
demo_chapter_navigation(reader)
demo_font_size_adjustment(reader)
demo_spacing_adjustment(reader)
demo_book_information(reader)
print("\n" + "="*70)
print(" Demo Complete!")
print("="*70)
print("\nGenerated demo images:")
demo_files = [
"demo_page_001.png",
"demo_page_002.png",
"demo_restored_position.png",
"demo_chapter_1.png",
"demo_font_normal.png",
"demo_font_larger.png",
"demo_font_largest.png",
"demo_spacing_default.png",
"demo_spacing_lines_10.png",
"demo_spacing_blocks_25.png"
]
for filename in demo_files:
if os.path.exists(filename):
print(f"{filename}")
print("\nAll features demonstrated successfully!")
except Exception as e:
print(f"\nError during demo: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+422
View File
@@ -0,0 +1,422 @@
#!/usr/bin/env python3
"""
Generate animated GIFs demonstrating EbookReader functionality.
This script creates animated GIFs showcasing:
1. Page navigation (next/previous)
2. Font size adjustment
3. Chapter navigation
4. Bookmark/position management
5. Word highlighting
The GIFs are saved to the examples/ directory and can be included in documentation.
Usage:
python generate_ereader_gifs.py path/to/book.epub [output_dir]
Example:
python generate_ereader_gifs.py ../tests/data/test.epub ../docs/images
"""
import sys
import os
from pathlib import Path
from typing import List
# Add parent directory to path to import pyWebLayout
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.application import EbookReader
from pyWebLayout.core.highlight import HighlightColor
from PIL import Image
def create_gif(images: List[Image.Image], output_path: str, duration: int = 800, loop: int = 0):
"""
Create an animated GIF from a list of PIL Images.
Args:
images: List of PIL Images to animate
output_path: Path where to save the GIF
duration: Duration of each frame in milliseconds
loop: Number of loops (0 = infinite)
"""
if not images:
print(f"Warning: No images provided for {output_path}")
return False
try:
# Save as animated GIF
images[0].save(
output_path,
save_all=True,
append_images=images[1:],
duration=duration,
loop=loop,
optimize=False
)
print(f"✓ Created: {output_path} ({len(images)} frames)")
return True
except Exception as e:
print(f"✗ Error creating {output_path}: {e}")
return False
def generate_page_navigation_gif(reader: EbookReader, output_path: str):
"""Generate GIF showing page navigation (forward and backward)."""
print("\n[1/4] Generating page navigation GIF...")
frames = []
# Go to beginning
reader.set_font_size(1.0)
# Capture 5 pages going forward
for i in range(5):
page = reader.get_current_page()
if page:
frames.append(page.copy())
reader.next_page()
# Go back to start
for _ in range(4):
reader.previous_page()
# Capture 5 pages going forward again (smoother loop)
for i in range(5):
page = reader.get_current_page()
if page:
frames.append(page.copy())
reader.next_page()
create_gif(frames, output_path, duration=600)
def generate_font_size_gif(reader: EbookReader, output_path: str):
"""Generate GIF showing font size adjustment."""
print("\n[2/4] Generating font size adjustment GIF...")
frames = []
# Reset to beginning and normal font
for _ in range(10):
reader.previous_page()
reader.set_font_size(1.0)
# Font sizes to demonstrate
font_scales = [0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.3, 1.2, 1.1, 1.0, 0.9, 0.8]
for scale in font_scales:
page = reader.set_font_size(scale)
if page:
frames.append(page.copy())
# Reset to normal
reader.set_font_size(1.0)
create_gif(frames, output_path, duration=500)
def generate_chapter_navigation_gif(reader: EbookReader, output_path: str):
"""Generate GIF showing chapter navigation."""
print("\n[3/4] Generating chapter navigation GIF...")
frames = []
# Reset font
reader.set_font_size(1.0)
# Get chapters
chapters = reader.get_chapters()
if len(chapters) == 0:
print(" Warning: No chapters found, skipping chapter navigation GIF")
return
# Visit first few chapters (or loop through available chapters)
chapter_indices = list(range(min(5, len(chapters))))
# Add some chapters twice for smoother animation
for idx in chapter_indices:
page = reader.jump_to_chapter(idx)
if page:
frames.append(page.copy())
# Add a second frame at each chapter for pause effect
frames.append(page.copy())
# Go back to first chapter
page = reader.jump_to_chapter(0)
if page:
frames.append(page.copy())
if frames:
create_gif(frames, output_path, duration=1000)
else:
print(" Warning: No frames captured for chapter navigation")
def generate_bookmark_gif(reader: EbookReader, output_path: str):
"""Generate GIF showing bookmark save/load functionality."""
print("\n[4/5] Generating bookmark/position GIF...")
frames = []
# Reset font
reader.set_font_size(1.0)
# Go to beginning
for _ in range(20):
reader.previous_page()
# Capture initial position
page = reader.get_current_page()
if page:
frames.append(page.copy())
frames.append(page.copy()) # Hold frame
# Navigate forward a bit
for i in range(3):
reader.next_page()
page = reader.get_current_page()
if page:
frames.append(page.copy())
# Save this position
reader.save_position("demo_bookmark")
page = reader.get_current_page()
if page:
frames.append(page.copy())
frames.append(page.copy()) # Hold frame to show saved position
# Navigate away
for i in range(5):
reader.next_page()
page = reader.get_current_page()
if page:
frames.append(page.copy())
# Hold at distant position
page = reader.get_current_page()
if page:
frames.append(page.copy())
frames.append(page.copy())
# Jump back to bookmark
page = reader.load_position("demo_bookmark")
if page:
frames.append(page.copy())
frames.append(page.copy())
frames.append(page.copy()) # Hold longer to show we're back
create_gif(frames, output_path, duration=600)
def generate_highlighting_gif(reader: EbookReader, output_path: str):
"""Generate GIF showing word highlighting functionality."""
print("\n[5/5] Generating word highlighting GIF...")
frames = []
# Reset font
reader.set_font_size(1.0)
# Find a page with actual text content (skip title/cover pages)
for _ in range(5):
reader.next_page()
# Collect text objects from the page with their actual positions
from pyWebLayout.concrete.text import Line
text_positions = []
# Try to find a page with text
max_attempts = 10
for attempt in range(max_attempts):
page = reader.manager.get_current_page()
text_positions = []
for child in page._children:
if isinstance(child, Line):
for text_obj in child._text_objects:
# Skip empty text
if not hasattr(text_obj, '_text') or not text_obj._text or not text_obj._text.strip():
continue
# Calculate center of text object, but clamp Y to Line bounds
origin = text_obj._origin
size = text_obj.size
center_x = int(origin[0] + size[0] / 2)
center_y = int(origin[1] + size[1] / 2)
# Clamp Y to be within Line bounds (avoids the baseline extension issue)
line_y_min = int(child._origin[1])
line_y_max = int(child._origin[1] + child._size[1])
clamped_y = max(line_y_min, min(line_y_max - 1, center_y))
text_positions.append((center_x, clamped_y, text_obj._text))
# If we found enough text, use this page
if len(text_positions) > 10:
print(f" Found page with {len(text_positions)} words")
break
# Otherwise try next page
reader.next_page()
if len(text_positions) == 0:
print(" Warning: Could not find a page with text after searching")
# Capture initial page without highlights
page_img = reader.get_current_page(include_highlights=False)
if page_img:
frames.append(page_img.copy())
frames.append(page_img.copy()) # Hold frame
# Use different colors for highlighting
colors = [
HighlightColor.YELLOW.value,
HighlightColor.GREEN.value,
HighlightColor.BLUE.value,
HighlightColor.PINK.value,
HighlightColor.ORANGE.value,
]
# Select a subset of words to highlight (spread across the page)
# Take every Nth word to get a good distribution
if len(text_positions) > 10:
step = len(text_positions) // 5
selected_positions = [text_positions[i * step] for i in range(5) if i * step < len(text_positions)]
else:
selected_positions = text_positions[:5]
highlighted_words = 0
color_names = ['YELLOW', 'GREEN', 'BLUE', 'PINK', 'ORANGE']
print(f"\n Highlighting words:")
for i, (x, y, text) in enumerate(selected_positions):
color = colors[i % len(colors)]
color_name = color_names[i % len(color_names)]
# Highlight the word at this position
highlight_id = reader.highlight_word(x, y, color=color)
if highlight_id:
highlighted_words += 1
print(f" [{color_name:6s}] {text}")
# Capture page with new highlight
page_img = reader.get_current_page(include_highlights=True)
if page_img:
frames.append(page_img.copy())
# Hold frame briefly to show the new highlight
frames.append(page_img.copy())
# If we managed to highlight any words, show the final result
if highlighted_words > 0:
page_img = reader.get_current_page(include_highlights=True)
if page_img:
# Hold final frame longer
for _ in range(3):
frames.append(page_img.copy())
# Clear highlights one by one
for highlight in reader.list_highlights():
reader.remove_highlight(highlight.id)
page_img = reader.get_current_page(include_highlights=True)
if page_img:
frames.append(page_img.copy())
# Show final cleared page
page_img = reader.get_current_page(include_highlights=False)
if page_img:
frames.append(page_img.copy())
frames.append(page_img.copy())
print(f" Successfully highlighted {highlighted_words} words")
else:
print(" Warning: No words found to highlight on current page")
if frames:
create_gif(frames, output_path, duration=700)
else:
print(" Warning: No frames captured for highlighting")
def main():
"""Main function to generate all GIFs."""
if len(sys.argv) < 2:
print("Usage: python generate_ereader_gifs.py path/to/book.epub [output_dir]")
print("\nExample:")
print(" python generate_ereader_gifs.py ../tests/data/test.epub ../docs/images")
sys.exit(1)
epub_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "."
# Validate EPUB path
if not os.path.exists(epub_path):
print(f"Error: EPUB file not found: {epub_path}")
sys.exit(1)
# Create output directory
os.makedirs(output_dir, exist_ok=True)
print("="*70)
print(" EbookReader Animated GIF Generator")
print("="*70)
print(f"\nInput EPUB: {epub_path}")
print(f"Output directory: {output_dir}")
# Create paths for output GIFs
nav_gif = os.path.join(output_dir, "ereader_page_navigation.gif")
font_gif = os.path.join(output_dir, "ereader_font_size.gif")
chapter_gif = os.path.join(output_dir, "ereader_chapter_navigation.gif")
bookmark_gif = os.path.join(output_dir, "ereader_bookmarks.gif")
highlight_gif = os.path.join(output_dir, "ereader_highlighting.gif")
try:
# Create reader
with EbookReader(page_size=(600, 800), margin=30) as reader:
# Load EPUB
print("\nLoading EPUB...")
if not reader.load_epub(epub_path):
print("Error: Failed to load EPUB file")
sys.exit(1)
print("✓ EPUB loaded successfully")
# Get book info
book_info = reader.get_book_info()
print(f"\nBook: {book_info['title']}")
print(f"Author: {book_info['author']}")
print(f"Chapters: {book_info['total_chapters']}")
print(f"Blocks: {book_info['total_blocks']}")
print("\nGenerating GIFs...")
print("-" * 70)
# Generate all GIFs
generate_page_navigation_gif(reader, nav_gif)
generate_font_size_gif(reader, font_gif)
generate_chapter_navigation_gif(reader, chapter_gif)
generate_bookmark_gif(reader, bookmark_gif)
generate_highlighting_gif(reader, highlight_gif)
print("\n" + "="*70)
print(" Generation Complete!")
print("="*70)
print("\nGenerated files:")
for gif_path in [nav_gif, font_gif, chapter_gif, bookmark_gif, highlight_gif]:
if os.path.exists(gif_path):
size = os.path.getsize(gif_path)
print(f"{gif_path} ({size/1024:.1f} KB)")
print("\nYou can now add these GIFs to your README.md!")
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
Generate demo GIF showing the complete library ↔ reading workflow.
This script creates an animated GIF demonstrating:
1. Library view with multiple books
2. Selecting a book by tapping
3. Reading the book (showing 5 pages)
4. Closing the book (back to library)
5. Reopening the same book
6. Auto-resuming at the saved position
Usage:
python generate_library_demo_gif.py path/to/library/directory output.gif
"""
import sys
import os
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.library import LibraryManager
from dreader.application import EbookReader
from dreader.gesture import TouchEvent, GestureType
def add_annotation(image: Image.Image, text: str, position: str = "top") -> Image.Image:
"""
Add annotation text to an image.
Args:
image: PIL Image to annotate
text: Annotation text
position: "top" or "bottom"
Returns:
New image with annotation
"""
# Create a copy
img = image.copy()
draw = ImageDraw.Draw(img)
# Try to use a nice font, fall back to default
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except:
font = ImageFont.load_default()
# Get text size
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Calculate position
x = (img.width - text_width) // 2
if position == "top":
y = 20
else:
y = img.height - text_height - 20
# Draw background rectangle
padding = 10
draw.rectangle(
[x - padding, y - padding, x + text_width + padding, y + text_height + padding],
fill=(0, 0, 0, 200)
)
# Draw text
draw.text((x, y), text, fill=(255, 255, 255), font=font)
return img
def add_tap_indicator(image: Image.Image, x: int, y: int, label: str = "TAP") -> Image.Image:
"""
Add a visual tap indicator at coordinates.
Args:
image: PIL Image
x, y: Tap coordinates
label: Label text
Returns:
New image with tap indicator
"""
img = image.copy()
draw = ImageDraw.Draw(img)
# Draw circle at tap location
radius = 30
draw.ellipse(
[x - radius, y - radius, x + radius, y + radius],
outline=(255, 0, 0),
width=4
)
# Draw crosshair
cross_size = 10
draw.line([x - cross_size, y, x + cross_size, y], fill=(255, 0, 0), width=3)
draw.line([x, y - cross_size, x, y + cross_size], fill=(255, 0, 0), width=3)
# Draw label
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 18)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Position label above tap
label_x = x - text_width // 2
label_y = y - radius - text_height - 10
# Background for label
padding = 5
draw.rectangle(
[label_x - padding, label_y - padding,
label_x + text_width + padding, label_y + text_height + padding],
fill=(255, 0, 0)
)
draw.text((label_x, label_y), label, fill=(255, 255, 255), font=font)
return img
def generate_library_demo_gif(library_path: str, output_path: str):
"""
Generate the demo GIF.
Args:
library_path: Path to directory containing EPUB files
output_path: Output GIF file path
"""
frames = []
frame_durations = [] # Duration for each frame in milliseconds
print("Generating library demo GIF...")
print("=" * 70)
# ===================================================================
# FRAME 1: Library view
# ===================================================================
print("\n1. Rendering library view...")
library = LibraryManager(
library_path=library_path,
page_size=(800, 1200)
)
books = library.scan_library()
print(f" Found {len(books)} books")
if len(books) == 0:
print("Error: No books found in library")
sys.exit(1)
library_image = library.render_library()
annotated = add_annotation(library_image, "📚 My Library - Select a book", "top")
frames.append(annotated)
frame_durations.append(2000) # Hold for 2 seconds
# ===================================================================
# FRAME 2: Show tap on first book
# ===================================================================
print("2. Showing book selection...")
tap_x, tap_y = 400, 150 # Approximate position of first book
tap_frame = add_tap_indicator(library_image, tap_x, tap_y, "SELECT BOOK")
annotated = add_annotation(tap_frame, "📚 Tap to open book", "top")
frames.append(annotated)
frame_durations.append(1500)
# Get the selected book
selected_book = library.handle_library_tap(tap_x, tap_y)
if not selected_book:
selected_book = books[0]['path']
print(f" Selected: {selected_book}")
# ===================================================================
# FRAME 3-7: Reading pages
# ===================================================================
print("3. Opening book and reading pages...")
reader = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
reader.load_epub(selected_book)
book_info = reader.get_book_info()
print(f" Title: {book_info['title']}")
# First page
page = reader.get_current_page()
annotated = add_annotation(page, f"📖 {book_info['title']} - Page 1", "top")
frames.append(annotated)
frame_durations.append(1500)
# Turn 4 more pages (total 5 pages)
for i in range(2, 6):
print(f" Reading page {i}...")
reader.next_page()
page = reader.get_current_page()
annotated = add_annotation(page, f"📖 Reading - Page {i}", "top")
frames.append(annotated)
frame_durations.append(1000) # Faster page turns
# ===================================================================
# FRAME 8: Show settings overlay with "Back to Library"
# ===================================================================
print("4. Opening settings overlay...")
settings_overlay = reader.open_settings_overlay()
if settings_overlay:
annotated = add_annotation(settings_overlay, "⚙️ Settings - Tap 'Back to Library'", "top")
# Show where to tap (estimated position of back button)
tap_frame = add_tap_indicator(annotated, 400, 950, "BACK")
frames.append(tap_frame)
frame_durations.append(2000)
# ===================================================================
# FRAME 9: Save position and return to library
# ===================================================================
print("5. Saving position and returning to library...")
# Save current position for resume
reader.save_position("__auto_resume__")
pos_info = reader.get_position_info()
saved_progress = pos_info['progress'] * 100
print(f" Saved at {saved_progress:.1f}% progress")
# Close reader
reader.close()
# Re-render library
library_image = library.render_library()
annotated = add_annotation(library_image, "📚 Back to Library (position saved)", "top")
frames.append(annotated)
frame_durations.append(2000)
# ===================================================================
# FRAME 10: Tap same book again
# ===================================================================
print("6. Re-selecting same book...")
tap_frame = add_tap_indicator(library_image, tap_x, tap_y, "REOPEN")
annotated = add_annotation(tap_frame, "📚 Tap to reopen book", "top")
frames.append(annotated)
frame_durations.append(1500)
# ===================================================================
# FRAME 11: Reopen book and auto-resume
# ===================================================================
print("7. Reopening book with auto-resume...")
reader2 = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
reader2.load_epub(selected_book)
# Load saved position
resumed_page = reader2.load_position("__auto_resume__")
if resumed_page:
pos_info = reader2.get_position_info()
progress = pos_info['progress'] * 100
print(f" ✓ Resumed at {progress:.1f}% progress")
annotated = add_annotation(resumed_page, f"✅ Auto-resumed at {progress:.1f}%", "top")
frames.append(annotated)
frame_durations.append(3000) # Hold final frame longer
reader2.close()
# ===================================================================
# Save GIF
# ===================================================================
print("\n8. Saving GIF...")
print(f" Total frames: {len(frames)}")
print(f" Output: {output_path}")
# Save as GIF with variable durations
frames[0].save(
output_path,
save_all=True,
append_images=frames[1:],
duration=frame_durations,
loop=0, # Loop forever
optimize=False # Keep quality
)
print(f"\n✓ Demo GIF created: {output_path}")
print(f" Size: {os.path.getsize(output_path) / 1024 / 1024:.1f} MB")
# Cleanup
library.cleanup()
print("\n" + "=" * 70)
print("Demo complete!")
print("\nThe GIF demonstrates:")
print(" 1. Library view with book selection")
print(" 2. Opening a book and reading 5 pages")
print(" 3. Settings overlay with 'Back to Library' button")
print(" 4. Returning to library (with position saved)")
print(" 5. Reopening the same book")
print(" 6. Auto-resuming at saved position")
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python generate_library_demo_gif.py path/to/library [output.gif]")
print("\nExample:")
print(" python generate_library_demo_gif.py tests/data/library-epub/")
print(" python generate_library_demo_gif.py tests/data/library-epub/ doc/images/custom_demo.gif")
sys.exit(1)
library_path = sys.argv[1]
output_path = sys.argv[2] if len(sys.argv) > 2 else "doc/images/library_reading_demo.gif"
if not os.path.exists(library_path):
print(f"Error: Directory not found: {library_path}")
sys.exit(1)
if not os.path.isdir(library_path):
print(f"Error: Not a directory: {library_path}")
sys.exit(1)
try:
generate_library_demo_gif(library_path, output_path)
except Exception as e:
print(f"\nError generating demo: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+400
View File
@@ -0,0 +1,400 @@
#!/usr/bin/env python3
"""
Integration demo: Library → Reading → Settings → Back to Library
This example demonstrates the complete LIBRARY ↔ READING mode transition workflow:
1. Display a library of EPUB files
2. Select a book by clicking/tapping
3. Open and read the selected book
4. Access settings overlay
5. Return to library from the settings overlay
6. Select another book
This demonstrates the full user flow for an e-reader application.
Usage:
python library_reading_integration.py path/to/library/directory
"""
import sys
import os
from pathlib import Path
# Add parent directory to path to import dreader
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.library import LibraryManager
from dreader.application import EbookReader
from dreader.gesture import TouchEvent, GestureType, ActionType
def print_separator():
"""Print a visual separator."""
print("\n" + "="*70 + "\n")
def simulate_mode_transition_workflow(library_path: str):
"""
Simulate the complete workflow of library browsing and book reading.
Args:
library_path: Path to directory containing EPUB files
"""
print_separator()
print("INTEGRATION TEST: LIBRARY ↔ READING MODE TRANSITIONS")
print_separator()
# ===================================================================
# STEP 1: LIBRARY MODE - Display available books
# ===================================================================
print("STEP 1: LIBRARY MODE - Displaying available books")
print("-" * 70)
# Initialize library manager
library = LibraryManager(
library_path=library_path,
page_size=(800, 1200)
)
# Scan for books
books = library.scan_library()
print(f"✓ Found {len(books)} books in library")
if len(books) == 0:
print("Error: No EPUB files found in library directory")
print(f"Please add some .epub files to: {library_path}")
sys.exit(1)
# Display book list
for i, book in enumerate(books):
print(f" [{i}] {book['title']} by {book['author']}")
# Render library view
print("\nRendering library view...")
library_image = library.render_library()
library_image.save("integration_01_library.png")
print("✓ Saved library view to: integration_01_library.png")
# ===================================================================
# STEP 2: SIMULATE BOOK SELECTION - User taps on first book
# ===================================================================
print_separator()
print("STEP 2: BOOK SELECTION - Simulating tap on first book")
print("-" * 70)
# Simulate a tap on the first book row
# Row positions depend on rendering, but first book is typically near top
# We'll tap in the middle of the first book row area
tap_x, tap_y = 400, 150 # Approximate center of first book row
print(f"Simulating tap at ({tap_x}, {tap_y})...")
selected_book_path = library.handle_library_tap(tap_x, tap_y)
if not selected_book_path:
print("Warning: Tap didn't hit a book. Selecting first book directly...")
selected_book_path = books[0]['path']
print(f"✓ Selected book: {selected_book_path}")
# ===================================================================
# STEP 3: READING MODE - Open the selected book
# ===================================================================
print_separator()
print("STEP 3: READING MODE - Opening selected book")
print("-" * 70)
# Create reader
reader = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
# Load the EPUB
print(f"Loading: {selected_book_path}")
success = reader.load_epub(selected_book_path)
if not success:
print("Error: Failed to load EPUB")
sys.exit(1)
print("✓ Book loaded successfully")
# Get book info
book_info = reader.get_book_info()
print(f" Title: {book_info['title']}")
print(f" Author: {book_info['author']}")
print(f" Chapters: {book_info['total_chapters']}")
# Render first page
print("\nRendering first page...")
page_image = reader.get_current_page()
page_image.save("integration_02_reading_page1.png")
print("✓ Saved first page to: integration_02_reading_page1.png")
# ===================================================================
# STEP 4: PAGE NAVIGATION - Turn some pages
# ===================================================================
print_separator()
print("STEP 4: PAGE NAVIGATION - Simulating page turns")
print("-" * 70)
# Simulate swipe left (next page)
print("Simulating SWIPE_LEFT (next page)...")
touch_event = TouchEvent(GestureType.SWIPE_LEFT, 600, 600)
response = reader.handle_touch(touch_event)
if response.action == ActionType.PAGE_TURN:
print(f"✓ Page turned: {response.data}")
page_image = reader.get_current_page()
page_image.save("integration_03_reading_page2.png")
print(" Saved to: integration_03_reading_page2.png")
# Turn another page
print("\nSimulating another SWIPE_LEFT...")
touch_event = TouchEvent(GestureType.SWIPE_LEFT, 600, 600)
response = reader.handle_touch(touch_event)
if response.action == ActionType.PAGE_TURN:
print(f"✓ Page turned: {response.data}")
# ===================================================================
# STEP 5: SETTINGS OVERLAY - Open and adjust settings
# ===================================================================
print_separator()
print("STEP 5: SETTINGS OVERLAY - Opening settings")
print("-" * 70)
# Open settings overlay
print("Opening settings overlay...")
overlay_image = reader.open_settings_overlay()
if overlay_image:
overlay_image.save("integration_04_settings_overlay.png")
print("✓ Settings overlay opened")
print(" Saved to: integration_04_settings_overlay.png")
# Simulate tapping "Increase Font Size" button
print("\nSimulating tap on 'Increase Font Size'...")
# The increase button is typically around y=250-280 in the overlay
tap_x, tap_y = 400, 270
touch_event = TouchEvent(GestureType.TAP, tap_x, tap_y)
response = reader.handle_touch(touch_event)
if response.action == ActionType.SETTING_CHANGED:
print(f"✓ Setting changed: {response.data}")
updated_overlay = reader.get_current_page()
updated_overlay.save("integration_05_settings_font_increased.png")
print(" Saved updated overlay to: integration_05_settings_font_increased.png")
# ===================================================================
# STEP 6: BACK TO LIBRARY - Use the new "Back to Library" button
# ===================================================================
print_separator()
print("STEP 6: BACK TO LIBRARY - Using 'Back to Library' button")
print("-" * 70)
# The settings overlay is 60% width x 70% height, centered
# For 800x1200: panel is 480x840, offset at (160, 180)
# The "Back to Library" button is near the bottom of the overlay panel
# Let's try scanning for it by querying multiple y-positions
print("Scanning for 'Back to Library' button...")
found_button = False
# Scan a wider range with finer granularity
# Settings overlay is 60% x 70% of 800x1200 = 480x840, centered at (160, 180)
# So overlay goes from y=180 to y=1020
# Button should be near bottom, scan from y=600 to y=1020
debug_results = []
for test_y in range(600, 1021, 20):
test_x = 400 # Center of screen
# Use the overlay manager's query method if there's an overlay open
if hasattr(reader, 'overlay_manager'):
result = reader.overlay_manager.query_overlay_pixel(test_x, test_y)
if result:
debug_results.append((test_y, result.get("link_target"), result.get("text", "")[:30]))
if result.get("is_interactive") and result.get("link_target"):
link = result["link_target"]
if link == "action:back_to_library":
print(f"✓ Found button at approximately ({test_x}, {test_y})")
tap_x, tap_y = test_x, test_y
found_button = True
break
if not found_button and debug_results:
print(f" Debug: Scanned {len(debug_results)} positions, found these links:")
for y, link, text in debug_results[-5:]: # Show last 5
if link:
print(f" y={y}: link={link}, text='{text}'")
if not found_button:
print(" Button not found via scan, using estimated position...")
# Fallback: overlay height is 840, centered at y=180
# Button is near bottom, approximately at panel_y + panel_height - 100
tap_x, tap_y = 400, 900
print(f"Simulating tap at ({tap_x}, {tap_y})...")
touch_event = TouchEvent(GestureType.TAP, tap_x, tap_y)
response = reader.handle_touch(touch_event)
if response.action == ActionType.BACK_TO_LIBRARY:
print("✓ BACK_TO_LIBRARY action received!")
print(" Application would now:")
print(" 1. Close the current book")
print(" 2. Return to library view")
print(" 3. Save reading position for resume")
# Save current position for resume
reader.save_position("__auto_resume__")
print("\n ✓ Auto-resume position saved")
# Close the reader
reader.close()
print(" ✓ Book closed")
# Re-render library
print("\n Re-rendering library view...")
library_image = library.render_library()
library_image.save("integration_06_back_to_library.png")
print(" ✓ Saved library view to: integration_06_back_to_library.png")
else:
print(f"Unexpected response: {response.action}")
print("Note: The button might be outside the overlay area or coordinates need adjustment")
# ===================================================================
# STEP 7: SELECT ANOTHER BOOK (if multiple books available)
# ===================================================================
if len(books) > 1:
print_separator()
print("STEP 7: SELECTING ANOTHER BOOK")
print("-" * 70)
# Select second book
second_book_path = books[1]['path']
print(f"Selecting second book: {second_book_path}")
# Create new reader instance
reader2 = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
# Load second book
success = reader2.load_epub(second_book_path)
if success:
book_info = reader2.get_book_info()
print(f"✓ Loaded: {book_info['title']} by {book_info['author']}")
# Render first page
page_image = reader2.get_current_page()
page_image.save("integration_07_second_book.png")
print(" Saved to: integration_07_second_book.png")
reader2.close()
# ===================================================================
# STEP 8: RESUME PREVIOUS BOOK (demonstrate auto-resume)
# ===================================================================
print_separator()
print("STEP 8: AUTO-RESUME - Reopening first book at saved position")
print("-" * 70)
# Create new reader
reader3 = EbookReader(
page_size=(800, 1200),
margin=40,
background_color=(255, 255, 255)
)
# Load the book
print(f"Reloading: {selected_book_path}")
success = reader3.load_epub(selected_book_path)
if success:
# Load auto-resume position
print("Loading auto-resume position...")
page = reader3.load_position("__auto_resume__")
if page:
print("✓ Resumed at saved position!")
pos_info = reader3.get_position_info()
print(f" Progress: {pos_info['progress']*100:.1f}%")
page.save("integration_08_resumed_position.png")
print(" Saved to: integration_08_resumed_position.png")
else:
print("No saved position found (started from beginning)")
reader3.close()
# Cleanup
library.cleanup()
print_separator()
print("✓ INTEGRATION TEST COMPLETE!")
print_separator()
print("\nGenerated demonstration images:")
demo_files = [
"integration_01_library.png",
"integration_02_reading_page1.png",
"integration_03_reading_page2.png",
"integration_04_settings_overlay.png",
"integration_05_settings_font_increased.png",
"integration_06_back_to_library.png",
"integration_07_second_book.png",
"integration_08_resumed_position.png"
]
for filename in demo_files:
if os.path.exists(filename):
print(f"{filename}")
print("\nThis demonstrates the complete workflow:")
print(" 1. Library view with book selection")
print(" 2. Opening and reading a book")
print(" 3. Page navigation")
print(" 4. Settings overlay with adjustments")
print(" 5. Back to library transition")
print(" 6. Selecting another book")
print(" 7. Auto-resume functionality")
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python library_reading_integration.py path/to/library/directory")
print("\nThis demo requires a directory containing EPUB files.")
print("\nExample:")
print(" mkdir my_library")
print(" cp tests/data/test.epub my_library/")
print(" cp tests/data/test2.epub my_library/")
print(" python library_reading_integration.py my_library/")
sys.exit(1)
library_path = sys.argv[1]
if not os.path.exists(library_path):
print(f"Error: Directory not found: {library_path}")
sys.exit(1)
if not os.path.isdir(library_path):
print(f"Error: Not a directory: {library_path}")
sys.exit(1)
try:
simulate_mode_transition_workflow(library_path)
except Exception as e:
print(f"\nError during integration test: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
+196
View File
@@ -0,0 +1,196 @@
"""
Example demonstrating the unified navigation overlay feature.
This example shows how to:
1. Open the navigation overlay with Contents and Bookmarks tabs
2. Switch between tabs
3. Navigate to chapters and bookmarks
4. Handle user interactions with the overlay
The navigation overlay replaces the separate TOC and Bookmarks overlays
with a single, unified interface that provides both features in a tabbed view.
"""
from pathlib import Path
from dreader.application import EbookReader
from dreader.state import OverlayState
def main():
# Create reader instance
reader = EbookReader(page_size=(800, 1200), margin=20)
# Load a sample book (adjust path as needed)
book_path = Path(__file__).parent / "books" / "hamlet.epub"
if not book_path.exists():
print(f"Book not found at {book_path}")
print("Creating a simple HTML book for demo...")
# Create a simple multi-chapter book
html = """
<html>
<head><title>Demo Book</title></head>
<body>
<h1>Chapter 1: Introduction</h1>
<p>This is the first chapter with some introductory content.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
<h1>Chapter 2: Main Content</h1>
<p>This is the second chapter with main content.</p>
<p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<h1>Chapter 3: Conclusion</h1>
<p>This is the final chapter with concluding remarks.</p>
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
</body>
</html>
"""
reader.load_html(
html_string=html,
title="Demo Book",
author="Example Author",
document_id="demo_navigation"
)
else:
print(f"Loading book: {book_path}")
reader.load_epub(str(book_path))
print("\n=== Navigation Overlay Demo ===\n")
# Display current page
position_info = reader.get_position_info()
print(f"Current position: {position_info}")
print(f"Reading progress: {reader.get_reading_progress():.1%}")
# Get chapters
chapters = reader.get_chapters()
print(f"\nAvailable chapters: {len(chapters)}")
for i, (title, idx) in enumerate(chapters[:5]): # Show first 5
print(f" {i+1}. {title}")
# Save some bookmarks for demonstration
print("\n--- Saving bookmarks ---")
reader.save_position("Start of Book")
print("Saved bookmark: 'Start of Book'")
reader.next_page()
reader.next_page()
reader.save_position("Chapter 1 Progress")
print("Saved bookmark: 'Chapter 1 Progress'")
# List saved bookmarks
bookmarks = reader.list_saved_positions()
print(f"\nTotal bookmarks: {len(bookmarks)}")
for name in bookmarks:
print(f" - {name}")
# === Demo 1: Open navigation overlay with Contents tab ===
print("\n\n--- Demo 1: Opening Navigation Overlay (Contents Tab) ---")
image = reader.open_navigation_overlay(active_tab="contents")
if image:
print(f"✓ Navigation overlay opened successfully")
print(f" Overlay state: {reader.get_overlay_state()}")
print(f" Is overlay open: {reader.is_overlay_open()}")
print(f" Image size: {image.size}")
# Save the rendered overlay for inspection
output_path = Path("/tmp/navigation_overlay_contents.png")
image.save(output_path)
print(f" Saved to: {output_path}")
# === Demo 2: Switch to Bookmarks tab ===
print("\n\n--- Demo 2: Switching to Bookmarks Tab ---")
image = reader.switch_navigation_tab("bookmarks")
if image:
print(f"✓ Switched to Bookmarks tab")
print(f" Overlay state: {reader.get_overlay_state()}")
# Save the rendered overlay for inspection
output_path = Path("/tmp/navigation_overlay_bookmarks.png")
image.save(output_path)
print(f" Saved to: {output_path}")
# === Demo 3: Switch back to Contents tab ===
print("\n\n--- Demo 3: Switching back to Contents Tab ---")
image = reader.switch_navigation_tab("contents")
if image:
print(f"✓ Switched back to Contents tab")
# Save the rendered overlay for inspection
output_path = Path("/tmp/navigation_overlay_contents_2.png")
image.save(output_path)
print(f" Saved to: {output_path}")
# === Demo 4: Close overlay ===
print("\n\n--- Demo 4: Closing Navigation Overlay ---")
image = reader.close_overlay()
if image:
print(f"✓ Overlay closed successfully")
print(f" Overlay state: {reader.get_overlay_state()}")
print(f" Is overlay open: {reader.is_overlay_open()}")
# === Demo 5: Open with Bookmarks tab directly ===
print("\n\n--- Demo 5: Opening directly to Bookmarks Tab ---")
image = reader.open_navigation_overlay(active_tab="bookmarks")
if image:
print(f"✓ Navigation overlay opened with Bookmarks tab")
# Save the rendered overlay for inspection
output_path = Path("/tmp/navigation_overlay_bookmarks_direct.png")
image.save(output_path)
print(f" Saved to: {output_path}")
# Close overlay
reader.close_overlay()
# === Demo 6: Simulate user interaction flow ===
print("\n\n--- Demo 6: Simulated User Interaction Flow ---")
print("Simulating: User opens overlay, switches tabs, selects bookmark")
# 1. User opens navigation overlay
print("\n 1. User taps navigation button -> Opens overlay with Contents tab")
reader.open_navigation_overlay(active_tab="contents")
print(f" State: {reader.get_overlay_state()}")
# 2. User switches to Bookmarks tab
print("\n 2. User taps 'Bookmarks' tab")
reader.switch_navigation_tab("bookmarks")
print(f" State: {reader.get_overlay_state()}")
# 3. User selects a bookmark
print("\n 3. User taps on bookmark 'Start of Book'")
page = reader.load_position("Start of Book")
if page:
print(f" ✓ Loaded bookmark successfully")
print(f" Position: {reader.get_position_info()}")
# 4. Close overlay
print("\n 4. System closes overlay after selection")
reader.close_overlay()
print(f" State: {reader.get_overlay_state()}")
# === Summary ===
print("\n\n=== Demo Complete ===")
print(f"\nGenerated overlay images in /tmp:")
print(f" - navigation_overlay_contents.png")
print(f" - navigation_overlay_bookmarks.png")
print(f" - navigation_overlay_contents_2.png")
print(f" - navigation_overlay_bookmarks_direct.png")
print("\n✓ Navigation overlay provides unified interface for:")
print(" • Table of Contents (chapter navigation)")
print(" • Bookmarks (saved positions)")
print(" • Tab switching between Contents and Bookmarks")
print(" • Consistent interaction patterns")
# Cleanup
reader.close()
print("\nReader closed.")
if __name__ == "__main__":
main()
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
Example demonstrating persistent rendering settings.
This shows how to:
1. Initialize StateManager to load saved settings
2. Apply saved settings to EbookReader
3. Modify settings during reading
4. Save settings automatically for next session
The settings (font size, line spacing, etc.) will persist between
application sessions, so the user doesn't have to reconfigure each time.
"""
import sys
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader import EbookReader
from dreader.state import StateManager, Settings
def demonstrate_persistent_settings():
"""Show how settings persist across sessions"""
print("=" * 70)
print("Persistent Settings Example")
print("=" * 70)
# 1. Initialize state manager (loads saved state from disk)
state_file = Path.home() / ".config" / "dreader" / "state.json"
state_file.parent.mkdir(parents=True, exist_ok=True)
state_manager = StateManager(state_file=state_file)
state = state_manager.load_state()
print(f"\nLoaded settings from: {state_file}")
print(f" Font scale: {state.settings.font_scale}")
print(f" Line spacing: {state.settings.line_spacing}px")
print(f" Inter-block spacing: {state.settings.inter_block_spacing}px")
print(f" Word spacing: {state.settings.word_spacing}px")
# 2. Create reader with saved settings
reader = EbookReader(
page_size=(800, 1000),
line_spacing=state.settings.line_spacing,
inter_block_spacing=state.settings.inter_block_spacing
)
# Load a book
epub_dir = Path(__file__).parent.parent / 'tests' / 'data' / 'library-epub'
epubs = list(epub_dir.glob('*.epub'))
if not epubs:
print("\nError: No test EPUB files found!")
print(f"Looked in: {epub_dir}")
return
epub_path = epubs[0]
print(f"\nLoading book: {epub_path.name}")
if not reader.load_epub(str(epub_path)):
print("Failed to load book!")
return
print(f"Loaded: {reader.book_title} by {reader.book_author}")
# 3. Apply saved settings to the book
print("\nApplying saved settings to book...")
settings_dict = state.settings.to_dict()
reader.apply_settings(settings_dict)
# Render initial page
print("\nRendering page with saved settings...")
page = reader.get_current_page()
reader.render_to_file("persistent_settings_before.png")
print("✓ Saved: persistent_settings_before.png")
# 4. Simulate user changing settings
print("\n" + "=" * 70)
print("User adjusts settings...")
print("=" * 70)
# Increase font size
print("\n1. Increasing font size...")
reader.increase_font_size()
reader.increase_font_size()
print(f" New font scale: {reader.base_font_scale}")
# Increase line spacing
print("2. Increasing line spacing...")
new_line_spacing = state.settings.line_spacing + 4
reader.set_line_spacing(new_line_spacing)
print(f" New line spacing: {new_line_spacing}px")
# Increase word spacing
print("3. Increasing word spacing...")
new_word_spacing = state.settings.word_spacing + 3
reader.set_word_spacing(new_word_spacing)
print(f" New word spacing: {new_word_spacing}px")
# Render page with new settings
print("\nRendering page with new settings...")
page = reader.get_current_page()
reader.render_to_file("persistent_settings_after.png")
print("✓ Saved: persistent_settings_after.png")
# 5. Save new settings to state
print("\n" + "=" * 70)
print("Saving settings for next session...")
print("=" * 70)
current_settings = reader.get_current_settings()
state_manager.update_settings(current_settings)
print(f"\nSettings to be saved:")
print(f" Font scale: {current_settings['font_scale']}")
print(f" Line spacing: {current_settings['line_spacing']}px")
print(f" Inter-block spacing: {current_settings['inter_block_spacing']}px")
print(f" Word spacing: {current_settings['word_spacing']}px")
# Save state to disk
if state_manager.save_state():
print(f"\n✓ Settings saved to: {state_file}")
print(" These settings will be used the next time you open a book!")
else:
print("\n✗ Failed to save settings")
# 6. Demonstrate that settings are saved
print("\n" + "=" * 70)
print("Verification: Reloading state from disk...")
print("=" * 70)
# Create new state manager to verify persistence
verification_manager = StateManager(state_file=state_file)
verification_state = verification_manager.load_state()
print(f"\nVerified saved settings:")
print(f" Font scale: {verification_state.settings.font_scale}")
print(f" Line spacing: {verification_state.settings.line_spacing}px")
print(f" Inter-block spacing: {verification_state.settings.inter_block_spacing}px")
print(f" Word spacing: {verification_state.settings.word_spacing}px")
if (verification_state.settings.font_scale == current_settings['font_scale'] and
verification_state.settings.line_spacing == current_settings['line_spacing'] and
verification_state.settings.word_spacing == current_settings['word_spacing']):
print("\n✓ Settings successfully persisted!")
else:
print("\n✗ Settings mismatch!")
# Cleanup
reader.close()
print("\n" + "=" * 70)
print("Demo Complete!")
print("=" * 70)
print("\nKey Points:")
print(" • Settings are automatically loaded from ~/.config/dreader/state.json")
print(" • Use reader.apply_settings() to apply saved settings after loading a book")
print(" • Use reader.get_current_settings() to get current settings")
print(" • Use state_manager.update_settings() to save new settings")
print(" • Settings persist across application restarts")
print("\nGenerated files:")
print(" • persistent_settings_before.png - Page with original settings")
print(" • persistent_settings_after.png - Page with modified settings")
if __name__ == '__main__':
demonstrate_persistent_settings()
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""
Run DReader on real e-ink hardware.
This example demonstrates running the DReader application on real e-ink hardware
using the dreader-hal library for hardware abstraction.
Requirements:
- Raspberry Pi (or compatible SBC)
- IT8951 e-ink display
- FT5xx6 capacitive touch sensor
- Optional: BMA400 accelerometer, PCF8523 RTC, INA219 power monitor
Hardware Setup:
See external/dreader-hal/README.md for wiring instructions
Usage:
# On Raspberry Pi with full hardware
python run_on_hardware.py /path/to/library
# For testing without hardware (virtual display mode)
python run_on_hardware.py /path/to/library --virtual
# Disable optional components
python run_on_hardware.py /path/to/library --no-orientation --no-rtc --no-power
"""
import sys
import asyncio
import argparse
import logging
from pathlib import Path
# Add parent directory to path to import dreader
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.main import DReaderApplication, AppConfig
async def main(args):
"""
Main application entry point.
Args:
args: Command line arguments
"""
# Set up logging
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger.info("Starting DReader on hardware")
logger.info(f"Library path: {args.library_path}")
logger.info(f"Display size: {args.width}x{args.height}")
logger.info(f"VCOM: {args.vcom}V")
logger.info(f"Virtual display: {args.virtual}")
# Create hardware HAL
logger.info("Initializing hardware HAL...")
hal = HardwareDisplayHAL(
width=args.width,
height=args.height,
vcom=args.vcom,
virtual_display=args.virtual,
auto_sleep_display=args.auto_sleep,
enable_orientation=args.orientation,
enable_rtc=args.rtc,
enable_power_monitor=args.power,
battery_capacity_mah=args.battery_capacity,
)
# Create application config
config = AppConfig(
display_hal=hal,
library_path=args.library_path,
page_size=(args.width, args.height),
auto_save_interval=60,
force_library_mode=args.force_library,
log_level=logging.DEBUG if args.verbose else logging.INFO,
)
# Create application
app = DReaderApplication(config)
try:
# Initialize hardware
logger.info("Initializing hardware...")
await hal.initialize()
# Start application
logger.info("Starting application...")
await app.start()
# Show battery level if available
if args.power and not args.virtual:
try:
battery = await hal.get_battery_level()
logger.info(f"Battery level: {battery:.1f}%")
if await hal.is_low_battery():
logger.warning("⚠️ Low battery!")
except Exception as e:
logger.warning(f"Could not read battery: {e}")
# Main event loop
logger.info("Entering main event loop (Ctrl+C to exit)")
logger.info("")
logger.info("Touch gestures:")
logger.info(" - Swipe left: Next page")
logger.info(" - Swipe right: Previous page")
logger.info(" - Swipe up (from bottom): Open navigation/TOC")
logger.info(" - Swipe down (from top): Open settings")
logger.info(" - Tap: Select book/word/link")
logger.info("")
while app.is_running():
# Get touch event (non-blocking)
event = await hal.get_touch_event()
if event:
logger.debug(f"Touch event: {event.gesture.value} at ({event.x}, {event.y})")
# Handle touch event
await app.handle_touch(event)
# Check battery periodically (every ~100 events)
if args.power and not args.virtual and args.show_battery:
if hasattr(app, '_event_count'):
app._event_count += 1
else:
app._event_count = 1
if app._event_count % 100 == 0:
battery = await hal.get_battery_level()
logger.info(f"Battery: {battery:.1f}%")
# Small delay to prevent CPU spinning
await asyncio.sleep(0.01)
except KeyboardInterrupt:
logger.info("Received interrupt signal, shutting down...")
except Exception as e:
logger.error(f"Error in main loop: {e}", exc_info=True)
finally:
# Shutdown
logger.info("Shutting down application...")
await app.shutdown()
logger.info("Cleaning up hardware...")
await hal.cleanup()
logger.info("DReader stopped")
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Run DReader on e-ink hardware",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run on real hardware
%(prog)s /home/pi/Books
# Test with virtual display (no hardware required)
%(prog)s /home/pi/Books --virtual
# Custom display size and VCOM
%(prog)s /home/pi/Books --width 1200 --height 1600 --vcom -2.3
# Disable optional sensors
%(prog)s /home/pi/Books --no-orientation --no-rtc --no-power
"""
)
# Required arguments
parser.add_argument(
'library_path',
type=str,
help='Path to directory containing EPUB files'
)
# Display arguments
parser.add_argument(
'--width',
type=int,
default=1872,
help='Display width in pixels (default: 1872)'
)
parser.add_argument(
'--height',
type=int,
default=1404,
help='Display height in pixels (default: 1404)'
)
parser.add_argument(
'--vcom',
type=float,
default=-2.0,
help='E-ink VCOM voltage - CHECK YOUR DISPLAY LABEL! (default: -2.0)'
)
# Virtual display mode
parser.add_argument(
'--virtual',
action='store_true',
help='Use virtual display mode for testing without hardware'
)
# Display features
parser.add_argument(
'--no-auto-sleep',
dest='auto_sleep',
action='store_false',
help='Disable automatic display sleep after updates'
)
# Optional hardware components
parser.add_argument(
'--no-orientation',
dest='orientation',
action='store_false',
help='Disable orientation sensor (BMA400)'
)
parser.add_argument(
'--no-rtc',
dest='rtc',
action='store_false',
help='Disable RTC (PCF8523)'
)
parser.add_argument(
'--no-power',
dest='power',
action='store_false',
help='Disable power monitor (INA219)'
)
# Battery monitoring
parser.add_argument(
'--battery-capacity',
type=float,
default=3000,
help='Battery capacity in mAh (default: 3000)'
)
parser.add_argument(
'--show-battery',
action='store_true',
help='Periodically log battery level'
)
# Application behavior
parser.add_argument(
'--force-library',
action='store_true',
help='Always start in library mode (ignore saved state)'
)
# Debugging
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose debug logging'
)
args = parser.parse_args()
# Validate library path
library_path = Path(args.library_path).expanduser()
if not library_path.exists():
parser.error(f"Library path does not exist: {library_path}")
if not library_path.is_dir():
parser.error(f"Library path is not a directory: {library_path}")
args.library_path = str(library_path)
return args
if __name__ == '__main__':
args = parse_args()
# Run async main
try:
asyncio.run(main(args))
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""
Run DReader on hardware using hardware_config.json configuration.
This script loads all hardware configuration from hardware_config.json,
including display settings, GPIO buttons, and optional components.
Usage:
# Use default config file (hardware_config.json)
python run_on_hardware_config.py
# Use custom config file
python run_on_hardware_config.py --config my_config.json
# Override config settings
python run_on_hardware_config.py --library ~/MyBooks --verbose
"""
import sys
import asyncio
import argparse
import logging
import json
from pathlib import Path
# Add parent directory to path to import dreader
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.main import DReaderApplication, AppConfig
from dreader.gpio_buttons import load_button_config_from_dict
def load_config(config_path: str) -> dict:
"""Load hardware configuration from JSON file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(
f"Configuration file not found: {config_path}\n"
f"Run 'sudo python3 setup_rpi.py' to create it."
)
with open(config_file, 'r') as f:
config = json.load(f)
return config
async def main(args):
"""Main application entry point."""
# Load configuration
logger = logging.getLogger(__name__)
logger.info(f"Loading configuration from {args.config}")
try:
config = load_config(args.config)
except Exception as e:
print(f"Error loading configuration: {e}")
sys.exit(1)
# Apply command-line overrides
if args.library:
config['application']['library_path'] = args.library
if args.verbose:
config['application']['log_level'] = 'DEBUG'
# Set up logging
log_level = getattr(logging, config['application']['log_level'].upper())
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger.info("="*70)
logger.info("DReader Hardware Mode")
logger.info("="*70)
# Display configuration summary
display_cfg = config['display']
logger.info(f"Display: {display_cfg['width']}x{display_cfg['height']}, VCOM={display_cfg['vcom']}V")
gpio_cfg = config.get('gpio_buttons', {})
if gpio_cfg.get('enabled', False):
logger.info(f"GPIO Buttons: {len(gpio_cfg.get('buttons', []))} configured")
accel_cfg = config.get('accelerometer', {})
if accel_cfg.get('enabled', False):
logger.info("Accelerometer: Enabled")
rtc_cfg = config.get('rtc', {})
if rtc_cfg.get('enabled', False):
logger.info("RTC: Enabled")
power_cfg = config.get('power_monitor', {})
if power_cfg.get('enabled', False):
logger.info("Power Monitor: Enabled")
# Create hardware HAL
logger.info("\nInitializing hardware HAL...")
hal = HardwareDisplayHAL(
width=display_cfg['width'],
height=display_cfg['height'],
vcom=display_cfg['vcom'],
spi_hz=display_cfg.get('spi_hz', 24_000_000),
virtual_display=False,
auto_sleep_display=display_cfg.get('auto_sleep', True),
enable_orientation=accel_cfg.get('enabled', True),
enable_rtc=rtc_cfg.get('enabled', True),
enable_power_monitor=power_cfg.get('enabled', True),
shunt_ohms=power_cfg.get('shunt_ohms', 0.1),
battery_capacity_mah=power_cfg.get('battery_capacity_mah', 3000),
)
# Load accelerometer tilt calibration if enabled
if accel_cfg.get('tilt_enabled', False):
calib_file = accel_cfg.get('calibration_file', 'accelerometer_config.json')
if hal.load_accelerometer_calibration(calib_file):
logger.info(f"Accelerometer tilt detection enabled (calibration from {calib_file})")
else:
logger.warning("Accelerometer tilt detection requested but calibration not loaded")
# Set up GPIO buttons
button_handler = None
if gpio_cfg.get('enabled', False):
logger.info("Setting up GPIO buttons...")
button_handler = load_button_config_from_dict(
config,
screen_width=display_cfg['width'],
screen_height=display_cfg['height']
)
if button_handler:
await button_handler.initialize()
logger.info(f"GPIO buttons initialized: {len(gpio_cfg.get('buttons', []))} buttons")
# Create application config
app_cfg = config['application']
app_config = AppConfig(
display_hal=hal,
library_path=app_cfg['library_path'],
page_size=(display_cfg['width'], display_cfg['height']),
auto_save_interval=app_cfg.get('auto_save_interval', 60),
force_library_mode=app_cfg.get('force_library_mode', False),
log_level=log_level,
)
# Create application
app = DReaderApplication(app_config)
try:
# Initialize hardware
logger.info("Initializing hardware...")
await hal.initialize()
# Start application
logger.info("Starting application...")
await app.start()
# Show battery level if available
if power_cfg.get('enabled', False):
try:
battery = await hal.get_battery_level()
logger.info(f"Battery level: {battery:.1f}%")
if await hal.is_low_battery(power_cfg.get('low_battery_threshold', 20.0)):
logger.warning("⚠️ Low battery!")
except Exception as e:
logger.warning(f"Could not read battery: {e}")
# Main event loop
logger.info("\nApplication ready!")
logger.info("="*70)
event_count = 0
show_battery_interval = power_cfg.get('show_battery_interval', 100)
while app.is_running():
# Check for touch events
touch_event = await hal.get_touch_event()
if touch_event:
logger.debug(f"Touch: {touch_event.gesture.value} at ({touch_event.x}, {touch_event.y})")
await app.handle_touch(touch_event)
event_count += 1
# Check for button events
if button_handler:
button_event = await button_handler.get_button_event()
if button_event:
logger.info(f"Button: {button_event.gesture.value}")
await app.handle_touch(button_event)
event_count += 1
# Check for tilt gestures if enabled
if accel_cfg.get('tilt_enabled', False):
tilt_event = await hal.get_tilt_gesture()
if tilt_event:
logger.info(f"Tilt: {tilt_event.gesture.value}")
await app.handle_touch(tilt_event)
event_count += 1
# Show battery periodically
if power_cfg.get('enabled', False) and event_count % show_battery_interval == 0 and event_count > 0:
try:
battery = await hal.get_battery_level()
logger.info(f"Battery: {battery:.1f}%")
except:
pass
# Small delay to prevent CPU spinning
await asyncio.sleep(0.01)
except KeyboardInterrupt:
logger.info("\nReceived interrupt signal, shutting down...")
except Exception as e:
logger.error(f"Error in main loop: {e}", exc_info=True)
finally:
# Shutdown
logger.info("Shutting down application...")
await app.shutdown()
logger.info("Cleaning up GPIO buttons...")
if button_handler:
await button_handler.cleanup()
logger.info("Cleaning up hardware...")
await hal.cleanup()
logger.info("DReader stopped")
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Run DReader using hardware_config.json",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Configuration:
Edit hardware_config.json to configure your hardware settings.
Run 'sudo python3 setup_rpi.py' to create/update the config file.
Examples:
# Use default config
%(prog)s
# Use custom config file
%(prog)s --config my_hardware.json
# Override library path
%(prog)s --library ~/MyBooks
# Enable verbose logging
%(prog)s --verbose
"""
)
parser.add_argument(
'--config',
type=str,
default='hardware_config.json',
help='Path to hardware configuration file (default: hardware_config.json)'
)
parser.add_argument(
'--library',
type=str,
help='Override library path from config'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose debug logging'
)
args = parser.parse_args()
return args
if __name__ == '__main__':
args = parse_args()
# Run async main
try:
asyncio.run(main(args))
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""
Simple example showing the most common EbookReader usage.
This script loads an EPUB and allows you to navigate through it,
saving each page as an image.
Usage:
python simple_ereader_example.py book.epub
"""
import sys
from pathlib import Path
# Add parent directory to path to import pyWebLayout
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.application import EbookReader
def main():
if len(sys.argv) < 2:
print("Usage: python simple_ereader_example.py book.epub")
sys.exit(1)
epub_path = sys.argv[1]
# Create reader and load EPUB
print(f"Loading: {epub_path}")
reader = EbookReader(page_size=(800, 1000))
if not reader.load_epub(epub_path):
print("Failed to load EPUB")
sys.exit(1)
# Get book information
info = reader.get_book_info()
print(f"\nBook: {info['title']}")
print(f"Author: {info['author']}")
print(f"Total blocks: {info['total_blocks']}")
# Get chapters
chapters = reader.get_chapters()
print(f"Chapters: {len(chapters)}")
if chapters:
print("\nChapter list:")
for title, idx in chapters[:10]: # Show first 10
print(f" {idx}: {title}")
if len(chapters) > 10:
print(f" ... and {len(chapters) - 10} more")
# Navigate through first 10 pages
print("\nRendering first 10 pages...")
for i in range(10):
page = reader.get_current_page()
if page:
filename = f"page_{i+1:03d}.png"
reader.render_to_file(filename)
# Show progress
progress = reader.get_reading_progress()
chapter_info = reader.get_current_chapter_info()
chapter_name = chapter_info['title'] if chapter_info else "N/A"
print(f" Page {i+1}: {filename} (Progress: {progress*100:.1f}%, Chapter: {chapter_name})")
# Move to next page
if not reader.next_page():
print(" Reached end of book")
break
# Save current position
reader.save_position("stopped_at_page_10")
print("\nSaved position as 'stopped_at_page_10'")
# Example: Jump to a chapter (if available)
if len(chapters) >= 2:
print(f"\nJumping to chapter: {chapters[1][0]}")
reader.jump_to_chapter(1)
reader.render_to_file("chapter_2_start.png")
print(" Saved to: chapter_2_start.png")
# Example: Increase font size
print("\nIncreasing font size...")
reader.increase_font_size()
reader.render_to_file("larger_font.png")
print(f" Font size now: {reader.get_font_size():.1f}x")
print(" Saved to: larger_font.png")
# Close reader (saves current position automatically)
reader.close()
print("\nDone! Current position saved automatically.")
if __name__ == "__main__":
main()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Simple Example: Highlight a Word on Tap
This is the minimal example showing how to:
1. Load an ebook
2. Simulate a tap
3. Find the word at that location
4. Highlight it
Perfect for understanding the basic query system.
"""
from PIL import Image, ImageDraw
from dreader.application import EbookReader
def main():
# 1. Create reader and load book
reader = EbookReader(page_size=(800, 1000))
reader.load_epub("tests/data/test.epub")
# 2. Get current page as image
page_img = reader.get_current_page()
# 3. Simulate a tap at pixel coordinates
tap_x, tap_y = 200, 300
result = reader.query_pixel(tap_x, tap_y)
# 4. If we found a word, highlight it
if result and result.text:
print(f"Tapped on word: '{result.text}'")
print(f"Bounds: {result.bounds}")
# Draw yellow highlight
x, y, w, h = result.bounds
overlay = Image.new('RGBA', page_img.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)
draw.rectangle([x, y, x + w, y + h], fill=(255, 255, 0, 100))
# Combine and save
highlighted = Image.alpha_composite(
page_img.convert('RGBA'),
overlay
)
highlighted.save("highlighted_word.png")
print("Saved: highlighted_word.png")
else:
print("No word found at that location")
reader.close()
if __name__ == "__main__":
main()
+96
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()
+360
View File
@@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""
Example: Word Selection and Highlighting
This example demonstrates how to:
1. Query a pixel location to find a word
2. Select a range of words between two points
3. Highlight selected words by drawing overlays
4. Handle tap gestures to select words
This is useful for:
- Word definition lookup
- Text highlighting/annotation
- Copy/paste functionality
- Interactive reading features
"""
from PIL import Image, ImageDraw
import numpy as np
from dreader import EbookReader, TouchEvent, GestureType
from pyWebLayout.core.query import QueryResult
def draw_highlight(image: Image.Image, bounds: tuple, color: tuple = (255, 255, 0, 100)):
"""
Draw a highlight overlay on an image at the given bounds.
Args:
image: PIL Image to draw on
bounds: (x, y, width, height) tuple
color: RGBA color tuple (with alpha for transparency)
"""
# Create a semi-transparent overlay
overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)
x, y, w, h = bounds
# Draw rectangle with rounded corners for nicer appearance
draw.rectangle([x, y, x + w, y + h], fill=color)
# Composite the overlay onto the original image
image = Image.alpha_composite(image.convert('RGBA'), overlay)
return image
def example_1_single_word_selection():
"""Example 1: Select and highlight a single word by tapping"""
print("=" * 60)
print("Example 1: Single Word Selection")
print("=" * 60)
# Create reader and load a book
reader = EbookReader(page_size=(800, 1000))
success = reader.load_epub("tests/data/test.epub")
if not success:
print("Failed to load EPUB")
return
print(f"Loaded: {reader.book_title} by {reader.book_author}")
# Get current page as image
page_img = reader.get_current_page()
if not page_img:
print("No page rendered")
return
# Simulate a tap at coordinates (200, 300)
tap_x, tap_y = 200, 300
print(f"\nSimulating tap at ({tap_x}, {tap_y})")
# Query what's at that location
result = reader.query_pixel(tap_x, tap_y)
if result and result.text:
print(f"Found word: '{result.text}'")
print(f"Type: {result.object_type}")
print(f"Bounds: {result.bounds}")
print(f"Is interactive: {result.is_interactive}")
# Highlight the word
highlighted_img = draw_highlight(page_img, result.bounds, color=(255, 255, 0, 80))
highlighted_img.save("output_single_word_highlight.png")
print(f"\nSaved highlighted image to: output_single_word_highlight.png")
else:
print("No word found at that location")
reader.close()
def example_2_range_selection():
"""Example 2: Select and highlight a range of words (text selection)"""
print("\n" + "=" * 60)
print("Example 2: Range Selection (Multi-word)")
print("=" * 60)
# Create reader and load a book
reader = EbookReader(page_size=(800, 1000))
success = reader.load_epub("tests/data/test.epub")
if not success:
print("Failed to load EPUB")
return
# Get current page
page_img = reader.get_current_page()
if not page_img:
return
# Simulate dragging from (100, 200) to (400, 250)
start_x, start_y = 100, 200
end_x, end_y = 400, 250
print(f"Simulating selection from ({start_x}, {start_y}) to ({end_x}, {end_y})")
# Create drag gesture events
drag_start = TouchEvent(GestureType.DRAG_START, start_x, start_y)
drag_move = TouchEvent(GestureType.DRAG_MOVE, end_x, end_y)
drag_end = TouchEvent(GestureType.DRAG_END, end_x, end_y)
# Handle the gesture (business logic)
reader.handle_touch(drag_start)
reader.handle_touch(drag_move)
response = reader.handle_touch(drag_end)
if response.action == "selection_complete":
selected_text = response.data.get('text', '')
bounds_list = response.data.get('bounds', [])
word_count = response.data.get('word_count', 0)
print(f"\nSelected {word_count} words:")
print(f"Text: \"{selected_text}\"")
# Highlight all selected words
highlighted_img = page_img
for bounds in bounds_list:
highlighted_img = draw_highlight(
highlighted_img,
bounds,
color=(100, 200, 255, 80) # Light blue highlight
)
highlighted_img.save("output_range_highlight.png")
print(f"\nSaved highlighted image to: output_range_highlight.png")
else:
print(f"Selection action: {response.action}")
reader.close()
def example_3_interactive_word_lookup():
"""Example 3: Interactive word lookup with gesture handling"""
print("\n" + "=" * 60)
print("Example 3: Interactive Word Lookup (with Gestures)")
print("=" * 60)
# Create reader
reader = EbookReader(page_size=(800, 1000))
success = reader.load_epub("tests/data/test.epub")
if not success:
print("Failed to load EPUB")
return
# Get page
page_img = reader.get_current_page()
if not page_img:
return
# Define some simulated touch events
test_gestures = [
("Tap at (250, 300)", TouchEvent(GestureType.TAP, 250, 300)),
("Long press at (250, 300)", TouchEvent(GestureType.LONG_PRESS, 250, 300)),
("Swipe left", TouchEvent(GestureType.SWIPE_LEFT, 600, 500)),
]
for description, event in test_gestures:
print(f"\n{description}:")
response = reader.handle_touch(event)
print(f" Action: {response.action}")
if response.action == "word_selected":
word = response.data.get('word', '')
bounds = response.data.get('bounds', (0, 0, 0, 0))
print(f" Selected word: '{word}'")
print(f" Bounds: {bounds}")
# Highlight the word
highlighted_img = draw_highlight(page_img, bounds, color=(255, 200, 0, 100))
filename = f"output_word_lookup_{word}.png"
highlighted_img.save(filename)
print(f" Saved: {filename}")
elif response.action == "define":
word = response.data.get('word', '')
print(f" Show definition for: '{word}'")
# In real app, you'd call a dictionary API here
elif response.action == "page_turn":
direction = response.data.get('direction', '')
progress = response.data.get('progress', 0)
print(f" Page turn {direction}, progress: {progress:.1%}")
reader.close()
def example_4_multi_word_annotation():
"""Example 4: Annotate multiple words with different colors"""
print("\n" + "=" * 60)
print("Example 4: Multi-word Annotation")
print("=" * 60)
# Create reader
reader = EbookReader(page_size=(800, 1000))
success = reader.load_epub("tests/data/test.epub")
if not success:
print("Failed to load EPUB")
return
# Get page
page_img = reader.get_current_page()
if not page_img:
return
# Simulate multiple taps at different locations
tap_locations = [
(150, 200, "Important word", (255, 100, 100, 80)), # Red
(300, 200, "Key concept", (100, 255, 100, 80)), # Green
(450, 200, "Notable term", (100, 100, 255, 80)), # Blue
]
annotated_img = page_img
annotations = []
for x, y, label, color in tap_locations:
result = reader.query_pixel(x, y)
if result and result.text:
print(f"\nFound word at ({x}, {y}): '{result.text}'")
print(f" Annotation: {label}")
# Highlight with specific color
annotated_img = draw_highlight(annotated_img, result.bounds, color)
annotations.append({
'word': result.text,
'label': label,
'bounds': result.bounds
})
# Save annotated image
annotated_img.save("output_multi_annotation.png")
print(f"\nSaved annotated image with {len(annotations)} highlights")
print("File: output_multi_annotation.png")
# Print annotation summary
print("\nAnnotation Summary:")
for i, ann in enumerate(annotations, 1):
print(f" {i}. '{ann['word']}' - {ann['label']}")
reader.close()
def example_5_link_highlighting():
"""Example 5: Find and highlight all links on a page"""
print("\n" + "=" * 60)
print("Example 5: Find and Highlight All Links")
print("=" * 60)
# Create reader
reader = EbookReader(page_size=(800, 1000))
success = reader.load_epub("tests/data/test.epub")
if not success:
print("Failed to load EPUB")
return
# Get page
page_img = reader.get_current_page()
if not page_img:
return
# Get the page object to scan for links
page = reader.manager.get_current_page()
# Scan through all rendered content to find links
links_found = []
from pyWebLayout.concrete.text import Line
from pyWebLayout.concrete.functional import LinkText
for child in page._children:
if isinstance(child, Line):
for text_obj in child._text_objects:
if isinstance(text_obj, LinkText):
origin = text_obj._origin
size = text_obj.size
bounds = (
int(origin[0]),
int(origin[1]),
int(size[0]),
int(size[1])
)
links_found.append({
'text': text_obj._text,
'target': text_obj._link.location,
'bounds': bounds
})
print(f"Found {len(links_found)} links on page")
# Highlight all links
highlighted_img = page_img
for link in links_found:
print(f"\nLink: '{link['text']}'{link['target']}")
highlighted_img = draw_highlight(
highlighted_img,
link['bounds'],
color=(0, 150, 255, 100) # Blue for links
)
if links_found:
highlighted_img.save("output_links_highlighted.png")
print(f"\nSaved image with {len(links_found)} highlighted links")
print("File: output_links_highlighted.png")
else:
print("\nNo links found on this page")
reader.close()
if __name__ == "__main__":
print("Word Selection and Highlighting Examples")
print("=" * 60)
print()
print("These examples demonstrate the query system for:")
print("- Single word selection")
print("- Range selection (multiple words)")
print("- Interactive gesture handling")
print("- Multi-word annotation")
print("- Link detection and highlighting")
print()
try:
# Run all examples
example_1_single_word_selection()
example_2_range_selection()
example_3_interactive_word_lookup()
example_4_multi_word_annotation()
example_5_link_highlighting()
print("\n" + "=" * 60)
print("All examples completed successfully!")
print("=" * 60)
except Exception as e:
print(f"\nError running examples: {e}")
import traceback
traceback.print_exc()