Skip to content

Instantly share code, notes, and snippets.

@m13253
Last active July 18, 2023 01:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m13253/66284bc244deeff0f0f8863c206421c7 to your computer and use it in GitHub Desktop.
Save m13253/66284bc244deeff0f0f8863c206421c7 to your computer and use it in GitHub Desktop.
Convert a pixel art into an SVG file
#!/usr/bin/env python3
# Convert a pixel art to SVG file
# Copyright (C) 2021 Star Brilliant
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List, Set, Tuple
import imageio
import numpy
import sys
def fill_block(image: numpy.ndarray, is_pixel_filled: numpy.ndarray, x: int, y: int) -> Set[Tuple[int, int]]:
# Flood fill algorithm
pixel_color = image[y, x, :3]
color_block = [(x, y)]
i = 0
while i < len(color_block):
x1, y1 = color_block[i]
for x2, y2 in {(x1, y1 - 1), (x1 - 1, y1), (x1 + 1, y1), (x1, y1 + 1)}:
if y2 < 0 or y2 >= image.shape[0] or x2 < 0 or x2 >= image.shape[1]:
continue
if is_pixel_filled[y2, x2]:
continue
if (image[y2, x2, :3] == pixel_color).all():
color_block.append((x2, y2))
is_pixel_filled[y2, x2] = True
i += 1
return set(color_block)
def generate_path(color_block: Set[Tuple[int, int]]) -> List[Tuple[int, int]]:
# Square tracing algorithm
# http://www.imageprocessingplace.com/downloads_V3/root_downloads/tutorials/contour_tracing_Abeer_George_Ghuneim/alg.html
x, y = sorted(color_block, key=lambda x: (x[0] + x[1], x[1], x[0]))[0]
path = [(x, y)]
x1, y1 = x, y
dx, dy = 1, 0
while (x1, y1) != (x, y) or len(path) == 1:
x2, y2 = x1 + (dx + dy - 1) // 2, y1 + (dy - dx - 1) // 2 # Left pixel
x3, y3 = x1 + (dx - dy - 1) // 2, y1 + (dx + dy - 1) // 2 # Right pixel
if (x3, y3) in color_block:
if (x2, y2) in color_block:
path.append((x1, y1))
dx, dy = dy, -dx # Turn left
else:
pass # Go straight
else:
path.append((x1, y1))
dx, dy = -dy, dx # Turn right
x1, y1 = x1 + dx, y1 + dy
return path
def main(argv: str) -> int:
if len(argv) != 3:
print('Usage: pixart2svg.py <input_image_file> <output_svg_file>')
return 1
input_file, output_file = argv[1], argv[2]
image = imageio.imread(input_file)
if len(image.shape) != 3:
print('Image must be RGB')
return 1
height, width, channels = image.shape
if channels not in (3, 4):
print('Image must be RGB')
return 1
is_pixel_filled = numpy.zeros((height, width), dtype=bool)
with open(output_file, 'w', encoding='UTF-8') as f:
f.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
f.write(f'<svg viewBox="0 0 {width} {height}" width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">\n')
f.write(' <style>\n')
f.write(' .pixart2svg {\n')
f.write(' color-interpolation: linearRGB;\n')
f.write(' shape-rendering: geometricPrecision;\n')
f.write(' }\n')
f.write(f' @media (min-width: {width * 2}px) and (min-height: {height * 2}px) {{\n')
f.write(' /* Disable anti-aliasing when zoom level > 200%.\n')
f.write(' However, there is currently no way to completely eliminate edge artifacts.\n')
f.write(' See also: https://bugs.webkit.org/show_bug.cgi?id=35211 */\n')
f.write(' .pixart2svg {\n')
f.write(' shape-rendering: crispEdges;\n')
f.write(' }\n')
f.write(' }\n')
f.write(' </style>\n')
f.write(' <g class="pixart2svg">\n')
f.flush()
for x, y in sorted([(x, y) for y in range(height) for x in range(width)], key=lambda x: (x[0] + x[1], x[1], x[0])):
if is_pixel_filled[y, x]:
continue
color_block = fill_block(image, is_pixel_filled, x, y)
path = generate_path(color_block)
pixel_color = '#{:02x}{:02x}{:02x}'.format(image[y, x, 0], image[y, x, 1], image[y, x, 2])
if len(path) == 4:
assert path[0][1] == path[1][1]
assert path[1][0] == path[2][0]
assert path[2][1] == path[3][1]
assert path[3][0] == path[0][0]
rect_x = path[0][0]
rect_y = path[0][1]
rect_w = path[2][0] - path[0][0]
rect_h = path[2][1] - path[0][1]
f.write(f' <rect fill="{pixel_color}" x="{rect_x}" y="{rect_y}" width="{rect_w}" height="{rect_h}" />\n')
else:
x1, y1 = path[0]
path_string = f'M {x1},{y1}'
for x2, y2 in path[1:]:
if x1 == x2:
path_string += f' V {y2}'
elif y1 == y2:
path_string += f' H {x2}'
else:
path_string += f' L {x2},{y2}' # Should not happen
x1, y1 = x2, y2
path_string += ' z'
f.write(f' <path fill="{pixel_color}" d="{path_string}"/>\n')
f.flush()
f.write(' </g>\n')
f.write('</svg>\n')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment