#!/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('<>', 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('', lambda e: self.previous_page()) self.root.bind('', lambda e: self.next_page()) self.root.bind('', 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('', 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()