All checks were successful
Python CI / test (push) Successful in 1m44s
Lint / lint (push) Successful in 1m29s
Tests / test (3.11) (push) Successful in 1m49s
Tests / test (3.12) (push) Successful in 1m52s
Tests / test (3.13) (push) Successful in 1m45s
Tests / test (3.14) (push) Successful in 1m28s
Improved pdf generation speed
335 lines
12 KiB
Python
335 lines
12 KiB
Python
"""
|
|
Unit tests for image_utils styling functions
|
|
"""
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
from pyPhotoAlbum.image_utils import (
|
|
apply_rounded_corners,
|
|
apply_drop_shadow,
|
|
create_border_image,
|
|
)
|
|
|
|
|
|
class TestApplyRoundedCorners:
|
|
"""Tests for apply_rounded_corners function"""
|
|
|
|
def test_zero_radius_returns_same_image(self):
|
|
"""Test that 0% radius returns unchanged image"""
|
|
img = Image.new("RGB", (100, 100), color="red")
|
|
result = apply_rounded_corners(img, 0.0)
|
|
assert result.size == img.size
|
|
|
|
def test_negative_radius_returns_same_image(self):
|
|
"""Test that negative radius returns unchanged image"""
|
|
img = Image.new("RGB", (100, 100), color="blue")
|
|
result = apply_rounded_corners(img, -10.0)
|
|
assert result.size == img.size
|
|
|
|
def test_returns_rgba_image(self):
|
|
"""Test that result is RGBA mode"""
|
|
img = Image.new("RGB", (100, 100), color="green")
|
|
result = apply_rounded_corners(img, 10.0)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_preserves_size(self):
|
|
"""Test that image size is preserved"""
|
|
img = Image.new("RGBA", (200, 150), color="yellow")
|
|
result = apply_rounded_corners(img, 15.0)
|
|
assert result.size == (200, 150)
|
|
|
|
def test_corners_are_transparent(self):
|
|
"""Test that corners become transparent"""
|
|
img = Image.new("RGB", (100, 100), color="red")
|
|
result = apply_rounded_corners(img, 25.0)
|
|
|
|
# Top-left corner should be transparent
|
|
pixel = result.getpixel((0, 0))
|
|
assert pixel[3] == 0, "Top-left corner should be transparent"
|
|
|
|
# Top-right corner should be transparent
|
|
pixel = result.getpixel((99, 0))
|
|
assert pixel[3] == 0, "Top-right corner should be transparent"
|
|
|
|
# Bottom-left corner should be transparent
|
|
pixel = result.getpixel((0, 99))
|
|
assert pixel[3] == 0, "Bottom-left corner should be transparent"
|
|
|
|
# Bottom-right corner should be transparent
|
|
pixel = result.getpixel((99, 99))
|
|
assert pixel[3] == 0, "Bottom-right corner should be transparent"
|
|
|
|
def test_center_is_opaque(self):
|
|
"""Test that center remains opaque"""
|
|
img = Image.new("RGB", (100, 100), color="blue")
|
|
result = apply_rounded_corners(img, 10.0)
|
|
|
|
# Center pixel should be fully opaque
|
|
pixel = result.getpixel((50, 50))
|
|
assert pixel[3] == 255, "Center should be opaque"
|
|
|
|
def test_50_percent_radius_creates_ellipse(self):
|
|
"""Test that 50% radius creates elliptical result"""
|
|
img = Image.new("RGB", (100, 100), color="purple")
|
|
result = apply_rounded_corners(img, 50.0)
|
|
|
|
# Corner should be transparent
|
|
assert result.getpixel((0, 0))[3] == 0
|
|
|
|
def test_radius_clamped_to_50(self):
|
|
"""Test that radius > 50 is clamped"""
|
|
img = Image.new("RGB", (100, 100), color="orange")
|
|
result = apply_rounded_corners(img, 100.0) # Should be clamped to 50
|
|
|
|
# Should still work and not crash
|
|
assert result.size == (100, 100)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_preserves_existing_rgba(self):
|
|
"""Test that existing RGBA image alpha is preserved"""
|
|
img = Image.new("RGBA", (100, 100), (255, 0, 0, 128)) # Semi-transparent red
|
|
result = apply_rounded_corners(img, 10.0)
|
|
|
|
# Center should still be semi-transparent (original alpha combined with mask)
|
|
center_pixel = result.getpixel((50, 50))
|
|
assert center_pixel[3] == 128 # Original alpha preserved
|
|
|
|
|
|
class TestApplyDropShadow:
|
|
"""Tests for apply_drop_shadow function"""
|
|
|
|
def test_returns_rgba_image(self):
|
|
"""Test that result is RGBA mode"""
|
|
img = Image.new("RGB", (50, 50), color="red")
|
|
result = apply_drop_shadow(img)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_expand_increases_size(self):
|
|
"""Test that expand=True increases canvas size"""
|
|
img = Image.new("RGBA", (50, 50), color="blue")
|
|
result = apply_drop_shadow(img, offset=(5, 5), blur_radius=3, expand=True)
|
|
assert result.width > 50
|
|
assert result.height > 50
|
|
|
|
def test_no_expand_preserves_size(self):
|
|
"""Test that expand=False preserves size"""
|
|
img = Image.new("RGBA", (50, 50), color="green")
|
|
result = apply_drop_shadow(img, offset=(2, 2), blur_radius=3, expand=False)
|
|
assert result.size == (50, 50)
|
|
|
|
def test_shadow_has_correct_color(self):
|
|
"""Test that shadow uses specified color"""
|
|
# Create image with transparent background and opaque center
|
|
img = Image.new("RGBA", (20, 20), (0, 0, 0, 0))
|
|
img.paste((255, 0, 0, 255), (5, 5, 15, 15))
|
|
|
|
result = apply_drop_shadow(
|
|
img, offset=(10, 10), blur_radius=0, shadow_color=(0, 255, 0, 255), expand=True
|
|
)
|
|
|
|
# Shadow should be visible in the offset area
|
|
# The shadow color should be green
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_zero_blur_radius(self):
|
|
"""Test that blur_radius=0 works"""
|
|
img = Image.new("RGBA", (30, 30), (255, 0, 0, 255))
|
|
result = apply_drop_shadow(img, blur_radius=0, expand=True)
|
|
assert result.mode == "RGBA"
|
|
assert result.width >= 30
|
|
assert result.height >= 30
|
|
|
|
def test_large_offset(self):
|
|
"""Test shadow with large offset"""
|
|
img = Image.new("RGBA", (50, 50), (0, 0, 255, 255))
|
|
result = apply_drop_shadow(img, offset=(20, 20), blur_radius=5, expand=True)
|
|
assert result.width > 50 + 20
|
|
assert result.height > 50 + 20
|
|
|
|
def test_negative_offset(self):
|
|
"""Test shadow with negative offset (shadow above/left of image)"""
|
|
img = Image.new("RGBA", (50, 50), (255, 255, 0, 255))
|
|
result = apply_drop_shadow(img, offset=(-10, -10), blur_radius=2, expand=True)
|
|
assert result.width > 50
|
|
assert result.height > 50
|
|
|
|
|
|
class TestCreateBorderImage:
|
|
"""Tests for create_border_image function"""
|
|
|
|
def test_returns_rgba_image(self):
|
|
"""Test that result is RGBA mode"""
|
|
result = create_border_image(100, 100, 5)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_correct_size(self):
|
|
"""Test that result has correct size"""
|
|
result = create_border_image(200, 150, 10)
|
|
assert result.size == (200, 150)
|
|
|
|
def test_zero_border_returns_transparent(self):
|
|
"""Test that 0 border width returns fully transparent image"""
|
|
result = create_border_image(100, 100, 0)
|
|
assert result.mode == "RGBA"
|
|
# All pixels should be transparent
|
|
for x in range(100):
|
|
for y in range(100):
|
|
assert result.getpixel((x, y))[3] == 0
|
|
|
|
def test_border_color_applied(self):
|
|
"""Test that border color is applied correctly"""
|
|
result = create_border_image(50, 50, 5, border_color=(255, 0, 0))
|
|
|
|
# Edge pixel should be red
|
|
edge_pixel = result.getpixel((0, 25)) # Left edge, middle
|
|
assert edge_pixel[:3] == (255, 0, 0)
|
|
assert edge_pixel[3] == 255 # Opaque
|
|
|
|
def test_center_is_transparent(self):
|
|
"""Test that center is transparent"""
|
|
result = create_border_image(100, 100, 10)
|
|
|
|
# Center pixel should be transparent
|
|
center_pixel = result.getpixel((50, 50))
|
|
assert center_pixel[3] == 0
|
|
|
|
def test_border_surrounds_image(self):
|
|
"""Test that border covers all edges"""
|
|
result = create_border_image(50, 50, 5, border_color=(0, 255, 0))
|
|
|
|
# Top edge
|
|
assert result.getpixel((25, 0))[3] == 255 # Opaque
|
|
# Bottom edge
|
|
assert result.getpixel((25, 49))[3] == 255
|
|
# Left edge
|
|
assert result.getpixel((0, 25))[3] == 255
|
|
# Right edge
|
|
assert result.getpixel((49, 25))[3] == 255
|
|
|
|
def test_with_corner_radius(self):
|
|
"""Test border with rounded corners"""
|
|
result = create_border_image(100, 100, 10, border_color=(0, 0, 255), corner_radius=20)
|
|
assert result.mode == "RGBA"
|
|
assert result.size == (100, 100)
|
|
|
|
def test_corner_radius_affects_transparency(self):
|
|
"""Test that corner radius creates rounded border"""
|
|
result = create_border_image(100, 100, 10, corner_radius=25)
|
|
|
|
# Outer corner should be transparent (outside the rounded border)
|
|
outer_corner = result.getpixel((0, 0))
|
|
assert outer_corner[3] == 0
|
|
|
|
def test_large_border_width(self):
|
|
"""Test with large border width"""
|
|
result = create_border_image(100, 100, 45) # Very thick border
|
|
assert result.mode == "RGBA"
|
|
|
|
# Center should still be transparent (just a small area)
|
|
center = result.getpixel((50, 50))
|
|
assert center[3] == 0
|
|
|
|
|
|
class TestStylingIntegration:
|
|
"""Integration tests combining multiple styling functions"""
|
|
|
|
def test_rounded_corners_then_shadow(self):
|
|
"""Test applying rounded corners then shadow"""
|
|
img = Image.new("RGB", (100, 100), color="red")
|
|
rounded = apply_rounded_corners(img, 15.0)
|
|
result = apply_drop_shadow(rounded, offset=(5, 5), blur_radius=3, expand=True)
|
|
|
|
assert result.mode == "RGBA"
|
|
assert result.width > 100
|
|
assert result.height > 100
|
|
|
|
def test_preserve_quality_through_chain(self):
|
|
"""Test that chaining operations preserves image quality"""
|
|
# Create a simple pattern
|
|
img = Image.new("RGB", (80, 80), color="blue")
|
|
img.paste((255, 0, 0), (20, 20, 60, 60)) # Red square in center
|
|
|
|
# Apply styling chain
|
|
result = apply_rounded_corners(img, 10.0)
|
|
result = apply_drop_shadow(result, expand=True)
|
|
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_small_image_styling(self):
|
|
"""Test styling on small images"""
|
|
img = Image.new("RGB", (10, 10), color="green")
|
|
rounded = apply_rounded_corners(img, 20.0)
|
|
shadow = apply_drop_shadow(rounded, offset=(1, 1), blur_radius=1, expand=True)
|
|
|
|
assert shadow.mode == "RGBA"
|
|
assert shadow.width >= 10
|
|
assert shadow.height >= 10
|
|
|
|
def test_large_image_styling(self):
|
|
"""Test styling on larger images"""
|
|
img = Image.new("RGB", (1000, 800), color="purple")
|
|
rounded = apply_rounded_corners(img, 5.0)
|
|
|
|
assert rounded.mode == "RGBA"
|
|
assert rounded.size == (1000, 800)
|
|
|
|
# Corners should be transparent
|
|
assert rounded.getpixel((0, 0))[3] == 0
|
|
assert rounded.getpixel((999, 0))[3] == 0
|
|
|
|
def test_non_square_image_styling(self):
|
|
"""Test styling on non-square images"""
|
|
img = Image.new("RGB", (200, 50), color="orange")
|
|
rounded = apply_rounded_corners(img, 30.0)
|
|
|
|
assert rounded.mode == "RGBA"
|
|
assert rounded.size == (200, 50)
|
|
|
|
# Radius should be based on shorter side (50)
|
|
# 30% of 50 = 15 pixels radius
|
|
assert rounded.getpixel((0, 0))[3] == 0
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Tests for edge cases and boundary conditions"""
|
|
|
|
def test_1x1_image_rounded_corners(self):
|
|
"""Test rounded corners on 1x1 image"""
|
|
img = Image.new("RGB", (1, 1), color="white")
|
|
result = apply_rounded_corners(img, 50.0)
|
|
assert result.size == (1, 1)
|
|
|
|
def test_very_small_radius(self):
|
|
"""Test with very small radius percentage"""
|
|
img = Image.new("RGB", (100, 100), color="cyan")
|
|
result = apply_rounded_corners(img, 0.1)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_shadow_with_transparent_image(self):
|
|
"""Test shadow on fully transparent image"""
|
|
img = Image.new("RGBA", (50, 50), (0, 0, 0, 0))
|
|
result = apply_drop_shadow(img, expand=True)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_border_on_small_image(self):
|
|
"""Test border on small image (larger than border)"""
|
|
# Use a 10x10 image with 2px border (edge case with very small image)
|
|
result = create_border_image(10, 10, 2)
|
|
assert result.size == (10, 10)
|
|
assert result.mode == "RGBA"
|
|
|
|
def test_styling_preserves_pixel_data(self):
|
|
"""Test that styling preserves underlying pixel data"""
|
|
# Create image with known pattern
|
|
img = Image.new("RGB", (50, 50), (0, 0, 0))
|
|
img.putpixel((25, 25), (255, 255, 255))
|
|
|
|
result = apply_rounded_corners(img, 5.0)
|
|
|
|
# Center white pixel should still be white (with alpha)
|
|
pixel = result.getpixel((25, 25))
|
|
assert pixel[0] == 255
|
|
assert pixel[1] == 255
|
|
assert pixel[2] == 255
|
|
assert pixel[3] == 255 # Opaque in center
|