#!/usr/bin/env python3 """ Generate test EPUBs for image rendering verification. Creates EPUBs with annotated JPEG and PNG images to verify: - Grayscale rendering (4 levels) - Image scaling - Image centering - Cache performance - Page serialization """ 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.""" try: return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", size) except: try: return ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSans.ttf", size) except: 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_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 = [] # 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: 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) # 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): """Create XHTML chapter content.""" return f''' {title}

    {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_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_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. 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'])]), ("7. 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'])]), ] 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. 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'])]), ("7. 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'])]), ] 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(f"\nTest EPUBs created in: {OUTPUT_DIR}") print("Files:") for f in OUTPUT_DIR.glob('*.epub'): print(f" - {f.name}") if __name__ == '__main__': main()