Skip to content

Instantly share code, notes, and snippets.

@dduan
Last active October 30, 2024 03:31
Show Gist options
  • Save dduan/251cb9816787f3e4125f5cb197d2144e to your computer and use it in GitHub Desktop.
Save dduan/251cb9816787f3e4125f5cb197d2144e to your computer and use it in GitHub Desktop.
Python script that resizes SVG to new canvas size
"""
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()
@DeadBranches
Copy link

I want to scale an svg and include the stroke width as well. So, from L126

        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)

I followed it with,

            stroke_width = element.get("stroke-width")
            if not stroke_width:
                continue
            element.set("stroke-width", str(float(stroke_width) * x_factor))

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:

            stroke_width = element.get("stroke-width")
            if not stroke_width:
                continue
            if x_factor == y_factor:
                element.set("stroke-width", str(float(stroke_width) * x_factor))
            if x_factor != y_factor:
                # A non-uniform scaling is being done.
                import math
                scale_factor = math.sqrt(x_factor * y_factor)
                element.set("stroke-width", str(float(stroke_width) * scale_factor))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment