cdelker / schemdraw

MIT License
103 stars 20 forks source link

Function to remove whitespace from svg #28

Open pozitron57 opened 6 months ago

pozitron57 commented 6 months ago

I already wrote about whitespace in svg in #6, but I need zero whitespace. Here is the function I use to remove all the whitespace from svg files generated by SchemDraw:

import inkex
import os

# This function processes an SVG file
def process_svg(svg_file):
    try:
        # Load the SVG file and get the root element
        svg_element = inkex.load_svg(svg_file).getroot()
        # Get the largest bounding box element
        _, largest_bbox_element = get_largest_geometric_bbox(svg_element)
    except:
        # If the SVG file cannot be loaded, print an error message
        print(f'Cannot load svg {svg_file}')
        return
    # Crop the SVG and save the processed file
    crop_and_save_processed_svg(svg_element, largest_bbox_element, svg_file)

# This function crops the SVG and saves it
def crop_and_save_processed_svg(svg_element, largest_bbox_element, svg_file):
    # Change the SVG canvas size according to the bounding box
    if largest_bbox_element and hasattr(largest_bbox_element, 'bounding_box'):
        bbox = largest_bbox_element.bounding_box()
        svg_element.set('width', str(bbox.width))
        svg_element.set('height', str(bbox.height))
        svg_element.set('viewBox', f'{bbox.left} {bbox.top} {bbox.width} {bbox.height}')
    # Write the changes to the same SVG file
    with open(svg_file, 'w') as output_file:
        output_file.write(svg_element.tostring().decode('utf-8'))
    # Define the actions for Inkscape command line
    my_actions = 'select-all;fit-canvas-to-selection;'
    export_actions = my_actions + f'export-type:svg;export-filename:{svg_file};export-do;'
    # Execute the Inkscape actions
    inkex.command.inkscape(svg_file, actions=export_actions)

# This function returns the largest geometric bounding box
def get_largest_geometric_bbox(svg_element):
    element_area = 0
    largest_bbox_element_id = None
    largest_bbox_element = None
    # Create a list of potential elements to check
    element_list = svg_element.xpath('//svg:path | //svg:polygon | //svg:polyline | //svg:rect | //svg:use | //svg:image')
    # Determine the largest bounding box
    for element in element_list:
        if hasattr(element, 'bounding_box'):
            bbox_area = float(element.bounding_box().width) * float(element.bounding_box().height)
            if bbox_area > element_area:
                element_area = bbox_area
                largest_bbox_element_id = element.get_id()
                largest_bbox_element = element
        else:
            continue
    return largest_bbox_element_id, largest_bbox_element

# Example usage
svg_file_name = 'filename.svg'
if not os.path.exists(svg_file_name):
    print(f'File Not Found: {svg_file_name}')
else:
    process_svg(svg_file_name)

It is not optimal as there is need for Inkscape installation. Nevertheless I'd like SchemDraw to allow for this usage, something like d.save('filename.svg', crop=True). It works for all my plots, but the elements I use are limited to a little part of basic elements, hence more testing might be necessary.

cdelker commented 6 months ago

4c1a8f8 made some improvements to bounding box calculations that should help when there is text falling on the border, but only when text is drawn as paths (also requires new ziafont and ziamath commits). Using d.config(margin=0) works to remove white space in many cases, but won't be perfect for example with the following two cases.

If text is drawn as svg elements, there's no way to know what font will be used by the renderer. May not work reliably with matplotlib either.

There are other situations where the bounding box won't be perfect, such as when a Bezier curve control point extends the box - it doesn't distinguish between actual and control points. Inkscape is probably smarter about tracing the actual curve path when cropping.

pozitron57 commented 6 months ago

Thanks. Quick note:

image

When the right resistor has label there is a whitespace at right (placed with elm.Resistor(l=0).label('$R$', loc='right')). Without a label, margin=0 works well at the right edge:

image