CREATIVEWITY / SCRIPT SOURCEPython 3.9+ / standard library / MIT

Readable source

#!/usr/bin/env python3
"""Set SVG display dimensions while preserving its drawing viewBox. Original stays untouched."""
import argparse,re,xml.etree.ElementTree as ET
from pathlib import Path
def pixels(s):
    m=re.fullmatch(r'\s*([0-9]+(?:\.[0-9]+)?)\s*(px|mm|cm|in|pt|pc)?\s*',s or '')
    if not m:raise ValueError('Cannot infer a viewBox from relative or missing dimensions.')
    return float(m[1])*{'px':1,'mm':96/25.4,'cm':96/2.54,'in':96,'pt':96/72,'pc':16}[m[2] or 'px']
def normalise(source,width,height):
    if '<!doctype' in source.lower() or '<!entity' in source.lower():raise ValueError('DOCTYPE and entities are not accepted.')
    root=ET.fromstring(source)
    if root.tag.split('}')[-1]!='svg':raise ValueError('Root is not SVG.')
    box=root.get('viewBox')
    if not box:
        w,h=pixels(root.get('width')),pixels(root.get('height'))
        if w<=0 or h<=0:raise ValueError('Dimensions must be positive.')
        root.set('viewBox',f'0 0 {w:g} {h:g}')
    else:
        values=re.split('[ ,]+',box.strip())
        if len(values)!=4 or float(values[2])<=0 or float(values[3])<=0:raise ValueError('Invalid viewBox.')
    root.set('width',f'{width:g}');root.set('height',f'{height:g}');root.set('preserveAspectRatio','xMidYMid meet')
    ET.register_namespace('','http://www.w3.org/2000/svg');ET.register_namespace('xlink','http://www.w3.org/1999/xlink')
    return ET.tostring(root,encoding='unicode')
def main():
    p=argparse.ArgumentParser(description=__doc__);p.add_argument('input',type=Path);p.add_argument('output',type=Path);p.add_argument('--width',type=float,required=True);p.add_argument('--height',type=float,required=True);a=p.parse_args()
    if not 0<a.width<=100000 or not 0<a.height<=100000:p.error('Dimensions must be positive and at most 100000 px.')
    try:
        if a.input.resolve()==a.output.resolve():raise ValueError('Output must differ from input.')
        if a.output.exists():raise ValueError('Output exists. Choose a new filename.')
        if a.input.stat().st_size>5*1024*1024:raise ValueError('File exceeds 5 MB.')
        a.output.write_text(normalise(a.input.read_text(encoding='utf-8'),a.width,a.height),encoding='utf-8')
    except (ValueError,OSError,ET.ParseError) as e:p.error(str(e))
if __name__=='__main__':main()