Release PPT workflow 0.7.3 with fixed spacing, paragraphs and native math
This commit is contained in:
+26
-22
@@ -10,6 +10,7 @@ import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import template_ref as ref
|
||||
from component_typography import resolve_role
|
||||
|
||||
SVG = "http://www.w3.org/2000/svg"
|
||||
ET.register_namespace("", SVG)
|
||||
@@ -19,9 +20,9 @@ def node(parent, tag, **attributes):
|
||||
return ET.SubElement(parent, f"{{{SVG}}}{tag}", {key.replace("_", "-"): str(value) for key, value in attributes.items()})
|
||||
|
||||
|
||||
def label(parent, value, x, y, font, size, color, anchor="start"):
|
||||
def label(parent, value, x, y, font, size, color, anchor="start", weight="normal"):
|
||||
result = node(parent, "text", x=x, y=y, font_family=font, font_size=size,
|
||||
font_weight="normal", fill=color, text_anchor=anchor)
|
||||
font_weight=weight, fill=color, text_anchor=anchor)
|
||||
result.text = str(value)
|
||||
return result
|
||||
|
||||
@@ -53,12 +54,13 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
ref.require(x >= 0 and y >= 0 and w > 0 and h > 0 and x+w <= canvas["width"] and y+h <= canvas["height"], "bounds outside canvas")
|
||||
kind = data.get("kind")
|
||||
ref.require(kind in ("table", "metric", "timeline", "flow"), "unsupported basic component")
|
||||
palette, roles = theme["palette"], style["typography"]["roles"]
|
||||
families = style["typography"]["families"]
|
||||
font = families[roles["label"]["family_role"]]["family"]
|
||||
ratio = canvas["pt_to_px"]
|
||||
size = roles["label"]["size_pt"] * ratio
|
||||
small = roles["caption"]["size_pt"] * ratio
|
||||
if kind == "table" and data.get("variant") == "grouped":
|
||||
from table_component import render_grouped_table
|
||||
return render_grouped_table(data, style, theme, bounds, component_id)
|
||||
palette = theme["palette"]
|
||||
label_role = resolve_role(style, "label")
|
||||
caption_role = resolve_role(style, "caption")
|
||||
font, size, small = label_role.family, label_role.size, caption_role.size
|
||||
ink, muted, accent = palette["dk1"], palette["dk2"], palette["accent1"]
|
||||
line, white = palette["line"], palette["lt1"]
|
||||
group = ET.Element(f"{{{SVG}}}g", {"id": component_id, "data-component-kind": kind})
|
||||
@@ -67,9 +69,6 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
|
||||
if kind == "table":
|
||||
ref.require(data.get("variant", "basic") in ("basic", "grouped"), "unsupported table variant")
|
||||
if data.get("variant") == "grouped":
|
||||
from table_component import render_grouped_table
|
||||
return render_grouped_table(data, style, theme, bounds, component_id)
|
||||
columns, rows = data.get("columns"), data.get("rows")
|
||||
ref.require(isinstance(columns, list) and columns, "table.columns requires labels")
|
||||
ref.require(isinstance(rows, list) and rows, "table.rows requires data")
|
||||
@@ -82,8 +81,11 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
else:
|
||||
ref.require(not any(ord(char) < 32 for char in value), "cell: control characters unsupported")
|
||||
cfg = style["components"]["table"]
|
||||
table_name = cfg.get("text_role", "label")
|
||||
table_role = label_role if table_name == "label" else resolve_role(style, table_name)
|
||||
font, size = table_role.family, table_role.size
|
||||
top, side = cfg["cell_padding_px"]
|
||||
row_height = math.ceil((size * roles["label"].get("line_height", 1.25) + 2*top) / 8) * 8
|
||||
row_height = math.ceil((table_role.line_size + 2*top) / 8) * 8
|
||||
ref.require(row_height*(len(rows)+1) <= h, "table rows exceed height")
|
||||
widths = data.get("column_widths", [w/len(columns)] * len(columns))
|
||||
ref.require(isinstance(widths, list) and len(widths) == len(columns), "column_widths mismatch")
|
||||
@@ -100,7 +102,7 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
fill = palette[cfg["header_fill_palette_index"]] if i == 0 else white
|
||||
align = "r" if i and j in numeric else "l"
|
||||
cell = {"text": str(value), "fill": fill, "color": color, "align": align,
|
||||
"font_size": size, "bold": False, "padding": {"top": top, "bottom": top, "left": side, "right": side},
|
||||
"font_family": font, "font_size": size, "bold": table_role.bold, "padding": {"top": top, "bottom": top, "left": side, "right": side},
|
||||
"border_color": line, "border_width": 1}
|
||||
cells.append(cell)
|
||||
fit(value, widths[j]-2*side, size, "table cell")
|
||||
@@ -108,7 +110,7 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
fill=fill, stroke=line, stroke_width=1)
|
||||
tx = offset+widths[j]-side if align == "r" else offset+side
|
||||
label(group, value, tx, y+i*row_height+(row_height+size*.72)/2, font, size, color,
|
||||
"end" if align == "r" else "start")
|
||||
"end" if align == "r" else "start", table_role.svg_weight)
|
||||
offset += widths[j]
|
||||
table_rows.append(cells)
|
||||
group.set("data-pptx-replace-with", "table")
|
||||
@@ -129,17 +131,19 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
detail = data.get("detail", "")
|
||||
ref.require(isinstance(unit, str) and isinstance(detail, str), "metric unit/detail must be strings")
|
||||
ref.require(not any(ord(char) < 32 for char in unit+detail), "metric: control characters unsupported")
|
||||
big = roles["number"]["size_pt"] * ratio
|
||||
number_role = resolve_role(style, "number")
|
||||
big = number_role.size
|
||||
ref.require(h >= 160, "metric needs at least 160px height")
|
||||
fit(title, w, size, "metric label")
|
||||
fit(str(value)+unit, w, big, "metric value")
|
||||
fit(detail, w, small, "metric detail")
|
||||
label(group, title, x, y+size, font, size, muted)
|
||||
value_node = label(group, value, x, y+104, font, big, accent)
|
||||
label(group, title, x, y+size, font, size, muted, weight=label_role.svg_weight)
|
||||
value_node = label(group, value, x, y+104, number_role.family, big, accent, weight=number_role.svg_weight)
|
||||
if unit:
|
||||
span = node(value_node, "tspan", dx=16, font_size=size, fill=muted)
|
||||
span = node(value_node, "tspan", dx=16, font_family=font, font_size=size,
|
||||
font_weight=label_role.svg_weight, fill=muted)
|
||||
span.text = unit
|
||||
if detail: label(group, detail, x, y+152, font, small, ink)
|
||||
if detail: label(group, detail, x, y+152, caption_role.family, small, ink, weight=caption_role.svg_weight)
|
||||
return group
|
||||
|
||||
items = data.get("items") if kind == "timeline" else data.get("nodes")
|
||||
@@ -156,8 +160,8 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
fit(title, slot-24, size, "timeline label")
|
||||
fit(date, slot-24, small, "timeline date")
|
||||
node(group, "circle", cx=centers[i], cy=cy, r=8, fill=accent, stroke=white, stroke_width=2)
|
||||
label(group, date, centers[i], y+56, font, small, muted, "middle")
|
||||
label(group, title, centers[i], y+152, font, size, ink, "middle")
|
||||
label(group, date, centers[i], y+56, caption_role.family, small, muted, "middle", caption_role.svg_weight)
|
||||
label(group, title, centers[i], y+152, font, size, ink, "middle", label_role.svg_weight)
|
||||
return group
|
||||
|
||||
ref.require(h >= 160, "flow needs at least 160px height")
|
||||
@@ -181,7 +185,7 @@ def render_component(data: dict, style: dict, theme: dict, bounds: tuple,
|
||||
nx = x+i*(nw+gap)
|
||||
title = text(item.get("label"), "flow label");fit(title, nw-24, size, "flow label")
|
||||
node(group, "rect", x=nx, y=cy-40, width=nw, height=80, fill=white, stroke=muted, stroke_width=2)
|
||||
label(group, title, nx+nw/2, cy+size*.36, font, size, ink, "middle")
|
||||
label(group, title, nx+nw/2, cy+size*.36, font, size, ink, "middle", label_role.svg_weight)
|
||||
if i < len(items)-1 and (ids[i], ids[i+1]) in actual:
|
||||
end = nx+nw+gap
|
||||
node(group, "line", x1=nx+nw, y1=cy, x2=end-8, y2=cy, stroke=muted, stroke_width=2)
|
||||
|
||||
Reference in New Issue
Block a user