Release PPT workflow 0.7.3 with fixed spacing, paragraphs and native math

This commit is contained in:
2026-10-13 17:16:50 +08:00
parent c4efbed9b0
commit d671e412ae
26 changed files with 1210 additions and 78 deletions
+16 -17
View File
@@ -12,6 +12,7 @@ import unicodedata
import xml.etree.ElementTree as ET
import template_ref as ref
from component_typography import resolve_role
SVG = 'http://www.w3.org/2000/svg'
@@ -48,9 +49,9 @@ def _node(parent, tag, **attributes):
return ET.SubElement(parent, '{'+SVG+'}'+tag, {key.replace('_', '-'):str(value) for key,value in attributes.items()})
def _label(parent, value, x, y, font, size, color, align):
def _label(parent, value, x, y, font, size, color, align, weight='normal'):
result = _node(parent, 'text', x=x, y=y, font_family=font, font_size=size,
font_weight='normal', fill=color, text_anchor='end' if align == 'r' else 'middle' if align == 'ctr' else 'start')
font_weight=weight, fill=color, text_anchor='end' if align == 'r' else 'middle' if align == 'ctr' else 'start')
result.text = value
@@ -143,12 +144,10 @@ def render_grouped_table(raw: dict, style: dict, theme: dict, bounds: tuple,
ref.require(all(row[status['column']] in status['values'] for row in rows), 'unknown status value')
cfg = style.get('components',{}).get('table_grouped',{})
_fields(cfg, {'group_height_px','header_height_px','row_height_px','cell_padding_px'}, set(), 'style.components.table_grouped')
typography = style['typography']; roles = typography['roles']; families = typography['families']
font = families[roles['label']['family_role']]['family']
caption_font = families[roles['caption']['family_role']]['family']
size = _number(roles['label']['size_pt'],'label size')*_number(canvas['pt_to_px'],'pt_to_px')
small = _number(roles['caption']['size_pt'],'caption size')*canvas['pt_to_px']
ref.require(size > 0 and small > 0, 'font sizes must be positive')
label_role = resolve_role(style, style.get('components',{}).get('table',{}).get('text_role','label'))
caption_role = resolve_role(style, 'caption')
font, caption_font = label_role.family, caption_role.family
size, small = label_role.size, caption_role.size
padding = cfg.get('cell_padding_px',[8,16])
ref.require(isinstance(padding,list) and len(padding) == 2 and all(_number(v,'cell padding') >= 0 and v % 8 == 0 for v in padding), 'cell_padding_px requires two nonnegative 8px multiples')
top, side = padding
@@ -179,11 +178,11 @@ def render_grouped_table(raw: dict, style: dict, theme: dict, bounds: tuple,
start,span = group['start'],group['span']; gx=x+sum(widths[:start]); gw=sum(widths[start:start+span])
fill = palette['accent1' if i%2 == 0 else 'accent2']
_fit(group['label'],gw-2*side,small,'group header')
cell = {'paragraphs':[{'align':'ctr','runs':[{'text':group['label'],'font_size':small,'font_family':caption_font,'bold':False,'color':palette['lt1']}]}],'fill':fill,'color':palette['lt1'],'font_size':small,'bold':False,'align':'ctr','valign':'middle','padding':{'top':0,'bottom':0,'left':side,'right':side},'borders':none()}
cell = {'paragraphs':[{'align':'ctr','runs':[{'text':group['label'],'font_size':small,'font_family':caption_font,'bold':caption_role.bold,'color':palette['lt1']}]}],'fill':fill,'color':palette['lt1'],'font_size':small,'bold':caption_role.bold,'align':'ctr','valign':'middle','padding':{'top':0,'bottom':0,'left':side,'right':side},'borders':none()}
if span > 1: cell['col_span']=span
table_rows[start]=cell
_node(fallback,'rect',x=gx,y=y,width=gw,height=gh,fill=fill)
_label(fallback,group['label'],gx+gw/2,y+(gh+small*.72)/2,caption_font,small,palette['lt1'],'ctr')
_label(fallback,group['label'],gx+gw/2,y+(gh+small*.72)/2,caption_font,small,palette['lt1'],'ctr',caption_role.svg_weight)
table_rows = [table_rows]
group_ends = {g['start']+g['span']-1 for g in groups[:-1]}
cy = y+gh
@@ -203,24 +202,24 @@ def render_grouped_table(raw: dict, style: dict, theme: dict, bounds: tuple,
if focused:
borders['top']={'style':'solid','color':palette['accent1'],'width':1}
if ci == 0: borders['left']={'style':'solid','color':palette['accent1'],'width':4}
cell={'fill':fill,'color':color,'align':align,'font_size':size,'bold':False,'padding':pad,'valign':'middle','borders':borders}
cell={'fill':fill,'color':color,'align':align,'font_family':font,'font_size':size,'bold':label_role.bold,'padding':pad,'valign':'middle','borders':borders}
_node(fallback,'rect',x=cx,y=cy,width=widths[ci],height=cell_height,fill=fill)
tx=cx+widths[ci]-side if align == 'r' else cx+pad['left']
if header:
title,unit=value['label'],value.get('unit','')
_fit(title,widths[ci]-2*side,size,'column header')
cell['paragraphs']=[{'align':align,'runs':[{'text':title,'font_size':size,'font_family':font,'bold':False,'color':color}]}]
cell['paragraphs']=[{'align':align,'runs':[{'text':title,'font_size':size,'font_family':font,'bold':label_role.bold,'color':color}]}]
if unit:
_fit(unit,widths[ci]-2*side,small,'column unit')
cell['paragraphs'].append({'align':align,'runs':[{'text':unit,'font_size':small,'font_family':caption_font,'bold':False,'color':palette['dk2']}]})
cell['paragraphs'].append({'align':align,'runs':[{'text':unit,'font_size':small,'font_family':caption_font,'bold':caption_role.bold,'color':palette['dk2']}]})
baseline=cy+(cell_height-size-small-8)/2+size*.8
_label(fallback,title,tx,baseline,font,size,color,align)
_label(fallback,unit,tx,baseline+small+8,caption_font,small,palette['dk2'],align)
else: _label(fallback,title,tx,cy+(cell_height+size*.72)/2,font,size,color,align)
_label(fallback,title,tx,baseline,font,size,color,align,label_role.svg_weight)
_label(fallback,unit,tx,baseline+small+8,caption_font,small,palette['dk2'],align,caption_role.svg_weight)
else: _label(fallback,title,tx,cy+(cell_height+size*.72)/2,font,size,color,align,label_role.svg_weight)
else:
displayed=_formatted(value,columns[ci]); cell['text']=displayed
_fit(displayed,widths[ci]-pad['left']-side,size,'table cell')
_label(fallback,displayed,tx,cy+(cell_height+size*.72-(8 if ci in bar_columns else 0))/2,font,size,color,align)
_label(fallback,displayed,tx,cy+(cell_height+size*.72-(8 if ci in bar_columns else 0))/2,font,size,color,align,label_role.svg_weight)
if ci in bar_columns:
maximum=max(r[ci] for r in rows) or 1; track=widths[ci]-2*side
marks.append({'id':f'{component_id}-bar-r{ri}-c{ci}','kind':'bar','x':cx+side,'y':cy+cell_height-12,'width':track,'length':track*(value/maximum),'value':value,'domain':[0,maximum],'column':ci,'row':ri-1,'focused':focused})