Refine rendering and packing logic
This commit is contained in:
+74
-43
@@ -9,7 +9,6 @@ from PIL import Image, ImageChops, ImageDraw, ImageFont
|
||||
|
||||
from .assets import meta_unit_scale
|
||||
from .settings import (
|
||||
BASE_CARD_SIZE,
|
||||
BOLD_OFFSETS,
|
||||
DECK_GROUPS,
|
||||
FIXED_CROP,
|
||||
@@ -19,7 +18,6 @@ from .settings import (
|
||||
TEXT_AREA_BOTTOM_CROPPED_RATIO,
|
||||
TEXT_AREA_TOP_CROPPED_RATIOS,
|
||||
TEXT_INDENT_CHARS,
|
||||
TITLE_WIDTH_LIMITS,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,14 +27,19 @@ class Fonts:
|
||||
self._consolas = FONTS_DIR / "consolas.ttf"
|
||||
self._unicode = Path("/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf")
|
||||
self._cache: dict[tuple[str, int], ImageFont.FreeTypeFont | ImageFont.ImageFont] = {}
|
||||
self._source_sizes: dict[int, float] = {}
|
||||
self._families: dict[int, str] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def title(self, java_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
size = max(8, int(java_size / 3) * 2)
|
||||
return self._font("xirod", self._xirod, size)
|
||||
|
||||
def text(self, size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
return self._font("consolas", self._consolas, max(8, size))
|
||||
def text(self, size: int, source_size: float | None = None) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
font = self._font("consolas", self._consolas, max(8, size))
|
||||
if source_size is not None:
|
||||
self._source_sizes[id(font)] = source_size
|
||||
return font
|
||||
|
||||
def unicode_text(self, size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
return self._font("unicode", self._unicode, max(8, size))
|
||||
@@ -49,8 +52,15 @@ class Fonts:
|
||||
self._cache[key] = ImageFont.truetype(str(path), size=size, layout_engine=ImageFont.Layout.RAQM)
|
||||
else:
|
||||
self._cache[key] = ImageFont.load_default()
|
||||
self._families[id(self._cache[key])] = name
|
||||
return self._cache[key]
|
||||
|
||||
def source_size(self, font: ImageFont.ImageFont) -> float:
|
||||
return self._source_sizes.get(id(font), float(font_px(font)))
|
||||
|
||||
def family(self, font: ImageFont.ImageFont) -> str:
|
||||
return self._families.get(id(font), "")
|
||||
|
||||
|
||||
FONTS = Fonts()
|
||||
|
||||
@@ -119,7 +129,7 @@ def format_card_text(text: str | None) -> str:
|
||||
parts = line.split("//", 1)
|
||||
action = parts[0]
|
||||
if not action.strip().endswith(":"):
|
||||
action = re.sub(r" *$", ";", action)
|
||||
action = action.rstrip() + ";"
|
||||
action = action.replace(";;", "; ").replace(":;", ":")
|
||||
action = KEYWORD_RE.sub(r"<b>\1</b>", action)
|
||||
result = leading + action
|
||||
@@ -239,43 +249,50 @@ def text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont,
|
||||
if not text:
|
||||
return 0
|
||||
styles = styles or set()
|
||||
bold_offset = max(dx for dx, _ in BOLD_OFFSETS) if "b" in styles else 0
|
||||
bold_offsets = bold_offsets_for_font(font) if "b" in styles else ()
|
||||
bold_offset = max((dx for dx, _ in bold_offsets), default=0)
|
||||
width = 0.0
|
||||
family = FONTS.family(font)
|
||||
if family not in {"consolas", "unicode"}:
|
||||
for segment, segment_font in text_segments(text, font):
|
||||
for cluster in text_clusters(segment):
|
||||
layout_text = cluster_layout_text(cluster)
|
||||
if layout_text:
|
||||
width += draw.textlength(layout_text, font=segment_font)
|
||||
if is_zalgo_cluster(cluster):
|
||||
mark_count = sum(1 for char in cluster if unicodedata.combining(char))
|
||||
width += zalgo_extra_advance(segment_font, mark_count)
|
||||
if bold_offset:
|
||||
width += bold_offset
|
||||
return round(width)
|
||||
|
||||
source_size = FONTS.source_size(font)
|
||||
source_px = max(1, round(source_size))
|
||||
scale = font_px(font) / max(1.0, source_size)
|
||||
source_font = FONTS.text(source_px, source_size)
|
||||
source_unicode_font = FONTS.unicode_text(source_px)
|
||||
for segment, segment_font in text_segments(text, font):
|
||||
measuring_font = source_unicode_font if segment_font is not font else source_font
|
||||
for cluster in text_clusters(segment):
|
||||
layout_text = cluster_layout_text(cluster)
|
||||
if layout_text:
|
||||
width += draw.textlength(layout_text, font=segment_font)
|
||||
width += draw.textlength(layout_text, font=measuring_font)
|
||||
if is_zalgo_cluster(cluster):
|
||||
mark_count = sum(1 for char in cluster if unicodedata.combining(char))
|
||||
width += zalgo_extra_advance(segment_font, mark_count)
|
||||
if bold_offset:
|
||||
width += bold_offset
|
||||
return round(width)
|
||||
width += zalgo_extra_advance(measuring_font, mark_count)
|
||||
return round(round(width) * scale + bold_offset)
|
||||
|
||||
|
||||
def bold_offsets_for_font(font: ImageFont.ImageFont) -> tuple[tuple[int, int], ...]:
|
||||
source_size = FONTS.source_size(font)
|
||||
scale = max(1, round(font_px(font) / max(1.0, source_size)))
|
||||
return tuple((dx * scale, dy * scale) for dx, dy in BOLD_OFFSETS)
|
||||
|
||||
|
||||
def text_line_height(font: ImageFont.ImageFont) -> int:
|
||||
if hasattr(font, "getmetrics"):
|
||||
ascent, descent = font.getmetrics()
|
||||
return max(1, ascent + descent + 2)
|
||||
return max(1, font_px(font) + 2)
|
||||
|
||||
|
||||
TRIMMED_CARD_WIDTHS: dict[Path, int] = {}
|
||||
|
||||
|
||||
def trimmed_card_width(deck_dir: Path) -> int:
|
||||
deck_dir = deck_dir.resolve()
|
||||
if deck_dir not in TRIMMED_CARD_WIDTHS:
|
||||
TRIMMED_CARD_WIDTHS[deck_dir] = BASE_CARD_SIZE[0] - FIXED_CROP[0] - FIXED_CROP[2]
|
||||
return TRIMMED_CARD_WIDTHS[deck_dir]
|
||||
|
||||
|
||||
def title_width_limit(deck_dir: Path, meta: dict) -> int:
|
||||
group = DECK_GROUPS.get(deck_dir.name, "")
|
||||
ratio = TITLE_WIDTH_LIMITS.get(group, 0.94)
|
||||
crop_scale = meta_unit_scale(meta)
|
||||
return math.floor(trimmed_card_width(deck_dir) * crop_scale * ratio)
|
||||
source_size = FONTS.source_size(font)
|
||||
scale = font_px(font) / max(1.0, source_size)
|
||||
return max(1, round(round(source_size * 35 / 32) * scale))
|
||||
|
||||
|
||||
def title_font_for_target_width(title: str, meta: dict, font_scale: float, max_width: int) -> ImageFont.ImageFont:
|
||||
@@ -290,24 +307,38 @@ def title_font_for_target_width(title: str, meta: dict, font_scale: float, max_w
|
||||
return FONTS.title(min_size)
|
||||
|
||||
|
||||
def standard_text_layout(meta: dict, target_size: tuple[int, int], crop_edges: bool) -> dict:
|
||||
def standard_text_layout(
|
||||
meta: dict,
|
||||
target_size: tuple[int, int],
|
||||
crop_edges: bool,
|
||||
crop_box: tuple[float, float, float, float] | None = None,
|
||||
scale: tuple[float, float] | None = None,
|
||||
) -> dict:
|
||||
card_w = int(meta["cardWidth"])
|
||||
card_h = int(meta["cardHeight"])
|
||||
unit = meta_unit_scale(meta)
|
||||
if crop_edges:
|
||||
if crop_box is not None:
|
||||
left, top, right_crop, bottom_crop = crop_box
|
||||
elif crop_edges:
|
||||
left = round(FIXED_CROP[0] * unit)
|
||||
top = round(FIXED_CROP[1] * unit)
|
||||
right = round(FIXED_CROP[2] * unit)
|
||||
bottom = round(FIXED_CROP[3] * unit)
|
||||
right_crop = round(FIXED_CROP[2] * unit)
|
||||
bottom_crop = round(FIXED_CROP[3] * unit)
|
||||
else:
|
||||
left = top = right = bottom = 0
|
||||
visible_w = max(1, card_w - left - right)
|
||||
visible_h = max(1, card_h - top - bottom)
|
||||
left = top = right_crop = bottom_crop = 0
|
||||
visible_w = max(1.0, card_w - left - right_crop)
|
||||
visible_h = max(1.0, card_h - top - bottom_crop)
|
||||
sx = target_size[0] / visible_w
|
||||
sy = target_size[1] / visible_h
|
||||
if scale is not None:
|
||||
sx, sy = scale
|
||||
return {
|
||||
"left": left,
|
||||
"top": top,
|
||||
"sx": target_size[0] / visible_w,
|
||||
"sy": target_size[1] / visible_h,
|
||||
"sx": sx,
|
||||
"sy": sy,
|
||||
"crop_w": visible_w,
|
||||
"crop_h": visible_h,
|
||||
"target_w": target_size[0],
|
||||
"target_h": target_size[1],
|
||||
}
|
||||
@@ -368,7 +399,7 @@ def draw_styled_text(
|
||||
if not text:
|
||||
return
|
||||
ascent = font.getmetrics()[0] if hasattr(font, "getmetrics") else font_px(font)
|
||||
bold_offsets = BOLD_OFFSETS if "b" in styles else ()
|
||||
bold_offsets = bold_offsets_for_font(font) if "b" in styles else ()
|
||||
|
||||
def draw_zalgo_cluster(
|
||||
target: Image.Image,
|
||||
@@ -508,7 +539,7 @@ def wrap_runs(
|
||||
current_w += text_width(draw, token, font, styles)
|
||||
continue
|
||||
token_w = text_width(draw, token, font, styles)
|
||||
should_wrap = current_w + token_w > max_width
|
||||
should_wrap = current and current[-1][0].isspace() and current_w + token_w > max_width
|
||||
if current and should_wrap:
|
||||
while current and current[-1][0].isspace():
|
||||
current.pop()
|
||||
|
||||
Reference in New Issue
Block a user