Created
January 5, 2018 01:54
-
-
Save zobar/5d99e749d7ebbe945c404177780f8ae8 to your computer and use it in GitHub Desktop.
outline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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