RD_CONF für SVG zu bearbeiten

This commit is contained in:
2025-06-24 13:30:47 +02:00
parent 9783ab756b
commit 97807e3a34
28 changed files with 3380 additions and 257 deletions
+332
View File
@@ -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()
+155
View File
@@ -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)