meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. meerk40t/__init__.py +1 -1
  2. meerk40t/balormk/balor_params.py +167 -167
  3. meerk40t/balormk/clone_loader.py +457 -457
  4. meerk40t/balormk/controller.py +1566 -1512
  5. meerk40t/balormk/cylindermod.py +64 -0
  6. meerk40t/balormk/device.py +966 -1959
  7. meerk40t/balormk/driver.py +778 -591
  8. meerk40t/balormk/galvo_commands.py +1194 -0
  9. meerk40t/balormk/gui/balorconfig.py +237 -111
  10. meerk40t/balormk/gui/balorcontroller.py +191 -184
  11. meerk40t/balormk/gui/baloroperationproperties.py +116 -115
  12. meerk40t/balormk/gui/corscene.py +845 -0
  13. meerk40t/balormk/gui/gui.py +179 -147
  14. meerk40t/balormk/livelightjob.py +466 -382
  15. meerk40t/balormk/mock_connection.py +131 -109
  16. meerk40t/balormk/plugin.py +133 -135
  17. meerk40t/balormk/usb_connection.py +306 -301
  18. meerk40t/camera/__init__.py +1 -1
  19. meerk40t/camera/camera.py +514 -397
  20. meerk40t/camera/gui/camerapanel.py +1241 -1095
  21. meerk40t/camera/gui/gui.py +58 -58
  22. meerk40t/camera/plugin.py +441 -399
  23. meerk40t/ch341/__init__.py +27 -27
  24. meerk40t/ch341/ch341device.py +628 -628
  25. meerk40t/ch341/libusb.py +595 -589
  26. meerk40t/ch341/mock.py +171 -171
  27. meerk40t/ch341/windriver.py +157 -157
  28. meerk40t/constants.py +13 -0
  29. meerk40t/core/__init__.py +1 -1
  30. meerk40t/core/bindalias.py +550 -539
  31. meerk40t/core/core.py +47 -47
  32. meerk40t/core/cutcode/cubiccut.py +73 -73
  33. meerk40t/core/cutcode/cutcode.py +315 -312
  34. meerk40t/core/cutcode/cutgroup.py +141 -137
  35. meerk40t/core/cutcode/cutobject.py +192 -185
  36. meerk40t/core/cutcode/dwellcut.py +37 -37
  37. meerk40t/core/cutcode/gotocut.py +29 -29
  38. meerk40t/core/cutcode/homecut.py +29 -29
  39. meerk40t/core/cutcode/inputcut.py +34 -34
  40. meerk40t/core/cutcode/linecut.py +33 -33
  41. meerk40t/core/cutcode/outputcut.py +34 -34
  42. meerk40t/core/cutcode/plotcut.py +335 -335
  43. meerk40t/core/cutcode/quadcut.py +61 -61
  44. meerk40t/core/cutcode/rastercut.py +168 -148
  45. meerk40t/core/cutcode/waitcut.py +34 -34
  46. meerk40t/core/cutplan.py +1843 -1316
  47. meerk40t/core/drivers.py +330 -329
  48. meerk40t/core/elements/align.py +801 -669
  49. meerk40t/core/elements/branches.py +1858 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4595 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4315 -3617
  54. meerk40t/core/elements/files.py +117 -64
  55. meerk40t/core/elements/geometry.py +473 -224
  56. meerk40t/core/elements/grid.py +467 -316
  57. meerk40t/core/elements/materials.py +158 -94
  58. meerk40t/core/elements/notes.py +50 -38
  59. meerk40t/core/elements/offset_clpr.py +934 -912
  60. meerk40t/core/elements/offset_mk.py +963 -955
  61. meerk40t/core/elements/penbox.py +339 -267
  62. meerk40t/core/elements/placements.py +300 -83
  63. meerk40t/core/elements/render.py +785 -687
  64. meerk40t/core/elements/shapes.py +2618 -2092
  65. meerk40t/core/elements/testcases.py +105 -0
  66. meerk40t/core/elements/trace.py +651 -563
  67. meerk40t/core/elements/tree_commands.py +415 -409
  68. meerk40t/core/elements/undo_redo.py +116 -58
  69. meerk40t/core/elements/wordlist.py +319 -200
  70. meerk40t/core/exceptions.py +9 -9
  71. meerk40t/core/laserjob.py +220 -220
  72. meerk40t/core/logging.py +63 -63
  73. meerk40t/core/node/blobnode.py +83 -86
  74. meerk40t/core/node/bootstrap.py +105 -103
  75. meerk40t/core/node/branch_elems.py +40 -31
  76. meerk40t/core/node/branch_ops.py +45 -38
  77. meerk40t/core/node/branch_regmark.py +48 -41
  78. meerk40t/core/node/cutnode.py +29 -32
  79. meerk40t/core/node/effect_hatch.py +375 -257
  80. meerk40t/core/node/effect_warp.py +398 -0
  81. meerk40t/core/node/effect_wobble.py +441 -309
  82. meerk40t/core/node/elem_ellipse.py +404 -309
  83. meerk40t/core/node/elem_image.py +1082 -801
  84. meerk40t/core/node/elem_line.py +358 -292
  85. meerk40t/core/node/elem_path.py +259 -201
  86. meerk40t/core/node/elem_point.py +129 -102
  87. meerk40t/core/node/elem_polyline.py +310 -246
  88. meerk40t/core/node/elem_rect.py +376 -286
  89. meerk40t/core/node/elem_text.py +445 -418
  90. meerk40t/core/node/filenode.py +59 -40
  91. meerk40t/core/node/groupnode.py +138 -74
  92. meerk40t/core/node/image_processed.py +777 -766
  93. meerk40t/core/node/image_raster.py +156 -113
  94. meerk40t/core/node/layernode.py +31 -31
  95. meerk40t/core/node/mixins.py +135 -107
  96. meerk40t/core/node/node.py +1427 -1304
  97. meerk40t/core/node/nutils.py +117 -114
  98. meerk40t/core/node/op_cut.py +463 -335
  99. meerk40t/core/node/op_dots.py +296 -251
  100. meerk40t/core/node/op_engrave.py +414 -311
  101. meerk40t/core/node/op_image.py +755 -369
  102. meerk40t/core/node/op_raster.py +787 -522
  103. meerk40t/core/node/place_current.py +37 -40
  104. meerk40t/core/node/place_point.py +329 -126
  105. meerk40t/core/node/refnode.py +58 -47
  106. meerk40t/core/node/rootnode.py +225 -219
  107. meerk40t/core/node/util_console.py +48 -48
  108. meerk40t/core/node/util_goto.py +84 -65
  109. meerk40t/core/node/util_home.py +61 -61
  110. meerk40t/core/node/util_input.py +102 -102
  111. meerk40t/core/node/util_output.py +102 -102
  112. meerk40t/core/node/util_wait.py +65 -65
  113. meerk40t/core/parameters.py +709 -707
  114. meerk40t/core/planner.py +875 -785
  115. meerk40t/core/plotplanner.py +656 -652
  116. meerk40t/core/space.py +120 -113
  117. meerk40t/core/spoolers.py +706 -705
  118. meerk40t/core/svg_io.py +1836 -1549
  119. meerk40t/core/treeop.py +534 -445
  120. meerk40t/core/undos.py +278 -124
  121. meerk40t/core/units.py +784 -680
  122. meerk40t/core/view.py +393 -322
  123. meerk40t/core/webhelp.py +62 -62
  124. meerk40t/core/wordlist.py +513 -504
  125. meerk40t/cylinder/cylinder.py +247 -0
  126. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  127. meerk40t/cylinder/gui/gui.py +24 -0
  128. meerk40t/device/__init__.py +1 -1
  129. meerk40t/device/basedevice.py +322 -123
  130. meerk40t/device/devicechoices.py +50 -0
  131. meerk40t/device/dummydevice.py +163 -128
  132. meerk40t/device/gui/defaultactions.py +618 -602
  133. meerk40t/device/gui/effectspanel.py +114 -0
  134. meerk40t/device/gui/formatterpanel.py +253 -290
  135. meerk40t/device/gui/warningpanel.py +337 -260
  136. meerk40t/device/mixins.py +13 -13
  137. meerk40t/dxf/__init__.py +1 -1
  138. meerk40t/dxf/dxf_io.py +766 -554
  139. meerk40t/dxf/plugin.py +47 -35
  140. meerk40t/external_plugins.py +79 -79
  141. meerk40t/external_plugins_build.py +28 -28
  142. meerk40t/extra/cag.py +112 -116
  143. meerk40t/extra/coolant.py +403 -0
  144. meerk40t/extra/encode_detect.py +204 -0
  145. meerk40t/extra/ezd.py +1165 -1165
  146. meerk40t/extra/hershey.py +834 -340
  147. meerk40t/extra/imageactions.py +322 -316
  148. meerk40t/extra/inkscape.py +628 -622
  149. meerk40t/extra/lbrn.py +424 -424
  150. meerk40t/extra/outerworld.py +283 -0
  151. meerk40t/extra/param_functions.py +1542 -1556
  152. meerk40t/extra/potrace.py +257 -253
  153. meerk40t/extra/serial_exchange.py +118 -0
  154. meerk40t/extra/updater.py +602 -453
  155. meerk40t/extra/vectrace.py +147 -146
  156. meerk40t/extra/winsleep.py +83 -83
  157. meerk40t/extra/xcs_reader.py +597 -0
  158. meerk40t/fill/fills.py +781 -335
  159. meerk40t/fill/patternfill.py +1061 -1061
  160. meerk40t/fill/patterns.py +614 -567
  161. meerk40t/grbl/control.py +87 -87
  162. meerk40t/grbl/controller.py +990 -903
  163. meerk40t/grbl/device.py +1084 -768
  164. meerk40t/grbl/driver.py +989 -771
  165. meerk40t/grbl/emulator.py +532 -497
  166. meerk40t/grbl/gcodejob.py +783 -767
  167. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  168. meerk40t/grbl/gui/grblcontroller.py +485 -271
  169. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  170. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  171. meerk40t/grbl/gui/gui.py +147 -116
  172. meerk40t/grbl/interpreter.py +44 -44
  173. meerk40t/grbl/loader.py +22 -22
  174. meerk40t/grbl/mock_connection.py +56 -56
  175. meerk40t/grbl/plugin.py +294 -264
  176. meerk40t/grbl/serial_connection.py +93 -88
  177. meerk40t/grbl/tcp_connection.py +81 -79
  178. meerk40t/grbl/ws_connection.py +112 -0
  179. meerk40t/gui/__init__.py +1 -1
  180. meerk40t/gui/about.py +2042 -296
  181. meerk40t/gui/alignment.py +1644 -1608
  182. meerk40t/gui/autoexec.py +199 -0
  183. meerk40t/gui/basicops.py +791 -670
  184. meerk40t/gui/bufferview.py +77 -71
  185. meerk40t/gui/busy.py +232 -133
  186. meerk40t/gui/choicepropertypanel.py +1662 -1469
  187. meerk40t/gui/consolepanel.py +706 -542
  188. meerk40t/gui/devicepanel.py +687 -581
  189. meerk40t/gui/dialogoptions.py +110 -107
  190. meerk40t/gui/executejob.py +316 -306
  191. meerk40t/gui/fonts.py +90 -90
  192. meerk40t/gui/functionwrapper.py +252 -0
  193. meerk40t/gui/gui_mixins.py +729 -0
  194. meerk40t/gui/guicolors.py +205 -182
  195. meerk40t/gui/help_assets/help_assets.py +218 -201
  196. meerk40t/gui/helper.py +154 -0
  197. meerk40t/gui/hersheymanager.py +1440 -846
  198. meerk40t/gui/icons.py +3422 -2747
  199. meerk40t/gui/imagesplitter.py +555 -508
  200. meerk40t/gui/keymap.py +354 -344
  201. meerk40t/gui/laserpanel.py +897 -806
  202. meerk40t/gui/laserrender.py +1470 -1232
  203. meerk40t/gui/lasertoolpanel.py +805 -793
  204. meerk40t/gui/magnetoptions.py +436 -0
  205. meerk40t/gui/materialmanager.py +2944 -0
  206. meerk40t/gui/materialtest.py +1722 -1694
  207. meerk40t/gui/mkdebug.py +646 -359
  208. meerk40t/gui/mwindow.py +163 -140
  209. meerk40t/gui/navigationpanels.py +2605 -2467
  210. meerk40t/gui/notes.py +143 -142
  211. meerk40t/gui/opassignment.py +414 -410
  212. meerk40t/gui/operation_info.py +310 -299
  213. meerk40t/gui/plugin.py +500 -328
  214. meerk40t/gui/position.py +714 -669
  215. meerk40t/gui/preferences.py +901 -650
  216. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  217. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  218. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  219. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  220. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  221. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  222. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  223. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  224. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  225. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  226. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  227. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  228. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  229. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  230. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  231. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  232. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  233. meerk40t/gui/propertypanels/textproperty.py +770 -755
  234. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  235. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  236. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  237. meerk40t/gui/ribbon.py +2471 -2210
  238. meerk40t/gui/scene/scene.py +1100 -1051
  239. meerk40t/gui/scene/sceneconst.py +22 -22
  240. meerk40t/gui/scene/scenepanel.py +439 -349
  241. meerk40t/gui/scene/scenespacewidget.py +365 -365
  242. meerk40t/gui/scene/widget.py +518 -505
  243. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  244. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  245. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  246. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  247. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  248. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  249. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  250. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  251. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  252. meerk40t/gui/scenewidgets/rectselectwidget.py +592 -346
  253. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  254. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  255. meerk40t/gui/scenewidgets/selectionwidget.py +2958 -2756
  256. meerk40t/gui/simpleui.py +362 -333
  257. meerk40t/gui/simulation.py +2451 -2094
  258. meerk40t/gui/snapoptions.py +208 -203
  259. meerk40t/gui/spoolerpanel.py +1227 -1180
  260. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  261. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  262. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  263. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  264. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  265. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  266. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  267. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  268. meerk40t/gui/themes.py +200 -78
  269. meerk40t/gui/tips.py +590 -0
  270. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  271. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  272. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  273. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  274. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  275. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  276. meerk40t/gui/toolwidgets/toolline.py +39 -144
  277. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  278. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  279. meerk40t/gui/toolwidgets/toolmeasure.py +163 -216
  280. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  281. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  282. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  283. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  284. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  285. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  286. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  287. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  288. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  289. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  290. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  291. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  292. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  293. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  294. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  295. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  296. meerk40t/gui/usbconnect.py +98 -91
  297. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  298. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  299. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  300. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  301. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  302. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  303. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  304. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  305. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  306. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  307. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  308. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  309. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  310. meerk40t/gui/wordlisteditor.py +985 -931
  311. meerk40t/gui/wxmeerk40t.py +1447 -1169
  312. meerk40t/gui/wxmmain.py +5644 -4112
  313. meerk40t/gui/wxmribbon.py +1591 -1076
  314. meerk40t/gui/wxmscene.py +1631 -1453
  315. meerk40t/gui/wxmtree.py +2416 -2089
  316. meerk40t/gui/wxutils.py +1769 -1099
  317. meerk40t/gui/zmatrix.py +102 -102
  318. meerk40t/image/__init__.py +1 -1
  319. meerk40t/image/dither.py +429 -0
  320. meerk40t/image/imagetools.py +2793 -2269
  321. meerk40t/internal_plugins.py +150 -130
  322. meerk40t/kernel/__init__.py +63 -12
  323. meerk40t/kernel/channel.py +259 -212
  324. meerk40t/kernel/context.py +538 -538
  325. meerk40t/kernel/exceptions.py +41 -41
  326. meerk40t/kernel/functions.py +463 -414
  327. meerk40t/kernel/jobs.py +100 -100
  328. meerk40t/kernel/kernel.py +3828 -3571
  329. meerk40t/kernel/lifecycles.py +71 -71
  330. meerk40t/kernel/module.py +49 -49
  331. meerk40t/kernel/service.py +147 -147
  332. meerk40t/kernel/settings.py +383 -343
  333. meerk40t/lihuiyu/controller.py +883 -876
  334. meerk40t/lihuiyu/device.py +1181 -1069
  335. meerk40t/lihuiyu/driver.py +1466 -1372
  336. meerk40t/lihuiyu/gui/gui.py +127 -106
  337. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  338. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  339. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  340. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  341. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  342. meerk40t/lihuiyu/interpreter.py +53 -53
  343. meerk40t/lihuiyu/laserspeed.py +450 -450
  344. meerk40t/lihuiyu/loader.py +90 -90
  345. meerk40t/lihuiyu/parser.py +404 -404
  346. meerk40t/lihuiyu/plugin.py +101 -102
  347. meerk40t/lihuiyu/tcp_connection.py +111 -109
  348. meerk40t/main.py +231 -165
  349. meerk40t/moshi/builder.py +788 -781
  350. meerk40t/moshi/controller.py +505 -499
  351. meerk40t/moshi/device.py +495 -442
  352. meerk40t/moshi/driver.py +862 -696
  353. meerk40t/moshi/gui/gui.py +78 -76
  354. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  355. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  356. meerk40t/moshi/plugin.py +43 -43
  357. meerk40t/network/console_server.py +140 -57
  358. meerk40t/network/kernelserver.py +10 -9
  359. meerk40t/network/tcp_server.py +142 -140
  360. meerk40t/network/udp_server.py +103 -77
  361. meerk40t/network/web_server.py +404 -0
  362. meerk40t/newly/controller.py +1158 -1144
  363. meerk40t/newly/device.py +874 -732
  364. meerk40t/newly/driver.py +540 -412
  365. meerk40t/newly/gui/gui.py +219 -188
  366. meerk40t/newly/gui/newlyconfig.py +116 -101
  367. meerk40t/newly/gui/newlycontroller.py +193 -186
  368. meerk40t/newly/gui/operationproperties.py +51 -51
  369. meerk40t/newly/mock_connection.py +82 -82
  370. meerk40t/newly/newly_params.py +56 -56
  371. meerk40t/newly/plugin.py +1214 -1246
  372. meerk40t/newly/usb_connection.py +322 -322
  373. meerk40t/rotary/gui/gui.py +52 -46
  374. meerk40t/rotary/gui/rotarysettings.py +240 -232
  375. meerk40t/rotary/rotary.py +202 -98
  376. meerk40t/ruida/control.py +291 -91
  377. meerk40t/ruida/controller.py +138 -1088
  378. meerk40t/ruida/device.py +676 -231
  379. meerk40t/ruida/driver.py +534 -472
  380. meerk40t/ruida/emulator.py +1494 -1491
  381. meerk40t/ruida/exceptions.py +4 -4
  382. meerk40t/ruida/gui/gui.py +71 -76
  383. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  384. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  385. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  386. meerk40t/ruida/loader.py +54 -52
  387. meerk40t/ruida/mock_connection.py +57 -109
  388. meerk40t/ruida/plugin.py +124 -87
  389. meerk40t/ruida/rdjob.py +2084 -945
  390. meerk40t/ruida/serial_connection.py +116 -0
  391. meerk40t/ruida/tcp_connection.py +146 -0
  392. meerk40t/ruida/udp_connection.py +73 -0
  393. meerk40t/svgelements.py +9671 -9669
  394. meerk40t/tools/driver_to_path.py +584 -579
  395. meerk40t/tools/geomstr.py +5583 -4680
  396. meerk40t/tools/jhfparser.py +357 -292
  397. meerk40t/tools/kerftest.py +904 -890
  398. meerk40t/tools/livinghinges.py +1168 -1033
  399. meerk40t/tools/pathtools.py +987 -949
  400. meerk40t/tools/pmatrix.py +234 -0
  401. meerk40t/tools/pointfinder.py +942 -942
  402. meerk40t/tools/polybool.py +941 -940
  403. meerk40t/tools/rasterplotter.py +1660 -547
  404. meerk40t/tools/shxparser.py +1047 -901
  405. meerk40t/tools/ttfparser.py +726 -446
  406. meerk40t/tools/zinglplotter.py +595 -593
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +21 -21
  408. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +150 -139
  409. meerk40t-0.9.7020.dist-info/RECORD +446 -0
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +1 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -1
  412. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +1 -1
  413. meerk40t/balormk/elementlightjob.py +0 -159
  414. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  415. test/bootstrap.py +0 -63
  416. test/test_cli.py +0 -12
  417. test/test_core_cutcode.py +0 -418
  418. test/test_core_elements.py +0 -144
  419. test/test_core_plotplanner.py +0 -397
  420. test/test_core_viewports.py +0 -312
  421. test/test_drivers_grbl.py +0 -108
  422. test/test_drivers_lihuiyu.py +0 -443
  423. test/test_drivers_newly.py +0 -113
  424. test/test_element_degenerate_points.py +0 -43
  425. test/test_elements_classify.py +0 -97
  426. test/test_elements_penbox.py +0 -22
  427. test/test_file_svg.py +0 -176
  428. test/test_fill.py +0 -155
  429. test/test_geomstr.py +0 -1523
  430. test/test_geomstr_nodes.py +0 -18
  431. test/test_imagetools_actualize.py +0 -306
  432. test/test_imagetools_wizard.py +0 -258
  433. test/test_kernel.py +0 -200
  434. test/test_laser_speeds.py +0 -3303
  435. test/test_length.py +0 -57
  436. test/test_lifecycle.py +0 -66
  437. test/test_operations.py +0 -251
  438. test/test_operations_hatch.py +0 -57
  439. test/test_ruida.py +0 -19
  440. test/test_spooler.py +0 -22
  441. test/test_tools_rasterplotter.py +0 -29
  442. test/test_wobble.py +0 -133
  443. test/test_zingl.py +0 -124
  444. {test → meerk40t/cylinder}/__init__.py +0 -0
  445. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  446. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
