rna_xml.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. # ***** BEGIN GPL LICENSE BLOCK *****
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ***** END GPL LICENSE BLOCK *****
  18. # <pep8 compliant>
  19. import bpy
  20. def build_property_typemap(skip_classes, skip_typemap):
  21. property_typemap = {}
  22. for attr in dir(bpy.types):
  23. cls = getattr(bpy.types, attr)
  24. if issubclass(cls, skip_classes):
  25. continue
  26. # # to support skip-save we can't get all props
  27. # properties = cls.bl_rna.properties.keys()
  28. properties = []
  29. for prop_id, prop in cls.bl_rna.properties.items():
  30. if not prop.is_skip_save:
  31. properties.append(prop_id)
  32. properties.remove("rna_type")
  33. property_typemap[attr] = properties
  34. if skip_typemap:
  35. for cls_name, properties_blacklist in skip_typemap.items():
  36. properties = property_typemap.get(cls_name)
  37. if properties is not None:
  38. for prop_id in properties_blacklist:
  39. try:
  40. properties.remove(prop_id)
  41. except:
  42. print("skip_typemap unknown prop_id '%s.%s'" % (cls_name, prop_id))
  43. else:
  44. print("skip_typemap unknown class '%s'" % cls_name)
  45. return property_typemap
  46. def print_ln(data):
  47. print(data, end="")
  48. def rna2xml(
  49. fw=print_ln,
  50. root_node="",
  51. root_rna=None, # must be set
  52. root_rna_skip=set(),
  53. root_ident="",
  54. ident_val=" ",
  55. skip_classes=(
  56. bpy.types.Operator,
  57. bpy.types.Panel,
  58. bpy.types.KeyingSet,
  59. bpy.types.Header,
  60. bpy.types.PropertyGroup,
  61. ),
  62. skip_typemap=None,
  63. pretty_format=True,
  64. method='DATA',
  65. ):
  66. from xml.sax.saxutils import quoteattr
  67. property_typemap = build_property_typemap(skip_classes, skip_typemap)
  68. # don't follow properties of this type, just reference them by name
  69. # they MUST have a unique 'name' property.
  70. # 'ID' covers most types
  71. referenced_classes = (
  72. bpy.types.ID,
  73. bpy.types.Bone,
  74. bpy.types.ActionGroup,
  75. bpy.types.PoseBone,
  76. bpy.types.Node,
  77. bpy.types.Sequence,
  78. )
  79. def number_to_str(val, val_type):
  80. if val_type == int:
  81. return "%d" % val
  82. elif val_type == float:
  83. return "%.6g" % val
  84. elif val_type == bool:
  85. return "TRUE" if val else "FALSE"
  86. else:
  87. raise NotImplemented("this type is not a number %s" % val_type)
  88. def rna2xml_node(ident, value, parent):
  89. ident_next = ident + ident_val
  90. # divide into attrs and nodes.
  91. node_attrs = []
  92. nodes_items = []
  93. nodes_lists = []
  94. value_type = type(value)
  95. if issubclass(value_type, skip_classes):
  96. return
  97. # XXX, fixme, pointcache has eternal nested pointer to its self.
  98. if value == parent:
  99. return
  100. value_type_name = value_type.__name__
  101. for prop in property_typemap[value_type_name]:
  102. subvalue = getattr(value, prop)
  103. subvalue_type = type(subvalue)
  104. if subvalue_type in {int, bool, float}:
  105. node_attrs.append("%s=\"%s\"" % (prop, number_to_str(subvalue, subvalue_type)))
  106. elif subvalue_type is str:
  107. node_attrs.append("%s=%s" % (prop, quoteattr(subvalue)))
  108. elif subvalue_type is set:
  109. node_attrs.append("%s=%s" % (prop, quoteattr("{" + ",".join(list(subvalue)) + "}")))
  110. elif subvalue is None:
  111. node_attrs.append("%s=\"NONE\"" % prop)
  112. elif issubclass(subvalue_type, referenced_classes):
  113. # special case, ID's are always referenced.
  114. node_attrs.append("%s=%s" % (prop, quoteattr(subvalue_type.__name__ + "::" + subvalue.name)))
  115. else:
  116. try:
  117. subvalue_ls = list(subvalue)
  118. except:
  119. subvalue_ls = None
  120. if subvalue_ls is None:
  121. nodes_items.append((prop, subvalue, subvalue_type))
  122. else:
  123. # check if the list contains native types
  124. subvalue_rna = value.path_resolve(prop, False)
  125. if type(subvalue_rna).__name__ == "bpy_prop_array":
  126. # check if this is a 0-1 color (rgb, rgba)
  127. # in that case write as a hexadecimal
  128. prop_rna = value.bl_rna.properties[prop]
  129. if (prop_rna.subtype == 'COLOR_GAMMA' and
  130. prop_rna.hard_min == 0.0 and
  131. prop_rna.hard_max == 1.0 and
  132. prop_rna.array_length in {3, 4}):
  133. # -----
  134. # color
  135. array_value = "#" + "".join(("%.2x" % int(v * 255) for v in subvalue_rna))
  136. else:
  137. # default
  138. def str_recursive(s):
  139. subsubvalue_type = type(s)
  140. if subsubvalue_type in {int, float, bool}:
  141. return number_to_str(s, subsubvalue_type)
  142. else:
  143. return " ".join([str_recursive(si) for si in s])
  144. array_value = " ".join(str_recursive(v) for v in subvalue_rna)
  145. node_attrs.append("%s=\"%s\"" % (prop, array_value))
  146. else:
  147. nodes_lists.append((prop, subvalue_ls, subvalue_type))
  148. # declare + attributes
  149. if pretty_format:
  150. if node_attrs:
  151. fw("%s<%s\n" % (ident, value_type_name))
  152. for node_attr in node_attrs:
  153. fw("%s%s\n" % (ident_next, node_attr))
  154. fw("%s>\n" % (ident_next,))
  155. else:
  156. fw("%s<%s>\n" % (ident, value_type_name))
  157. else:
  158. fw("%s<%s %s>\n" % (ident, value_type_name, " ".join(node_attrs)))
  159. # unique members
  160. for prop, subvalue, subvalue_type in nodes_items:
  161. fw("%s<%s>\n" % (ident_next, prop)) # XXX, this is awkward, how best to solve?
  162. rna2xml_node(ident_next + ident_val, subvalue, value)
  163. fw("%s</%s>\n" % (ident_next, prop)) # XXX, need to check on this.
  164. # list members
  165. for prop, subvalue, subvalue_type in nodes_lists:
  166. fw("%s<%s>\n" % (ident_next, prop))
  167. for subvalue_item in subvalue:
  168. if subvalue_item is not None:
  169. rna2xml_node(ident_next + ident_val, subvalue_item, value)
  170. fw("%s</%s>\n" % (ident_next, prop))
  171. fw("%s</%s>\n" % (ident, value_type_name))
  172. # -------------------------------------------------------------------------
  173. # needs re-working to be generic
  174. if root_node:
  175. fw("%s<%s>\n" % (root_ident, root_node))
  176. # bpy.data
  177. if method == 'DATA':
  178. ident = root_ident + ident_val
  179. for attr in dir(root_rna):
  180. # exceptions
  181. if attr.startswith("_"):
  182. continue
  183. elif attr in root_rna_skip:
  184. continue
  185. value = getattr(root_rna, attr)
  186. try:
  187. ls = value[:]
  188. except:
  189. ls = None
  190. if type(ls) == list:
  191. fw("%s<%s>\n" % (ident, attr))
  192. for blend_id in ls:
  193. rna2xml_node(ident + ident_val, blend_id, None)
  194. fw("%s</%s>\n" % (ident_val, attr))
  195. # any attribute
  196. elif method == 'ATTR':
  197. rna2xml_node(root_ident, root_rna, None)
  198. if root_node:
  199. fw("%s</%s>\n" % (root_ident, root_node))
  200. def xml2rna(root_xml,
  201. root_rna=None, # must be set
  202. ):
  203. def rna2xml_node(xml_node, value):
  204. # print("evaluating:", xml_node.nodeName)
  205. # ---------------------------------------------------------------------
  206. # Simple attributes
  207. for attr in xml_node.attributes.keys():
  208. # print(" ", attr)
  209. subvalue = getattr(value, attr, Ellipsis)
  210. if subvalue is Ellipsis:
  211. print("%s.%s not found" % (type(value).__name__, attr))
  212. else:
  213. value_xml = xml_node.attributes[attr].value
  214. subvalue_type = type(subvalue)
  215. tp_name = 'UNKNOWN'
  216. if subvalue_type == float:
  217. value_xml_coerce = float(value_xml)
  218. tp_name = 'FLOAT'
  219. elif subvalue_type == int:
  220. value_xml_coerce = int(value_xml)
  221. tp_name = 'INT'
  222. elif subvalue_type == bool:
  223. value_xml_coerce = {'TRUE': True, 'FALSE': False}[value_xml]
  224. tp_name = 'BOOL'
  225. elif subvalue_type == str:
  226. value_xml_coerce = value_xml
  227. tp_name = 'STR'
  228. elif hasattr(subvalue, "__len__"):
  229. if value_xml.startswith("#"):
  230. # read hexadecimal value as float array
  231. value_xml_split = value_xml[1:]
  232. value_xml_coerce = [int(value_xml_split[i:i + 2], 16) /
  233. 255 for i in range(0, len(value_xml_split), 2)]
  234. del value_xml_split
  235. else:
  236. value_xml_split = value_xml.split()
  237. try:
  238. value_xml_coerce = [int(v) for v in value_xml_split]
  239. except ValueError:
  240. try:
  241. value_xml_coerce = [float(v) for v in value_xml_split]
  242. except ValueError: # bool vector property
  243. value_xml_coerce = [{'TRUE': True, 'FALSE': False}[v] for v in value_xml_split]
  244. del value_xml_split
  245. tp_name = 'ARRAY'
  246. # print(" %s.%s (%s) --- %s" % (type(value).__name__, attr, tp_name, subvalue_type))
  247. try:
  248. setattr(value, attr, value_xml_coerce)
  249. except ValueError:
  250. # size mismatch
  251. val = getattr(value, attr)
  252. if len(val) < len(value_xml_coerce):
  253. setattr(value, attr, value_xml_coerce[:len(val)])
  254. else:
  255. setattr(value, attr, list(value_xml_coerce) + list(val)[len(value_xml_coerce):])
  256. # ---------------------------------------------------------------------
  257. # Complex attributes
  258. for child_xml in xml_node.childNodes:
  259. if child_xml.nodeType == child_xml.ELEMENT_NODE:
  260. # print()
  261. # print(child_xml.nodeName)
  262. subvalue = getattr(value, child_xml.nodeName, None)
  263. if subvalue is not None:
  264. elems = []
  265. for child_xml_real in child_xml.childNodes:
  266. if child_xml_real.nodeType == child_xml_real.ELEMENT_NODE:
  267. elems.append(child_xml_real)
  268. del child_xml_real
  269. if hasattr(subvalue, "__len__"):
  270. # Collection
  271. if len(elems) != len(subvalue):
  272. print("Size Mismatch! collection:", child_xml.nodeName)
  273. else:
  274. for i in range(len(elems)):
  275. child_xml_real = elems[i]
  276. subsubvalue = subvalue[i]
  277. if child_xml_real is None or subsubvalue is None:
  278. print("None found %s - %d collection:", (child_xml.nodeName, i))
  279. else:
  280. rna2xml_node(child_xml_real, subsubvalue)
  281. else:
  282. # print(elems)
  283. if len(elems) == 1:
  284. # sub node named by its type
  285. child_xml_real, = elems
  286. # print(child_xml_real, subvalue)
  287. rna2xml_node(child_xml_real, subvalue)
  288. else:
  289. # empty is valid too
  290. pass
  291. rna2xml_node(root_xml, root_rna)
  292. # -----------------------------------------------------------------------------
  293. # Utility function used by presets.
  294. # The idea is you can run a preset like a script with a few args.
  295. #
  296. # This roughly matches the operator 'bpy.ops.script.python_file_run'
  297. def _get_context_val(context, path):
  298. path_full = "context." + path
  299. try:
  300. value = eval(path_full)
  301. except:
  302. import traceback
  303. traceback.print_exc()
  304. print("Error: %r could not be found" % path_full)
  305. value = Ellipsis
  306. return value
  307. def xml_file_run(context, filepath, rna_map):
  308. import xml.dom.minidom
  309. xml_nodes = xml.dom.minidom.parse(filepath)
  310. bpy_xml = xml_nodes.getElementsByTagName("bpy")[0]
  311. for rna_path, xml_tag in rna_map:
  312. # first get xml
  313. # TODO, error check
  314. xml_node = bpy_xml.getElementsByTagName(xml_tag)[0]
  315. value = _get_context_val(context, rna_path)
  316. if value is not Ellipsis and value is not None:
  317. print(" loading XML: %r -> %r" % (filepath, rna_path))
  318. xml2rna(xml_node, root_rna=value)
  319. def xml_file_write(context, filepath, rna_map, skip_typemap=None):
  320. file = open(filepath, "w", encoding="utf-8")
  321. fw = file.write
  322. fw("<bpy>\n")
  323. for rna_path, _xml_tag in rna_map:
  324. # xml_tag is ignored, we get this from the rna
  325. value = _get_context_val(context, rna_path)
  326. rna2xml(fw,
  327. root_rna=value,
  328. method='ATTR',
  329. root_ident=" ",
  330. ident_val=" ",
  331. skip_typemap=skip_typemap,
  332. )
  333. fw("</bpy>\n")
  334. file.close()