You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
280 lines
9.2 KiB
280 lines
9.2 KiB
#!/usr/bin/python
|
|
|
|
# Written By Marcio Teixeira 2018 - Aleph Objects, Inc.
|
|
#
|
|
# 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.
|
|
#
|
|
# To view a copy of the GNU General Public License, go to the following
|
|
# location: <https://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import print_function
|
|
import argparse,re,sys
|
|
|
|
usage = '''
|
|
This program extracts line segments from a SVG file and writes
|
|
them as coordinates in a C array. The x and y values will be
|
|
scaled from 0x0000 to 0xFFFE. 0xFFFF is used as path separator.
|
|
|
|
This program can only interpret straight segments, not curves.
|
|
It also cannot handle SVG transform attributes. To convert an
|
|
SVG file into the proper format, use the following procedure:
|
|
|
|
- Load SVG file into Inkscape
|
|
- Convert all Objects to Paths (Path -> Object to Path)
|
|
- Convert all Strokes to Paths (Path -> Stroke to Path)
|
|
- Combine all paths into one (Path -> Combine) [1]
|
|
- Convert all curves into short line segments
|
|
(Extensions -> Modify Paths -> Flatten Beziers...)
|
|
- Save as new SVG
|
|
- Convert into a header file using this utility
|
|
- To give paths individual names, break apart paths and
|
|
use the XML Editor to set the "id" attributes.
|
|
|
|
[1] Combining paths is necessary to remove transforms. You
|
|
could also use inkscape-applytransforms Inkscape extension.
|
|
|
|
'''
|
|
|
|
header = '''
|
|
/****************************************************************************
|
|
* 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. *
|
|
* *
|
|
* To view a copy of the GNU General Public License, go to the following *
|
|
* location: <https://www.gnu.org/licenses/>. *
|
|
****************************************************************************/
|
|
|
|
/**
|
|
* This file was auto-generated using "svg2cpp.py"
|
|
*
|
|
* The encoding consists of x,y pairs with the min and max scaled to
|
|
* 0x0000 and 0xFFFE. A single 0xFFFF in the data stream indicates the
|
|
* start of a new closed path.
|
|
*/
|
|
|
|
#pragma once
|
|
'''
|
|
|
|
class ComputeBoundingBox:
|
|
def reset(self):
|
|
self.x_min = float(" inf")
|
|
self.y_min = float(" inf")
|
|
self.x_max = float("-inf")
|
|
self.y_max = float("-inf")
|
|
self.n_points = 0
|
|
self.n_paths = 0
|
|
|
|
def command(self, type, x, y):
|
|
self.x_min = min(self.x_min, x)
|
|
self.x_max = max(self.x_max, x)
|
|
self.y_min = min(self.y_min, y)
|
|
self.y_max = max(self.y_max, y)
|
|
|
|
if type == "M":
|
|
self.n_paths += 1
|
|
self.n_points += 1
|
|
|
|
def scale(self, x, y):
|
|
x -= self.x_min
|
|
y -= self.y_min
|
|
x /= self.x_max - self.x_min
|
|
y /= self.y_max - self.y_min
|
|
#y = 1 - y # Flip upside down
|
|
return (x, y)
|
|
|
|
def path_finished(self, id):
|
|
pass
|
|
|
|
def write(self):
|
|
print("constexpr float x_min = %f;" % self.x_min)
|
|
print("constexpr float x_max = %f;" % self.x_max)
|
|
print("constexpr float y_min = %f;" % self.y_min)
|
|
print("constexpr float y_max = %f;" % self.y_max)
|
|
print()
|
|
|
|
def from_svg_view_box(self, svg):
|
|
s = re.search('<svg[^>]+>', svg);
|
|
if s:
|
|
m = re.search('viewBox="([0-9-.]+) ([0-9-.]+) ([0-9-.]+) ([0-9-.]+)"', svg)
|
|
if m:
|
|
self.x_min = float(m[1])
|
|
self.y_min = float(m[2])
|
|
self.x_max = float(m[3])
|
|
self.y_max = float(m[4])
|
|
return True
|
|
return False
|
|
|
|
# op
|
|
class WriteDataStructure:
|
|
def __init__(self, bounding_box):
|
|
self.bounds = bounding_box
|
|
|
|
def reset(self, ):
|
|
self.hex_words = []
|
|
|
|
def push(self, value):
|
|
self.hex_words.append("0x%04X" % (0xFFFF & int(value)))
|
|
|
|
def command(self, type, x, y):
|
|
if type == "M":
|
|
self.push(0xFFFF)
|
|
x, y = self.bounds.scale(x,y)
|
|
self.push(x * 0xFFFE)
|
|
self.push(y * 0xFFFE)
|
|
|
|
def path_finished(self, id):
|
|
if self.hex_words and self.hex_words[0] == "0xFFFF":
|
|
self.hex_words.pop(0)
|
|
print("const PROGMEM uint16_t", id + "[] = {" + ", ".join (self.hex_words) + "};")
|
|
self.hex_words = []
|
|
|
|
class Parser:
|
|
def __init__(self, op):
|
|
self.op = op
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
self.last_x = 0
|
|
self.last_y = 0
|
|
self.initial_x = 0
|
|
self.initial_y = 0
|
|
|
|
def process_svg_path_L_or_M(self, cmd, x, y):
|
|
self.op.command(cmd, x, y)
|
|
self.last_x = x
|
|
self.last_y = y
|
|
if cmd == "M":
|
|
self.initial_x = x
|
|
self.initial_y = y
|
|
|
|
def process_svg_path_data_cmd(self, id, cmd, a, b):
|
|
"""Converts the various types of moves into L or M commands
|
|
and dispatches to process_svg_path_L_or_M for further processing."""
|
|
if cmd == "Z" or cmd == "z":
|
|
self.process_svg_path_L_or_M("L", self.initial_x, self.initial_y)
|
|
elif cmd == "H":
|
|
self.process_svg_path_L_or_M("L", a, self.last_y)
|
|
elif cmd == "V":
|
|
self.process_svg_path_L_or_M("L", self.last_x, a)
|
|
elif cmd == "h":
|
|
self.process_svg_path_L_or_M("L", self.last_x + a, self.last_y)
|
|
elif cmd == "v":
|
|
self.process_svg_path_L_or_M("L", self.last_x, self.last_y + a)
|
|
elif cmd == "L":
|
|
self.process_svg_path_L_or_M("L", a, b)
|
|
elif cmd == "l":
|
|
self.process_svg_path_L_or_M("L", self.last_x + a, self.last_y + b)
|
|
elif cmd == "M":
|
|
self.process_svg_path_L_or_M("M", a, b)
|
|
elif cmd == "m":
|
|
self.process_svg_path_L_or_M("M", self.last_x + a, self.last_y + b)
|
|
else:
|
|
print("Unsupported path data command:", cmd, "in path", id, "\n", file=sys.stderr)
|
|
quit()
|
|
|
|
def eat_token(self, regex):
|
|
"""Looks for a token at the start of self.d.
|
|
If found, the token is removed."""
|
|
self.m = re.match(regex,self.d)
|
|
if self.m:
|
|
self.d = self.d[self.m.end():]
|
|
return self.m
|
|
|
|
def process_svg_path_data(self, id, d):
|
|
"""Breaks up the "d" attribute into individual commands
|
|
and calls "process_svg_path_data_cmd" for each"""
|
|
|
|
self.d = d
|
|
while (self.d):
|
|
if self.eat_token('\s+'):
|
|
pass # Just eat the spaces
|
|
|
|
elif self.eat_token('([LMHVZlmhvz])'):
|
|
cmd = self.m[1]
|
|
# The following commands take no arguments
|
|
if cmd == "Z" or cmd == "z":
|
|
self.process_svg_path_data_cmd(id, cmd, 0, 0)
|
|
|
|
elif self.eat_token('([CScsQqTtAa])'):
|
|
print("Unsupported path data command:", self.m[1], "in path", id, "\n", file=sys.stderr)
|
|
quit()
|
|
|
|
elif self.eat_token('([ ,]*[-0-9e.]+)+'):
|
|
# Process list of coordinates following command
|
|
coords = re.split('[ ,]+', self.m[0])
|
|
# The following commands take two arguments
|
|
if cmd == "L" or cmd == "l":
|
|
while coords:
|
|
self.process_svg_path_data_cmd(id, cmd, float(coords.pop(0)), float(coords.pop(0)))
|
|
elif cmd == "M":
|
|
while coords:
|
|
self.process_svg_path_data_cmd(id, cmd, float(coords.pop(0)), float(coords.pop(0)))
|
|
# If a MOVETO has multiple points, the subsequent ones are assumed to be LINETO
|
|
cmd = "L"
|
|
elif cmd == "m":
|
|
while coords:
|
|
self.process_svg_path_data_cmd(id, cmd, float(coords.pop(0)), float(coords.pop(0)))
|
|
# If a MOVETO has multiple points, the subsequent ones are assumed to be LINETO
|
|
cmd = "l"
|
|
# Assume all other commands are single argument
|
|
else:
|
|
while coords:
|
|
self.process_svg_path_data_cmd(id, cmd, float(coords.pop(0)), 0)
|
|
else:
|
|
print("Syntax error:", d, "in path", id, "\n", file=sys.stderr)
|
|
quit()
|
|
|
|
def process_svg_paths(self, svg):
|
|
self.op.reset()
|
|
for path in re.findall('<path[^>]+>', svg):
|
|
id = "<none>"
|
|
m = re.search(' id="(.*)"', path)
|
|
if m:
|
|
id = m[1]
|
|
|
|
m = re.search(' transform="(.*)"', path)
|
|
if m:
|
|
print("Found transform in path", id, "! Cannot process file!", file=sys.stderr)
|
|
quit()
|
|
|
|
m = re.search(' d="(.*)"', path)
|
|
if m:
|
|
self.process_svg_path_data(id, m[1])
|
|
self.op.path_finished(id)
|
|
self.reset()
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("filename")
|
|
args = parser.parse_args()
|
|
|
|
f = open(args.filename, "r")
|
|
data = f.read()
|
|
|
|
print(header)
|
|
|
|
b = ComputeBoundingBox()
|
|
if not b.from_svg_view_box(data):
|
|
# Can't find the view box, so use the bounding box of the elements themselves.
|
|
p = Parser(b)
|
|
p.process_svg_paths(data)
|
|
b.write()
|
|
|
|
w = WriteDataStructure(b)
|
|
p = Parser(w)
|
|
p.process_svg_paths(data)
|
|
|