more refactoring
All checks were successful
Python CI / test (push) Successful in 6m36s

This commit is contained in:
Duncan Tourolle 2025-11-07 20:34:28 +01:00
parent 03918fc716
commit 5fe4db4cbe
5 changed files with 0 additions and 1723 deletions

View File

@ -1,111 +0,0 @@
#!/usr/bin/env python3
"""
Demonstration of the refactored alignment handler system.
This shows how the nested alignment logic has been replaced with a clean handler pattern.
"""
from pyWebLayout.concrete.text import (
Line, Text,
LeftAlignmentHandler, CenterRightAlignmentHandler, JustifyAlignmentHandler
)
from pyWebLayout.style import Alignment
from pyWebLayout.style import Font
def demonstrate_handler_system():
"""Demonstrate the new alignment handler system."""
print("=" * 60)
print("ALIGNMENT HANDLER SYSTEM DEMONSTRATION")
print("=" * 60)
print("\n1. HANDLER CREATION:")
print(" The system now uses three specialized handlers:")
# Create handlers
left_handler = LeftAlignmentHandler()
center_handler = CenterRightAlignmentHandler(Alignment.CENTER)
right_handler = CenterRightAlignmentHandler(Alignment.RIGHT)
justify_handler = JustifyAlignmentHandler()
print(f" • LeftAlignmentHandler: {type(left_handler).__name__}")
print(f" • CenterRightAlignmentHandler (Center): {type(center_handler).__name__}")
print(f" • CenterRightAlignmentHandler (Right): {type(right_handler).__name__}")
print(f" • JustifyAlignmentHandler: {type(justify_handler).__name__}")
print("\n2. AUTOMATIC HANDLER SELECTION:")
print(" Lines automatically choose the correct handler based on alignment:")
font = Font()
line_size = (300, 30)
spacing = (5, 20)
alignments = [
(Alignment.LEFT, "Left"),
(Alignment.CENTER, "Center"),
(Alignment.RIGHT, "Right"),
(Alignment.JUSTIFY, "Justify")
]
for alignment, name in alignments:
line = Line(spacing, (0, 0), line_size, font, halign=alignment)
handler_type = type(line._alignment_handler).__name__
print(f"{name:7}{handler_type}")
print("\n3. HYPHENATION INTEGRATION:")
print(" Each handler has its own hyphenation strategy:")
# Sample text objects and test conditions
sample_text = [Text("Hello", font), Text("World", font)]
word_width = 80
available_width = 70 # Word doesn't fit
min_spacing = 5
handlers = [
("Left", left_handler),
("Center", center_handler),
("Right", right_handler),
("Justify", justify_handler)
]
for name, handler in handlers:
should_hyphenate = handler.should_try_hyphenation(
sample_text, word_width, available_width, min_spacing)
print(f"{name:7}: should_hyphenate = {should_hyphenate}")
print("\n4. SPACING CALCULATIONS:")
print(" Each handler calculates spacing and positioning differently:")
for name, handler in handlers:
spacing_calc, x_position = handler.calculate_spacing_and_position(
sample_text, 300, 5, 20)
print(f"{name:7}: spacing={spacing_calc:2d}, position={x_position:3d}")
print("\n5. WORD ADDITION WITH INTELLIGENT HYPHENATION:")
print(" The system now tries different hyphenation options for optimal spacing:")
# Test with a word that might benefit from hyphenation
test_line = Line(spacing, (0, 0), (200, 30), font, halign=Alignment.JUSTIFY)
test_words = ["This", "is", "a", "demonstration", "of", "smart", "hyphenation"]
for word in test_words:
result = test_line.add_word(word)
if result:
print(f" • Word '{word}' → remainder: '{result}' (line full)")
break
else:
print(f" • Added '{word}' successfully")
print(f" • Final line contains {len(test_line.text_objects)} text objects")
print("\n6. BENEFITS OF THE NEW SYSTEM:")
print(" ✓ Separation of concerns - each alignment has its own handler")
print(" ✓ Extensible - easy to add new alignment types")
print(" ✓ Intelligent hyphenation - considers spacing quality")
print(" ✓ Clean code - no more nested if/else alignment logic")
print(" ✓ Testable - each handler can be tested independently")
print("\n" + "=" * 60)
print("REFACTORING COMPLETE - ALIGNMENT HANDLERS WORKING!")
print("=" * 60)
if __name__ == "__main__":
demonstrate_handler_system()

View File

