Last active
October 30, 2024 03:31
-
-
Save dduan/251cb9816787f3e4125f5cb197d2144e to your computer and use it in GitHub Desktop.
Python script that resizes SVG to new canvas size
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Resize a SVG file to a new size. Coordinates of paths and gradient definitions get transposed to corresponding | |
values in the new canvas. Everything else remain unchanged. | |
""" | |
import re | |
from argparse import ArgumentParser | |
from xml.etree import ElementTree | |
_SVG_NAMESPACE = 'http://www.w3.org/2000/svg' | |
_VALID_PATH_COMMANDS = "MLTHVCSQAZ" | |
_RE_FLOAT = re.compile(r'[+-]?\d*(\.\d+)?') | |
def resize_path(path, x_factor, y_factor): | |
result = "" | |
index = 0 | |
length = len(path) | |
def eat_number(factor): | |
nonlocal result | |
nonlocal index | |
match = _RE_FLOAT.match(path[index:]) | |
if not match: | |
return | |
found = match.group(0) | |
scaled = factor * float(found) | |
index += len(found) | |
result += f"{scaled:.4f}".rstrip('0').rstrip('.') | |
def skip_space(): | |
nonlocal index | |
while path[index] == " " or path[index] == ",": | |
index += 1 | |
def eat_space(): | |
nonlocal result | |
skip_space() | |
result += " " | |
def eat_scale_xy(): | |
nonlocal result | |
eat_number(x_factor) | |
skip_space() | |
result += "," | |
eat_number(y_factor) | |
def eat_for_command(command): | |
if command in "MLT": | |
eat_scale_xy() | |
elif command == "H": | |
eat_number(x_factor) | |
elif command == "V": | |
eat_number(y_factor) | |
elif command == "C": | |
eat_scale_xy() | |
eat_space() | |
eat_scale_xy() | |
eat_space() | |
eat_scale_xy() | |
elif command in "SQ": | |
eat_scale_xy() | |
eat_space() | |
eat_scale_xy() | |
elif command == "A": | |
eat_scale_xy() | |
eat_space() | |
eat_number(1) # x-axis-rotation | |
eat_space() | |
eat_number(1) # large-arc-flag | |
eat_space() | |
eat_number(1) # sweep-flag | |
eat_space() | |
eat_scale_xy() | |
elif command == "Z": | |
pass | |
else: | |
raise ValueError("Unknown command", command) | |
repeating_command = '' | |
while index < length: | |
skip_space() | |
lead = path[index].upper() | |
if lead in _VALID_PATH_COMMANDS: | |
result += path[index] | |
index += 1 | |
eat_for_command(lead) | |
repeating_command = lead | |
else: | |
result += " " | |
eat_for_command(repeating_command) | |
return result | |
def _resize_element_path(el, x_factor, y_factor): | |
path = el.get('d') | |
el.set('d', resize_path(path, x_factor, y_factor)) | |
def _resize_element_svg(el, width, height): | |
assert type(width) == str | |
assert type(height) == str | |
el.set('width', width) | |
el.set('height', height) | |
el.set('viewBox', f'0 0 {width} {height}') | |
def _resize_element_gradient(el, x_factor, y_factor): | |
for attr in ['x1', 'y1', 'x2', 'y2']: | |
value = el.get(attr) | |
if value: | |
factor = x_factor if attr.startswith('x') else y_factor | |
new_value = float(value) * factor | |
el.set(attr, str(new_value)) | |
def resize_svg(source, width, height): | |
"Resize source svg to `width` and `height`" | |
ElementTree.register_namespace('', _SVG_NAMESPACE) | |
x_factor = 1 | |
y_factor = 1 | |
root = ElementTree.fromstring(source) | |
for element in root.iter(): | |
if element.tag.endswith('svg'): | |
viewbox = element.get('viewBox').split(' ') | |
_resize_element_svg(element, str(width), str(height)) | |
x_factor = 1.0 / float(viewbox[2]) * float(width) | |
y_factor = 1.0 / float(viewbox[3]) * float(height) | |
elif element.tag.endswith('Gradient'): # (linear|radial)Gradient | |
_resize_element_gradient(element, x_factor, y_factor) | |
elif element.tag.endswith('path'): | |
_resize_element_path(element, x_factor, y_factor) | |
return ElementTree.tostring(root) | |
def main(): | |
arg_parser = ArgumentParser(description="Resize SVG files.") | |
arg_parser.add_argument( | |
'source', | |
metavar='SOURCE_SVG', | |
help='Original SVG to resize' | |
) | |
arg_parser.add_argument( | |
'width', | |
metavar='WIDTH', | |
type=float, | |
help='Width of the new SVG' | |
) | |
arg_parser.add_argument( | |
'height', | |
metavar='HEIGHT', | |
type=float, | |
help='Height of the new SVG' | |
) | |
args = arg_parser.parse_args() | |
print(resize_svg(open(args.source).read(), args.width, args.height)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I want to scale an svg and include the stroke width as well. So, from L126
I followed it with,
Which works, but is not perfect. If the x and y axis are scaled non-uniformly the stroke width would just take the x_factor.
One alternative is to use the geometric mean of x_factor and y_factor, like this: