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'"])
return "\n".join(body) + "\n"