svg2commands.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. SVG2COMMANDS creates Pebble Draw Commands (the Python Objects, _not_ a serialized .pdc) from SVG file(s).
  16. 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.
  17. Currently the following SVG elements are supported:
  18. g, layer, path, rect, polyline, polygon, line, circle,
  19. '''
  20. import xml.etree.ElementTree as ET
  21. import svg.path
  22. import glob
  23. from . import pebble_commands
  24. xmlns = '{http://www.w3.org/2000/svg}'
  25. def get_viewbox(root):
  26. try:
  27. coords = root.get('viewBox').split()
  28. return (float(coords[0]), float(coords[1])), (float(coords[2]), float(coords[3]))
  29. except (ValueError, TypeError):
  30. return (0, 0), (0, 0)
  31. def get_translate(group):
  32. trans = group.get('translate')
  33. if trans is not None:
  34. pos = trans.find('translate')
  35. if pos < 0:
  36. print("No translation in translate")
  37. return 0, 0
  38. import ast
  39. try:
  40. return ast.literal_eval(trans[pos + len('translate'):])
  41. except (ValueError, TypeError):
  42. print("translate contains unsupported elements in addition to translation")
  43. return 0, 0
  44. def parse_color(color, opacity, truncate):
  45. if color is None or color[0] != '#':
  46. return 0
  47. rgb = int(color[1:7], 16)
  48. r, g, b = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF
  49. a = int(opacity * 255)
  50. return pebble_commands.convert_color(r, g, b, a, truncate)
  51. def calc_opacity(a1, a2):
  52. try:
  53. a1 = float(a1)
  54. except (ValueError, TypeError):
  55. a1 = 1.0
  56. try:
  57. a2 = float(a2)
  58. except (ValueError, TypeError):
  59. a2 = 1.0
  60. return a1 * a2
  61. def get_points_from_str(point_str):
  62. points = []
  63. for p in point_str.split():
  64. pair = p.split(',')
  65. try:
  66. points.append((float(pair[0]), float(pair[1])))
  67. except (ValueError, TypeError):
  68. return None
  69. return points
  70. def parse_path(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  71. raise_error):
  72. import svg.path
  73. d = element.get('d')
  74. if d is not None:
  75. path = svg.path.parse_path(d)
  76. points = [(lambda l: (l.real, l.imag))(line.start) for line in path]
  77. if not points:
  78. print("No points in parsed path")
  79. return None
  80. path_open = path[-1].end != path[0].start
  81. if path_open:
  82. points.append((path[-1].end.real, path[-1].end.imag))
  83. # remove last point if it matches first point
  84. if pebble_commands.compare_points(points[0], points[-1]):
  85. points = points[0:-1]
  86. return pebble_commands.PathCommand(points, path_open, translate, stroke_width, stroke_color,
  87. fill_color, verbose, precise, raise_error)
  88. else:
  89. print("Path element does not have path attribute")
  90. def parse_circle(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  91. raise_error):
  92. cx = element.get('cx') # center x-value
  93. cy = element.get('cy') # center y-value
  94. radius = element.get('r') # radius
  95. if radius is None:
  96. radius = element.get('z') # 'z' sometimes used instead of 'r' for radius
  97. if cx is not None and cy is not None and radius is not None:
  98. try:
  99. center = (float(cx), float(cy))
  100. radius = float(radius)
  101. return pebble_commands.CircleCommand(center, radius, translate, stroke_width,
  102. stroke_color, fill_color, verbose)
  103. except ValueError:
  104. print("Unrecognized circle format")
  105. else:
  106. print("Unrecognized circle format")
  107. def parse_polyline(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  108. raise_error):
  109. points = get_points_from_str(element.get('points'))
  110. if not points:
  111. return None
  112. return pebble_commands.PathCommand(points, True, translate, stroke_width, stroke_color,
  113. fill_color, verbose, precise, raise_error)
  114. def parse_polygon(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  115. raise_error):
  116. points = get_points_from_str(element.get('points'))
  117. if not points:
  118. return None
  119. return pebble_commands.PathCommand(points, False, translate, stroke_width, stroke_color,
  120. fill_color, verbose, precise, raise_error)
  121. def parse_line(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  122. raise_error):
  123. try:
  124. points = [(float(element.get('x1')), float(element.get('y1'))),
  125. (float(element.get('x2')), float(element.get('y2')))]
  126. except (TypeError, ValueError):
  127. return None
  128. return pebble_commands.PathCommand(points, True, translate, stroke_width, stroke_color,
  129. fill_color, verbose, precise, raise_error)
  130. def parse_rect(element, translate, stroke_width, stroke_color, fill_color, verbose, precise,
  131. raise_error):
  132. try:
  133. origin = (float(element.get('x')), float(element.get('y')))
  134. width = float(element.get('width'))
  135. height = float(element.get('height'))
  136. except (ValueError, TypeError):
  137. return None
  138. points = [origin, pebble_commands.sum_points(origin, (width, 0)), pebble_commands.sum_points(origin,
  139. (width, height)), pebble_commands.sum_points(origin, (0, height))]
  140. return pebble_commands.PathCommand(points, False, translate, stroke_width, stroke_color,
  141. fill_color, verbose, precise, raise_error)
  142. svg_element_parser = {'path': parse_path,
  143. 'circle': parse_circle,
  144. 'polyline': parse_polyline,
  145. 'polygon': parse_polygon,
  146. 'line': parse_line,
  147. 'rect': parse_rect}
  148. def create_command(translate, element, verbose=False, precise=False, raise_error=False,
  149. truncate_color=True):
  150. try:
  151. stroke_width = int(element.get('stroke-width'))
  152. except TypeError:
  153. stroke_width = 1
  154. except ValueError:
  155. stroke_width = 0
  156. stroke_color = parse_color(element.get('stroke'), calc_opacity(element.get('stroke-opacity'),
  157. element.get('opacity')), truncate_color)
  158. fill_color = parse_color(element.get('fill'), calc_opacity(element.get('fill-opacity'), element.get('opacity')),
  159. truncate_color)
  160. if stroke_color == 0 and fill_color == 0:
  161. return None
  162. if stroke_color == 0:
  163. stroke_width = 0
  164. elif stroke_width == 0:
  165. stroke_color = 0
  166. try:
  167. tag = element.tag[len(xmlns):]
  168. except IndexError:
  169. return None
  170. try:
  171. return svg_element_parser[tag](element, translate, stroke_width, stroke_color, fill_color,
  172. verbose, precise, raise_error)
  173. except KeyError:
  174. if tag != 'g' and tag != 'layer':
  175. print("Unsupported element: " + tag)
  176. return None
  177. def get_commands(translate, group, verbose=False, precise=False, raise_error=False,
  178. truncate_color=True):
  179. commands = []
  180. error = False
  181. for child in list(group):
  182. # ignore elements that are marked display="none"
  183. display = child.get('display')
  184. if display is not None and display == 'none':
  185. continue
  186. try:
  187. tag = child.tag[len(xmlns):]
  188. except IndexError:
  189. continue
  190. # traverse tree of nested layers or groups
  191. if tag == 'layer' or tag == 'g':
  192. translate += get_translate(child)
  193. cmd_list, err = get_commands(translate, child, verbose, precise, raise_error,
  194. truncate_color)
  195. commands += cmd_list
  196. if err:
  197. error = True
  198. else:
  199. try:
  200. c = create_command(translate, child, verbose, precise, raise_error, truncate_color)
  201. if c is not None:
  202. commands.append(c)
  203. except pebble_commands.InvalidPointException:
  204. error = True
  205. return commands, error
  206. def get_xml(filename):
  207. try:
  208. root = ET.parse(filename).getroot()
  209. except IOError:
  210. return None
  211. return root
  212. def get_info(xml):
  213. viewbox = get_viewbox(xml)
  214. # subtract origin point in viewbox to get relative positions
  215. translate = (-viewbox[0][0], -viewbox[0][1])
  216. return translate, viewbox[1]
  217. def parse_svg_image(filename, verbose=False, precise=False, raise_error=False):
  218. root = get_xml(filename)
  219. translate, size = get_info(root)
  220. cmd_list, error = get_commands(translate, root, verbose, precise, raise_error)
  221. return size, cmd_list, error
  222. def parse_svg_sequence(dir_name, verbose=False, precise=False, raise_error=False):
  223. frames = []
  224. error_files = []
  225. file_list = sorted(glob.glob(dir_name + "/*.svg"))
  226. if not file_list:
  227. return
  228. translate, size = get_info(get_xml(file_list[0])) # get the viewbox from the first file
  229. for filename in file_list:
  230. cmd_list, error = get_commands(translate, get_xml(filename), verbose, precise, raise_error)
  231. if cmd_list is not None:
  232. frames.append(cmd_list)
  233. if error:
  234. error_files.append(filename)
  235. return size, frames, error_files