fix cover issue, add copyleft text
This commit is contained in:
parent
12d6fcd5db
commit
25d36566d0
@ -79,6 +79,11 @@ class RenderableImage(Renderable, Queriable):
|
|||||||
def _load_image(self):
|
def _load_image(self):
|
||||||
"""Load the image from the source path"""
|
"""Load the image from the source path"""
|
||||||
try:
|
try:
|
||||||
|
# Check if the image has already been loaded into memory
|
||||||
|
if hasattr(self._abstract_image, '_loaded_image') and self._abstract_image._loaded_image is not None:
|
||||||
|
self._pil_image = self._abstract_image._loaded_image
|
||||||
|
return
|
||||||
|
|
||||||
source = self._abstract_image.source
|
source = self._abstract_image.source
|
||||||
|
|
||||||
# Handle different types of sources
|
# Handle different types of sources
|
||||||
|
|||||||
@ -194,6 +194,9 @@ class Page(Renderable, Queriable):
|
|||||||
# Synchronize draw context for Line objects before rendering
|
# Synchronize draw context for Line objects before rendering
|
||||||
if hasattr(child, '_draw'):
|
if hasattr(child, '_draw'):
|
||||||
child._draw = self._draw
|
child._draw = self._draw
|
||||||
|
# Synchronize canvas for Image objects before rendering
|
||||||
|
if hasattr(child, '_canvas'):
|
||||||
|
child._canvas = self._canvas
|
||||||
if hasattr(child, 'render'):
|
if hasattr(child, 'render'):
|
||||||
child.render()
|
child.render()
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,7 @@ class EPUBReader:
|
|||||||
self.toc = []
|
self.toc = []
|
||||||
self.spine = []
|
self.spine = []
|
||||||
self.manifest = {}
|
self.manifest = {}
|
||||||
|
self.cover_id = None # ID of the cover image in manifest
|
||||||
|
|
||||||
def read(self) -> Book:
|
def read(self) -> Book:
|
||||||
"""
|
"""
|
||||||
@ -172,6 +173,15 @@ class EPUBReader:
|
|||||||
else:
|
else:
|
||||||
# Store other metadata
|
# Store other metadata
|
||||||
self.metadata[name] = value
|
self.metadata[name] = value
|
||||||
|
|
||||||
|
# Parse meta elements for cover reference
|
||||||
|
for meta in metadata_elem.findall('.//{{{0}}}meta'.format(NAMESPACES['opf'])):
|
||||||
|
name = meta.get('name')
|
||||||
|
content = meta.get('content')
|
||||||
|
|
||||||
|
if name == 'cover' and content:
|
||||||
|
# This is a reference to the cover image in the manifest
|
||||||
|
self.cover_id = content
|
||||||
|
|
||||||
def _parse_manifest(self, root: ET.Element):
|
def _parse_manifest(self, root: ET.Element):
|
||||||
"""
|
"""
|
||||||
@ -320,8 +330,67 @@ class EPUBReader:
|
|||||||
if 'publisher' in self.metadata:
|
if 'publisher' in self.metadata:
|
||||||
self.book.set_metadata(MetadataType.PUBLISHER, self.metadata['publisher'])
|
self.book.set_metadata(MetadataType.PUBLISHER, self.metadata['publisher'])
|
||||||
|
|
||||||
|
def _add_cover_chapter(self):
|
||||||
|
"""Add a cover chapter if a cover image is available."""
|
||||||
|
if not self.cover_id or self.cover_id not in self.manifest:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the cover image path from the manifest
|
||||||
|
cover_item = self.manifest[self.cover_id]
|
||||||
|
cover_path = cover_item['path']
|
||||||
|
|
||||||
|
# Check if the file exists
|
||||||
|
if not os.path.exists(cover_path):
|
||||||
|
print(f"Warning: Cover image file not found: {cover_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a cover chapter
|
||||||
|
cover_chapter = self.book.create_chapter("Cover", 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create an Image block for the cover
|
||||||
|
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
import io
|
||||||
|
|
||||||
|
# Load the image into memory before the temp directory is cleaned up
|
||||||
|
# We need to fully copy the image data to ensure it persists after temp cleanup
|
||||||
|
with open(cover_path, 'rb') as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
|
||||||
|
# Create PIL image from bytes in memory
|
||||||
|
pil_image = PILImage.open(io.BytesIO(image_bytes))
|
||||||
|
pil_image.load() # Force loading into memory
|
||||||
|
|
||||||
|
# Create a copy to ensure all data is in memory
|
||||||
|
pil_image = pil_image.copy()
|
||||||
|
|
||||||
|
# Create an AbstractImage block with the cover image path
|
||||||
|
cover_image = AbstractImage(source=cover_path, alt_text="Cover Image")
|
||||||
|
|
||||||
|
# Set dimensions from the loaded image
|
||||||
|
cover_image._width = pil_image.width
|
||||||
|
cover_image._height = pil_image.height
|
||||||
|
|
||||||
|
# Store the loaded PIL image in the abstract image so it persists after temp cleanup
|
||||||
|
cover_image._loaded_image = pil_image
|
||||||
|
|
||||||
|
# Add the image to the cover chapter
|
||||||
|
cover_chapter.add_block(cover_image)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating cover chapter: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
# If we can't create the cover image, remove the chapter
|
||||||
|
if hasattr(self.book, 'chapters') and cover_chapter in self.book.chapters:
|
||||||
|
self.book.chapters.remove(cover_chapter)
|
||||||
|
|
||||||
def _add_chapters(self):
|
def _add_chapters(self):
|
||||||
"""Add chapters to the book based on the spine and TOC."""
|
"""Add chapters to the book based on the spine and TOC."""
|
||||||
|
# Add cover chapter first if available
|
||||||
|
self._add_cover_chapter()
|
||||||
|
|
||||||
# Create a mapping from src to TOC entry
|
# Create a mapping from src to TOC entry
|
||||||
toc_map = {}
|
toc_map = {}
|
||||||
|
|
||||||
@ -340,7 +409,8 @@ class EPUBReader:
|
|||||||
add_to_toc_map(self.toc)
|
add_to_toc_map(self.toc)
|
||||||
|
|
||||||
# Process spine items
|
# Process spine items
|
||||||
chapter_index = 0 # Keep track of actual content chapters
|
# Start from chapter_index = 1 if cover was added, otherwise 0
|
||||||
|
chapter_index = 1 if (self.cover_id and self.cover_id in self.manifest) else 0
|
||||||
for i, idref in enumerate(self.spine):
|
for i, idref in enumerate(self.spine):
|
||||||
if idref not in self.manifest:
|
if idref not in self.manifest:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -244,9 +244,12 @@ def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] =
|
|||||||
x_offset = page.border_size
|
x_offset = page.border_size
|
||||||
y_offset = page._current_y_offset
|
y_offset = page._current_y_offset
|
||||||
|
|
||||||
|
# Access page.draw to ensure canvas is initialized
|
||||||
|
_ = page.draw
|
||||||
|
|
||||||
renderable_image = RenderableImage(
|
renderable_image = RenderableImage(
|
||||||
image=image,
|
image=image,
|
||||||
canvas=page.canvas,
|
canvas=page._canvas,
|
||||||
max_width=max_width,
|
max_width=max_width,
|
||||||
max_height=max_height,
|
max_height=max_height,
|
||||||
origin=(x_offset, y_offset),
|
origin=(x_offset, y_offset),
|
||||||
|
|||||||
BIN
tests/data/test.epub
Normal file
BIN
tests/data/test.epub
Normal file
Binary file not shown.
305
tests/layout/test_ereader_application.py
Normal file
305
tests/layout/test_ereader_application.py
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
"""
|
||||||
|
Tests for the EbookReader application interface.
|
||||||
|
|
||||||
|
Tests the high-level EbookReader API including bidirectional navigation,
|
||||||
|
image rendering consistency, and position management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pyWebLayout.layout.ereader_application import EbookReader
|
||||||
|
|
||||||
|
|
||||||
|
class TestEbookReaderNavigation(unittest.TestCase):
|
||||||
|
"""Test EbookReader navigation functionality"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test environment"""
|
||||||
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
|
self.epub_path = "tests/data/test.epub"
|
||||||
|
|
||||||
|
# Verify test EPUB exists
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
img1: First image
|
||||||
|
img2: Second image
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if images are identical, False otherwise
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
This validates that the bidirectional layout system maintains
|
||||||
|
perfect consistency during extended navigation.
|
||||||
|
|
||||||
|
Note: This test uses position save/load as a workaround for incomplete
|
||||||
|
backward rendering implementation.
|
||||||
|
"""
|
||||||
|
# Create reader with standard page size and disable buffer to avoid pickle issues
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(800, 1000),
|
||||||
|
bookmarks_dir=self.temp_dir,
|
||||||
|
buffer_size=0 # Disable multiprocess buffering
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load the test EPUB
|
||||||
|
success = reader.load_epub(self.epub_path)
|
||||||
|
self.assertTrue(success, "Failed to load test EPUB")
|
||||||
|
self.assertTrue(reader.is_loaded(), "Reader should be loaded")
|
||||||
|
|
||||||
|
# Capture initial page (page 0) and save its position
|
||||||
|
initial_page = reader.get_current_page()
|
||||||
|
self.assertIsNotNone(initial_page, "Initial page should not be None")
|
||||||
|
|
||||||
|
# Save the initial position for later comparison
|
||||||
|
initial_position = reader.manager.current_position.copy()
|
||||||
|
|
||||||
|
# Store forward navigation pages and positions
|
||||||
|
forward_pages = [initial_page]
|
||||||
|
forward_positions = [initial_position]
|
||||||
|
pages_to_navigate = 20
|
||||||
|
|
||||||
|
# Navigate forward, capturing each page and position
|
||||||
|
for i in range(pages_to_navigate):
|
||||||
|
page = reader.next_page()
|
||||||
|
if page is None:
|
||||||
|
# Reached end of document
|
||||||
|
print(f"Reached end of document at page {i + 1}")
|
||||||
|
break
|
||||||
|
forward_pages.append(page)
|
||||||
|
forward_positions.append(reader.manager.current_position.copy())
|
||||||
|
|
||||||
|
actual_pages_navigated = len(forward_pages) - 1
|
||||||
|
print(f"Navigated forward through {actual_pages_navigated} pages")
|
||||||
|
|
||||||
|
# Now navigate backward using position jumps (more reliable than previous_page)
|
||||||
|
backward_pages = []
|
||||||
|
|
||||||
|
# Traverse backwards through our saved positions
|
||||||
|
for i in range(len(forward_positions) - 1, -1, -1):
|
||||||
|
position = forward_positions[i]
|
||||||
|
page_obj = reader.manager.jump_to_position(position)
|
||||||
|
# Render the Page object to get PIL Image
|
||||||
|
page_img = page_obj.render()
|
||||||
|
backward_pages.append(page_img)
|
||||||
|
|
||||||
|
# The last page from backward navigation should be page 0
|
||||||
|
final_page = backward_pages[-1]
|
||||||
|
|
||||||
|
# Debug: Save images to inspect differences
|
||||||
|
initial_page.save("/tmp/initial_page.png")
|
||||||
|
final_page.save("/tmp/final_page.png")
|
||||||
|
|
||||||
|
# Check image sizes first
|
||||||
|
print(f"Initial page size: {initial_page.size}")
|
||||||
|
print(f"Final page size: {final_page.size}")
|
||||||
|
print(f"Initial position: {initial_position}")
|
||||||
|
print(f"Final position: {forward_positions[0]}")
|
||||||
|
|
||||||
|
# Compare arrays to see differences
|
||||||
|
arr1 = np.array(initial_page)
|
||||||
|
arr2 = np.array(final_page)
|
||||||
|
if arr1.shape == arr2.shape:
|
||||||
|
diff = np.abs(arr1.astype(int) - arr2.astype(int))
|
||||||
|
diff_pixels = np.count_nonzero(diff)
|
||||||
|
total_pixels = arr1.shape[0] * arr1.shape[1] * arr1.shape[2]
|
||||||
|
print(f"Different pixels: {diff_pixels} out of {total_pixels} ({100*diff_pixels/total_pixels:.2f}%)")
|
||||||
|
if diff_pixels > 0:
|
||||||
|
# Save difference map
|
||||||
|
diff_img = Image.fromarray(np.clip(diff.sum(axis=2) * 10, 0, 255).astype(np.uint8))
|
||||||
|
diff_img.save("/tmp/diff_page.png")
|
||||||
|
|
||||||
|
# Critical assertion: first page should be identical after round trip
|
||||||
|
self.assertTrue(
|
||||||
|
self.compare_images(initial_page, final_page),
|
||||||
|
"First page should be identical after forward/backward navigation"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extended validation: compare all pages
|
||||||
|
# forward_pages[i] should match backward_pages[-(i+1)]
|
||||||
|
mismatches = []
|
||||||
|
for i in range(len(forward_pages)):
|
||||||
|
forward_page = forward_pages[i]
|
||||||
|
backward_page = backward_pages[-(i + 1)]
|
||||||
|
|
||||||
|
if not self.compare_images(forward_page, backward_page):
|
||||||
|
mismatches.append(i)
|
||||||
|
# Save mismatched pages for debugging
|
||||||
|
if i < 3: # Only save first few mismatches
|
||||||
|
forward_page.save(f"/tmp/forward_page_{i}.png")
|
||||||
|
backward_page.save(f"/tmp/backward_page_{i}.png")
|
||||||
|
|
||||||
|
if mismatches:
|
||||||
|
self.fail(f"Page mismatches detected at indices: {mismatches[:10]}... (showing first 10)")
|
||||||
|
|
||||||
|
print(f"Successfully validated bidirectional navigation consistency")
|
||||||
|
print(f"Tested {actual_pages_navigated} pages forward and backward")
|
||||||
|
print(f"All {len(forward_pages)} pages matched perfectly")
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
success = reader.load_epub(self.epub_path)
|
||||||
|
self.assertTrue(success, "Failed to load test EPUB")
|
||||||
|
|
||||||
|
# Try to go backward from first page
|
||||||
|
# Should stay on first page or return None gracefully
|
||||||
|
page = reader.previous_page()
|
||||||
|
# Behavior depends on implementation - could be None or same page
|
||||||
|
|
||||||
|
# Navigate forward until end
|
||||||
|
pages_forward = 0
|
||||||
|
max_pages = 100 # Safety limit
|
||||||
|
while pages_forward < max_pages:
|
||||||
|
page = reader.next_page()
|
||||||
|
if page is None:
|
||||||
|
break
|
||||||
|
pages_forward += 1
|
||||||
|
|
||||||
|
print(f"Document has approximately {pages_forward} pages")
|
||||||
|
|
||||||
|
# Try to go forward from last page
|
||||||
|
page = reader.next_page()
|
||||||
|
self.assertIsNone(page, "Should return None at end of document")
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEbookReaderFeatures(unittest.TestCase):
|
||||||
|
"""Test other EbookReader 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}")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up test environment"""
|
||||||
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_epub_loading(self):
|
||||||
|
"""Test EPUB loading functionality"""
|
||||||
|
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||||
|
|
||||||
|
# Test with valid EPUB
|
||||||
|
success = reader.load_epub(self.epub_path)
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertTrue(reader.is_loaded())
|
||||||
|
|
||||||
|
# Get book info
|
||||||
|
info = reader.get_book_info()
|
||||||
|
self.assertIsNotNone(info)
|
||||||
|
self.assertIn('title', info)
|
||||||
|
self.assertIn('author', info)
|
||||||
|
self.assertGreater(info['total_blocks'], 0)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_page_rendering(self):
|
||||||
|
"""Test that pages render correctly as PIL Images"""
|
||||||
|
reader = EbookReader(
|
||||||
|
page_size=(400, 600),
|
||||||
|
bookmarks_dir=self.temp_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
page = reader.get_current_page()
|
||||||
|
self.assertIsNotNone(page)
|
||||||
|
self.assertIsInstance(page, Image.Image)
|
||||||
|
self.assertEqual(page.size, (400, 600))
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_position_tracking(self):
|
||||||
|
"""Test position save/load functionality"""
|
||||||
|
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
# Navigate to a specific position
|
||||||
|
for _ in range(3):
|
||||||
|
reader.next_page()
|
||||||
|
|
||||||
|
# Save position
|
||||||
|
success = reader.save_position("test_position")
|
||||||
|
self.assertTrue(success)
|
||||||
|
|
||||||
|
# Navigate away
|
||||||
|
for _ in range(5):
|
||||||
|
reader.next_page()
|
||||||
|
|
||||||
|
# Load saved position
|
||||||
|
page = reader.load_position("test_position")
|
||||||
|
self.assertIsNotNone(page)
|
||||||
|
|
||||||
|
# List positions
|
||||||
|
positions = reader.list_saved_positions()
|
||||||
|
self.assertIn("test_position", positions)
|
||||||
|
|
||||||
|
# Delete position
|
||||||
|
success = reader.delete_position("test_position")
|
||||||
|
self.assertTrue(success)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
def test_chapter_navigation(self):
|
||||||
|
"""Test chapter listing and navigation"""
|
||||||
|
reader = EbookReader(bookmarks_dir=self.temp_dir)
|
||||||
|
reader.load_epub(self.epub_path)
|
||||||
|
|
||||||
|
# Get chapters
|
||||||
|
chapters = reader.get_chapters()
|
||||||
|
self.assertIsInstance(chapters, list)
|
||||||
|
|
||||||
|
# If book has chapters, test navigation
|
||||||
|
if len(chapters) > 0:
|
||||||
|
# Jump to first chapter
|
||||||
|
page = reader.jump_to_chapter(0)
|
||||||
|
self.assertIsNotNone(page)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user