JacobLinCool's picture
fix: render share png through server endpoint
18b8de2 verified
from __future__ import annotations
from io import BytesIO
import math
import re
from typing import Any
from PIL import Image, ImageDraw, ImageFont
CANVAS_WIDTH = 1200
CANVAS_HEIGHT = 675
INK = (37, 22, 14)
INK_SOFT = (107, 78, 53)
PARCHMENT_LIGHT = (234, 215, 167)
PARCHMENT_MID = (212, 180, 118)
PARCHMENT_DARK = (185, 138, 76)
OXBLOOD = (141, 45, 38)
LEAF = (47, 122, 73)
GOLD = (182, 138, 18)
CREAM = (255, 240, 181)
def artifact_png_filename(artifact: dict[str, Any]) -> str:
slug = re.sub(r"[^a-z0-9]+", "-", str(artifact.get("title") or "unwritten-page").lower())
slug = slug.strip("-")[:60] or "unwritten-page"
return f"{slug}.png"
def render_artifact_png(artifact: dict[str, Any]) -> bytes:
image = Image.new("RGB", (CANVAS_WIDTH, CANVAS_HEIGHT), PARCHMENT_LIGHT)
draw = ImageDraw.Draw(image)
_draw_parchment(draw)
seal = artifact.get("seal") if isinstance(artifact.get("seal"), dict) else {}
verdict = str(artifact.get("verdict") or seal.get("verdict") or "UNWRITTEN")
overall = _number(artifact.get("overall") or seal.get("overall"), default=0)
title_font = _font(58)
body_font = _font(28)
label_font = _font(20)
small_font = _font(15)
stamp_font = _font(27)
score_font = _font(58)
map_label_font = _font(18)
_wrap_text(
draw,
str(artifact.get("title") or "Unwritten Page"),
(78, 94),
760,
66,
title_font,
INK,
)
_wrap_text(draw, str(artifact.get("caption") or ""), (82, 235), 720, 36, body_font, INK_SOFT)
echoes = seal.get("echoes") if isinstance(seal.get("echoes"), list) else []
_draw_citations(draw, echoes, (742, 335), 330, small_font)
_draw_stamp(draw, (930, 205), verdict, overall, stamp_font, score_font)
_draw_score_bars(draw, seal, verdict, (82, 418), label_font)
wood_map = artifact.get("wood_map") if isinstance(artifact.get("wood_map"), dict) else {}
_draw_wood_map(draw, wood_map, (742, 465), (330, 105), verdict, map_label_font, small_font)
buffer = BytesIO()
image.save(buffer, format="PNG", optimize=True)
return buffer.getvalue()
def _draw_parchment(draw: ImageDraw.ImageDraw) -> None:
for y in range(CANVAS_HEIGHT):
t = y / max(1, CANVAS_HEIGHT - 1)
left = _mix(PARCHMENT_LIGHT, PARCHMENT_MID, min(1, t * 1.6))
right = _mix(PARCHMENT_MID, PARCHMENT_DARK, t)
for x in range(CANVAS_WIDTH):
u = x / max(1, CANVAS_WIDTH - 1)
draw.point((x, y), fill=_mix(left, right, u))
for i in range(360):
x = (i * 73) % CANVAS_WIDTH
y = (i * 37) % CANVAS_HEIGHT
color = (59, 33, 15)
draw.rectangle((x, y, x + 2 + (i % 7), y), fill=_blend(color, 0.16, PARCHMENT_MID))
draw.rectangle((28, 28, CANVAS_WIDTH - 28, CANVAS_HEIGHT - 28), outline=(72, 39, 18), width=16)
def _draw_stamp(
draw: ImageDraw.ImageDraw,
center: tuple[int, int],
verdict: str,
overall: float,
stamp_font: ImageFont.ImageFont,
score_font: ImageFont.ImageFont,
) -> None:
color = GOLD if verdict.startswith("UNWRITTEN") else OXBLOOD
x, y = center
radius = 104
draw.ellipse((x - radius, y - radius, x + radius, y + radius), fill=color)
_wrap_text(draw, verdict, (x - 86, y - 30), 172, 32, stamp_font, CREAM, align="center")
text = f"{overall:.1f}"
box = draw.textbbox((0, 0), text, font=score_font)
draw.text((x - (box[2] - box[0]) / 2, y + 4), text, font=score_font, fill=CREAM)
def _draw_score_bars(
draw: ImageDraw.ImageDraw,
seal: dict[str, Any],
verdict: str,
origin: tuple[int, int],
font: ImageFont.ImageFont,
) -> None:
rows = [
("Originality", seal.get("originality")),
("Delight", seal.get("delight")),
("AI Need", seal.get("ai_necessity")),
("Feasible", seal.get("feasibility")),
("Goal Fit", seal.get("goal_fit")),
]
fill = LEAF if verdict.startswith("UNWRITTEN") else OXBLOOD
x, y = origin
for index, (label, value) in enumerate(rows):
row_y = y + index * 34
score = _number(value, default=0)
draw.text((x, row_y - 18), label, font=font, fill=INK_SOFT)
draw.rectangle((240, row_y - 17, 560, row_y - 1), fill=(177, 148, 111))
draw.rectangle((240, row_y - 17, 240 + int(32 * score), row_y - 1), fill=fill)
draw.text((582, row_y - 20), str(int(score)), font=font, fill=INK)
def _draw_wood_map(
draw: ImageDraw.ImageDraw,
map_data: dict[str, Any],
origin: tuple[int, int],
size: tuple[int, int],
verdict: str,
label_font: ImageFont.ImageFont,
small_font: ImageFont.ImageFont,
) -> None:
dots = map_data.get("dots") if isinstance(map_data.get("dots"), list) else []
if not dots:
return
x, y = origin
width, height = size
draw.rounded_rectangle(
(x, y, x + width, y + height),
radius=8,
fill=(231, 205, 149),
outline=(80, 47, 22),
width=2,
)
draw.text((x, y - 26), "IDEA MAP", font=label_font, fill=INK_SOFT)
for dot in dots:
if not isinstance(dot, dict):
continue
px = x + int(width * _bounded_percent(dot.get("x")) / 100)
py = y + int(height * _bounded_percent(dot.get("y")) / 100)
radius = max(3, min(10, int(_number(dot.get("radius"), default=4))))
kind = str(dot.get("kind") or "inked")
if kind == "idea":
color = LEAF if verdict.startswith("UNWRITTEN") else OXBLOOD
outline = CREAM
elif kind == "echo":
color = OXBLOOD
outline = CREAM
else:
color = (120, 88, 58)
outline = color
draw.ellipse(
(px - radius, py - radius, px + radius, py + radius),
fill=color,
outline=outline,
width=2,
)
caption = _ellipsize(str(map_data.get("caption") or ""), 78)
_wrap_text(draw, caption, (x, y + height + 12), width, 18, small_font, INK_SOFT)
def _draw_citations(
draw: ImageDraw.ImageDraw,
echoes: list[Any],
origin: tuple[int, int],
max_width: int,
font: ImageFont.ImageFont,
) -> None:
if not echoes:
return
x, y = origin
draw.text((x, y - 20), "CLOSEST PAGES", font=_font(18), fill=INK_SOFT)
for index, echo in enumerate(echoes[:3]):
if not isinstance(echo, dict):
continue
project = echo.get("project") if isinstance(echo.get("project"), dict) else {}
page = echo.get("page_number") or "?"
title = project.get("title") or project.get("id") or "Untitled"
label = f"Page {page}: {title}"
_wrap_text(draw, label, (x, y + 8 + index * 30), max_width, 18, font, INK_SOFT)
def _wrap_text(
draw: ImageDraw.ImageDraw,
text: str,
origin: tuple[int, int],
max_width: int,
line_height: int,
font: ImageFont.ImageFont,
fill: tuple[int, int, int],
align: str = "left",
) -> int:
words = str(text).split()
if not words:
return origin[1]
x, y = origin
line = ""
for word in words:
next_line = f"{line} {word}".strip()
if _text_width(draw, next_line, font) > max_width and line:
_draw_line(draw, line, x, y, max_width, font, fill, align)
line = word
y += line_height
else:
line = next_line
if line:
_draw_line(draw, line, x, y, max_width, font, fill, align)
y += line_height
return y
def _draw_line(
draw: ImageDraw.ImageDraw,
text: str,
x: int,
y: int,
max_width: int,
font: ImageFont.ImageFont,
fill: tuple[int, int, int],
align: str,
) -> None:
if align == "center":
x = x + (max_width - _text_width(draw, text, font)) // 2
draw.text((x, y), text, font=font, fill=fill)
def _text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> int:
box = draw.textbbox((0, 0), text, font=font)
return box[2] - box[0]
def _font(size: int) -> ImageFont.ImageFont:
return ImageFont.load_default(size=size)
def _ellipsize(value: str, limit: int) -> str:
text = " ".join(value.split())
if len(text) <= limit:
return text
return text[: max(0, limit - 1)].rstrip() + "..."
def _number(value: Any, default: float) -> float:
try:
number = float(value)
except (TypeError, ValueError):
return default
return number if math.isfinite(number) else default
def _bounded_percent(value: Any) -> float:
return max(4, min(96, _number(value, default=50)))
def _mix(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]:
t = max(0, min(1, t))
return tuple(round(a[index] * (1 - t) + b[index] * t) for index in range(3))
def _blend(
foreground: tuple[int, int, int],
alpha: float,
background: tuple[int, int, int],
) -> tuple[int, int, int]:
return _mix(background, foreground, alpha)