Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View 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.

View 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)

View 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)

View 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)

View 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

View 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