Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Best fit for SVG files
#1
I have a single SVG file, called "container.svg" that contains a closed curve on a transparent background, something like this:

1
2
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="tiny" viewbox="0 0 720 1080" height="1080" version="1.2" width="720" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><ellipse cx="360.0" cy="540.0" fill="none" rx="324.0" ry="486.0" stroke="black" stroke-width="2" /></svg>
then I have a directory with other SVG files, each one contains some colored areas over a transparent background.
For the matter of this question we can just assume a simple rect or circle.

My goal is to load one image at time, move and scale it in order to "best fit" the inside of the container.svg shape.
That means the transparent areas can overlap, but the colored pixels should be kept inside the shape (with a specified gap) and each time a new image is loaded all of them should be moved and scaled (but not rotated) to fill all the available space.

This is similar to the "stock problem" you might face when you want to cut a piece of wood with several shapes and you want to use all the available surface minimizing the waste.

The output of my program should be the absolute position and size for each loaded image, so I can make an HTML page to represent the final result.
Is there any Python library I can use to do this? Or any idea to put me on the right way?
Reply
#2
(Apr-22-2025, 12:39 PM)chihuahua998 Wrote: I have a single SVG file, called "container.svg" that contains a closed curve on a transparent background, something like this:

1
2
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="tiny" viewbox="0 0 720 1080" height="1080" version="1.2" width="720" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><ellipse cx="360.0" cy="540.0" fill="none" rx="324.0" ry="486.0" stroke="black" stroke-width="2" /></svg>
then I have a directory with other SVG files, each one contains some colored areas over a transparent background.
For the matter of this question we can just assume a simple rect or circle.

My goal is to load one image at time, move and scale it in order to "best fit" the inside of the container.svg shape.
That means the transparent areas can overlap, but the colored pixels should be kept inside the shape (with a specified gap) and each time a new image is loaded all of them should be moved and scaled (but not rotated) to fill all the available space.

This is similar to the "stock problem" you might face when you want to cut a piece of wood with several shapes and you want to use all the available surface minimizing the waste.

The output of my program should be the absolute position and size for each loaded image, so I can make an HTML page to represent the final result.
Is there any Python library I can use to do this? Or any idea to put me on the right way?

I applied your query to Grok on X. Here's the response which may, or may not be helpful:

Disclaimer: This solution was generated by Grok, created by xAI, and adapted by me.

To solve this problem, we need to create a program that loads a container SVG with a closed curve (e.g., an ellipse) and a set of other SVG files containing colored shapes (e.g., rectangles or circles), then positions and scales these shapes to "best fit" inside the container while ensuring colored areas stay within the container's boundary (with a specified gap) and minimizing wasted space. The output will be the absolute positions and sizes for each loaded image, suitable for generating an HTML page to display the result.

This is akin to a 2D bin packing or stock cutting problem, but with the added complexity of a non-rectangular container and SVG handling. We'll use Python with libraries like svgpathtools for SVG parsing, shapely for geometric operations, and a heuristic approach for packing. The shapes will be scaled and translated (no rotation) to fit within the container, respecting a gap and ensuring no colored areas overlap or exceed the container's boundary.

Approach
  • Parse SVGs:
    Load container.svg to extract the closed curve (e.g., ellipse) as a geometric path.
    Load each shape SVG to extract its colored shape (e.g., rectangle or circle) and compute its bounding box and geometry.
  • Geometric Setup:
    Convert the container's path to a shapely polygon, accounting for the specified gap by shrinking the polygon.
    Convert each shape's geometry to a shapely object (e.g., rectangle or circle), preserving its aspect ratio during scaling.
  • Packing Algorithm:
    Use a heuristic packing strategy (e.g., sorting shapes by area and placing them one by one).
    For each shape:
    • Iterate over possible positions within the container (e.g., on a grid or using a sliding approach).
    • Scale the shape (maintaining aspect ratio) to the maximum size that fits without overlapping other shapes or exceeding the container.
    • Place the shape at the position that minimizes wasted space or maximizes compactness.
    • Reposition and rescale all shapes iteratively when a new shape is added to optimize the layout.
  • Output:
    For each shape, output its absolute position (x, y) and scale factor (or width/height) relative to the original SVG size.
    Generate an HTML page that uses <image> tags to place the SVGs at the computed positions and sizes.

