Update coverage badges [skip ci]
This commit is contained in:
Executable
+363
@@ -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)
|
||||
Executable
+212
@@ -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)
|
||||
Executable
+118
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+296
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+341
@@ -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()
|
||||
Executable
+400
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+171
@@ -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()
|
||||
Executable
+292
@@ -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)
|
||||
Executable
+293
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Executable
+96
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user