Skip to content

Instantly share code, notes, and snippets.

@davidfraser
Created January 6, 2025 18:26
Show Gist options
  • Save davidfraser/aa57d9eae1c13723f27213a93b5aba7c to your computer and use it in GitHub Desktop.
Save davidfraser/aa57d9eae1c13723f27213a93b5aba7c to your computer and use it in GitHub Desktop.
paint-by-number-svg-output-separator

SVG Processor for Paint by Numbers

This is designed to post-process the SVG output from the brilliant free Paint by Number Generator at paint-by-number.com

Given the SVG file, it separates the different kinds of shapes (number labels, region outlines, and colours for regions) into different layers It also produces an output that doesn't contain the region colours, so is more suitable for printing. It outputs in SVG and PNG, with the names having a suffix of Layers or Outlines accordingly.

It requires Python 3.x but no additional other libraries.

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!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment