Spaces:
Running
Running
File size: 4,724 Bytes
008947a d79b6e6 008947a 4fb118c 008947a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | """Typography formatting and fitting helpers."""
from __future__ import annotations
import re
def resolve_text_positions(
*,
figure_height: float,
text_rect_y: float,
text_rect_height: float,
title_y: float,
subtitle_y: float,
caption_y: float,
coordinates_y: float,
title_size: float,
subtitle_size: float,
caption_size: float,
coordinate_size: float,
has_title: bool,
has_subtitle: bool,
has_caption: bool,
show_coordinates: bool,
) -> dict[str, float]:
"""Keep poster text boxes separated when the figure becomes short and wide.
Layout coordinates are normalized to the figure. Font sizes are points, so
their normalized height changes with the physical figure height. Computing
the stack from those two units prevents the fixed portrait coordinates from
causing overlap in landscape posters.
"""
if figure_height <= 0:
raise ValueError("Figure height must be greater than zero.")
gap = 0.012
def normalized_height(size: float) -> float:
return size / 72 / figure_height
heights = {
"title": normalized_height(title_size),
"subtitle": normalized_height(subtitle_size),
"caption": normalized_height(caption_size),
"coordinates": normalized_height(coordinate_size),
}
positions = {
"title": title_y,
"subtitle": subtitle_y,
"caption": caption_y,
"coordinates": coordinates_y,
}
active = [
("title", title_y, has_title),
("subtitle", subtitle_y, has_subtitle),
("caption", caption_y, has_caption),
("coordinates", coordinates_y, show_coordinates),
]
previous_key: str | None = None
for key, desired, enabled in active:
if not enabled:
continue
if previous_key is not None:
desired = min(
desired,
positions[previous_key]
- heights[previous_key] / 2
- gap
- heights[key] / 2,
)
positions[key] = desired
previous_key = key
if has_title:
top_limit = text_rect_y + text_rect_height - heights["title"] / 2
positions["title"] = min(positions["title"], top_limit)
if has_title and has_subtitle:
title_bottom = positions["title"] - heights["title"] / 2
subtitle_top = positions["subtitle"] + heights["subtitle"] / 2
positions["divider"] = (title_bottom + subtitle_top) / 2
elif has_title:
positions["divider"] = positions["title"] - heights["title"] / 2 - gap / 2
else:
positions["divider"] = subtitle_y
return positions
def has_chinese(text: str) -> bool:
"""Return whether text contains a CJK Unified Ideograph."""
return any("\u4e00" <= char <= "\u9fff" for char in text)
def format_coordinates(latitude: float, longitude: float) -> str:
"""Format signed coordinates with cardinal directions."""
lat_direction = "N" if latitude >= 0 else "S"
lon_direction = "E" if longitude >= 0 else "W"
return f"{abs(latitude):.4f}° {lat_direction} / {abs(longitude):.4f}° {lon_direction}"
def apply_letter_spacing(text: str, spacing: float) -> str:
"""Approximate tracking for Latin text without altering CJK text."""
if spacing <= 0 or has_chinese(text):
return text
gap = " " if spacing < 0.16 else " "
return gap.join(text.upper())
def safe_slug(value: str, fallback: str = "map-poster") -> str:
"""Create a readable Unicode-safe filename slug."""
value = value.replace("_", "-")
value = re.sub(r"[^\w\-]+", "-", value.strip().lower(), flags=re.UNICODE)
value = re.sub(r"-+", "-", value).strip("-_")
return value[:80] or fallback
def safe_ascii_slug(value: str, fallback: str = "map-poster") -> str:
"""Create an ASCII-only filename slug by dropping non-ASCII characters.
Used to build the Latin-1-safe ``filename=`` fallback of an HTTP
``Content-Disposition`` header, while ``safe_slug`` keeps the readable
Unicode form transported through RFC 5987 ``filename*``.
"""
value = value.replace("_", "-")
value = re.sub(r"[^A-Za-z0-9]+", "-", value.strip().lower())
value = re.sub(r"-+", "-", value).strip("-_")
return value[:80] or fallback
def fitted_font_size(text: str, requested: float, max_units: float, minimum: float = 18) -> float:
"""Return a deterministic first-pass title size for a bounded layout region."""
if not text:
return requested
units = sum(1.0 if has_chinese(char) else 0.62 for char in text)
if units <= max_units:
return requested
return max(minimum, requested * max_units / units)
|