RD_CONF für SVG zu bearbeiten
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from xml.dom import minidom
|
||||
import argparse
|
||||
|
||||
def analyze_and_normalize_path(d, path_type, path_id=""):
|
||||
"""Strictly normalize path direction and generate detailed report"""
|
||||
original_d = d
|
||||
report = []
|
||||
normalized = []
|
||||
changed = False
|
||||
|
||||
# Ensure path starts with M command
|
||||
if not d.strip().startswith('M'):
|
||||
d = 'M' + d[1:] if d.startswith('L') else 'M ' + d
|
||||
report.append(" Fix: Added M command at path start")
|
||||
changed = True
|
||||
|
||||
# Parse path commands
|
||||
commands = []
|
||||
current_cmd = None
|
||||
for token in re.split('([A-Za-z])', d):
|
||||
if not token:
|
||||
continue
|
||||
if token in 'MLAZHVCSQTa-z':
|
||||
current_cmd = token
|
||||
else:
|
||||
if current_cmd:
|
||||
commands.append((current_cmd, token.strip()))
|
||||
|
||||
prev_x, prev_y = None, None
|
||||
|
||||
for cmd, params in commands:
|
||||
params = [float(p) for p in re.split('[, ]+', params.strip()) if p]
|
||||
|
||||
if cmd == 'M':
|
||||
if len(params) >= 2:
|
||||
x, y = params[0], params[1]
|
||||
normalized.append(f"M {x} {y}")
|
||||
prev_x, prev_y = x, y
|
||||
continue
|
||||
|
||||
if path_type == 'Line segment' and cmd == 'L':
|
||||
if len(params) >= 2 and prev_x is not None:
|
||||
x, y = params[0], params[1]
|
||||
need_swap = x < prev_x or (x == prev_x and y < prev_y)
|
||||
|
||||
if need_swap:
|
||||
new_segment = [f"M {x} {y}", f"L {prev_x} {prev_y}"]
|
||||
report.append(f" Need to swap start/end → New path: {' '.join(new_segment)}")
|
||||
normalized = new_segment
|
||||
changed = True
|
||||
else:
|
||||
normalized.append(f"L {x} {y}")
|
||||
report.append(" Path direction already correct, no changes made")
|
||||
|
||||
prev_x, prev_y = x, y
|
||||
continue
|
||||
|
||||
if path_type == 'Arc' and cmd == 'A':
|
||||
if len(params) >= 7 and prev_x is not None:
|
||||
rx, ry, xrot, large, sweep, x, y = params[0], params[1], params[2], int(params[3]), int(params[4]), params[5], params[6]
|
||||
|
||||
# Force left-to-right clockwise
|
||||
need_swap = x < prev_x
|
||||
new_sweep = 1
|
||||
|
||||
if need_swap:
|
||||
new_segment = [f"M {x} {y}", f"A {rx} {ry} {xrot} {large} {new_sweep} {prev_x} {prev_y}"]
|
||||
report.append(f" Need to swap start/end → New path: {' '.join(new_segment)}")
|
||||
normalized = new_segment
|
||||
changed = True
|
||||
else:
|
||||
if sweep != new_sweep:
|
||||
new_segment = [f"A {rx} {ry} {xrot} {large} {new_sweep} {x} {y}"]
|
||||
report.append(f" Need to adjust sweep to 1 → New path: {' '.join(new_segment)}")
|
||||
normalized.extend(new_segment)
|
||||
changed = True
|
||||
else:
|
||||
normalized.append(f"A {rx} {ry} {xrot} {large} {sweep} {x} {y}")
|
||||
report.append(" Arc direction already correct, no changes made")
|
||||
|
||||
prev_x, prev_y = x, y
|
||||
continue
|
||||
|
||||
normalized.append(f"{cmd} {' '.join(map(str, params))}")
|
||||
|
||||
new_d = ' '.join(normalized)
|
||||
report_header = f"[{path_id}-Analysis] Type: {path_type}"
|
||||
full_report = [report_header, f"Original path: {original_d}"] + report
|
||||
|
||||
if changed:
|
||||
full_report.append(f"Modified path: {new_d}")
|
||||
else:
|
||||
full_report.append("Path not modified")
|
||||
|
||||
return new_d, '\n'.join(full_report)
|
||||
|
||||
def optimize_svg(input_path, output_path):
|
||||
"""Process single SVG file with all optimizations"""
|
||||
try:
|
||||
# Register namespace
|
||||
ET.register_namespace('', 'http://www.w3.org/2000/svg')
|
||||
|
||||
# Parse SVG file
|
||||
tree = ET.parse(input_path)
|
||||
root = tree.getroot()
|
||||
|
||||
print(f"\nProcessing file: {os.path.basename(input_path)}")
|
||||
print("="*60)
|
||||
|
||||
# Create parent map
|
||||
parent_map = {c: p for p in tree.iter() for c in p}
|
||||
|
||||
# 1. Remove xlink namespace
|
||||
for attr in list(root.attrib):
|
||||
if 'xlink' in attr:
|
||||
del root.attrib[attr]
|
||||
|
||||
# 2. Set standard dimensions
|
||||
root.set('width', '1e3')
|
||||
root.set('height', '1e3')
|
||||
root.set('viewBox', '0 0 1e3 1e3')
|
||||
|
||||
# 3. Remove all clipPath definitions and references
|
||||
defs = root.find('{http://www.w3.org/2000/svg}defs')
|
||||
if defs is not None:
|
||||
clip_paths = defs.findall('{http://www.w3.org/2000/svg}clipPath')
|
||||
for cp in clip_paths:
|
||||
defs.remove(cp)
|
||||
if len(defs) == 0:
|
||||
root.remove(defs)
|
||||
|
||||
# 4. Remove clip-path attributes
|
||||
for elem in tree.iter():
|
||||
if 'clip-path' in elem.attrib:
|
||||
del elem.attrib['clip-path']
|
||||
|
||||
# 5. Force square line caps
|
||||
for g in root.findall('.//{http://www.w3.org/2000/svg}g'):
|
||||
g.set('stroke-linecap', 'square')
|
||||
|
||||
# 6. Remove dashed line styles
|
||||
for elem in tree.iter():
|
||||
if 'stroke-dasharray' in elem.attrib:
|
||||
del elem.attrib['stroke-dasharray']
|
||||
|
||||
# 7. Completely remove empty groups
|
||||
removed_groups = True
|
||||
while removed_groups:
|
||||
removed_groups = False
|
||||
for g in root.findall('.//{http://www.w3.org/2000/svg}g'):
|
||||
is_empty = (
|
||||
len(g) == 0 and
|
||||
not (g.text or '').strip() and
|
||||
not (g.tail or '').strip() and
|
||||
all(not k.startswith('{') for k in g.attrib))
|
||||
if is_empty:
|
||||
parent = parent_map.get(g)
|
||||
if parent is not None:
|
||||
parent.remove(g)
|
||||
removed_groups = True
|
||||
parent_map = {c: p for p in tree.iter() for c in p}
|
||||
|
||||
# 8. Convert polylines to paths
|
||||
for polyline in root.findall('.//{http://www.w3.org/2000/svg}polyline'):
|
||||
points = polyline.get('points', '').strip()
|
||||
if points:
|
||||
coords = [p for p in re.split(r'[\s,]', points) if p]
|
||||
path_data = []
|
||||
for i in range(0, len(coords), 2):
|
||||
if i == 0:
|
||||
path_data.append(f"M {coords[i]} {coords[i+1]}")
|
||||
else:
|
||||
path_data.append(f"L {coords[i]} {coords[i+1]}")
|
||||
|
||||
path = ET.Element('{http://www.w3.org/2000/svg}path')
|
||||
new_d, analysis_report = analyze_and_normalize_path(
|
||||
' '.join(path_data),
|
||||
'Line segment',
|
||||
"Polyline conversion"
|
||||
)
|
||||
path.set('d', new_d)
|
||||
print(f"\n[Polyline conversion analysis]\n{analysis_report}")
|
||||
|
||||
for attr, value in polyline.attrib.items():
|
||||
if attr not in ('points', 'stroke-dasharray'):
|
||||
path.set(attr, value)
|
||||
|
||||
parent = parent_map.get(polyline)
|
||||
if parent is not None:
|
||||
parent.remove(polyline)
|
||||
parent.append(path)
|
||||
|
||||
# 9. Normalize path directions (final step)
|
||||
group_num = 0
|
||||
for g in root.findall('.//{http://www.w3.org/2000/svg}g'):
|
||||
group_num += 1
|
||||
path_num = 0
|
||||
group_report = []
|
||||
|
||||
for path in g.findall('.//{http://www.w3.org/2000/svg}path'):
|
||||
path_num += 1
|
||||
if 'd' not in path.attrib:
|
||||
continue
|
||||
|
||||
# Auto-detect path type
|
||||
path_type = 'Arc' if 'A' in path.get('d') else 'Line segment'
|
||||
path_id = f"Group{group_num}-Path{path_num}"
|
||||
|
||||
new_d, analysis_report = analyze_and_normalize_path(
|
||||
path.get('d'),
|
||||
path_type,
|
||||
path_id
|
||||
)
|
||||
|
||||
if new_d != path.get('d'):
|
||||
path.set('d', new_d)
|
||||
|
||||
group_report.append(analysis_report)
|
||||
|
||||
# Print all path reports for current group
|
||||
if group_report:
|
||||
print(f"\n=== Group {group_num} Path Analysis ===")
|
||||
print('\n\n'.join(group_report))
|
||||
|
||||
# 10. Update colors and line width
|
||||
for elem in root.findall('.//*[@stroke]'):
|
||||
stroke = elem.get('stroke', '').lower()
|
||||
if stroke == '#0ff' or stroke == 'rgb(0,255,255)':
|
||||
elem.set('stroke', '#ffe31b')
|
||||
elif stroke == '#000' or stroke == 'rgb(255,0,0)'or stroke == 'rgb(0,0,0)':
|
||||
elem.set('stroke', '#ec2525')
|
||||
elem.set('stroke-width', '1')
|
||||
|
||||
# Generate final XML
|
||||
rough_string = ET.tostring(root, encoding='utf-8', xml_declaration=True)
|
||||
rough_string = rough_string.replace(b'standalone="no"', b'')
|
||||
|
||||
# Format output (remove empty lines)
|
||||
reparsed = minidom.parseString(rough_string)
|
||||
pretty_svg = reparsed.toprettyxml(indent=' ', encoding='utf-8')
|
||||
pretty_svg = b'\n'.join(
|
||||
line for line in pretty_svg.splitlines()
|
||||
if line.strip()
|
||||
).replace(b'<?xml version="1.0" ?>', b'<?xml version="1.0" encoding="utf-8"?>')
|
||||
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(pretty_svg)
|
||||
|
||||
print("\n" + "="*60)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nProcessing error: {str(e)}")
|
||||
print("="*60)
|
||||
return False
|
||||
|
||||
def batch_process_svgs(input_dir, output_dir):
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
|
||||
for filename in os.listdir(input_dir):
|
||||
if os.path.isdir(os.path.join(input_dir, filename)):
|
||||
continue
|
||||
if filename.lower().endswith('.svg') or '_new.svg' in filename.lower():
|
||||
input_path = os.path.join(input_dir, filename)
|
||||
output_filename = filename.replace('_new.svg', '_optimized.svg')
|
||||
output_path = os.path.join(output_dir, output_filename)
|
||||
|
||||
if optimize_svg(input_path, output_path):
|
||||
success_count += 1
|
||||
print(f"✓ Processed successfully: {filename} → {output_filename}")
|
||||
else:
|
||||
failure_count += 1
|
||||
print(f"✗ Processing failed: {filename}")
|
||||
print("\n" + "="*60 + "\n")
|
||||
|
||||
print("\nProcessing summary:")
|
||||
print(f"Successfully processed: {success_count} files")
|
||||
print(f"Failed to process: {failure_count} files")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser(description='copies svg files from directory and optimizes them', prog='svg_optimizer')
|
||||
parser.add_argument('-f', '--file', action='store', help='just optimize this file')
|
||||
parser.add_argument('-i', '--inputdir', action='store', help='input directory for all svg files which should be rewritten. Mandatory argument')
|
||||
parser.add_argument('-o', '--outputdir', action='store', help='output directory for all svg files which are writte new')
|
||||
parser.add_argument('-c', '--console', action='store_true', help='put the result to console')
|
||||
parser.add_argument( '--bogen', action='store_true', help='just optimize all "bogen"')
|
||||
parser.add_argument( '--tefbogen', action='store_true', help='just optimize all "tefbogen"')
|
||||
parser.add_argument( '--weichen', action='store_true', help='just optimize all "weichen"')
|
||||
parser.add_argument( '--tefweichen', action='store_true', help='just optimize all "tefweichen"')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
in_dir = None
|
||||
if args.bogen:
|
||||
in_dir = os.environ.get('RD_CONF_BOGEN')
|
||||
if args.tefbogen:
|
||||
in_dir = os.environ.get('RD_CONF_TEFBOGEN')
|
||||
if args.weichen:
|
||||
in_dir = os.environ.get('RD_CONF_WEICHEN')
|
||||
if args.tefweichen:
|
||||
in_dir = os.environ.get('RD_CONF_TEFWEICHEN')
|
||||
|
||||
out_dir = os.environ.get('RD_CONF_WORK')
|
||||
|
||||
if args.outputdir:
|
||||
out_dir = args.outputdir
|
||||
|
||||
|
||||
if args.file:
|
||||
filename = args.file
|
||||
input_path = os.path.join(in_dir, filename)
|
||||
if os.path.exists(input_path):
|
||||
optimize_svg(input_path, out_dir)
|
||||
else:
|
||||
print(f"file {filename} does not exist")
|
||||
|
||||
if in_dir:
|
||||
"""Batch process SVG files"""
|
||||
batch_process_svgs(in_dir, out_dir)
|
||||
else:
|
||||
parser.print_help()
|
||||
@@ -0,0 +1,155 @@
|
||||
import os
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Tuple, List
|
||||
|
||||
def parse_svg_path(d: str) -> List[Tuple[float, float]]:
|
||||
"""Extract all coordinates from SVG path data (including curve control points)"""
|
||||
points = []
|
||||
commands = re.findall(
|
||||
r'([MmLlCcQqAaHhVvZz])([^MmLlCcQqAaHhVvZz]*)',
|
||||
d.strip().replace(',', ' ')
|
||||
)
|
||||
current_pos = (0.0, 0.0)
|
||||
|
||||
for cmd, args in commands:
|
||||
args = list(map(float, re.findall(r'[-+]?\d*\.?\d+', args)))
|
||||
if cmd in ('M', 'm', 'L', 'l'): # Move/line commands
|
||||
for i in range(0, len(args), 2):
|
||||
x, y = args[i], args[i+1]
|
||||
if cmd.islower(): # Relative coordinates
|
||||
x += current_pos[0]
|
||||
y += current_pos[1]
|
||||
points.append((x, y))
|
||||
current_pos = (x, y)
|
||||
elif cmd in ('C', 'c'): # Cubic Bezier curves
|
||||
for i in range(0, len(args), 6):
|
||||
x1, y1, x2, y2, x, y = args[i:i+6]
|
||||
if cmd.islower():
|
||||
x1 += current_pos[0]; y1 += current_pos[1]
|
||||
x2 += current_pos[0]; y2 += current_pos[1]
|
||||
x += current_pos[0]; y += current_pos[1]
|
||||
points.extend([(x1, y1), (x2, y2), (x, y)])
|
||||
current_pos = (x, y)
|
||||
elif cmd in ('A', 'a'): # Arc commands (start/end points only)
|
||||
for i in range(0, len(args), 7):
|
||||
x, y = args[i+5], args[i+6]
|
||||
if cmd.islower():
|
||||
x += current_pos[0]
|
||||
y += current_pos[1]
|
||||
points.append((x, y))
|
||||
current_pos = (x, y)
|
||||
return points
|
||||
|
||||
def calculate_bounding_box(svg_file: str) -> Tuple[float, float, float, float]:
|
||||
"""Calculate true bounding box of all paths in SVG"""
|
||||
tree = ET.parse(svg_file)
|
||||
root = tree.getroot()
|
||||
all_points = []
|
||||
|
||||
for path in root.findall('.//{http://www.w3.org/2000/svg}path'):
|
||||
d = path.get('d', '')
|
||||
all_points.extend(parse_svg_path(d))
|
||||
|
||||
if not all_points:
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
x_min = min(p[0] for p in all_points)
|
||||
x_max = max(p[0] for p in all_points)
|
||||
y_min = min(p[1] for p in all_points)
|
||||
y_max = max(p[1] for p in all_points)
|
||||
return (x_min, y_min, x_max, y_max)
|
||||
|
||||
|
||||
def normalize_stroke_widths2(root, scale):
|
||||
"""Normalize stroke widths to ensure consistent appearance after scaling"""
|
||||
for elem in root.iter():
|
||||
if 'stroke-width' in elem.attrib:
|
||||
# Remove existing stroke-width
|
||||
del elem.attrib['stroke-width']
|
||||
# Apply scaled stroke width (e.g., 1px → 1/scale)
|
||||
elem.set('stroke-width', f'{1/scale:.6f}mm')
|
||||
|
||||
def apply_non_scaling_stroke(root):
|
||||
"""Ensure stroke widths don't scale with the SVG"""
|
||||
for elem in root.iter():
|
||||
if 'stroke' in elem.attrib and elem.attrib['stroke'] != 'none':
|
||||
elem.set('vector-effect', 'non-scaling-stroke')
|
||||
|
||||
def scale_svg_file(input_path: str, output_path: str):
|
||||
"""Scale SVG file with consistent stroke widths"""
|
||||
print(f"\nProcessing: {os.path.basename(input_path)}")
|
||||
|
||||
tree = ET.parse(input_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Calculate original bounding box
|
||||
x_min, y_min, x_max, y_max = calculate_bounding_box(input_path)
|
||||
width = x_max - x_min
|
||||
height = y_max - y_min
|
||||
|
||||
print(f"Original bounding box: ({x_min:.3f}, {y_min:.3f}) to ({x_max:.3f}, {y_max:.3f})")
|
||||
print(f"Original dimensions: {width:.3f} (w) × {height:.3f} (h)")
|
||||
|
||||
|
||||
# Determine scaling base (larger dimension → 1000px)
|
||||
if width > height:
|
||||
scale = 1000.0 / width
|
||||
new_width = 1000.0
|
||||
new_height = round(height * scale)
|
||||
print(f"Scaling base: Width (larger dimension)")
|
||||
else:
|
||||
scale = 1000.0 / height
|
||||
new_height = 1000.0
|
||||
new_width = round(width * scale,3)
|
||||
print(f"Scaling base: Height (larger dimension)")
|
||||
|
||||
print(f"Scale factor: {scale:.6f}")
|
||||
print(f"New dimensions: {new_width:.3f}px × {new_height:.3f}px")
|
||||
print(f"ViewBox: 0 0 {new_width:.3f} {new_height:.3f}")
|
||||
normalize_stroke_widths2(root, scale)
|
||||
apply_non_scaling_stroke(root)
|
||||
#
|
||||
# Update SVG attributes
|
||||
root.set('viewBox', f'0 0 {new_width} {new_height}')
|
||||
root.set('width', f'{new_width}')
|
||||
root.set('height', f'{new_height}')
|
||||
|
||||
# Apply translation and scaling
|
||||
for g in root.findall('{http://www.w3.org/2000/svg}g'):
|
||||
transform = g.get('transform', '')
|
||||
new_transform = f'translate({-x_min*scale},{-y_min*scale}) scale({scale})'
|
||||
if transform:
|
||||
new_transform = f'{transform} {new_transform}'
|
||||
g.set('transform', new_transform)
|
||||
|
||||
# Save modified SVG
|
||||
tree.write(output_path, encoding='utf-8', xml_declaration=True)
|
||||
print(f"Finished processing: {os.path.basename(input_path)}")
|
||||
|
||||
def batch_process_svg(input_dir: str, output_dir: str):
|
||||
"""Batch process SVG files in directory (non-recursive)"""
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
svg_files = [
|
||||
f for f in os.listdir(input_dir)
|
||||
if f.lower().endswith('.svg') and os.path.isfile(os.path.join(input_dir, f))
|
||||
]
|
||||
|
||||
if not svg_files:
|
||||
print("No SVG files found in the input directory!")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(svg_files)} SVG files to process")
|
||||
for filename in svg_files:
|
||||
input_path = os.path.join(input_dir, filename)
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
scale_svg_file(input_path, output_path)
|
||||
|
||||
print("\nAll files processed successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
input_dir = r'C:\Users\y.wang\Documents\SSG-Ruledesigner-Konfigurator\SVGs\Omniflo\TEFBogen\NewSVG'
|
||||
output_dir = r'C:\Users\y.wang\Documents\SSG-Ruledesigner-Konfigurator\SVGs\Omniflo\TEFBogen\NewSVG\test'
|
||||
batch_process_svg(input_dir, output_dir)
|
||||
Reference in New Issue
Block a user