407 lines
16 KiB
Python
407 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Basic EPUB Reader with Pagination using pyWebLayout
|
|
|
|
This reader loads EPUB files and displays them with page-by-page navigation
|
|
using the pyWebLayout system. It follows the proper architecture where:
|
|
- EPUBReader loads EPUB files into Document/Chapter objects
|
|
- Page renders those abstract objects into visual pages
|
|
- The UI handles pagination and navigation
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox
|
|
import os
|
|
from typing import List, Optional
|
|
from PIL import Image, ImageTk
|
|
|
|
from pyWebLayout.io.readers.epub_reader import EPUBReader
|
|
from pyWebLayout.concrete.page import Page
|
|
from pyWebLayout.style.fonts import Font
|
|
from pyWebLayout.abstract.document import Document, Chapter, Book
|
|
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
|
|
|
|
|
class EPUBReaderApp:
|
|
"""Main EPUB reader application using Tkinter"""
|
|
|
|
def __init__(self):
|
|
self.root = tk.Tk()
|
|
self.root.title("pyWebLayout EPUB Reader")
|
|
self.root.geometry("900x700")
|
|
|
|
# Application state
|
|
self.current_epub: Optional[EPUBReader] = None
|
|
self.current_document: Optional[Document] = None
|
|
self.rendered_pages: List[Page] = []
|
|
self.current_page_index = 0
|
|
|
|
# Page settings
|
|
self.page_width = 700
|
|
self.page_height = 550
|
|
self.blocks_per_page = 3 # Fewer blocks per page for better readability
|
|
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
"""Setup the user interface"""
|
|
# Create main frame
|
|
main_frame = ttk.Frame(self.root)
|
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# Top control frame
|
|
control_frame = ttk.Frame(main_frame)
|
|
control_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
# File operations
|
|
self.open_btn = ttk.Button(control_frame, text="Open EPUB", command=self.open_epub)
|
|
self.open_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
# Book info
|
|
self.book_info_label = ttk.Label(control_frame, text="No book loaded")
|
|
self.book_info_label.pack(side=tk.LEFT, expand=True)
|
|
|
|
# Navigation frame
|
|
nav_frame = ttk.Frame(main_frame)
|
|
nav_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
# Navigation buttons
|
|
self.prev_btn = ttk.Button(nav_frame, text="◀ Previous", command=self.previous_page, state=tk.DISABLED)
|
|
self.prev_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
self.next_btn = ttk.Button(nav_frame, text="Next ▶", command=self.next_page, state=tk.DISABLED)
|
|
self.next_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
# Page info
|
|
self.page_info_label = ttk.Label(nav_frame, text="Page 0 of 0")
|
|
self.page_info_label.pack(side=tk.LEFT, padx=(20, 0))
|
|
|
|
# Chapter selector
|
|
ttk.Label(nav_frame, text="Chapter:").pack(side=tk.LEFT, padx=(20, 5))
|
|
self.chapter_var = tk.StringVar()
|
|
self.chapter_combo = ttk.Combobox(nav_frame, textvariable=self.chapter_var, state="readonly", width=30)
|
|
self.chapter_combo.pack(side=tk.LEFT, padx=(0, 10))
|
|
self.chapter_combo.bind('<<ComboboxSelected>>', self.on_chapter_selected)
|
|
|
|
# Content frame with canvas
|
|
content_frame = ttk.Frame(main_frame)
|
|
content_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Create canvas for page display
|
|
self.canvas = tk.Canvas(content_frame, bg='white', width=self.page_width, height=self.page_height)
|
|
self.canvas.pack(expand=True)
|
|
|
|
# Status bar
|
|
self.status_var = tk.StringVar(value="Ready - Open an EPUB file to begin")
|
|
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
|
|
status_bar.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
# Bind keyboard shortcuts
|
|
self.root.bind('<Key-Left>', lambda e: self.previous_page())
|
|
self.root.bind('<Key-Right>', lambda e: self.next_page())
|
|
self.root.bind('<Key-space>', lambda e: self.next_page())
|
|
self.root.focus_set() # Allow keyboard input
|
|
|
|
def open_epub(self):
|
|
"""Open and load an EPUB file"""
|
|
file_path = filedialog.askopenfilename(
|
|
title="Open EPUB File",
|
|
filetypes=[("EPUB files", "*.epub"), ("All files", "*.*")]
|
|
)
|
|
|
|
if file_path:
|
|
self.load_epub(file_path)
|
|
|
|
def load_epub(self, file_path: str):
|
|
"""Load an EPUB file and prepare for display"""
|
|
try:
|
|
self.status_var.set("Loading EPUB file...")
|
|
self.root.update()
|
|
|
|
# Load the EPUB using the EPUBReader
|
|
self.current_epub = EPUBReader(file_path)
|
|
|
|
# Get the document structure from the EPUB
|
|
self.current_document = self.current_epub.read()
|
|
|
|
# Update book info
|
|
if isinstance(self.current_document, Book):
|
|
title = self.current_document.get_title() or "Unknown Title"
|
|
author = self.current_document.get_author() or "Unknown Author"
|
|
self.book_info_label.config(text=f"{title} by {author}")
|
|
else:
|
|
title = getattr(self.current_document, 'title', 'Unknown Title')
|
|
self.book_info_label.config(text=title)
|
|
|
|
# Populate chapter list
|
|
self.populate_chapter_list()
|
|
|
|
# Create pages from the document
|
|
self.create_pages_from_document()
|
|
|
|
# Show first page
|
|
self.current_page_index = 0
|
|
self.display_current_page()
|
|
self.update_navigation()
|
|
|
|
self.status_var.set(f"Loaded: {os.path.basename(file_path)} - {len(self.rendered_pages)} pages")
|
|
|
|
except Exception as e:
|
|
self.status_var.set(f"Error loading EPUB: {str(e)}")
|
|
messagebox.showerror("Error", f"Failed to load EPUB file:\n{str(e)}")
|
|
print(f"Detailed error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
def populate_chapter_list(self):
|
|
"""Populate the chapter selection dropdown"""
|
|
if not self.current_document:
|
|
return
|
|
|
|
chapters = []
|
|
|
|
# Check if it's a Book with chapters
|
|
if isinstance(self.current_document, Book) and self.current_document.chapters:
|
|
for i, chapter in enumerate(self.current_document.chapters):
|
|
chapter_title = chapter.title or f"Chapter {i+1}"
|
|
chapters.append(chapter_title)
|
|
else:
|
|
# Fallback: add a single "Document" entry
|
|
chapters.append("Document")
|
|
|
|
self.chapter_combo['values'] = chapters
|
|
if chapters:
|
|
self.chapter_combo.set(chapters[0])
|
|
|
|
def create_pages_from_document(self):
|
|
"""Create pages using the new external pagination system with block handlers"""
|
|
if not self.current_document:
|
|
return
|
|
|
|
self.rendered_pages.clear()
|
|
|
|
try:
|
|
# Get all blocks from the document
|
|
all_blocks = []
|
|
|
|
if isinstance(self.current_document, Book) and self.current_document.chapters:
|
|
# Process chapters
|
|
for chapter in self.current_document.chapters:
|
|
all_blocks.extend(chapter.blocks)
|
|
else:
|
|
# Process document blocks directly
|
|
all_blocks = self.current_document.blocks
|
|
|
|
# If no blocks found, try to create some from EPUB content
|
|
if not all_blocks:
|
|
all_blocks = self.create_blocks_from_epub_content()
|
|
|
|
# Use the new external pagination system
|
|
remaining_blocks = all_blocks
|
|
|
|
while remaining_blocks:
|
|
# Create a new page
|
|
current_page = Page(size=(self.page_width, self.page_height))
|
|
|
|
# Fill the page using the external pagination system
|
|
next_index, remainder_blocks = current_page.fill_with_blocks(remaining_blocks)
|
|
|
|
# Add the page if it has content
|
|
if current_page._children:
|
|
self.rendered_pages.append(current_page)
|
|
|
|
# Update remaining blocks for next iteration
|
|
if remainder_blocks:
|
|
# We have remainder blocks (partial content)
|
|
remaining_blocks = remainder_blocks
|
|
elif next_index < len(remaining_blocks):
|
|
# We stopped at a specific index
|
|
remaining_blocks = remaining_blocks[next_index:]
|
|
else:
|
|
# All blocks processed
|
|
remaining_blocks = []
|
|
|
|
# Safety check to prevent infinite loops
|
|
if not current_page._children and remaining_blocks:
|
|
print(f"Warning: Could not fit any content on page, skipping {len(remaining_blocks)} blocks")
|
|
break
|
|
|
|
# If no pages were created, create a default one
|
|
if not self.rendered_pages:
|
|
self.create_default_page()
|
|
|
|
except Exception as e:
|
|
print(f"Error creating pages: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
self.create_default_page()
|
|
|
|
|
|
def create_blocks_from_epub_content(self):
|
|
"""Create blocks from raw EPUB content when document parsing fails"""
|
|
blocks = []
|
|
|
|
try:
|
|
# Get HTML content from EPUB spine items
|
|
spine_items = self.current_epub.spine[:3] # Limit to first 3 items
|
|
|
|
for item_id in spine_items:
|
|
try:
|
|
# Get the manifest item
|
|
if item_id in self.current_epub.manifest:
|
|
item = self.current_epub.manifest[item_id]
|
|
file_path = item['path']
|
|
|
|
# Read the HTML content
|
|
if os.path.exists(file_path):
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Parse HTML content into blocks
|
|
html_blocks = parse_html_string(content)
|
|
blocks.extend(html_blocks[:5]) # Limit blocks per item
|
|
except Exception as e:
|
|
print(f"Error processing spine item {item_id}: {e}")
|
|
continue
|
|
|
|
except Exception as e:
|
|
print(f"Error getting EPUB content: {e}")
|
|
|
|
return blocks
|
|
|
|
def create_default_page(self):
|
|
"""Create a default page when content loading fails"""
|
|
page = Page(size=(self.page_width, self.page_height))
|
|
|
|
# Add some default content
|
|
from pyWebLayout.concrete.text import Text
|
|
default_font = Font()
|
|
|
|
if self.current_document:
|
|
title = getattr(self.current_document, 'title', None)
|
|
if title:
|
|
page.add_child(Text(f"Book: {title}", default_font))
|
|
page.add_child(Text("Content is loading...", default_font))
|
|
else:
|
|
page.add_child(Text("EPUB content loaded", default_font))
|
|
page.add_child(Text("Use arrow keys or buttons to navigate", default_font))
|
|
|
|
self.rendered_pages = [page]
|
|
|
|
def display_current_page(self):
|
|
"""Display the current page on the canvas"""
|
|
if not self.rendered_pages or self.current_page_index >= len(self.rendered_pages):
|
|
return
|
|
|
|
try:
|
|
# Clear the canvas
|
|
self.canvas.delete("all")
|
|
|
|
# Get the current page
|
|
page = self.rendered_pages[self.current_page_index]
|
|
|
|
# Render the page
|
|
page_image = page.render()
|
|
|
|
# Convert to PhotoImage
|
|
self.photo = ImageTk.PhotoImage(page_image)
|
|
|
|
# Calculate position to center the page
|
|
canvas_width = self.canvas.winfo_width()
|
|
canvas_height = self.canvas.winfo_height()
|
|
|
|
if canvas_width > 1 and canvas_height > 1: # Canvas is properly sized
|
|
x_pos = max(0, (canvas_width - page_image.width) // 2)
|
|
y_pos = max(0, (canvas_height - page_image.height) // 2)
|
|
else:
|
|
x_pos, y_pos = 0, 0
|
|
|
|
# Display the page
|
|
self.canvas.create_image(x_pos, y_pos, anchor=tk.NW, image=self.photo)
|
|
|
|
except Exception as e:
|
|
# Display error message
|
|
self.canvas.delete("all")
|
|
self.canvas.create_text(
|
|
self.page_width // 2, self.page_height // 2,
|
|
text=f"Error displaying page: {str(e)}",
|
|
fill="red", font=("Arial", 12)
|
|
)
|
|
print(f"Display error: {e}")
|
|
|
|
def previous_page(self):
|
|
"""Navigate to the previous page"""
|
|
if self.current_page_index > 0:
|
|
self.current_page_index -= 1
|
|
self.display_current_page()
|
|
self.update_navigation()
|
|
|
|
def next_page(self):
|
|
"""Navigate to the next page"""
|
|
if self.current_page_index < len(self.rendered_pages) - 1:
|
|
self.current_page_index += 1
|
|
self.display_current_page()
|
|
self.update_navigation()
|
|
|
|
def update_navigation(self):
|
|
"""Update navigation button states and page info"""
|
|
if not self.rendered_pages:
|
|
self.prev_btn.config(state=tk.DISABLED)
|
|
self.next_btn.config(state=tk.DISABLED)
|
|
self.page_info_label.config(text="Page 0 of 0")
|
|
return
|
|
|
|
# Update button states
|
|
self.prev_btn.config(state=tk.NORMAL if self.current_page_index > 0 else tk.DISABLED)
|
|
self.next_btn.config(state=tk.NORMAL if self.current_page_index < len(self.rendered_pages) - 1 else tk.DISABLED)
|
|
|
|
# Update page info
|
|
page_num = self.current_page_index + 1
|
|
total_pages = len(self.rendered_pages)
|
|
self.page_info_label.config(text=f"Page {page_num} of {total_pages}")
|
|
|
|
def on_chapter_selected(self, event=None):
|
|
"""Handle chapter selection"""
|
|
if not self.current_document or not self.rendered_pages:
|
|
return
|
|
|
|
selected_chapter = self.chapter_var.get()
|
|
|
|
# For now, just go to the first page
|
|
# In a more sophisticated implementation, we'd track chapter start pages
|
|
self.current_page_index = 0
|
|
self.display_current_page()
|
|
self.update_navigation()
|
|
|
|
self.status_var.set(f"Viewing: {selected_chapter}")
|
|
|
|
def run(self):
|
|
"""Start the EPUB reader application"""
|
|
# Make canvas responsive
|
|
def on_configure(event):
|
|
# Redisplay current page when canvas is resized
|
|
if hasattr(self, 'photo'):
|
|
self.root.after_idle(self.display_current_page)
|
|
|
|
self.canvas.bind('<Configure>', on_configure)
|
|
|
|
# Start the main loop
|
|
self.root.mainloop()
|
|
|
|
|
|
def main():
|
|
"""Main function to run the EPUB reader"""
|
|
print("Starting pyWebLayout EPUB Reader...")
|
|
|
|
try:
|
|
app = EPUBReaderApp()
|
|
app.run()
|
|
except Exception as e:
|
|
print(f"Error starting EPUB reader: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|