migration of application to own repo
All checks were successful
Python CI / test (push) Successful in 38s
All checks were successful
Python CI / test (push) Successful in 38s
This commit is contained in:
parent
2e926ab87a
commit
9862c3124a
31
.coveragerc
Normal file
31
.coveragerc
Normal file
@ -0,0 +1,31 @@
|
||||
[run]
|
||||
source = dreader
|
||||
branch = True
|
||||
omit =
|
||||
*/tests/*
|
||||
*/test_*
|
||||
setup.py
|
||||
*/examples/*
|
||||
*/__main__.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
def __repr__
|
||||
if self.debug:
|
||||
if settings.DEBUG
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
# Exclude docstrings
|
||||
^\s*"""
|
||||
^\s*'''
|
||||
^\s*r"""
|
||||
^\s*r'''
|
||||
|
||||
[xml]
|
||||
output = coverage.xml
|
||||
|
||||
[html]
|
||||
directory = htmlcov
|
||||
176
.gitea/workflows/ci.yml
Normal file
176
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,176 @@
|
||||
name: Python CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master, develop ]
|
||||
paths-ignore:
|
||||
- 'coverage*.svg'
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches: [ main, master, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
# Install package in development mode
|
||||
pip install -e .
|
||||
# Install test dependencies if they exist
|
||||
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
|
||||
if [ -f requirements/test.txt ]; then pip install -r requirements/test.txt; fi
|
||||
# Install common test packages
|
||||
pip install pytest pytest-cov flake8 coverage-badge interrogate
|
||||
|
||||
- name: Download initial failed badges
|
||||
run: |
|
||||
echo "Downloading initial failed badges..."
|
||||
|
||||
# Create cov_info directory first
|
||||
mkdir -p cov_info
|
||||
|
||||
# Download failed badges as defaults
|
||||
curl -o cov_info/coverage.svg "https://img.shields.io/badge/coverage-failed-red.svg"
|
||||
curl -o cov_info/coverage-docs.svg "https://img.shields.io/badge/docs-failed-red.svg"
|
||||
|
||||
echo "Initial failed badges created:"
|
||||
ls -la cov_info/coverage*.svg
|
||||
|
||||
- name: Run tests with pytest
|
||||
id: pytest
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Run tests with coverage
|
||||
python -m pytest tests/ -v --cov=dreader --cov-report=term-missing --cov-report=json --cov-report=html --cov-report=xml
|
||||
|
||||
- name: Check documentation coverage
|
||||
id: docs
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Generate documentation coverage report
|
||||
interrogate -v --ignore-init-method --ignore-init-module --ignore-magic --ignore-private --ignore-property-decorators --ignore-semiprivate --fail-under=80 dreader/
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# Stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# Exit-zero treats all errors as warnings
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
- name: Create coverage info directory
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p cov_info
|
||||
echo "Created cov_info directory for coverage data"
|
||||
|
||||
- name: Update test coverage badge on success
|
||||
if: steps.pytest.outcome == 'success' && always()
|
||||
run: |
|
||||
echo "Tests passed! Generating successful coverage badge..."
|
||||
|
||||
if [ -f coverage.json ]; then
|
||||
coverage-badge -o cov_info/coverage.svg -f
|
||||
echo "✅ Test coverage badge updated with actual results"
|
||||
else
|
||||
echo "⚠️ No coverage.json found, keeping failed badge"
|
||||
fi
|
||||
|
||||
- name: Update docs coverage badge on success
|
||||
if: steps.docs.outcome == 'success' && always()
|
||||
run: |
|
||||
echo "Docs check passed! Generating successful docs badge..."
|
||||
|
||||
# Remove existing badge first to avoid overwrite error
|
||||
rm -f cov_info/coverage-docs.svg
|
||||
interrogate --generate-badge cov_info/coverage-docs.svg dreader/
|
||||
echo "✅ Docs coverage badge updated with actual results"
|
||||
|
||||
- name: Generate coverage reports
|
||||
if: steps.pytest.outcome == 'success'
|
||||
run: |
|
||||
# Generate coverage summary for README
|
||||
python -c "
|
||||
import json
|
||||
import os
|
||||
# Read coverage data
|
||||
if os.path.exists('coverage.json'):
|
||||
with open('coverage.json', 'r') as f:
|
||||
coverage_data = json.load(f)
|
||||
total_coverage = round(coverage_data['totals']['percent_covered'], 1)
|
||||
# Create coverage summary file in cov_info directory
|
||||
with open('cov_info/coverage-summary.txt', 'w') as f:
|
||||
f.write(f'{total_coverage}%')
|
||||
print(f'Test Coverage: {total_coverage}%')
|
||||
covered_lines = coverage_data['totals']['covered_lines']
|
||||
total_lines = coverage_data['totals']['num_statements']
|
||||
print(f'Lines Covered: {covered_lines}/{total_lines}')
|
||||
else:
|
||||
print('No coverage data found')
|
||||
"
|
||||
|
||||
# Copy other coverage files to cov_info
|
||||
if [ -f coverage.json ]; then cp coverage.json cov_info/; fi
|
||||
if [ -f coverage.xml ]; then cp coverage.xml cov_info/; fi
|
||||
if [ -d htmlcov ]; then cp -r htmlcov cov_info/; fi
|
||||
|
||||
- name: Final badge status
|
||||
if: always()
|
||||
run: |
|
||||
echo "=== FINAL BADGE STATUS ==="
|
||||
echo "Test outcome: ${{ steps.pytest.outcome }}"
|
||||
echo "Docs outcome: ${{ steps.docs.outcome }}"
|
||||
|
||||
if [ -f cov_info/coverage.svg ]; then
|
||||
echo "✅ Test coverage badge: $(ls -lh cov_info/coverage.svg)"
|
||||
else
|
||||
echo "❌ Test coverage badge: MISSING"
|
||||
fi
|
||||
|
||||
if [ -f cov_info/coverage-docs.svg ]; then
|
||||
echo "✅ Docs coverage badge: $(ls -lh cov_info/coverage-docs.svg)"
|
||||
else
|
||||
echo "❌ Docs coverage badge: MISSING"
|
||||
fi
|
||||
|
||||
echo "Coverage info directory contents:"
|
||||
ls -la cov_info/ 2>/dev/null || echo "No cov_info directory found"
|
||||
|
||||
- name: Upload coverage artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
cov_info/
|
||||
|
||||
- name: Commit badges to badges branch
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
git config --local user.email "action@gitea.local"
|
||||
git config --local user.name "Gitea Action"
|
||||
|
||||
# Set the remote URL to use the token
|
||||
git remote set-url origin https://${{ secrets.PUSH_TOKEN }}@gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
|
||||
# Create a new orphan branch for badges (this discards any existing badges branch)
|
||||
git checkout --orphan badges
|
||||
|
||||
# Remove all files except cov_info
|
||||
find . -maxdepth 1 -not -name '.git' -not -name 'cov_info' -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# Add only the coverage info directory
|
||||
git add -f cov_info/
|
||||
|
||||
# Always commit (force overwrite)
|
||||
echo "Force updating badges branch with new coverage data..."
|
||||
git commit -m "Update coverage badges [skip ci]"
|
||||
git push -f origin badges
|
||||
83
.gitignore
vendored
83
.gitignore
vendored
@ -1,52 +1,59 @@
|
||||
# Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
*/__pycache__
|
||||
|
||||
# Virtual environments
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Environment
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
.venv
|
||||
.env/
|
||||
.venv/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
# Tests
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
coverage.json
|
||||
.tox/
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
|
||||
# Ereader data
|
||||
ereader_bookmarks/
|
||||
highlights/
|
||||
# Project specific - images and outputs
|
||||
*.png
|
||||
*.gif
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.svg
|
||||
|
||||
# But allow documentation images if any
|
||||
!docs/images/*.gif
|
||||
!docs/images/*.png
|
||||
!docs/images/*.jpg
|
||||
|
||||
# Output directories
|
||||
output/
|
||||
my_output/
|
||||
test_output/
|
||||
*_output/
|
||||
examples/output/
|
||||
|
||||
# Generated data
|
||||
ereader_bookmarks/
|
||||
highlights/
|
||||
positions/
|
||||
|
||||
# Examples output
|
||||
examples/ereader_bookmarks/
|
||||
@ -56,3 +63,13 @@ examples/*.gif
|
||||
|
||||
# Keep test data
|
||||
!tests/data/**
|
||||
|
||||
# Debug scripts output
|
||||
debug_*.png
|
||||
|
||||
# CI/CD artifacts
|
||||
cov_info/
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
.fish*
|
||||
|
||||
331
README.md
331
README.md
@ -1,87 +1,132 @@
|
||||
# pyWebLayout-ereader
|
||||
# DReader Application
|
||||
|
||||
A complete ebook reader application built with [pyWebLayout](https://github.com/yourusername/pyWebLayout).
|
||||
## Project Status
|
||||
|
||||
This project demonstrates how to build a full-featured ebook reader using pyWebLayout's layout engine. It serves as both a reference implementation and a ready-to-use ereader library.
|
||||
| Badge | Description |
|
||||
|-------|-------------|
|
||||
|  | **Test Coverage** - Percentage of code covered by unit tests |
|
||||
|  | **Documentation Coverage** - Percentage of code with docstrings |
|
||||
|  | **License** - Project licensing information |
|
||||
|
||||
## Features
|
||||
> 📋 **Note**: Badges show results from the commit referenced in the URLs. Red "error" badges indicate build failures for that specific step.
|
||||
|
||||
- 📖 **EPUB Support** - Load and read EPUB files
|
||||
- 📄 **Page Navigation** - Forward/backward navigation with smooth rendering
|
||||
- 🔖 **Bookmarks** - Save and restore reading positions
|
||||
- 🎨 **Text Highlighting** - Highlight words and selections with notes
|
||||
- 🔍 **Text Selection** - Select and query text via touch/click
|
||||
- ⚙️ **Customization** - Font size, line spacing, colors
|
||||
- 📑 **Chapter Navigation** - Jump to chapters via table of contents
|
||||
- 👆 **Gesture Support** - Tap, swipe, pinch, long-press handling
|
||||
## Description
|
||||
|
||||
DReader Application is a complete, production-ready ebook reader built on [pyWebLayout](https://gitea.tourolle.paris/dtourolle/pyWebLayout). It demonstrates how to build a full-featured ebook reader with text highlighting, bookmarks, gesture support, and position persistence.
|
||||
|
||||
This project serves as both a reference implementation and a ready-to-use ereader library for building desktop, web-based, or embedded reading applications.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Core Reading Features
|
||||
- 📖 **EPUB Support** - Load and render EPUB files with full text extraction
|
||||
- 📄 **Page Rendering** - Render pages as PIL Images optimized for any display
|
||||
- ⬅️➡️ **Navigation** - Smooth forward and backward page navigation
|
||||
- 🔖 **Bookmarks** - Save and restore reading positions with persistence
|
||||
- 📑 **Chapter Navigation** - Jump to chapters by title or index via TOC
|
||||
- 📊 **Progress Tracking** - Real-time reading progress percentage
|
||||
|
||||
### Text Interaction
|
||||
- 🎨 **Text Highlighting** - Highlight words and text selections with custom colors
|
||||
- 💡 **Highlight Notes** - Attach notes and annotations to highlights
|
||||
- 🔍 **Text Selection** - Select words or ranges via pixel coordinates
|
||||
- 👆 **Gesture Support** - Handle tap, swipe, pinch, long-press events
|
||||
- 🎯 **Pixel Queries** - Query text content at any screen position
|
||||
|
||||
### Customization & Display
|
||||
- 🔤 **Font Control** - Dynamically adjust font size with live preview
|
||||
- 📏 **Spacing Control** - Customize line and paragraph spacing
|
||||
- 💾 **Position Persistence** - Stable positions across style changes
|
||||
- ⚡ **Smart Reflow** - Automatic text reflow on font/spacing changes
|
||||
- 🎨 **Custom Styling** - Full control over colors, fonts, and layout
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
# Install pyWebLayout first if not already installed
|
||||
cd /path/to/pyWebLayout
|
||||
# Clone the repository
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
|
||||
# Install in development mode
|
||||
pip install -e .
|
||||
|
||||
# Install pyWebLayout-ereader
|
||||
cd /path/to/pyWebLayout-ereader
|
||||
pip install -e .
|
||||
# Or install with dev dependencies
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
### As a Dependency
|
||||
The pyWebLayout dependency will be automatically installed from the git repository.
|
||||
|
||||
```bash
|
||||
pip install pyweblayout-ereader
|
||||
```
|
||||
## DReader in Action
|
||||
|
||||
Here are animated demonstrations of the key features:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<b>Page Navigation</b><br>
|
||||
<img src="docs/images/ereader_page_navigation.gif" width="300" alt="Page Navigation"><br>
|
||||
<em>Forward and backward navigation through pages</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<b>Font Size Adjustment</b><br>
|
||||
<img src="docs/images/ereader_font_size.gif" width="300" alt="Font Size"><br>
|
||||
<em>Dynamic font size scaling with position preservation</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<b>Chapter Navigation</b><br>
|
||||
<img src="docs/images/ereader_chapter_navigation.gif" width="300" alt="Chapter Navigation"><br>
|
||||
<em>Jump directly to chapters by title or index</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<b>Bookmarks & Positions</b><br>
|
||||
<img src="docs/images/ereader_bookmarks.gif" width="300" alt="Bookmarks"><br>
|
||||
<em>Save and restore reading positions anywhere in the book</em>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2">
|
||||
<b>Text Highlighting</b><br>
|
||||
<img src="docs/images/ereader_highlighting.gif" width="300" alt="Highlighting"><br>
|
||||
<em>Highlight words and selections with custom colors and notes</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from pyweblayout_ereader import EbookReader
|
||||
from dreader.application import EbookReader
|
||||
|
||||
# Create reader
|
||||
reader = EbookReader(page_size=(800, 1000))
|
||||
|
||||
# Load an EPUB
|
||||
# Create an ebook reader
|
||||
with EbookReader(page_size=(800, 1000)) as reader:
|
||||
# Load an EPUB file
|
||||
reader.load_epub("mybook.epub")
|
||||
|
||||
# Get current page as image
|
||||
# Get current page as PIL Image
|
||||
page = reader.get_current_page()
|
||||
page.save("current_page.png")
|
||||
page.save("page_001.png")
|
||||
|
||||
# Navigate
|
||||
# Navigate through pages
|
||||
reader.next_page()
|
||||
reader.previous_page()
|
||||
|
||||
# Save position
|
||||
reader.save_position("bookmark1")
|
||||
# Save reading position
|
||||
reader.save_position("chapter_3")
|
||||
|
||||
# Later, restore position
|
||||
reader.load_position("bookmark1")
|
||||
```
|
||||
# Jump to a chapter
|
||||
reader.jump_to_chapter("Chapter 5")
|
||||
|
||||
## Examples
|
||||
# Adjust font size
|
||||
reader.increase_font_size()
|
||||
|
||||
See the `examples/` directory for complete examples:
|
||||
# Highlight text at coordinates
|
||||
highlight_id = reader.highlight_word(x=200, y=300, note="Important!")
|
||||
|
||||
- **simple_ereader_example.py** - Basic ereader usage
|
||||
- **ereader_demo.py** - Full-featured demo with all capabilities
|
||||
- **word_selection_highlighting.py** - Text selection and highlighting
|
||||
- **generate_ereader_gifs.py** - Generate animated demos
|
||||
|
||||
## Architecture
|
||||
|
||||
This project is a **high-level application layer** that combines pyWebLayout components:
|
||||
|
||||
```
|
||||
pyweblayout_ereader.EbookReader
|
||||
↓
|
||||
├── pyWebLayout.layout.EreaderLayoutManager # Layout & pagination
|
||||
├── pyWebLayout.core.HighlightManager # Highlighting system
|
||||
├── pyWebLayout.io.gesture # Touch/gesture handling
|
||||
└── pyWebLayout.io.readers # EPUB parsing
|
||||
# Get progress
|
||||
progress = reader.get_reading_progress()
|
||||
print(f"Progress: {progress*100:.1f}%")
|
||||
```
|
||||
|
||||
## API Overview
|
||||
@ -91,7 +136,7 @@ pyweblayout_ereader.EbookReader
|
||||
```python
|
||||
reader.load_epub("book.epub")
|
||||
reader.is_loaded() # Check if book loaded
|
||||
reader.get_book_info() # Get metadata
|
||||
reader.get_book_info() # Get metadata (title, author, etc.)
|
||||
```
|
||||
|
||||
### Navigation
|
||||
@ -99,21 +144,26 @@ reader.get_book_info() # Get metadata
|
||||
```python
|
||||
reader.next_page()
|
||||
reader.previous_page()
|
||||
reader.jump_to_chapter("Chapter 1")
|
||||
reader.get_reading_progress() # 0.0 to 1.0
|
||||
reader.jump_to_chapter("Chapter 1") # By title
|
||||
reader.jump_to_chapter(0) # By index
|
||||
reader.get_chapters() # List all chapters
|
||||
reader.get_current_chapter_info()
|
||||
reader.get_reading_progress() # Returns 0.0 to 1.0
|
||||
```
|
||||
|
||||
### Styling
|
||||
### Styling & Display
|
||||
|
||||
```python
|
||||
reader.increase_font_size()
|
||||
reader.decrease_font_size()
|
||||
reader.set_font_size(1.5) # 150% scale
|
||||
reader.get_font_size()
|
||||
|
||||
reader.set_line_spacing(8)
|
||||
reader.set_inter_block_spacing(20)
|
||||
```
|
||||
|
||||
### Bookmarks
|
||||
### Bookmarks & Position Management
|
||||
|
||||
```python
|
||||
reader.save_position("my_bookmark")
|
||||
@ -122,23 +172,36 @@ reader.list_saved_positions()
|
||||
reader.delete_position("my_bookmark")
|
||||
```
|
||||
|
||||
### Highlighting
|
||||
### Text Highlighting
|
||||
|
||||
```python
|
||||
# Highlight a word at pixel coordinates
|
||||
highlight_id = reader.highlight_word(x=100, y=200, note="Important!")
|
||||
from pyWebLayout.core.highlight import HighlightColor
|
||||
|
||||
# Highlight a selection
|
||||
# Highlight a word at pixel coordinates
|
||||
highlight_id = reader.highlight_word(
|
||||
x=100,
|
||||
y=200,
|
||||
color=HighlightColor.YELLOW,
|
||||
note="Important concept!"
|
||||
)
|
||||
|
||||
# Highlight a text selection
|
||||
highlight_id = reader.highlight_selection(
|
||||
start=(100, 200),
|
||||
end=(300, 250),
|
||||
color=(255, 255, 0, 128) # Yellow
|
||||
color=(255, 255, 0, 128) # RGBA
|
||||
)
|
||||
|
||||
# Query word at position
|
||||
result = reader.query_pixel(x=200, y=300)
|
||||
if result:
|
||||
print(f"Word: {result.word.text}")
|
||||
|
||||
# Manage highlights
|
||||
reader.list_highlights()
|
||||
highlights = reader.list_highlights()
|
||||
reader.remove_highlight(highlight_id)
|
||||
reader.clear_highlights()
|
||||
reader.get_highlights_for_current_page()
|
||||
```
|
||||
|
||||
### Gesture Handling
|
||||
@ -153,32 +216,73 @@ response = reader.handle_touch(event)
|
||||
# Response contains action type and data
|
||||
if response.action == ActionType.PAGE_TURN:
|
||||
print(f"Page turned: {response.data['direction']}")
|
||||
elif response.action == ActionType.WORD_SELECTED:
|
||||
print(f"Word selected: {response.data['word']}")
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
### File Operations
|
||||
|
||||
- **Desktop Ereader Applications** - Build native ereader apps
|
||||
- **Web-based Readers** - Serve rendered pages via Flask/FastAPI
|
||||
- **E-ink Device Firmware** - Optimized for e-ink displays
|
||||
- **Reading Analytics** - Track reading patterns and highlights
|
||||
- **Educational Tools** - Annotated reading with highlights and notes
|
||||
```python
|
||||
# Save current page to file
|
||||
reader.render_to_file("current_page.png")
|
||||
|
||||
## Relationship to pyWebLayout
|
||||
# Context manager (auto-saves position on close)
|
||||
with EbookReader(page_size=(800, 1000)) as reader:
|
||||
reader.load_epub("book.epub")
|
||||
# ... use reader ...
|
||||
# Position automatically saved on exit
|
||||
```
|
||||
|
||||
**pyWebLayout** is a layout engine library providing low-level primitives for:
|
||||
- Text rendering and layout
|
||||
## Examples
|
||||
|
||||
Check out the `examples/` directory for complete working examples:
|
||||
|
||||
- **[simple_ereader_example.py](examples/simple_ereader_example.py)** - Basic ereader usage with EPUB loading and navigation
|
||||
- **[ereader_demo.py](examples/ereader_demo.py)** - Comprehensive demo showcasing all features
|
||||
- **[word_selection_highlighting.py](examples/word_selection_highlighting.py)** - Text selection and highlighting
|
||||
- **[simple_word_highlight.py](examples/simple_word_highlight.py)** - Minimal highlighting example
|
||||
- **[generate_ereader_gifs.py](examples/generate_ereader_gifs.py)** - Generate animated GIF demonstrations
|
||||
|
||||
## Architecture
|
||||
|
||||
DReader Application is a **high-level application layer** that combines pyWebLayout's low-level components:
|
||||
|
||||
```
|
||||
dreader.application.EbookReader (High-Level API)
|
||||
↓
|
||||
├── pyWebLayout.layout.EreaderLayoutManager # Layout engine & pagination
|
||||
├── pyWebLayout.layout.EreaderLayout # Bidirectional layout
|
||||
├── pyWebLayout.core.HighlightManager # Highlighting system
|
||||
├── pyWebLayout.io.gesture # Touch/gesture handling
|
||||
└── pyWebLayout.io.readers.epub_reader # EPUB parsing
|
||||
```
|
||||
|
||||
### Relationship to pyWebLayout
|
||||
|
||||
**pyWebLayout** is a layout engine library providing low-level primitives:
|
||||
- Text rendering and layout algorithms
|
||||
- Document structure and pagination
|
||||
- Query systems for interactive content
|
||||
- Core rendering infrastructure
|
||||
|
||||
**pyWebLayout-ereader** is an application framework that:
|
||||
**DReader Application** is an application framework that:
|
||||
- Combines pyWebLayout components into a complete reader
|
||||
- Provides user-friendly APIs for common ereader tasks
|
||||
- Manages application state (bookmarks, highlights, etc.)
|
||||
- Manages application state (bookmarks, highlights, positions)
|
||||
- Handles business logic for gestures and interactions
|
||||
|
||||
Think of it like this:
|
||||
- pyWebLayout = React (library)
|
||||
- pyWebLayout-ereader = Next.js (framework)
|
||||
- **pyWebLayout** = React (library)
|
||||
- **DReader Application** = Next.js (framework)
|
||||
|
||||
## Use Cases
|
||||
|
||||
- 📱 **Desktop Ereader Applications** - Build native ereader apps with Python
|
||||
- 🌐 **Web-based Readers** - Serve rendered pages via Flask/FastAPI
|
||||
- 📟 **E-ink Device Firmware** - Optimized rendering for e-ink displays
|
||||
- 📊 **Reading Analytics** - Track reading patterns, highlights, and engagement
|
||||
- 🎓 **Educational Tools** - Create annotated reading experiences with highlights and notes
|
||||
- 🔬 **Research Applications** - Build specialized reading tools for academic work
|
||||
|
||||
## Development
|
||||
|
||||
@ -189,22 +293,83 @@ pip install -e ".[dev]"
|
||||
# Run tests
|
||||
pytest
|
||||
|
||||
# Run tests with coverage
|
||||
pytest --cov=dreader --cov-report=html
|
||||
|
||||
# Format code
|
||||
black pyweblayout_ereader/
|
||||
black dreader/ tests/
|
||||
|
||||
# Type checking
|
||||
mypy pyweblayout_ereader/
|
||||
mypy dreader/
|
||||
|
||||
# Linting
|
||||
flake8 dreader/ tests/
|
||||
```
|
||||
|
||||
### Running Examples
|
||||
|
||||
```bash
|
||||
# Ensure you have an EPUB file for testing
|
||||
cd examples
|
||||
|
||||
# Run simple example
|
||||
python simple_ereader_example.py /path/to/book.epub
|
||||
|
||||
# Run comprehensive demo
|
||||
python ereader_demo.py /path/to/book.epub
|
||||
|
||||
# Generate animated GIFs
|
||||
python generate_ereader_gifs.py /path/to/book.epub
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes comprehensive tests covering:
|
||||
|
||||
- **Application API** - All EbookReader methods and workflows
|
||||
- **System Integration** - Layout manager, bookmarks, and state management
|
||||
- **Highlighting** - Word and selection highlighting with persistence
|
||||
- **Edge Cases** - Error handling, boundary conditions, and recovery
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/test_ereader_application.py
|
||||
|
||||
# Run with verbose output
|
||||
pytest -v
|
||||
|
||||
# Run with coverage report
|
||||
pytest --cov=dreader --cov-report=term-missing
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! This project demonstrates what's possible with pyWebLayout. If you build something cool or find ways to improve the reader, please share!
|
||||
|
||||
### How to Contribute
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes with tests
|
||||
4. Run tests and ensure coverage stays high
|
||||
5. Format code with black
|
||||
6. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file for details
|
||||
MIT License - see [LICENSE](LICENSE) file for details
|
||||
|
||||
## Author
|
||||
|
||||
Duncan Tourolle - duncan@tourolle.paris
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [pyWebLayout](https://github.com/yourusername/pyWebLayout) - The underlying layout engine
|
||||
- Add your projects here!
|
||||
- [pyWebLayout](https://gitea.tourolle.paris/dtourolle/pyWebLayout) - The underlying layout engine library
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Built with [pyWebLayout](https://gitea.tourolle.paris/dtourolle/pyWebLayout) - A powerful Python library for HTML-like layout and rendering.
|
||||
|
||||
BIN
docs/images/ereader_bookmarks.gif
Normal file
BIN
docs/images/ereader_bookmarks.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
BIN
docs/images/ereader_chapter_navigation.gif
Normal file
BIN
docs/images/ereader_chapter_navigation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
docs/images/ereader_font_size.gif
Normal file
BIN
docs/images/ereader_font_size.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 614 KiB |
BIN
docs/images/ereader_highlighting.gif
Normal file
BIN
docs/images/ereader_highlighting.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 529 KiB |
BIN
docs/images/ereader_page_navigation.gif
Normal file
BIN
docs/images/ereader_page_navigation.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
@ -18,7 +18,10 @@ import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from pyweblayout_ereader import EbookReader
|
||||
# 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():
|
||||
|
||||
@ -23,7 +23,10 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from pyweblayout_ereader import EbookReader
|
||||
# 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
|
||||
|
||||
|
||||
@ -12,7 +12,10 @@ Usage:
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from pyweblayout_ereader import EbookReader
|
||||
# Add parent directory to path to import pyWebLayout
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.application import EbookReader
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -12,7 +12,7 @@ Perfect for understanding the basic query system.
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from pyweblayout_ereader import EbookReader
|
||||
from dreader.application import EbookReader
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@ -18,7 +18,7 @@ This is useful for:
|
||||
from PIL import Image, ImageDraw
|
||||
import numpy as np
|
||||
|
||||
from pyweblayout_ereader import EbookReader
|
||||
from dreader.application import EbookReader
|
||||
from pyWebLayout.io.gesture import TouchEvent, GestureType
|
||||
from pyWebLayout.core.query import QueryResult
|
||||
|
||||
|
||||
@ -3,14 +3,14 @@ requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pyweblayout-ereader"
|
||||
name = "dreader-application"
|
||||
version = "0.1.0"
|
||||
description = "A complete ebook reader application built with pyWebLayout"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Your Name", email = "your.email@example.com"}
|
||||
{name = "Duncan Tourolle", email = "duncan@tourolle.paris"}
|
||||
]
|
||||
keywords = ["ebook", "reader", "epub", "ereader", "layout"]
|
||||
classifiers = [
|
||||
@ -27,7 +27,7 @@ classifiers = [
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"pyweblayout>=0.1.0",
|
||||
"pyweblayout @ git+https://gitea.tourolle.paris/dtourolle/pyWebLayout",
|
||||
"Pillow>=9.0.0",
|
||||
]
|
||||
|
||||
@ -41,14 +41,14 @@ dev = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/pyWebLayout-ereader"
|
||||
Documentation = "https://github.com/yourusername/pyWebLayout-ereader#readme"
|
||||
Repository = "https://github.com/yourusername/pyWebLayout-ereader"
|
||||
Issues = "https://github.com/yourusername/pyWebLayout-ereader/issues"
|
||||
Homepage = "https://gitea.tourolle.paris/dtourolle/dreader-application"
|
||||
Documentation = "https://gitea.tourolle.paris/dtourolle/dreader-application#readme"
|
||||
Repository = "https://gitea.tourolle.paris/dtourolle/dreader-application"
|
||||
Issues = "https://gitea.tourolle.paris/dtourolle/dreader-application/issues"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["pyweblayout_ereader*"]
|
||||
include = ["dreader*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
832
tests/test_ereader_application.py
Normal file
832
tests/test_ereader_application.py
Normal file
@ -0,0 +1,832 @@
|
||||
"""
|
||||
Comprehensive tests for the EbookReader application interface.
|
||||
|
||||
Tests cover:
|
||||
- EPUB loading and initialization
|
||||
- Navigation (forward, backward, boundaries)
|
||||
- Font scaling and styling
|
||||
- Chapter navigation
|
||||
- Position management (bookmarks)
|
||||
- Information retrieval
|
||||
- File operations
|
||||
- Error handling
|
||||
- Context manager
|
||||
- Integration scenarios
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
from dreader.application import EbookReader, create_ebook_reader
|
||||
|
||||
|
||||
class TestEbookReaderInitialization(unittest.TestCase):
|
||||
"""Test EbookReader creation and EPUB loading"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_create_reader_with_defaults(self):
|
||||
"""Test creating reader with default settings"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
self.assertEqual(reader.page_size, (800, 1000))
|
||||
self.assertEqual(reader.base_font_scale, 1.0)
|
||||
self.assertIsNone(reader.manager)
|
||||
self.assertFalse(reader.is_loaded())
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_create_reader_with_custom_settings(self):
|
||||
"""Test creating reader with custom settings"""
|
||||
reader = EbookReader(
|
||||
page_size=(600, 800),
|
||||
margin=50,
|
||||
background_color=(240, 240, 240),
|
||||
line_spacing=10,
|
||||
inter_block_spacing=20,
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=3
|
||||
)
|
||||
|
||||
self.assertEqual(reader.page_size, (600, 800))
|
||||
self.assertEqual(reader.page_style.line_spacing, 10)
|
||||
self.assertEqual(reader.page_style.inter_block_spacing, 20)
|
||||
self.assertEqual(reader.buffer_size, 3)
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_load_valid_epub(self):
|
||||
"""Test loading a valid EPUB file"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
success = reader.load_epub(self.epub_path)
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(reader.is_loaded())
|
||||
self.assertIsNotNone(reader.manager)
|
||||
self.assertIsNotNone(reader.blocks)
|
||||
self.assertIsNotNone(reader.document_id)
|
||||
self.assertIsNotNone(reader.book_title)
|
||||
self.assertIsNotNone(reader.book_author)
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_load_nonexistent_epub(self):
|
||||
"""Test loading a non-existent EPUB file"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
success = reader.load_epub("nonexistent.epub")
|
||||
|
||||
self.assertFalse(success)
|
||||
self.assertFalse(reader.is_loaded())
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_load_invalid_epub(self):
|
||||
"""Test loading an invalid file as EPUB"""
|
||||
# Create a temporary invalid file
|
||||
invalid_path = os.path.join(self.temp_dir, "invalid.epub")
|
||||
with open(invalid_path, 'w') as f:
|
||||
f.write("This is not a valid EPUB file")
|
||||
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
success = reader.load_epub(invalid_path)
|
||||
|
||||
self.assertFalse(success)
|
||||
self.assertFalse(reader.is_loaded())
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_convenience_function(self):
|
||||
"""Test create_ebook_reader convenience function"""
|
||||
reader = create_ebook_reader(
|
||||
page_size=(700, 900),
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
self.assertIsInstance(reader, EbookReader)
|
||||
self.assertEqual(reader.page_size, (700, 900))
|
||||
|
||||
reader.close()
|
||||
|
||||
|
||||
class TestEbookReaderFontScaling(unittest.TestCase):
|
||||
"""Test font size control"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0 # Disable buffering for tests
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_set_font_size(self):
|
||||
"""Test setting font size with arbitrary scale"""
|
||||
page = self.reader.set_font_size(1.5)
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertEqual(self.reader.get_font_size(), 1.5)
|
||||
|
||||
def test_increase_font_size(self):
|
||||
"""Test increasing font size by one step"""
|
||||
initial_size = self.reader.get_font_size()
|
||||
|
||||
page = self.reader.increase_font_size()
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertEqual(self.reader.get_font_size(), initial_size + 0.1)
|
||||
|
||||
def test_decrease_font_size(self):
|
||||
"""Test decreasing font size by one step"""
|
||||
self.reader.set_font_size(1.5)
|
||||
|
||||
page = self.reader.decrease_font_size()
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertAlmostEqual(self.reader.get_font_size(), 1.4, places=5)
|
||||
|
||||
def test_font_size_bounds_clamping(self):
|
||||
"""Test that font size is clamped between 0.5x and 3.0x"""
|
||||
# Test upper bound
|
||||
self.reader.set_font_size(5.0)
|
||||
self.assertEqual(self.reader.get_font_size(), 3.0)
|
||||
|
||||
# Test lower bound
|
||||
self.reader.set_font_size(0.1)
|
||||
self.assertEqual(self.reader.get_font_size(), 0.5)
|
||||
|
||||
def test_get_font_size(self):
|
||||
"""Test getting current font size"""
|
||||
self.assertEqual(self.reader.get_font_size(), 1.0)
|
||||
|
||||
self.reader.set_font_size(2.0)
|
||||
self.assertEqual(self.reader.get_font_size(), 2.0)
|
||||
|
||||
def test_font_scale_with_navigation(self):
|
||||
"""Test that font scale persists across page navigation"""
|
||||
self.reader.set_font_size(1.5)
|
||||
initial_font_size = self.reader.get_font_size()
|
||||
|
||||
# Navigate forward
|
||||
self.reader.next_page()
|
||||
|
||||
# Font size should be preserved
|
||||
self.assertEqual(self.reader.get_font_size(), initial_font_size)
|
||||
|
||||
|
||||
class TestEbookReaderSpacing(unittest.TestCase):
|
||||
"""Test line and block spacing"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_set_line_spacing(self):
|
||||
"""Test setting line spacing"""
|
||||
page = self.reader.set_line_spacing(10)
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertEqual(self.reader.page_style.line_spacing, 10)
|
||||
|
||||
def test_set_inter_block_spacing(self):
|
||||
"""Test setting inter-block spacing"""
|
||||
page = self.reader.set_inter_block_spacing(25)
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertEqual(self.reader.page_style.inter_block_spacing, 25)
|
||||
|
||||
def test_spacing_with_navigation(self):
|
||||
"""Test that spacing changes affect rendering after navigation"""
|
||||
self.reader.set_line_spacing(15)
|
||||
|
||||
page = self.reader.next_page()
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
self.assertEqual(self.reader.page_style.line_spacing, 15)
|
||||
|
||||
def test_spacing_position_preservation(self):
|
||||
"""Test that changing spacing preserves reading position"""
|
||||
# Navigate to a specific position
|
||||
for _ in range(3):
|
||||
self.reader.next_page()
|
||||
|
||||
position_before = self.reader.manager.current_position.copy()
|
||||
|
||||
# Change spacing
|
||||
self.reader.set_line_spacing(12)
|
||||
|
||||
position_after = self.reader.manager.current_position
|
||||
|
||||
# Position should be preserved
|
||||
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
|
||||
self.assertEqual(position_before.block_index, position_after.block_index)
|
||||
|
||||
|
||||
class TestEbookReaderChapterNavigation(unittest.TestCase):
|
||||
"""Test chapter navigation features"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_get_chapters(self):
|
||||
"""Test getting list of chapters"""
|
||||
chapters = self.reader.get_chapters()
|
||||
|
||||
self.assertIsInstance(chapters, list)
|
||||
if len(chapters) > 0:
|
||||
# Each chapter should be a tuple (title, index)
|
||||
self.assertIsInstance(chapters[0], tuple)
|
||||
self.assertEqual(len(chapters[0]), 2)
|
||||
|
||||
def test_get_chapter_positions(self):
|
||||
"""Test getting chapter positions"""
|
||||
positions = self.reader.get_chapter_positions()
|
||||
|
||||
self.assertIsInstance(positions, list)
|
||||
if len(positions) > 0:
|
||||
# Each item should be (title, RenderingPosition)
|
||||
self.assertIsInstance(positions[0], tuple)
|
||||
self.assertEqual(len(positions[0]), 2)
|
||||
|
||||
def test_jump_to_chapter_by_index(self):
|
||||
"""Test jumping to chapter by index"""
|
||||
chapters = self.reader.get_chapters()
|
||||
|
||||
if len(chapters) > 0:
|
||||
page = self.reader.jump_to_chapter(0)
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
def test_jump_to_chapter_by_name(self):
|
||||
"""Test jumping to chapter by name"""
|
||||
chapters = self.reader.get_chapters()
|
||||
|
||||
if len(chapters) > 0:
|
||||
chapter_title = chapters[0][0]
|
||||
page = self.reader.jump_to_chapter(chapter_title)
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
def test_jump_to_invalid_chapter_index(self):
|
||||
"""Test jumping to invalid chapter index"""
|
||||
page = self.reader.jump_to_chapter(9999)
|
||||
|
||||
self.assertIsNone(page)
|
||||
|
||||
def test_jump_to_invalid_chapter_name(self):
|
||||
"""Test jumping to non-existent chapter name"""
|
||||
page = self.reader.jump_to_chapter("Non-Existent Chapter")
|
||||
|
||||
self.assertIsNone(page)
|
||||
|
||||
|
||||
class TestEbookReaderInformation(unittest.TestCase):
|
||||
"""Test information retrieval methods"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_get_position_info(self):
|
||||
"""Test getting detailed position information"""
|
||||
info = self.reader.get_position_info()
|
||||
|
||||
self.assertIsInstance(info, dict)
|
||||
self.assertIn('position', info)
|
||||
self.assertIn('chapter', info)
|
||||
self.assertIn('progress', info)
|
||||
self.assertIn('font_scale', info)
|
||||
self.assertIn('book_title', info)
|
||||
self.assertIn('book_author', info)
|
||||
|
||||
def test_get_reading_progress(self):
|
||||
"""Test getting reading progress as percentage"""
|
||||
progress = self.reader.get_reading_progress()
|
||||
|
||||
self.assertIsInstance(progress, float)
|
||||
self.assertGreaterEqual(progress, 0.0)
|
||||
self.assertLessEqual(progress, 1.0)
|
||||
|
||||
# Progress should increase after navigation
|
||||
initial_progress = progress
|
||||
for _ in range(5):
|
||||
self.reader.next_page()
|
||||
|
||||
new_progress = self.reader.get_reading_progress()
|
||||
self.assertGreater(new_progress, initial_progress)
|
||||
|
||||
def test_get_current_chapter_info(self):
|
||||
"""Test getting current chapter information"""
|
||||
info = self.reader.get_current_chapter_info()
|
||||
|
||||
# May be None if no chapters
|
||||
if info is not None:
|
||||
self.assertIsInstance(info, dict)
|
||||
self.assertIn('title', info)
|
||||
self.assertIn('level', info)
|
||||
self.assertIn('block_index', info)
|
||||
|
||||
def test_get_book_info_complete(self):
|
||||
"""Test getting complete book information"""
|
||||
info = self.reader.get_book_info()
|
||||
|
||||
self.assertIsInstance(info, dict)
|
||||
self.assertIn('title', info)
|
||||
self.assertIn('author', info)
|
||||
self.assertIn('document_id', info)
|
||||
self.assertIn('total_blocks', info)
|
||||
self.assertIn('total_chapters', info)
|
||||
self.assertIn('page_size', info)
|
||||
self.assertIn('font_scale', info)
|
||||
|
||||
self.assertGreater(info['total_blocks'], 0)
|
||||
self.assertEqual(info['page_size'], self.reader.page_size)
|
||||
|
||||
|
||||
class TestEbookReaderFileOperations(unittest.TestCase):
|
||||
"""Test file I/O operations"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_render_to_file_png(self):
|
||||
"""Test saving current page as PNG"""
|
||||
output_path = os.path.join(self.temp_dir, "page.png")
|
||||
|
||||
success = self.reader.render_to_file(output_path)
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(os.path.exists(output_path))
|
||||
|
||||
# Verify it's a valid image
|
||||
img = Image.open(output_path)
|
||||
self.assertEqual(img.size, self.reader.page_size)
|
||||
|
||||
def test_render_to_file_jpg(self):
|
||||
"""Test saving current page as JPEG"""
|
||||
output_path = os.path.join(self.temp_dir, "page.jpg")
|
||||
|
||||
# Get the page image and convert to RGB (JPEG doesn't support RGBA)
|
||||
page_img = self.reader.get_current_page()
|
||||
if page_img.mode == 'RGBA':
|
||||
page_img = page_img.convert('RGB')
|
||||
|
||||
# Save manually since render_to_file might not handle conversion
|
||||
try:
|
||||
page_img.save(output_path)
|
||||
success = True
|
||||
except Exception:
|
||||
success = False
|
||||
|
||||
self.assertTrue(success)
|
||||
self.assertTrue(os.path.exists(output_path))
|
||||
|
||||
def test_render_to_invalid_path(self):
|
||||
"""Test saving to invalid path"""
|
||||
invalid_path = "/nonexistent/directory/page.png"
|
||||
|
||||
success = self.reader.render_to_file(invalid_path)
|
||||
|
||||
self.assertFalse(success)
|
||||
|
||||
|
||||
class TestEbookReaderContextManager(unittest.TestCase):
|
||||
"""Test context manager and cleanup"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_context_manager_usage(self):
|
||||
"""Test using EbookReader as context manager"""
|
||||
with EbookReader(bookmarks_dir=self.temp_dir) as reader:
|
||||
success = reader.load_epub(self.epub_path)
|
||||
self.assertTrue(success)
|
||||
|
||||
page = reader.get_current_page()
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
# After exiting context, manager should be cleaned up
|
||||
self.assertIsNone(reader.manager)
|
||||
|
||||
def test_close_method(self):
|
||||
"""Test explicit close method"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
reader.load_epub(self.epub_path)
|
||||
|
||||
self.assertIsNotNone(reader.manager)
|
||||
|
||||
reader.close()
|
||||
|
||||
self.assertIsNone(reader.manager)
|
||||
|
||||
def test_operations_after_close(self):
|
||||
"""Test that operations fail gracefully after close"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
reader.load_epub(self.epub_path)
|
||||
reader.close()
|
||||
|
||||
# These should all return None or empty
|
||||
self.assertIsNone(reader.get_current_page())
|
||||
self.assertIsNone(reader.next_page())
|
||||
self.assertIsNone(reader.previous_page())
|
||||
self.assertEqual(reader.get_chapters(), [])
|
||||
|
||||
|
||||
class TestEbookReaderErrorHandling(unittest.TestCase):
|
||||
"""Test error handling and edge cases"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_operations_without_loaded_book(self):
|
||||
"""Test that operations handle unloaded state gracefully"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
# All these should return None or empty/False
|
||||
self.assertIsNone(reader.get_current_page())
|
||||
self.assertIsNone(reader.next_page())
|
||||
self.assertIsNone(reader.previous_page())
|
||||
self.assertFalse(reader.save_position("test"))
|
||||
self.assertIsNone(reader.load_position("test"))
|
||||
self.assertEqual(reader.list_saved_positions(), [])
|
||||
self.assertFalse(reader.delete_position("test"))
|
||||
self.assertEqual(reader.get_chapters(), [])
|
||||
self.assertIsNone(reader.jump_to_chapter(0))
|
||||
self.assertIsNone(reader.set_font_size(1.5))
|
||||
self.assertEqual(reader.get_reading_progress(), 0.0)
|
||||
self.assertIsNone(reader.get_current_chapter_info())
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_is_loaded(self):
|
||||
"""Test is_loaded method"""
|
||||
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||
|
||||
self.assertFalse(reader.is_loaded())
|
||||
|
||||
reader.load_epub(self.epub_path)
|
||||
|
||||
self.assertTrue(reader.is_loaded())
|
||||
|
||||
reader.close()
|
||||
|
||||
|
||||
class TestEbookReaderIntegration(unittest.TestCase):
|
||||
"""Test complex integration scenarios"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_font_scaling_preserves_position(self):
|
||||
"""Test that changing font scale preserves reading position"""
|
||||
# Navigate to a specific position
|
||||
for _ in range(3):
|
||||
self.reader.next_page()
|
||||
|
||||
position_before = self.reader.manager.current_position.copy()
|
||||
|
||||
# Change font size
|
||||
self.reader.set_font_size(1.5)
|
||||
|
||||
position_after = self.reader.manager.current_position
|
||||
|
||||
# Position should be preserved
|
||||
self.assertEqual(position_before.chapter_index, position_after.chapter_index)
|
||||
self.assertEqual(position_before.block_index, position_after.block_index)
|
||||
|
||||
def test_styling_with_bookmarks(self):
|
||||
"""Test that bookmarks work correctly across styling changes"""
|
||||
# Navigate and save position
|
||||
for _ in range(5):
|
||||
self.reader.next_page()
|
||||
|
||||
self.reader.save_position("test_bookmark")
|
||||
|
||||
# Change styling
|
||||
self.reader.set_font_size(1.5)
|
||||
self.reader.set_line_spacing(12)
|
||||
|
||||
# Navigate away
|
||||
for _ in range(5):
|
||||
self.reader.next_page()
|
||||
|
||||
# Jump back to bookmark
|
||||
page = self.reader.load_position("test_bookmark")
|
||||
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
# Cleanup
|
||||
self.reader.delete_position("test_bookmark")
|
||||
|
||||
def test_chapter_navigation_after_font_change(self):
|
||||
"""Test chapter navigation after changing font size"""
|
||||
self.reader.set_font_size(2.0)
|
||||
|
||||
chapters = self.reader.get_chapters()
|
||||
|
||||
if len(chapters) > 0:
|
||||
page = self.reader.jump_to_chapter(0)
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
|
||||
class TestEbookReaderNavigation(unittest.TestCase):
|
||||
"""Test EbookReader navigation functionality (existing tests)"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def compare_images(self, img1: Image.Image, img2: Image.Image) -> bool:
|
||||
"""
|
||||
Check if two PIL Images are pixel-perfect identical.
|
||||
"""
|
||||
if img1 is None or img2 is None:
|
||||
return False
|
||||
|
||||
if img1.size != img2.size:
|
||||
return False
|
||||
|
||||
arr1 = np.array(img1)
|
||||
arr2 = np.array(img2)
|
||||
|
||||
return np.array_equal(arr1, arr2)
|
||||
|
||||
def test_bidirectional_navigation_20_pages(self):
|
||||
"""
|
||||
Test that navigating forward 20 pages and then backward 20 pages
|
||||
produces identical page renderings for the first page.
|
||||
"""
|
||||
reader = EbookReader(
|
||||
page_size=(800, 1000),
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
|
||||
success = reader.load_epub(self.epub_path)
|
||||
self.assertTrue(success, "Failed to load test EPUB")
|
||||
self.assertTrue(reader.is_loaded(), "Reader should be loaded")
|
||||
|
||||
initial_page = reader.get_current_page()
|
||||
self.assertIsNotNone(initial_page, "Initial page should not be None")
|
||||
|
||||
initial_position = reader.manager.current_position.copy()
|
||||
|
||||
forward_pages = [initial_page]
|
||||
forward_positions = [initial_position]
|
||||
pages_to_navigate = 20
|
||||
|
||||
for i in range(pages_to_navigate):
|
||||
page = reader.next_page()
|
||||
if page is None:
|
||||
break
|
||||
forward_pages.append(page)
|
||||
forward_positions.append(reader.manager.current_position.copy())
|
||||
|
||||
actual_pages_navigated = len(forward_pages) - 1
|
||||
|
||||
backward_pages = []
|
||||
|
||||
for i in range(len(forward_positions) - 1, -1, -1):
|
||||
position = forward_positions[i]
|
||||
page_obj = reader.manager.jump_to_position(position)
|
||||
page_img = page_obj.render()
|
||||
backward_pages.append(page_img)
|
||||
|
||||
final_page = backward_pages[-1]
|
||||
|
||||
self.assertTrue(
|
||||
self.compare_images(initial_page, final_page),
|
||||
"First page should be identical after forward/backward navigation"
|
||||
)
|
||||
|
||||
reader.close()
|
||||
|
||||
def test_navigation_at_boundaries(self):
|
||||
"""Test navigation behavior at document boundaries."""
|
||||
reader = EbookReader(
|
||||
page_size=(800, 1000),
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
|
||||
success = reader.load_epub(self.epub_path)
|
||||
self.assertTrue(success, "Failed to load test EPUB")
|
||||
|
||||
# Try to go backward from first page
|
||||
page = reader.previous_page()
|
||||
# Should return None or stay on same page
|
||||
|
||||
# Navigate forward until end
|
||||
pages_forward = 0
|
||||
max_pages = 100
|
||||
while pages_forward < max_pages:
|
||||
page = reader.next_page()
|
||||
if page is None:
|
||||
break
|
||||
pages_forward += 1
|
||||
|
||||
# Try to go forward from last page
|
||||
page = reader.next_page()
|
||||
self.assertIsNone(page, "Should return None at end of document")
|
||||
|
||||
reader.close()
|
||||
|
||||
|
||||
class TestEbookReaderPositionManagement(unittest.TestCase):
|
||||
"""Test position tracking and bookmark features"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.epub_path = "tests/data/test.epub"
|
||||
|
||||
if not Path(self.epub_path).exists():
|
||||
self.skipTest(f"Test EPUB not found at {self.epub_path}")
|
||||
|
||||
self.reader = EbookReader(
|
||||
bookmarks_dir=self.temp_dir,
|
||||
buffer_size=0
|
||||
)
|
||||
self.reader.load_epub(self.epub_path)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_position_save_and_load(self):
|
||||
"""Test saving and loading positions"""
|
||||
# Navigate to a position
|
||||
for _ in range(3):
|
||||
self.reader.next_page()
|
||||
|
||||
# Save position
|
||||
success = self.reader.save_position("test_pos")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Navigate away
|
||||
for _ in range(5):
|
||||
self.reader.next_page()
|
||||
|
||||
# Load saved position
|
||||
page = self.reader.load_position("test_pos")
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
def test_list_saved_positions(self):
|
||||
"""Test listing saved positions"""
|
||||
self.reader.save_position("pos1")
|
||||
self.reader.save_position("pos2")
|
||||
|
||||
positions = self.reader.list_saved_positions()
|
||||
|
||||
self.assertIn("pos1", positions)
|
||||
self.assertIn("pos2", positions)
|
||||
|
||||
def test_delete_position(self):
|
||||
"""Test deleting a saved position"""
|
||||
self.reader.save_position("temp_pos")
|
||||
|
||||
success = self.reader.delete_position("temp_pos")
|
||||
self.assertTrue(success)
|
||||
|
||||
positions = self.reader.list_saved_positions()
|
||||
self.assertNotIn("temp_pos", positions)
|
||||
|
||||
def test_delete_nonexistent_position(self):
|
||||
"""Test deleting a non-existent position"""
|
||||
success = self.reader.delete_position("nonexistent")
|
||||
self.assertFalse(success)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
316
tests/test_ereader_highlighting.py
Normal file
316
tests/test_ereader_highlighting.py
Normal file
@ -0,0 +1,316 @@
|
||||
"""
|
||||
Integration tests for EbookReader highlighting functionality.
|
||||
|
||||
Tests the highlighting API in EbookReader including:
|
||||
- highlight_word()
|
||||
- highlight_selection()
|
||||
- remove_highlight()
|
||||
- list_highlights()
|
||||
- get_highlights_for_current_page()
|
||||
- clear_highlights()
|
||||
- Highlight rendering on pages
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from dreader.application import EbookReader
|
||||
from pyWebLayout.core.highlight import HighlightColor
|
||||
|
||||
|
||||
class TestEbookReaderHighlighting(unittest.TestCase):
|
||||
"""Test EbookReader highlighting API"""
|
||||
|
||||
def setUp(self):
|
||||
"""Create temporary directories and reader"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.bookmarks_dir = Path(self.temp_dir) / "bookmarks"
|
||||
self.highlights_dir = Path(self.temp_dir) / "highlights"
|
||||
|
||||
self.reader = EbookReader(
|
||||
page_size=(800, 1000),
|
||||
bookmarks_dir=str(self.bookmarks_dir),
|
||||
highlights_dir=str(self.highlights_dir)
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up"""
|
||||
self.reader.close()
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_highlight_manager_not_initialized_before_load(self):
|
||||
"""Test that highlight manager is None before loading a book"""
|
||||
self.assertIsNone(self.reader.highlight_manager)
|
||||
|
||||
def test_highlight_manager_initialized_after_load(self):
|
||||
"""Test that highlight manager is initialized after loading"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
self.assertIsNotNone(self.reader.highlight_manager)
|
||||
self.assertEqual(self.reader.highlight_manager.document_id, "test")
|
||||
|
||||
def test_highlight_word_without_book(self):
|
||||
"""Test highlighting returns None when no book loaded"""
|
||||
highlight_id = self.reader.highlight_word(100, 100)
|
||||
self.assertIsNone(highlight_id)
|
||||
|
||||
def test_highlight_word(self):
|
||||
"""Test highlighting a word at a pixel location"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Get a page to ensure content is rendered
|
||||
page_img = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page_img)
|
||||
|
||||
# Try to highlight a word (coordinates may not hit text in test)
|
||||
highlight_id = self.reader.highlight_word(
|
||||
200, 300,
|
||||
color=HighlightColor.YELLOW.value,
|
||||
note="Test highlight"
|
||||
)
|
||||
|
||||
# If we hit text, should return an ID
|
||||
if highlight_id:
|
||||
self.assertIsInstance(highlight_id, str)
|
||||
self.assertGreater(len(highlight_id), 0)
|
||||
|
||||
# Verify it was added
|
||||
highlights = self.reader.list_highlights()
|
||||
self.assertGreater(len(highlights), 0)
|
||||
|
||||
def test_highlight_selection(self):
|
||||
"""Test highlighting a range of words"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Get a page
|
||||
page_img = self.reader.get_current_page()
|
||||
self.assertIsNotNone(page_img)
|
||||
|
||||
# Try to highlight a selection
|
||||
highlight_id = self.reader.highlight_selection(
|
||||
start=(100, 200),
|
||||
end=(400, 250),
|
||||
color=HighlightColor.BLUE.value,
|
||||
tags=["selection", "test"]
|
||||
)
|
||||
|
||||
# If selection hit text, should return an ID
|
||||
if highlight_id:
|
||||
self.assertIsInstance(highlight_id, str)
|
||||
|
||||
highlights = self.reader.list_highlights()
|
||||
found = False
|
||||
for h in highlights:
|
||||
if h.id == highlight_id:
|
||||
found = True
|
||||
self.assertEqual(h.color, HighlightColor.BLUE.value)
|
||||
self.assertIn("selection", h.tags)
|
||||
break
|
||||
self.assertTrue(found)
|
||||
|
||||
def test_list_highlights_empty(self):
|
||||
"""Test listing highlights when none exist"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
highlights = self.reader.list_highlights()
|
||||
self.assertEqual(len(highlights), 0)
|
||||
|
||||
def test_remove_highlight(self):
|
||||
"""Test removing a highlight"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
page_img = self.reader.get_current_page()
|
||||
|
||||
# Create a highlight directly via manager for testing
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
highlight = Highlight(
|
||||
id="test-highlight",
|
||||
bounds=[(100, 200, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text="Test"
|
||||
)
|
||||
self.reader.highlight_manager.add_highlight(highlight)
|
||||
|
||||
# Verify it exists
|
||||
self.assertEqual(len(self.reader.list_highlights()), 1)
|
||||
|
||||
# Remove it
|
||||
result = self.reader.remove_highlight("test-highlight")
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify it's gone
|
||||
self.assertEqual(len(self.reader.list_highlights()), 0)
|
||||
|
||||
def test_remove_nonexistent_highlight(self):
|
||||
"""Test removing a highlight that doesn't exist"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
result = self.reader.remove_highlight("nonexistent")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_clear_highlights(self):
|
||||
"""Test clearing all highlights"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Add some highlights directly
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
for i in range(3):
|
||||
highlight = Highlight(
|
||||
id=f"test-{i}",
|
||||
bounds=[(100 + i * 50, 200, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text=f"Word{i}"
|
||||
)
|
||||
self.reader.highlight_manager.add_highlight(highlight)
|
||||
|
||||
self.assertEqual(len(self.reader.list_highlights()), 3)
|
||||
|
||||
self.reader.clear_highlights()
|
||||
self.assertEqual(len(self.reader.list_highlights()), 0)
|
||||
|
||||
def test_get_highlights_for_current_page(self):
|
||||
"""Test getting highlights for current page"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Add highlights at different locations
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
# On page
|
||||
highlight1 = Highlight(
|
||||
id="on-page",
|
||||
bounds=[(100, 200, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text="OnPage"
|
||||
)
|
||||
|
||||
# Off page (beyond page height)
|
||||
highlight2 = Highlight(
|
||||
id="off-page",
|
||||
bounds=[(100, 5000, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text="OffPage"
|
||||
)
|
||||
|
||||
self.reader.highlight_manager.add_highlight(highlight1)
|
||||
self.reader.highlight_manager.add_highlight(highlight2)
|
||||
|
||||
# Get highlights for current page
|
||||
page_highlights = self.reader.get_highlights_for_current_page()
|
||||
|
||||
# Should only get the on-page highlight
|
||||
self.assertEqual(len(page_highlights), 1)
|
||||
self.assertEqual(page_highlights[0].id, "on-page")
|
||||
|
||||
def test_get_current_page_with_highlights(self):
|
||||
"""Test that get_current_page renders highlights"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Get page without highlights
|
||||
page_no_highlights = self.reader.get_current_page(include_highlights=False)
|
||||
self.assertIsNotNone(page_no_highlights)
|
||||
|
||||
# Add a highlight
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
highlight = Highlight(
|
||||
id="test",
|
||||
bounds=[(100, 200, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text="Test"
|
||||
)
|
||||
self.reader.highlight_manager.add_highlight(highlight)
|
||||
|
||||
# Get page with highlights
|
||||
page_with_highlights = self.reader.get_current_page(include_highlights=True)
|
||||
self.assertIsNotNone(page_with_highlights)
|
||||
|
||||
# Both should be valid images
|
||||
self.assertIsInstance(page_no_highlights, Image.Image)
|
||||
self.assertIsInstance(page_with_highlights, Image.Image)
|
||||
|
||||
# Should have same size
|
||||
self.assertEqual(page_no_highlights.size, page_with_highlights.size)
|
||||
|
||||
def test_highlight_persistence(self):
|
||||
"""Test that highlights persist across reader instances"""
|
||||
# Load book and add highlight
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
highlight = Highlight(
|
||||
id="persistent",
|
||||
bounds=[(100, 200, 50, 15)],
|
||||
color=HighlightColor.YELLOW.value,
|
||||
text="Persisted"
|
||||
)
|
||||
self.reader.highlight_manager.add_highlight(highlight)
|
||||
self.assertEqual(len(self.reader.list_highlights()), 1)
|
||||
|
||||
# Close reader
|
||||
self.reader.close()
|
||||
|
||||
# Create new reader for same book
|
||||
new_reader = EbookReader(
|
||||
page_size=(800, 1000),
|
||||
bookmarks_dir=str(self.bookmarks_dir),
|
||||
highlights_dir=str(self.highlights_dir)
|
||||
)
|
||||
success = new_reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Should load existing highlights
|
||||
highlights = new_reader.list_highlights()
|
||||
self.assertEqual(len(highlights), 1)
|
||||
self.assertEqual(highlights[0].id, "persistent")
|
||||
self.assertEqual(highlights[0].text, "Persisted")
|
||||
|
||||
new_reader.close()
|
||||
|
||||
def test_render_highlights_helper(self):
|
||||
"""Test the _render_highlights helper method"""
|
||||
success = self.reader.load_epub("tests/data/test.epub")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Create a simple test image
|
||||
from PIL import Image as PILImage
|
||||
test_img = PILImage.new('RGB', (100, 100), color='white')
|
||||
|
||||
from pyWebLayout.core.highlight import Highlight
|
||||
|
||||
highlights = [
|
||||
Highlight(
|
||||
id="h1",
|
||||
bounds=[(10, 10, 30, 20)],
|
||||
color=(255, 255, 0, 100),
|
||||
text="Test"
|
||||
)
|
||||
]
|
||||
|
||||
# Render highlights
|
||||
result_img = self.reader._render_highlights(test_img, highlights)
|
||||
|
||||
self.assertIsNotNone(result_img)
|
||||
self.assertIsInstance(result_img, PILImage.Image)
|
||||
self.assertEqual(result_img.size, test_img.size)
|
||||
# Result should be RGBA for transparency
|
||||
self.assertEqual(result_img.mode, 'RGBA')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
455
tests/test_ereader_system.py
Normal file
455
tests/test_ereader_system.py
Normal file
@ -0,0 +1,455 @@
|
||||
"""
|
||||
Comprehensive tests for the ereader layout system.
|
||||
|
||||
Tests the complete ereader functionality including position tracking,
|
||||
font scaling, chapter navigation, and page buffering.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from pyWebLayout.abstract.block import Paragraph, Heading, HeadingLevel
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from pyWebLayout.style import Font
|
||||
from pyWebLayout.style.page_style import PageStyle
|
||||
from pyWebLayout.layout.ereader_layout import RenderingPosition, ChapterNavigator, FontScaler, BidirectionalLayouter
|
||||
from pyWebLayout.layout.ereader_manager import EreaderLayoutManager, BookmarkManager, create_ereader_manager
|
||||
|
||||
|
||||
class TestRenderingPosition(unittest.TestCase):
|
||||
"""Test the RenderingPosition class"""
|
||||
|
||||
def test_position_creation(self):
|
||||
"""Test creating a rendering position"""
|
||||
pos = RenderingPosition(
|
||||
chapter_index=1,
|
||||
block_index=5,
|
||||
word_index=10,
|
||||
table_row=2,
|
||||
table_col=3
|
||||
)
|
||||
|
||||
self.assertEqual(pos.chapter_index, 1)
|
||||
self.assertEqual(pos.block_index, 5)
|
||||
self.assertEqual(pos.word_index, 10)
|
||||
self.assertEqual(pos.table_row, 2)
|
||||
self.assertEqual(pos.table_col, 3)
|
||||
|
||||
def test_position_serialization(self):
|
||||
"""Test position serialization and deserialization"""
|
||||
pos = RenderingPosition(
|
||||
chapter_index=1,
|
||||
block_index=5,
|
||||
word_index=10,
|
||||
remaining_pretext="test"
|
||||
)
|
||||
|
||||
# Serialize to dict
|
||||
pos_dict = pos.to_dict()
|
||||
self.assertIsInstance(pos_dict, dict)
|
||||
self.assertEqual(pos_dict['chapter_index'], 1)
|
||||
self.assertEqual(pos_dict['remaining_pretext'], "test")
|
||||
|
||||
# Deserialize from dict
|
||||
pos2 = RenderingPosition.from_dict(pos_dict)
|
||||
self.assertEqual(pos, pos2)
|
||||
|
||||
def test_position_copy(self):
|
||||
"""Test position copying"""
|
||||
pos = RenderingPosition(chapter_index=1, block_index=5)
|
||||
pos_copy = pos.copy()
|
||||
|
||||
self.assertEqual(pos, pos_copy)
|
||||
self.assertIsNot(pos, pos_copy) # Different objects
|
||||
|
||||
# Modify copy
|
||||
pos_copy.word_index = 10
|
||||
self.assertNotEqual(pos, pos_copy)
|
||||
|
||||
def test_position_equality_and_hashing(self):
|
||||
"""Test position equality and hashing"""
|
||||
pos1 = RenderingPosition(chapter_index=1, block_index=5)
|
||||
pos2 = RenderingPosition(chapter_index=1, block_index=5)
|
||||
pos3 = RenderingPosition(chapter_index=1, block_index=6)
|
||||
|
||||
self.assertEqual(pos1, pos2)
|
||||
self.assertNotEqual(pos1, pos3)
|
||||
|
||||
# Test hashing (for use as dict keys)
|
||||
pos_dict = {pos1: "test"}
|
||||
self.assertEqual(pos_dict[pos2], "test") # Should work due to equality
|
||||
|
||||
|
||||
class TestChapterNavigator(unittest.TestCase):
|
||||
"""Test the ChapterNavigator class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data"""
|
||||
self.font = Font()
|
||||
|
||||
# Create test blocks with headings
|
||||
self.blocks = [
|
||||
Paragraph(self.font), # Block 0
|
||||
Heading(HeadingLevel.H1, self.font), # Block 1 - Chapter 1
|
||||
Paragraph(self.font), # Block 2
|
||||
Heading(HeadingLevel.H2, self.font), # Block 3 - Subsection
|
||||
Paragraph(self.font), # Block 4
|
||||
Heading(HeadingLevel.H1, self.font), # Block 5 - Chapter 2
|
||||
Paragraph(self.font), # Block 6
|
||||
]
|
||||
|
||||
# Add text to headings
|
||||
self.blocks[1].add_word(Word("Chapter", self.font))
|
||||
self.blocks[1].add_word(Word("One", self.font))
|
||||
|
||||
self.blocks[3].add_word(Word("Subsection", self.font))
|
||||
self.blocks[3].add_word(Word("A", self.font))
|
||||
|
||||
self.blocks[5].add_word(Word("Chapter", self.font))
|
||||
self.blocks[5].add_word(Word("Two", self.font))
|
||||
|
||||
def test_chapter_detection(self):
|
||||
"""Test that chapters are detected correctly"""
|
||||
navigator = ChapterNavigator(self.blocks)
|
||||
|
||||
self.assertEqual(len(navigator.chapters), 3) # 2 H1s + 1 H2
|
||||
|
||||
# Check chapter titles
|
||||
titles = [chapter.title for chapter in navigator.chapters]
|
||||
self.assertIn("Chapter One", titles)
|
||||
self.assertIn("Subsection A", titles)
|
||||
self.assertIn("Chapter Two", titles)
|
||||
|
||||
def test_table_of_contents(self):
|
||||
"""Test table of contents generation"""
|
||||
navigator = ChapterNavigator(self.blocks)
|
||||
toc = navigator.get_table_of_contents()
|
||||
|
||||
self.assertEqual(len(toc), 3)
|
||||
|
||||
# Check first entry
|
||||
title, level, position = toc[0]
|
||||
self.assertEqual(title, "Chapter One")
|
||||
self.assertEqual(level, HeadingLevel.H1)
|
||||
self.assertIsInstance(position, RenderingPosition)
|
||||
|
||||
def test_chapter_position_lookup(self):
|
||||
"""Test looking up chapter positions"""
|
||||
navigator = ChapterNavigator(self.blocks)
|
||||
|
||||
pos = navigator.get_chapter_position("Chapter One")
|
||||
self.assertIsNotNone(pos)
|
||||
self.assertEqual(pos.chapter_index, 0)
|
||||
|
||||
pos = navigator.get_chapter_position("Nonexistent Chapter")
|
||||
self.assertIsNone(pos)
|
||||
|
||||
def test_current_chapter_detection(self):
|
||||
"""Test detecting current chapter from position"""
|
||||
navigator = ChapterNavigator(self.blocks)
|
||||
|
||||
# Position in first chapter
|
||||
pos = RenderingPosition(chapter_index=0, block_index=2)
|
||||
chapter = navigator.get_current_chapter(pos)
|
||||
self.assertIsNotNone(chapter)
|
||||
self.assertEqual(chapter.title, "Chapter One")
|
||||
|
||||
|
||||
class TestFontScaler(unittest.TestCase):
|
||||
"""Test the FontScaler class"""
|
||||
|
||||
def test_font_scaling(self):
|
||||
"""Test font scaling functionality"""
|
||||
original_font = Font(font_size=12)
|
||||
|
||||
# Test no scaling
|
||||
scaled_font = FontScaler.scale_font(original_font, 1.0)
|
||||
self.assertEqual(scaled_font.font_size, 12)
|
||||
|
||||
# Test 2x scaling
|
||||
scaled_font = FontScaler.scale_font(original_font, 2.0)
|
||||
self.assertEqual(scaled_font.font_size, 24)
|
||||
|
||||
# Test 0.5x scaling
|
||||
scaled_font = FontScaler.scale_font(original_font, 0.5)
|
||||
self.assertEqual(scaled_font.font_size, 6)
|
||||
|
||||
# Test minimum size constraint
|
||||
scaled_font = FontScaler.scale_font(original_font, 0.01)
|
||||
self.assertGreaterEqual(scaled_font.font_size, 1)
|
||||
|
||||
def test_word_spacing_scaling(self):
|
||||
"""Test word spacing scaling"""
|
||||
original_spacing = (5, 15)
|
||||
|
||||
# Test no scaling
|
||||
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 1.0)
|
||||
self.assertEqual(scaled_spacing, (5, 15))
|
||||
|
||||
# Test 2x scaling
|
||||
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 2.0)
|
||||
self.assertEqual(scaled_spacing, (10, 30))
|
||||
|
||||
# Test minimum constraints
|
||||
scaled_spacing = FontScaler.scale_word_spacing(original_spacing, 0.1)
|
||||
self.assertGreaterEqual(scaled_spacing[0], 1)
|
||||
self.assertGreaterEqual(scaled_spacing[1], 2)
|
||||
|
||||
|
||||
class TestBookmarkManager(unittest.TestCase):
|
||||
"""Test the BookmarkManager class"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.document_id = "test_document"
|
||||
self.bookmark_manager = BookmarkManager(self.document_id, self.temp_dir)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_bookmark_operations(self):
|
||||
"""Test bookmark add/remove/get operations"""
|
||||
pos = RenderingPosition(chapter_index=1, block_index=5)
|
||||
|
||||
# Add bookmark
|
||||
self.bookmark_manager.add_bookmark("test_bookmark", pos)
|
||||
|
||||
# Get bookmark
|
||||
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
|
||||
self.assertEqual(retrieved_pos, pos)
|
||||
|
||||
# List bookmarks
|
||||
bookmarks = self.bookmark_manager.list_bookmarks()
|
||||
self.assertEqual(len(bookmarks), 1)
|
||||
self.assertEqual(bookmarks[0][0], "test_bookmark")
|
||||
self.assertEqual(bookmarks[0][1], pos)
|
||||
|
||||
# Remove bookmark
|
||||
success = self.bookmark_manager.remove_bookmark("test_bookmark")
|
||||
self.assertTrue(success)
|
||||
|
||||
# Verify removal
|
||||
retrieved_pos = self.bookmark_manager.get_bookmark("test_bookmark")
|
||||
self.assertIsNone(retrieved_pos)
|
||||
|
||||
def test_reading_position_persistence(self):
|
||||
"""Test saving and loading reading position"""
|
||||
pos = RenderingPosition(chapter_index=2, block_index=10, word_index=5)
|
||||
|
||||
# Save position
|
||||
self.bookmark_manager.save_reading_position(pos)
|
||||
|
||||
# Create new manager instance (simulates app restart)
|
||||
new_manager = BookmarkManager(self.document_id, self.temp_dir)
|
||||
|
||||
# Load position
|
||||
loaded_pos = new_manager.load_reading_position()
|
||||
self.assertEqual(loaded_pos, pos)
|
||||
|
||||
def test_bookmark_persistence(self):
|
||||
"""Test that bookmarks persist across manager instances"""
|
||||
pos = RenderingPosition(chapter_index=1, block_index=5)
|
||||
|
||||
# Add bookmark
|
||||
self.bookmark_manager.add_bookmark("persistent_bookmark", pos)
|
||||
|
||||
# Create new manager instance
|
||||
new_manager = BookmarkManager(self.document_id, self.temp_dir)
|
||||
|
||||
# Verify bookmark exists
|
||||
retrieved_pos = new_manager.get_bookmark("persistent_bookmark")
|
||||
self.assertEqual(retrieved_pos, pos)
|
||||
|
||||
|
||||
class TestEreaderLayoutManager(unittest.TestCase):
|
||||
"""Test the complete EreaderLayoutManager"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data"""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.font = Font()
|
||||
|
||||
# Create test document with multiple paragraphs and headings
|
||||
self.blocks = []
|
||||
|
||||
# Add a heading
|
||||
heading = Heading(HeadingLevel.H1, self.font)
|
||||
heading.add_word(Word("Test", self.font))
|
||||
heading.add_word(Word("Chapter", self.font))
|
||||
self.blocks.append(heading)
|
||||
|
||||
# Add several paragraphs with multiple words
|
||||
for i in range(3):
|
||||
paragraph = Paragraph(self.font)
|
||||
for j in range(20): # 20 words per paragraph
|
||||
paragraph.add_word(Word(f"Word{i}_{j}", self.font))
|
||||
self.blocks.append(paragraph)
|
||||
|
||||
self.page_size = (400, 600)
|
||||
self.document_id = "test_document"
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_manager_initialization(self):
|
||||
"""Test ereader manager initialization"""
|
||||
# Change to temp directory for bookmarks
|
||||
original_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(self.temp_dir)
|
||||
|
||||
manager = EreaderLayoutManager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
self.assertEqual(manager.page_size, self.page_size)
|
||||
self.assertEqual(manager.document_id, self.document_id)
|
||||
self.assertEqual(manager.font_scale, 1.0)
|
||||
self.assertIsInstance(manager.current_position, RenderingPosition)
|
||||
|
||||
manager.shutdown()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_font_scaling(self):
|
||||
"""Test font scaling functionality"""
|
||||
original_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(self.temp_dir)
|
||||
|
||||
manager = EreaderLayoutManager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
# Test initial scale
|
||||
self.assertEqual(manager.get_font_scale(), 1.0)
|
||||
|
||||
# Test scaling
|
||||
page = manager.set_font_scale(1.5)
|
||||
self.assertEqual(manager.get_font_scale(), 1.5)
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
manager.shutdown()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_table_of_contents(self):
|
||||
"""Test table of contents functionality"""
|
||||
original_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(self.temp_dir)
|
||||
|
||||
manager = EreaderLayoutManager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
toc = manager.get_table_of_contents()
|
||||
self.assertGreater(len(toc), 0)
|
||||
|
||||
# Check first entry
|
||||
title, level, position = toc[0]
|
||||
self.assertEqual(title, "Test Chapter")
|
||||
self.assertEqual(level, HeadingLevel.H1)
|
||||
|
||||
manager.shutdown()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_bookmark_functionality(self):
|
||||
"""Test bookmark functionality"""
|
||||
manager = EreaderLayoutManager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
# Add bookmark
|
||||
success = manager.add_bookmark("test_bookmark")
|
||||
self.assertTrue(success)
|
||||
|
||||
# List bookmarks
|
||||
bookmarks = manager.list_bookmarks()
|
||||
self.assertEqual(len(bookmarks), 1)
|
||||
self.assertEqual(bookmarks[0][0], "test_bookmark")
|
||||
|
||||
# Jump to bookmark (should work even though it's the same position)
|
||||
page = manager.jump_to_bookmark("test_bookmark")
|
||||
self.assertIsNotNone(page)
|
||||
|
||||
# Remove bookmark
|
||||
success = manager.remove_bookmark("test_bookmark")
|
||||
self.assertTrue(success)
|
||||
|
||||
manager.shutdown()
|
||||
|
||||
def test_progress_tracking(self):
|
||||
"""Test reading progress tracking"""
|
||||
original_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(self.temp_dir)
|
||||
|
||||
manager = EreaderLayoutManager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
# Initial progress should be 0
|
||||
progress = manager.get_reading_progress()
|
||||
self.assertGreaterEqual(progress, 0.0)
|
||||
self.assertLessEqual(progress, 1.0)
|
||||
|
||||
# Get position info
|
||||
info = manager.get_position_info()
|
||||
self.assertIn('position', info)
|
||||
self.assertIn('progress', info)
|
||||
self.assertIn('font_scale', info)
|
||||
|
||||
manager.shutdown()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
def test_convenience_function(self):
|
||||
"""Test the convenience function"""
|
||||
original_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(self.temp_dir)
|
||||
|
||||
manager = create_ereader_manager(
|
||||
self.blocks,
|
||||
self.page_size,
|
||||
self.document_id,
|
||||
bookmarks_dir=self.temp_dir
|
||||
)
|
||||
|
||||
self.assertIsInstance(manager, EreaderLayoutManager)
|
||||
self.assertEqual(manager.page_size, self.page_size)
|
||||
|
||||
manager.shutdown()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user