mirror of
https://github.com/google/pebble.git
synced 2025-05-22 19:34:51 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
14
tools/generate_pdcs/__init__.py
Normal file
14
tools/generate_pdcs/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 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.
|
||||
|
144
tools/generate_pdcs/graph.py
Normal file
144
tools/generate_pdcs/graph.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
|
||||
Credit to Bernd Klein for this graph class.
|
||||
http://www.python-course.eu/graphs_python.php
|
||||
|
||||
Modifications by kmisquitta:
|
||||
1) Added support for pretty printing
|
||||
2) Added function to return a vertex's neighbours
|
||||
3) Added support for traversing to a vertex more than once in find_all_paths
|
||||
4) Removed support for adding edges that are sets
|
||||
5) Removed support for multiple edges between two vertices
|
||||
6) Added support for traversing beyond the end vertex in find_all_paths
|
||||
7) Removed many unneeded features
|
||||
|
||||
"""
|
||||
|
||||
""" A Python Class
|
||||
A simple Python graph class, demonstrating the essential
|
||||
facts and functionalities of graphs.
|
||||
"""
|
||||
|
||||
import pprint
|
||||
|
||||
|
||||
def is_line_segment_in_path(path, vertex_1, vertex_2):
|
||||
for i in range(len(path) - 1):
|
||||
if path[i] == vertex_1 and path[i + 1] == vertex_2 \
|
||||
or path[i] == vertex_2 and path[i + 1] == vertex_1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Graph(object):
|
||||
|
||||
def __init__(self, graph_dict={}):
|
||||
""" initializes a graph object """
|
||||
self.__graph_dict = graph_dict
|
||||
|
||||
def get_vertices(self):
|
||||
""" returns the vertices of a graph """
|
||||
return list(self.__graph_dict.keys())
|
||||
|
||||
def get_edges(self):
|
||||
""" returns the edges of a graph """
|
||||
return self.__generate_edges()
|
||||
|
||||
def get_neighbours(self, vertex):
|
||||
""" returns the neighbours of a vertex """
|
||||
return list(self.__graph_dict[vertex])
|
||||
|
||||
def add_vertex(self, vertex):
|
||||
""" If the vertex "vertex" is not in
|
||||
self.__graph_dict, a key "vertex" with an empty
|
||||
list as a value is added to the dictionary.
|
||||
Otherwise nothing has to be done.
|
||||
"""
|
||||
if vertex not in self.__graph_dict:
|
||||
self.__graph_dict[vertex] = []
|
||||
|
||||
def add_edge(self, edge):
|
||||
""" assumes that edge is of type tuple or list """
|
||||
if len(edge) < 2:
|
||||
return
|
||||
|
||||
vertex1 = edge[0]
|
||||
vertex2 = edge[1]
|
||||
if vertex1 in self.__graph_dict:
|
||||
if not (vertex2 in self.get_neighbours(vertex1)):
|
||||
self.__graph_dict[vertex1].append(vertex2)
|
||||
else:
|
||||
self.__graph_dict[vertex1] = [vertex2]
|
||||
|
||||
def __generate_edges(self):
|
||||
""" A static method generating the edges of the
|
||||
graph "graph". Edges are represented as sets
|
||||
with one (a loop back to the vertex) or two
|
||||
vertices
|
||||
"""
|
||||
edges = []
|
||||
for vertex in self.__graph_dict:
|
||||
for neighbour in self.__graph_dict[vertex]:
|
||||
if {vertex, neighbour} not in edges:
|
||||
edges.append({vertex, neighbour})
|
||||
return edges
|
||||
|
||||
def __str__(self):
|
||||
res = "vertices: "
|
||||
for k in self.__graph_dict:
|
||||
res += str(k) + " "
|
||||
res += "\nedges: "
|
||||
for edge in self.__generate_edges():
|
||||
res += str(edge) + " "
|
||||
return res
|
||||
|
||||
def find_all_paths(self, start_vertex, end_vertex, path=[]):
|
||||
""" Recursive function that finds all paths from the start vertex to the end vertex.
|
||||
Starts from the start vertex and traverses through vertices until the end vertex is reached.
|
||||
If there are untraversed edges when the end vertex is reached, will continue traversing
|
||||
to check for paths back to the end vertex (loops).
|
||||
There is no limit to how many times a vertex can be traversed.
|
||||
An edge may be traversed only once.
|
||||
"""
|
||||
graph = self.__graph_dict
|
||||
paths = []
|
||||
path = path + [start_vertex]
|
||||
if start_vertex == end_vertex:
|
||||
# Check if additional traversals is possible
|
||||
neighbours = self.get_neighbours(end_vertex)
|
||||
no_possible_traversals = True
|
||||
for neighbour in neighbours:
|
||||
if not is_line_segment_in_path(path, end_vertex, neighbour):
|
||||
no_possible_traversals = False
|
||||
break
|
||||
if no_possible_traversals: # Base case
|
||||
return [path]
|
||||
else:
|
||||
paths.append(path) # Add current path, continue finding
|
||||
if start_vertex not in graph:
|
||||
return []
|
||||
for vertex in graph[start_vertex]:
|
||||
if not is_line_segment_in_path(path, vertex, start_vertex):
|
||||
extended_paths = self.find_all_paths(vertex,
|
||||
end_vertex,
|
||||
path)
|
||||
for p in extended_paths:
|
||||
paths.append(p)
|
||||
return paths
|
||||
|
||||
def prettyprint(self):
|
||||
pprint.pprint(self.__graph_dict)
|
337
tools/generate_pdcs/json2commands.py
Normal file
337
tools/generate_pdcs/json2commands.py
Normal file
|
@ -0,0 +1,337 @@
|
|||
# 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.
|
||||
|
||||
'''
|
||||
JSON2COMMANDS creates Pebble Draw Commands (the Python Objects, _not_ a serialized .pdc) from a JSON file.
|
||||
Currently only the PathCommand is supported.
|
||||
|
||||
The JSON file can contain multiple frames (i.e. PDC Sequence).
|
||||
Each frame is composed of 'fillGroups'.
|
||||
A fillGroup may be: An individual filled polygon (a.k.a. a fill), or _all_ unfilled polylines (a.k.a. all open paths).
|
||||
Each fillGroup is parsed separately and a list of Pebble Draw Commands that describe it is created.
|
||||
The created list should have the length of the lowest number of commands possible in order to draw that fillGroup.
|
||||
|
||||
Currently, there is no support for a JSON to contain the viewbox size or fill colors.
|
||||
The viewbox size is currently passed in as a parameter.
|
||||
The fill color is currently defaulted to solid white.
|
||||
'''
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import pebble_commands
|
||||
import json
|
||||
import graph
|
||||
from itertools import groupby
|
||||
|
||||
INVISIBLE_POINT_THRESHOLD = 500
|
||||
DISPLAY_DIM_X = 144
|
||||
DISPLAY_DIM_Y = 168
|
||||
OPEN_PATH_TAG = "_"
|
||||
|
||||
|
||||
def parse_color(color_opacity, truncate):
|
||||
if color_opacity is None:
|
||||
return 0
|
||||
|
||||
r = int(round(255 * color_opacity[0]))
|
||||
g = int(round(255 * color_opacity[1]))
|
||||
b = int(round(255 * color_opacity[2]))
|
||||
a = int(round(255 * color_opacity[3]))
|
||||
|
||||
return pebble_commands.convert_color(r, g, b, a, truncate)
|
||||
|
||||
|
||||
def parse_json_line_data(json_line_data, viewbox_size=(DISPLAY_DIM_X, DISPLAY_DIM_Y)):
|
||||
# A list of one-way vectors, but intended to store their negatives at all times.
|
||||
bidirectional_lines = []
|
||||
|
||||
for line_data in json_line_data:
|
||||
# Skip invisible lines
|
||||
if abs(line_data['startPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['startPoint'][1]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['endPoint'][0]) > INVISIBLE_POINT_THRESHOLD or \
|
||||
abs(line_data['endPoint'][1]) > INVISIBLE_POINT_THRESHOLD:
|
||||
continue
|
||||
|
||||
# Center the viewbox of all lines (by moving the lines' absolute
|
||||
# coordinates relative to the screen)
|
||||
dx = -(DISPLAY_DIM_X - viewbox_size[0]) / 2
|
||||
dy = -(DISPLAY_DIM_Y - viewbox_size[1]) / 2
|
||||
start_point = (line_data["startPoint"][0] + dx, line_data["startPoint"][1] + dy)
|
||||
end_point = (line_data["endPoint"][0] + dx, line_data["endPoint"][1] + dy)
|
||||
|
||||
# Since lines are represented and stored as one-way vectors, but may be
|
||||
# drawn in either direction, all operations must be done on their reverse
|
||||
line = (start_point, end_point)
|
||||
reverse_line = (end_point, start_point)
|
||||
|
||||
# Skip duplicate lines
|
||||
if line in bidirectional_lines:
|
||||
continue
|
||||
|
||||
bidirectional_lines.append(line)
|
||||
bidirectional_lines.append(reverse_line)
|
||||
|
||||
return bidirectional_lines
|
||||
|
||||
|
||||
def determine_longest_path(bidirectional_lines):
|
||||
'''
|
||||
Returns the longest path in 'bidirectional_lines', and removes all its segments from 'bidirectional_lines'
|
||||
If 'bidirectional_lines' contains more than one possible longest path, only one will be returned.
|
||||
'''
|
||||
# Construct graph out of bidirectional_lines
|
||||
g = graph.Graph({})
|
||||
for line in bidirectional_lines:
|
||||
g.add_edge(line)
|
||||
|
||||
# Find longest path
|
||||
longest_path_length = 0
|
||||
longest_path = []
|
||||
vertices = g.get_vertices()
|
||||
for i in range(len(vertices)):
|
||||
start_vertex = vertices[i]
|
||||
|
||||
for j in range(i, len(vertices)):
|
||||
end_vertex = vertices[j]
|
||||
paths = g.find_all_paths(start_vertex, end_vertex)
|
||||
for path in paths:
|
||||
if (len(path) - 1) > longest_path_length:
|
||||
longest_path = path
|
||||
longest_path_length = len(path) - 1
|
||||
|
||||
# Edge case - Line is a point
|
||||
if len(longest_path) == 1:
|
||||
longest_path = [longest_path, longest_path]
|
||||
|
||||
# Remove longest_path's line segments from bidirectional_lines
|
||||
# Since bidirectional_lines is a list of one-way vectors but represents
|
||||
# bidirectional lines, a line segment and its reverse must be removed to
|
||||
# keep its integrity
|
||||
for k in range(len(longest_path) - 1):
|
||||
path_line = (longest_path[k], longest_path[k + 1])
|
||||
reverse_path_line = (path_line[1], path_line[0])
|
||||
bidirectional_lines.remove(path_line)
|
||||
bidirectional_lines.remove(reverse_path_line)
|
||||
|
||||
return longest_path
|
||||
|
||||
|
||||
def process_unique_group_of_lines(unique_group_data, translate, viewbox_size, path_open, stroke_width, stroke_color, fill_color, precise, raise_error):
|
||||
'''
|
||||
Creates a list of commands that draw out a unique group of lines.
|
||||
|
||||
A unique group of lines is defined as having a unique stroke width, stroke color, and fill.
|
||||
Note that this does _not_ guarantee the group may be described by a single Pebble Draw Command.
|
||||
'''
|
||||
|
||||
unique_group_commands = []
|
||||
|
||||
bidirectional_lines = parse_json_line_data(unique_group_data, viewbox_size)
|
||||
if not bidirectional_lines:
|
||||
return unique_group_commands
|
||||
|
||||
while bidirectional_lines:
|
||||
longest_path = determine_longest_path(bidirectional_lines)
|
||||
|
||||
try:
|
||||
c = pebble_commands.PathCommand(longest_path,
|
||||
path_open,
|
||||
translate,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if c is not None:
|
||||
unique_group_commands.append(c)
|
||||
except pebble_commands.InvalidPointException:
|
||||
raise
|
||||
|
||||
return unique_group_commands
|
||||
|
||||
|
||||
def process_fill(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
||||
fill_command = []
|
||||
error = False
|
||||
|
||||
# A fill is implicitly a unique group of lines - all line segments must have the same stroke width, stroke color
|
||||
# Get line style from first line segment
|
||||
stroke_width = fillGroup_data[0]['thickness']
|
||||
stroke_color = parse_color(fillGroup_data[0]['color'], truncate_color)
|
||||
# Fill color should be solid white until it can be inserted in the JSON
|
||||
fill_color = parse_color([1, 1, 1, 1], truncate_color)
|
||||
if stroke_color == 0:
|
||||
stroke_width = 0
|
||||
elif stroke_width == 0:
|
||||
stroke_color = 0
|
||||
|
||||
try:
|
||||
unique_group_commands = process_unique_group_of_lines(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if unique_group_commands:
|
||||
fill_command += unique_group_commands
|
||||
except pebble_commands.InvalidPointException:
|
||||
error = True
|
||||
|
||||
return fill_command, error
|
||||
|
||||
|
||||
def process_open_paths(fillGroup_data, translate, viewbox_size, path_open, precise, raise_error, truncate_color):
|
||||
open_paths_commands = []
|
||||
error = False
|
||||
|
||||
fill_color = parse_color([0, 0, 0, 0], truncate_color) # No fill color
|
||||
|
||||
# These open paths are part of the same fillGroup, but may have varied stroke width
|
||||
fillGroup_data = sorted(fillGroup_data, key=lambda a: a['thickness'])
|
||||
for stroke_width, unique_width_group in groupby(fillGroup_data, lambda c: c['thickness']):
|
||||
unique_width_data = list(unique_width_group)
|
||||
|
||||
# These open paths have the same width, but may have varied color
|
||||
unique_width_data = sorted(unique_width_data, key=lambda d: d['color'])
|
||||
for stroke_color_raw, unique_width_and_color_group in groupby(unique_width_data, lambda e: e['color']):
|
||||
# These are a unique group of lines
|
||||
unique_width_and_color_data = list(unique_width_and_color_group)
|
||||
|
||||
stroke_color = parse_color(stroke_color_raw, truncate_color)
|
||||
if stroke_color == 0:
|
||||
stroke_width = 0
|
||||
elif stroke_width == 0:
|
||||
stroke_color = 0
|
||||
|
||||
try:
|
||||
unique_group_commands = process_unique_group_of_lines(
|
||||
unique_width_and_color_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
stroke_width,
|
||||
stroke_color,
|
||||
fill_color,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if unique_group_commands:
|
||||
open_paths_commands += unique_group_commands
|
||||
except pebble_commands.InvalidPointException:
|
||||
error = True
|
||||
|
||||
return open_paths_commands, error
|
||||
|
||||
|
||||
def get_commands(translate, viewbox_size, frame_data, precise=False, raise_error=False, truncate_color=True):
|
||||
commands = []
|
||||
errors = []
|
||||
|
||||
fillGroups_data = frame_data['lineData']
|
||||
|
||||
# The 'fillGroup' property describes the type of group: A unique letter
|
||||
# (e.g. "A", "B", "C" etc.) for a unique fill, and a special identifier
|
||||
# for ALL open paths (non-fills)
|
||||
only_fills = list([d for d in fillGroups_data if d["fillGroup"] != OPEN_PATH_TAG])
|
||||
only_fills = sorted(only_fills, key=lambda f: f["fillGroup"]) # Don't assume data is sorted
|
||||
only_open_paths = list([d for d in fillGroups_data if d["fillGroup"] == OPEN_PATH_TAG])
|
||||
# Fills must be drawn before open paths, so place them first
|
||||
ordered_fill_groups = only_fills + only_open_paths
|
||||
|
||||
# Process fillGroups
|
||||
for path_type, fillGroup in groupby(ordered_fill_groups, lambda b: b['fillGroup']):
|
||||
fillGroup_data = list(fillGroup)
|
||||
|
||||
path_open = path_type == '_'
|
||||
if not path_open:
|
||||
# Filled fillGroup
|
||||
fillGroup_commands, error = process_fill(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
precise,
|
||||
raise_error,
|
||||
truncate_color)
|
||||
else:
|
||||
# Open path fillGroup
|
||||
fillGroup_commands, error = process_open_paths(
|
||||
fillGroup_data,
|
||||
translate,
|
||||
viewbox_size,
|
||||
path_open,
|
||||
precise,
|
||||
raise_error,
|
||||
truncate_color)
|
||||
|
||||
if error:
|
||||
errors += str(path_type)
|
||||
elif fillGroup_commands:
|
||||
commands += fillGroup_commands
|
||||
|
||||
if not commands:
|
||||
# Insert one 'invisible' command so the frame is valid
|
||||
c = pebble_commands.PathCommand([((0.0), (0.0)), ((0.0), (0.0))],
|
||||
True,
|
||||
translate,
|
||||
0,
|
||||
0,
|
||||
0)
|
||||
commands.append(c)
|
||||
|
||||
return commands, errors
|
||||
|
||||
|
||||
def parse_json_sequence(filename, viewbox_size, precise=False, raise_error=False):
|
||||
frames = []
|
||||
errors = []
|
||||
translate = (0, 0)
|
||||
|
||||
with open(filename) as json_file:
|
||||
try:
|
||||
data = json.load(json_file)
|
||||
except ValueError:
|
||||
print('Invalid JSON format')
|
||||
return frames, 0, 0
|
||||
|
||||
frames_data = data['lineData']
|
||||
frame_duration = int(data['compData']['frameDuration'] * 1000)
|
||||
for idx, frame_data in enumerate(frames_data):
|
||||
cmd_list, frame_errors = get_commands(
|
||||
translate,
|
||||
viewbox_size,
|
||||
frame_data,
|
||||
precise,
|
||||
raise_error)
|
||||
|
||||
if frame_errors:
|
||||
errors.append((idx, frame_errors))
|
||||
elif cmd_list is not None:
|
||||
frames.append(cmd_list)
|
||||
|
||||
return frames, errors, frame_duration
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('path', type=str, help="Path to json file")
|
||||
args = parser.parse_args()
|
||||
path = os.path.abspath(args.path)
|
||||
parse_json_sequence(path)
|
136
tools/generate_pdcs/pdc_gen.py
Normal file
136
tools/generate_pdcs/pdc_gen.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
# 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.
|
||||
|
||||
'''
|
||||
PDC_GEN converts SVG images, SVG sequences, or JSON sequences to a PDC (Pebble Draw Command) binary format image or sequence. The PDC file format
|
||||
consists of a header, followed by the binary representation of a PDC image or sequence.
|
||||
The file header is as follows:
|
||||
Magic Word (4 bytes) - 'PDCI' for image, 'PDCS' for sequence
|
||||
Size (4 bytes) - size of PDC image or sequence following the header in bytes
|
||||
'''
|
||||
|
||||
import os
|
||||
import argparse
|
||||
|
||||
import pebble_commands
|
||||
import svg2commands
|
||||
import json2commands
|
||||
|
||||
|
||||
def create_pdc_data_from_path(path, viewbox_size, verbose, duration, play_count,
|
||||
precise=False, raise_error=False):
|
||||
dir_name = path
|
||||
output = ''
|
||||
errors = []
|
||||
if not os.path.exists(path):
|
||||
raise Exception("Invalid path")
|
||||
|
||||
if verbose:
|
||||
print path + ":"
|
||||
if os.path.isfile(path):
|
||||
dir_name = os.path.dirname(path)
|
||||
frames = []
|
||||
commands = []
|
||||
|
||||
if os.path.isfile(path):
|
||||
ext = os.path.splitext(path)[-1]
|
||||
if ext == '.json':
|
||||
# JSON file
|
||||
result = json2commands.parse_json_sequence(path, viewbox_size, precise, raise_error)
|
||||
if result:
|
||||
frames = result[0]
|
||||
errors += result[1]
|
||||
frame_duration = result[2]
|
||||
output = pebble_commands.serialize_sequence(
|
||||
frames, viewbox_size, frame_duration, play_count)
|
||||
elif ext == '.svg':
|
||||
# SVG file
|
||||
size, commands, error = svg2commands.parse_svg_image(path, verbose, precise,
|
||||
raise_error)
|
||||
if commands:
|
||||
output = pebble_commands.serialize_image(commands, size)
|
||||
if error:
|
||||
errors += [path]
|
||||
else:
|
||||
# SVG files
|
||||
# get all .svg files in directory
|
||||
result = svg2commands.parse_svg_sequence(dir_name, verbose, precise, raise_error)
|
||||
if result:
|
||||
frames = result[1]
|
||||
size = result[0]
|
||||
errors += result[2]
|
||||
output = pebble_commands.serialize_sequence(frames, size, duration, play_count)
|
||||
|
||||
if verbose:
|
||||
if frames:
|
||||
pebble_commands.print_frames(frames)
|
||||
elif commands:
|
||||
pebble_commands.print_commands(commands)
|
||||
|
||||
return output, errors
|
||||
|
||||
|
||||
def create_pdc_from_path(path, out_path, viewbox_size, verbose, duration, play_count,
|
||||
precise=False, raise_error=False):
|
||||
|
||||
output, errors = create_pdc_data_from_path(path, viewbox_size, verbose, duration, play_count,
|
||||
precise=False, raise_error=False)
|
||||
|
||||
if output != '':
|
||||
if out_path is None:
|
||||
if sequence:
|
||||
f = os.path.basename(dir_name.rstrip('/')) + '.pdc'
|
||||
else:
|
||||
base = os.path.basename(path)
|
||||
f = '.'.join(base.split('.')[:-1]) + '.pdc'
|
||||
out_path = os.path.join(dir_name, f)
|
||||
with open(out_path, 'w') as out_file:
|
||||
out_file.write(output)
|
||||
out_file.close()
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main(args):
|
||||
path = os.path.abspath(args.path)
|
||||
viewbox_size = (args.viewbox_x, args.viewbox_y)
|
||||
errors = create_pdc_from_path(path, args.output, viewbox_size, args.verbose, args.duration,
|
||||
args.play_count, args.precise)
|
||||
if errors:
|
||||
print "Errors in the following files or frames:"
|
||||
for ef in errors:
|
||||
print "\t" + str(ef)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('path', type=str,
|
||||
help="Path to svg file or directory (with multiple svg files)")
|
||||
parser.add_argument('-o', '--output', type=str,
|
||||
help="Output file path (.pdc will be appended to file name if it is not included in the path "
|
||||
"specified")
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help="Verbose output")
|
||||
parser.add_argument('-d', '--duration', type=int, default=33,
|
||||
help="Duration (ms) of each frame in a sequence (SVG sequence only) - default = 33ms")
|
||||
parser.add_argument('-c', '--play_count', type=int, default=1,
|
||||
help="Number of times the sequence should play - default = 1")
|
||||
parser.add_argument('-p', '--precise', action='store_true',
|
||||
help="Use sub-pixel precision for paths")
|
||||
parser.add_argument('-x', '--viewbox_x', help="Viewbox length (JSON sequence only)",
|
||||
type=int, default=json2commands.DISPLAY_DIM_X)
|
||||
parser.add_argument('-y', '--viewbox_y', help="Viewbox height (JSON sequence only)",
|
||||
type=int, default=json2commands.DISPLAY_DIM_Y)
|
||||
args = parser.parse_args()
|
||||
main(args)
|
281
tools/generate_pdcs/pebble_commands.py
Normal file
281
tools/generate_pdcs/pebble_commands.py
Normal file
|
@ -0,0 +1,281 @@
|
|||
# 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.
|
||||
|
||||
'''
|
||||
PEBBLE_COMMANDS contains all the classes and methods to create Pebble Images and Sequences in PDC file format.
|
||||
|
||||
Images and Sequences are drawn from a list of Pebble Draw Commands (PDCs).
|
||||
An Image may be drawn from multiple commands.
|
||||
A Sequence is an ordered list of 'frames' (or Images).
|
||||
|
||||
There are two types of Draw Commands ('PathCommand' and 'CircleCommand') that can be created from a list of properties.
|
||||
The serialization of both types of commands is described in the 'Command' class below.
|
||||
'''
|
||||
|
||||
import sys
|
||||
from struct import pack
|
||||
from pebble_image_routines import nearest_color_to_pebble64_palette, \
|
||||
truncate_color_to_pebble64_palette, \
|
||||
rgba32_triplet_to_argb8
|
||||
|
||||
epsilon = sys.float_info.epsilon
|
||||
|
||||
DRAW_COMMAND_VERSION = 1
|
||||
DRAW_COMMAND_TYPE_PATH = 1
|
||||
DRAW_COMMAND_TYPE_CIRCLE = 2
|
||||
DRAW_COMMAND_TYPE_PRECISE_PATH = 3
|
||||
|
||||
COORDINATE_SHIFT_WARNING_THRESHOLD = 0.1
|
||||
|
||||
xmlns = '{http://www.w3.org/2000/svg}'
|
||||
|
||||
|
||||
def sum_points(p1, p2):
|
||||
return p1[0] + p2[0], p1[1] + p2[1]
|
||||
|
||||
|
||||
def subtract_points(p1, p2):
|
||||
return p1[0] - p2[0], p1[1] - p2[1]
|
||||
|
||||
|
||||
def round_point(p):
|
||||
# hack to get around the fact that python rounds negative
|
||||
# numbers downwards
|
||||
return round(p[0] + epsilon), round(p[1] + epsilon)
|
||||
|
||||
|
||||
def scale_point(p, factor):
|
||||
return p[0] * factor, p[1] * factor
|
||||
|
||||
|
||||
def find_nearest_valid_point(p):
|
||||
return (round(p[0] * 2.0) / 2.0), (round(p[1] * 2.0) / 2.0)
|
||||
|
||||
|
||||
def find_nearest_valid_precise_point(p):
|
||||
return (round(p[0] * 8.0) / 8.0), (round(p[1] * 8.0) / 8.0)
|
||||
|
||||
|
||||
def convert_to_pebble_coordinates(point, verbose=False, precise=False):
|
||||
# convert from graphic tool coordinate system to pebble coordinate system so that they render the same on
|
||||
# both
|
||||
|
||||
if not precise:
|
||||
# used to give feedback to user if the point shifts considerably
|
||||
nearest = find_nearest_valid_point(point)
|
||||
else:
|
||||
nearest = find_nearest_valid_precise_point(point)
|
||||
|
||||
valid = compare_points(point, nearest)
|
||||
if not valid and verbose:
|
||||
print "Invalid point: ({}, {}). Closest supported coordinate: ({}, {})".format(point[0], point[1],
|
||||
nearest[0], nearest[1])
|
||||
|
||||
translated = sum_points(point, (-0.5, -0.5)) # translate point by (-0.5, -0.5)
|
||||
if precise:
|
||||
translated = scale_point(translated, 8) # scale point for precise coordinates
|
||||
rounded = round_point(translated)
|
||||
|
||||
return rounded, valid
|
||||
|
||||
|
||||
def compare_points(p1, p2):
|
||||
return p1[0] == p2[0] and p1[1] == p2[1]
|
||||
|
||||
|
||||
def valid_color(r, g, b, a):
|
||||
return (r <= 0xFF) and (g <= 0xFF) and (b <= 0xFF) and (a <= 0xFF) and \
|
||||
(r >= 0x00) and (g >= 0x00) and (b >= 0x00) and (a >= 0x00)
|
||||
|
||||
|
||||
def convert_color(r, g, b, a, truncate=True):
|
||||
|
||||
valid = valid_color(r, g, b, a)
|
||||
if not valid:
|
||||
print "Invalid color: ({}, {}, {}, {})".format(r, g, b, a)
|
||||
return 0
|
||||
|
||||
if truncate:
|
||||
(r, g, b, a) = truncate_color_to_pebble64_palette(r, g, b, a)
|
||||
else:
|
||||
(r, g, b, a) = nearest_color_to_pebble64_palette(r, g, b, a)
|
||||
|
||||
return rgba32_triplet_to_argb8(r, g, b, a)
|
||||
|
||||
|
||||
class InvalidPointException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Command():
|
||||
|
||||
'''
|
||||
Draw command serialized structure:
|
||||
| Bytes | Field
|
||||
| 1 | Draw command type
|
||||
| 1 | Reserved byte
|
||||
| 1 | Stroke color
|
||||
| 1 | Stroke width
|
||||
| 1 | Fill color
|
||||
|
||||
For Paths:
|
||||
| 1 | Open path
|
||||
| 1 | Unused/Reserved
|
||||
|
||||
For Circles:
|
||||
| 2 | Radius
|
||||
|
||||
Common:
|
||||
| 2 | Number of points (should always be 1 for circles)
|
||||
| n * 4 | Array of n points in the format below:
|
||||
|
||||
|
||||
Point:
|
||||
| 2 | x
|
||||
| 2 | y
|
||||
'''
|
||||
|
||||
def __init__(self, points, translate, stroke_width=0, stroke_color=0, fill_color=0,
|
||||
verbose=False, precise=False, raise_error=False):
|
||||
for i in range(len(points)):
|
||||
points[i], valid = convert_to_pebble_coordinates(
|
||||
sum_points(points[i], translate), verbose, precise)
|
||||
if not valid and raise_error:
|
||||
raise InvalidPointException("Invalid point in command")
|
||||
|
||||
self.points = points
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.fill_color = fill_color
|
||||
|
||||
def serialize_common(self):
|
||||
return pack('<BBBB',
|
||||
0, # reserved byte
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
self.fill_color)
|
||||
|
||||
def serialize_points(self):
|
||||
s = pack('H', len(self.points)) # number of points (16-bit)
|
||||
for p in self.points:
|
||||
s += pack('<hh',
|
||||
int(p[0]), # x (16-bit)
|
||||
int(p[1])) # y (16-bit)
|
||||
return s
|
||||
|
||||
|
||||
class PathCommand(Command):
|
||||
|
||||
def __init__(self, points, path_open, translate, stroke_width=0, stroke_color=0, fill_color=0,
|
||||
verbose=False, precise=False, raise_error=False):
|
||||
self.open = path_open
|
||||
self.type = DRAW_COMMAND_TYPE_PATH if not precise else DRAW_COMMAND_TYPE_PRECISE_PATH
|
||||
Command.__init__(self, points, translate, stroke_width, stroke_color, fill_color, verbose,
|
||||
precise, raise_error)
|
||||
|
||||
def serialize(self):
|
||||
s = pack('B', self.type) # command type
|
||||
s += self.serialize_common()
|
||||
s += pack('<BB',
|
||||
int(self.open), # open path boolean
|
||||
0) # unused byte in path
|
||||
s += self.serialize_points()
|
||||
return s
|
||||
|
||||
def __str__(self):
|
||||
points = self.points[:]
|
||||
if self.type == DRAW_COMMAND_TYPE_PRECISE_PATH:
|
||||
type = 'P'
|
||||
for i in range(len(points)):
|
||||
points[i] = scale_point(points[i], 0.125)
|
||||
else:
|
||||
type = ''
|
||||
return "Path: [fill color:{}; stroke color:{}; stroke width:{}] {} {} {}".format(self.fill_color,
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
points,
|
||||
self.open,
|
||||
type)
|
||||
|
||||
|
||||
class CircleCommand(Command):
|
||||
|
||||
def __init__(self, center, radius, translate, stroke_width=0, stroke_color=0, fill_color=0,
|
||||
verbose=False):
|
||||
points = [(center[0], center[1])]
|
||||
Command.__init__(self, points, translate, stroke_width, stroke_color, fill_color, verbose)
|
||||
self.radius = radius
|
||||
|
||||
def serialize(self):
|
||||
s = pack('B', DRAW_COMMAND_TYPE_CIRCLE) # command type
|
||||
s += self.serialize_common()
|
||||
s += pack('H', self.radius) # circle radius (16-bit)
|
||||
s += self.serialize_points()
|
||||
return s
|
||||
|
||||
def __str__(self):
|
||||
return "Circle: [fill color:{}; stroke color:{}; stroke width:{}] {} {}".format(self.fill_color,
|
||||
self.stroke_color,
|
||||
self.stroke_width,
|
||||
self.points[
|
||||
0],
|
||||
self.radius)
|
||||
|
||||
|
||||
def serialize(commands):
|
||||
output = pack('H', len(commands)) # number of commands in list
|
||||
for c in commands:
|
||||
output += c.serialize()
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def print_commands(commands):
|
||||
for c in commands:
|
||||
print str(c)
|
||||
|
||||
|
||||
def print_frames(frames):
|
||||
for i in range(len(frames)):
|
||||
print 'Frame {}:'.format(i + 1)
|
||||
print_commands(frames[i])
|
||||
|
||||
|
||||
def serialize_frame(frame, duration):
|
||||
return pack('H', duration) + serialize(frame) # Frame duration
|
||||
|
||||
|
||||
def pack_header(size):
|
||||
return pack('<BBhh', DRAW_COMMAND_VERSION, 0, int(round(size[0])), int(round(size[1])))
|
||||
|
||||
|
||||
def serialize_sequence(frames, size, duration, play_count):
|
||||
s = pack_header(size) + pack('H', play_count) + pack('H', len(frames))
|
||||
for f in frames:
|
||||
s += serialize_frame(f, duration)
|
||||
|
||||
output = "PDCS"
|
||||
output += pack('I', len(s))
|
||||
output += s
|
||||
return output
|
||||
|
||||
|
||||
def serialize_image(commands, size):
|
||||
s = pack_header(size)
|
||||
s += serialize(commands)
|
||||
|
||||
output = "PDCI"
|
||||
output += pack('I', len(s))
|
||||
output += s
|
||||
return output
|
296
tools/generate_pdcs/svg2commands.py
Normal file
296
tools/generate_pdcs/svg2commands.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
# 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
|
||||
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 group.getchildren():
|
||||
# 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
|
Loading…
Add table
Add a link
Reference in a new issue