Assumptions
  • The container SVG has a single closed path (e.g., an ellipse).
  • Shape SVGs contain a single colored shape (rectangle or circle) on a transparent background.
  • The gap is a fixed pixel distance (e.g., 10 pixels) applied as an inward buffer to the container.
  • Shapes can overlap in transparent areas but not in colored areas.
  • No rotation is allowed; only translation and uniform scaling (preserving aspect ratio).
  • The output positions and sizes are in the coordinate system of container.svg (e.g., 720x1080 viewBox).

Python Implementation

Below is a Python script that implements the described functionality. It uses svgpathtools for SVG parsing, shapely for geometric computations, and a simple
packing heuristic. The script assumes the shape SVGs are in a directory called shapes/.
python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import os
import svgpathtools
from shapely.geometry import Polygon, Point
from shapely.affinity import scale, translate
import numpy as np
from xml.etree import ElementTree as ET
 
# Configuration
CONTAINER_SVG = "container.svg"
SHAPES_DIR = "shapes"
GAP = 10  # Pixel gap from container boundary
OUTPUT_HTML = "output.html"
 
def parse_container_svg(file_path):
    """Parse container SVG and return its closed path as a Shapely polygon."""
    doc = svgpathtools.Document(file_path)
    paths = doc.paths()
    if not paths:
        raise ValueError("No paths found in container SVG")
    path = paths[0# Assume single path
    points = []
    for segment in path:
        if isinstance(segment, svgpathtools.Line):
            points.append((segment.start.real, segment.start.imag))
        elif isinstance(segment, svgpathtools.CubicBezier):
            # Approximate Bezier with points (simplified)
            t = np.linspace(0, 1, 10)
            for ti in t:
                pt = segment.point(ti)
                points.append((pt.real, pt.imag))
    polygon = Polygon(points)
    # Apply gap by buffering inward
    return polygon.buffer(-GAP, resolution=16)
 
def parse_shape_svg(file_path):
    """Parse shape SVG and return its geometry and bounding box."""
    tree = ET.parse(file_path)
    root = tree.getroot()
    ns = {"svg": "http://www.w3.org/2000/svg"}
    # Look for rect or circle
    shape = None
    for elem in root.findall(".//svg:rect|.//svg:circle", ns):
        if elem.tag.endswith("rect"):
            x = float(elem.get("x", 0))
            y = float(elem.get("y", 0))
            w = float(elem.get("width"))
            h = float(elem.get("height"))
            shape = Polygon([(x, y), (x+w, y), (x+w, y+h), (x, y+h)])
            bbox = (w, h)
        elif elem.tag.endswith("circle"):
            cx = float(elem.get("cx"))
            cy = float(elem.get("cy"))
            r = float(elem.get("r"))
            shape = Point(cx, cy).buffer(r, resolution=16)
            bbox = (2*r, 2*r)
        break
    if shape is None:
        raise ValueError(f"No supported shape found in {file_path}")
    return shape, bbox, file_path
 
def try_place_shape(container, shapes, new_shape, orig_bbox, grid_step=10):
    """Attempt to place and scale new_shape within container without overlap."""
    best_pos = None
    best_scale = 0
    best_area = 0
    # Get container bounds
    minx, miny, maxx, maxy = container.bounds
    # Try positions on a grid
    for x in np.arange(minx, maxx, grid_step):
        for y in np.arange(miny, maxy, grid_step):
            # Translate shape to position
            placed_shape = translate(new_shape, xoff=x, yoff=y)
            if not container.contains(placed_shape):
                continue
            # Check for overlap with other shapes
            overlap = any(placed_shape.intersects(s[0]) for s in shapes)
            if overlap:
                continue
            # Find maximum scale factor
            scale_factor = 1.0
            while True:
                scaled = scale(placed_shape, xfact=scale_factor, yfact=scale_factor, origin=(x, y))
                if not container.contains(scaled) or any(scaled.intersects(s[0]) for s in shapes):
                    scale_factor -= 0.01
                    if scale_factor <= 0:
                        break
                else:
                    scale_factor += 0.01
                if scale_factor > 5# Prevent excessive scaling
                    break
            if scale_factor > best_scale:
                best_scale = scale_factor
                best_pos = (x, y)
                best_area = placed_shape.area * scale_factor ** 2
    return best_pos, best_scale, best_area
 
def pack_shapes(container, shape_files):
    """Pack shapes into container and return their positions and scales."""
    placed_shapes = []
    # Load all shapes
    shape_geometries = [parse_shape_svg(f) for f in shape_files]
    # Sort by area (descending) for better packing
    shape_geometries.sort(key=lambda x: x[0].area, reverse=True)
     
    for shape, bbox, file_path in shape_geometries:
        pos, scale, _ = try_place_shape(container, placed_shapes, shape, bbox)
        if pos is None:
            print(f"Could not place {file_path}")
            continue
        x, y = pos
        # Store: (geometry, position, scale, file_path)
        scaled_shape = scale(shape, xfact=scale, yfact=scale, origin=(x, y))
        translated_shape = translate(scaled_shape, xoff=x, yoff=y)
        placed_shapes.append((translated_shape, (x, y), scale, file_path))
     
    return placed_shapes
 
def generate_html(placed_shapes, container_file, width=720, height=1080):
    """Generate HTML to display the packed shapes."""
    html = f"""<!DOCTYPE html>
<html>
<head>
    <title>Packed SVGs</title>
    <style>
        body {{ margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }}
        .container {{ position: relative; width: {width}px; height: {height}px; }}
        img {{ position: absolute; }}
    </style>
</head>
<body>
    <div class="container">
        <img src="{container_file}" style="width: {width}px; height: {height}px;">
"""
    for _, (x, y), scale, file_path in placed_shapes:
        # Get original dimensions from SVG
        tree = ET.parse(file_path)
        root = tree.getroot()
        orig_width = float(root.get("width", 100))
        orig_height = float(root.get("height", 100))
        new_width = orig_width * scale
        new_height = orig_height * scale
        html += f'        <img src="{file_path}" style="left: {x}px; top: {y}px; width: {new_width}px; height: {new_height}px;">\n'
    html += """    </div>
</body>
</html>"""
    with open(OUTPUT_HTML, "w") as f:
        f.write(html)
 
def main():
    # Load container
    container = parse_container_svg(CONTAINER_SVG)
     
    # Load shape files
    shape_files = [os.path.join(SHAPES_DIR, f) for f in os.listdir(SHAPES_DIR) if f.endswith(".svg")]
     
    # Pack shapes
    placed_shapes = pack_shapes(container, shape_files)
     
    # Output positions and scales
    print("Placed shapes:")
    for _, (x, y), scale, file_path in placed_shapes:
        print(f"File: {file_path}, Position: ({x:.2f}, {y:.2f}), Scale: {scale:.2f}")
     
    # Generate HTML
    generate_html(placed_shapes, CONTAINER_SVG)
 
if __name__ == "__main__":
    main()
Explanation
  • SVG Parsing:
    parse_container_svg: Extracts the container's path (e.g., ellipse) and converts it to a shapely polygon, applying the gap via buffering.
    parse_shape_svg: Parses shape SVGs to extract rectangles or circles as shapely geometries and their bounding boxes.
  • Packing:
    try_place_shape: Tests positions on a grid, scales the shape to the maximum size that fits, and selects the best position based on the scaled area.
    pack_shapes: Sorts shapes by area and places them one by one, storing their final positions and scales.
  • Output:
    generate_html: Creates an HTML page with a container SVG and shape SVGs positioned using absolute CSS styling.
    The main function prints the position and scale for each shape and saves the HTML.

Dependencies
Install required libraries:
bash

pip install svgpathtools shapely numpy

Example Shape SVG
For testing, create shape SVGs in the shapes/ directory, e.g., rect.svg:
Output:
xml <?xml version="1.0" encoding="utf-8"?> <svg viewBox="0 0 100 100" width="100" height="100" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="80" height="80" fill="red"/> </svg>
And circle.svg:
Output:
xml <?xml version="1.0" encoding="utf-8"?> <svg viewBox="0 0 100 100" width="100" height="100" xmlns="http://www.w3.org/2000/svg"> <circle cx="50" cy="50" r="40" fill="blue"/> </svg>
Output

Output:
Console: Lists each shape's file path, position (x, y), and scale factor. HTML: An output.html file that displays the container SVG with shape SVGs overlaid at their computed positions and sizes.
Limitations and Improvements

Performance: The grid-based placement is simple but slow for large containers or many shapes. Consider using a more advanced packing algorithm (e.g., rectangle packing with a binary tree or genetic algorithm).
Precision: Bezier curves in the container SVG are approximated; use finer sampling for complex paths.
Repositioning: The current script places shapes sequentially without repositioning existing shapes when a new one is added. For better optimization, implement an iterative refinement step (e.g., simulated annealing).
Shape Support: Only rectangles and circles are supported; extend parse_shape_svg for other SVG elements (e.g., polygons, paths).

This solution provides a functional starting point for the described problem, producing absolute positions and sizes for an HTML visualization. Adjust the grid_step and packing strategy based on performance and precision needs.
Reply
#3
Not sure about the claims:

Quote:Shape Support: Only rectangles and circles are supported

My SVGs contains only rects at the moment, example:


Output:
<?xml version="1.0" encoding="utf-8" ?> <svg baseProfile="full" height="600" version="1.1" width="720" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><rect fill="none" height="100%" width="100%" x="0" y="0" /><rect fill="black" height="20" width="20" x="0" y="288" /><rect fill="black" height="20" width="20" x="0" y="264" /><rect fill="black" height="20" width="20" x="24" y="264" /><rect fill="black" height="20" width="20" x="24" y="288" /><rect fill="black" height="20" width="20" x="48" y="288" /><rect fill="black" height="20" width="20" x="24" y="288" /><rect fill="black" height="20" width="20" x="0" y="288" /><rect fill="black" height="20" width="20" x="24" y="288" /><rect fill="black" height="20" width="20" x="48" y="288" /><rect fill="black" height="20" width="20" x="24" y="288" /><rect fill="black" height="20" width="20" x="0" y="288" /><rect fill="black" height="20" width="20" x="24" y="288" /><rect fill="black" height="20" width="20" x="24" y="312" /><rect fill="black" height="20" width="20" x="48" y="312" /><rect fill="black" height="20" width="20" x="72" y="312" /><rect fill="black" height="20" width="20" x="48" y="312" /><rect fill="black" height="20" width="20" x="72" y="312" /><rect fill="black" height="20" width="20" x="96" y="312" /><rect fill="black" height="20" width="20" x="96" y="336" /><rect fill="black" height="20" width="20" x="96" y="360" /><rect fill="black" height="20" width="20" x="120" y="360" /><rect fill="black" height="20" width="20" x="144" y="360" /><rect fill="black" height="20" width="20" x="144" y="336" /><rect fill="black" height="20" width="20" x="120" y="336" /><rect...
But still I get the error:

Quote:ValueError: No supported shape found in output_svgs/random_shape_5.svg

What am I missing?
Reply
#4
Editing SVG Files

Best tools:

Inkscape (Free & Open Source) – Full-featured vector editor, great alternative to Adobe Illustrator.

Adobe Illustrator – Professional-grade SVG editing.

Figma – Web-based, collaborative vector editor with SVG export/import.

Boxy SVG – Lightweight and user-friendly SVG editor.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020