Files
mcm-26-b/scripts/render_q1_figures.py

95 lines
4.5 KiB
Python

"""Rasterize our restricted SVG primitives with Pillow, without a browser.
This inspects geometry/labels at both widths/themes. It does not emulate HTML/CSS.
"""
import math
from pathlib import Path
import re
import xml.etree.ElementTree as ET
from PIL import Image,ImageDraw,ImageFont
ROOT=Path(__file__).resolve().parents[1]
S=2
FONT=ImageFont.truetype('C:/Windows/Fonts/msyh.ttc',12*S)
def render(path):
root=ET.fromstring(path.read_text(encoding='utf-8'))
width,height=map(float,(root.get('width'),root.get('height')))
bg='#191c21' if 'dark' in path.stem else '#ffffff'
image=Image.new('RGBA',(round(width*S),round(height*S)),bg)
clip=next(el for el in root.iter() if el.tag.endswith('clipPath'))[0]
clipbox=tuple(float(clip.get(k))*S for k in ('x','y','width','height'))
def color(value,opacity=1):
if value in (None,'none'): return None
value=value.lstrip('#')
return tuple(int(value[i:i+2],16) for i in (0,2,4))+(round(opacity*255),)
def draw(el,clipped=False):
nonlocal image
tag=el.tag.rsplit('}',1)[-1]
if tag in ('defs','title','desc'):return
if tag in ('svg','g'):
for child in el:draw(child,clipped or bool(el.get('clip-path')))
return
layer=Image.new('RGBA',image.size,(0,0,0,0));d=ImageDraw.Draw(layer)
num=lambda key,default=0:float(el.get(key,default))*S
fill=color(el.get('fill'),float(el.get('fill-opacity',1)))
stroke=color(el.get('stroke'),float(el.get('stroke-opacity',1)))
sw=max(1,round(num('stroke-width',1)))
if tag=='line':
xy=[(num('x1'),num('y1')),(num('x2'),num('y2'))]
dash=el.get('stroke-dasharray','none')
if dash=='none': d.line(xy,fill=stroke,width=sw)
else:
a,b=xy;dist=math.dist(a,b);unit=((b[0]-a[0])/max(dist,1e-15),(b[1]-a[1])/max(dist,1e-15))
on,off=[float(v)*S for v in dash.split()]
# Bound offscreen ray work to the finite exported viewport vicinity.
segments=min(100000,int(dist/(on+off))+1)
for i in range(segments):
lo=i*(on+off);hi=min(lo+on,dist)
d.line([(a[0]+lo*unit[0],a[1]+lo*unit[1]),(a[0]+hi*unit[0],a[1]+hi*unit[1])],fill=stroke,width=sw)
elif tag=='rect':
box=(num('x'),num('y'),num('x')+num('width'),num('y')+num('height'))
d.rectangle(box,fill=fill,outline=stroke,width=sw)
elif tag=='circle':
x,y,r=num('cx'),num('cy'),num('r');box=(x-r,y-r,x+r,y+r)
if el.get('stroke-dasharray') not in (None,'none'):
for a in range(0,360,18):d.arc(box,a,a+10,fill=stroke,width=sw)
else:d.ellipse(box,fill=fill,outline=stroke,width=sw)
elif tag=='polygon':
points=[tuple(float(v)*S for v in pair.split(',')) for pair in el.get('points').split()]
d.polygon(points,fill=fill)
if stroke:d.line(points+[points[0]],fill=stroke,width=sw,joint='curve')
elif tag=='path':
for x,y,axis,length in re.findall(r'M([\d.e+-]+),([\d.e+-]+)([hv])([\d.e+-]+)',el.get('d')):
x,y,length=float(x)*S,float(y)*S,float(length)*S
d.line([(x,y),(x+length,y) if axis=='h' else (x,y+length)],fill=stroke,width=sw)
elif tag=='text':
value=el.text or '';x,y=num('x'),num('y');anchor=el.get('text-anchor','start')
length=d.textlength(value,font=FONT)
if el.get('transform'):
tile=Image.new('RGBA',(math.ceil(length+8*S),20*S));td=ImageDraw.Draw(tile)
td.text((4*S,0),value,font=FONT,fill=fill)
tile=tile.rotate(90,expand=True)
layer.alpha_composite(tile,(round(x-tile.width/2),round(y-tile.height/2)))
else:
x-=length/2 if anchor=='middle' else length if anchor=='end' else 0
d.text((x,y),value,font=FONT,fill=fill,anchor='ls')
else:raise ValueError('unsupported SVG primitive '+tag)
if clipped:
x,y,w,h=clipbox
mask=Image.new('L',image.size);ImageDraw.Draw(mask).rectangle((x,y,x+w,y+h),fill=255)
transparent=Image.new('RGBA',image.size)
layer=Image.composite(layer,transparent,mask)
image=Image.alpha_composite(image,layer)
draw(root)
output=path.with_suffix('.png');image.convert('RGB').save(output)
return str(output.relative_to(ROOT))
if __name__=='__main__':
for path in sorted((ROOT/'output/q1/figures').glob('*.svg')):print(render(path))