@ -1,406 +0,0 @@
#!/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()

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python3
"""
Example EPUB viewer using pyWebLayout.
This example demonstrates how to use pyWebLayout to load an EPUB file,
paginate it, and render pages as images.
"""
import os
import sys
import argparse
from pathlib import Path
from PIL import Image
# Add the parent directory to the path to import pyWebLayout
sys.path.append(str(Path(__file__).parent.parent.parent))
from pyWebLayout import (
Document, Book, read_epub,
DocumentPaginator, Page
)
def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='EPUB viewer example')
parser.add_argument('epub_file', help='Path to EPUB file')
parser.add_argument('--output-dir', '-o', default='output', help='Output directory for rendered pages')
parser.add_argument('--width', '-w', type=int, default=800, help='Page width')
parser.add_argument('--height', '-y', type=int, default=1000, help='Page height')
parser.add_argument('--margin', '-m', type=int, default=50, help='Page margin')
parser.add_argument('--max-pages', '-p', type=int, default=10, help='Maximum number of pages to render')
args = parser.parse_args()
# Create output directory
os.makedirs(args.output_dir, exist_ok=True)
# Read EPUB file
print(f"Reading EPUB file: {args.epub_file}")
book = read_epub(args.epub_file)
# Display book metadata
print(f"Title: {book.get_title()}")
print(f"Author: {book.get_metadata('AUTHOR')}")
print(f"Chapters: {len(book.chapters)}")
# Create a paginator
page_size = (args.width, args.height)
margins = (args.margin, args.margin, args.margin, args.margin)
paginator = DocumentPaginator(
document=book,
page_size=page_size,
margins=margins
)
# Paginate and render pages
print("Paginating and rendering pages...")
# Option 1: Render all pages at once
pages = paginator.paginate(max_pages=args.max_pages)
for i, page in enumerate(pages):
# Render the page
image = page.render()
# Save the image
output_path = os.path.join(args.output_dir, f"page_{i+1:03d}.png")
image.save(output_path)
print(f"Saved page {i+1} to {output_path}")
# Option 2: Render pages one by one with state saving
"""
# Clear paginator state
paginator.state = DocumentPaginationState()
for i in range(args.max_pages):
# Get next page
page = paginator.paginate_next()
if page is None:
print(f"No more pages after page {i}")
break
# Render the page
image = page.render()
# Save the image
output_path = os.path.join(args.output_dir, f"page_{i+1:03d}.png")
image.save(output_path)
print(f"Saved page {i+1} to {output_path}")
# Save pagination state (could be saved to a file for later resumption)
state_dict = paginator.get_state()
# Progress information
progress = paginator.get_progress() * 100
print(f"Progress: {progress:.1f}%")
"""
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page for pyWebLayout Browser</title>
</head>
<body>
<h1>pyWebLayout Browser Test Page</h1>
<h3>Images</h3>
<p>Here's a sample image:</p>
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
<h2>Text Formatting</h2>
<p>This is a paragraph with <b>bold text</b>, <i>italic text</i>, and <u>underlined text</u>.</p>
<h3>Links</h3>
<p>Here are some test links:</p>
<ul>
<li><a href="https://www.google.com" title="Google">External link to Google</a></li>
<li><a href="#section1" title="Section 1">Internal link to Section 1</a></li>
</ul>
<h3>Headers</h3>
<h1>H1 Header</h1>
<h2>H2 Header</h2>
<h3>H3 Header</h3>
<h4>H4 Header</h4>
<h5>H5 Header</h5>
<h6>H6 Header</h6>
<h3>Line Breaks and Paragraphs</h3>
<p>This is the first paragraph.</p>
<br>
<p>This is the second paragraph after a line break.</p>
<p>
It transpired after a confused five minutes that the man had heard Gatsbys name around his office in a connection which he either wouldnt reveal or didnt fully understand. This was his day off and with laudable initiative he had hurried out “to see.”
</p>
<p>
It was a random shot, and yet the reporters instinct was right. Gatsbys notoriety, spread about by the hundreds who had accepted his hospitality and so become authorities upon his past, had increased all summer until he fell just short of being news. Contemporary legends such as the “underground pipeline to Canada” attached themselves to him, and there was one persistent story that he didnt live in a house at all, but in a boat that looked like a house and was moved secretly up and down the Long Island shore. Just why these inventions were a source of satisfaction to James Gatz of North Dakota, isnt easy to say.
</p>
<p>
James Gatz—that was really, or at least legally, his name. He had changed it at the age of seventeen and at the specific moment that witnessed the beginning of his career—when he saw Dan Codys yacht drop anchor over the most insidious flat on Lake Superior. It was James Gatz who had been loafing along the beach that afternoon in a torn green jersey and a pair of canvas pants, but it was already Jay Gatsby who borrowed a rowboat, pulled out to the <i>Tuolomee</i>, and informed Cody that a wind might catch him and break him up in half an hour.
</p>
<p>
I suppose hed had the name ready for a long time, even then. His parents were shiftless and unsuccessful farm people—his imagination had never really accepted them as his parents at all. The truth was that Jay Gatsby of West Egg, Long Island, sprang from his Platonic conception of himself. He was a son of God—a phrase which, if it means anything, means just that—and he must be about His Fathers business, the service of a vast, vulgar, and meretricious beauty. So he invented just the sort of Jay Gatsby that a seventeen-year-old boy would be likely to invent, and to this conception he was faithful to the end.
</p>
<h3 id="section1">Section 1</h3>
<p>This is the content of section 1. You can link to this section using the internal link above.</p>
<h3>Images</h3>
<p>Here's a sample image:</p>
<img src="tests/data/sample_image.jpg" alt="Sample Image" width="200" height="150">
<h3>Mixed Content</h3>
<p>This paragraph contains <b>bold</b> and <i>italic</i> text, as well as an <a href="https://www.example.com">external link</a>.</p>
<p><strong>Strong text</strong> and <em>emphasized text</em> should also work.</p>
</body>
</html>