from __future__ import annotations import html import math from pathlib import Path from typing import Any from PIL import Image, ImageDraw, ImageFont from .verifier import ( make_container_shape, make_item_shape, normalize_solution, solution_case, ) GREEN = (205, 247, 214) BLACK = (0, 0, 0) WHITE = (255, 255, 255) def font(size: int, bold: bool = False, scale: int = 1) -> ImageFont.ImageFont: names = [ "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Arial.ttf", "/System/Library/Fonts/Supplemental/Helvetica Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Helvetica.ttf", "/Library/Fonts/Arial Bold.ttf" if bold else "/Library/Fonts/Arial.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ] for name in names: if Path(name).exists(): return ImageFont.truetype(name, size=size * scale) return ImageFont.load_default() def all_points(container: dict[str, Any], shapes: list[dict[str, Any]]) -> list[tuple[float, float]]: points: list[tuple[float, float]] = [] if container["kind"] == "circle": cx, cy = container["center"] r = container["radius"] points.extend([(cx - r, cy - r), (cx + r, cy + r)]) else: points.extend(container["vertices"]) for shape in shapes: if shape["kind"] == "circle": cx, cy = shape["center"] r = shape["radius"] points.extend([(cx - r, cy - r), (cx + r, cy + r)]) else: points.extend(shape["vertices"]) return points def transform_factory( points: list[tuple[float, float]], size: int, label_height: int, margin: int, supersample: int, ): xs = [x for x, _ in points] ys = [y for _, y in points] minx, maxx = min(xs), max(xs) miny, maxy = min(ys), max(ys) width = max(maxx - minx, 1.0e-12) height = max(maxy - miny, 1.0e-12) left, top = margin, label_height right, bottom = size - margin, size - margin scale = min((right - left) / width, (bottom - top) / height) offx = left + ((right - left) - width * scale) * 0.5 offy = top + ((bottom - top) - height * scale) * 0.5 def xy(point: tuple[float, float]) -> tuple[int, int]: x, y = point px = offx + (x - minx) * scale py = offy + (maxy - y) * scale return round(px * supersample), round(py * supersample) def length(value: float) -> int: return round(value * scale * supersample) return xy, length def draw_closed(draw: ImageDraw.ImageDraw, points: list[tuple[int, int]], width: int) -> None: draw.line(points + [points[0]], fill=BLACK, width=width, joint="curve") def render_png(solution: dict[str, Any], path: Path, size: int = 900, label: bool = True) -> Path: solution = normalize_solution(solution) container = make_container_shape(solution["container"]) shapes = [make_item_shape(solution["item"], placement) for placement in solution["placements"]] ss = 3 label_height = 82 if label else 28 xy, lens = transform_factory(all_points(container, shapes), size, label_height, 30, ss) img = Image.new("RGB", (size * ss, size * ss), WHITE) draw = ImageDraw.Draw(img) if label: title_font = font(30, bold=True, scale=ss) side_font = font(28, bold=True, scale=ss) case = solution_case(solution) side = make_container_shape(solution["container"])["side"] side_label = f"s={side:.5f}" draw.text((24 * ss, 18 * ss), case, fill=BLACK, font=title_font) bbox = draw.textbbox((0, 0), side_label, font=side_font) draw.text(((size - 24) * ss - (bbox[2] - bbox[0]), 20 * ss), side_label, fill=BLACK, font=side_font) for shape in shapes: if shape["kind"] == "circle": cx, cy = xy(shape["center"]) r = lens(shape["radius"]) draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=GREEN, outline=BLACK, width=3 * ss) else: pts = [xy(point) for point in shape["vertices"]] draw.polygon(pts, fill=GREEN) draw_closed(draw, pts, width=3 * ss) if container["kind"] == "circle": cx, cy = xy(container["center"]) r = lens(container["radius"]) draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=BLACK, width=6 * ss) else: draw_closed(draw, [xy(point) for point in container["vertices"]], width=6 * ss) img = img.resize((size, size), Image.Resampling.LANCZOS) path.parent.mkdir(parents=True, exist_ok=True) img.save(path, optimize=True) return path def svg_markup(solution: dict[str, Any], size: int = 900) -> str: solution = normalize_solution(solution) container = make_container_shape(solution["container"]) shapes = [make_item_shape(solution["item"], placement) for placement in solution["placements"]] points = all_points(container, shapes) xs = [x for x, _ in points] ys = [y for _, y in points] minx, maxx = min(xs), max(xs) miny, maxy = min(ys), max(ys) pad = max(maxx - minx, maxy - miny) * 0.05 + 1.0e-9 view = f"{minx - pad:.12g} {-maxy - pad:.12g} {maxx - minx + 2 * pad:.12g} {maxy - miny + 2 * pad:.12g}" stroke = max(maxx - minx, maxy - miny) / 260.0 def pts(poly: list[tuple[float, float]]) -> str: return " ".join(f"{x:.12g},{-y:.12g}" for x, y in poly) body = [ f'', f" {html.escape(solution_case(solution))}", ' ', ] for shape in shapes: if shape["kind"] == "circle": cx, cy = shape["center"] body.append( f' ' ) else: body.append(f' ') if container["kind"] == "circle": cx, cy = container["center"] body.append( f' ' ) else: body.append(f' ') body.extend([" ", ""]) return "\n".join(body) + "\n"