159 lines
6.0 KiB
Python
159 lines
6.0 KiB
Python
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):
|
||
"""确保所有有描边的元素的描边宽度不随缩放变化"""
|
||
for elem in root.iter():
|
||
if 'stroke' in elem.attrib and elem.attrib['stroke'] != 'none':
|
||
# 强制设置 stroke-width=1(无单位)
|
||
elem.set('stroke-width', '1')
|
||
# 确保 vector-effect 生效
|
||
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:.3f} {new_height:.3f}')
|
||
root.set('width', f'{new_width:.3f}')
|
||
root.set('height', f'{new_height:.3f}')
|
||
|
||
# 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\work'
|
||
output_dir = r'C:\Users\y.wang\Documents\SSG-Ruledesigner-Konfigurator\SVGs\Omniflo\Weichen'
|
||
batch_process_svg(input_dir, output_dir)
|