Skip to content

Instantly share code, notes, and snippets.

@zobar
Created January 5, 2018 01:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zobar/5d99e749d7ebbe945c404177780f8ae8 to your computer and use it in GitHub Desktop.
Save zobar/5d99e749d7ebbe945c404177780f8ae8 to your computer and use it in GitHub Desktop.
outline
FROM python:2
RUN apt-get update\
&& apt-get install --assume-yes --no-install-recommends python-pil\
&& rm --recursive /var/lib/apt/lists/*
WORKDIR /usr/src/app
#!/usr/bin/python
from __future__ import with_statement
import cStringIO as StringIO
import os.path
import PIL.Image
import sys
import xml.dom.minidom as dom
_DOMImplementation = dom.getDOMImplementation()
_xmlns_svg = 'http://www.w3.org/2000/svg'
_xmlns_xlink = 'http://www.w3.org/1999/xlink'
_symbols = dom.parse(os.path.join(os.path.dirname(__file__), 'symbols.svg')).getElementsByTagNameNS(_xmlns_svg, 'symbol')
def _format_svg(number, space='', precision=2):
prefix = '-' if number < 0 else space
result = ('%.*f' % (precision, abs(number))).strip('0').rstrip('.')
if not result:
result = '0'
return prefix + result
class Path(object):
def get_fill_opacity(self):
return self._fill_opacity
def set_fill_opacity(self, value):
self._fill_opacity = value
fill_opacity = property(get_fill_opacity, set_fill_opacity)
def get_fill(self):
return self._fill
def set_fill(self, value):
self._fill = value
fill = property(get_fill, set_fill)
@property
def fill_brightness(self):
fill = self._fill
return max(fill >> 16 & 0xff, fill >> 8 & 0xff, fill & 0xff)
def get_stroke(self):
return self._stroke
def set_stroke(self, value):
self._stroke = value
stroke = property(get_stroke, set_stroke)
@property
def x(self):
return self._primary[0][0] if self._primary else None
@property
def y(self):
return self._primary[0][1] if self._primary else None
def __init__(self, fill=0x000000, fill_opacity=1, stroke=None):
super(Path, self).__init__()
self.segments = []
self._fill = fill
self._fill_opacity = fill_opacity
self._primary = None
self._stroke = stroke
def __iter__(self):
x = 0
y = 0
if self._primary is not None:
move = True
x0, y0 = self._primary[0]
for point in self._primary:
x2, y2 = point
if move:
yield ('m', (x2 - x, y2 - y))
move = False
elif x2 == x0 and y2 == y0:
yield ('z', ())
elif x == x2:
yield ('v', (y2 - y,))
elif y == y2:
yield ('h', (x2 - x,))
x, y = point
for segment in self.segments:
if segment is not self._primary:
move = True
x0, y0 = segment[0]
for point in reversed(segment):
x2, y2 = point
if move:
yield ('m', (x2 - x, y2 - y))
move = False
elif x2 == x0 and y2 == y0:
yield ('z', ())
elif x == x2:
yield ('v', (y2 - y,))
elif y == y2:
yield ('h', (x2 - x,))
x, y = point
def __repr__(self):
if self.segments:
result = StringIO.StringIO()
for segment in self.segments:
result.write('*' if segment == self._primary else ' ')
result.write(repr(segment))
result.write('\n')
return result.getvalue()
else:
return 'Empty\n'
def __str__(self):
result = StringIO.StringIO()
for op, coords in self:
result.write(op)
if len(coords):
result.write(_format_svg(coords[0]))
for coord in coords[1:]:
result.write(_format_svg(coord, ' '))
return result.getvalue()
def add_path(self, other):
for segment in other.segments:
self.add_line(segment)
def add_line(self, points, anchor=False):
first, last = points[0], points[-1]
for i, segment in enumerate(self.segments):
seg_first, seg_last = segment[0], segment[-1]
head = tail = None
if first == seg_last:
head = segment
tail = points[1:]
elif last == seg_first:
head = points[:-1]
tail = segment
elif first == seg_first:
head = points[1:]
tail = segment
if anchor:
head, tail = tail, head
head.reverse()
elif last == seg_last:
head = segment
tail = points[:-1]
if anchor:
head, tail = tail, head
tail.reverse()
if head is not None and tail is not None:
if len(head) >= len(tail) and ((head[-2][0] == head[-1][0] and head[-1][0] == tail[0][0]) or (head[-2][1] == head[-1][1] and head[-1][1] == tail[0][1])):
head = head[:-1]
elif len(tail) >= len(head) and ((head[-1][0] == tail[0][0] and tail[0][0] == tail[1][0]) or (head[-1][1] == tail[0][1] and tail[0][1] == tail[1][1])):
tail = tail[1:]
line = head + tail
if line[0] == line[-1]:
tl_i = 0
tl_x, tl_y = line[0]
for j, point in enumerate(line):
if point[1] < tl_y or (point[1] == tl_y and point[0] < tl_x):
tl_i = j
tl_x, tl_y = point
line = line[tl_i:-1] + line[:tl_i] + [line[tl_i]]
del self.segments[i]
result = self.add_line(line, segment == self._primary)
if segment == self._primary:
self._primary = result
break
else:
self.segments.append(points)
result = points
if self._primary is None:
self._primary = result
return result
def to_svg(self, document):
svg = document.createElementNS(_xmlns_svg, 'path')
svg.setAttribute('d', str(self))
if self.fill is None:
svg.setAttribute('fill', 'none')
elif self.fill != 0x000000:
svg.setAttribute('fill', '#%06x' % self.fill)
if self.fill_opacity != 1:
svg.setAttribute('fill-opacity', _format_svg(self.fill_opacity))
if self.stroke is not None:
svg.setAttribute('stroke', '#%06x' % self.stroke)
return svg
def outline(input_file):
def get_rgba(x, y):
rgba = palette[data[x, y]] if palette is not None else data[x, y]
rgb = (rgba[0] << 16) | (rgba[1] << 8) | rgba[2]
return (rgb, 1) if len(rgba) == 3 else (rgb, rgba[3] / 255.0)
def match(path):
if path is None:
return False
return (rgb == path.fill and a == path.fill_opacity)
fill_symbols = {}
input = PIL.Image.open(input_file)
data = input.load()
document = _DOMImplementation.createDocument(_xmlns_svg, 'svg', None)
merged = {}
next_symbol = 0
palette = _get_palette(input)
x = 0
y = 0
svg = document.documentElement
defs = svg.appendChild(document.createElementNS(_xmlns_svg, 'defs'))
shape_layer = svg.appendChild(document.createElementNS(_xmlns_svg, 'g'))
shape_layer.setAttribute('id', 'shapes')
shape_layer.setAttribute('stroke', '#ff00ff')
shape_layer.setAttribute('stroke-width', '0.5')
symbol_layer = document.createElementNS(_xmlns_svg, 'g')
symbol_layer.setAttribute('id', 'symbols')
width, height = input.size
paths = [[None] * width for i in xrange(height)]
svg.setAttribute('height', '%sin' % _format_svg(height / 14.0))
svg.setAttribute('viewBox', '0 0 %s %s' % (_format_svg(width), _format_svg(height)))
svg.setAttribute('width', '%sin' % _format_svg(width / 14.0))
svg.setAttribute('xmlns', _xmlns_svg)
svg.setAttribute('xmlns:xlink', _xmlns_xlink)
for y in xrange(height):
for x in xrange(width):
path_l = paths[y][x - 1] if x > 0 else None
path_t = paths[y - 1][x] if y > 0 else None
rgb, a = get_rgba(x, y)
match_l = match(path_l)
match_t = match(path_t)
while path_l in merged:
path_l = merged[path_l]
while path_t in merged:
path_t = merged[path_t]
if match_l and match_t and path_l != path_t:
path_t.add_path(path_l)
merged[path_l] = path_t
path_l = path_t
if match_t:
path = path_t
elif match_l:
path = path_l
else:
path = Path(fill=rgb, fill_opacity=a)
if not match_l:
line = [(x, y + 1), (x, y)]
path.add_line(line)
if path_l is not None:
path_l.add_line(line)
if not match_t:
line = [(x, y), (x + 1, y)]
path.add_line(line)
if path_t is not None:
path_t.add_line(line)
paths[y][x] = path
paths[y][width - 1].add_line([(width, y), (width, y + 1)])
for x in xrange(width):
paths[height - 1][x].add_line([(x, height), (x + 1, height)])
visited = set()
for row in paths:
for path in row:
while path in merged:
path = merged[path]
if path not in visited:
visited.add(path)
if path.fill_opacity:
shape_layer.appendChild(path.to_svg(document))
if path.fill in fill_symbols:
symbol_name = fill_symbols[path.fill]
else:
symbol = _symbols.item(next_symbol)
symbol_name = symbol.getAttribute('id')
viewbox = [float(coord) for coord in symbol.getAttribute('viewBox').split(' ')]
symbol_path = None
for child in symbol.childNodes:
if child.nodeType == child.ELEMENT_NODE:
symbol_path = child.cloneNode(False)
break
if symbol_path:
height = viewbox[3]
width = viewbox[2]
side = max(height, width)
scale = 1/side
tx = (side - width) / 2
ty = (side - height) / 2
symbol_path.setAttribute('id', symbol_name)
if path.fill_brightness < 0x80:
symbol_path.setAttribute('fill', '#ffffff')
symbol_path.setAttribute('transform', 'scale(%s) translate(%s %s)' % (_format_svg(scale, precision=6), _format_svg(tx), _format_svg(ty)))
defs.appendChild(symbol_path)
fill_symbols[path.fill] = symbol_name
next_symbol += 1
if symbol_name:
use = document.createElementNS(_xmlns_svg, 'use')
use.setAttribute('transform', 'translate(%s %s)' % (_format_svg(path.x), _format_svg(path.y)))
use.setAttributeNS(_xmlns_xlink, 'xlink:href', '#%s' % symbol_name)
symbol_layer.appendChild(use)
svg.appendChild(symbol_layer)
return document
def _get_palette(input):
p = input.getpalette()
if p is None:
palette = None
else:
palette = [(p[i], p[i+1], p[i+2], 255) for i in range(0, len(p), 3)]
if 'transparency' in input.info:
t = input.info['transparency']
c = palette[t]
palette[t] = (c[0], c[1], c[2], 0)
return palette
if __name__ == '__main__':
if len(sys.argv) > 1:
for arg in sys.argv[1:]:
input = arg
document = outline(input)
sys.stdout.write(document.toprettyxml(encoding='utf-8'))
else:
print >> sys.stderr, 'Usage: %s input.png [input.png...]' % sys.argv[0]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment