123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- # Copyright 2024 Google LLC
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- '''
- SVG2COMMANDS creates Pebble Draw Commands (the Python Objects, _not_ a serialized .pdc) from SVG file(s).
- Either a single SVG file may be parsed into a list of commands for a PDC Image, or a directory of files may be parsed into a list of commands for a PDC Sequence.
- Currently the following SVG elements are supported:
- g, layer, path, rect, polyline, polygon, line, circle,
- '''
- import xml.etree.ElementTree as ET
- import svg.path
- import glob
- from . import pebble_commands
- xmlns = '{http://www.w3.org/2000/svg}'
- def get_viewbox(root):
- try:
- coords = root.get('viewBox').split()
- return (float(coords[0]), float(coords[1])), (float(coords[2]), float(coords[3]))
- except (ValueError, TypeError):
- return (0, 0), (0, 0)
- def get_translate(group):
- trans = group.get('translate')
- if trans is not None:
- pos = trans.find('translate')
- if pos < 0:
- print("No translation in translate")
- return 0, 0
- import ast
- try:
- return ast.literal_eval(trans[pos + len('translate'):])
- except (ValueError, TypeError):
- print("translate contains unsupported elements in addition to translation")
- return 0, 0
- def parse_color(color, opacity, truncate):
- if color is None or color[0] != '#':
- return 0
- rgb = int(color[1:7], 16)
- r, g, b = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF
- a = int(opacity * 255)
- return pebble_commands.convert_color(r, g, b, a, truncate)
- def calc_opacity(a1, a2):
- try:
- a1 = float(a1)
- except (ValueError, TypeError):
- a1 = 1.0
- try:
- a2 = float(a2)
- except (ValueError, TypeError):
- a2 = 1.0
- return a1 * a2
- def get_points_from_str(point_str):
- points = []
- for p in point_str.split():
- pair = p.split(',')
- try:
- points.append((float(pair[0]), float(pair[1])))
- except (ValueError, TypeError):
- return None
- return points
- def parse_path(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- import svg.path
- d = element.get('d')
- if d is not None:
- path = svg.path.parse_path(d)
- points = [(lambda l: (l.real, l.imag))(line.start) for line in path]
- if not points:
- print("No points in parsed path")
- return None
- path_open = path[-1].end != path[0].start
- if path_open:
- points.append((path[-1].end.real, path[-1].end.imag))
- # remove last point if it matches first point
- if pebble_commands.compare_points(points[0], points[-1]):
- points = points[0:-1]
- return pebble_commands.PathCommand(points, path_open, translate, stroke_width, stroke_color,
- fill_color, verbose, precise, raise_error)
- else:
- print("Path element does not have path attribute")
- def parse_circle(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- cx = element.get('cx') # center x-value
- cy = element.get('cy') # center y-value
- radius = element.get('r') # radius
- if radius is None:
- radius = element.get('z') # 'z' sometimes used instead of 'r' for radius
- if cx is not None and cy is not None and radius is not None:
- try:
- center = (float(cx), float(cy))
- radius = float(radius)
- return pebble_commands.CircleCommand(center, radius, translate, stroke_width,
- stroke_color, fill_color, verbose)
- except ValueError:
- print("Unrecognized circle format")
- else:
- print("Unrecognized circle format")
- def parse_polyline(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- points = get_points_from_str(element.get('points'))
- if not points:
- return None
- return pebble_commands.PathCommand(points, True, translate, stroke_width, stroke_color,
- fill_color, verbose, precise, raise_error)
- def parse_polygon(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- points = get_points_from_str(element.get('points'))
- if not points:
- return None
- return pebble_commands.PathCommand(points, False, translate, stroke_width, stroke_color,
- fill_color, verbose, precise, raise_error)
- def parse_line(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- try:
- points = [(float(element.get('x1')), float(element.get('y1'))),
- (float(element.get('x2')), float(element.get('y2')))]
- except (TypeError, ValueError):
- return None
- return pebble_commands.PathCommand(points, True, translate, stroke_width, stroke_color,
- fill_color, verbose, precise, raise_error)
- def parse_rect(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
- raise_error):
- try:
- origin = (float(element.get('x')), float(element.get('y')))
- width = float(element.get('width'))
- height = float(element.get('height'))
- except (ValueError, TypeError):
- return None
- points = [origin, pebble_commands.sum_points(origin, (width, 0)), pebble_commands.sum_points(origin,
- (width, height)), pebble_commands.sum_points(origin, (0, height))]
- return pebble_commands.PathCommand(points, False, translate, stroke_width, stroke_color,
- fill_color, verbose, precise, raise_error)
- svg_element_parser = {'path': parse_path,
- 'circle': parse_circle,
- 'polyline': parse_polyline,
- 'polygon': parse_polygon,
- 'line': parse_line,
- 'rect': parse_rect}
- def create_command(translate, element, verbose=False, precise=False, raise_error=False,
- truncate_color=True):
- try:
- stroke_width = int(element.get('stroke-width'))
- except TypeError:
- stroke_width = 1
- except ValueError:
- stroke_width = 0
- stroke_color = parse_color(element.get('stroke'), calc_opacity(element.get('stroke-opacity'),
- element.get('opacity')), truncate_color)
- fill_color = parse_color(element.get('fill'), calc_opacity(element.get('fill-opacity'), element.get('opacity')),
- truncate_color)
- if stroke_color == 0 and fill_color == 0:
- return None
- if stroke_color == 0:
- stroke_width = 0
- elif stroke_width == 0:
- stroke_color = 0
- try:
- tag = element.tag[len(xmlns):]
- except IndexError:
- return None
- try:
- return svg_element_parser[tag](element, translate, stroke_width, stroke_color, fill_color,
- verbose, precise, raise_error)
- except KeyError:
- if tag != 'g' and tag != 'layer':
- print("Unsupported element: " + tag)
- return None
- def get_commands(translate, group, verbose=False, precise=False, raise_error=False,
- truncate_color=True):
- commands = []
- error = False
- for child in list(group):
- # ignore elements that are marked display="none"
- display = child.get('display')
- if display is not None and display == 'none':
- continue
- try:
- tag = child.tag[len(xmlns):]
- except IndexError:
- continue
- # traverse tree of nested layers or groups
- if tag == 'layer' or tag == 'g':
- translate += get_translate(child)
- cmd_list, err = get_commands(translate, child, verbose, precise, raise_error,
- truncate_color)
- commands += cmd_list
- if err:
- error = True
- else:
- try:
- c = create_command(translate, child, verbose, precise, raise_error, truncate_color)
- if c is not None:
- commands.append(c)
- except pebble_commands.InvalidPointException:
- error = True
- return commands, error
- def get_xml(filename):
- try:
- root = ET.parse(filename).getroot()
- except IOError:
- return None
- return root
- def get_info(xml):
- viewbox = get_viewbox(xml)
- # subtract origin point in viewbox to get relative positions
- translate = (-viewbox[0][0], -viewbox[0][1])
- return translate, viewbox[1]
- def parse_svg_image(filename, verbose=False, precise=False, raise_error=False):
- root = get_xml(filename)
- translate, size = get_info(root)
- cmd_list, error = get_commands(translate, root, verbose, precise, raise_error)
- return size, cmd_list, error
- def parse_svg_sequence(dir_name, verbose=False, precise=False, raise_error=False):
- frames = []
- error_files = []
- file_list = sorted(glob.glob(dir_name + "/*.svg"))
- if not file_list:
- return
- translate, size = get_info(get_xml(file_list[0])) # get the viewbox from the first file
- for filename in file_list:
- cmd_list, error = get_commands(translate, get_xml(filename), verbose, precise, raise_error)
- if cmd_list is not None:
- frames.append(cmd_list)
- if error:
- error_files.append(filename)
- return size, frames, error_files
|