pyPhotoAlbum/tests/test_pdf_export.py
Duncan Tourolle 73c504abb7
Some checks failed
Python CI / test (push) Has been cancelled
Lint / lint (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.9) (push) Has been cancelled
fixed bug in pdf export
fixed bug in asset loading
2025-11-26 22:59:42 +01:00

1022 lines
36 KiB
Python
Executable File

"""
Tests for PDF export functionality
"""
import os
import tempfile
from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData, TextBoxData
from pyPhotoAlbum.pdf_exporter import PDFExporter
def test_pdf_exporter_basic():
"""Test basic PDF export with single page"""
# Create a simple project
project = Project("Test Project")
project.page_size_mm = (210, 297) # A4
# Add a single page
page = Page(page_number=1, is_double_spread=False)
project.add_page(page)
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(tmp_path), "PDF file was not created"
assert os.path.getsize(tmp_path) > 0, "PDF file is empty"
print(f"✓ Basic PDF export successful: {tmp_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_exporter_double_spread():
"""Test PDF export with double-page spread"""
project = Project("Test Spread Project")
project.page_size_mm = (210, 297) # A4
# Add a double-page spread
spread_page = Page(page_number=1, is_double_spread=True)
project.add_page(spread_page)
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(tmp_path), "PDF file was not created"
print(f"✓ Double-spread PDF export successful: {tmp_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_exporter_with_text():
"""Test PDF export with text boxes"""
project = Project("Test Text Project")
project.page_size_mm = (210, 297)
# Create page with text box
page = Page(page_number=1, is_double_spread=False)
# Add a text box
text_box = TextBoxData(
text_content="Hello, World!",
font_settings={"family": "Helvetica", "size": 24, "color": (0, 0, 0)},
alignment="center",
x=50, y=50, width=100, height=30
)
page.layout.add_element(text_box)
project.add_page(page)
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(tmp_path), "PDF file was not created"
print(f"✓ Text box PDF export successful: {tmp_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_text_position_and_size():
"""
Test that text in PDF is correctly positioned and sized relative to its text box.
This test verifies:
1. Font size is properly scaled (not used directly as PDF points)
2. Text is positioned inside the text box (not above it)
3. Text respects the top-alignment used in the UI
"""
import pdfplumber
project = Project("Test Text Position")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create page with text box
page = Page(page_number=1, is_double_spread=False)
# Create a text box with specific dimensions in pixels (at 96 DPI)
# Text box: 200px wide x 100px tall, positioned at (100, 100)
# Font size: 48 pixels (stored in same units as element size)
text_box_x_px = 100
text_box_y_px = 100
text_box_width_px = 200
text_box_height_px = 100
font_size_px = 48 # Font size in same pixel units as element
text_box = TextBoxData(
text_content="Test",
font_settings={"family": "Helvetica", "size": font_size_px, "color": (0, 0, 0)},
alignment="left",
x=text_box_x_px,
y=text_box_y_px,
width=text_box_width_px,
height=text_box_height_px
)
page.layout.add_element(text_box)
project.add_page(page)
# Calculate expected PDF values
MM_TO_POINTS = 2.834645669
dpi = project.working_dpi
page_height_pt = 297 * MM_TO_POINTS # ~842 points
# Convert text box dimensions to points
text_box_x_mm = text_box_x_px * 25.4 / dpi
text_box_y_mm = text_box_y_px * 25.4 / dpi
text_box_width_mm = text_box_width_px * 25.4 / dpi
text_box_height_mm = text_box_height_px * 25.4 / dpi
text_box_x_pt = text_box_x_mm * MM_TO_POINTS
text_box_y_pt_bottom = page_height_pt - (text_box_y_mm * MM_TO_POINTS) - (text_box_height_mm * MM_TO_POINTS)
text_box_y_pt_top = text_box_y_pt_bottom + (text_box_height_mm * MM_TO_POINTS)
text_box_height_pt = text_box_height_mm * MM_TO_POINTS
# Font size should also be converted from pixels to points
expected_font_size_pt = font_size_px * 25.4 / dpi * MM_TO_POINTS
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
# Extract text position from PDF
with pdfplumber.open(tmp_path) as pdf:
page_pdf = pdf.pages[0]
chars = page_pdf.chars
assert len(chars) > 0, "No text found in PDF"
# Get the first character's position and font size
first_char = chars[0]
text_x = first_char['x0']
text_y_baseline = first_char['y0'] # This is the baseline y position
actual_font_size = first_char['size']
print(f"\nText Position Analysis:")
print(f" Text box (in pixels at {dpi} DPI): x={text_box_x_px}, y={text_box_y_px}, "
f"w={text_box_width_px}, h={text_box_height_px}")
print(f" Text box (in PDF points): x={text_box_x_pt:.1f}, "
f"y_bottom={text_box_y_pt_bottom:.1f}, y_top={text_box_y_pt_top:.1f}, "
f"height={text_box_height_pt:.1f}")
print(f" Font size (pixels): {font_size_px}")
print(f" Expected font size (points): {expected_font_size_pt:.1f}")
print(f" Actual font size (points): {actual_font_size:.1f}")
print(f" Actual text x: {text_x:.1f}")
print(f" Actual text y (baseline): {text_y_baseline:.1f}")
# Verify font size is scaled correctly (tolerance of 1pt)
font_size_diff = abs(actual_font_size - expected_font_size_pt)
assert font_size_diff < 2.0, (
f"Font size mismatch: expected ~{expected_font_size_pt:.1f}pt, "
f"got {actual_font_size:.1f}pt (diff: {font_size_diff:.1f}pt). "
f"Font size should be converted from pixels to points."
)
# Verify text X position is near the left edge of the text box
x_diff = abs(text_x - text_box_x_pt)
assert x_diff < 5.0, (
f"Text X position mismatch: expected ~{text_box_x_pt:.1f}, "
f"got {text_x:.1f} (diff: {x_diff:.1f}pt)"
)
# Verify text Y baseline is INSIDE the text box (not above it)
# For top-aligned text, baseline should be within the box bounds
# pdfplumber y-coordinates use PDF coordinate system: origin at bottom-left, y increases upward
# So y0 is already the y-coordinate from the bottom of the page
text_y_from_bottom = text_y_baseline
# Text baseline should be between box bottom and box top
# Allow some margin for ascender/descender
margin = actual_font_size * 0.3 # 30% margin for font metrics
assert text_y_from_bottom >= text_box_y_pt_bottom - margin, (
f"Text is below the text box! "
f"Text baseline y={text_y_from_bottom:.1f}, box bottom={text_box_y_pt_bottom:.1f}"
)
assert text_y_from_bottom <= text_box_y_pt_top + margin, (
f"Text baseline is above the text box! "
f"Text baseline y={text_y_from_bottom:.1f}, box top={text_box_y_pt_top:.1f}. "
f"Text should be positioned inside the box, not above it."
)
print(f" Text y (from bottom): {text_y_from_bottom:.1f}")
print(f" Text is inside box bounds: ✓")
print(f"\n✓ Text position and size test passed!")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_text_wrapping():
"""
Test that text wraps correctly within the text box boundaries.
This test verifies:
1. Long text is word-wrapped to fit within the box width
2. Multiple lines are rendered correctly
3. Text stays within the box boundaries
"""
import pdfplumber
project = Project("Test Text Wrapping")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create page with text box
page = Page(page_number=1, is_double_spread=False)
# Create a text box with long text that should wrap
text_box_x_px = 100
text_box_y_px = 100
text_box_width_px = 200 # Narrow box to force wrapping
text_box_height_px = 200 # Tall enough for multiple lines
font_size_px = 24
long_text = "This is a long piece of text that should wrap to multiple lines within the text box boundaries."
text_box = TextBoxData(
text_content=long_text,
font_settings={"family": "Helvetica", "size": font_size_px, "color": (0, 0, 0)},
alignment="left",
x=text_box_x_px,
y=text_box_y_px,
width=text_box_width_px,
height=text_box_height_px
)
page.layout.add_element(text_box)
project.add_page(page)
# Calculate box boundaries in PDF points
MM_TO_POINTS = 2.834645669
dpi = project.working_dpi
text_box_x_mm = text_box_x_px * 25.4 / dpi
text_box_width_mm = text_box_width_px * 25.4 / dpi
text_box_x_pt = text_box_x_mm * MM_TO_POINTS
text_box_width_pt = text_box_width_mm * MM_TO_POINTS
text_box_right_pt = text_box_x_pt + text_box_width_pt
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
# Extract text from PDF
with pdfplumber.open(tmp_path) as pdf:
page_pdf = pdf.pages[0]
chars = page_pdf.chars
assert len(chars) > 0, "No text found in PDF"
# Get all unique Y positions (lines)
y_positions = sorted(set(round(c['top'], 1) for c in chars))
print(f"\nText Wrapping Analysis:")
print(f" Text box width: {text_box_width_pt:.1f}pt")
print(f" Text box x: {text_box_x_pt:.1f}pt to {text_box_right_pt:.1f}pt")
print(f" Number of lines: {len(y_positions)}")
print(f" Line Y positions: {y_positions[:5]}...") # Show first 5
# Verify text wrapped to multiple lines
assert len(y_positions) > 1, (
f"Text should wrap to multiple lines but only found {len(y_positions)} line(s)"
)
# Verify all characters are within box width (with small tolerance)
tolerance = 5.0 # Small tolerance for rounding
for char in chars:
char_x = char['x0']
char_right = char['x1']
assert char_x >= text_box_x_pt - tolerance, (
f"Character '{char['text']}' at x={char_x:.1f} is left of box start {text_box_x_pt:.1f}"
)
assert char_right <= text_box_right_pt + tolerance, (
f"Character '{char['text']}' ends at x={char_right:.1f} which exceeds box right {text_box_right_pt:.1f}"
)
print(f" All characters within box width: ✓")
print(f"\n✓ Text wrapping test passed!")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_exporter_facing_pages_alignment():
"""Test that double spreads align to facing pages"""
project = Project("Test Facing Pages")
project.page_size_mm = (210, 297)
# Add single page (page 1)
page1 = Page(page_number=1, is_double_spread=False)
project.add_page(page1)
# Add double spread (should start on page 2, which requires blank insert)
# Since page 1 is odd, a blank page should be inserted, making the spread pages 2-3
spread = Page(page_number=2, is_double_spread=True)
project.add_page(spread)
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(tmp_path), "PDF file was not created"
print(f"✓ Facing pages alignment successful: {tmp_path}")
print(f" Expected: Page 1 (single), blank page, Pages 2-3 (spread)")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_exporter_missing_image():
"""Test PDF export with missing image (should warn but not fail)"""
project = Project("Test Missing Image")
project.page_size_mm = (210, 297)
# Create page with image that doesn't exist
page = Page(page_number=1, is_double_spread=False)
# Add image with non-existent path
image = ImageData(
image_path="/nonexistent/path/to/image.jpg",
x=50, y=50, width=100, height=100
)
page.layout.add_element(image)
project.add_page(page)
# Export to temporary file
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
tmp_path = tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(tmp_path)
assert success, "Export should succeed even with missing images"
assert len(warnings) > 0, "Should have warnings for missing image"
assert "not found" in warnings[0].lower(), "Warning should mention missing image"
print(f"✓ Missing image handling successful: {tmp_path}")
print(f" Warnings: {warnings}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_pdf_exporter_spanning_image():
"""Test PDF export with image spanning across center line of double spread"""
import tempfile
from PIL import Image as PILImage
project = Project("Test Spanning Image")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96 # Standard DPI
# Create a test image (solid color for easy verification)
test_img = PILImage.new('RGB', (400, 200), color='red')
# Save test image to temporary file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
test_img.save(img_path)
try:
# Create a double-page spread
spread_page = Page(page_number=1, is_double_spread=True)
# Calculate center position in pixels (for a 210mm page width at 96 DPI)
# Spread width is 2 * 210mm = 420mm
spread_width_px = 420 * 96 / 25.4 # ~1587 pixels
center_px = spread_width_px / 2 # ~794 pixels
# Add an image that spans across the center
# Position it so it overlaps the center line
image_width_px = 400
image_x_px = center_px - 200 # Start 200px before center, end 200px after
spanning_image = ImageData(
image_path=img_path,
x=image_x_px,
y=100,
width=image_width_px,
height=200
)
spread_page.layout.add_element(spanning_image)
project.add_page(spread_page)
# Export to temporary PDF
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(pdf_path), "PDF file was not created"
print(f"✓ Spanning image export successful: {pdf_path}")
print(f" Image spans from {image_x_px:.1f}px to {image_x_px + image_width_px:.1f}px")
print(f" Center line at {center_px:.1f}px")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
finally:
if os.path.exists(img_path):
os.remove(img_path)
def test_pdf_exporter_multiple_spanning_elements():
"""Test PDF export with multiple images spanning the center line"""
import tempfile
from PIL import Image as PILImage
project = Project("Test Multiple Spanning")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create test images
test_img1 = PILImage.new('RGB', (300, 150), color='blue')
test_img2 = PILImage.new('RGB', (250, 200), color='green')
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp1:
img_path1 = img_tmp1.name
test_img1.save(img_path1)
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp2:
img_path2 = img_tmp2.name
test_img2.save(img_path2)
try:
spread_page = Page(page_number=1, is_double_spread=True)
# Calculate positions
spread_width_px = 420 * 96 / 25.4
center_px = spread_width_px / 2
# First spanning image
image1 = ImageData(
image_path=img_path1,
x=center_px - 150, # Centered on split line
y=50,
width=300,
height=150
)
# Second spanning image (different position)
image2 = ImageData(
image_path=img_path2,
x=center_px - 100,
y=250,
width=250,
height=200
)
spread_page.layout.add_element(image1)
spread_page.layout.add_element(image2)
project.add_page(spread_page)
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(pdf_path), "PDF file was not created"
print(f"✓ Multiple spanning images export successful: {pdf_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
finally:
if os.path.exists(img_path1):
os.remove(img_path1)
if os.path.exists(img_path2):
os.remove(img_path2)
def test_pdf_exporter_edge_case_barely_spanning():
"""Test image that barely crosses the threshold"""
import tempfile
from PIL import Image as PILImage
project = Project("Test Edge Case")
project.page_size_mm = (210, 297)
project.working_dpi = 96
test_img = PILImage.new('RGB', (100, 100), color='yellow')
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
test_img.save(img_path)
try:
spread_page = Page(page_number=1, is_double_spread=True)
spread_width_px = 420 * 96 / 25.4
center_px = spread_width_px / 2
# Image that just barely crosses the center line
image = ImageData(
image_path=img_path,
x=center_px - 5, # Just 5px overlap
y=100,
width=100,
height=100
)
spread_page.layout.add_element(image)
project.add_page(spread_page)
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
print(f"✓ Edge case (barely spanning) export successful: {pdf_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
finally:
if os.path.exists(img_path):
os.remove(img_path)
def test_pdf_exporter_text_spanning():
"""Test text box spanning the center line"""
project = Project("Test Spanning Text")
project.page_size_mm = (210, 297)
project.working_dpi = 96
spread_page = Page(page_number=1, is_double_spread=True)
spread_width_px = 420 * 96 / 25.4
center_px = spread_width_px / 2
# Text box spanning the center
text_box = TextBoxData(
text_content="Spanning Text",
font_settings={"family": "Helvetica", "size": 24, "color": (0, 0, 0)},
alignment="center",
x=center_px - 100,
y=100,
width=200,
height=50
)
spread_page.layout.add_element(text_box)
project.add_page(spread_page)
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
print(f"✓ Spanning text box export successful: {pdf_path}")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
def test_pdf_exporter_spanning_image_aspect_ratio():
"""Test that spanning images maintain correct aspect ratio and can be recombined"""
import tempfile
from PIL import Image as PILImage, ImageDraw
project = Project("Test Aspect Ratio")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create a distinctive test image: red left half, blue right half, with a vertical line in center
test_width, test_height = 800, 400
test_img = PILImage.new('RGB', (test_width, test_height))
draw = ImageDraw.Draw(test_img)
# Fill left half red
draw.rectangle([0, 0, test_width // 2, test_height], fill=(255, 0, 0))
# Fill right half blue
draw.rectangle([test_width // 2, 0, test_width, test_height], fill=(0, 0, 255))
# Draw a black vertical line in the middle
draw.line([test_width // 2, 0, test_width // 2, test_height], fill=(0, 0, 0), width=5)
# Draw horizontal reference lines for visual verification
for y in range(0, test_height, 50):
draw.line([0, y, test_width, y], fill=(255, 255, 255), width=2)
# Save test image to temporary file
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
test_img.save(img_path)
try:
# Create a double-page spread
spread_page = Page(page_number=1, is_double_spread=True)
# Calculate positions
spread_width_px = 420 * 96 / 25.4 # ~1587 pixels
center_px = spread_width_px / 2 # ~794 pixels
# Create an image element that spans the center with a specific aspect ratio
# Make it 600px wide and 300px tall (2:1 aspect ratio)
image_width_px = 600
image_height_px = 300
image_x_px = center_px - 300 # Centered on the split line
spanning_image = ImageData(
image_path=img_path,
x=image_x_px,
y=100,
width=image_width_px,
height=image_height_px
)
spread_page.layout.add_element(spanning_image)
project.add_page(spread_page)
# Export to temporary PDF
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(pdf_path), "PDF file was not created"
# Verify the PDF was created and has expected properties
# We can't easily extract and verify pixel-perfect image reconstruction without
# additional dependencies, but we can verify the export succeeded
file_size = os.path.getsize(pdf_path)
assert file_size > 1000, "PDF file seems too small"
print(f"✓ Spanning image aspect ratio test successful: {pdf_path}")
print(f" Original image: {test_width}x{test_height} (aspect {test_width/test_height:.2f}:1)")
print(f" Element size: {image_width_px}x{image_height_px} (aspect {image_width_px/image_height_px:.2f}:1)")
print(f" Split at: {center_px:.1f}px")
print(f" Left portion: {center_px - image_x_px:.1f}px wide")
print(f" Right portion: {image_width_px - (center_px - image_x_px):.1f}px wide")
print(f" PDF size: {file_size} bytes")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
finally:
if os.path.exists(img_path):
os.remove(img_path)
def test_pdf_exporter_varying_aspect_ratios():
"""Test spanning images with various aspect ratios"""
import tempfile
from PIL import Image as PILImage, ImageDraw
project = Project("Test Varying Aspects")
project.page_size_mm = (210, 297)
project.working_dpi = 96
# Test different aspect ratios
test_configs = [
("Square", 400, 400), # 1:1
("Landscape", 800, 400), # 2:1
("Portrait", 400, 800), # 1:2
("Wide", 1200, 400), # 3:1
]
spread_width_px = 420 * 96 / 25.4
center_px = spread_width_px / 2
for idx, (name, img_w, img_h) in enumerate(test_configs):
# Create test image
test_img = PILImage.new('RGB', (img_w, img_h))
draw = ImageDraw.Draw(test_img)
# Different colors for each test
colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
draw.rectangle([0, 0, img_w // 2, img_h], fill=colors[idx])
draw.rectangle([img_w // 2, 0, img_w, img_h], fill=(255-colors[idx][0], 255-colors[idx][1], 255-colors[idx][2]))
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
test_img.save(img_path)
try:
spread_page = Page(page_number=idx + 1, is_double_spread=True)
# Position spanning element
element_width_px = 500
element_height_px = int(500 * img_h / img_w) # Maintain aspect ratio
spanning_image = ImageData(
image_path=img_path,
x=center_px - 250,
y=100 + idx * 200,
width=element_width_px,
height=element_height_px
)
spread_page.layout.add_element(spanning_image)
project.add_page(spread_page)
finally:
if os.path.exists(img_path):
os.remove(img_path)
# Export all pages
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(pdf_path), "PDF file was not created"
print(f"✓ Varying aspect ratios test successful: {pdf_path}")
print(f" Tested {len(test_configs)} different aspect ratios")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
def test_pdf_exporter_rotated_image():
"""Test that PIL rotation is applied when exporting to PDF"""
import tempfile
from PIL import Image as PILImage, ImageDraw
project = Project("Test Rotated Image")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create a distinctive test image that shows rotation clearly
# Make it wider than tall (400x200) so we can verify rotation
test_img = PILImage.new('RGB', (400, 200), color='white')
draw = ImageDraw.Draw(test_img)
# Draw a pattern that shows orientation
# Red bar at top
draw.rectangle([0, 0, 400, 50], fill=(255, 0, 0))
# Blue bar at bottom
draw.rectangle([0, 150, 400, 200], fill=(0, 0, 255))
# Green vertical stripe on left
draw.rectangle([0, 0, 50, 200], fill=(0, 255, 0))
# Yellow vertical stripe on right
draw.rectangle([350, 0, 400, 200], fill=(255, 255, 0))
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
test_img.save(img_path)
try:
# Create a page
page = Page(page_number=1, is_double_spread=False)
# Add image with 90-degree PIL rotation
image = ImageData(
image_path=img_path,
x=50,
y=50,
width=200, # These dimensions are for the rotated version
height=400
)
image.pil_rotation_90 = 1 # 90 degree rotation
image.image_dimensions = (400, 200) # Original dimensions before rotation
page.layout.add_element(image)
project.add_page(page)
# Export to PDF
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp:
pdf_path = pdf_tmp.name
try:
exporter = PDFExporter(project)
success, warnings = exporter.export(pdf_path)
assert success, f"Export failed: {warnings}"
assert os.path.exists(pdf_path), "PDF file was not created"
print(f"✓ Rotated image export successful: {pdf_path}")
print(f" Original image: 400x200 (landscape)")
print(f" PIL rotation: 90 degrees")
print(f" Expected in PDF: rotated image (portrait orientation)")
if warnings:
print(f" Warnings: {warnings}")
finally:
if os.path.exists(pdf_path):
os.remove(pdf_path)
finally:
if os.path.exists(img_path):
os.remove(img_path)
def test_pdf_exporter_image_downsampling():
"""Test that export DPI controls image downsampling and reduces file size"""
import tempfile
from PIL import Image as PILImage
project = Project("Test Downsampling")
project.page_size_mm = (210, 297) # A4
project.working_dpi = 96
# Create a large test image (4000x3000 - typical high-res camera)
large_img = PILImage.new('RGB', (4000, 3000))
# Add some pattern so it doesn't compress too much
import random
pixels = large_img.load()
for i in range(0, 4000, 10):
for j in range(0, 3000, 10):
pixels[i, j] = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as img_tmp:
img_path = img_tmp.name
large_img.save(img_path)
try:
# Create a page with the large image
page = Page(page_number=1, is_double_spread=False)
# Add image at reasonable size (100mm x 75mm)
image = ImageData(
image_path=img_path,
x=50,
y=50,
width=int(100 * 96 / 25.4), # ~378 px
height=int(75 * 96 / 25.4) # ~283 px
)
page.layout.add_element(image)
project.add_page(page)
# Export with high DPI (300 - print quality)
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp1:
pdf_path_300dpi = pdf_tmp1.name
# Export with low DPI (150 - screen quality)
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as pdf_tmp2:
pdf_path_150dpi = pdf_tmp2.name
try:
# Export at 300 DPI
exporter_300 = PDFExporter(project, export_dpi=300)
success1, warnings1 = exporter_300.export(pdf_path_300dpi)
assert success1, f"300 DPI export failed: {warnings1}"
# Export at 150 DPI
exporter_150 = PDFExporter(project, export_dpi=150)
success2, warnings2 = exporter_150.export(pdf_path_150dpi)
assert success2, f"150 DPI export failed: {warnings2}"
# Check file sizes
size_300dpi = os.path.getsize(pdf_path_300dpi)
size_150dpi = os.path.getsize(pdf_path_150dpi)
print(f"✓ Image downsampling test successful:")
print(f" Original image: 4000x3000 pixels")
print(f" Element size: 100mm x 75mm")
print(f" PDF at 300 DPI: {size_300dpi:,} bytes")
print(f" PDF at 150 DPI: {size_150dpi:,} bytes")
print(f" Size reduction: {(1 - size_150dpi/size_300dpi)*100:.1f}%")
# 150 DPI should be smaller than 300 DPI
assert size_150dpi < size_300dpi, \
f"150 DPI file ({size_150dpi}) should be smaller than 300 DPI file ({size_300dpi})"
# 150 DPI should be significantly smaller (at least 50% reduction)
reduction_ratio = size_150dpi / size_300dpi
assert reduction_ratio < 0.7, \
f"150 DPI should be at least 30% smaller, got {(1-reduction_ratio)*100:.1f}%"
finally:
if os.path.exists(pdf_path_300dpi):
os.remove(pdf_path_300dpi)
if os.path.exists(pdf_path_150dpi):
os.remove(pdf_path_150dpi)
finally:
if os.path.exists(img_path):
os.remove(img_path)
if __name__ == "__main__":
print("Running PDF export tests...\n")
try:
test_pdf_exporter_basic()
test_pdf_exporter_double_spread()
test_pdf_exporter_with_text()
test_pdf_text_position_and_size()
test_pdf_text_wrapping()
test_pdf_exporter_facing_pages_alignment()
test_pdf_exporter_missing_image()
test_pdf_exporter_spanning_image()
test_pdf_exporter_multiple_spanning_elements()
test_pdf_exporter_edge_case_barely_spanning()
test_pdf_exporter_text_spanning()
test_pdf_exporter_spanning_image_aspect_ratio()
test_pdf_exporter_varying_aspect_ratios()
test_pdf_exporter_rotated_image()
test_pdf_exporter_image_downsampling()
print("\n✓ All tests passed!")
except AssertionError as e:
print(f"\n✗ Test failed: {e}")
raise
except Exception as e:
print(f"\n✗ Unexpected error: {e}")
raise