meerk40t/core/svg_io.py CHANGED
@@ -1,1549 +1,1836 @@
1
- """
2
- This extension governs SVG loading and saving, registering both the load and the save values for SVG.
3
- """
4
-
5
- import ast
6
- import gzip
7
- import math
8
- import os
9
- from base64 import b64encode
10
- from io import BytesIO
11
- from xml.etree.ElementTree import Element, ElementTree, ParseError, SubElement
12
-
13
- from meerk40t.core.exceptions import BadFileError
14
- from meerk40t.core.node.node import Fillrule, Linecap, Linejoin
15
-
16
- from ..svgelements import (
17
- SVG,
18
- SVG_ATTR_CENTER_X,
19
- SVG_ATTR_CENTER_Y,
20
- SVG_ATTR_DATA,
21
- SVG_ATTR_FILL,
22
- SVG_ATTR_FILL_OPACITY,
23
- SVG_ATTR_FONT_FAMILY,
24
- SVG_ATTR_FONT_SIZE,
25
- SVG_ATTR_FONT_STRETCH,
26
- SVG_ATTR_FONT_STYLE,
27
- SVG_ATTR_FONT_VARIANT,
28
- SVG_ATTR_HEIGHT,
29
- SVG_ATTR_ID,
30
- SVG_ATTR_POINTS,
31
- SVG_ATTR_RADIUS_X,
32
- SVG_ATTR_RADIUS_Y,
33
- SVG_ATTR_STROKE,
34
- SVG_ATTR_STROKE_OPACITY,
35
- SVG_ATTR_STROKE_WIDTH,
36
- SVG_ATTR_TAG,
37
- SVG_ATTR_TEXT_ALIGNMENT_BASELINE,
38
- SVG_ATTR_TEXT_ANCHOR,
39
- SVG_ATTR_TEXT_DOMINANT_BASELINE,
40
- SVG_ATTR_TRANSFORM,
41
- SVG_ATTR_VECTOR_EFFECT,
42
- SVG_ATTR_VERSION,
43
- SVG_ATTR_VIEWBOX,
44
- SVG_ATTR_WIDTH,
45
- SVG_ATTR_X,
46
- SVG_ATTR_X1,
47
- SVG_ATTR_X2,
48
- SVG_ATTR_XMLNS,
49
- SVG_ATTR_XMLNS_EV,
50
- SVG_ATTR_XMLNS_LINK,
51
- SVG_ATTR_Y,
52
- SVG_ATTR_Y1,
53
- SVG_ATTR_Y2,
54
- SVG_NAME_TAG,
55
- SVG_RULE_EVENODD,
56
- SVG_RULE_NONZERO,
57
- SVG_TAG_ELLIPSE,
58
- SVG_TAG_GROUP,
59
- SVG_TAG_IMAGE,
60
- SVG_TAG_LINE,
61
- SVG_TAG_PATH,
62
- SVG_TAG_POLYLINE,
63
- SVG_TAG_RECT,
64
- SVG_TAG_TEXT,
65
- SVG_VALUE_NON_SCALING_STROKE,
66
- SVG_VALUE_NONE,
67
- SVG_VALUE_VERSION,
68
- SVG_VALUE_XLINK,
69
- SVG_VALUE_XMLNS,
70
- SVG_VALUE_XMLNS_EV,
71
- Circle,
72
- Color,
73
- Ellipse,
74
- Group,
75
- Matrix,
76
- Path,
77
- Point,
78
- Polygon,
79
- Polyline,
80
- Rect,
81
- SimpleLine,
82
- SVGImage,
83
- SVGText,
84
- Use,
85
- )
86
- from .units import DEFAULT_PPI, NATIVE_UNIT_PER_INCH, Length
87
-
88
- SVG_ATTR_STROKE_JOIN = "stroke-linejoin"
89
- SVG_ATTR_STROKE_CAP = "stroke-linecap"
90
- SVG_ATTR_FILL_RULE = "fill-rule"
91
-
92
-
93
- def plugin(kernel, lifecycle=None):
94
- if lifecycle == "register":
95
- _ = kernel.translation
96
- choices = [
97
- {
98
- "attr": "svg_viewport_bed",
99
- "object": kernel.elements,
100
- "default": True,
101
- "type": bool,
102
- "label": _("SVG Viewport is Bed"),
103
- "tip": _(
104
- "SVG files can be saved without real physical units.\n"
105
- "This setting uses the SVG viewport dimensions to scale the rest of the elements in the file."
106
- ),
107
- "page": "Input/Output",
108
- "section": "Input",
109
- },
110
- ]
111
- kernel.register_choices("preferences", choices)
112
- # The order is relevant as both loaders support SVG
113
- # By definition the very first matching loader is used as a default
114
- # so that needs to be the full loader
115
- kernel.register("load/SVGLoader", SVGLoader)
116
- kernel.register("load/SVGPlainLoader", SVGLoaderPlain)
117
- kernel.register("save/SVGWriter", SVGWriter)
118
-
119
-
120
- MEERK40T_NAMESPACE = "https://github.com/meerk40t/meerk40t/wiki/Namespace"
121
- MEERK40T_XMLS_ID = "meerk40t"
122
-
123
-
124
- def capstr(linecap):
125
- """
126
- Given the mk enum values for linecap, returns the svg string.
127
- @param linecap:
128
- @return:
129
- """
130
- if linecap == Linecap.CAP_BUTT:
131
- return "butt"
132
- elif linecap == Linecap.CAP_SQUARE:
133
- return "square"
134
- else:
135
- return "round"
136
-
137
-
138
- def joinstr(linejoin):
139
- """
140
- Given the mk enum value for linejoin, returns the svg string.
141
-
142
- @param linejoin:
143
- @return:
144
- """
145
- if linejoin == Linejoin.JOIN_ARCS:
146
- return "arcs"
147
- elif linejoin == Linejoin.JOIN_BEVEL:
148
- return "bevel"
149
- elif linejoin == Linejoin.JOIN_MITER_CLIP:
150
- return "miter-clip"
151
- elif linejoin == Linejoin.JOIN_ROUND:
152
- return "round"
153
- else:
154
- return "miter"
155
-
156
-
157
- def rulestr(fillrule):
158
- """
159
- Given the mk enum value for fillrule, returns the svg string.
160
-
161
- @param fillrule:
162
- @return:
163
- """
164
- if fillrule == Fillrule.FILLRULE_EVENODD:
165
- return "evenodd"
166
- else:
167
- return "nonzero"
168
-
169
-
170
- class SVGWriter:
171
- @staticmethod
172
- def save_types():
173
- yield "Scalable Vector Graphics", "svg", "image/svg+xml", "default"
174
- yield "SVG-Plain (no extensions)", "svg", "image/svg+xml", "plain"
175
- yield "SVG-Compressed", "svgz", "image/svg+xml", "compressed"
176
-
177
- @staticmethod
178
- def save(context, f, version="default"):
179
- # print (f"Version was set to '{version}'")
180
- root = Element(SVG_NAME_TAG)
181
- root.set(SVG_ATTR_VERSION, SVG_VALUE_VERSION)
182
- root.set(SVG_ATTR_XMLNS, SVG_VALUE_XMLNS)
183
- root.set(SVG_ATTR_XMLNS_LINK, SVG_VALUE_XLINK)
184
- root.set(SVG_ATTR_XMLNS_EV, SVG_VALUE_XMLNS_EV)
185
- if version != "plain":
186
- root.set(
187
- "xmlns:" + MEERK40T_XMLS_ID,
188
- MEERK40T_NAMESPACE,
189
- )
190
- scene_width = Length(context.device.view.width)
191
- scene_height = Length(context.device.view.height)
192
- root.set(SVG_ATTR_WIDTH, scene_width.length_mm)
193
- root.set(SVG_ATTR_HEIGHT, scene_height.length_mm)
194
- viewbox = f"{0} {0} {int(float(scene_width))} {int(float(scene_height))}"
195
- root.set(SVG_ATTR_VIEWBOX, viewbox)
196
- elements = context.elements
197
- elements.validate_ids()
198
- # If we want to write labels then we need to establish the inkscape namespace
199
- has_labels = False
200
- for n in elements.elems_nodes():
201
- if hasattr(n, "label") and n.label is not None and n.label != "":
202
- has_labels = True
203
- break
204
- if n.type == "file":
205
- has_labels = True
206
- break
207
- if not has_labels:
208
- for n in elements.regmarks_nodes():
209
- if hasattr(n, "label") and n.label is not None and n.label != "":
210
- has_labels = True
211
- break
212
- if has_labels:
213
- root.set(
214
- "xmlns:inkscape",
215
- "http://www.inkscape.org/namespaces/inkscape",
216
- )
217
- if version != "plain":
218
- # If there is a note set then we save the note with the project.
219
- if elements.note is not None:
220
- subelement = SubElement(root, "note")
221
- subelement.set(SVG_TAG_TEXT, str(elements.note))
222
-
223
- SVGWriter._write_tree(root, elements._tree, version)
224
-
225
- SVGWriter._pretty_print(root)
226
- tree = ElementTree(root)
227
- if f.lower().endswith("svgz"):
228
- f = gzip.open(f, "wb")
229
- tree.write(f)
230
-
231
- @staticmethod
232
- def _write_tree(xml_tree, node_tree, version):
233
- # print (f"Write_tree with {version}")
234
- for node in node_tree.children:
235
- if version != "plain" and node.type == "branch ops":
236
- SVGWriter._write_operations(xml_tree, node, version)
237
- if node.type == "branch elems":
238
- SVGWriter._write_elements(xml_tree, node, version)
239
- elif node.type == "branch reg":
240
- SVGWriter._write_regmarks(xml_tree, node, version)
241
-
242
- @staticmethod
243
- def _write_elements(xml_tree, elem_tree, version):
244
- """
245
- Write the elements branch part of the tree to disk.
246
-
247
- @param xml_tree:
248
- @param elem_tree:
249
- @return:
250
- """
251
- for c in elem_tree.children:
252
- SVGWriter._write_element(xml_tree, c, version)
253
-
254
- @staticmethod
255
- def _write_element(xml_tree, c, version):
256
- def single_file_node():
257
- # do we have more than one element on the top level hierarchy?
258
- # If no then return True
259
- flag = True
260
- if len(c.children) > 1:
261
- flag = False
262
- return flag
263
-
264
- if c.type == "elem ellipse":
265
- subelement = SubElement(xml_tree, SVG_TAG_ELLIPSE)
266
- subelement.set(SVG_ATTR_CENTER_X, str(c.cx))
267
- subelement.set(SVG_ATTR_CENTER_Y, str(c.cy))
268
- subelement.set(SVG_ATTR_RADIUS_X, str(c.rx))
269
- subelement.set(SVG_ATTR_RADIUS_Y, str(c.ry))
270
- t = Matrix(c.matrix)
271
- if not t.is_identity():
272
- subelement.set(
273
- "transform",
274
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
275
- )
276
- elif c.type == "elem image":
277
- subelement = SubElement(xml_tree, SVG_TAG_IMAGE)
278
- stream = BytesIO()
279
- try:
280
- c.image.save(stream, format="PNG", dpi=(c.dpi, c.dpi))
281
- except OSError:
282
- # Edge condition if the original image was CMYK and never touched it can't encode to PNG
283
- c.image.convert("RGBA").save(stream, format="PNG", dpi=(c.dpi, c.dpi))
284
- subelement.set(
285
- "xlink:href",
286
- f"data:image/png;base64,{b64encode(stream.getvalue()).decode('utf8')}",
287
- )
288
- subelement.set(SVG_ATTR_X, "0")
289
- subelement.set(SVG_ATTR_Y, "0")
290
- subelement.set(SVG_ATTR_WIDTH, str(c.image.width))
291
- subelement.set(SVG_ATTR_HEIGHT, str(c.image.height))
292
- t = c.matrix
293
- if not t.is_identity():
294
- subelement.set(
295
- "transform",
296
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
297
- )
298
- elif c.type == "elem line":
299
- subelement = SubElement(xml_tree, SVG_TAG_LINE)
300
- subelement.set(SVG_ATTR_X1, str(c.x1))
301
- subelement.set(SVG_ATTR_Y1, str(c.y1))
302
- subelement.set(SVG_ATTR_X2, str(c.x2))
303
- subelement.set(SVG_ATTR_Y2, str(c.y2))
304
- t = c.matrix
305
- if not t.is_identity():
306
- subelement.set(
307
- "transform",
308
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
309
- )
310
- elif c.type == "elem path":
311
- element = c.geometry.as_path()
312
- subelement = SubElement(xml_tree, SVG_TAG_PATH)
313
- subelement.set(SVG_ATTR_DATA, element.d(transformed=False))
314
- t = c.matrix
315
- if not t.is_identity():
316
- subelement.set(
317
- "transform",
318
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
319
- )
320
- elif c.type == "elem point":
321
- subelement = SubElement(xml_tree, "element")
322
- t = c.matrix
323
- if not t.is_identity():
324
- subelement.set(
325
- "transform",
326
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
327
- )
328
- subelement.set("x", str(c.x))
329
- subelement.set("y", str(c.y))
330
- elif c.type == "elem polyline":
331
- subelement = SubElement(xml_tree, SVG_TAG_POLYLINE)
332
- points = list(c.geometry.as_points())
333
- subelement.set(
334
- SVG_ATTR_POINTS,
335
- " ".join([f"{e.real} {e.imag}" for e in points]),
336
- )
337
- t = c.matrix
338
- if not t.is_identity():
339
- subelement.set(
340
- "transform",
341
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
342
- )
343
- elif c.type == "elem rect":
344
- subelement = SubElement(xml_tree, SVG_TAG_RECT)
345
- subelement.set(SVG_ATTR_X, str(c.x))
346
- subelement.set(SVG_ATTR_Y, str(c.y))
347
- subelement.set(SVG_ATTR_RADIUS_X, str(c.rx))
348
- subelement.set(SVG_ATTR_RADIUS_Y, str(c.ry))
349
- subelement.set(SVG_ATTR_WIDTH, str(c.width))
350
- subelement.set(SVG_ATTR_HEIGHT, str(c.height))
351
- t = c.matrix
352
- if not t.is_identity():
353
- subelement.set(
354
- "transform",
355
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
356
- )
357
- elif c.type == "elem text":
358
- subelement = SubElement(xml_tree, SVG_TAG_TEXT)
359
- subelement.text = c.text
360
- t = c.matrix
361
- if not t.is_identity():
362
- subelement.set(
363
- SVG_ATTR_TRANSFORM,
364
- f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
365
- )
366
- # Font features are covered by the `font` value shorthand
367
- if c.font_family:
368
- subelement.set(SVG_ATTR_FONT_FAMILY, str(c.font_family))
369
- if c.font_style:
370
- subelement.set(SVG_ATTR_FONT_STYLE, str(c.font_style))
371
- if c.font_variant:
372
- subelement.set(SVG_ATTR_FONT_VARIANT, str(c.font_variant))
373
- if c.font_stretch:
374
- subelement.set(SVG_ATTR_FONT_STRETCH, str(c.font_stretch))
375
- if c.font_size:
376
- subelement.set(SVG_ATTR_FONT_SIZE, str(c.font_size))
377
- if c.line_height:
378
- subelement.set("line_height", str(c.line_height))
379
- if c.anchor:
380
- subelement.set(SVG_ATTR_TEXT_ANCHOR, str(c.anchor))
381
- if c.baseline:
382
- subelement.set(SVG_ATTR_TEXT_DOMINANT_BASELINE, str(c.baseline))
383
- decor = ""
384
- if c.underline:
385
- decor += " underline"
386
- if c.overline:
387
- decor += " overline"
388
- if c.strikethrough:
389
- decor += " line-through"
390
- decor = decor.strip()
391
- if decor:
392
- subelement.set("text-decoration", decor)
393
- element = c
394
- elif c.type == "group":
395
- # This is a structural group node of elements. Recurse call to write values.
396
- group_element = SubElement(xml_tree, SVG_TAG_GROUP)
397
- if hasattr(c, "label") and c.label is not None and c.label != "":
398
- group_element.set("inkscape:label", str(c.label))
399
- SVGWriter._write_elements(group_element, c, version)
400
- return
401
- elif c.type.startswith("effect"):
402
- # This is a structural group node of elements. Recurse call to write values.
403
- group_element = SubElement(xml_tree, SVG_TAG_GROUP)
404
- SVGWriter._write_custom(group_element, c)
405
- SVGWriter._write_elements(group_element, c, version)
406
- return
407
- elif c.type == "file":
408
- # This is a structural group node of elements. Recurse call to write values.
409
- # is this the only file node? If yes then no need to generate an additional group
410
- if single_file_node():
411
- SVGWriter._write_elements(xml_tree, c, version)
412
- else:
413
- group_element = SubElement(xml_tree, SVG_TAG_GROUP)
414
- if hasattr(c, "name") and c.name is not None and c.name != "":
415
- group_element.set("inkscape:label", str(c.name))
416
- SVGWriter._write_elements(group_element, c, version)
417
- return
418
- else:
419
- if version != "plain":
420
- # This is a non-standard element. Save custom.
421
- subelement = SubElement(xml_tree, "element")
422
- SVGWriter._write_custom(subelement, c)
423
- return
424
-
425
- ###############
426
- # GENERIC SAVING STANDARD ELEMENT
427
- ###############
428
- for key, value in c.__dict__.items():
429
- if (
430
- not key.startswith("_")
431
- and key
432
- not in (
433
- "settings",
434
- "attributes",
435
- "linecap",
436
- "linejoin",
437
- "fillrule",
438
- "stroke_width",
439
- )
440
- and value is not None
441
- and isinstance(value, (str, int, float, complex, list, tuple, dict))
442
- ):
443
- subelement.set(key, str(value))
444
-
445
- ###############
446
- # SAVE STROKE
447
- ###############
448
- if hasattr(c, "stroke_scaled"):
449
- if not c.stroke_scaled:
450
- subelement.set(SVG_ATTR_VECTOR_EFFECT, SVG_VALUE_NON_SCALING_STROKE)
451
-
452
- ###############
453
- # SAVE CAP/JOIN/FILL-RULE
454
- ###############
455
- if hasattr(c, "linecap"):
456
- subelement.set(SVG_ATTR_STROKE_CAP, capstr(c.linecap))
457
- if hasattr(c, "linejoin"):
458
- subelement.set(SVG_ATTR_STROKE_JOIN, joinstr(c.linejoin))
459
- if hasattr(c, "fillrule"):
460
- subelement.set(SVG_ATTR_FILL_RULE, rulestr(c.fillrule))
461
-
462
- ###############
463
- # SAVE LABEL
464
- ###############
465
- if hasattr(c, "label") and c.label is not None and c.label != "":
466
- subelement.set("inkscape:label", c.label)
467
-
468
- ###############
469
- # SAVE STROKE
470
- ###############
471
- if hasattr(c, "stroke"):
472
- stroke = c.stroke
473
- else:
474
- stroke = None
475
- if stroke is not None:
476
- stroke_opacity = stroke.opacity
477
- stroke = (
478
- str(abs(stroke))
479
- if stroke is not None and stroke.value is not None
480
- else SVG_VALUE_NONE
481
- )
482
- subelement.set(SVG_ATTR_STROKE, stroke)
483
- if stroke_opacity != 1.0 and stroke_opacity is not None:
484
- subelement.set(SVG_ATTR_STROKE_OPACITY, str(stroke_opacity))
485
- try:
486
- factor = 1.0
487
- try:
488
- if c.stroke_scaled:
489
- factor = c.stroke_factor
490
- except AttributeError:
491
- pass
492
- if c.matrix.determinant == 0:
493
- c_m_d = 1
494
- else:
495
- c_m_d = math.sqrt(abs(c.matrix.determinant))
496
- if c.stroke_width is not None:
497
- stroke_width = str(factor * c.stroke_width / c_m_d)
498
- subelement.set(SVG_ATTR_STROKE_WIDTH, stroke_width)
499
- except AttributeError:
500
- pass
501
-
502
- ###############
503
- # SAVE FILL
504
- ###############
505
- if hasattr(c, "fill"):
506
- fill = c.fill
507
- else:
508
- fill = None
509
- if fill is not None:
510
- fill_opacity = fill.opacity
511
- fill = (
512
- str(abs(fill))
513
- if fill is not None and fill.value is not None
514
- else SVG_VALUE_NONE
515
- )
516
- subelement.set(SVG_ATTR_FILL, fill)
517
- if fill_opacity != 1.0 and fill_opacity is not None:
518
- subelement.set(SVG_ATTR_FILL_OPACITY, str(fill_opacity))
519
- else:
520
- subelement.set(SVG_ATTR_FILL, SVG_VALUE_NONE)
521
- subelement.set(SVG_ATTR_ID, str(c.id))
522
-
523
- @staticmethod
524
- def _write_operations(xml_tree, op_tree, version):
525
- """
526
- Write the operations branch part of the tree to disk.
527
-
528
- @param xml_tree:
529
- @param op_tree:
530
- @return:
531
- """
532
- for c in op_tree.children:
533
- SVGWriter._write_operation(xml_tree, c, version)
534
-
535
- @staticmethod
536
- def _write_regmarks(xml_tree, reg_tree, version):
537
- if len(reg_tree.children):
538
- regmark = SubElement(xml_tree, SVG_TAG_GROUP)
539
- regmark.set("id", "regmarks")
540
- regmark.set("visibility", "hidden")
541
- SVGWriter._write_elements(regmark, reg_tree, version)
542
-
543
- @staticmethod
544
- def _write_operation(xml_tree, node, version):
545
- """
546
- Write an individual operation. This is any node directly under `branch ops`
547
-
548
- @param xml_tree:
549
- @param node:
550
- @return:
551
- """
552
- # All operations are groups.
553
- subelement = SubElement(xml_tree, SVG_TAG_GROUP)
554
- subelement.set("type", str(node.type))
555
-
556
- if node.label is not None:
557
- subelement.set("label", str(node.label))
558
-
559
- if node.lock is not None:
560
- subelement.set("lock", str(node.lock))
561
-
562
- try:
563
- for key, value in node.settings.items():
564
- if not key:
565
- # If key is None, do not save.
566
- continue
567
- if key.startswith("_"):
568
- continue
569
- if value is None:
570
- continue
571
- if key in ("references", "tag", "type"):
572
- # References key from previous loaded version (filter out, rebuild)
573
- continue
574
- subelement.set(key, str(value))
575
- except AttributeError:
576
- # Node does not have settings, write object dict
577
- for key, value in node.__dict__.items():
578
- if not key:
579
- # If key is None, do not save.
580
- continue
581
- if key.startswith("_"):
582
- continue
583
- if value is None:
584
- continue
585
- if key in (
586
- "references",
587
- "tag",
588
- "type",
589
- "draw",
590
- "stroke_width",
591
- "matrix",
592
- ):
593
- # References key from previous loaded version (filter out, rebuild)
594
- continue
595
- subelement.set(key, str(value))
596
-
597
- # Store current node reference values.
598
- SVGWriter._write_references(subelement, node)
599
- subelement.set(SVG_ATTR_ID, str(node.id))
600
-
601
- for c in node.children:
602
- # Recurse all non-ref nodes
603
- if c.type == "reference":
604
- continue
605
- SVGWriter._write_operation(subelement, c, version)
606
-
607
- @staticmethod
608
- def _write_references(subelement, node):
609
- contains = list()
610
- for c in node.children:
611
- if c.type == "reference":
612
- c = c.node # Contain direct reference not reference node reference.
613
- contains.append(c.id)
614
- if contains:
615
- subelement.set("references", " ".join(contains))
616
-
617
- @staticmethod
618
- def _write_custom(subelement, node):
619
- subelement.set("type", node.type)
620
- for key, value in node.__dict__.items():
621
- if not key:
622
- # If key is None, do not save.
623
- continue
624
- if key.startswith("_"):
625
- continue
626
- if value is None:
627
- continue
628
- if key in ("references", "tag", "type", "draw", "stroke_width", "matrix"):
629
- # References key from previous loaded version (filter out, rebuild)
630
- continue
631
- subelement.set(key, str(value))
632
- SVGWriter._write_references(subelement, node)
633
- subelement.set(SVG_ATTR_ID, str(node.id))
634
-
635
- @staticmethod
636
- def _pretty_print(current, parent=None, index=-1, depth=0):
637
- for i, node in enumerate(current):
638
- SVGWriter._pretty_print(node, current, i, depth + 1)
639
- if parent is not None:
640
- if index == 0:
641
- parent.text = "\n" + ("\t" * depth)
642
- else:
643
- parent[index - 1].tail = "\n" + ("\t" * depth)
644
- if index == len(parent) - 1:
645
- current.tail = "\n" + ("\t" * (depth - 1))
646
-
647
-
648
- class SVGProcessor:
649
- """
650
- SVGProcessor is the parser for svg objects. We employ svgelements to do the actual parsing of the file and convert
651
- the parsed objects into mk nodes, operations, elements, and regmarks.
652
-
653
- Special care is taken to load MK specific objects like `note` and `operations`
654
- """
655
-
656
- def __init__(self, elements, load_operations):
657
- self.elements = elements
658
-
659
- self.operation_list = list()
660
- self.element_list = list()
661
- self.regmark_list = list()
662
-
663
- self.reverse = False
664
- self.requires_classification = True
665
- self.operations_replaced = False
666
- self.pathname = None
667
- self.load_operations = load_operations
668
-
669
- # Setting this is bringing as much benefit as anticipated
670
- # Both the time to load the file (unexpectedly) and the time
671
- # for the first emphasis when all the nonpopulated bounding
672
- # boxes will be calculated are benefiting from this precalculation:
673
- # (All values as average over three consecutive loads)
674
- # | Load | First Select
675
- # File | Old | Precalc | Speedup | Old | Precalc | Speedup
676
- # Star Wars Calendar | 10,3 | 4,8 | 115% | 3,4 | 1,0 | 243%
677
- # Element Classific | 1,7 | 1,1 | 59% | 0,6 | 0,4 | 54%
678
- # Egyptian Bark | 72,1 | 43,9 | 64% | 34,6 | 20,1 | 72%
679
- self.precalc_bbox = True
680
-
681
- def process(self, svg, pathname):
682
- """
683
- Process sends the data to parse and deals with creating the file_node, setting the operations, classifying
684
- either directly from the data within the file or automatically.
685
-
686
- @param svg:
687
- @param pathname:
688
- @return:
689
- """
690
- self.pathname = pathname
691
-
692
- context_node = self.elements.elem_branch
693
- file_node = context_node.add(type="file", filepath=pathname)
694
- file_node.focus()
695
-
696
- self.parse(svg, file_node, self.element_list, branch="elements")
697
-
698
- if self.load_operations and self.operations_replaced:
699
- for child in list(self.elements.op_branch.children):
700
- if not hasattr(child, "_ref_load"):
701
- child.remove_all_children(fast=True, destroy=True)
702
- child.remove_node(fast=True, destroy=True)
703
- self.elements.undo.mark("op-replaced")
704
- for op in self.elements.op_branch.flat():
705
- try:
706
- refs = op._ref_load
707
- del op._ref_load
708
- except AttributeError:
709
- continue
710
- if refs is None:
711
- continue
712
-
713
- self.requires_classification = False
714
-
715
- for ref in refs.split(" "):
716
- for e in self.element_list:
717
- if e.id == ref:
718
- op.add_reference(e)
719
-
720
- if self.requires_classification and self.elements.classify_new:
721
- self.elements.classify(self.element_list)
722
-
723
- def check_for_mk_path_attributes(self, node, element):
724
- """
725
- Checks for some mk special parameters starting with mk. Especially mkparam, and uses this property to fill in
726
- the functional_parameter attribute for the node.
727
-
728
- @param node:
729
- @param element:
730
- @return:
731
- """
732
- for prop in element.values:
733
- lc = element.values.get(prop)
734
- if prop.startswith("mk"):
735
- # print (f"Property: {prop} = [{type(lc).__name__}] {lc}")
736
- if lc is not None:
737
- setattr(node, prop, lc)
738
- # This needs to be done as some node types are not based on Parameters
739
- # and hence would not convert the stringified tuple
740
- if prop == "mkparam" and hasattr(node, "functional_parameter"):
741
- try:
742
- value = ast.literal_eval(lc)
743
- node.functional_parameter = value
744
- except (ValueError, SyntaxError):
745
- pass
746
- elif prop == "mkbcparam":
747
- try:
748
- value = ast.literal_eval(lc)
749
- node.mkbcparam = value
750
- except (ValueError, SyntaxError):
751
- pass
752
-
753
- def check_for_fill_attributes(self, node, element):
754
- """
755
- Called for paths and poly lines. This checks for an attribute of `fill-rule` in the SVG and sets the MK equal.
756
-
757
- @param node:
758
- @param element:
759
- @return:
760
- """
761
- lc = element.values.get(SVG_ATTR_FILL_RULE)
762
- if lc is not None:
763
- nlc = Fillrule.FILLRULE_NONZERO
764
- lc = lc.lower()
765
- if lc == SVG_RULE_EVENODD:
766
- nlc = Fillrule.FILLRULE_EVENODD
767
- elif lc == SVG_RULE_NONZERO:
768
- nlc = Fillrule.FILLRULE_NONZERO
769
- node.fillrule = nlc
770
-
771
- def check_for_line_attributes(self, node, element):
772
- """
773
- Called for many element types. This checks for the stroke-cap and line-join attributes in the svgelements
774
- primitive and sets the node with the mk equal
775
-
776
- @param node:
777
- @param element:
778
- @return:
779
- """
780
- lc = element.values.get(SVG_ATTR_STROKE_CAP)
781
- if lc is not None:
782
- nlc = Linecap.CAP_ROUND
783
- if lc == "butt":
784
- nlc = Linecap.CAP_BUTT
785
- elif lc == "round":
786
- nlc = Linecap.CAP_ROUND
787
- elif lc == "square":
788
- nlc = Linecap.CAP_SQUARE
789
- node.linecap = nlc
790
- lj = element.values.get(SVG_ATTR_STROKE_JOIN)
791
- if lj is not None:
792
- nlj = Linejoin.JOIN_MITER
793
- if lj == "arcs":
794
- nlj = Linejoin.JOIN_ARCS
795
- elif lj == "bevel":
796
- nlj = Linejoin.JOIN_BEVEL
797
- elif lj == "miter":
798
- nlj = Linejoin.JOIN_MITER
799
- elif lj == "miter-clip":
800
- nlj = Linejoin.JOIN_MITER_CLIP
801
- elif lj == "round":
802
- nlj = Linejoin.JOIN_ROUND
803
- node.linejoin = nlj
804
-
805
- @staticmethod
806
- def is_dot(element):
807
- """
808
- Check for the degenerate shape dots. This could be a Path that consisting of a Move + Close, Move, or Move any
809
- path-segment that has a distance of 0 units. It could be a simple line to the same spot. It could be a polyline
810
- which has a single point.
811
-
812
- We avoid doing any calculations without checking the degenerate nature of the would-be dot first.
813
-
814
- @param element:
815
- @return:
816
- """
817
- if isinstance(element, Path):
818
- if len(element) > 2 or element.length(error=1, min_depth=1) > 0:
819
- return False, None
820
- return True, abs(element).first_point
821
- elif isinstance(element, SimpleLine):
822
- if element.length() == 0:
823
- return True, abs(Path(element)).first_point
824
- elif isinstance(element, (Polyline, Polygon)):
825
- if len(element) > 1:
826
- return False, None
827
- if element.length() == 0:
828
- return True, abs(Path(element)).first_point
829
- return False, None
830
-
831
- def get_tag_label(self, element):
832
- """
833
- Gets the tag label from the element. This is usually an inkscape label.
834
-
835
- Let's see whether we can get the label from an inkscape save
836
- We only want the 'label' attribute from the current tag, so
837
- we look at element.values["attributes"]
838
-
839
- @param element:
840
- @return:
841
- """
842
-
843
- if "attributes" in element.values:
844
- local_dict = element.values["attributes"]
845
- else:
846
- local_dict = element.values
847
- ink_tag = "inkscape:label"
848
- try:
849
- inkscape = element.values.get("inkscape")
850
- if inkscape is not None and inkscape != "":
851
- ink_tag = "{" + inkscape + "}label"
852
- except (AttributeError, KeyError):
853
- pass
854
- try:
855
- tag_label = local_dict.get(ink_tag)
856
- if tag_label == "":
857
- tag_label = None
858
- except (AttributeError, KeyError):
859
- # Label might simply be "label"
860
- pass
861
- if tag_label is None:
862
- tag_label = local_dict.get("label")
863
- return tag_label
864
-
865
- def _parse_text(self, element, ident, label, lock, context_node, e_list):
866
- """
867
- Parses an SVGText object, into an `elem text` node.
868
-
869
- @param element:
870
- @param ident:
871
- @param label:
872
- @param lock:
873
- @param context_node:
874
- @param e_list:
875
- @return:
876
- """
877
-
878
- if element.text is None:
879
- return
880
-
881
- decor = element.values.get("text-decoration", "").lower()
882
- node = context_node.add(
883
- id=ident,
884
- text=element.text,
885
- x=element.x,
886
- y=element.y,
887
- font=element.values.get("font"),
888
- anchor=element.values.get(SVG_ATTR_TEXT_ANCHOR),
889
- baseline=element.values.get(
890
- SVG_ATTR_TEXT_ALIGNMENT_BASELINE,
891
- element.values.get(SVG_ATTR_TEXT_DOMINANT_BASELINE, "baseline"),
892
- ),
893
- matrix=element.transform,
894
- fill=element.fill,
895
- stroke=element.stroke,
896
- stroke_width=element.stroke_width,
897
- stroke_scale=bool(
898
- SVG_VALUE_NON_SCALING_STROKE
899
- not in element.values.get(SVG_ATTR_VECTOR_EFFECT, "")
900
- ),
901
- underline="underline" in decor,
902
- strikethrough="line-through" in decor,
903
- overline="overline" in decor,
904
- texttransform=element.values.get("text-transform"),
905
- type="elem text",
906
- label=label,
907
- settings=element.values,
908
- )
909
- e_list.append(node)
910
-
911
- def _parse_path(self, element, ident, label, lock, context_node, e_list):
912
- """
913
- Parses an SVG Path object.
914
-
915
- There were a few versions of meerk40t where Path was used to store other save nodes. But, there is not
916
- enough information to reconstruct those elements.
917
-
918
- @param element:
919
- @param ident:
920
- @param label:
921
- @param lock:
922
- @param context_node:
923
- @param e_list:
924
- @return:
925
- """
926
- if len(element) < 0:
927
- return
928
-
929
- if element.values.get("type") == "elem polyline":
930
- # Type is polyline we should restore the node type if we have sufficient info to do so.
931
- pass
932
- if element.values.get("type") == "elem ellipse":
933
- # There is not enough info to reconstruct this.
934
- pass
935
- if element.values.get("type") == "elem rect":
936
- # There is not enough info to reconstruct this.
937
- pass
938
- if element.values.get("type") == "elem line":
939
- pass
940
- element.approximate_arcs_with_cubics()
941
- node = context_node.add(
942
- path=element, type="elem path", id=ident, label=label, lock=lock
943
- )
944
- self.check_for_line_attributes(node, element)
945
- self.check_for_fill_attributes(node, element)
946
- self.check_for_mk_path_attributes(node, element)
947
- e_list.append(node)
948
-
949
- def _parse_polyline(self, element, ident, label, lock, context_node, e_list):
950
- """
951
- Parses svg Polyline and Polygon objects into `elem polyline` nodes.
952
-
953
- @param element:
954
- @param ident:
955
- @param label:
956
- @param lock:
957
- @param context_node:
958
- @param e_list:
959
- @return:
960
- """
961
- if element.is_degenerate():
962
- return
963
- node = context_node.add(
964
- shape=element,
965
- type="elem polyline",
966
- id=ident,
967
- label=label,
968
- lock=lock,
969
- )
970
- self.check_for_line_attributes(node, element)
971
- self.check_for_fill_attributes(node, element)
972
- self.check_for_mk_path_attributes(node, element)
973
- if self.precalc_bbox:
974
- # bounds will be done here, paintbounds wont...
975
- if element.transform.is_identity():
976
- points = element.points
977
- else:
978
- points = list(
979
- map(element.transform.point_in_matrix_space, element.points)
980
- )
981
- xmin = min(p.x for p in points if p is not None)
982
- ymin = min(p.y for p in points if p is not None)
983
- xmax = max(p.x for p in points if p is not None)
984
- ymax = max(p.y for p in points if p is not None)
985
- node._bounds = [
986
- xmin,
987
- ymin,
988
- xmax,
989
- ymax,
990
- ]
991
- node._bounds_dirty = False
992
- node.revalidate_points()
993
- node._points_dirty = False
994
- e_list.append(node)
995
-
996
- def _parse_ellipse(self, element, ident, label, lock, context_node, e_list):
997
- """
998
- Parses the SVG Circle, and Ellipse nodes into `elem ellipse` nodes.
999
-
1000
- @param element:
1001
- @param ident:
1002
- @param label:
1003
- @param lock:
1004
- @param context_node:
1005
- @param e_list:
1006
- @return:
1007
- """
1008
- if element.is_degenerate():
1009
- return
1010
- node = context_node.add(
1011
- shape=element,
1012
- type="elem ellipse",
1013
- id=ident,
1014
- label=label,
1015
- lock=lock,
1016
- )
1017
- e_list.append(node)
1018
-
1019
- def _parse_rect(self, element, ident, label, lock, context_node, e_list):
1020
- """
1021
- Parse SVG Rect objects into `elem rect` objects.
1022
-
1023
- @param element:
1024
- @param ident:
1025
- @param label:
1026
- @param lock:
1027
- @param context_node:
1028
- @param e_list:
1029
- @return:
1030
- """
1031
- if element.is_degenerate():
1032
- return
1033
- node = context_node.add(
1034
- shape=element, type="elem rect", id=ident, label=label, lock=lock
1035
- )
1036
- self.check_for_line_attributes(node, element)
1037
- if self.precalc_bbox:
1038
- # bounds will be done here, paintbounds wont...
1039
- points = (
1040
- Point(element.x, element.y),
1041
- Point(element.x + element.width, element.y),
1042
- Point(element.x + element.width, element.y + element.height),
1043
- Point(element.x, element.y + element.height),
1044
- )
1045
- if not element.transform.is_identity():
1046
- points = list(map(element.transform.point_in_matrix_space, points))
1047
- xmin = min(p.x for p in points)
1048
- ymin = min(p.y for p in points)
1049
- xmax = max(p.x for p in points)
1050
- ymax = max(p.y for p in points)
1051
- node._bounds = [
1052
- xmin,
1053
- ymin,
1054
- xmax,
1055
- ymax,
1056
- ]
1057
- node._bounds_dirty = False
1058
- node.revalidate_points()
1059
- node._points_dirty = False
1060
- e_list.append(node)
1061
-
1062
- def _parse_line(self, element, ident, label, lock, context_node, e_list):
1063
- """
1064
- Parse SVG Line objects into `elem line`
1065
-
1066
- @param element:
1067
- @param ident:
1068
- @param label:
1069
- @param lock:
1070
- @param context_node:
1071
- @param e_list:
1072
- @return:
1073
- """
1074
- if element.is_degenerate():
1075
- return
1076
- node = context_node.add(
1077
- shape=element, type="elem line", id=ident, label=label, lock=lock
1078
- )
1079
- self.check_for_line_attributes(node, element)
1080
- if self.precalc_bbox:
1081
- # bounds will be done here, paintbounds wont...
1082
- points = (
1083
- Point(element.x1, element.y1),
1084
- Point(element.x2, element.y2),
1085
- )
1086
- if not element.transform.is_identity():
1087
- points = list(map(element.transform.point_in_matrix_space, points))
1088
- xmin = min(p.x for p in points)
1089
- ymin = min(p.y for p in points)
1090
- xmax = max(p.x for p in points)
1091
- ymax = max(p.y for p in points)
1092
- node._bounds = [
1093
- xmin,
1094
- ymin,
1095
- xmax,
1096
- ymax,
1097
- ]
1098
- node._bounds_dirty = False
1099
- node.revalidate_points()
1100
- node._points_dirty = False
1101
- e_list.append(node)
1102
-
1103
- def _parse_image(self, element, ident, label, lock, context_node, e_list):
1104
- """
1105
- Parse SVG Image objects into either `image raster` or `elem image` objects, potentially other classes.
1106
-
1107
- @param element:
1108
- @param ident:
1109
- @param label:
1110
- @param lock:
1111
- @param context_node:
1112
- @param e_list:
1113
- @return:
1114
- """
1115
- try:
1116
- element.load(os.path.dirname(self.pathname))
1117
- try:
1118
- operations = ast.literal_eval(element.values["operations"])
1119
- except (ValueError, SyntaxError, KeyError):
1120
- operations = None
1121
-
1122
- if element.image is not None:
1123
- try:
1124
- dpi = element.image.info["dpi"]
1125
- except KeyError:
1126
- dpi = None
1127
- _dpi = 500
1128
- if (
1129
- isinstance(dpi, tuple)
1130
- and len(dpi) >= 2
1131
- and dpi[0] != 0
1132
- and dpi[1] != 0
1133
- ):
1134
- _dpi = round((float(dpi[0]) + float(dpi[1])) / 2, 0)
1135
- _overscan = None
1136
- try:
1137
- _overscan = str(element.values.get("overscan"))
1138
- except (ValueError, TypeError):
1139
- pass
1140
- _direction = None
1141
- try:
1142
- _direction = int(element.values.get("direction"))
1143
- except (ValueError, TypeError):
1144
- pass
1145
- _invert = None
1146
- try:
1147
- _invert = bool(element.values.get("invert") == "True")
1148
- except (ValueError, TypeError):
1149
- pass
1150
- _dither = None
1151
- try:
1152
- _dither = bool(element.values.get("dither") == "True")
1153
- except (ValueError, TypeError):
1154
- pass
1155
- _dither_type = None
1156
- try:
1157
- _dither_type = element.values.get("dither_type")
1158
- except (ValueError, TypeError):
1159
- pass
1160
- _red = None
1161
- try:
1162
- _red = float(element.values.get("red"))
1163
- except (ValueError, TypeError):
1164
- pass
1165
- _green = None
1166
- try:
1167
- _green = float(element.values.get("green"))
1168
- except (ValueError, TypeError):
1169
- pass
1170
- _blue = None
1171
- try:
1172
- _blue = float(element.values.get("blue"))
1173
- except (ValueError, TypeError):
1174
- pass
1175
- _lightness = None
1176
- try:
1177
- _lightness = float(element.values.get("lightness"))
1178
- except (ValueError, TypeError):
1179
- pass
1180
- node = context_node.add(
1181
- image=element.image,
1182
- matrix=element.transform,
1183
- type="elem image",
1184
- id=ident,
1185
- overscan=_overscan,
1186
- direction=_direction,
1187
- dpi=_dpi,
1188
- invert=_invert,
1189
- dither=_dither,
1190
- dither_type=_dither_type,
1191
- red=_red,
1192
- green=_green,
1193
- blue=_blue,
1194
- lightness=_lightness,
1195
- label=label,
1196
- operations=operations,
1197
- lock=lock,
1198
- )
1199
- e_list.append(node)
1200
- except OSError:
1201
- pass
1202
-
1203
- def _parse_element(self, element, ident, label, lock, context_node, e_list):
1204
- """
1205
- SVGElement is type. Generic or unknown node type. These nodes do not have children, these are used in
1206
- meerk40t contain notes and operations. Element type="elem point", and other points will also load with
1207
- this code.
1208
-
1209
- @param element:
1210
- @param ident:
1211
- @param label:
1212
- @param lock:
1213
- @param context_node:
1214
- @param e_list:
1215
- @return:
1216
- """
1217
-
1218
- # Fix: we have mixed capitalisaton in full_ns and tag --> adjust
1219
- tag = element.values.get(SVG_ATTR_TAG).lower()
1220
- if tag is not None:
1221
- # We remove the name space.
1222
- full_ns = f"{{{MEERK40T_NAMESPACE.lower()}}}"
1223
- if full_ns in tag:
1224
- tag = tag.replace(full_ns, "")
1225
-
1226
- # Check if note-type
1227
- if tag == "note":
1228
- self.elements.note = element.values.get(SVG_TAG_TEXT)
1229
- self.elements.signal("note", self.pathname)
1230
- return
1231
-
1232
- node_type = element.values.get("type")
1233
- if node_type is None:
1234
- # Type is not given. Abort.
1235
- return
1236
-
1237
- if node_type == "op":
1238
- # Meerk40t 0.7.x fallback node types.
1239
- op_type = element.values.get("operation")
1240
- if op_type is None:
1241
- return
1242
- node_type = f"op {op_type.lower()}"
1243
- element.values["attributes"]["type"] = node_type
1244
-
1245
- node_id = element.values.get("id")
1246
-
1247
- # Get node dictionary.
1248
- try:
1249
- attrs = element.values["attributes"]
1250
- except KeyError:
1251
- attrs = element.values
1252
-
1253
- # If type exists in the dictionary, delete it to avoid double attribute issues.
1254
- try:
1255
- del attrs["type"]
1256
- except KeyError:
1257
- pass
1258
-
1259
- # Set dictionary types with proper classes.
1260
- if "lock" in attrs:
1261
- attrs["lock"] = lock
1262
- if "transform" in element.values:
1263
- # Uses chained transforms from primary context.
1264
- attrs["matrix"] = Matrix(element.values["transform"])
1265
- if "fill" in attrs:
1266
- attrs["fill"] = Color(attrs["fill"])
1267
- if "stroke" in attrs:
1268
- attrs["stroke"] = Color(attrs["stroke"])
1269
-
1270
- if tag == "operation":
1271
- # Operation type node.
1272
- if not self.load_operations:
1273
- # We don't do that.
1274
- return
1275
- if not self.operations_replaced:
1276
- self.operations_replaced = True
1277
-
1278
- try:
1279
- if node_type == "op hatch":
1280
- # Special fallback operation, op hatch is an op engrave with an effect hatch within it.
1281
- node_type = "op engrave"
1282
- op = self.elements.op_branch.add(type=node_type, **attrs)
1283
- effect = op.add(type="effect hatch", **attrs)
1284
- else:
1285
- op = self.elements.op_branch.add(type=node_type, **attrs)
1286
- op._ref_load = element.values.get("references")
1287
-
1288
- if op is None or not hasattr(op, "type") or op.type is None:
1289
- return
1290
- if hasattr(op, "validate"):
1291
- op.validate()
1292
-
1293
- op.id = node_id
1294
- self.operation_list.append(op)
1295
- except AttributeError:
1296
- # This operation is invalid.
1297
- return
1298
- except ValueError:
1299
- # This operation type failed to bootstrap.
1300
- return
1301
-
1302
- elif tag == "element":
1303
- # Check if SVGElement: element
1304
- if "settings" in attrs:
1305
- del attrs[
1306
- "settings"
1307
- ] # If settings was set, delete it, or it will mess things up
1308
- elem = context_node.add(type=node_type, **attrs)
1309
- try:
1310
- elem.validate()
1311
- except AttributeError:
1312
- pass
1313
- elem.id = node_id
1314
- e_list.append(elem)
1315
-
1316
- def parse(self, element, context_node, e_list, branch=None, uselabel=None):
1317
- """
1318
- Parse does the bulk of the work. Given an element, here the base case is an SVG itself, we parse such that
1319
- any groups will call and check all children recursively, updating the context_node, and passing each element
1320
- to this same function.
1321
-
1322
-
1323
- @param element: Element to parse.
1324
- @param context_node: Current context parent we're writing to.
1325
- @param e_list: elements list of all the nodes added by this function.
1326
- @param branch: Branch we are currently adding elements to.
1327
- @param uselabel:
1328
- @return:
1329
- """
1330
-
1331
- if element.values.get("visibility") == "hidden":
1332
- if branch != "regmarks":
1333
- self.parse(
1334
- element,
1335
- self.elements.reg_branch,
1336
- self.regmark_list,
1337
- branch="regmarks",
1338
- )
1339
- return
1340
-
1341
- ident = element.id
1342
-
1343
- if uselabel:
1344
- _label = uselabel
1345
- else:
1346
- _label = self.get_tag_label(element)
1347
-
1348
- _lock = None
1349
- try:
1350
- _lock = bool(element.values.get("lock") == "True")
1351
- except (ValueError, TypeError):
1352
- pass
1353
-
1354
- is_dot, dot_point = SVGProcessor.is_dot(element)
1355
- if is_dot:
1356
- node = context_node.add(
1357
- point=dot_point,
1358
- type="elem point",
1359
- matrix=Matrix(),
1360
- fill=element.fill,
1361
- stroke=element.stroke,
1362
- label=_label,
1363
- lock=_lock,
1364
- )
1365
- e_list.append(node)
1366
- elif isinstance(element, SVGText):
1367
- self._parse_text(element, ident, _label, _lock, context_node, e_list)
1368
- elif isinstance(element, Path):
1369
- self._parse_path(element, ident, _label, _lock, context_node, e_list)
1370
- elif isinstance(element, (Polygon, Polyline)):
1371
- self._parse_polyline(element, ident, _label, _lock, context_node, e_list)
1372
- elif isinstance(element, (Circle, Ellipse)):
1373
- self._parse_ellipse(element, ident, _label, _lock, context_node, e_list)
1374
- elif isinstance(element, Rect):
1375
- self._parse_rect(element, ident, _label, _lock, context_node, e_list)
1376
- elif isinstance(element, SimpleLine):
1377
- self._parse_line(element, ident, _label, _lock, context_node, e_list)
1378
- elif isinstance(element, SVGImage):
1379
- self._parse_image(element, ident, _label, _lock, context_node, e_list)
1380
- elif isinstance(element, SVG):
1381
- # SVG is type of group, it must be processed before Group. Nothing special is done with the type.
1382
- if self.reverse:
1383
- for child in reversed(element):
1384
- self.parse(child, context_node, e_list, branch=branch)
1385
- else:
1386
- for child in element:
1387
- self.parse(child, context_node, e_list, branch=branch)
1388
- elif isinstance(element, Group):
1389
- if branch != "regmarks" and (_label == "regmarks" or ident == "regmarks"):
1390
- # Recurse at same level within regmarks.
1391
- self.parse(
1392
- element,
1393
- self.elements.reg_branch,
1394
- self.regmark_list,
1395
- branch="regmarks",
1396
- )
1397
- return
1398
-
1399
- # Load group with specific group attributes (if needed)
1400
- e_dict = dict(element.values["attributes"])
1401
- e_type = e_dict.get("type", "group")
1402
- if branch != "operations" and (
1403
- e_type.startswith("op ")
1404
- or e_type.startswith("place ")
1405
- or e_type.startswith("util ")
1406
- ):
1407
- # This is an operations but we are not in operations context.
1408
- if not self.load_operations:
1409
- # We don't do that.
1410
- return
1411
- self.operations_replaced = True
1412
- self.parse(
1413
- element,
1414
- self.elements.op_branch,
1415
- self.operation_list,
1416
- branch="operations",
1417
- )
1418
- return
1419
- if "stroke" in e_dict:
1420
- e_dict["stroke"] = Color(e_dict.get("stroke"))
1421
- if "fill" in e_dict:
1422
- e_dict["fill"] = Color(e_dict.get("fill"))
1423
- for attr in ("type", "id", "label"):
1424
- if attr in e_dict:
1425
- del e_dict[attr]
1426
- context_node = context_node.add(
1427
- type=e_type, id=ident, label=_label, **e_dict
1428
- )
1429
- context_node._ref_load = element.values.get("references")
1430
- e_list.append(context_node)
1431
- if hasattr(context_node, "validate"):
1432
- context_node.validate()
1433
-
1434
- # recurse to children
1435
- if self.reverse:
1436
- for child in reversed(element):
1437
- self.parse(child, context_node, e_list, branch=branch)
1438
- else:
1439
- for child in element:
1440
- self.parse(child, context_node, e_list, branch=branch)
1441
- elif isinstance(element, Use):
1442
- # recurse to children, but do not subgroup elements.
1443
- # We still use the original label
1444
- if self.reverse:
1445
- for child in reversed(element):
1446
- self.parse(
1447
- child, context_node, e_list, branch=branch, uselabel=_label
1448
- )
1449
- else:
1450
- for child in element:
1451
- self.parse(
1452
- child, context_node, e_list, branch=branch, uselabel=_label
1453
- )
1454
- else:
1455
- self._parse_element(element, ident, _label, _lock, context_node, e_list)
1456
-
1457
-
1458
- class SVGLoader:
1459
- """
1460
- SVG loader - loading elements, regmarks and operations
1461
- """
1462
-
1463
- @staticmethod
1464
- def load_types():
1465
- yield "Scalable Vector Graphics", ("svg", "svgz"), "image/svg+xml"
1466
-
1467
- @staticmethod
1468
- def load(context, elements_service, pathname, **kwargs):
1469
- if "svg_ppi" in kwargs:
1470
- ppi = float(kwargs["svg_ppi"])
1471
- else:
1472
- ppi = DEFAULT_PPI
1473
- if ppi == 0:
1474
- ppi = DEFAULT_PPI
1475
- scale_factor = NATIVE_UNIT_PER_INCH / ppi
1476
- source = pathname
1477
- if pathname.lower().endswith("svgz"):
1478
- source = gzip.open(pathname, "rb")
1479
- try:
1480
- if context.elements.svg_viewport_bed:
1481
- width = Length(amount=context.device.view.unit_width).length_mm
1482
- height = Length(amount=context.device.view.unit_height).length_mm
1483
- else:
1484
- width = None
1485
- height = None
1486
- # The color attribute of SVG.parse decides which default color
1487
- # a stroke / fill will get if the attribute "currentColor" is
1488
- # set - we opt for "black"
1489
- svg = SVG.parse(
1490
- source=source,
1491
- reify=False,
1492
- width=width,
1493
- height=height,
1494
- ppi=ppi,
1495
- color="black",
1496
- transform=f"scale({scale_factor})",
1497
- )
1498
- except ParseError as e:
1499
- raise BadFileError(str(e)) from e
1500
- svg_processor = SVGProcessor(elements_service, True)
1501
- svg_processor.process(svg, pathname)
1502
- return True
1503
-
1504
-
1505
- class SVGLoaderPlain:
1506
- """
1507
- SVG loader but without loading the operations branch
1508
- """
1509
-
1510
- @staticmethod
1511
- def load_types():
1512
- yield "SVG (elements only)", ("svg", "svgz"), "image/svg+xml"
1513
-
1514
- @staticmethod
1515
- def load(context, elements_service, pathname, **kwargs):
1516
- if "svg_ppi" in kwargs:
1517
- ppi = float(kwargs["svg_ppi"])
1518
- else:
1519
- ppi = DEFAULT_PPI
1520
- if ppi == 0:
1521
- ppi = DEFAULT_PPI
1522
- scale_factor = NATIVE_UNIT_PER_INCH / ppi
1523
- source = pathname
1524
- if pathname.lower().endswith("svgz"):
1525
- source = gzip.open(pathname, "rb")
1526
- try:
1527
- if context.elements.svg_viewport_bed:
1528
- width = Length(amount=context.device.view.unit_width).length_mm
1529
- height = Length(amount=context.device.view.unit_height).length_mm
1530
- else:
1531
- width = None
1532
- height = None
1533
- # The color attribute of SVG.parse decides which default color
1534
- # a stroke / fill will get if the attribute "currentColor" is
1535
- # set - we opt for "black"
1536
- svg = SVG.parse(
1537
- source=source,
1538
- reify=False,
1539
- width=width,
1540
- height=height,
1541
- ppi=ppi,
1542
- color="black",
1543
- transform=f"scale({scale_factor})",
1544
- )
1545
- except ParseError as e:
1546
- raise BadFileError(str(e)) from e
1547
- svg_processor = SVGProcessor(elements_service, False)
1548
- svg_processor.process(svg, pathname)
1549
- return True
1
+ """
2
+ This extension governs SVG loading and saving, registering both the load and the save values for SVG.
3
+ """
4
+
5
+ import ast
6
+ import gzip
7
+ import math
8
+ import os
9
+ from base64 import b64encode
10
+ from io import BytesIO
11
+ from xml.etree.ElementTree import Element, ElementTree, ParseError, SubElement
12
+
13
+ from meerk40t.core.exceptions import BadFileError
14
+ from meerk40t.core.node.node import Fillrule, Linecap, Linejoin
15
+
16
+ from ..svgelements import (
17
+ SVG,
18
+ SVG_ATTR_CENTER_X,
19
+ SVG_ATTR_CENTER_Y,
20
+ SVG_ATTR_DATA,
21
+ SVG_ATTR_FILL,
22
+ SVG_ATTR_FILL_OPACITY,
23
+ SVG_ATTR_FONT_FAMILY,
24
+ SVG_ATTR_FONT_SIZE,
25
+ SVG_ATTR_FONT_STRETCH,
26
+ SVG_ATTR_FONT_STYLE,
27
+ SVG_ATTR_FONT_VARIANT,
28
+ SVG_ATTR_HEIGHT,
29
+ SVG_ATTR_ID,
30
+ SVG_ATTR_POINTS,
31
+ SVG_ATTR_RADIUS_X,
32
+ SVG_ATTR_RADIUS_Y,
33
+ SVG_ATTR_STROKE,
34
+ SVG_ATTR_STROKE_OPACITY,
35
+ SVG_ATTR_STROKE_WIDTH,
36
+ SVG_ATTR_TAG,
37
+ SVG_ATTR_TEXT_ALIGNMENT_BASELINE,
38
+ SVG_ATTR_TEXT_ANCHOR,
39
+ SVG_ATTR_TEXT_DOMINANT_BASELINE,
40
+ SVG_ATTR_TRANSFORM,
41
+ SVG_ATTR_VECTOR_EFFECT,
42
+ SVG_ATTR_VERSION,
43
+ SVG_ATTR_VIEWBOX,
44
+ SVG_ATTR_WIDTH,
45
+ SVG_ATTR_X,
46
+ SVG_ATTR_X1,
47
+ SVG_ATTR_X2,
48
+ SVG_ATTR_XMLNS,
49
+ SVG_ATTR_XMLNS_EV,
50
+ SVG_ATTR_XMLNS_LINK,
51
+ SVG_ATTR_Y,
52
+ SVG_ATTR_Y1,
53
+ SVG_ATTR_Y2,
54
+ SVG_NAME_TAG,
55
+ SVG_RULE_EVENODD,
56
+ SVG_RULE_NONZERO,
57
+ SVG_TAG_ELLIPSE,
58
+ SVG_TAG_GROUP,
59
+ SVG_TAG_IMAGE,
60
+ SVG_TAG_LINE,
61
+ SVG_TAG_PATH,
62
+ SVG_TAG_POLYLINE,
63
+ SVG_TAG_RECT,
64
+ SVG_TAG_TEXT,
65
+ SVG_VALUE_NON_SCALING_STROKE,
66
+ SVG_VALUE_NONE,
67
+ SVG_VALUE_VERSION,
68
+ SVG_VALUE_XLINK,
69
+ SVG_VALUE_XMLNS,
70
+ SVG_VALUE_XMLNS_EV,
71
+ Circle,
72
+ Color,
73
+ Ellipse,
74
+ Group,
75
+ Matrix,
76
+ Path,
77
+ Point,
78
+ Polygon,
79
+ Polyline,
80
+ Rect,
81
+ SimpleLine,
82
+ SVGImage,
83
+ SVGText,
84
+ Use,
85
+ )
86
+ from .units import DEFAULT_PPI, NATIVE_UNIT_PER_INCH, Length
87
+
88
+ SVG_ATTR_STROKE_JOIN = "stroke-linejoin"
89
+ SVG_ATTR_STROKE_CAP = "stroke-linecap"
90
+ SVG_ATTR_FILL_RULE = "fill-rule"
91
+ SVG_ATTR_STROKE_DASH = "stroke-dasharray"
92
+
93
+
94
+ def plugin(kernel, lifecycle=None):
95
+ if lifecycle == "register":
96
+ _ = kernel.translation
97
+ choices = [
98
+ {
99
+ "attr": "svg_viewport_bed",
100
+ "object": kernel.elements,
101
+ "default": True,
102
+ "type": bool,
103
+ "label": _("SVG Viewport is Bed"),
104
+ "tip": _(
105
+ "SVG files can be saved without real physical units.\n"
106
+ "This setting uses the SVG viewport dimensions to scale the rest of the elements in the file."
107
+ ),
108
+ "page": "Input/Output",
109
+ "section": "Input",
110
+ },
111
+ {
112
+ "attr": "load_hidden_to_regmarks",
113
+ "object": kernel.elements,
114
+ "default": True,
115
+ "type": bool,
116
+ "label": _("Load hidden objects to regmarks"),
117
+ "tip": _(
118
+ "Ticked: When loading a file invisible elements will be loaded to the regmarks branch."
119
+ )
120
+ + "\n"
121
+ + _(
122
+ "Unticked: Invisible elements will be loaded as regular elements and will be hidden."
123
+ ),
124
+ "page": "Input/Output",
125
+ "section": "Input",
126
+ },
127
+
128
+ ]
129
+ kernel.register_choices("preferences", choices)
130
+ # The order is relevant as both loaders support SVG
131
+ # By definition the very first matching loader is used as a default
132
+ # so that needs to be the full loader
133
+ kernel.register("load/SVGLoader", SVGLoader)
134
+ kernel.register("load/SVGPlainLoader", SVGLoaderPlain)
135
+ kernel.register("save/SVGWriter", SVGWriter)
136
+
137
+
138
+ MEERK40T_NAMESPACE = "https://github.com/meerk40t/meerk40t/wiki/Namespace"
139
+ MEERK40T_XMLS_ID = "meerk40t"
140
+
141
+
142
+ def capstr(linecap):
143
+ """
144
+ Given the mk enum values for linecap, returns the svg string.
145
+ @param linecap:
146
+ @return:
147
+ """
148
+ if linecap == Linecap.CAP_BUTT:
149
+ return "butt"
150
+ elif linecap == Linecap.CAP_SQUARE:
151
+ return "square"
152
+ else:
153
+ return "round"
154
+
155
+
156
+ def joinstr(linejoin):
157
+ """
158
+ Given the mk enum value for linejoin, returns the svg string.
159
+
160
+ @param linejoin:
161
+ @return:
162
+ """
163
+ if linejoin == Linejoin.JOIN_ARCS:
164
+ return "arcs"
165
+ elif linejoin == Linejoin.JOIN_BEVEL:
166
+ return "bevel"
167
+ elif linejoin == Linejoin.JOIN_MITER_CLIP:
168
+ return "miter-clip"
169
+ elif linejoin == Linejoin.JOIN_ROUND:
170
+ return "round"
171
+ else:
172
+ return "miter"
173
+
174
+
175
+ def rulestr(fillrule):
176
+ """
177
+ Given the mk enum value for fillrule, returns the svg string.
178
+
179
+ @param fillrule:
180
+ @return:
181
+ """
182
+ return "evenodd" if fillrule == Fillrule.FILLRULE_EVENODD else "nonzero"
183
+
184
+
185
+ class SVGWriter:
186
+ @staticmethod
187
+ def save_types():
188
+ yield "Scalable Vector Graphics", "svg", "image/svg+xml", "default"
189
+ yield "SVG-Plain (no extensions)", "svg", "image/svg+xml", "plain"
190
+ yield "SVG-Compressed", "svgz", "image/svg+xml", "compressed"
191
+
192
+ @staticmethod
193
+ def save(context, f, version="default"):
194
+ # print (f"Version was set to '{version}'")
195
+ root = Element(SVG_NAME_TAG)
196
+ root.set(SVG_ATTR_VERSION, SVG_VALUE_VERSION)
197
+ root.set(SVG_ATTR_XMLNS, SVG_VALUE_XMLNS)
198
+ root.set(SVG_ATTR_XMLNS_LINK, SVG_VALUE_XLINK)
199
+ root.set(SVG_ATTR_XMLNS_EV, SVG_VALUE_XMLNS_EV)
200
+ if version != "plain":
201
+ root.set(
202
+ "xmlns:" + MEERK40T_XMLS_ID,
203
+ MEERK40T_NAMESPACE,
204
+ )
205
+ scene_width = Length(context.device.view.width)
206
+ scene_height = Length(context.device.view.height)
207
+ root.set(SVG_ATTR_WIDTH, scene_width.length_mm)
208
+ root.set(SVG_ATTR_HEIGHT, scene_height.length_mm)
209
+ viewbox = f"{0} {0} {int(float(scene_width))} {int(float(scene_height))}"
210
+ root.set(SVG_ATTR_VIEWBOX, viewbox)
211
+ elements = context.elements
212
+ elements.validate_ids()
213
+ # If we want to write labels then we need to establish the inkscape namespace
214
+ has_labels = False
215
+ for n in elements.elems_nodes():
216
+ if hasattr(n, "label") and n.label is not None and n.label != "":
217
+ has_labels = True
218
+ break
219
+ if n.type == "file":
220
+ has_labels = True
221
+ break
222
+ if not has_labels:
223
+ for n in elements.regmarks_nodes():
224
+ if hasattr(n, "label") and n.label is not None and n.label != "":
225
+ has_labels = True
226
+ break
227
+ if has_labels:
228
+ root.set(
229
+ "xmlns:inkscape",
230
+ "http://www.inkscape.org/namespaces/inkscape",
231
+ )
232
+ if version != "plain":
233
+ # If there is a note set then we save the note with the project.
234
+ if elements.note is not None:
235
+ subelement = SubElement(root, "note")
236
+ subelement.set(SVG_TAG_TEXT, str(elements.note))
237
+ if elements.last_file_autoexec is not None:
238
+ subelement = SubElement(root, "autoexec")
239
+ subelement.set("autoexec", str(elements.last_file_autoexec))
240
+ subelement.set("autoexec-active", str(elements.last_file_autoexec_active))
241
+
242
+ SVGWriter._write_tree(root, elements._tree, version)
243
+
244
+ SVGWriter._pretty_print(root)
245
+ tree = ElementTree(root)
246
+ if f.lower().endswith("svgz"):
247
+ f = gzip.open(f, "wb")
248
+ tree.write(f)
249
+
250
+ @staticmethod
251
+ def _write_tree(xml_tree, node_tree, version):
252
+ # print (f"Write_tree with {version}")
253
+ for node in node_tree.children:
254
+ if version != "plain" and node.type == "branch ops":
255
+ SVGWriter._write_operations(xml_tree, node, version)
256
+ if node.type == "branch elems":
257
+ SVGWriter._write_elements(xml_tree, node, version)
258
+ elif node.type == "branch reg":
259
+ SVGWriter._write_regmarks(xml_tree, node, version)
260
+
261
+ @staticmethod
262
+ def _write_elements(xml_tree, elem_tree, version):
263
+ """
264
+ Write the elements branch part of the tree to disk.
265
+
266
+ @param xml_tree:
267
+ @param elem_tree:
268
+ @return:
269
+ """
270
+ for c in elem_tree.children:
271
+ SVGWriter._write_element(xml_tree, c, version)
272
+
273
+ @staticmethod
274
+ def _write_element(xml_tree, c, version):
275
+ def single_file_node():
276
+ # do we have more than one element on the top level hierarchy?
277
+ # If no then return True
278
+ flag = True
279
+ if len(c.children) > 1:
280
+ flag = False
281
+ return flag
282
+
283
+ if c.type == "elem ellipse":
284
+ subelement = SubElement(xml_tree, SVG_TAG_ELLIPSE)
285
+ subelement.set(SVG_ATTR_CENTER_X, str(c.cx))
286
+ subelement.set(SVG_ATTR_CENTER_Y, str(c.cy))
287
+ subelement.set(SVG_ATTR_RADIUS_X, str(c.rx))
288
+ subelement.set(SVG_ATTR_RADIUS_Y, str(c.ry))
289
+ t = Matrix(c.matrix)
290
+ if not t.is_identity():
291
+ subelement.set(
292
+ "transform",
293
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
294
+ )
295
+ elif c.type in ("elem image", "image raster"):
296
+ subelement = SubElement(xml_tree, SVG_TAG_IMAGE)
297
+ stream = BytesIO()
298
+ try:
299
+ c.image.save(stream, format="PNG", dpi=(c.dpi, c.dpi))
300
+ except OSError:
301
+ # Edge condition if the original image was CMYK and never touched it can't encode to PNG
302
+ c.image.convert("RGBA").save(stream, format="PNG", dpi=(c.dpi, c.dpi))
303
+ subelement.set(
304
+ "xlink:href",
305
+ f"data:image/png;base64,{b64encode(stream.getvalue()).decode('utf8')}",
306
+ )
307
+ ref = c.keyhole_reference
308
+ if ref is not None:
309
+ subelement.set("keyhole_reference", ref)
310
+ subelement.set(SVG_ATTR_X, "0")
311
+ subelement.set(SVG_ATTR_Y, "0")
312
+ subelement.set(SVG_ATTR_WIDTH, str(c.image.width))
313
+ subelement.set(SVG_ATTR_HEIGHT, str(c.image.height))
314
+ t = c.matrix
315
+ if not t.is_identity():
316
+ subelement.set(
317
+ "transform",
318
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
319
+ )
320
+ elif c.type == "elem line":
321
+ subelement = SubElement(xml_tree, SVG_TAG_LINE)
322
+ subelement.set(SVG_ATTR_X1, str(c.x1))
323
+ subelement.set(SVG_ATTR_Y1, str(c.y1))
324
+ subelement.set(SVG_ATTR_X2, str(c.x2))
325
+ subelement.set(SVG_ATTR_Y2, str(c.y2))
326
+ t = c.matrix
327
+ if not t.is_identity():
328
+ subelement.set(
329
+ "transform",
330
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
331
+ )
332
+ elif c.type == "elem path":
333
+ element = c.geometry.as_path()
334
+ subelement = SubElement(xml_tree, SVG_TAG_PATH)
335
+ subelement.set(SVG_ATTR_DATA, element.d(transformed=False))
336
+ t = c.matrix
337
+ if not t.is_identity():
338
+ subelement.set(
339
+ "transform",
340
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
341
+ )
342
+ elif c.type == "elem point":
343
+ subelement = SubElement(xml_tree, "element")
344
+ t = c.matrix
345
+ if not t.is_identity():
346
+ subelement.set(
347
+ "transform",
348
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
349
+ )
350
+ subelement.set("x", str(c.x))
351
+ subelement.set("y", str(c.y))
352
+ elif c.type == "elem polyline":
353
+ subelement = SubElement(xml_tree, SVG_TAG_POLYLINE)
354
+ points = list(c.geometry.as_points())
355
+ subelement.set(
356
+ SVG_ATTR_POINTS,
357
+ " ".join([f"{e.real} {e.imag}" for e in points]),
358
+ )
359
+ t = c.matrix
360
+ if not t.is_identity():
361
+ subelement.set(
362
+ "transform",
363
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
364
+ )
365
+ elif c.type == "elem rect":
366
+ subelement = SubElement(xml_tree, SVG_TAG_RECT)
367
+ subelement.set(SVG_ATTR_X, str(c.x))
368
+ subelement.set(SVG_ATTR_Y, str(c.y))
369
+ subelement.set(SVG_ATTR_RADIUS_X, str(c.rx))
370
+ subelement.set(SVG_ATTR_RADIUS_Y, str(c.ry))
371
+ subelement.set(SVG_ATTR_WIDTH, str(c.width))
372
+ subelement.set(SVG_ATTR_HEIGHT, str(c.height))
373
+ t = c.matrix
374
+ if not t.is_identity():
375
+ subelement.set(
376
+ "transform",
377
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
378
+ )
379
+ elif c.type == "elem text":
380
+ subelement = SubElement(xml_tree, SVG_TAG_TEXT)
381
+ subelement.text = c.text
382
+ t = c.matrix
383
+ if not t.is_identity():
384
+ subelement.set(
385
+ SVG_ATTR_TRANSFORM,
386
+ f"matrix({t.a}, {t.b}, {t.c}, {t.d}, {t.e}, {t.f})",
387
+ )
388
+ # Font features are covered by the `font` value shorthand
389
+ if c.font_family:
390
+ subelement.set(SVG_ATTR_FONT_FAMILY, str(c.font_family))
391
+ if c.font_style:
392
+ subelement.set(SVG_ATTR_FONT_STYLE, str(c.font_style))
393
+ if c.font_variant:
394
+ subelement.set(SVG_ATTR_FONT_VARIANT, str(c.font_variant))
395
+ if c.font_stretch:
396
+ subelement.set(SVG_ATTR_FONT_STRETCH, str(c.font_stretch))
397
+ if c.font_size:
398
+ subelement.set(SVG_ATTR_FONT_SIZE, str(c.font_size))
399
+ if c.line_height:
400
+ subelement.set("line_height", str(c.line_height))
401
+ if c.anchor:
402
+ subelement.set(SVG_ATTR_TEXT_ANCHOR, str(c.anchor))
403
+ if c.baseline:
404
+ subelement.set(SVG_ATTR_TEXT_DOMINANT_BASELINE, str(c.baseline))
405
+ decor = ""
406
+ if c.underline:
407
+ decor += " underline"
408
+ if c.overline:
409
+ decor += " overline"
410
+ if c.strikethrough:
411
+ decor += " line-through"
412
+ decor = decor.strip()
413
+ if decor:
414
+ subelement.set("text-decoration", decor)
415
+ elif c.type == "group":
416
+ # This is a structural group node of elements. Recurse call to write values.
417
+ group_element = SubElement(xml_tree, SVG_TAG_GROUP)
418
+ if hasattr(c, "label") and c.label is not None and c.label != "":
419
+ group_element.set("inkscape:label", str(c.label))
420
+ if hasattr(c, "label_display") and c.label_display is not None:
421
+ group_element.set("label_display", str(c.label_display))
422
+ SVGWriter._write_elements(group_element, c, version)
423
+ return
424
+ elif c.type.startswith("effect"):
425
+ # This is a structural group node of elements. Recurse call to write values.
426
+ group_element = SubElement(xml_tree, SVG_TAG_GROUP)
427
+ SVGWriter._write_custom(group_element, c)
428
+ SVGWriter._write_elements(group_element, c, version)
429
+ return
430
+ elif c.type == "file":
431
+ # This is a structural group node of elements. Recurse call to write values.
432
+ # is this the only file node? If yes then no need to generate an additional group
433
+ if single_file_node():
434
+ SVGWriter._write_elements(xml_tree, c, version)
435
+ else:
436
+ group_element = SubElement(xml_tree, SVG_TAG_GROUP)
437
+ if hasattr(c, "name") and c.name is not None and c.name != "":
438
+ group_element.set("inkscape:label", str(c.name))
439
+ SVGWriter._write_elements(group_element, c, version)
440
+ return
441
+ else:
442
+ if version == "plain":
443
+ # Plain does not save custom.
444
+ return
445
+ # This is a non-standard element. Save custom.
446
+ subelement = SubElement(xml_tree, "element")
447
+ SVGWriter._write_custom(subelement, c)
448
+ return
449
+
450
+ ###############
451
+ # GENERIC SAVING STANDARD ELEMENT
452
+ ###############
453
+ for key, value in c.__dict__.items():
454
+ if (
455
+ not key.startswith("_")
456
+ and key
457
+ not in (
458
+ "settings",
459
+ "attributes",
460
+ "linecap",
461
+ "linejoin",
462
+ "fillrule",
463
+ "stroke_width",
464
+ "stroke_dash",
465
+ )
466
+ and value is not None
467
+ and isinstance(value, (str, int, float, complex, list, tuple, dict))
468
+ ):
469
+ subelement.set(key, str(value))
470
+
471
+ # ###########################
472
+ # # SAVE SVG STROKE-SCALING
473
+ # ###########################
474
+ # if hasattr(c, "stroke_scaled"):
475
+ # if not c.stroke_scaled:
476
+ # subelement.set(SVG_ATTR_VECTOR_EFFECT, SVG_VALUE_NON_SCALING_STROKE)
477
+
478
+ ###############
479
+ # SAVE CAP/JOIN/FILL-RULE
480
+ ###############
481
+ if hasattr(c, "linecap"):
482
+ subelement.set(SVG_ATTR_STROKE_CAP, capstr(c.linecap))
483
+ if hasattr(c, "linejoin"):
484
+ subelement.set(SVG_ATTR_STROKE_JOIN, joinstr(c.linejoin))
485
+ if hasattr(c, "fillrule"):
486
+ subelement.set(SVG_ATTR_FILL_RULE, rulestr(c.fillrule))
487
+ if hasattr(c, "stroke_dash") and c.stroke_dash:
488
+ subelement.set(SVG_ATTR_STROKE_DASH, c.stroke_dash)
489
+
490
+ ###############
491
+ # SAVE LABEL
492
+ ###############
493
+ if hasattr(c, "label") and c.label is not None and c.label != "":
494
+ subelement.set("inkscape:label", c.label)
495
+
496
+ ###############
497
+ # SAVE STROKE
498
+ ###############
499
+ stroke = c.stroke if hasattr(c, "stroke") else None
500
+ if stroke is not None:
501
+ stroke_opacity = stroke.opacity
502
+ stroke = (
503
+ str(abs(stroke))
504
+ if stroke is not None and stroke.value is not None
505
+ else SVG_VALUE_NONE
506
+ )
507
+ subelement.set(SVG_ATTR_STROKE, stroke)
508
+ if stroke_opacity != 1.0 and stroke_opacity is not None:
509
+ subelement.set(SVG_ATTR_STROKE_OPACITY, str(stroke_opacity))
510
+ try:
511
+ factor = 1.0
512
+ try:
513
+ if c.stroke_scaled:
514
+ factor = c.stroke_factor
515
+ except AttributeError:
516
+ pass
517
+ if c.matrix.determinant == 0:
518
+ c_m_d = 1
519
+ else:
520
+ c_m_d = math.sqrt(abs(c.matrix.determinant))
521
+ if c.stroke_width is not None:
522
+ stroke_width = str(factor * c.stroke_width / c_m_d)
523
+ subelement.set(SVG_ATTR_STROKE_WIDTH, stroke_width)
524
+ except AttributeError:
525
+ pass
526
+
527
+ ###############
528
+ # SAVE FILL
529
+ ###############
530
+ fill = c.fill if hasattr(c, "fill") else None
531
+ if fill is not None:
532
+ fill_opacity = fill.opacity
533
+ fill = (
534
+ str(abs(fill))
535
+ if fill is not None and fill.value is not None
536
+ else SVG_VALUE_NONE
537
+ )
538
+ subelement.set(SVG_ATTR_FILL, fill)
539
+ if fill_opacity != 1.0 and fill_opacity is not None:
540
+ subelement.set(SVG_ATTR_FILL_OPACITY, str(fill_opacity))
541
+ else:
542
+ subelement.set(SVG_ATTR_FILL, SVG_VALUE_NONE)
543
+
544
+ if hasattr(c, "hidden") and c.hidden:
545
+ subelement.set("visibility", "hidden")
546
+
547
+ subelement.set(SVG_ATTR_ID, str(c.id))
548
+ if hasattr(c, "bounds"):
549
+ bb = c.bounds
550
+ bbstr = f"{bb[0]}, {bb[1]}, {bb[2]}, {bb[3]}"
551
+ subelement.set("bounds", bbstr)
552
+ if hasattr(c, "paint_bounds"):
553
+ bb = c.paint_bounds
554
+ bbstr = f"{bb[0]}, {bb[1]}, {bb[2]}, {bb[3]}"
555
+ subelement.set("paint_bounds", bbstr)
556
+
557
+ @staticmethod
558
+ def _write_operations(xml_tree, op_tree, version):
559
+ """
560
+ Write the operations branch part of the tree to disk.
561
+
562
+ @param xml_tree:
563
+ @param op_tree:
564
+ @return:
565
+ """
566
+ for c in op_tree.children:
567
+ SVGWriter._write_operation(xml_tree, c, version)
568
+
569
+ @staticmethod
570
+ def _write_regmarks(xml_tree, reg_tree, version):
571
+ if len(reg_tree.children):
572
+ regmark = SubElement(xml_tree, SVG_TAG_GROUP)
573
+ regmark.set("id", "regmarks")
574
+ regmark.set("visibility", "hidden")
575
+ SVGWriter._write_elements(regmark, reg_tree, version)
576
+
577
+ @staticmethod
578
+ def _write_operation(xml_tree, node, version):
579
+ """
580
+ Write an individual operation. This is any node directly under `branch ops`
581
+
582
+ @param xml_tree:
583
+ @param node:
584
+ @return:
585
+ """
586
+ # All operations are groups.
587
+ subelement = SubElement(xml_tree, SVG_TAG_GROUP)
588
+ subelement.set("type", str(node.type))
589
+
590
+ if node.label is not None:
591
+ subelement.set("label", str(node.label))
592
+
593
+ # We might end up with items in settings that have an unwanted equivalent in the node.dict
594
+ # as the settings instance is read and initiated on svg load...
595
+ for key, value in node.__dict__.items():
596
+ if not key or key.startswith("_"):
597
+ continue
598
+ if key in (
599
+ "references",
600
+ "tag",
601
+ "type",
602
+ "draw",
603
+ "stroke_width",
604
+ "matrix",
605
+ "settings",
606
+ ):
607
+ continue
608
+ if hasattr(node, "settings"):
609
+ if key in node.settings:
610
+ settings_value = node.settings[key]
611
+ if settings_value != value:
612
+ # print (f"Needed to fix {key}: node-value: {value}, settings-value: {settings_value}")
613
+ node.settings[key] = value
614
+
615
+ saved_attributes = []
616
+ if hasattr(node, "settings"):
617
+ try:
618
+ for key, value in node.settings.items():
619
+ saved_attributes.append(key)
620
+ if not key:
621
+ # If key is None, do not save.
622
+ continue
623
+ if key.startswith("_"):
624
+ continue
625
+ if value is None:
626
+ continue
627
+ if key in ("references", "tag", "type"):
628
+ # References key from previous loaded version (filter out, rebuild)
629
+ continue
630
+ subelement.set(key, str(value))
631
+ except AttributeError:
632
+ pass
633
+ # Node does not have settings, write object dict
634
+ for key, value in node.__dict__.items():
635
+ if not key or key.startswith("_") or key in saved_attributes or value is None:
636
+ continue
637
+ if key in (
638
+ "references",
639
+ "tag",
640
+ "type",
641
+ "draw",
642
+ "stroke_width",
643
+ "matrix",
644
+ "settings",
645
+ ):
646
+ # References key from previous loaded version (filter out, rebuild)
647
+ continue
648
+ subelement.set(key, str(value))
649
+
650
+ # Store current node reference values.
651
+ SVGWriter._write_references(subelement, node)
652
+ subelement.set(SVG_ATTR_ID, str(node.id))
653
+
654
+ for c in node.children:
655
+ # Recurse all non-ref nodes
656
+ if c.type == "reference":
657
+ continue
658
+ SVGWriter._write_operation(subelement, c, version)
659
+
660
+ @staticmethod
661
+ def _write_references(subelement, node):
662
+ contains = list()
663
+ for c in node.children:
664
+ if c.type == "reference":
665
+ c = c.node # Contain direct reference not reference node reference.
666
+ contains.append(c.id)
667
+ if contains:
668
+ subelement.set("references", " ".join(contains))
669
+
670
+ @staticmethod
671
+ def _write_custom(subelement, node):
672
+ subelement.set("type", node.type)
673
+ for key, value in node.__dict__.items():
674
+ if not key:
675
+ # If key is None, do not save.
676
+ continue
677
+ if key.startswith("_"):
678
+ continue
679
+ if value is None:
680
+ continue
681
+ if key in ("references", "tag", "type", "draw", "stroke_width", "matrix"):
682
+ # References key from previous loaded version (filter out, rebuild)
683
+ continue
684
+ subelement.set(key, str(value))
685
+ SVGWriter._write_references(subelement, node)
686
+ subelement.set(SVG_ATTR_ID, str(node.id))
687
+
688
+ @staticmethod
689
+ def _pretty_print(current, parent=None, index=-1, depth=0):
690
+ for i, node in enumerate(current):
691
+ SVGWriter._pretty_print(node, current, i, depth + 1)
692
+ if parent is not None:
693
+ if index == 0:
694
+ parent.text = "\n" + ("\t" * depth)
695
+ else:
696
+ parent[index - 1].tail = "\n" + ("\t" * depth)
697
+ if index == len(parent) - 1:
698
+ current.tail = "\n" + ("\t" * (depth - 1))
699
+
700
+
701
+ class SVGProcessor:
702
+ """
703
+ SVGProcessor is the parser for svg objects. We employ svgelements to do the actual parsing of the file and convert
704
+ the parsed objects into mk nodes, operations, elements, and regmarks.
705
+
706
+ Special care is taken to load MK specific objects like `note` and `operations`
707
+ """
708
+
709
+ def __init__(self, elements, load_operations, load_hidden_to_regmarks = True, reuse_operations=True):
710
+ self.elements = elements
711
+
712
+ self.operation_list = list()
713
+ self.element_list = list()
714
+ self.regmark_list = list()
715
+ self.load_hidden_to_regmarks = load_hidden_to_regmarks
716
+
717
+ self.reverse = False
718
+ self.requires_classification = True
719
+ self.operations_generated = False
720
+ self.pathname = None
721
+ self.load_operations = load_operations
722
+ self.reuse_operations = reuse_operations
723
+ self.mk_params = list(
724
+ self.elements.kernel.lookup_all("registered_mk_svg_parameters")
725
+ )
726
+ # Append barcode from external plugin
727
+ self.mk_params.append("mkbcparam")
728
+
729
+ # Setting this is bringing as much benefit as anticipated
730
+ # Both the time to load the file (unexpectedly) and the time
731
+ # for the first emphasis when all the nonpopulated bounding
732
+ # boxes will be calculated are benefiting from this precalculation:
733
+ # (All values as average over three consecutive loads)
734
+ # | Load | First Select
735
+ # File | Old | Precalc | Speedup | Old | Precalc | Speedup
736
+ # Star Wars Calendar | 10,3 | 4,8 | 115% | 3,4 | 1,0 | 243%
737
+ # Element Classific | 1,7 | 1,1 | 59% | 0,6 | 0,4 | 54%
738
+ # Egyptian Bark | 72,1 | 43,9 | 64% | 34,6 | 20,1 | 72%
739
+ self.precalc_bbox = True
740
+
741
+ def process(self, svg, pathname):
742
+ """
743
+ Process sends the data to parse and deals with creating the file_node, setting the operations, classifying
744
+ either directly from the data within the file or automatically.
745
+
746
+ @param svg:
747
+ @param pathname:
748
+ @return:
749
+ """
750
+ retain_op_list = [
751
+ child
752
+ for child in list(self.elements.ops())
753
+ if child._children is not None and len(child._children) > 0
754
+ ]
755
+ self.pathname = pathname
756
+
757
+ context_node = self.elements.elem_branch
758
+ file_node = context_node.add(type="file", filepath=pathname)
759
+ file_node.focus()
760
+
761
+ self.parse(svg, file_node, self.element_list, branch="elements")
762
+
763
+ if self.load_operations and self.operations_generated:
764
+ # print ("Will replace all operations...")
765
+ self.requires_classification = False
766
+ for child in list(self.elements.op_branch.children):
767
+ if child in retain_op_list:
768
+ continue
769
+ if not hasattr(child, "_ref_load"):
770
+ child.remove_all_children(fast=True, destroy=True)
771
+ child.remove_node(fast=True, destroy=True)
772
+ # Hint for translate check: _("File loaded")
773
+ self.elements.undo.mark("File loaded")
774
+ for op in self.elements.op_branch.flat():
775
+ try:
776
+ refs = op._ref_load
777
+ del op._ref_load
778
+ except AttributeError:
779
+ continue
780
+ if refs is None:
781
+ continue
782
+
783
+ for ref in refs.split(" "):
784
+ for e in self.element_list:
785
+ if e.id == ref:
786
+ op.add_reference(e)
787
+
788
+ if self.requires_classification and self.elements.classify_new:
789
+ self.elements.classify(self.element_list)
790
+
791
+ def check_for_bound_information(self, node, element):
792
+ # Do we have existing boundary information?
793
+ if "bounds" not in element.values:
794
+ return False
795
+ bbstr = element.values["bounds"]
796
+ if not bbstr:
797
+ return False
798
+ bb_info = bbstr.split(",")
799
+ if len(bb_info) == 4:
800
+ bbox = [0, 0, 0, 0]
801
+ try:
802
+ for idx in range(4):
803
+ val = float(bb_info[idx])
804
+ bbox[idx] = val
805
+ except ValueError:
806
+ return False
807
+ node._bounds = list(bbox)
808
+ node._bounds_dirty = False
809
+ if "paint_bounds" in element.values:
810
+ try:
811
+ bbstr = element.values["paint_bounds"]
812
+ bb_info = bbstr.split(",")
813
+ if len(bb_info) == 4:
814
+ for idx in range(4):
815
+ val = float(bb_info[idx])
816
+ bbox[idx] = val
817
+ except Exception:
818
+ # Whatever it was, we don't continue...
819
+ pass
820
+ node._paint_bounds = list(bbox)
821
+ node._paint_bounds_dirty = False
822
+ return True
823
+
824
+ def check_for_mk_path_attributes(self, node, element):
825
+ """
826
+ Checks for some mk special parameters starting with mk. Especially mkparam, and uses this property to fill in
827
+ the functional_parameter attribute for the node.
828
+
829
+ @param node:
830
+ @param element:
831
+ @return:
832
+ """
833
+ for prop in element.values:
834
+ lc = element.values.get(prop)
835
+ if prop.startswith("mk"):
836
+ # print (f"Property: {prop} = [{type(lc).__name__}] {lc}")
837
+ if lc is not None:
838
+ setattr(node, prop, lc)
839
+ # This needs to be done as some node types are not based on Parameters
840
+ # and hence would not convert the stringified tuple
841
+ if prop == "mkparam" and hasattr(node, "functional_parameter"):
842
+ try:
843
+ value = ast.literal_eval(lc)
844
+ node.functional_parameter = value
845
+ except (ValueError, SyntaxError):
846
+ pass
847
+ elif prop in self.mk_params:
848
+ try:
849
+ value = ast.literal_eval(lc)
850
+ setattr(node, prop, value)
851
+ except (ValueError, SyntaxError):
852
+ pass
853
+
854
+ def check_for_label_display(self, node, element):
855
+ """
856
+ Called for all nodes to check whether the label_display needs to be set
857
+ @param node:
858
+ @param element:
859
+ @return:
860
+ """
861
+ lc = element.values.get("label_display")
862
+ if lc is not None and hasattr(node, "label_display"):
863
+ d_val = bool(ast.literal_eval(lc))
864
+ node.label_display = d_val
865
+
866
+ def check_for_fill_attributes(self, node, element):
867
+ """
868
+ Called for paths and poly lines. This checks for an attribute of `fill-rule` in the SVG and sets the MK equal.
869
+
870
+ @param node:
871
+ @param element:
872
+ @return:
873
+ """
874
+ lc = element.values.get(SVG_ATTR_FILL_RULE)
875
+ # SVG default is nonzero
876
+ nlc = Fillrule.FILLRULE_NONZERO
877
+ if lc is not None:
878
+ lc = lc.lower()
879
+ if lc == SVG_RULE_EVENODD:
880
+ nlc = Fillrule.FILLRULE_EVENODD
881
+ elif lc == SVG_RULE_NONZERO:
882
+ nlc = Fillrule.FILLRULE_NONZERO
883
+ node.fillrule = nlc
884
+
885
+ def check_for_line_attributes(self, node, element):
886
+ """
887
+ Called for many element types. This checks for the stroke-cap and line-join attributes in the svgelements
888
+ primitive and sets the node with the mk equal
889
+
890
+ @param node:
891
+ @param element:
892
+ @return:
893
+ """
894
+ lc = element.values.get(SVG_ATTR_STROKE_CAP)
895
+ # SVG default is butt
896
+ nlc = Linecap.CAP_BUTT
897
+ if lc is not None:
898
+ if lc == "butt":
899
+ nlc = Linecap.CAP_BUTT
900
+ elif lc == "round":
901
+ nlc = Linecap.CAP_ROUND
902
+ elif lc == "square":
903
+ nlc = Linecap.CAP_SQUARE
904
+ node.linecap = nlc
905
+ lj = element.values.get(SVG_ATTR_STROKE_JOIN)
906
+ # SVG default is miter
907
+ nlj = Linejoin.JOIN_MITER
908
+ if lj is not None:
909
+ nlj = Linejoin.JOIN_MITER
910
+ if lj == "arcs":
911
+ nlj = Linejoin.JOIN_ARCS
912
+ elif lj == "bevel":
913
+ nlj = Linejoin.JOIN_BEVEL
914
+ elif lj == "miter":
915
+ nlj = Linejoin.JOIN_MITER
916
+ elif lj == "miter-clip":
917
+ nlj = Linejoin.JOIN_MITER_CLIP
918
+ elif lj == "round":
919
+ nlj = Linejoin.JOIN_ROUND
920
+ node.linejoin = nlj
921
+ lj = element.values.get(SVG_ATTR_STROKE_DASH)
922
+ if lj not in (None, "", "none"):
923
+ node.stroke_dash = lj
924
+
925
+ @staticmethod
926
+ def is_dot(element):
927
+ """
928
+ Check for the degenerate shape dots. This could be a Path that consisting of a Move + Close, Move, or Move any
929
+ path-segment that has a distance of 0 units. It could be a simple line to the same spot. It could be a polyline
930
+ which has a single point.
931
+
932
+ We avoid doing any calculations without checking the degenerate nature of the would-be dot first.
933
+
934
+ @param element:
935
+ @return:
936
+ """
937
+ if isinstance(element, Path):
938
+ if len(element) > 2 or element.length(error=1, min_depth=1) > 0:
939
+ return False, None
940
+ return True, abs(element).first_point
941
+ elif isinstance(element, SimpleLine):
942
+ if element.length() == 0:
943
+ return True, abs(Path(element)).first_point
944
+ elif isinstance(element, (Polyline, Polygon)):
945
+ if len(element) > 1:
946
+ return False, None
947
+ if element.length() == 0:
948
+ return True, abs(Path(element)).first_point
949
+ return False, None
950
+
951
+ def get_tag_label(self, element):
952
+ """
953
+ Gets the tag label from the element. This is usually an inkscape label.
954
+
955
+ Let's see whether we can get the label from an inkscape save
956
+ We only want the 'label' attribute from the current tag, so
957
+ we look at element.values["attributes"]
958
+
959
+ @param element:
960
+ @return:
961
+ """
962
+
963
+ if "attributes" in element.values:
964
+ local_dict = element.values["attributes"]
965
+ else:
966
+ local_dict = element.values
967
+ if local_dict is None:
968
+ return None
969
+ ink_tag = "inkscape:label"
970
+ inkscape = element.values.get("inkscape")
971
+ if inkscape:
972
+ ink_tag = "{" + inkscape + "}label"
973
+ tag_label = local_dict.get(ink_tag)
974
+ if tag_label:
975
+ return tag_label
976
+ return local_dict.get("label")
977
+
978
+ def _parse_text(self, element, ident, label, lock, context_node, e_list, set_hidden):
979
+ """
980
+ Parses an SVGText object, into an `elem text` node.
981
+
982
+ @param element:
983
+ @param ident:
984
+ @param label:
985
+ @param lock:
986
+ @param context_node:
987
+ @param e_list:
988
+ @return:
989
+ """
990
+
991
+ if element.text is None:
992
+ return
993
+
994
+ decor = element.values.get("text-decoration", "").lower()
995
+ node = context_node.add(
996
+ id=ident,
997
+ text=element.text,
998
+ x=element.x,
999
+ y=element.y,
1000
+ font=element.values.get("font"),
1001
+ anchor=element.values.get(SVG_ATTR_TEXT_ANCHOR),
1002
+ baseline=element.values.get(
1003
+ SVG_ATTR_TEXT_ALIGNMENT_BASELINE,
1004
+ element.values.get(SVG_ATTR_TEXT_DOMINANT_BASELINE, "baseline"),
1005
+ ),
1006
+ matrix=element.transform,
1007
+ fill=element.fill,
1008
+ stroke=element.stroke,
1009
+ stroke_width=element.stroke_width,
1010
+ stroke_scale=bool(
1011
+ SVG_VALUE_NON_SCALING_STROKE
1012
+ not in element.values.get(SVG_ATTR_VECTOR_EFFECT, "")
1013
+ ),
1014
+ underline="underline" in decor,
1015
+ strikethrough="line-through" in decor,
1016
+ overline="overline" in decor,
1017
+ texttransform=element.values.get("text-transform"),
1018
+ type="elem text",
1019
+ label=label,
1020
+ settings=element.values,
1021
+ hidden=set_hidden,
1022
+ )
1023
+ self.check_for_label_display(node, element)
1024
+ self.check_for_bound_information(node, element)
1025
+ e_list.append(node)
1026
+
1027
+ def _parse_path(self, element, ident, label, lock, context_node, e_list, set_hidden):
1028
+ """
1029
+ Parses an SVG Path object.
1030
+
1031
+ There were a few versions of meerk40t where Path was used to store other save nodes. But, there is not
1032
+ enough information to reconstruct those elements.
1033
+
1034
+ @param element:
1035
+ @param ident:
1036
+ @param label:
1037
+ @param lock:
1038
+ @param context_node:
1039
+ @param e_list:
1040
+ @return:
1041
+ """
1042
+ if len(element) < 0:
1043
+ return
1044
+
1045
+ if element.values.get("type") == "elem polyline":
1046
+ # Type is polyline we should restore the node type if we have sufficient info to do so.
1047
+ pass
1048
+ if element.values.get("type") == "elem ellipse":
1049
+ # There is not enough info to reconstruct this.
1050
+ pass
1051
+ if element.values.get("type") == "elem rect":
1052
+ # There is not enough info to reconstruct this.
1053
+ pass
1054
+ if element.values.get("type") == "elem line":
1055
+ pass
1056
+ element.approximate_arcs_with_cubics()
1057
+ node = context_node.add(
1058
+ path=element, type="elem path", id=ident, label=label, lock=lock, hidden=set_hidden
1059
+ )
1060
+ self.check_for_label_display(node, element)
1061
+ self.check_for_line_attributes(node, element)
1062
+ self.check_for_fill_attributes(node, element)
1063
+ self.check_for_mk_path_attributes(node, element)
1064
+ self.check_for_bound_information(node, element)
1065
+ e_list.append(node)
1066
+
1067
+ def _parse_polyline(self, element, ident, label, lock, context_node, e_list, set_hidden):
1068
+ """
1069
+ Parses svg Polyline and Polygon objects into `elem polyline` nodes.
1070
+
1071
+ @param element:
1072
+ @param ident:
1073
+ @param label:
1074
+ @param lock:
1075
+ @param context_node:
1076
+ @param e_list:
1077
+ @return:
1078
+ """
1079
+ if element.is_degenerate():
1080
+ return
1081
+ node = context_node.add(
1082
+ shape=element,
1083
+ type="elem polyline",
1084
+ id=ident,
1085
+ label=label,
1086
+ lock=lock,
1087
+ hidden=set_hidden,
1088
+ )
1089
+ self.check_for_label_display(node, element)
1090
+ self.check_for_line_attributes(node, element)
1091
+ self.check_for_fill_attributes(node, element)
1092
+ self.check_for_mk_path_attributes(node, element)
1093
+ if not self.check_for_bound_information(node, element) and self.precalc_bbox:
1094
+ # bounds will be done here, paintbounds won't...
1095
+ if element.transform.is_identity():
1096
+ points = element.points
1097
+ else:
1098
+ points = list(
1099
+ map(element.transform.point_in_matrix_space, element.points)
1100
+ )
1101
+ xmin = min(p.x for p in points if p is not None)
1102
+ ymin = min(p.y for p in points if p is not None)
1103
+ xmax = max(p.x for p in points if p is not None)
1104
+ ymax = max(p.y for p in points if p is not None)
1105
+ node._bounds = [
1106
+ xmin,
1107
+ ymin,
1108
+ xmax,
1109
+ ymax,
1110
+ ]
1111
+ node._bounds_dirty = False
1112
+ node.revalidate_points()
1113
+ node._points_dirty = False
1114
+ e_list.append(node)
1115
+
1116
+ def _parse_ellipse(self, element, ident, label, lock, context_node, e_list, set_hidden):
1117
+ """
1118
+ Parses the SVG Circle, and Ellipse nodes into `elem ellipse` nodes.
1119
+
1120
+ @param element:
1121
+ @param ident:
1122
+ @param label:
1123
+ @param lock:
1124
+ @param context_node:
1125
+ @param e_list:
1126
+ @return:
1127
+ """
1128
+ if element.is_degenerate():
1129
+ return
1130
+ node = context_node.add(
1131
+ shape=element,
1132
+ type="elem ellipse",
1133
+ id=ident,
1134
+ label=label,
1135
+ lock=lock,
1136
+ hidden=set_hidden,
1137
+ )
1138
+ self.check_for_label_display(node, element)
1139
+ self.check_for_line_attributes(node, element)
1140
+ self.check_for_mk_path_attributes(node, element)
1141
+ self.check_for_bound_information(node, element)
1142
+ e_list.append(node)
1143
+
1144
+ def _parse_rect(self, element, ident, label, lock, context_node, e_list, set_hidden):
1145
+ """
1146
+ Parse SVG Rect objects into `elem rect` objects.
1147
+
1148
+ @param element:
1149
+ @param ident:
1150
+ @param label:
1151
+ @param lock:
1152
+ @param context_node:
1153
+ @param e_list:
1154
+ @return:
1155
+ """
1156
+ if element.is_degenerate():
1157
+ return
1158
+ node = context_node.add(
1159
+ shape=element,
1160
+ type="elem rect",
1161
+ id=ident,
1162
+ label=label,
1163
+ lock=lock,
1164
+ hidden=set_hidden,
1165
+ )
1166
+ self.check_for_label_display(node, element)
1167
+ self.check_for_line_attributes(node, element)
1168
+ self.check_for_mk_path_attributes(node, element)
1169
+ if not self.check_for_bound_information(node, element) and self.precalc_bbox:
1170
+ # bounds will be done here, paintbounds won't...
1171
+ points = (
1172
+ Point(element.x, element.y),
1173
+ Point(element.x + element.width, element.y),
1174
+ Point(element.x + element.width, element.y + element.height),
1175
+ Point(element.x, element.y + element.height),
1176
+ )
1177
+ if not element.transform.is_identity():
1178
+ points = list(map(element.transform.point_in_matrix_space, points))
1179
+ xmin = min(p.x for p in points)
1180
+ ymin = min(p.y for p in points)
1181
+ xmax = max(p.x for p in points)
1182
+ ymax = max(p.y for p in points)
1183
+ node._bounds = [
1184
+ xmin,
1185
+ ymin,
1186
+ xmax,
1187
+ ymax,
1188
+ ]
1189
+ node._bounds_dirty = False
1190
+ node.revalidate_points()
1191
+ node._points_dirty = False
1192
+ e_list.append(node)
1193
+
1194
+ def _parse_line(self, element, ident, label, lock, context_node, e_list, set_hidden):
1195
+ """
1196
+ Parse SVG Line objects into `elem line`
1197
+
1198
+ @param element:
1199
+ @param ident:
1200
+ @param label:
1201
+ @param lock:
1202
+ @param context_node:
1203
+ @param e_list:
1204
+ @return:
1205
+ """
1206
+ if element.is_degenerate():
1207
+ return
1208
+ node = context_node.add(
1209
+ shape=element,
1210
+ type="elem line",
1211
+ id=ident,
1212
+ label=label,
1213
+ lock=lock,
1214
+ hidden=set_hidden,
1215
+ )
1216
+ self.check_for_label_display(node, element)
1217
+ self.check_for_line_attributes(node, element)
1218
+ self.check_for_mk_path_attributes(node, element)
1219
+ if not self.check_for_bound_information(node, element) and self.precalc_bbox:
1220
+ # bounds will be done here, paintbounds won't...
1221
+ points = (
1222
+ Point(element.x1, element.y1),
1223
+ Point(element.x2, element.y2),
1224
+ )
1225
+ if not element.transform.is_identity():
1226
+ points = list(map(element.transform.point_in_matrix_space, points))
1227
+ xmin = min(p.x for p in points)
1228
+ ymin = min(p.y for p in points)
1229
+ xmax = max(p.x for p in points)
1230
+ ymax = max(p.y for p in points)
1231
+ node._bounds = [
1232
+ xmin,
1233
+ ymin,
1234
+ xmax,
1235
+ ymax,
1236
+ ]
1237
+ node._bounds_dirty = False
1238
+ node.revalidate_points()
1239
+ node._points_dirty = False
1240
+ e_list.append(node)
1241
+
1242
+ def _parse_image(self, element, ident, label, lock, context_node, e_list, set_hidden):
1243
+ """
1244
+ Parse SVG Image objects into either `image raster` or `elem image` objects, potentially other classes.
1245
+
1246
+ @param element:
1247
+ @param ident:
1248
+ @param label:
1249
+ @param lock:
1250
+ @param context_node:
1251
+ @param e_list:
1252
+ @return:
1253
+ """
1254
+ try:
1255
+ element.load(os.path.dirname(self.pathname))
1256
+ if element.image is not None:
1257
+ try:
1258
+ from PIL import ImageOps
1259
+
1260
+ element.image = ImageOps.exif_transpose(element.image)
1261
+ except ImportError:
1262
+ pass
1263
+ try:
1264
+ operations = ast.literal_eval(element.values["operations"])
1265
+ except (ValueError, SyntaxError, KeyError):
1266
+ operations = None
1267
+
1268
+ if element.image is not None:
1269
+ try:
1270
+ dpi = element.image.info["dpi"]
1271
+ except KeyError:
1272
+ dpi = None
1273
+ _dpi = 500
1274
+ if (
1275
+ isinstance(dpi, tuple)
1276
+ and len(dpi) >= 2
1277
+ and dpi[0] != 0
1278
+ and dpi[1] != 0
1279
+ ):
1280
+ _dpi = round((float(dpi[0]) + float(dpi[1])) / 2, 0)
1281
+ _overscan = None
1282
+ try:
1283
+ _overscan = str(element.values.get("overscan"))
1284
+ except (ValueError, TypeError):
1285
+ pass
1286
+ _direction = None
1287
+ try:
1288
+ _direction = int(element.values.get("direction"))
1289
+ except (ValueError, TypeError):
1290
+ pass
1291
+ _invert = None
1292
+ try:
1293
+ _invert = bool(element.values.get("invert") == "True")
1294
+ except (ValueError, TypeError):
1295
+ pass
1296
+ _dither = None
1297
+ try:
1298
+ _dither = bool(element.values.get("dither") == "True")
1299
+ except (ValueError, TypeError):
1300
+ pass
1301
+ _dither_type = None
1302
+ try:
1303
+ _dither_type = element.values.get("dither_type")
1304
+ except (ValueError, TypeError):
1305
+ pass
1306
+ _keyhole = None
1307
+ try:
1308
+ _keyhole = element.values.get("keyhole_reference")
1309
+ except (ValueError, TypeError):
1310
+ pass
1311
+
1312
+ _red = None
1313
+ try:
1314
+ _red = float(element.values.get("red"))
1315
+ except (ValueError, TypeError):
1316
+ pass
1317
+ _green = None
1318
+ try:
1319
+ _green = float(element.values.get("green"))
1320
+ except (ValueError, TypeError):
1321
+ pass
1322
+ _blue = None
1323
+ try:
1324
+ _blue = float(element.values.get("blue"))
1325
+ except (ValueError, TypeError):
1326
+ pass
1327
+ _lightness = None
1328
+ try:
1329
+ _lightness = float(element.values.get("lightness"))
1330
+ except (ValueError, TypeError):
1331
+ pass
1332
+ _is_depthmap = False
1333
+ try:
1334
+ _is_depthmap = bool(element.values.get("is_depthmap") == "True")
1335
+ except (ValueError, TypeError):
1336
+ pass
1337
+ _depth_resolution = 256
1338
+ try:
1339
+ _depth_resolution = int(element.values.get("depth_resolution"))
1340
+ if _depth_resolution <= 1 or _depth_resolution > 256:
1341
+ _depth_resolution = 256
1342
+ except (ValueError, TypeError):
1343
+ pass
1344
+ node = context_node.add(
1345
+ image=element.image,
1346
+ matrix=element.transform,
1347
+ type="elem image",
1348
+ id=ident,
1349
+ overscan=_overscan,
1350
+ direction=_direction,
1351
+ dpi=_dpi,
1352
+ invert=_invert,
1353
+ dither=_dither,
1354
+ dither_type=_dither_type,
1355
+ red=_red,
1356
+ green=_green,
1357
+ blue=_blue,
1358
+ lightness=_lightness,
1359
+ label=label,
1360
+ operations=operations,
1361
+ lock=lock,
1362
+ is_depthmap=_is_depthmap,
1363
+ depth_resolution=_depth_resolution,
1364
+ keyhole_reference=_keyhole,
1365
+ hidden=set_hidden,
1366
+ )
1367
+ self.check_for_label_display(node, element)
1368
+ self.check_for_bound_information(node, element)
1369
+ e_list.append(node)
1370
+ except OSError:
1371
+ pass
1372
+
1373
+ def _parse_element(self, element, ident, label, lock, context_node, e_list):
1374
+ """
1375
+ SVGElement is type. Generic or unknown node type. These nodes do not have children, these are used in
1376
+ meerk40t contain notes and operations. Element type="elem point", and other points will also load with
1377
+ this code.
1378
+
1379
+ @param element:
1380
+ @param ident:
1381
+ @param label:
1382
+ @param lock:
1383
+ @param context_node:
1384
+ @param e_list:
1385
+ @return:
1386
+ """
1387
+
1388
+ # Fix: we have mixed capitalisaton in full_ns and tag --> adjust
1389
+ tag = element.values.get(SVG_ATTR_TAG).lower()
1390
+ if tag is not None:
1391
+ # We remove the name space.
1392
+ full_ns = f"{{{MEERK40T_NAMESPACE.lower()}}}"
1393
+ if full_ns in tag:
1394
+ tag = tag.replace(full_ns, "")
1395
+
1396
+ # Check if note-type
1397
+ if tag == "note":
1398
+ self.elements.note = element.values.get(SVG_TAG_TEXT)
1399
+ self.elements.signal("note", self.pathname)
1400
+ return
1401
+
1402
+ # Check if note-type
1403
+ if tag == "autoexec":
1404
+ self.elements.last_file_autoexec = element.values.get("autoexec")
1405
+ s = element.values.get("autoexec-active")
1406
+ self.elements.last_file_autoexec_active = bool(s in ("1", "True"))
1407
+ self.elements.signal("autoexec", self.pathname)
1408
+ return
1409
+
1410
+ node_type = element.values.get("type")
1411
+ if node_type is None:
1412
+ # Type is not given. Abort.
1413
+ return
1414
+
1415
+ if node_type == "op":
1416
+ # Meerk40t 0.7.x fallback node types.
1417
+ op_type = element.values.get("operation")
1418
+ if op_type is None:
1419
+ return
1420
+ node_type = f"op {op_type.lower()}"
1421
+ element.values["attributes"]["type"] = node_type
1422
+
1423
+ node_id = element.values.get("id")
1424
+
1425
+ # Get node dictionary.
1426
+ try:
1427
+ attrs = element.values["attributes"]
1428
+ except KeyError:
1429
+ attrs = element.values
1430
+
1431
+ # If type exists in the dictionary, delete it to avoid double attribute issues.
1432
+ try:
1433
+ del attrs["type"]
1434
+ except KeyError:
1435
+ pass
1436
+
1437
+ # Set dictionary types with proper classes.
1438
+ if "lock" in attrs:
1439
+ attrs["lock"] = lock
1440
+ if "transform" in element.values:
1441
+ # Uses chained transforms from primary context.
1442
+ attrs["matrix"] = Matrix(element.values["transform"])
1443
+ if "fill" in attrs:
1444
+ attrs["fill"] = Color(attrs["fill"])
1445
+ if "stroke" in attrs:
1446
+ attrs["stroke"] = Color(attrs["stroke"])
1447
+
1448
+ if tag == "operation":
1449
+ # Operation type node.
1450
+ if not self.load_operations:
1451
+ # We don't do that.
1452
+ return
1453
+
1454
+ self.operations_generated = True
1455
+
1456
+ try:
1457
+ if node_type == "op hatch":
1458
+ # Special fallback operation, op hatch is an op engrave with an effect hatch within it.
1459
+ node_type = "op engrave"
1460
+ op = self.elements.op_branch.add(type=node_type, **attrs)
1461
+ effect = op.add(type="effect hatch", **attrs)
1462
+ else:
1463
+ op = self.elements.op_branch.add(type=node_type, **attrs)
1464
+ op._ref_load = element.values.get("references")
1465
+
1466
+ if op is None or not hasattr(op, "type") or op.type is None:
1467
+ return
1468
+ if hasattr(op, "validate"):
1469
+ op.validate()
1470
+
1471
+ op.id = node_id
1472
+ self.operation_list.append(op)
1473
+ except AttributeError:
1474
+ # This operation is invalid.
1475
+ return
1476
+ except ValueError:
1477
+ # This operation type failed to bootstrap.
1478
+ return
1479
+
1480
+ elif tag == "element":
1481
+ # Check if SVGElement: element
1482
+ if "settings" in attrs:
1483
+ del attrs[
1484
+ "settings"
1485
+ ] # If settings was set, delete it, or it will mess things up
1486
+ elem = context_node.add(type=node_type, **attrs)
1487
+ # This could be an elem point
1488
+ self.check_for_label_display(elem, element)
1489
+ self.check_for_bound_information(elem, element)
1490
+ try:
1491
+ elem.validate()
1492
+ except AttributeError:
1493
+ pass
1494
+ elem.id = node_id
1495
+ e_list.append(elem)
1496
+
1497
+ def parse(self, element, context_node, e_list, branch=None, uselabel=None):
1498
+ """
1499
+ Parse does the bulk of the work. Given an element, here the base case is an SVG itself, we parse such that
1500
+ any groups will call and check all children recursively, updating the context_node, and passing each element
1501
+ to this same function.
1502
+
1503
+
1504
+ @param element: Element to parse.
1505
+ @param context_node: Current context parent we're writing to.
1506
+ @param e_list: elements list of all the nodes added by this function.
1507
+ @param branch: Branch we are currently adding elements to.
1508
+ @param uselabel:
1509
+ @return:
1510
+ """
1511
+ set_hidden = False
1512
+ display = ""
1513
+ if "display" in element.values:
1514
+ display = element.values.get("display").lower()
1515
+ if display == "none":
1516
+ if branch not in ("elements", "regmarks"):
1517
+ return
1518
+ if element.values.get("visibility") == "hidden" or display == "none":
1519
+
1520
+ if self.load_hidden_to_regmarks:
1521
+ if branch != "regmarks":
1522
+ self.parse(
1523
+ element,
1524
+ self.elements.reg_branch,
1525
+ self.regmark_list,
1526
+ branch="regmarks",
1527
+ )
1528
+ return
1529
+ else:
1530
+ set_hidden = True
1531
+
1532
+ ident = element.id
1533
+
1534
+ _label = uselabel if uselabel else self.get_tag_label(element)
1535
+ _lock = None
1536
+ try:
1537
+ _lock = bool(element.values.get("lock") == "True")
1538
+ except (ValueError, TypeError):
1539
+ pass
1540
+
1541
+ is_dot, dot_point = SVGProcessor.is_dot(element)
1542
+ if is_dot:
1543
+ node = context_node.add(
1544
+ point=dot_point,
1545
+ type="elem point",
1546
+ matrix=Matrix(),
1547
+ fill=element.fill,
1548
+ stroke=element.stroke,
1549
+ label=_label,
1550
+ lock=_lock,
1551
+ hidden=set_hidden,
1552
+ )
1553
+ self.check_for_label_display(node, element)
1554
+ self.check_for_bound_information(node, element)
1555
+ e_list.append(node)
1556
+ elif isinstance(element, SVGText):
1557
+ self._parse_text(element, ident, _label, _lock, context_node, e_list, set_hidden)
1558
+ elif isinstance(element, Path):
1559
+ self._parse_path(element, ident, _label, _lock, context_node, e_list, set_hidden)
1560
+ elif isinstance(element, (Polygon, Polyline)):
1561
+ self._parse_polyline(element, ident, _label, _lock, context_node, e_list, set_hidden)
1562
+ elif isinstance(element, (Circle, Ellipse)):
1563
+ self._parse_ellipse(element, ident, _label, _lock, context_node, e_list, set_hidden)
1564
+ elif isinstance(element, Rect):
1565
+ self._parse_rect(element, ident, _label, _lock, context_node, e_list, set_hidden)
1566
+ elif isinstance(element, SimpleLine):
1567
+ self._parse_line(element, ident, _label, _lock, context_node, e_list, set_hidden)
1568
+ elif isinstance(element, SVGImage):
1569
+ self._parse_image(element, ident, _label, _lock, context_node, e_list, set_hidden)
1570
+ elif isinstance(element, SVG):
1571
+ # SVG is type of group, it must be processed before Group. Nothing special is done with the type.
1572
+ if self.reverse:
1573
+ for child in reversed(element):
1574
+ self.parse(child, context_node, e_list, branch=branch)
1575
+ else:
1576
+ for child in element:
1577
+ self.parse(child, context_node, e_list, branch=branch)
1578
+ elif isinstance(element, Group):
1579
+ if branch != "regmarks" and (_label == "regmarks" or ident == "regmarks"):
1580
+ # Recurse at same level within regmarks.
1581
+ self.parse(
1582
+ element,
1583
+ self.elements.reg_branch,
1584
+ self.regmark_list,
1585
+ branch="regmarks",
1586
+ )
1587
+ return
1588
+
1589
+ # Load group with specific group attributes (if needed)
1590
+ e_dict = dict(element.values["attributes"])
1591
+ e_type = e_dict.get("type", "group")
1592
+ if branch != "operations" and (
1593
+ e_type.startswith("op ")
1594
+ or e_type.startswith("place ")
1595
+ or e_type.startswith("util ")
1596
+ ):
1597
+ # This is an operations, but we are not in operations context.
1598
+ if not self.load_operations:
1599
+ # We don't do that.
1600
+ return
1601
+ self.operations_generated = True
1602
+ self.parse(
1603
+ element,
1604
+ self.elements.op_branch,
1605
+ self.operation_list,
1606
+ branch="operations",
1607
+ )
1608
+ return
1609
+ if "stroke" in e_dict:
1610
+ e_dict["stroke"] = Color(e_dict.get("stroke"))
1611
+ if "fill" in e_dict:
1612
+ e_dict["fill"] = Color(e_dict.get("fill"))
1613
+ for attr in ("type", "id", "label"):
1614
+ if attr in e_dict:
1615
+ del e_dict[attr]
1616
+
1617
+ #
1618
+ already = False
1619
+ if self.reuse_operations:
1620
+ # No need to create another operation, if we do
1621
+ # have an identical operation in place
1622
+ if e_type.startswith("op "):
1623
+ # It needs to be non-empty to be used!
1624
+ for testop in self.elements.ops():
1625
+ if len(testop.children) == 0:
1626
+ continue
1627
+ if e_type != testop.type:
1628
+ continue
1629
+ differs = False
1630
+ for check_attr, check_default in (
1631
+ ("id", None),
1632
+ ("power", "1000"),
1633
+ ("speed", None),
1634
+ ("passes", "0"),
1635
+ ("color", None),
1636
+ ):
1637
+ if not hasattr(testop, check_attr):
1638
+ if check_attr in e_dict:
1639
+ differs = True
1640
+ break
1641
+ continue
1642
+ test_val = getattr(testop, check_attr, check_default)
1643
+ if test_val is None:
1644
+ test_val = ""
1645
+ else:
1646
+ test_val = str(test_val)
1647
+ if check_attr == "id":
1648
+ eop_val = ident
1649
+ else:
1650
+ if check_attr not in e_dict:
1651
+ eop_val = check_default
1652
+ else:
1653
+ eop_val = e_dict[check_attr]
1654
+ if eop_val is None:
1655
+ eop_val = ""
1656
+ if test_val != eop_val:
1657
+ differs = True
1658
+ # print (f"{testop.type}.{check_attr}: {eop_val} != {test_val}")
1659
+ break
1660
+ if differs:
1661
+ continue
1662
+ context_node = testop
1663
+ already = True
1664
+ break
1665
+
1666
+ if not already:
1667
+ context_node = context_node.add(
1668
+ type=e_type, id=ident, label=_label, **e_dict
1669
+ )
1670
+ self.check_for_label_display(context_node, element)
1671
+ self.check_for_bound_information(context_node, element)
1672
+ context_node._ref_load = element.values.get("references")
1673
+ e_list.append(context_node)
1674
+ if hasattr(context_node, "validate"):
1675
+ context_node.validate()
1676
+
1677
+ # recurse to children
1678
+ if self.reverse:
1679
+ for child in reversed(element):
1680
+ self.parse(child, context_node, e_list, branch=branch)
1681
+ else:
1682
+ for child in element:
1683
+ self.parse(child, context_node, e_list, branch=branch)
1684
+ elif isinstance(element, Use):
1685
+ # recurse to children, but do not subgroup elements.
1686
+ # We still use the original label
1687
+ if self.reverse:
1688
+ for child in reversed(element):
1689
+ self.parse(
1690
+ child, context_node, e_list, branch=branch, uselabel=_label
1691
+ )
1692
+ else:
1693
+ for child in element:
1694
+ self.parse(
1695
+ child, context_node, e_list, branch=branch, uselabel=_label
1696
+ )
1697
+ else:
1698
+ self._parse_element(element, ident, _label, _lock, context_node, e_list)
1699
+
1700
+ def cleanup(self):
1701
+ # Make a couple of structural fixes that would be to cumbersome to integrate at parse level
1702
+ # 1) Fix regmark grouping.
1703
+ # Regmarks nodes are saved under a group with visibility=False set
1704
+ # So let's flatten this top group
1705
+ if len(self.regmark_list) > 0:
1706
+ # We need to add another filenode under regmarks and move all elements to it
1707
+ context_node = self.elements.reg_branch
1708
+ file_node = context_node.add(type="file", filepath=self.pathname)
1709
+ for node in self.regmark_list:
1710
+ if node._parent is context_node:
1711
+ if node.type == "group" and (node.id == "regmarks" or node.label == "regmarks"):
1712
+ for n in list(node.children):
1713
+ file_node.append_child(n)
1714
+ node.remove_node() # Removing group/file node.
1715
+ else:
1716
+ file_node.append_child(node)
1717
+
1718
+ regmark = self.elements.reg_branch
1719
+ for c in regmark.children:
1720
+ if c.type == "group" and (c.id == "regmarks" or c.label == "regmarks"):
1721
+ for n in list(c.children):
1722
+ c.insert_sibling(n)
1723
+ c.remove_node() # Removing group/file node.
1724
+
1725
+ needs_update = False
1726
+ for c in self.elements.flat():
1727
+ # All nodes including regmarks and elements
1728
+ if c.type == "elem image" and c.keyhole_reference is not None:
1729
+ refnode = self.elements.find_node(c.keyhole_reference)
1730
+ if refnode is None or not hasattr(refnode, "as_geometry"):
1731
+ # Invalid -> remove
1732
+ c.keyhole_reference = None
1733
+ else:
1734
+ try:
1735
+ self.elements.register_keyhole(refnode, c)
1736
+ needs_update = True
1737
+ except ValueError as e:
1738
+ c.keyhole_reference = None
1739
+
1740
+ if needs_update:
1741
+ self.elements.process_keyhole_updates(None)
1742
+
1743
+ class SVGLoader:
1744
+ """
1745
+ SVG loader - loading elements, regmarks and operations
1746
+ """
1747
+
1748
+ @staticmethod
1749
+ def load_types():
1750
+ yield "Scalable Vector Graphics", ("svg", "svgz"), "image/svg+xml"
1751
+
1752
+ @staticmethod
1753
+ def load(context, elements_service, pathname, **kwargs):
1754
+ ppi = float(kwargs["svg_ppi"]) if "svg_ppi" in kwargs else DEFAULT_PPI
1755
+ if ppi == 0:
1756
+ ppi = DEFAULT_PPI
1757
+ scale_factor = NATIVE_UNIT_PER_INCH / ppi
1758
+ source = pathname
1759
+ if pathname.lower().endswith("svgz"):
1760
+ source = gzip.open(pathname, "rb")
1761
+ try:
1762
+ if context.elements.svg_viewport_bed:
1763
+ width = Length(amount=context.device.view.unit_width).length_mm
1764
+ height = Length(amount=context.device.view.unit_height).length_mm
1765
+ else:
1766
+ width = None
1767
+ height = None
1768
+ # The color attribute of SVG.parse decides which default color
1769
+ # a stroke / fill will get if the attribute "currentColor" is
1770
+ # set - we opt for "black"
1771
+ svg = SVG.parse(
1772
+ source=source,
1773
+ reify=False,
1774
+ width=width,
1775
+ height=height,
1776
+ ppi=ppi,
1777
+ color="black",
1778
+ parse_display_none=True,
1779
+ transform=f"scale({scale_factor})",
1780
+ )
1781
+ except ParseError as e:
1782
+ raise BadFileError(str(e)) from e
1783
+ reuse = elements_service.reuse_operations_on_load
1784
+ to_regmarks = elements_service.load_hidden_to_regmarks
1785
+ elements_service._loading_cleared = True
1786
+ svg_processor = SVGProcessor(elements_service, load_operations=True, reuse_operations=reuse, load_hidden_to_regmarks=to_regmarks)
1787
+ svg_processor.process(svg, pathname)
1788
+ svg_processor.cleanup()
1789
+ return True
1790
+
1791
+
1792
+ class SVGLoaderPlain:
1793
+ """
1794
+ SVG loader but without loading the operations branch
1795
+ """
1796
+
1797
+ @staticmethod
1798
+ def load_types():
1799
+ yield "SVG (elements only)", ("svg", "svgz"), "image/svg+xml"
1800
+
1801
+ @staticmethod
1802
+ def load(context, elements_service, pathname, **kwargs):
1803
+ ppi = float(kwargs["svg_ppi"]) if "svg_ppi" in kwargs else DEFAULT_PPI
1804
+ if ppi == 0:
1805
+ ppi = DEFAULT_PPI
1806
+ scale_factor = NATIVE_UNIT_PER_INCH / ppi
1807
+ source = pathname
1808
+ if pathname.lower().endswith("svgz"):
1809
+ source = gzip.open(pathname, "rb")
1810
+ try:
1811
+ if context.elements.svg_viewport_bed:
1812
+ width = Length(amount=context.device.view.unit_width).length_mm
1813
+ height = Length(amount=context.device.view.unit_height).length_mm
1814
+ else:
1815
+ width = None
1816
+ height = None
1817
+ # The color attribute of SVG.parse decides which default color
1818
+ # a stroke / fill will get if the attribute "currentColor" is
1819
+ # set - we opt for "black"
1820
+ svg = SVG.parse(
1821
+ source=source,
1822
+ reify=False,
1823
+ width=width,
1824
+ height=height,
1825
+ ppi=ppi,
1826
+ color="black",
1827
+ transform=f"scale({scale_factor})",
1828
+ )
1829
+ except ParseError as e:
1830
+ raise BadFileError(str(e)) from e
1831
+ elements_service._loading_cleared = True
1832
+ to_regmarks = elements_service.load_hidden_to_regmarks
1833
+ svg_processor = SVGProcessor(elements_service, load_operations=False, load_hidden_to_regmarks=to_regmarks)
1834
+ svg_processor.process(svg, pathname)
1835
+ svg_processor.cleanup()
1836
+ return True