"""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))