#!/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 = '''
This EPUB tests JPEG image rendering.
Navigate through chapters to verify each test case.
Test Plan:
Basic JPEG decoding 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.jpg', images['grayscale_test.jpg'])]),
("3. Gradient", make_chapter("Gradient Test", """
Verify gradient quantizes to 4 bands.
"""), [('gradient_test.jpg', images['gradient_test.jpg'])]),
("4. Centering", make_chapter("Centering Test", """
Verify image is centered horizontally.
"""), [('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.jpg', images['scaling_test.jpg'])]),
("6. Cache Test A", make_chapter("Cache Test - Page A", """
First cache test page. Note the load time.
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.
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:
Basic PNG decoding 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.png', images['grayscale_test.png'])]),
("3. Gradient", make_chapter("Gradient Test", """
Verify gradient quantizes to 4 bands.
"""), [('gradient_test.png', images['gradient_test.png'])]),
("4. Centering", make_chapter("Centering Test", """
Verify image is centered horizontally.
"""), [('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.png', images['scaling_test.png'])]),
("6. Cache Test A", make_chapter("Cache Test - Page A", """
First cache test page. Note the load time.
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.
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_format.jpg', images['jpeg_format.jpg'])]),
("2. PNG Image", make_chapter("PNG in Mixed EPUB", """
This is a PNG image:
"""), [('png_format.png', images['png_format.png'])]),
("3. Both Formats", make_chapter("Both Formats on One Page", """
JPEG image:
PNG image:
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()