Badges : photo du bénévole et texte libre
Champ « Photo du bénévole » (PHOTO) : photo du formulaire recadrée au cadre, orientation EXIF appliquée, réduite à 300 dpi ; cadre vide sans photo. Champ « Texte libre » (FREE_TEXT) alimenté par le nouveau « Texte du badge » de la fiche bénévole (Volunteer.badge_text, migration 0024), repris dans l'export Excel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5b493c9737
commit
54cba467fc
@@ -1,11 +1,17 @@
|
||||
"""Génération des planches de badges (module pur, sans base)."""
|
||||
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from app.badges.pdf import (
|
||||
ASSUMED_DPI,
|
||||
DEFAULT_BADGE_HEIGHT_MM,
|
||||
DEFAULT_BADGE_WIDTH_MM,
|
||||
BadgeFieldSpec,
|
||||
BadgeSpec,
|
||||
_cell_origin,
|
||||
_fit_photo,
|
||||
_grid_size,
|
||||
generate_badge_sheets_pdf,
|
||||
image_size_mm,
|
||||
@@ -97,3 +103,51 @@ def test_more_badges_than_one_sheet_produces_a_larger_pdf():
|
||||
[_badge() for _ in range(per_page + 1)], front_image=None, back_image=None
|
||||
)
|
||||
assert len(two_pages) > len(one_page)
|
||||
|
||||
|
||||
def _jpeg(width_px: int, height_px: int) -> bytes:
|
||||
out = io.BytesIO()
|
||||
Image.new("RGB", (width_px, height_px), "red").save(out, format="JPEG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _photo_badge(image: bytes | None) -> BadgeSpec:
|
||||
return BadgeSpec(
|
||||
fields=(
|
||||
BadgeFieldSpec(
|
||||
text="", x_mm=60, y_mm=5, width_mm=20, height_mm=25,
|
||||
font_size=8, align="LEFT", bold=False, color="#000000",
|
||||
is_photo=True, image=image,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_fit_photo_crops_to_the_frame_ratio_at_print_resolution():
|
||||
# Photo paysage 4000×3000 dans un cadre portrait 20×25 mm : recadrée
|
||||
# (pas déformée) et réduite à 300 dpi.
|
||||
fitted = _fit_photo(_jpeg(4000, 3000), width_mm=20, height_mm=25)
|
||||
|
||||
with Image.open(io.BytesIO(fitted)) as img:
|
||||
assert img.size == (round(20 / 25.4 * ASSUMED_DPI), round(25 / 25.4 * ASSUMED_DPI))
|
||||
|
||||
|
||||
def test_fit_photo_returns_none_for_an_unreadable_image():
|
||||
assert _fit_photo(b"pas une image", width_mm=20, height_mm=25) is None
|
||||
|
||||
|
||||
def test_photo_field_embeds_the_volunteer_photo():
|
||||
without = generate_badge_sheets_pdf([_photo_badge(None)], front_image=None, back_image=None)
|
||||
with_photo = generate_badge_sheets_pdf(
|
||||
[_photo_badge(_jpeg(400, 500))], front_image=None, back_image=None
|
||||
)
|
||||
assert without.startswith(b"%PDF")
|
||||
assert b"/Subtype /Image" in with_photo
|
||||
assert b"/Subtype /Image" not in without
|
||||
|
||||
|
||||
def test_unreadable_photo_does_not_crash_generation():
|
||||
pdf = generate_badge_sheets_pdf(
|
||||
[_photo_badge(b"pas une image")], front_image=None, back_image=None
|
||||
)
|
||||
assert pdf.startswith(b"%PDF")
|
||||
|
||||
Reference in New Issue
Block a user