|
from genericpath import isdir |
|
from bs4 import BeautifulSoup, NavigableString |
|
import re |
|
from svgpathtools import parse_path |
|
import math |
|
|
|
|
|
STYLE_ATTRS = ( |
|
'fill', 'fill-rule', 'stroke', 'stroke-width', 'stroke-linecap', |
|
'stroke-linejoin', 'd', 'width', 'height', 'x', 'y', 'rx', 'ry', |
|
'cx', 'cy', 'r' |
|
) |
|
|
|
|
|
def children_elements(parent): |
|
return [ |
|
x for x in parent.children |
|
if not isinstance(x, NavigableString) |
|
] |
|
|
|
|
|
def remove_groups(root): |
|
if not root: |
|
return |
|
childrens = children_elements(root) |
|
if len(childrens) == 0: |
|
return |
|
for item in childrens: |
|
remove_groups(item) |
|
if root.name == 'g': |
|
for item in childrens: |
|
for style in STYLE_ATTRS: |
|
if style in root.attrs and style not in item.attrs: |
|
item.attrs[style] = root.attrs[style] |
|
root.unwrap() |
|
|
|
|
|
def remove_xml_br(xml): |
|
return re.sub(r'\n[ ]*', ' ', xml) |
|
|
|
|
|
def extract_styles(original): |
|
return { |
|
k: re.sub(r'\n? +', ' ', v) for k, v in original.items() |
|
if k in STYLE_ATTRS |
|
} |
|
|
|
|
|
def format_svg(xml, fit_viewbox=False, stroke_width=None): |
|
soup = BeautifulSoup(remove_xml_br(xml), "xml") |
|
remove_groups(soup.svg) |
|
x_min, x_max, y_min, y_max = math.inf, 0, math.inf, 0 |
|
for path in children_elements(soup.svg): |
|
# Change color |
|
if ('stroke' in path.attrs) and (path.attrs['stroke'] != 'none'): |
|
path.attrs['stroke'] = 'currentColor' |
|
elif ('fill' not in path.attrs) or (path.attrs['fill'] != 'none'): |
|
path.attrs['fill'] = 'currentColor' |
|
# Format path |
|
path.attrs = extract_styles(path.attrs) |
|
if stroke_width and 'stroke-width' in path.attrs: |
|
path.attrs['stroke-width'] = stroke_width |
|
if fit_viewbox \ |
|
and ((path.attrs.get('fill', 'none') != 'none') |
|
or (path.attrs.get('stroke', 'none') != 'none')) \ |
|
and 'd' in path.attrs: |
|
x1, x2, y1, y2 = parse_path(path.attrs['d']).bbox() |
|
x_min = min(x_min, x1) |
|
x_max = max(x_max, x2) |
|
y_min = min(y_min, y1) |
|
y_max = max(y_max, y2) |
|
# Center paths in viewBox |
|
if fit_viewbox: |
|
width, height = x_max-x_min, y_max-y_min |
|
size = max(width, height) |
|
x0, y0 = (x_max+x_min)/2, (y_max+y_min)/2 |
|
x, y, w, h = round(max(0, x0-size/2), 1), round(max(y0 - size/2, 0), 1), round(size, 1), round(size, 1) |
|
viewbox = f'{x} {y} {w} {h}' |
|
else: |
|
viewbox = soup.svg.attrs['viewBox'] |
|
soup.svg.attrs = { |
|
'viewBox': viewbox, |
|
'id': 'main', |
|
'xmlns': "http://www.w3.org/2000/svg", |
|
**extract_styles(soup.svg.attrs) |
|
} |
|
if soup.svg.attrs['width']: |
|
del soup.svg.attrs['width'] |
|
if soup.svg.attrs['height']: |
|
del soup.svg.attrs['height'] |
|
return remove_xml_br(soup.decode_contents(formatter="minimal")) |
|
|
|
|
|
def format_svg_file(in_file, out_file, fit_viewbox=False, stroke_width=None, in_enc='utf8', out_enc='utf8'): |
|
with open(in_file, 'r', encoding=in_enc) as f: |
|
xml = f.read() |
|
with open(out_file, 'w', encoding=out_enc) as f: |
|
f.write(format_svg(xml, fit_viewbox, stroke_width)) |
|
|
|
|
|
if __name__ == '__main__': |
|
from os import listdir, path |
|
from sys import argv, stderr |
|
import argparse |
|
|
|
parser = argparse.ArgumentParser() |
|
parser.add_argument("input", help="input file or directory") |
|
parser.add_argument("-f", "--fit-viewbox", default=False, action="store_true", |
|
help="auto-fit viewbox to content outline") |
|
parser.add_argument("-s", "--stroke-width", default=None, type=float, help="change path stroke width") |
|
parser.add_argument("-o", "--output", help="output file or directory") |
|
if len(argv) <= 1: |
|
parser.print_help(stderr) |
|
exit() |
|
args = parser.parse_args() |
|
|
|
in_ = args.input |
|
out_ = args.output |
|
if path.isdir(in_) and path.isdir(out_): |
|
in_files = [path.join(in_, x) for x in listdir(in_)] |
|
out_files = [path.join(out_, x) for x in listdir(in_)] |
|
elif path.isfile(in_): |
|
in_files = [in_] |
|
if path.isdir(out_): |
|
filename = path.basename(in_) |
|
out_files = [path.join(out_, filename)] |
|
elif path.isfile(out_): |
|
out_files = [out_] |
|
else: |
|
raise ValueError("Invalid input/output parameters!") |
|
else: |
|
raise ValueError("Invalid input/output parameters!") |
|
for in_, out_ in zip(in_files, out_files): |
|
if not in_.endswith('.svg'): |
|
continue |
|
try: |
|
format_svg_file(in_, out_, args.fit_viewbox, args.stroke_width) |
|
except Exception as e: |
|
print(in_) |
|
raise e |