#!/usr/bin/env python3 import argparse import json import math import os import re import shutil import sys import tempfile import numpy as np from wand.api import library from wand.color import Color from wand.image import Image import pcbnew from lxml import etree default_style = { "copper": "#417e5a", "board": "#4ca06c", "silk": "#f0f0f0", "pads": "#b5ae30", "outline": "#000000", "clad": "#9c6b28", "vcut": "#bf2600", "highlight-on-top": False, "highlight-style": "stroke:none;fill:#ff0000;opacity:0.5;", "highlight-padding": 1.5, "highlight-offset": 0 } float_re = r'([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?)' class SvgPathItem: def __init__(self, path): path = re.sub(r"([MLA])(\d+)", r"\1 \2", path) path = re.split("[, ]", path) path = list(filter(lambda x: x, path)) if path[0] != "M": raise SyntaxError("Only paths with absolute position are supported") self.start = tuple(map(float, path[1:3])) path = path[3:] if path[0] == "L": x = float(path[1]) y = float(path[2]) self.end = (x, y) self.type = path[0] self.args = None elif path[0] == "A": args = list(map(float, path[1:8])) self.end = (args[5], args[6]) self.args = args[0:5] self.type = path[0] else: raise SyntaxError("Unsupported path element " + path[0]) @staticmethod def is_same(p1, p2): dx = p1[0] - p2[0] dy = p1[1] - p2[1] return math.sqrt(dx*dx+dy*dy) < 5 def format(self, first): ret = "" if first: ret += " M {} {} ".format(*self.start) ret += self.type if self.args: ret += " " + " ".join(map(lambda x: str(x).rstrip('0').rstrip('.'), self.args)) ret += " {} {} ".format(*self.end) return ret def flip(self): self.start, self.end = self.end, self.start if self.type == "A": self.args[4] = 1 if self.args[4] < 0.5 else 0 def unique_prefix(): unique_prefix.counter += 1 return "pref_" + str(unique_prefix.counter) unique_prefix.counter = 0 def matrix(data): return np.array(data, dtype=np.float32) def extract_arg(args, index, default=None): """ Return n-th element of array or default if out of range """ if index >= len(args): return default return args[index] def to_trans_matrix(transform): """ Given SVG transformation string returns corresponding matrix """ m = matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) if transform is None: return m trans = re.findall(r'[a-z]+?\(.*?\)', transform) for t in trans: op, args = t.split('(') args = [float(x) for x in re.findall(float_re, args)] if op == 'matrix': m = np.matmul(m, matrix([ [args[0], args[2], args[4]], [args[1], args[3], args[5]], [0, 0, 1]])) if op == 'translate': x = args[0] y = extract_arg(args, 1, 0) m = np.matmul(m, matrix([ [1, 0, x], [0, 1, y], [0, 0, 1]])) if op == 'scale': x = args[0] y = extract_arg(args, 1, 1) m = np.matmul(m, matrix([ [x, 0, 0], [0, y, 0], [0, 0, 1]])) if op == 'rotate': cosa = np.cos(np.radians(args[0])) sina = np.sin(np.radians(args[0])) if len(args) != 1: x, y = args[1:3] m = np.matmul(m, matrix([ [1, 0, x], [0, 1, y], [0, 0, 1]])) m = np.matmul(m, matrix([ [cosa, -sina, 0], [sina, cosa, 0], [0, 0, 1]])) if len(args) != 1: m = np.matmul(m, matrix([ [1, 0, -x], [0, 1, -y], [0, 0, 1]])) if op == 'skewX': tana = np.tan(np.radians(args[0])) m = np.matmul(m, matrix([ [1, tana, 0], [0, 1, 0], [0, 0, 1]])) if op == 'skewY': tana = np.tan(np.radians(args[0])) m = np.matmul(m, matrix([ [1, 0, 0], [tana, 1, 0], [0, 0, 1]])) return m def collect_transformation(element, root=None): """ Collect all the transformation applied to an element and return it as matrix """ if root is None: if element.getparent() is not None: m = collect_transformation(element.getparent(), root) else: m = matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) else: if element.getparent() != root: m = collect_transformation(element.getparent(), root) else: m = matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) if "transform" not in element.attrib: return m trans = element.attrib["transform"] return np.matmul(m, to_trans_matrix(trans)) def element_position(element, root=None): position = matrix([ [element.attrib["x"]], [element.attrib["y"]], [1]]) r = root trans = collect_transformation(element, root=r) position = np.matmul(trans, position) return position[0][0] / position[2][0], position[1][0] / position[2][0] def ki2dmil(val): return val // 2540 def to_user_units(val): x = float_re + r'\s*(pt|pc|mm|cm|in)?' value, unit = re.findall(x, val)[0] value = float(value) if unit == "" or unit == "px": return value if unit == "pt": return 1.25 * value if unit == "pc": return 15 * value if unit == "mm": return 3.543307 * value if unit == "cm": return 35.43307 * value if unit == "in": return 90 def read_svg_unique(filename): prefix = unique_prefix() + "_" root = etree.parse(filename).getroot() # We have to ensure all Ids in SVG are unique. Let's make it nasty by # collecting all ids and doing search & replace # Potentially dangerous (can break user text) ids = [] for el in root.getiterator(): if "id" in el.attrib and el.attrib["id"] != "origin": ids.append(el.attrib["id"]) with open(filename) as f: content = f.read() for i in ids: content = content.replace("#"+i, "#" + prefix + i) root = etree.fromstring(str.encode(content)) for el in root.getiterator(): if "id" in el.attrib and el.attrib["id"] != "origin": el.attrib["id"] = prefix + el.attrib["id"] return root def extract_svg_content(root): # Remove SVG namespace to ease our lifes and change ids for el in root.getiterator(): if '}' in str(el.tag): el.tag = el.tag.split('}', 1)[1] return [ x for x in root if x.tag and x.tag not in ["title", "desc"]] def strip_fill_svg(root): keys = ["fill", "stroke"] for el in root.getiterator(): if "style" in el.attrib: s = el.attrib["style"].split(";") s = filter(lambda x: x.strip().split(":")[0] not in keys, s) el.attrib["style"] = ";".join(s).replace(" ", " ").strip() def empty_svg(**attrs): document = etree.ElementTree(etree.fromstring( """<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="29.7002cm" height="21.0007cm" viewBox="0 0 116930 82680 "> <title>Picture generated by PcbDraw </title> <desc>Picture generated by PcbDraw</desc> </svg>""")) root = document.getroot() for key, value in attrs.items(): root.attrib[key] = value return document def get_board_polygon(svg_elements): """ Try to connect independents segments on Edge.Cuts and form a polygon return SVG path element with the polygon """ elements = [] path = "" for group in svg_elements: for svg_element in group: if svg_element.tag == "path": elements.append(SvgPathItem(svg_element.attrib["d"])) elif svg_element.tag == "circle": # Convert circle to path att = svg_element.attrib s = " M {0} {1} m-{2} 0 a {2} {2} 0 1 0 {3} 0 a {2} {2} 0 1 0 -{3} 0 ".format( att["cx"], att["cy"], att["r"], 2 * float(att["r"])) path += s outline = [elements[0]] elements = elements[1:] while True: size = len(outline) for i, e in enumerate(elements): if SvgPathItem.is_same(outline[0].start, e.end): outline.insert(0, e) elif SvgPathItem.is_same(outline[0].start, e.start): e.flip() outline.insert(0, e) elif SvgPathItem.is_same(outline[-1].end, e.start): outline.append(e) elif SvgPathItem.is_same(outline[-1].end, e.end): e.flip() outline.append(e) else: continue del elements[i] break if size == len(outline): first = True for x in outline: path += x.format(first) first = False if elements: outline = [elements[0]] elements = elements[1:] else: e = etree.Element("path", d=path, style="fill-rule=evenodd;") return e def process_board_substrate_layer(container, name, source, colors, boardsize): layer = etree.SubElement(container, "g", id="substrate-"+name, style="fill:{0}; stroke:{0};".format(colors[name])) if name == "pads": layer.attrib["mask"] = "url(#pads-mask)" if name == "silk": layer.attrib["mask"] = "url(#pads-mask-silkscreen)" for element in extract_svg_content(read_svg_unique(source)): strip_fill_svg(element) layer.append(element) def process_board_substrate_base(container, name, source, colors, boardsize): clipPath = etree.SubElement(etree.SubElement(container, "defs"), "clipPath") clipPath.attrib["id"] = "cut-off" clipPath.append(get_board_polygon(extract_svg_content(read_svg_unique(source)))) layer = etree.SubElement(container, "g", id="substrate-"+name, style="fill:{0}; stroke:{0};".format(colors[name])) layer.append(get_board_polygon(extract_svg_content(read_svg_unique(source)))) outline = etree.SubElement(layer, "g", style="fill:{0}; stroke: {0};".format(colors["outline"])) for element in extract_svg_content(read_svg_unique(source)): strip_fill_svg(element) outline.append(element) def process_board_substrate_mask(container, name, source, colors, boardsize): mask = etree.SubElement(etree.SubElement(container, "defs"), "mask") mask.attrib["id"] = name for element in extract_svg_content(read_svg_unique(source)): for item in element.getiterator(): if "style" in item.attrib: # KiCAD plots in black, for mask we need white item.attrib["style"] = item.attrib["style"].replace("#000000", "#ffffff") mask.append(element) silkMask = etree.SubElement(etree.SubElement(container, "defs"), "mask") silkMask.attrib["id"] = name + "-silkscreen" for element in extract_svg_content(read_svg_unique(source)): # KiCAD plots black, no need to change fill silkMask.append(element) bg = etree.SubElement(silkMask, "rect", attrib={ "x": str(ki2dmil(boardsize.GetX())), "y": str(ki2dmil(boardsize.GetY())), "width": str(ki2dmil(boardsize.GetWidth())), "height": str(ki2dmil(boardsize.GetHeight())), "fill": "white" }) def get_layers(board, colors, toPlot): """ Plot given layers, process them and return them as <g> """ container = etree.Element('g') tmp = tempfile.mkdtemp() pctl = pcbnew.PLOT_CONTROLLER(board) popt = pctl.GetPlotOptions() popt.SetOutputDirectory(tmp) popt.SetScale(1) popt.SetMirror(False) popt.SetSubtractMaskFromSilk(True) try: popt.SetPlotOutlineMode(False) except: # Method does not exist in older versions of KiCad pass popt.SetTextMode(pcbnew.PLOTTEXTMODE_STROKE) for f, layers, _ in toPlot: pctl.OpenPlotfile(f, pcbnew.PLOT_FORMAT_SVG, f) for l in layers: pctl.SetColorMode(False) pctl.SetLayer(l) pctl.PlotLayer() pctl.ClosePlot() for f, _, process in toPlot: for svg_file in os.listdir(tmp): if svg_file.endswith("-" + f + ".svg"): process(container, f, os.path.join(tmp, svg_file), colors) shutil.rmtree(tmp) return container def get_board_substrate(board, colors, holes, back): """ Plots all front layers from the board and arranges them in a visually appealing style. return SVG g element with the board substrate """ toPlot = [] if(back): toPlot = [ ("board", [pcbnew.Edge_Cuts], process_board_substrate_base), ("clad", [pcbnew.B_Mask], process_board_substrate_layer), ("copper", [pcbnew.B_Cu], process_board_substrate_layer), ("pads", [pcbnew.B_Cu], process_board_substrate_layer), ("pads-mask", [pcbnew.B_Mask], process_board_substrate_mask), ("silk", [pcbnew.B_SilkS], process_board_substrate_layer), ("outline", [pcbnew.Edge_Cuts], process_board_substrate_layer)] else: toPlot = [ ("board", [pcbnew.Edge_Cuts], process_board_substrate_base), ("clad", [pcbnew.F_Mask], process_board_substrate_layer), ("copper", [pcbnew.F_Cu], process_board_substrate_layer), ("pads", [pcbnew.F_Cu], process_board_substrate_layer), ("pads-mask", [pcbnew.F_Mask], process_board_substrate_mask), ("silk", [pcbnew.F_SilkS], process_board_substrate_layer), ("outline", [pcbnew.Edge_Cuts], process_board_substrate_layer)] container = etree.Element('g') container.attrib["clip-path"] = "url(#cut-off)" tmp = tempfile.mkdtemp() pctl = pcbnew.PLOT_CONTROLLER(board) popt = pctl.GetPlotOptions() popt.SetOutputDirectory(tmp) popt.SetScale(1) popt.SetMirror(False) popt.SetSubtractMaskFromSilk(True) try: popt.SetPlotOutlineMode(False) except: # Method does not exist in older versions of KiCad pass popt.SetTextMode(pcbnew.PLOTTEXTMODE_STROKE) for f, layers, _ in toPlot: pctl.OpenPlotfile(f, pcbnew.PLOT_FORMAT_SVG, f) for l in layers: pctl.SetColorMode(False) pctl.SetLayer(l) pctl.PlotLayer() pctl.ClosePlot() boardsize = board.ComputeBoundingBox() for f, _, process in toPlot: for svg_file in os.listdir(tmp): if svg_file.endswith("-" + f + ".svg"): process(container, f, os.path.join(tmp, svg_file), colors, boardsize) shutil.rmtree(tmp) if holes: container.append(get_hole_mask(board)) container.attrib["mask"] = "url(#hole-mask)" return container def walk_components(board, back, export): module = board.GetModules() while True: if not module: return # Top is for Eagle boards imported to KiCAD if (str(module.GetLayerName()) in ["Back", "B.Cu"] and not back) or \ (str(module.GetLayerName()) in ["Top", "F.Cu"] and back): module = module.Next() continue lib = str(module.GetFPID().GetLibNickname()).strip() try: name = str(module.GetFPID().GetFootprintName()).strip() except AttributeError: # it seems we are working on Kicad >4.0.6, which has a changed method name name = str(module.GetFPID().GetLibItemName()).strip() value = module.GetValue().strip() ref = module.GetReference().strip() center = module.GetCenter() orient = math.radians(module.GetOrientation() / 10) pos = (center.x, center.y, orient) export(lib, name, value, ref, pos) module = module.Next() def get_hole_mask(board): defs = etree.Element("defs") mask = etree.SubElement(defs, "mask", id="hole-mask") container = etree.SubElement(mask, "g") bb = board.ComputeBoundingBox() bg = etree.SubElement(container, "rect", x="0", y="0", fill="white") bg.attrib["x"] = str(ki2dmil(bb.GetX())) bg.attrib["y"] = str(ki2dmil(bb.GetY())) bg.attrib["width"] = str(ki2dmil(bb.GetWidth())) bg.attrib["height"] = str(ki2dmil(bb.GetHeight())) module = board.GetModules() while module: if module.GetPadCount() == 0: module = module.Next() continue pad = module.Pads() try: pad.GetPosition() except: # Newest nightly renames Pads to PadList pad = module.PadsList() while pad: pos = pad.GetPosition() padOrientation = pad.GetOrientation() pos.x = ki2dmil(pos.x) pos.y = ki2dmil(pos.y) size = list(map(ki2dmil, pad.GetDrillSize())) if size[0] > 0 and size[1] > 0: if size[0] < size[1]: stroke = size[0] length = size[1] - size[0] points = "{} {} {} {}".format(0, -length / 2, 0, length / 2) else: stroke = size[1] length = size[0] - size[1] points = "{} {} {} {}".format(-length / 2, 0, length / 2, 0) el = etree.SubElement(container, "polyline") el.attrib["stroke-linecap"] = "round" el.attrib["stroke"] = "black" el.attrib["stroke-width"] = str(stroke) el.attrib["points"] = points el.attrib["transform"] = "translate({} {}) rotate({})".format( pos.x, pos.y, -padOrientation / 10) pad = pad.Next() module = module.Next() return defs def get_model_file(paths, lib, name, ref, remapping): """ Find model file in library considering component remapping """ for path in paths: if ref in remapping: lib, name = tuple(remapping[ref].split(":")) f = os.path.join(path, lib, name + ".svg") if os.path.isfile(f): return f return None def print_component(paths, lib, name, value, ref, pos, remapping={}): f = get_model_file(paths, lib, name, ref, remapping) msg = "{} with package {}:{} at [{},{},{}] -> {}".format( ref, lib, name, pos[0], pos[1], math.degrees(pos[2]), f if f else "Not found") print(msg) def component_from_library(lib, name, value, ref, pos, comp, highlight, silent): if not name: return if comp["filter"] is not None and ref not in comp["filter"]: return f = get_model_file(comp["libraries"], lib, name, ref, comp["remapping"]) if not f: if not silent: print("Warning: component '{}' for footprint '{}' from library '{}' was not found".format(name, ref, lib)) if comp["placeholder"]: etree.SubElement(comp["container"], "rect", x=str(ki2dmil(pos[0]) - 150), y=str(ki2dmil(pos[1]) - 150), width="300", height="300", style="fill:red;") return comp["container"].append(etree.Comment("{}:{}".format(lib, name))) r = etree.SubElement(comp["container"], "g") svg_tree = read_svg_unique(f) for x in extract_svg_content(svg_tree): r.append(x) origin_x = 0 origin_y = 0 origin = r.find(".//*[@id='origin']") if origin is not None: origin_x, origin_y = element_position(origin, root=r) origin.getparent().remove(origin) else: print("Warning: component '{}' from library '{}' has no ORIGIN".format(name, lib)) r.attrib["transform"] = "translate({} {}) scale(393.700787402) rotate({}) translate({}, {})".format( ki2dmil(pos[0]), ki2dmil(pos[1]), -math.degrees(pos[2]), -origin_x, -origin_y) if ref in highlight["items"]: if "width" in svg_tree.attrib and "height" in svg_tree.attrib: w = to_user_units(svg_tree.attrib["width"]) h = to_user_units(svg_tree.attrib["height"]) build_highlight(highlight, w, h, pos, (origin_x, origin_y), ref) elif "viewBox" in svg_tree.attrib: viewbox = re.split(" |,", svg_tree.attrib["viewBox"]) w = to_user_units(viewbox[2]) h = to_user_units(viewbox[3]) build_highlight(highlight, w, h, pos, (origin_x, origin_y), ref) else: print("Warning: component '{}' from library '{}' has no viewBox. Cannot highlight".format(name, lib)) def build_highlight(preset, width, height, pos, origin, ref): h = etree.SubElement(preset["container"], "rect") scale = 393.700787402 h.attrib["style"] = preset["style"] h.attrib["x"] = str(-preset["padding"]) h.attrib["y"] = str(-preset["padding"]) h.attrib["width"] = str(width + 2 * preset["padding"]) h.attrib["height"] = str(height + 2 * preset["padding"]) h.attrib["transform"] = "translate({} {}) scale(393.700787402) rotate({}) translate({}, {})".format( ki2dmil(pos[0]), ki2dmil(pos[1]), -math.degrees(pos[2]), -origin[0], -origin[1]) h.attrib["id"] = "h_" + ref def svg_to_png(infile, outfile, dpi=300): with Image(resolution=300) as image: with Color('transparent') as background_color: library.MagickSetBackgroundColor(image.wand, background_color.resource) image.read(filename=infile, resolution=300) png_image = image.make_blob("png32") with open(outfile, "wb") as out: out.write(png_image) def load_style(style_file): try: with open(style_file, "r") as f: style = json.load(f) except IOError: raise RuntimeError("Cannot open style " + style_file) required = set(["copper", "board", "clad", "silk", "pads", "outline", "highlight-style", "highlight-offset", "highlight-on-top"]) missing = required - set(style.keys()) if missing: raise RuntimeError("Missing following keys in style {}: {}" .format(style_file, ", ".join(missing))) extra = set(style.keys()) - required for x in extra: print("Warning: extra key '" + x + "' in style") # ToDo: Check validity of colors (SVG compatible format) return style def load_remapping(remap_file): if not remap_file: return {} try: with open(remap_file, "r") as f: return json.load(f) except IOError: raise RuntimeError("Cannot open remapping file " + remap_file) def adjust_lib_path(path): base = os.path.dirname(__file__) if path == "default" or path == "kicad-default": return os.path.join(base, "footprints", "KiCAD-base") if path == "eagle-default": return os.path.join(base, "footprints", "Eagle-export") return path def main(): parser = argparse.ArgumentParser() parser.add_argument("-s", "--style", help="JSON file with board style") parser.add_argument("board", help=".kicad_pcb file to draw") parser.add_argument("output", help="destination for final SVG or PNG file") parser.add_argument("-l", "--libs", help="coma separated list of libraries; use default, kicad-default or eagle-default for built-in libraries", default="default") parser.add_argument("-p", "--placeholder", action="store_true", help="show placeholder for missing components") parser.add_argument("-m", "--remap", help="JSON file with map part reference to <lib>:<model> to remap packages") parser.add_argument("-c", "--list-components", action="store_true", help="Dry run, just list the components") parser.add_argument("--no-drillholes", action="store_true", help="Do not make holes transparent") parser.add_argument("-b","--back", action="store_true", help="render the backside of the board") parser.add_argument("--mirror", action="store_true", help="mirror the board") parser.add_argument("-a", "--highlight", help="comma separated list of components to highlight") parser.add_argument("-f", "--filter", help="comma separated list of components to show") parser.add_argument("-v", "--vcuts", action="store_true", help="Render V-CUTS on the Cmts.User layer") parser.add_argument("--silent", action="store_true", help="Silent warning messages about missing footprints") args = parser.parse_args() args.libs = [adjust_lib_path(path) for path in args.libs.split(',')] args.highlight = args.highlight.split(',') if args.highlight is not None else [] args.filter = args.filter.split(',') if args.filter is not None else None try: if args.style: style = load_style(args.style) else: style = default_style remapping = load_remapping(args.remap) except RuntimeError as e: print(e) sys.exit(1) if os.path.splitext(args.output)[-1].lower() not in [".svg", ".png"]: print("Output can be either an SVG or PNG file") sys.exit(1) try: board = pcbnew.LoadBoard(args.board) except IOError: print("Cannot open board " + args.board) sys.exit(1) if args.list_components: walk_components(board, args.back,lambda lib, name, val, ref, pos: print_component(args.libs, lib, name, val, ref, pos, remapping=remapping)) sys.exit(0) bb = board.ComputeBoundingBox() transform_string = "" if(args.back ^ args.mirror): transform_string = "translate({}, {}) scale(-1,1)".format(ki2dmil(bb.GetX()+bb.GetWidth()), ki2dmil(-bb.GetY())) else: transform_string = "translate({}, {})".format(ki2dmil(-bb.GetX()), ki2dmil(-bb.GetY())) document = empty_svg( width="{}cm".format(bb.GetWidth()/10000000.0), height="{}cm".format(bb.GetHeight()/10000000.0), viewBox="0 0 {} {}".format(ki2dmil(bb.GetWidth()), ki2dmil(bb.GetHeight()))) board_cont = etree.SubElement(document.getroot(), "g", transform=transform_string) if style["highlight-on-top"]: comp_cont = etree.SubElement(document.getroot(), "g", transform=transform_string) high_cont = etree.SubElement(document.getroot(), "g", transform=transform_string) else: high_cont = etree.SubElement(document.getroot(), "g", transform=transform_string) comp_cont = etree.SubElement(document.getroot(), "g", transform=transform_string) board_cont.attrib["id"] = "boardContainer" comp_cont.attrib["id"] = "componentContainer" high_cont.attrib["id"] = "highlightContainer" components = { "container": comp_cont, "placeholder": args.placeholder, "remapping": remapping, "libraries": args.libs, "filter": args.filter } highlight = { "container": high_cont, "items": args.highlight, "style": style["highlight-style"], "padding": style["highlight-padding"] } board_cont.append(get_board_substrate(board, style, not args.no_drillholes, args.back)) if args.vcuts: board_cont.append(get_layers(board, style, [("vcut", [pcbnew.Cmts_User], process_board_substrate_layer)])) walk_components(board, args.back, lambda lib, name, val, ref, pos: component_from_library(lib, name, val, ref, pos, components, highlight, args.silent)) # make another pass for search, and if found, render the back side of the component # the function will search for file with extension ".back.svg" walk_components(board, not args.back, lambda lib, name, val, ref, pos: component_from_library(lib, name+".back", val, ref, pos, components, highlight, args.silent)) if args.output.endswith(".svg") or args.output.endswith(".SVG"): document.write(args.output) else: with tempfile.NamedTemporaryFile(suffix=".svg") as tmp_f: document.write(tmp_f) tmp_f.flush() svg_to_png(tmp_f.name, args.output) if __name__ == '__main__': main()