pebble_commands.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. '''
  15. PEBBLE_COMMANDS contains all the classes and methods to create Pebble Images and Sequences in PDC file format.
  16. Images and Sequences are drawn from a list of Pebble Draw Commands (PDCs).
  17. An Image may be drawn from multiple commands.
  18. A Sequence is an ordered list of 'frames' (or Images).
  19. There are two types of Draw Commands ('PathCommand' and 'CircleCommand') that can be created from a list of properties.
  20. The serialization of both types of commands is described in the 'Command' class below.
  21. '''
  22. import sys
  23. from struct import pack
  24. from pebble_image_routines import nearest_color_to_pebble64_palette, \
  25. truncate_color_to_pebble64_palette, \
  26. rgba32_triplet_to_argb8
  27. epsilon = sys.float_info.epsilon
  28. DRAW_COMMAND_VERSION = 1
  29. DRAW_COMMAND_TYPE_PATH = 1
  30. DRAW_COMMAND_TYPE_CIRCLE = 2
  31. DRAW_COMMAND_TYPE_PRECISE_PATH = 3
  32. COORDINATE_SHIFT_WARNING_THRESHOLD = 0.1
  33. xmlns = '{http://www.w3.org/2000/svg}'
  34. def sum_points(p1, p2):
  35. return p1[0] + p2[0], p1[1] + p2[1]
  36. def subtract_points(p1, p2):
  37. return p1[0] - p2[0], p1[1] - p2[1]
  38. def round_point(p):
  39. # hack to get around the fact that python rounds negative
  40. # numbers downwards
  41. return round(p[0] + epsilon), round(p[1] + epsilon)
  42. def scale_point(p, factor):
  43. return p[0] * factor, p[1] * factor
  44. def find_nearest_valid_point(p):
  45. return (round(p[0] * 2.0) / 2.0), (round(p[1] * 2.0) / 2.0)
  46. def find_nearest_valid_precise_point(p):
  47. return (round(p[0] * 8.0) / 8.0), (round(p[1] * 8.0) / 8.0)
  48. def convert_to_pebble_coordinates(point, verbose=False, precise=False):
  49. # convert from graphic tool coordinate system to pebble coordinate system so that they render the same on
  50. # both
  51. if not precise:
  52. # used to give feedback to user if the point shifts considerably
  53. nearest = find_nearest_valid_point(point)
  54. else:
  55. nearest = find_nearest_valid_precise_point(point)
  56. valid = compare_points(point, nearest)
  57. if not valid and verbose:
  58. print("Invalid point: ({}, {}). Closest supported coordinate: ({}, {})".format(point[0], point[1],
  59. nearest[0],
  60. nearest[1]))
  61. translated = sum_points(point, (-0.5, -0.5)) # translate point by (-0.5, -0.5)
  62. if precise:
  63. translated = scale_point(translated, 8) # scale point for precise coordinates
  64. rounded = round_point(translated)
  65. return rounded, valid
  66. def compare_points(p1, p2):
  67. return p1[0] == p2[0] and p1[1] == p2[1]
  68. def valid_color(r, g, b, a):
  69. return (r <= 0xFF) and (g <= 0xFF) and (b <= 0xFF) and (a <= 0xFF) and \
  70. (r >= 0x00) and (g >= 0x00) and (b >= 0x00) and (a >= 0x00)
  71. def convert_color(r, g, b, a, truncate=True):
  72. valid = valid_color(r, g, b, a)
  73. if not valid:
  74. print("Invalid color: ({}, {}, {}, {})".format(r, g, b, a))
  75. return 0
  76. if truncate:
  77. (r, g, b, a) = truncate_color_to_pebble64_palette(r, g, b, a)
  78. else:
  79. (r, g, b, a) = nearest_color_to_pebble64_palette(r, g, b, a)
  80. return rgba32_triplet_to_argb8(r, g, b, a)
  81. class InvalidPointException(Exception):
  82. pass
  83. class Command():
  84. '''
  85. Draw command serialized structure:
  86. | Bytes | Field
  87. | 1 | Draw command type
  88. | 1 | Reserved byte
  89. | 1 | Stroke color
  90. | 1 | Stroke width
  91. | 1 | Fill color
  92. For Paths:
  93. | 1 | Open path
  94. | 1 | Unused/Reserved
  95. For Circles:
  96. | 2 | Radius
  97. Common:
  98. | 2 | Number of points (should always be 1 for circles)
  99. | n * 4 | Array of n points in the format below:
  100. Point:
  101. | 2 | x
  102. | 2 | y
  103. '''
  104. def __init__(self, points, translate, stroke_width=0, stroke_color=0, fill_color=0,
  105. verbose=False, precise=False, raise_error=False):
  106. for i in range(len(points)):
  107. points[i], valid = convert_to_pebble_coordinates(
  108. sum_points(points[i], translate), verbose, precise)
  109. if not valid and raise_error:
  110. raise InvalidPointException("Invalid point in command")
  111. self.points = points
  112. self.stroke_width = stroke_width
  113. self.stroke_color = stroke_color
  114. self.fill_color = fill_color
  115. def serialize_common(self):
  116. return pack('<BBBB',
  117. 0, # reserved byte
  118. self.stroke_color,
  119. self.stroke_width,
  120. self.fill_color)
  121. def serialize_points(self):
  122. s = pack('H', len(self.points)) # number of points (16-bit)
  123. for p in self.points:
  124. s += pack('<hh',
  125. int(p[0]), # x (16-bit)
  126. int(p[1])) # y (16-bit)
  127. return s
  128. class PathCommand(Command):
  129. def __init__(self, points, path_open, translate, stroke_width=0, stroke_color=0, fill_color=0,
  130. verbose=False, precise=False, raise_error=False):
  131. self.open = path_open
  132. self.type = DRAW_COMMAND_TYPE_PATH if not precise else DRAW_COMMAND_TYPE_PRECISE_PATH
  133. Command.__init__(self, points, translate, stroke_width, stroke_color, fill_color, verbose,
  134. precise, raise_error)
  135. def serialize(self):
  136. s = pack('B', self.type) # command type
  137. s += self.serialize_common()
  138. s += pack('<BB',
  139. int(self.open), # open path boolean
  140. 0) # unused byte in path
  141. s += self.serialize_points()
  142. return s
  143. def __str__(self):
  144. points = self.points[:]
  145. if self.type == DRAW_COMMAND_TYPE_PRECISE_PATH:
  146. type = 'P'
  147. for i in range(len(points)):
  148. points[i] = scale_point(points[i], 0.125)
  149. else:
  150. type = ''
  151. return "Path: [fill color:{}; stroke color:{}; stroke width:{}] {} {} {}".format(self.fill_color,
  152. self.stroke_color,
  153. self.stroke_width,
  154. points,
  155. self.open,
  156. type)
  157. class CircleCommand(Command):
  158. def __init__(self, center, radius, translate, stroke_width=0, stroke_color=0, fill_color=0,
  159. verbose=False):
  160. points = [(center[0], center[1])]
  161. Command.__init__(self, points, translate, stroke_width, stroke_color, fill_color, verbose)
  162. self.radius = radius
  163. def serialize(self):
  164. s = pack('B', DRAW_COMMAND_TYPE_CIRCLE) # command type
  165. s += self.serialize_common()
  166. s += pack('H', self.radius) # circle radius (16-bit)
  167. s += self.serialize_points()
  168. return s
  169. def __str__(self):
  170. return "Circle: [fill color:{}; stroke color:{}; stroke width:{}] {} {}".format(self.fill_color,
  171. self.stroke_color,
  172. self.stroke_width,
  173. self.points[
  174. 0],
  175. self.radius)
  176. def serialize(commands):
  177. output = pack('H', len(commands)) # number of commands in list
  178. for c in commands:
  179. output += c.serialize()
  180. return output
  181. def print_commands(commands):
  182. for c in commands:
  183. print(str(c))
  184. def print_frames(frames):
  185. for i in range(len(frames)):
  186. print('Frame {}:'.format(i + 1))
  187. print_commands(frames[i])
  188. def serialize_frame(frame, duration):
  189. return pack('H', duration) + serialize(frame) # Frame duration
  190. def pack_header(size):
  191. return pack('<BBhh', DRAW_COMMAND_VERSION, 0, int(round(size[0])), int(round(size[1])))
  192. def serialize_sequence(frames, size, duration, play_count):
  193. s = pack_header(size) + pack('H', play_count) + pack('H', len(frames))
  194. for f in frames:
  195. s += serialize_frame(f, duration)
  196. output = b"PDCS"
  197. output += pack('I', len(s))
  198. output += s
  199. return output
  200. def serialize_image(commands, size):
  201. s = pack_header(size)
  202. s += serialize(commands)
  203. output = b"PDCI"
  204. output += pack('I', len(s))
  205. output += s
  206. return output