Skip to content

Instantly share code, notes, and snippets.

@will4381
Created June 5, 2025 22:36
Show Gist options
  • Save will4381/d6d752f2223709e604f9a9785cd09ec7 to your computer and use it in GitHub Desktop.
Save will4381/d6d752f2223709e604f9a9785cd09ec7 to your computer and use it in GitHub Desktop.
SVGs to Swift
import os
import subprocess
import re
import shutil
def to_camel_case(text):
text = text.replace('-', ' ').replace('_', ' ')
parts = text.split()
if not parts:
return ""
return "".join(word.capitalize() for word in parts)
def generate_icon_code():
assets_dir = "Sources/PlushIconKit/Assets"
generated_dir = "Sources/PlushIconKit/GeneratedIcons"
if not shutil.which("swiftdraw"):
print("Error: swiftdraw command not found. Please ensure it's installed and in your PATH.")
print("You can install it via Homebrew: brew install swiftdraw")
return
os.makedirs(generated_dir, exist_ok=True)
icon_details = []
svg_files = sorted([f for f in os.listdir(assets_dir) if f.endswith(".svg")])
if not svg_files:
print(f"No SVG files found in {assets_dir}")
return
print(f"Found {len(svg_files)} SVG files to process.")
for svg_filename in svg_files:
print(f"Processing {svg_filename}...")
if svg_filename.endswith("--Streamline-Plump.svg"):
short_name_part = svg_filename.removesuffix("--Streamline-Plump.svg")
elif svg_filename.endswith(".svg"):
short_name_part = svg_filename.removesuffix(".svg")
else:
print(f" Unrecognized SVG filename suffix for {svg_filename}. Skipping.")
continue
base_swift_name = to_camel_case(short_name_part)
if not base_swift_name:
print(f" Could not determine base_swift_name for {svg_filename} (short_name_part: '{short_name_part}'). Skipping.")
continue
original_swiftdraw_input_name = svg_filename.removesuffix(".svg")
original_swiftdraw_func_suffix = to_camel_case(original_swiftdraw_input_name)
if not original_swiftdraw_func_suffix:
print(f" Could not determine original_swiftdraw_func_suffix for {svg_filename}. Skipping.")
continue
input_svg_path = os.path.join(assets_dir, svg_filename)
output_swift_filename = f"{base_swift_name}.swift"
output_swift_path = os.path.join(generated_dir, output_swift_filename)
try:
swiftdraw_command = ["swiftdraw", input_svg_path, "--format", "swift", "--output", output_swift_path]
result = subprocess.run(
swiftdraw_command,
check=True, capture_output=True, text=True
)
except subprocess.CalledProcessError as e:
print(f" Error generating Swift code for {svg_filename}:")
print(f" Command: {' '.join(e.cmd)}")
print(f" Stdout: {e.stdout}")
print(f" Stderr: {e.stderr}")
continue
try:
with open(output_swift_path, 'r') as f:
content = f.read()
# modify the public static function declaration
public_func_original_pattern = r"static func svg" + re.escape(original_swiftdraw_func_suffix) + r"\("
public_func_new_replacement = f"static func plush{base_swift_name}(color: UIColor, "
content = re.sub(public_func_original_pattern, public_func_new_replacement, content)
# nodify the draw call within the UIGraphicsImageRenderer closure
draw_call_in_closure_pattern_str = r"(draw" + re.escape(original_swiftdraw_func_suffix) + r")(\(in: \$0\.cgContext, scale: scale\))"
draw_call_in_closure_pattern = re.compile(draw_call_in_closure_pattern_str)
def build_replacement_for_closure_draw_call(match_obj):
original_params_with_parens = match_obj.group(2)
if original_params_with_parens.endswith(')'):
params_core = original_params_with_parens[:-1]
new_call_string = f"drawPlush{base_swift_name}{params_core}, color: color)"
else:
print(f" Warning: Unexpected parameter format for closure draw call in {svg_filename}. Modification might be incorrect.")
new_call_string = f"drawPlush{base_swift_name}{original_params_with_parens}, color: color"
return new_call_string
content, num_replacements = draw_call_in_closure_pattern.subn(build_replacement_for_closure_draw_call, content)
if num_replacements == 0:
print(f" Warning: Draw call in closure for '{original_swiftdraw_func_suffix}' not found or not replaced in {svg_filename}. "
f"File '{output_swift_path}' might require manual check for dynamic color in the UIImage closure part.")
# modify the private static draw function definition
private_func_original_pattern = r"private static func draw" + re.escape(original_swiftdraw_func_suffix) + r"\(in ctx: CGContext, scale: CGSize\)"
private_func_new_replacement = f"private static func drawPlush{base_swift_name}(in ctx: CGContext, scale: CGSize, color: UIColor)"
content = re.sub(private_func_original_pattern, private_func_new_replacement, content)
# replace hardcoded fill color with the dynamic color parameter in the private draw function
color_setting_pattern = re.compile(
r"\s*let rgb = CGColorSpaceCreateDeviceRGB\(\)\s*\n"
r"\s*let color1 = CGColor\(colorSpace: rgb, components: \[(?:\d+(?:\.\d*)?), (?:\d+(?:\.\d*)?), (?:\d+(?:\.\d*)?), (?:\d+(?:\.\d*)?)\]\)!\s*\n"
r"\s*ctx\.setFillColor\(color1\)"
)
new_content_after_color_mod, num_color_replacements = color_setting_pattern.subn(r"\n ctx.setFillColor(color.cgColor)", content)
if num_color_replacements > 0:
content = new_content_after_color_mod
else:
fallback_fill_pattern = re.compile(r"(ctx\.setFillColor\()([^)]+)(\))")
content, num_fallback_replacements = fallback_fill_pattern.subn(r"\1color.cgColor\3", content)
if num_fallback_replacements == 0:
print(f" Warning: Could not find or replace fill color logic in {svg_filename}. Manual check needed for {output_swift_path}.")
with open(output_swift_path, 'w') as f:
f.write(content)
enum_case_name = base_swift_name[0].lower() + base_swift_name[1:] if base_swift_name else ""
public_function_name_for_enum = f"plush{base_swift_name}"
icon_details.append((enum_case_name, public_function_name_for_enum, base_swift_name))
except FileNotFoundError:
print(f" Error: Generated file {output_swift_path} not found for modification. Skipping.")
continue
except Exception as e:
print(f" Error modifying Swift code for {output_swift_filename}: {e}")
continue
print("\nFinished processing all SVG files.")
if icon_details:
print("\nIcon details for enum generation (enum_case_name, UIImage.function_name, OriginalBaseName):")
# Sort icon_details by enum_case_name before printing
icon_details.sort(key=lambda x: x[0])
for detail in icon_details:
print(f'("{detail[0]}", "{detail[1]}", "{detail[2]}")')
else:
print("\nNo icons were successfully processed to generate enum details.")
if __name__ == "__main__":
generate_icon_code()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment