Add PNG to SVG color separation tool for multi-color 3D printing

Created split_png_by_brightness.py to split rasterized logos into separate
SVG files by color for Underground Magnetics logo conversion.

Key features:
- OpenCV contour hierarchy detection for proper hole handling
- Letters with enclosed shapes (d, o, g, etc.) now render correctly
- Tight brightness thresholds to avoid antialiasing artifacts
- Automatic bounding box cropping for optimal file sizes
- Black text: brightness < 40 (236k pixels, 21 shape groups)
- Grey icon: brightness 108-118 (119k pixels, 2 shape groups)

Results:
- um_black.svg: 6.1KB, 3070x233px (was 139KB, 3613x391px)
- um_grey.svg: 728B, 413x390px (was 253KB, 3613x391px)

Files:
- split_png_by_brightness.py: Main color separation tool
- IMPROVEMENTS.md: Detailed changelog and comparison
- README_underground_magnetics.md: Usage documentation
- underground-magnetics.eps: Source logo file
- um_black.svg: Separated black text (cropped)
- um_grey.svg: Separated grey icon (cropped)
- requirements.txt: Added opencv-python dependency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-31 09:51:24 -05:00
parent 8b80f68a19
commit 2719ec244d
7 changed files with 423 additions and 0 deletions

62
IMPROVEMENTS.md Normal file
View File

@@ -0,0 +1,62 @@
# Improvements to split_png_by_brightness.py
## Version 2 Fixes
### 1. Proper Hole Handling
**Problem**: Letters with enclosed shapes (like "d", "o", "g") were missing their holes, making them appear filled.
**Solution**:
- Switched from `scikit-image` contour detection to OpenCV's `cv2.findContours` with `RETR_CCOMP` mode
- This detects contour hierarchy (parent/child relationships)
- Holes are now included in the same SVG path as their parent contour
- Using `fill-rule="evenodd"` properly renders the holes
### 2. Better Color Detection
**Problem**: Antialiasing pixels around edges were being included, creating artifacts and incorrect bounding boxes.
**Solution**:
- Changed from single threshold to tight ranges
- Black text: brightness < 40 (was < 50)
- Grey icon: brightness 108-118 (was 80-120)
- This excludes antialiased edge pixels and prevents the grey icon from spanning the full image width
### 3. Automatic Bounding Box Cropping
**Problem**: SVG files contained a lot of empty space.
**Solution**:
- Added automatic cropping to bounding box by default
- Each SVG is sized to fit its content exactly
- `--no-crop` flag available if full canvas is needed
## Results
**Before (v1)**:
- um_black.svg: 139K, 27 separate paths, many filled letters
- um_grey.svg: 253K, 1609 paths, antialiasing artifacts, full width (3613px)
**After (v2)**:
- um_black.svg: 6.1K, 21 shape groups with proper holes, cropped to 3070x233
- um_grey.svg: 728B, 2 shape groups, clean edges, cropped to 413x390
## Dependencies Added
- opencv-python==4.13.0.92
## Default Parameters
- Black threshold: < 40 brightness
- Grey range: 108-118 brightness
- Cropping: enabled by default
## Usage
```bash
# Default (optimized for Underground Magnetics logo)
python split_png_by_brightness.py input.png --prefix output
# Custom thresholds
python split_png_by_brightness.py input.png \
--dark-threshold 40 \
--mid-min 108 \
--mid-max 118
# Disable cropping
python split_png_by_brightness.py input.png --no-crop
```

View File

@@ -0,0 +1,66 @@
# Underground Magnetics Logo - Color Separation
Successfully split the Underground Magnetics EPS logo into separate SVG files by color for multi-color 3D printing.
## Process
1. **EPS to PNG Conversion**: Used ImageMagick to convert `underground-magnetics.eps` to PNG format
- Output: `underground-magnetics-0.png` and `underground-magnetics-1.png`
2. **Color Separation**: Used `split_png_by_brightness.py` to extract separate colors
- Analyzed brightness values to distinguish grey icon from black text
- Created separate SVG files for each color region
## Output Files
- **underground-magnetics_grey.svg** - Grey "UM" icon (126,801 pixels, 1609 paths)
- **underground-magnetics_black.svg** - Black "Underground Magnetics" text (237,130 pixels, 27 paths)
## Usage
To convert these SVG files to 3D models for multi-color printing:
```bash
# Convert grey icon to STL
python svg_logo_to_stl.py underground-magnetics_grey.svg um_grey --width-mm 100 --base-thickness 1.5 --feature-height 2.5
# Convert black text to STL
python svg_logo_to_stl.py underground-magnetics_black.svg um_black --width-mm 100 --base-thickness 1.5 --feature-height 2.5
```
## Tools Created
### split_png_by_brightness.py
Splits PNG images into separate SVG files based on brightness/color clusters.
**Usage:**
```bash
python split_png_by_brightness.py <input.png> [options]
Options:
--output-dir, -o Output directory (default: same as input)
--prefix, -p Prefix for output files (default: input filename)
--dark-threshold INT Brightness threshold for dark/black (default: 50)
--mid-threshold INT Brightness threshold for mid/grey (default: 150)
```
**Example:**
```bash
python split_png_by_brightness.py underground-magnetics-0.png --prefix underground-magnetics
```
This creates separate SVG files for each color region detected in the PNG.
## Color Detection
The script analyzes pixel brightness to separate colors:
- **Dark pixels** (brightness < 50): Black text → `_black.svg`
- **Mid-range pixels** (50 ≤ brightness < 150): Grey icon → `_grey.svg`
- **Light pixels** (brightness ≥ 150): White background (ignored)
## Next Steps
1. Import the separate SVG files into the 3D conversion tool
2. Generate STL files for each color
3. Create 3MF assembly with multiple selectable parts
4. Import into BambuStudio for multi-color printing

View File

@@ -8,6 +8,7 @@ lazy-loader==0.5
mapbox_earcut==2.0.0
networkx==3.4.2
numpy==2.2.6
opencv-python==4.13.0.92
packaging==26.2
pillow==12.2.0
pycparser==3.0

265
split_png_by_brightness.py Normal file
View File

@@ -0,0 +1,265 @@
#!/usr/bin/env python3
"""
Split a PNG file into multiple SVG files based on brightness/color clusters.
Useful for multi-color 3D printing when the source is a rasterized logo.
"""
import argparse
import numpy as np
from PIL import Image
from pathlib import Path
import cv2
import xml.etree.ElementTree as ET
def extract_color_regions(image_path: Path, dark_threshold: int = 30, mid_min: int = 108, mid_max: int = 118):
"""
Extract different color regions from PNG based on brightness.
Uses tighter thresholds to avoid antialiasing artifacts.
Returns dict of {color_name: binary_mask}
"""
img = Image.open(image_path).convert('RGBA')
img_array = np.array(img)
# Get alpha mask (non-transparent pixels)
alpha = img_array[..., 3] > 128
# Calculate brightness for each pixel
rgb = img_array[..., :3]
brightness = np.mean(rgb, axis=-1)
# Create masks for different brightness levels
masks = {}
# Dark pixels (black text) - very tight threshold to avoid antialiasing
dark_mask = alpha & (brightness < dark_threshold)
if np.sum(dark_mask) > 100:
masks['black'] = dark_mask
# Mid-range pixels (grey icon) - tight range to get only the core grey
mid_mask = alpha & (brightness >= mid_min) & (brightness <= mid_max)
if np.sum(mid_mask) > 100:
masks['grey'] = mid_mask
return masks
def compute_contour_area(contour):
"""Compute the signed area of a contour (positive = clockwise, negative = counter-clockwise)"""
if len(contour) < 3:
return 0
# Shoelace formula
x = contour[:, 0]
y = contour[:, 1]
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
def mask_to_svg_paths_with_holes(mask: np.ndarray, simplify_epsilon: float = 1.0):
"""
Convert a binary mask to SVG path data with proper hole handling.
Returns list of path d attributes with fill-rule evenodd.
"""
# Convert to uint8 for OpenCV
mask_uint8 = (mask * 255).astype(np.uint8)
# Find contours with hierarchy (to detect holes)
contours, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if hierarchy is None or len(contours) == 0:
return []
# Simplify contours
simplified_contours = []
for contour in contours:
if simplify_epsilon > 0:
simplified = cv2.approxPolyDP(contour, simplify_epsilon, True)
else:
simplified = contour
simplified_contours.append(simplified)
# Group contours by parent/child relationship
# hierarchy format: [Next, Previous, First_Child, Parent]
hierarchy = hierarchy[0]
# Find all top-level contours (no parent)
top_level_indices = [i for i in range(len(hierarchy)) if hierarchy[i][3] == -1]
paths = []
for top_idx in top_level_indices:
contour = simplified_contours[top_idx]
if len(contour) < 3:
continue
# Start with outer contour
path_data = contour_to_svg_path(contour)
# Find all children (holes) of this contour
child_idx = hierarchy[top_idx][2]
while child_idx != -1:
child_contour = simplified_contours[child_idx]
if len(child_contour) >= 3:
# Add hole to the same path (evenodd fill-rule will handle it)
path_data += " " + contour_to_svg_path(child_contour)
# Move to next sibling
child_idx = hierarchy[child_idx][0]
paths.append(path_data)
return paths
def contour_to_svg_path(contour):
"""Convert OpenCV contour to SVG path data"""
# OpenCV contours are shape (N, 1, 2)
points = contour.reshape(-1, 2)
if len(points) < 2:
return ""
path_data = f"M {points[0, 0]},{points[0, 1]}"
for point in points[1:]:
path_data += f" L {point[0]},{point[1]}"
path_data += " Z"
return path_data
def get_mask_bounds(mask: np.ndarray):
"""Get the bounding box of a binary mask"""
rows = np.any(mask, axis=1)
cols = np.any(mask, axis=0)
if not np.any(rows) or not np.any(cols):
return None
y_min, y_max = np.where(rows)[0][[0, -1]]
x_min, x_max = np.where(cols)[0][[0, -1]]
return (x_min, y_min, x_max + 1, y_max + 1)
def create_svg_from_mask(mask: np.ndarray, output_path: Path, color: str, image_size: tuple, crop_to_bounds: bool = True):
"""Create SVG file from binary mask with proper hole handling and optional cropping"""
width, height = image_size
# Get bounding box of the mask
if crop_to_bounds:
bounds = get_mask_bounds(mask)
if bounds is None:
print(f"WARNING: No content found for {color}")
return
x_min, y_min, x_max, y_max = bounds
cropped_width = x_max - x_min
cropped_height = y_max - y_min
# Crop the mask
cropped_mask = mask[y_min:y_max, x_min:x_max]
else:
x_min, y_min = 0, 0
cropped_width, cropped_height = width, height
cropped_mask = mask
# Create SVG root with cropped dimensions
svg = ET.Element('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'width': str(cropped_width),
'height': str(cropped_height),
'viewBox': f'0 0 {cropped_width} {cropped_height}'
})
# Convert mask to paths with hole detection
paths = mask_to_svg_paths_with_holes(cropped_mask, simplify_epsilon=1.0)
# Color mapping
color_hex = {
'black': '#000000',
'grey': '#808080',
'gray': '#808080',
'white': '#FFFFFF',
}.get(color, color)
# Add paths to SVG (no translation needed since we cropped the mask)
for path_data in paths:
ET.SubElement(svg, 'path', {
'd': path_data,
'fill': color_hex,
'fill-rule': 'evenodd'
})
# Write SVG
tree = ET.ElementTree(svg)
ET.indent(tree, space=' ')
tree.write(output_path, encoding='utf-8', xml_declaration=True)
if crop_to_bounds:
print(f"Created {output_path} with color {color} ({len(paths)} shape groups, cropped to {cropped_width}x{cropped_height})")
else:
print(f"Created {output_path} with color {color} ({len(paths)} shape groups)")
def main():
parser = argparse.ArgumentParser(
description='Split PNG into separate SVG files by brightness/color'
)
parser.add_argument('input', help='Input PNG file')
parser.add_argument('--output-dir', '-o', help='Output directory (default: same as input)')
parser.add_argument('--prefix', '-p', help='Prefix for output files (default: input filename)')
parser.add_argument('--dark-threshold', type=int, default=30,
help='Brightness threshold for dark/black elements (default: 30)')
parser.add_argument('--mid-min', type=int, default=108,
help='Minimum brightness for mid/grey elements (default: 108)')
parser.add_argument('--mid-max', type=int, default=118,
help='Maximum brightness for mid/grey elements (default: 118)')
parser.add_argument('--no-crop', action='store_true',
help='Do not crop to bounding box (default: crop enabled)')
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"ERROR: File not found: {input_path}")
return 1
# Setup output directory
if args.output_dir:
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
else:
output_dir = input_path.parent
# Setup prefix
prefix = args.prefix if args.prefix else input_path.stem
# Load image to get size
img = Image.open(input_path)
image_size = img.size
# Extract color regions
print(f"Analyzing colors in {input_path}...")
masks = extract_color_regions(input_path, args.dark_threshold, args.mid_min, args.mid_max)
if not masks:
print("No color regions found!")
return 1
print(f"\nFound {len(masks)} color regions:")
for color, mask in masks.items():
pixel_count = np.sum(mask)
print(f" {color}: {pixel_count:,} pixels")
# Create SVG for each color
print(f"\nCreating separate SVG files...")
crop_enabled = not args.no_crop
for color, mask in masks.items():
output_path = output_dir / f"{prefix}_{color}.svg"
create_svg_from_mask(mask, output_path, color, image_size, crop_to_bounds=crop_enabled)
print(f"\nDone! Created {len(masks)} SVG files in {output_dir}")
if __name__ == '__main__':
main()

24
um_black.svg Normal file
View File

@@ -0,0 +1,24 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" width="3070" height="233" viewBox="0 0 3070 233">
<path d="M 2952,58 L 2945,66 L 2940,80 L 2940,102 L 2945,116 L 2951,122 L 2965,127 L 3026,129 L 3029,130 L 3035,136 L 3036,146 L 3031,153 L 3023,156 L 2942,156 L 2942,183 L 3037,183 L 3046,181 L 3056,175 L 3064,165 L 3069,150 L 3069,133 L 3063,117 L 3053,108 L 3040,103 L 2983,101 L 2977,97 L 2975,93 L 2975,85 L 2979,79 L 2990,76 L 3062,76 L 3062,49 L 2982,49 L 2964,52 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2820,63 L 2809,76 L 2804,86 L 2799,104 L 2799,128 L 2801,138 L 2808,154 L 2815,164 L 2827,174 L 2837,179 L 2860,183 L 2916,183 L 2916,155 L 2861,155 L 2850,151 L 2840,141 L 2837,136 L 2834,123 L 2834,110 L 2836,99 L 2839,92 L 2846,84 L 2857,79 L 2868,77 L 2916,77 L 2916,49 L 2851,50 L 2835,54 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2735,49 L 2735,183 L 2771,183 L 2771,49 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2501,63 L 2494,70 L 2487,82 L 2483,94 L 2481,107 L 2481,125 L 2483,138 L 2486,148 L 2494,162 L 2508,174 L 2527,181 L 2541,183 L 2599,183 L 2599,156 L 2535,155 L 2528,152 L 2523,147 L 2519,135 L 2520,129 L 2599,129 L 2599,103 L 2519,102 L 2519,97 L 2523,86 L 2527,81 L 2533,78 L 2543,76 L 2599,76 L 2599,49 L 2533,50 L 2512,56 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2332,49 L 2332,183 L 2366,183 L 2366,79 L 2368,77 L 2406,78 L 2416,83 L 2420,94 L 2420,183 L 2454,183 L 2454,87 L 2450,72 L 2446,65 L 2440,59 L 2431,54 L 2415,50 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2298,49 L 2228,49 L 2210,51 L 2200,54 L 2188,61 L 2176,74 L 2167,94 L 2164,120 L 2166,135 L 2170,148 L 2179,163 L 2192,175 L 2207,181 L 2224,183 L 2264,184 L 2262,196 L 2258,201 L 2251,205 L 2179,206 L 2179,232 L 2262,231 L 2279,225 L 2291,213 L 2298,192 Z M 2216,81 L 2228,77 L 2264,78 L 2263,156 L 2226,155 L 2213,150 L 2206,143 L 2201,131 L 2200,108 L 2205,92 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2011,49 L 2011,76 L 2081,76 L 2090,78 L 2100,86 L 2103,101 L 2038,103 L 2025,106 L 2015,111 L 2007,120 L 2002,136 L 2003,157 L 2010,171 L 2021,179 L 2038,183 L 2138,183 L 2138,92 L 2134,75 L 2127,64 L 2120,58 L 2112,54 L 2098,50 Z M 2038,137 L 2044,130 L 2051,128 L 2102,128 L 2103,156 L 2045,156 L 2041,154 L 2038,150 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1370,49 L 1370,183 L 1405,183 L 1406,77 L 1445,78 L 1452,81 L 1455,84 L 1458,93 L 1458,183 L 1492,183 L 1492,86 L 1489,74 L 1485,66 L 1477,58 L 1469,54 L 1453,50 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1212,49 L 1212,136 L 1214,152 L 1221,167 L 1230,175 L 1251,182 L 1334,183 L 1334,49 L 1300,49 L 1299,154 L 1260,154 L 1253,151 L 1249,147 L 1246,132 L 1246,49 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1012,52 L 996,49 L 932,49 L 932,183 L 966,183 L 967,74 L 987,74 L 994,79 L 995,104 L 1028,104 L 1028,76 L 1026,66 L 1023,60 L 1018,55 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 898,49 L 828,49 L 805,52 L 795,56 L 786,62 L 774,76 L 768,88 L 765,99 L 764,128 L 768,145 L 779,164 L 793,176 L 806,181 L 823,183 L 863,184 L 863,191 L 860,198 L 850,205 L 778,206 L 778,232 L 861,231 L 880,224 L 886,219 L 891,212 L 896,198 L 898,183 Z M 814,82 L 827,77 L 863,78 L 862,156 L 831,156 L 821,154 L 811,149 L 804,141 L 801,134 L 799,122 L 800,104 L 805,91 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 730,52 L 713,49 L 649,49 L 649,183 L 684,183 L 685,74 L 705,74 L 712,79 L 713,104 L 746,104 L 746,78 L 744,67 L 740,59 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 525,62 L 517,70 L 509,84 L 504,106 L 504,126 L 506,139 L 516,161 L 531,174 L 550,181 L 564,183 L 622,183 L 622,156 L 558,155 L 551,152 L 546,147 L 542,136 L 543,129 L 622,129 L 622,103 L 542,102 L 545,87 L 550,81 L 556,78 L 566,76 L 622,76 L 622,49 L 556,50 L 540,54 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 197,49 L 197,183 L 231,183 L 232,77 L 271,78 L 278,81 L 281,84 L 284,93 L 284,183 L 318,183 L 318,86 L 315,74 L 311,66 L 303,58 L 295,54 L 279,50 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1081,54 L 1070,61 L 1059,72 L 1051,86 L 1047,99 L 1046,127 L 1050,144 L 1056,156 L 1074,174 L 1086,180 L 1100,184 L 1133,184 L 1145,181 L 1158,175 L 1173,162 L 1182,147 L 1186,134 L 1187,104 L 1183,88 L 1177,76 L 1171,68 L 1162,60 L 1150,53 L 1138,49 L 1126,47 L 1107,47 L 1095,49 Z M 1109,75 L 1124,75 L 1136,80 L 1145,89 L 1151,105 L 1151,128 L 1147,140 L 1143,146 L 1135,153 L 1129,156 L 1119,158 L 1104,156 L 1094,150 L 1088,143 L 1082,126 L 1082,106 L 1087,91 L 1097,80 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2735,0 L 2735,32 L 2771,32 L 2771,0 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 2638,0 L 2638,48 L 2613,49 L 2613,75 L 2638,76 L 2638,147 L 2642,163 L 2651,175 L 2663,181 L 2673,183 L 2710,183 L 2710,157 L 2688,157 L 2681,154 L 2674,145 L 2672,133 L 2672,76 L 2710,75 L 2710,49 L 2673,49 L 2672,0 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1764,0 L 1764,183 L 1802,183 L 1798,64 L 1799,40 L 1848,183 L 1888,183 L 1923,74 L 1935,41 L 1936,66 L 1933,183 L 1969,183 L 1969,0 L 1914,0 L 1867,141 L 1865,139 L 1819,0 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 1650,0 L 1616,0 L 1615,49 L 1580,49 L 1558,52 L 1546,57 L 1539,62 L 1528,75 L 1523,84 L 1519,97 L 1518,127 L 1520,138 L 1527,155 L 1531,161 L 1544,173 L 1563,181 L 1576,183 L 1650,183 Z M 1564,85 L 1575,79 L 1585,77 L 1616,78 L 1615,155 L 1575,154 L 1566,150 L 1559,143 L 1554,129 L 1554,105 L 1556,97 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 476,0 L 442,0 L 441,49 L 406,49 L 384,52 L 369,59 L 359,68 L 354,75 L 348,87 L 344,103 L 345,133 L 353,155 L 357,161 L 370,173 L 389,181 L 402,183 L 476,183 Z M 390,85 L 401,79 L 411,77 L 442,78 L 441,155 L 406,155 L 392,150 L 386,144 L 383,139 L 380,128 L 380,106 L 383,95 Z" fill="#000000" fill-rule="evenodd" />
<path d="M 0,0 L 0,125 L 2,142 L 10,160 L 22,171 L 35,178 L 53,183 L 71,185 L 87,185 L 110,182 L 123,178 L 140,168 L 149,159 L 153,152 L 158,130 L 158,0 L 121,0 L 121,122 L 119,133 L 115,142 L 109,149 L 97,155 L 83,157 L 66,156 L 54,152 L 44,143 L 40,135 L 37,119 L 37,0 Z" fill="#000000" fill-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

5
um_grey.svg Normal file
View File

@@ -0,0 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<svg xmlns="http://www.w3.org/2000/svg" width="413" height="390" viewBox="0 0 413 390">
<path d="M 159,0 L 159,116 L 164,135 L 175,150 L 187,157 L 198,160 L 214,160 L 223,158 L 236,151 L 243,144 L 250,131 L 252,124 L 253,0 Z" fill="#808080" fill-rule="evenodd" />
<path d="M 0,0 L 0,389 L 99,389 L 100,292 L 107,302 L 122,317 L 135,326 L 161,337 L 178,341 L 198,343 L 234,341 L 263,333 L 279,325 L 294,314 L 306,301 L 312,292 L 313,389 L 412,389 L 412,0 L 314,0 L 314,119 L 311,140 L 305,159 L 299,171 L 290,184 L 272,201 L 261,208 L 241,217 L 212,223 L 200,223 L 182,220 L 153,209 L 134,196 L 120,181 L 111,167 L 102,143 L 98,114 L 98,0 Z" fill="#808080" fill-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 728 B

BIN
underground-magnetics.eps Normal file

Binary file not shown.