#!/usr/bin/env python3 """ Generate test EPUBs for rendering verification. Creates EPUBs to verify: - Image: Grayscale rendering (4 levels), scaling, centering, cache performance - Text: pre element line breaks, blank lines, nested code element - Layout: nested block margins, sibling style restoration, image wrapper spacing """ import os import zipfile from pathlib import Path try: from PIL import Image, ImageDraw, ImageFont except ImportError: print("Please install Pillow: pip install Pillow") exit(1) OUTPUT_DIR = Path(__file__).parent.parent / "test" / "epubs" SCREEN_WIDTH = 480 SCREEN_HEIGHT = 800 def get_font(size=20): """Get a font, falling back to default if needed.""" import sys candidates = [] if sys.platform == "win32": windir = os.environ.get("WINDIR", "C:\\Windows") candidates = [ os.path.join(windir, "Fonts", "arial.ttf"), os.path.join(windir, "Fonts", "calibri.ttf"), ] elif sys.platform == "darwin": candidates = [ "/System/Library/Fonts/Helvetica.ttc", "/Library/Fonts/Arial.ttf", ] else: candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", ] for path in candidates: try: return ImageFont.truetype(path, size) except Exception: continue return ImageFont.load_default() def draw_text_centered(draw, y, text, font, fill=0): """Draw centered text at given y position.""" bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] x = (draw.im.size[0] - text_width) // 2 draw.text((x, y), text, font=font, fill=fill) def draw_text_wrapped(draw, x, y, text, font, max_width, fill=0): """Draw text with word wrapping.""" words = text.split() lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=font) if bbox[2] - bbox[0] <= max_width: current_line.append(word) else: if current_line: lines.append(" ".join(current_line)) current_line = [word] if current_line: lines.append(" ".join(current_line)) line_height = font.size + 4 if hasattr(font, "size") else 20 for i, line in enumerate(lines): draw.text((x, y + i * line_height), line, font=font, fill=fill) return len(lines) * line_height def create_grayscale_test_image(filename, is_png=True): """ Create image with 4 grayscale squares to verify 4-level rendering. """ width, height = 400, 600 img = Image.new("L", (width, height), 255) draw = ImageDraw.Draw(img) font = get_font(16) font_small = get_font(14) # Title draw_text_centered(draw, 10, "GRAYSCALE TEST", font, fill=0) draw_text_centered(draw, 35, "Verify 4 distinct gray levels", font_small, fill=64) # Draw 4 grayscale squares square_size = 70 start_y = 65 gap = 10 # Gray levels chosen to avoid Bayer dithering threshold boundaries (±40 dither offset) # Thresholds at 64, 128, 192 - use values in the middle of each band for solid output # Safe zones: 0-23 (black), 88-103 (dark gray), 152-167 (light gray), 232-255 (white) levels = [ (0, "Level 0: BLACK"), (96, "Level 1: DARK GRAY"), (160, "Level 2: LIGHT GRAY"), (255, "Level 3: WHITE"), ] for i, (gray_value, label) in enumerate(levels): y = start_y + i * (square_size + gap + 22) x = (width - square_size) // 2 # Draw square with border draw.rectangle([x - 2, y - 2, x + square_size + 2, y + square_size + 2], fill=0) draw.rectangle([x, y, x + square_size, y + square_size], fill=gray_value) # Label below square bbox = draw.textbbox((0, 0), label, font=font_small) label_width = bbox[2] - bbox[0] draw.text( ((width - label_width) // 2, y + square_size + 5), label, font=font_small, fill=0, ) # Instructions at bottom (well below the last square) y = height - 70 draw_text_centered(draw, y, "PASS: 4 distinct shades visible", font_small, fill=0) draw_text_centered(draw, y + 20, "FAIL: Only black/white or", font_small, fill=64) draw_text_centered(draw, y + 38, "muddy/indistinct grays", font_small, fill=64) # Save if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_centering_test_image(filename, is_png=True): """ Create image with border markers to verify centering. """ width, height = 350, 400 img = Image.new("L", (width, height), 255) draw = ImageDraw.Draw(img) font = get_font(16) font_small = get_font(14) # Draw border draw.rectangle([0, 0, width - 1, height - 1], outline=0, width=3) # Corner markers marker_size = 20 for x, y in [ (0, 0), (width - marker_size, 0), (0, height - marker_size), (width - marker_size, height - marker_size), ]: draw.rectangle([x, y, x + marker_size, y + marker_size], fill=0) # Center cross cx, cy = width // 2, height // 2 draw.line([cx - 30, cy, cx + 30, cy], fill=0, width=2) draw.line([cx, cy - 30, cx, cy + 30], fill=0, width=2) # Title draw_text_centered(draw, 40, "CENTERING TEST", font, fill=0) # Instructions y = 80 draw_text_centered(draw, y, "Image should be centered", font_small, fill=0) draw_text_centered(draw, y + 20, "horizontally on screen", font_small, fill=0) y = 150 draw_text_centered(draw, y, "Check:", font_small, fill=0) draw_text_centered( draw, y + 25, "- Equal margins left & right", font_small, fill=64 ) draw_text_centered(draw, y + 45, "- All 4 corners visible", font_small, fill=64) draw_text_centered( draw, y + 65, "- Border is complete rectangle", font_small, fill=64 ) # Pass/fail y = height - 80 draw_text_centered( draw, y, "PASS: Centered, all corners visible", font_small, fill=0 ) draw_text_centered(draw, y + 20, "FAIL: Off-center or cropped", font_small, fill=64) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_scaling_test_image(filename, is_png=True): """ Create large image to verify scaling works. """ # Make image larger than screen but within decoder limits (max 2048x1536) width, height = 1200, 1500 img = Image.new("L", (width, height), 240) draw = ImageDraw.Draw(img) font = get_font(48) font_medium = get_font(32) font_small = get_font(24) # Border draw.rectangle([0, 0, width - 1, height - 1], outline=0, width=8) draw.rectangle([20, 20, width - 21, height - 21], outline=128, width=4) # Title draw_text_centered(draw, 60, "SCALING TEST", font, fill=0) draw_text_centered( draw, 130, f"Original: {width}x{height} (larger than screen)", font_medium, fill=64, ) # Grid pattern to verify scaling quality grid_start_y = 220 grid_size = 400 cell_size = 50 draw_text_centered( draw, grid_start_y - 40, "Grid pattern (check for artifacts):", font_small, fill=0, ) grid_x = (width - grid_size) // 2 for row in range(grid_size // cell_size): for col in range(grid_size // cell_size): x = grid_x + col * cell_size y = grid_start_y + row * cell_size if (row + col) % 2 == 0: draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0) else: draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200) # Size indicator bars y = grid_start_y + grid_size + 60 draw_text_centered( draw, y, "Width markers (should fit on screen):", font_small, fill=0 ) bar_y = y + 40 # Full width bar draw.rectangle([50, bar_y, width - 50, bar_y + 30], fill=0) draw.text((60, bar_y + 5), "FULL WIDTH", font=font_small, fill=255) # Half width bar bar_y += 60 half_start = width // 4 draw.rectangle([half_start, bar_y, width - half_start, bar_y + 30], fill=85) draw.text((half_start + 10, bar_y + 5), "HALF WIDTH", font=font_small, fill=255) # Instructions y = height - 350 draw_text_centered(draw, y, "VERIFICATION:", font_medium, fill=0) y += 50 instructions = [ "1. Image fits within screen bounds", "2. All borders visible (not cropped)", "3. Grid pattern clear (no moire)", "4. Text readable after scaling", "5. Aspect ratio preserved (not stretched)", ] for i, text in enumerate(instructions): draw_text_centered(draw, y + i * 35, text, font_small, fill=64) y = height - 100 draw_text_centered( draw, y, "PASS: Scaled down, readable, complete", font_small, fill=0 ) draw_text_centered( draw, y + 30, "FAIL: Cropped, distorted, or unreadable", font_small, fill=64 ) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_wide_scaling_test_image(filename, is_png=True): """ Create wide image (1807x736) to test scaling with specific dimensions that can trigger cache dimension mismatches due to floating-point rounding. """ width, height = 1807, 736 img = Image.new("L", (width, height), 240) draw = ImageDraw.Draw(img) font = get_font(48) font_medium = get_font(32) font_small = get_font(24) # Border draw.rectangle([0, 0, width - 1, height - 1], outline=0, width=6) draw.rectangle([15, 15, width - 16, height - 16], outline=128, width=3) # Title draw_text_centered(draw, 40, "WIDE SCALING TEST", font, fill=0) draw_text_centered( draw, 100, f"Original: {width}x{height} (tests rounding edge case)", font_medium, fill=64, ) # Grid pattern to verify scaling quality grid_start_x = 100 grid_start_y = 180 grid_width = 600 grid_height = 300 cell_size = 50 draw.text( (grid_start_x, grid_start_y - 35), "Grid pattern (check for artifacts):", font=font_small, fill=0, ) for row in range(grid_height // cell_size): for col in range(grid_width // cell_size): x = grid_start_x + col * cell_size y = grid_start_y + row * cell_size if (row + col) % 2 == 0: draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0) else: draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200) # Verification section on the right text_x = 800 text_y = 180 draw.text((text_x, text_y), "VERIFICATION:", font=font_medium, fill=0) text_y += 50 instructions = [ "1. Image fits within screen", "2. All borders visible", "3. Grid pattern clear", "4. Text readable", "5. No double-decode in log", ] for i, text in enumerate(instructions): draw.text((text_x, text_y + i * 35), text, font=font_small, fill=64) # Dimension info draw.text((text_x, 450), f"Dimensions: {width}x{height}", font=font_small, fill=0) draw.text((text_x, 485), "Tests cache dimension matching", font=font_small, fill=64) # Pass/fail at bottom y = height - 80 draw_text_centered( draw, y, "PASS: Single decode, cached correctly", font_small, fill=0 ) draw_text_centered( draw, y + 30, "FAIL: Cache mismatch, multiple decodes", font_small, fill=64 ) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_cache_test_image(filename, page_num, is_png=True): """ Create image for cache performance testing. """ width, height = 400, 300 img = Image.new("L", (width, height), 255) draw = ImageDraw.Draw(img) font = get_font(18) font_small = get_font(14) font_large = get_font(36) # Border draw.rectangle([0, 0, width - 1, height - 1], outline=0, width=2) # Page number prominent draw_text_centered(draw, 30, f"CACHE TEST PAGE {page_num}", font, fill=0) draw_text_centered(draw, 80, f"#{page_num}", font_large, fill=0) # Instructions y = 140 draw_text_centered(draw, y, "Navigate away then return", font_small, fill=64) draw_text_centered( draw, y + 25, "Second load should be faster", font_small, fill=64 ) y = 220 draw_text_centered(draw, y, "PASS: Faster reload from cache", font_small, fill=0) draw_text_centered( draw, y + 20, "FAIL: Same slow decode each time", font_small, fill=64 ) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_gradient_test_image(filename, is_png=True): """ Create horizontal gradient to test grayscale banding. """ width, height = 400, 500 img = Image.new("L", (width, height), 255) draw = ImageDraw.Draw(img) font = get_font(16) font_small = get_font(14) draw_text_centered(draw, 10, "GRADIENT TEST", font, fill=0) draw_text_centered( draw, 35, "Smooth gradient → 4 bands expected", font_small, fill=64 ) # Horizontal gradient gradient_y = 70 gradient_height = 100 for x in range(width): gray = int(255 * x / width) draw.line([(x, gradient_y), (x, gradient_y + gradient_height)], fill=gray) # Border around gradient draw.rectangle( [0, gradient_y - 1, width - 1, gradient_y + gradient_height + 1], outline=0, width=1, ) # Labels y = gradient_y + gradient_height + 10 draw.text((5, y), "BLACK", font=font_small, fill=0) draw.text((width - 50, y), "WHITE", font=font_small, fill=0) # 4-step gradient (what it should look like) y = 220 draw_text_centered( draw, y, "Expected result (4 distinct bands):", font_small, fill=0 ) band_y = y + 25 band_height = 60 band_width = width // 4 for i, gray in enumerate([0, 85, 170, 255]): x = i * band_width draw.rectangle([x, band_y, x + band_width, band_y + band_height], fill=gray) draw.rectangle( [0, band_y - 1, width - 1, band_y + band_height + 1], outline=0, width=1 ) # Vertical gradient y = 340 draw_text_centered(draw, y, "Vertical gradient:", font_small, fill=0) vgrad_y = y + 25 vgrad_height = 80 for row in range(vgrad_height): gray = int(255 * row / vgrad_height) draw.line([(50, vgrad_y + row), (width - 50, vgrad_y + row)], fill=gray) draw.rectangle( [49, vgrad_y - 1, width - 49, vgrad_y + vgrad_height + 1], outline=0, width=1 ) # Pass/fail y = height - 50 draw_text_centered(draw, y, "PASS: Clear 4-band quantization", font_small, fill=0) draw_text_centered( draw, y + 20, "FAIL: Binary/noisy dithering", font_small, fill=64 ) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_format_test_image(filename, format_name, is_png=True): """ Create simple image to verify format support. """ width, height = 350, 250 img = Image.new("L", (width, height), 255) draw = ImageDraw.Draw(img) font = get_font(20) font_large = get_font(36) font_small = get_font(14) # Border draw.rectangle([0, 0, width - 1, height - 1], outline=0, width=3) # Format name draw_text_centered(draw, 30, f"{format_name} FORMAT TEST", font, fill=0) draw_text_centered(draw, 80, format_name, font_large, fill=0) # Checkmark area y = 140 draw_text_centered(draw, y, "If you can read this,", font_small, fill=64) draw_text_centered( draw, y + 20, f"{format_name} decoding works!", font_small, fill=64 ) y = height - 40 draw_text_centered( draw, y, f"PASS: {format_name} image visible", font_small, fill=0 ) if is_png: img.save(filename, "PNG") else: img.save(filename, "JPEG", quality=95) def create_epub(epub_path, title, chapters): """ Create an EPUB file with the given chapters. chapters: list of (chapter_title, html_content, images) images: list of (image_filename, image_data) """ with zipfile.ZipFile(epub_path, "w", zipfile.ZIP_DEFLATED) as epub: # mimetype (must be first, uncompressed) epub.writestr( "mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED ) # Container container_xml = """ """ epub.writestr("META-INF/container.xml", container_xml) # Collect all images and chapters manifest_items = [] spine_items = [] written_images = set() # Add chapters and images for i, (chapter_title, html_content, images) in enumerate(chapters): chapter_id = f"chapter{i + 1}" chapter_file = f"chapter{i + 1}.xhtml" # Add images for this chapter for img_filename, img_data in images: if img_filename in written_images: continue media_type = ( "image/png" if img_filename.endswith(".png") else "image/jpeg" ) manifest_items.append( f' ' ) epub.writestr(f"OEBPS/images/{img_filename}", img_data) written_images.add(img_filename) # Add chapter manifest_items.append( f' ' ) spine_items.append(f' ') epub.writestr(f"OEBPS/{chapter_file}", html_content) # content.opf content_opf = f""" test-epub-{title.lower().replace(" ", "-")} {title} en {chr(10).join(manifest_items)} {chr(10).join(spine_items)} """ epub.writestr("OEBPS/content.opf", content_opf) # Navigation document nav_items = "\n".join( [ f'
  • {chapters[i][0]}
  • ' for i in range(len(chapters)) ] ) nav_xhtml = f""" Navigation """ epub.writestr("OEBPS/nav.xhtml", nav_xhtml) def make_chapter(title, body_content, head_content=""): """Create XHTML chapter content.""" return f""" {title}{head_content}

    {title}

    {body_content} """ def main(): OUTPUT_DIR.mkdir(exist_ok=True) # Temp directory for images import tempfile with tempfile.TemporaryDirectory() as tmpdir: tmpdir = Path(tmpdir) print("Generating test images...") # Generate all test images images = {} # JPEG tests create_grayscale_test_image(tmpdir / "grayscale_test.jpg", is_png=False) create_centering_test_image(tmpdir / "centering_test.jpg", is_png=False) create_scaling_test_image(tmpdir / "scaling_test.jpg", is_png=False) create_wide_scaling_test_image(tmpdir / "wide_scaling_test.jpg", is_png=False) create_gradient_test_image(tmpdir / "gradient_test.jpg", is_png=False) create_format_test_image(tmpdir / "jpeg_format.jpg", "JPEG", is_png=False) create_cache_test_image(tmpdir / "cache_test_1.jpg", 1, is_png=False) create_cache_test_image(tmpdir / "cache_test_2.jpg", 2, is_png=False) # PNG tests create_grayscale_test_image(tmpdir / "grayscale_test.png", is_png=True) create_centering_test_image(tmpdir / "centering_test.png", is_png=True) create_scaling_test_image(tmpdir / "scaling_test.png", is_png=True) create_wide_scaling_test_image(tmpdir / "wide_scaling_test.png", is_png=True) create_gradient_test_image(tmpdir / "gradient_test.png", is_png=True) create_format_test_image(tmpdir / "png_format.png", "PNG", is_png=True) create_cache_test_image(tmpdir / "cache_test_1.png", 1, is_png=True) create_cache_test_image(tmpdir / "cache_test_2.png", 2, is_png=True) # Read all images for img_file in tmpdir.glob("*.*"): images[img_file.name] = img_file.read_bytes() print("Creating JPEG test EPUB...") jpeg_chapters = [ ( "Introduction", make_chapter( "JPEG Image Tests", """

    This EPUB tests JPEG image rendering.

    Navigate through chapters to verify each test case.

    Test Plan:

    • Grayscale rendering (4 levels)
    • Image centering
    • Large image scaling
    • Cache performance
    """, ), [], ), ( "1. JPEG Format", make_chapter( "JPEG Format Test", """

    Basic JPEG decoding test.

    JPEG format test

    If the image above is visible, JPEG decoding works.

    """, ), [("jpeg_format.jpg", images["jpeg_format.jpg"])], ), ( "2. Grayscale", make_chapter( "Grayscale Test", """

    Verify 4 distinct gray levels are visible.

    Grayscale test """, ), [("grayscale_test.jpg", images["grayscale_test.jpg"])], ), ( "3. Gradient", make_chapter( "Gradient Test", """

    Verify gradient quantizes to 4 bands.

    Gradient test """, ), [("gradient_test.jpg", images["gradient_test.jpg"])], ), ( "4. Centering", make_chapter( "Centering Test", """

    Verify image is centered horizontally.

    Centering test """, ), [("centering_test.jpg", images["centering_test.jpg"])], ), ( "5. Scaling", make_chapter( "Scaling Test", """

    This image is 1200x1500 pixels - larger than the screen.

    It should be scaled down to fit.

    Scaling test """, ), [("scaling_test.jpg", images["scaling_test.jpg"])], ), ( "6. Wide Scaling", make_chapter( "Wide Scaling Test", """

    This image is 1807x736 pixels - a wide landscape format.

    Tests scaling with dimensions that can cause cache mismatches.

    Wide scaling test """, ), [("wide_scaling_test.jpg", images["wide_scaling_test.jpg"])], ), ( "7. Cache Test A", make_chapter( "Cache Test - Page A", """

    First cache test page. Note the load time.

    Cache test 1

    Navigate to next page, then come back.

    """, ), [("cache_test_1.jpg", images["cache_test_1.jpg"])], ), ( "8. Cache Test B", make_chapter( "Cache Test - Page B", """

    Second cache test page.

    Cache test 2

    Navigate back to Page A - it should load faster from cache.

    """, ), [("cache_test_2.jpg", images["cache_test_2.jpg"])], ), ( "9. Alignment Bleed", make_chapter( "Image Centering Bleed Test", """

    Tests that image centering does not bleed into following text blocks (issue #1026).

    Set Paragraph Alignment to Justify and Embedded Style to OFF before testing.

    All paragraphs below the images should be justified, not centered.

    Test image

    FIRST PARAGRAPH after image. This paragraph follows an empty heading and an image-only paragraph. With the bug present, this text appears centered instead of justified because the empty heading's default Center alignment bleeds through the chain of empty text blocks. Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor.

    SECOND PARAGRAPH in the same div. This paragraph should always be justified because the first paragraph's text block was flushed. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia.

    """, ), [], ), # centering_test.jpg already included by chapter 4 ] create_epub( OUTPUT_DIR / "test_jpeg_images.epub", "JPEG Image Tests", jpeg_chapters ) print("Creating PNG test EPUB...") png_chapters = [ ( "Introduction", make_chapter( "PNG Image Tests", """

    This EPUB tests PNG image rendering.

    Navigate through chapters to verify each test case.

    Test Plan:

    • PNG decoding (no crash)
    • Grayscale rendering (4 levels)
    • Image centering
    • Large image scaling
    """, ), [], ), ( "1. PNG Format", make_chapter( "PNG Format Test", """

    Basic PNG decoding test.

    PNG format test

    If the image above is visible and no crash occurred, PNG decoding works.

    """, ), [("png_format.png", images["png_format.png"])], ), ( "2. Grayscale", make_chapter( "Grayscale Test", """

    Verify 4 distinct gray levels are visible.

    Grayscale test """, ), [("grayscale_test.png", images["grayscale_test.png"])], ), ( "3. Gradient", make_chapter( "Gradient Test", """

    Verify gradient quantizes to 4 bands.

    Gradient test """, ), [("gradient_test.png", images["gradient_test.png"])], ), ( "4. Centering", make_chapter( "Centering Test", """

    Verify image is centered horizontally.

    Centering test """, ), [("centering_test.png", images["centering_test.png"])], ), ( "5. Scaling", make_chapter( "Scaling Test", """

    This image is 1200x1500 pixels - larger than the screen.

    It should be scaled down to fit.

    Scaling test """, ), [("scaling_test.png", images["scaling_test.png"])], ), ( "6. Wide Scaling", make_chapter( "Wide Scaling Test", """

    This image is 1807x736 pixels - a wide landscape format.

    Tests scaling with dimensions that can cause cache mismatches.

    Wide scaling test """, ), [("wide_scaling_test.png", images["wide_scaling_test.png"])], ), ( "7. Cache Test A", make_chapter( "Cache Test - Page A", """

    First cache test page. Note the load time.

    Cache test 1

    Navigate to next page, then come back.

    """, ), [("cache_test_1.png", images["cache_test_1.png"])], ), ( "8. Cache Test B", make_chapter( "Cache Test - Page B", """

    Second cache test page.

    Cache test 2

    Navigate back to Page A - it should load faster from cache.

    """, ), [("cache_test_2.png", images["cache_test_2.png"])], ), ( "9. Alignment Bleed", make_chapter( "Image Centering Bleed Test", """

    Tests that image centering does not bleed into following text blocks (issue #1026).

    Set Paragraph Alignment to Justify and Embedded Style to OFF before testing.

    All paragraphs below the images should be justified, not centered.

    Test image

    FIRST PARAGRAPH after image. This paragraph follows an empty heading and an image-only paragraph. With the bug present, this text appears centered instead of justified because the empty heading's default Center alignment bleeds through the chain of empty text blocks. Lorem ipsum dolor sit amet, consectetur adipiscing elit sed do eiusmod tempor.

    SECOND PARAGRAPH in the same div. This paragraph should always be justified because the first paragraph's text block was flushed. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident sunt in culpa qui officia.

    """, ), [], ), # centering_test.png already included by chapter 4 ] create_epub( OUTPUT_DIR / "test_png_images.epub", "PNG Image Tests", png_chapters ) print("Creating mixed format test EPUB...") mixed_chapters = [ ( "Introduction", make_chapter( "Mixed Image Format Tests", """

    This EPUB contains both JPEG and PNG images.

    Tests format detection and mixed rendering.

    """, ), [], ), ( "1. JPEG Image", make_chapter( "JPEG in Mixed EPUB", """

    This is a JPEG image:

    JPEG """, ), [("jpeg_format.jpg", images["jpeg_format.jpg"])], ), ( "2. PNG Image", make_chapter( "PNG in Mixed EPUB", """

    This is a PNG image:

    PNG """, ), [("png_format.png", images["png_format.png"])], ), ( "3. Both Formats", make_chapter( "Both Formats on One Page", """

    JPEG image:

    JPEG grayscale

    PNG image:

    PNG grayscale

    Both should render with proper grayscale.

    """, ), [ ("grayscale_test.jpg", images["grayscale_test.jpg"]), ("grayscale_test.png", images["grayscale_test.png"]), ], ), ] create_epub( OUTPUT_DIR / "test_mixed_images.epub", "Mixed Format Tests", mixed_chapters ) print("Creating layout regression test EPUB...") layout_css = """ """ layout_chapters = [ ( "Introduction", make_chapter( "Layout Regression Tests", """

    This EPUB exercises recent parser edge cases around nested block styles and image wrappers.

    Recommended settings: Embedded Style ON, Paragraph Alignment set to Book Style or Justify.

    • Nested horizontal margin inheritance for sibling blocks
    • Vertical paragraph spacing should not explode with nested wrappers
    • Image wrapper spacing should apply to the image, not leak into following text
    • Hidden images should not leave a large blank gap before the next paragraph
    """, ), [], ), ( "1. Nested Horizontal Margins", make_chapter( "Nested Horizontal Margin Inheritance", """

    This chapter mirrors the c1/c2/c3/c4 example behind PR 1582.

    Expected: the first paragraph is visibly more indented than the second, but both still inherit the outer wrapper indentation.

    C3 paragraph. This text should have the largest left indent because it inherits c1, c2, and c3. Repeat text to make the paragraph wrap across multiple lines and make the effective left inset obvious while reading.

    C4 paragraph. This text should still inherit the outer c1 indent, but not the inner c2 indent. It should therefore appear less indented than the paragraph above, not flush with the body text.

    Reference paragraph outside the wrappers. This paragraph should align with the normal body text and helps compare the wrapper indentation against the baseline.

    """, head_content=layout_css, ), [], ), ( "2. Nested Vertical Margins", make_chapter( "Nested Vertical Margin Sanity", """

    Expected: wrapper nesting should not create an oversized blank vertical gulf between these paragraphs.

    Nested vertical spacing paragraph. There should be some breathing room above and below, but not dramatically more than a normal section break.

    Sibling paragraph after the nested block. Spacing before this paragraph should feel normal and should not keep growing with every ancestor wrapper.

    Baseline paragraph after the wrapper section. This should not be pushed far down the page.

    """, head_content=layout_css, ), [], ), ( "3. Image Wrapper Spacing", make_chapter( "Image Wrapper Spacing", """

    Expected: the wrapper's margins should create space around the image, and the paragraph after the image should start with normal spacing rather than inheriting a second copy of that gap.

    Wrapped image spacing test

    Paragraph after wrapped image. If the wrapper spacing leaks, this paragraph will begin too far down the page. If container width is ignored, the image may also appear too wide for the wrapper.

    """, head_content=layout_css, ), [("centering_test.jpg", images["centering_test.jpg"])], ), ( "4. Hidden Image Spacing", make_chapter( "Hidden Image Spacing Reset", """

    Expected: the hidden image wrapper should not leave a large blank gap before the following paragraph.

    This image is intentionally hidden by CSS

    Paragraph after hidden image. This should follow with near-normal spacing, not the large gap that would be appropriate for a visible wrapped image.

    """, head_content=layout_css, ), [("centering_test.jpg", images["centering_test.jpg"])], ), ] create_epub( OUTPUT_DIR / "test_layout_regressions.epub", "Layout Regression Tests", layout_chapters, ) print("Creating text rendering test EPUB...") text_chapters = [ ( "Introduction", make_chapter( "Text Rendering Tests", """

    This EPUB tests text rendering edge cases.

    Test Plan:

    • pre element: intrinsic line breaks preserved
    • pre element: leading/trailing blank lines
    • pre with inline code element
    """, ), [], ), ( "1. pre Line Breaks", make_chapter( "pre: Intrinsic Line Breaks", """

    The block below is a single <pre> element. Each source line must appear on its own line:

    Line one
    Line two
    Line three

    Normal paragraph after the pre block. Text resumes as a regular wrapped paragraph.

    """, ), [], ), ( "2. pre Blank Lines", make_chapter( "pre: Blank Lines", """

    The pre block below contains intentional blank lines between code lines:

    First line
    
    Third line (blank line above)
    
    Fifth line (blank line above)

    Each blank line in the source should produce an empty line in the output.

    """, ), [], ), ( "3. pre with code", make_chapter( "pre with Nested code Element", """

    Common EPUB pattern: <pre><code>...</code></pre>. Line breaks must still be preserved:

    function greet(name) {
        return "Hello, " + name;
    }
    
    greet("World");

    Normal paragraph after pre/code block.

    """, ), [], ), ] create_epub( OUTPUT_DIR / "test_text_rendering.epub", "Text Rendering Tests", text_chapters, ) print(f"\nTest EPUBs created in: {OUTPUT_DIR}") print("Files:") for f in OUTPUT_DIR.glob("*.epub"): print(f" - {f.name}") if __name__ == "__main__": main()