99 lines
4.7 KiB
Python
99 lines
4.7 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 # pylint: disable=import-error # 可选依赖
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
S=2
|
|
FONT=ImageFont.truetype('C:/Windows/Fonts/msyh.ttc',12*S)
|
|
|
|
|
|
def render(svg_path):
|
|
root=ET.fromstring(svg_path.read_text(encoding='utf-8'))
|
|
width,height=map(float,(root.get('width'),root.get('height')))
|
|
bg='#191c21' if 'dark' in svg_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)
|
|
def num(key,default=0):
|
|
return 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':
|
|
pattern=r'M([\d.e+-]+),([\d.e+-]+)([hv])([\d.e+-]+)'
|
|
for x,y,axis,length in re.findall(pattern,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=svg_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))
|