|
import xml.etree.ElementTree as ET |
|
import copy |
|
import os |
|
import subprocess |
|
|
|
def process_svg(tree): |
|
root = tree.getroot() |
|
|
|
# Extract the namespace if it exists |
|
ns = '' |
|
if '}' in root.tag: |
|
ns = root.tag[0:root.tag.index('}')+1] |
|
|
|
# Create new SVG document with three layers |
|
new_root = copy.deepcopy(root) |
|
new_root.clear() |
|
|
|
# Copy attributes from original root |
|
for key, value in root.attrib.items(): |
|
new_root.set(key, value) |
|
new_root.set('xmlns:inkscape', "http://www.inkscape.org/namespaces/inkscape") |
|
|
|
# Create layers |
|
layers = { |
|
"Regions": ET.SubElement(new_root, f"{ns}g", {"id": "Regions", "inkscape:groupmode": "layer", "inkscape:label": "Regions"}), |
|
"Outlines": ET.SubElement(new_root, f"{ns}g", {"id": "Outlines", "inkscape:groupmode": "layer", "inkscape:label": "Outlines"}), |
|
"Numbers": ET.SubElement(new_root, f"{ns}g", {"id": "Numbers", "inkscape:groupmode": "layer", "inkscape:label": "Numbers"}), |
|
} |
|
|
|
# Process elements from original SVG |
|
for elem in root: |
|
# Skip if element is already a layer |
|
if elem.get('inkscape:groupmode') == 'layer': |
|
continue |
|
|
|
# Copy element |
|
new_elem = copy.deepcopy(elem) |
|
|
|
# Process based on element type |
|
if elem.tag == f"{ns}g": |
|
# Add to Numbers layer |
|
layers["Numbers"].append(new_elem) |
|
|
|
elif elem.tag == f"{ns}path": |
|
# Extract fill and stroke information |
|
style_dict = {} |
|
if elem.get('style'): |
|
# Parse style attribute |
|
style_parts = elem.get('style').split(';') |
|
for part in style_parts: |
|
if ':' in part: |
|
key, value = part.split(':') |
|
style_dict[key.strip()] = value.strip() |
|
|
|
# Get fill information |
|
fill = elem.get('fill', '') |
|
fill_from_style = style_dict.get('fill', '') |
|
has_fill = (fill and fill not in ['none', 'None']) or (fill_from_style and fill_from_style not in ['none', 'None']) |
|
|
|
# Get stroke information |
|
stroke = elem.get('stroke', '') |
|
stroke_from_style = style_dict.get('stroke', '') |
|
has_stroke = stroke or stroke_from_style |
|
|
|
if has_fill: |
|
# Create copy for Regions layer (with fill, no stroke) |
|
regions_elem = copy.deepcopy(new_elem) |
|
|
|
# Clear any style attribute and set fill directly |
|
if fill: |
|
regions_elem.set('fill', fill) |
|
elif fill_from_style: |
|
regions_elem.set('fill', fill_from_style) |
|
|
|
# Remove stroke attributes |
|
if 'stroke' in regions_elem.attrib: |
|
del regions_elem.attrib['stroke'] |
|
if 'stroke-width' in regions_elem.attrib: |
|
del regions_elem.attrib['stroke-width'] |
|
|
|
# Update style attribute to remove stroke properties |
|
if regions_elem.get('style'): |
|
style_parts = [p for p in regions_elem.get('style').split(';') |
|
if not p.strip().startswith('stroke')] |
|
if style_parts: |
|
regions_elem.set('style', ';'.join(style_parts)) |
|
else: |
|
del regions_elem.attrib['style'] |
|
|
|
layers["Regions"].append(regions_elem) |
|
|
|
if has_stroke: |
|
# Create copy for Outlines layer (with stroke, no fill) |
|
outlines_elem = copy.deepcopy(new_elem) |
|
|
|
# Set fill to none |
|
outlines_elem.set('fill', 'none') |
|
|
|
# Update style attribute to remove fill |
|
if outlines_elem.get('style'): |
|
style_parts = [p for p in outlines_elem.get('style').split(';') |
|
if not p.strip().startswith('fill')] |
|
if style_parts: |
|
outlines_elem.set('style', ';'.join(style_parts)) |
|
else: |
|
del outlines_elem.attrib['style'] |
|
|
|
layers["Outlines"].append(outlines_elem) |
|
return layers, new_root |
|
|
|
def output_files(input_filename, new_root, layers, height_multiplier=1.0): |
|
# Create two versions of the processed SVG |
|
base_name = os.path.splitext(input_filename)[0] |
|
ext = os.path.splitext(input_filename)[1] |
|
|
|
# Version 1: All layers visible |
|
tree_layers = ET.ElementTree(new_root) |
|
layers_filename = f"{base_name} Layers{ext}" |
|
print(f"Writing layer-separated SVG to {layers_filename}") |
|
tree_layers.write(layers_filename, |
|
encoding='utf-8', |
|
xml_declaration=True) |
|
|
|
# Version 2: Regions layer hidden |
|
layers["Regions"].set('style', 'display:none') |
|
tree_outlines = ET.ElementTree(new_root) |
|
outlines_filename = f"{base_name} Outlines{ext}" |
|
print(f"Writing outlines SVG to {outlines_filename}") |
|
tree_outlines.write(outlines_filename, |
|
encoding='utf-8', |
|
xml_declaration=True) |
|
|
|
height = new_root.attrib['height'] |
|
bitmap_ext = ".png" |
|
svg_to_bitmap(layers_filename, bitmap_ext, src_height=int(height), height_multiplier=height_multiplier) |
|
svg_to_bitmap(outlines_filename, bitmap_ext, src_height=int(height), height_multiplier=height_multiplier) |
|
|
|
def svg_to_bitmap(src_filename, bitmap_ext, src_height=None, height_multiplier=1.0): |
|
"""Converts an svg file to a bitmap""" |
|
bitmap_ext = ".png" |
|
bitmap_filename = src_filename[:src_filename.rfind('.')] + bitmap_ext |
|
print(f"Converting SVG to {bitmap_filename}") |
|
args = ["C:\\Program Files\\Inkscape\\bin\\inkscape.com", "-C", f"--export-filename={bitmap_filename}", "--export-background=white"] |
|
if src_height or height_multiplier != 1.0: |
|
bitmap_height = int(int(src_height) * height_multiplier) |
|
args.append(f"--export-height={bitmap_height}") |
|
args.append(src_filename) |
|
subprocess.check_call(args) |
|
|
|
|
|
if __name__ == "__main__": |
|
import sys |
|
import argparse |
|
|
|
parser = argparse.ArgumentParser() |
|
parser.add_argument('-x', '--height-multiplier', type=float, default=4.0, help='Multiply height for bitmap') |
|
parser.add_argument('svg_filename', help='SVG Input File for paint by numbers DIY') |
|
args = parser.parse_args() |
|
|
|
input_filename = args.svg_filename |
|
|
|
if not os.path.exists(input_filename): |
|
print(f"Error: File '{input_filename}' not found") |
|
sys.exit(1) |
|
|
|
# Parse the SVG file |
|
tree = ET.parse(input_filename) |
|
layers, new_root = process_svg(tree) |
|
output_files(input_filename, new_root, layers, height_multiplier=args.height_multiplier) |
|
print("Processing complete!") |