""" 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