fixed issue with cover and image rendering
Python CI / test (3.10) (push) Successful in 2m10s
Python CI / test (3.12) (push) Successful in 2m3s
Python CI / test (3.13) (push) Successful in 1m57s

This commit is contained in:
2025-11-10 13:06:21 +01:00
parent 9fb6792e10
commit a8e459bce5
6 changed files with 681 additions and 15 deletions
+9 -2
View File
@@ -53,6 +53,9 @@ class RenderableImage(Renderable, Queriable):
if size[0] is None or size[1] is None:
size = (100, 100) # Default size when image dimensions are unavailable
# Ensure dimensions are positive (can be negative if calculated from insufficient space)
size = (max(1, size[0]), max(1, size[1]))
# Set size as numpy array
self._size = np.array(size)
@@ -172,6 +175,10 @@ class RenderableImage(Renderable, Queriable):
# Get the target dimensions
target_width, target_height = self._size
# Ensure target dimensions are positive
target_width = max(1, int(target_width))
target_height = max(1, int(target_height))
# Get the original dimensions
orig_width, orig_height = self._pil_image.size
@@ -183,8 +190,8 @@ class RenderableImage(Renderable, Queriable):
ratio = min(width_ratio, height_ratio)
# Calculate new dimensions
new_width = int(orig_width * ratio)
new_height = int(orig_height * ratio)
new_width = max(1, int(orig_width * ratio))
new_height = max(1, int(orig_height * ratio))
# Resize the image
if self._pil_image.mode == 'RGBA':
+39 -8
View File
@@ -446,17 +446,43 @@ class EPUBReader:
def _process_chapter_images(self, chapter: Chapter):
"""
Process images in a single chapter.
Load and process images in a single chapter.
This method loads images from disk into memory and applies image processing.
Images must be loaded before the temporary EPUB directory is cleaned up.
Args:
chapter: The chapter containing images to process
"""
from pyWebLayout.abstract.block import Image as AbstractImage
from PIL import Image as PILImage
import io
for block in chapter.blocks:
if isinstance(block, AbstractImage):
# Only process if image has been loaded and processor is enabled
if hasattr(block, '_loaded_image') and block._loaded_image:
# Load image into memory if not already loaded
if not hasattr(block, '_loaded_image') or not block._loaded_image:
try:
# Load the image from the source path
if os.path.isfile(block.source):
with open(block.source, '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
block._loaded_image = pil_image.copy() # Create a copy to ensure it persists
# Set width and height on the block from the loaded image
# This is required for layout calculations
block._width = pil_image.width
block._height = pil_image.height
except Exception as e:
print(f"Warning: Failed to load image '{block.source}': {str(e)}")
# Continue without the image
continue
# Apply image processing if enabled and image is loaded
if self.image_processor and hasattr(block, '_loaded_image') and block._loaded_image:
try:
block._loaded_image = self.image_processor(block._loaded_image)
except Exception as e:
@@ -466,10 +492,12 @@ class EPUBReader:
# Continue with unprocessed image
def _process_content_images(self):
"""Apply image processing to all images in chapters."""
if not self.image_processor:
return
"""
Load all images into memory and apply image processing.
This must be called before the temporary EPUB directory is cleaned up,
to ensure images are loaded from disk into memory.
"""
for chapter in self.book.chapters:
self._process_chapter_images(chapter)
@@ -527,8 +555,11 @@ class EPUBReader:
with open(path, 'r', encoding='utf-8') as f:
html = f.read()
# Parse HTML and add blocks to chapter
blocks = parse_html_string(html, document=self.book)
# Get the directory of the HTML file for resolving relative paths
html_dir = os.path.dirname(path)
# Parse HTML and add blocks to chapter, passing base_path for image resolution
blocks = parse_html_string(html, document=self.book, base_path=html_dir)
# Copy blocks to the chapter
for block in blocks:
+19 -3
View File
@@ -41,6 +41,7 @@ class StyleContext(NamedTuple):
element_attributes: Dict[str, Any]
parent_elements: List[str] # Stack of parent element names
document: Optional[Any] # Reference to document for font registry
base_path: Optional[str] = None # Base path for resolving relative URLs
def with_font(self, font: Font) -> "StyleContext":
"""Create new context with modified font."""
@@ -71,13 +72,15 @@ class StyleContext(NamedTuple):
def create_base_context(
base_font: Optional[Font] = None,
document=None) -> StyleContext:
document=None,
base_path: Optional[str] = None) -> StyleContext:
"""
Create a base style context with default values.
Args:
base_font: Base font to use, defaults to system default
document: Document instance for font registry
base_path: Base directory path for resolving relative URLs
Returns:
StyleContext with default values
@@ -97,6 +100,7 @@ def create_base_context(
element_attributes={},
parent_elements=[],
document=document,
base_path=base_path,
)
@@ -792,9 +796,19 @@ def line_break_handler(element: Tag, context: StyleContext) -> None:
def image_handler(element: Tag, context: StyleContext) -> Image:
"""Handle <img> elements."""
import os
import urllib.parse
src = context.element_attributes.get("src", "")
alt_text = context.element_attributes.get("alt", "")
# Resolve relative paths if base_path is provided
if context.base_path and src and not src.startswith(('http://', 'https://', '/')):
# Parse the src to handle URL-encoded characters
src_decoded = urllib.parse.unquote(src)
# Resolve relative path to absolute path
src = os.path.normpath(os.path.join(context.base_path, src_decoded))
# Parse dimensions if provided
width = height = None
try:
@@ -883,7 +897,7 @@ HANDLERS: Dict[str, Callable[[Tag, StyleContext], Union[Block, List[Block], None
def parse_html_string(
html_string: str, base_font: Optional[Font] = None, document=None
html_string: str, base_font: Optional[Font] = None, document=None, base_path: Optional[str] = None
) -> List[Block]:
"""
Parse HTML string and return list of Block objects.
@@ -892,12 +906,14 @@ def parse_html_string(
html_string: HTML content to parse
base_font: Base font for styling, defaults to system default
document: Document instance for font registry to avoid duplicate fonts
base_path: Base directory path for resolving relative URLs (e.g., image sources)
Returns:
List of Block objects representing the document structure
"""
soup = BeautifulSoup(html_string, "html.parser")
context = create_base_context(base_font, document)
context = create_base_context(base_font, document, base_path)
blocks = []
# Process the body if it exists, otherwise process all top-level elements
+5
View File
@@ -306,6 +306,11 @@ def image_layouter(image: AbstractImage, page: Page, max_width: Optional[int] =
# Calculate available height on page
available_height = page.size[1] - page._current_y_offset - page.border_size
# If no space available, image doesn't fit
if available_height <= 0:
return False
if max_height is None:
max_height = available_height
else:
+64 -2
View File
@@ -15,13 +15,13 @@ from __future__ import annotations
from dataclasses import dataclass, asdict
from typing import List, Dict, Tuple, Optional, Any
from pyWebLayout.abstract.block import Block, Paragraph, Heading, HeadingLevel, Table, HList
from pyWebLayout.abstract.block import Block, Paragraph, Heading, HeadingLevel, Table, HList, Image
from pyWebLayout.abstract.inline import Word
from pyWebLayout.concrete.page import Page
from pyWebLayout.concrete.text import Text
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style import Font
from pyWebLayout.layout.document_layouter import paragraph_layouter
from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter
@dataclass
@@ -94,6 +94,26 @@ class ChapterNavigator:
"""Scan blocks for headings and build chapter navigation map"""
current_chapter_index = 0
# Check if first block is a cover image and add it to TOC
if self.blocks and isinstance(self.blocks[0], Image):
cover_position = RenderingPosition(
chapter_index=0,
block_index=0,
word_index=0,
table_row=0,
table_col=0,
list_item_index=0
)
cover_info = ChapterInfo(
title="Cover",
level=HeadingLevel.H1, # Treat as top-level entry
position=cover_position,
block_index=0
)
self.chapters.append(cover_info)
for block_index, block in enumerate(self.blocks):
if isinstance(block, Heading):
# Create position for this heading
@@ -384,6 +404,8 @@ class BidirectionalLayouter:
return self._layout_table_on_page(block, page, position, font_scale)
elif isinstance(block, HList):
return self._layout_list_on_page(block, page, position, font_scale)
elif isinstance(block, Image):
return self._layout_image_on_page(block, page, position, font_scale)
else:
# Skip unknown block types
new_pos = position.copy()
@@ -496,6 +518,46 @@ class BidirectionalLayouter:
new_pos.list_item_index = 0
return True, new_pos
def _layout_image_on_page(self,
image: Image,
page: Page,
position: RenderingPosition,
font_scale: float) -> Tuple[bool,
RenderingPosition]:
"""
Layout an image on the page using the image_layouter.
Args:
image: The Image block to layout
page: The page to layout on
position: Current rendering position (should be at the start of this image block)
font_scale: Font scaling factor (not used for images, but kept for consistency)
Returns:
Tuple of (success, new_position)
- success: True if image was laid out, False if page ran out of space
- new_position: Updated position (next block if success, same block if failed)
"""
# Try to layout the image on the current page
success = image_layouter(
image=image,
page=page,
max_width=None, # Use page available width
max_height=None # Use page available height
)
new_pos = position.copy()
if success:
# Image was successfully laid out, move to next block
new_pos.block_index += 1
new_pos.word_index = 0
return True, new_pos
else:
# Image didn't fit on current page, signal to continue on next page
# Keep same position so it will be attempted on the next page
return False, position
def _estimate_page_start(
self,
end_position: RenderingPosition,