meerk40t 0.9.3001__py2.py3-none-any.whl → 0.9.7010__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 (445) 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 +1195 -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 +1844 -1507
  50. meerk40t/core/elements/clipboard.py +229 -219
  51. meerk40t/core/elements/element_treeops.py +4561 -2837
  52. meerk40t/core/elements/element_types.py +125 -105
  53. meerk40t/core/elements/elements.py +4329 -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 +933 -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/trace.py +651 -563
  66. meerk40t/core/elements/tree_commands.py +415 -409
  67. meerk40t/core/elements/undo_redo.py +116 -58
  68. meerk40t/core/elements/wordlist.py +319 -200
  69. meerk40t/core/exceptions.py +9 -9
  70. meerk40t/core/laserjob.py +220 -220
  71. meerk40t/core/logging.py +63 -63
  72. meerk40t/core/node/blobnode.py +83 -86
  73. meerk40t/core/node/bootstrap.py +105 -103
  74. meerk40t/core/node/branch_elems.py +40 -31
  75. meerk40t/core/node/branch_ops.py +45 -38
  76. meerk40t/core/node/branch_regmark.py +48 -41
  77. meerk40t/core/node/cutnode.py +29 -32
  78. meerk40t/core/node/effect_hatch.py +375 -257
  79. meerk40t/core/node/effect_warp.py +398 -0
  80. meerk40t/core/node/effect_wobble.py +441 -309
  81. meerk40t/core/node/elem_ellipse.py +404 -309
  82. meerk40t/core/node/elem_image.py +1082 -801
  83. meerk40t/core/node/elem_line.py +358 -292
  84. meerk40t/core/node/elem_path.py +259 -201
  85. meerk40t/core/node/elem_point.py +129 -102
  86. meerk40t/core/node/elem_polyline.py +310 -246
  87. meerk40t/core/node/elem_rect.py +376 -286
  88. meerk40t/core/node/elem_text.py +445 -418
  89. meerk40t/core/node/filenode.py +59 -40
  90. meerk40t/core/node/groupnode.py +138 -74
  91. meerk40t/core/node/image_processed.py +777 -766
  92. meerk40t/core/node/image_raster.py +156 -113
  93. meerk40t/core/node/layernode.py +31 -31
  94. meerk40t/core/node/mixins.py +135 -107
  95. meerk40t/core/node/node.py +1427 -1304
  96. meerk40t/core/node/nutils.py +117 -114
  97. meerk40t/core/node/op_cut.py +462 -335
  98. meerk40t/core/node/op_dots.py +296 -251
  99. meerk40t/core/node/op_engrave.py +414 -311
  100. meerk40t/core/node/op_image.py +755 -369
  101. meerk40t/core/node/op_raster.py +787 -522
  102. meerk40t/core/node/place_current.py +37 -40
  103. meerk40t/core/node/place_point.py +329 -126
  104. meerk40t/core/node/refnode.py +58 -47
  105. meerk40t/core/node/rootnode.py +225 -219
  106. meerk40t/core/node/util_console.py +48 -48
  107. meerk40t/core/node/util_goto.py +84 -65
  108. meerk40t/core/node/util_home.py +61 -61
  109. meerk40t/core/node/util_input.py +102 -102
  110. meerk40t/core/node/util_output.py +102 -102
  111. meerk40t/core/node/util_wait.py +65 -65
  112. meerk40t/core/parameters.py +709 -707
  113. meerk40t/core/planner.py +875 -785
  114. meerk40t/core/plotplanner.py +656 -652
  115. meerk40t/core/space.py +120 -113
  116. meerk40t/core/spoolers.py +706 -705
  117. meerk40t/core/svg_io.py +1836 -1549
  118. meerk40t/core/treeop.py +534 -445
  119. meerk40t/core/undos.py +278 -124
  120. meerk40t/core/units.py +784 -680
  121. meerk40t/core/view.py +393 -322
  122. meerk40t/core/webhelp.py +62 -62
  123. meerk40t/core/wordlist.py +513 -504
  124. meerk40t/cylinder/cylinder.py +247 -0
  125. meerk40t/cylinder/gui/cylindersettings.py +41 -0
  126. meerk40t/cylinder/gui/gui.py +24 -0
  127. meerk40t/device/__init__.py +1 -1
  128. meerk40t/device/basedevice.py +322 -123
  129. meerk40t/device/devicechoices.py +50 -0
  130. meerk40t/device/dummydevice.py +163 -128
  131. meerk40t/device/gui/defaultactions.py +618 -602
  132. meerk40t/device/gui/effectspanel.py +114 -0
  133. meerk40t/device/gui/formatterpanel.py +253 -290
  134. meerk40t/device/gui/warningpanel.py +337 -260
  135. meerk40t/device/mixins.py +13 -13
  136. meerk40t/dxf/__init__.py +1 -1
  137. meerk40t/dxf/dxf_io.py +766 -554
  138. meerk40t/dxf/plugin.py +47 -35
  139. meerk40t/external_plugins.py +79 -79
  140. meerk40t/external_plugins_build.py +28 -28
  141. meerk40t/extra/cag.py +112 -116
  142. meerk40t/extra/coolant.py +403 -0
  143. meerk40t/extra/encode_detect.py +198 -0
  144. meerk40t/extra/ezd.py +1165 -1165
  145. meerk40t/extra/hershey.py +835 -340
  146. meerk40t/extra/imageactions.py +322 -316
  147. meerk40t/extra/inkscape.py +630 -622
  148. meerk40t/extra/lbrn.py +424 -424
  149. meerk40t/extra/outerworld.py +284 -0
  150. meerk40t/extra/param_functions.py +1542 -1556
  151. meerk40t/extra/potrace.py +257 -253
  152. meerk40t/extra/serial_exchange.py +118 -0
  153. meerk40t/extra/updater.py +602 -453
  154. meerk40t/extra/vectrace.py +147 -146
  155. meerk40t/extra/winsleep.py +83 -83
  156. meerk40t/extra/xcs_reader.py +597 -0
  157. meerk40t/fill/fills.py +781 -335
  158. meerk40t/fill/patternfill.py +1061 -1061
  159. meerk40t/fill/patterns.py +614 -567
  160. meerk40t/grbl/control.py +87 -87
  161. meerk40t/grbl/controller.py +990 -903
  162. meerk40t/grbl/device.py +1081 -768
  163. meerk40t/grbl/driver.py +989 -771
  164. meerk40t/grbl/emulator.py +532 -497
  165. meerk40t/grbl/gcodejob.py +783 -767
  166. meerk40t/grbl/gui/grblconfiguration.py +373 -298
  167. meerk40t/grbl/gui/grblcontroller.py +485 -271
  168. meerk40t/grbl/gui/grblhardwareconfig.py +269 -153
  169. meerk40t/grbl/gui/grbloperationconfig.py +105 -0
  170. meerk40t/grbl/gui/gui.py +147 -116
  171. meerk40t/grbl/interpreter.py +44 -44
  172. meerk40t/grbl/loader.py +22 -22
  173. meerk40t/grbl/mock_connection.py +56 -56
  174. meerk40t/grbl/plugin.py +294 -264
  175. meerk40t/grbl/serial_connection.py +93 -88
  176. meerk40t/grbl/tcp_connection.py +81 -79
  177. meerk40t/grbl/ws_connection.py +112 -0
  178. meerk40t/gui/__init__.py +1 -1
  179. meerk40t/gui/about.py +2042 -296
  180. meerk40t/gui/alignment.py +1644 -1608
  181. meerk40t/gui/autoexec.py +199 -0
  182. meerk40t/gui/basicops.py +791 -670
  183. meerk40t/gui/bufferview.py +77 -71
  184. meerk40t/gui/busy.py +170 -133
  185. meerk40t/gui/choicepropertypanel.py +1673 -1469
  186. meerk40t/gui/consolepanel.py +706 -542
  187. meerk40t/gui/devicepanel.py +687 -581
  188. meerk40t/gui/dialogoptions.py +110 -107
  189. meerk40t/gui/executejob.py +316 -306
  190. meerk40t/gui/fonts.py +90 -90
  191. meerk40t/gui/functionwrapper.py +252 -0
  192. meerk40t/gui/gui_mixins.py +729 -0
  193. meerk40t/gui/guicolors.py +205 -182
  194. meerk40t/gui/help_assets/help_assets.py +218 -201
  195. meerk40t/gui/helper.py +154 -0
  196. meerk40t/gui/hersheymanager.py +1430 -846
  197. meerk40t/gui/icons.py +3422 -2747
  198. meerk40t/gui/imagesplitter.py +555 -508
  199. meerk40t/gui/keymap.py +354 -344
  200. meerk40t/gui/laserpanel.py +892 -806
  201. meerk40t/gui/laserrender.py +1470 -1232
  202. meerk40t/gui/lasertoolpanel.py +805 -793
  203. meerk40t/gui/magnetoptions.py +436 -0
  204. meerk40t/gui/materialmanager.py +2917 -0
  205. meerk40t/gui/materialtest.py +1722 -1694
  206. meerk40t/gui/mkdebug.py +646 -359
  207. meerk40t/gui/mwindow.py +163 -140
  208. meerk40t/gui/navigationpanels.py +2605 -2467
  209. meerk40t/gui/notes.py +143 -142
  210. meerk40t/gui/opassignment.py +414 -410
  211. meerk40t/gui/operation_info.py +310 -299
  212. meerk40t/gui/plugin.py +494 -328
  213. meerk40t/gui/position.py +714 -669
  214. meerk40t/gui/preferences.py +901 -650
  215. meerk40t/gui/propertypanels/attributes.py +1461 -1131
  216. meerk40t/gui/propertypanels/blobproperty.py +117 -114
  217. meerk40t/gui/propertypanels/consoleproperty.py +83 -80
  218. meerk40t/gui/propertypanels/gotoproperty.py +77 -0
  219. meerk40t/gui/propertypanels/groupproperties.py +223 -217
  220. meerk40t/gui/propertypanels/hatchproperty.py +489 -469
  221. meerk40t/gui/propertypanels/imageproperty.py +2244 -1384
  222. meerk40t/gui/propertypanels/inputproperty.py +59 -58
  223. meerk40t/gui/propertypanels/opbranchproperties.py +82 -80
  224. meerk40t/gui/propertypanels/operationpropertymain.py +1890 -1638
  225. meerk40t/gui/propertypanels/outputproperty.py +59 -58
  226. meerk40t/gui/propertypanels/pathproperty.py +389 -380
  227. meerk40t/gui/propertypanels/placementproperty.py +1214 -383
  228. meerk40t/gui/propertypanels/pointproperty.py +140 -136
  229. meerk40t/gui/propertypanels/propertywindow.py +313 -181
  230. meerk40t/gui/propertypanels/rasterwizardpanels.py +996 -912
  231. meerk40t/gui/propertypanels/regbranchproperties.py +76 -0
  232. meerk40t/gui/propertypanels/textproperty.py +770 -755
  233. meerk40t/gui/propertypanels/waitproperty.py +56 -55
  234. meerk40t/gui/propertypanels/warpproperty.py +121 -0
  235. meerk40t/gui/propertypanels/wobbleproperty.py +255 -204
  236. meerk40t/gui/ribbon.py +2468 -2210
  237. meerk40t/gui/scene/scene.py +1100 -1051
  238. meerk40t/gui/scene/sceneconst.py +22 -22
  239. meerk40t/gui/scene/scenepanel.py +439 -349
  240. meerk40t/gui/scene/scenespacewidget.py +365 -365
  241. meerk40t/gui/scene/widget.py +518 -505
  242. meerk40t/gui/scenewidgets/affinemover.py +215 -215
  243. meerk40t/gui/scenewidgets/attractionwidget.py +315 -309
  244. meerk40t/gui/scenewidgets/bedwidget.py +120 -97
  245. meerk40t/gui/scenewidgets/elementswidget.py +137 -107
  246. meerk40t/gui/scenewidgets/gridwidget.py +785 -745
  247. meerk40t/gui/scenewidgets/guidewidget.py +765 -765
  248. meerk40t/gui/scenewidgets/laserpathwidget.py +66 -66
  249. meerk40t/gui/scenewidgets/machineoriginwidget.py +86 -86
  250. meerk40t/gui/scenewidgets/nodeselector.py +28 -28
  251. meerk40t/gui/scenewidgets/rectselectwidget.py +589 -346
  252. meerk40t/gui/scenewidgets/relocatewidget.py +33 -33
  253. meerk40t/gui/scenewidgets/reticlewidget.py +83 -83
  254. meerk40t/gui/scenewidgets/selectionwidget.py +2952 -2756
  255. meerk40t/gui/simpleui.py +357 -333
  256. meerk40t/gui/simulation.py +2431 -2094
  257. meerk40t/gui/snapoptions.py +208 -203
  258. meerk40t/gui/spoolerpanel.py +1227 -1180
  259. meerk40t/gui/statusbarwidgets/defaultoperations.py +480 -353
  260. meerk40t/gui/statusbarwidgets/infowidget.py +520 -483
  261. meerk40t/gui/statusbarwidgets/opassignwidget.py +356 -355
  262. meerk40t/gui/statusbarwidgets/selectionwidget.py +172 -171
  263. meerk40t/gui/statusbarwidgets/shapepropwidget.py +754 -236
  264. meerk40t/gui/statusbarwidgets/statusbar.py +272 -260
  265. meerk40t/gui/statusbarwidgets/statusbarwidget.py +268 -270
  266. meerk40t/gui/statusbarwidgets/strokewidget.py +267 -251
  267. meerk40t/gui/themes.py +200 -78
  268. meerk40t/gui/tips.py +591 -0
  269. meerk40t/gui/toolwidgets/circlebrush.py +35 -35
  270. meerk40t/gui/toolwidgets/toolcircle.py +248 -242
  271. meerk40t/gui/toolwidgets/toolcontainer.py +82 -77
  272. meerk40t/gui/toolwidgets/tooldraw.py +97 -90
  273. meerk40t/gui/toolwidgets/toolellipse.py +219 -212
  274. meerk40t/gui/toolwidgets/toolimagecut.py +25 -132
  275. meerk40t/gui/toolwidgets/toolline.py +39 -144
  276. meerk40t/gui/toolwidgets/toollinetext.py +79 -236
  277. meerk40t/gui/toolwidgets/toollinetext_inline.py +296 -0
  278. meerk40t/gui/toolwidgets/toolmeasure.py +160 -216
  279. meerk40t/gui/toolwidgets/toolnodeedit.py +2088 -2074
  280. meerk40t/gui/toolwidgets/toolnodemove.py +92 -94
  281. meerk40t/gui/toolwidgets/toolparameter.py +754 -668
  282. meerk40t/gui/toolwidgets/toolplacement.py +108 -108
  283. meerk40t/gui/toolwidgets/toolpoint.py +68 -59
  284. meerk40t/gui/toolwidgets/toolpointlistbuilder.py +294 -0
  285. meerk40t/gui/toolwidgets/toolpointmove.py +183 -0
  286. meerk40t/gui/toolwidgets/toolpolygon.py +288 -403
  287. meerk40t/gui/toolwidgets/toolpolyline.py +38 -196
  288. meerk40t/gui/toolwidgets/toolrect.py +211 -207
  289. meerk40t/gui/toolwidgets/toolrelocate.py +72 -72
  290. meerk40t/gui/toolwidgets/toolribbon.py +598 -113
  291. meerk40t/gui/toolwidgets/tooltabedit.py +546 -0
  292. meerk40t/gui/toolwidgets/tooltext.py +98 -89
  293. meerk40t/gui/toolwidgets/toolvector.py +213 -204
  294. meerk40t/gui/toolwidgets/toolwidget.py +39 -39
  295. meerk40t/gui/usbconnect.py +98 -91
  296. meerk40t/gui/utilitywidgets/buttonwidget.py +18 -18
  297. meerk40t/gui/utilitywidgets/checkboxwidget.py +90 -90
  298. meerk40t/gui/utilitywidgets/controlwidget.py +14 -14
  299. meerk40t/gui/utilitywidgets/cyclocycloidwidget.py +343 -340
  300. meerk40t/gui/utilitywidgets/debugwidgets.py +148 -0
  301. meerk40t/gui/utilitywidgets/handlewidget.py +27 -27
  302. meerk40t/gui/utilitywidgets/harmonograph.py +450 -447
  303. meerk40t/gui/utilitywidgets/openclosewidget.py +40 -40
  304. meerk40t/gui/utilitywidgets/rotationwidget.py +54 -54
  305. meerk40t/gui/utilitywidgets/scalewidget.py +75 -75
  306. meerk40t/gui/utilitywidgets/seekbarwidget.py +183 -183
  307. meerk40t/gui/utilitywidgets/togglewidget.py +142 -142
  308. meerk40t/gui/utilitywidgets/toolbarwidget.py +8 -8
  309. meerk40t/gui/wordlisteditor.py +985 -931
  310. meerk40t/gui/wxmeerk40t.py +1444 -1169
  311. meerk40t/gui/wxmmain.py +5578 -4112
  312. meerk40t/gui/wxmribbon.py +1591 -1076
  313. meerk40t/gui/wxmscene.py +1635 -1453
  314. meerk40t/gui/wxmtree.py +2410 -2089
  315. meerk40t/gui/wxutils.py +1769 -1099
  316. meerk40t/gui/zmatrix.py +102 -102
  317. meerk40t/image/__init__.py +1 -1
  318. meerk40t/image/dither.py +429 -0
  319. meerk40t/image/imagetools.py +2778 -2269
  320. meerk40t/internal_plugins.py +150 -130
  321. meerk40t/kernel/__init__.py +63 -12
  322. meerk40t/kernel/channel.py +259 -212
  323. meerk40t/kernel/context.py +538 -538
  324. meerk40t/kernel/exceptions.py +41 -41
  325. meerk40t/kernel/functions.py +463 -414
  326. meerk40t/kernel/jobs.py +100 -100
  327. meerk40t/kernel/kernel.py +3809 -3571
  328. meerk40t/kernel/lifecycles.py +71 -71
  329. meerk40t/kernel/module.py +49 -49
  330. meerk40t/kernel/service.py +147 -147
  331. meerk40t/kernel/settings.py +383 -343
  332. meerk40t/lihuiyu/controller.py +883 -876
  333. meerk40t/lihuiyu/device.py +1181 -1069
  334. meerk40t/lihuiyu/driver.py +1466 -1372
  335. meerk40t/lihuiyu/gui/gui.py +127 -106
  336. meerk40t/lihuiyu/gui/lhyaccelgui.py +377 -363
  337. meerk40t/lihuiyu/gui/lhycontrollergui.py +741 -651
  338. meerk40t/lihuiyu/gui/lhydrivergui.py +470 -446
  339. meerk40t/lihuiyu/gui/lhyoperationproperties.py +238 -237
  340. meerk40t/lihuiyu/gui/tcpcontroller.py +226 -190
  341. meerk40t/lihuiyu/interpreter.py +53 -53
  342. meerk40t/lihuiyu/laserspeed.py +450 -450
  343. meerk40t/lihuiyu/loader.py +90 -90
  344. meerk40t/lihuiyu/parser.py +404 -404
  345. meerk40t/lihuiyu/plugin.py +101 -102
  346. meerk40t/lihuiyu/tcp_connection.py +111 -109
  347. meerk40t/main.py +231 -165
  348. meerk40t/moshi/builder.py +788 -781
  349. meerk40t/moshi/controller.py +505 -499
  350. meerk40t/moshi/device.py +495 -442
  351. meerk40t/moshi/driver.py +862 -696
  352. meerk40t/moshi/gui/gui.py +78 -76
  353. meerk40t/moshi/gui/moshicontrollergui.py +538 -522
  354. meerk40t/moshi/gui/moshidrivergui.py +87 -75
  355. meerk40t/moshi/plugin.py +43 -43
  356. meerk40t/network/console_server.py +102 -57
  357. meerk40t/network/kernelserver.py +10 -9
  358. meerk40t/network/tcp_server.py +142 -140
  359. meerk40t/network/udp_server.py +103 -77
  360. meerk40t/network/web_server.py +390 -0
  361. meerk40t/newly/controller.py +1158 -1144
  362. meerk40t/newly/device.py +874 -732
  363. meerk40t/newly/driver.py +540 -412
  364. meerk40t/newly/gui/gui.py +219 -188
  365. meerk40t/newly/gui/newlyconfig.py +116 -101
  366. meerk40t/newly/gui/newlycontroller.py +193 -186
  367. meerk40t/newly/gui/operationproperties.py +51 -51
  368. meerk40t/newly/mock_connection.py +82 -82
  369. meerk40t/newly/newly_params.py +56 -56
  370. meerk40t/newly/plugin.py +1214 -1246
  371. meerk40t/newly/usb_connection.py +322 -322
  372. meerk40t/rotary/gui/gui.py +52 -46
  373. meerk40t/rotary/gui/rotarysettings.py +240 -232
  374. meerk40t/rotary/rotary.py +202 -98
  375. meerk40t/ruida/control.py +291 -91
  376. meerk40t/ruida/controller.py +138 -1088
  377. meerk40t/ruida/device.py +672 -231
  378. meerk40t/ruida/driver.py +534 -472
  379. meerk40t/ruida/emulator.py +1494 -1491
  380. meerk40t/ruida/exceptions.py +4 -4
  381. meerk40t/ruida/gui/gui.py +71 -76
  382. meerk40t/ruida/gui/ruidaconfig.py +239 -72
  383. meerk40t/ruida/gui/ruidacontroller.py +187 -184
  384. meerk40t/ruida/gui/ruidaoperationproperties.py +48 -47
  385. meerk40t/ruida/loader.py +54 -52
  386. meerk40t/ruida/mock_connection.py +57 -109
  387. meerk40t/ruida/plugin.py +124 -87
  388. meerk40t/ruida/rdjob.py +2084 -945
  389. meerk40t/ruida/serial_connection.py +116 -0
  390. meerk40t/ruida/tcp_connection.py +146 -0
  391. meerk40t/ruida/udp_connection.py +73 -0
  392. meerk40t/svgelements.py +9671 -9669
  393. meerk40t/tools/driver_to_path.py +584 -579
  394. meerk40t/tools/geomstr.py +5583 -4680
  395. meerk40t/tools/jhfparser.py +357 -292
  396. meerk40t/tools/kerftest.py +904 -890
  397. meerk40t/tools/livinghinges.py +1168 -1033
  398. meerk40t/tools/pathtools.py +987 -949
  399. meerk40t/tools/pmatrix.py +234 -0
  400. meerk40t/tools/pointfinder.py +942 -942
  401. meerk40t/tools/polybool.py +940 -940
  402. meerk40t/tools/rasterplotter.py +1660 -547
  403. meerk40t/tools/shxparser.py +989 -901
  404. meerk40t/tools/ttfparser.py +726 -446
  405. meerk40t/tools/zinglplotter.py +595 -593
  406. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/LICENSE +21 -21
  407. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/METADATA +150 -139
  408. meerk40t-0.9.7010.dist-info/RECORD +445 -0
  409. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/WHEEL +1 -1
  410. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/top_level.txt +0 -1
  411. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.dist-info}/zip-safe +1 -1
  412. meerk40t/balormk/elementlightjob.py +0 -159
  413. meerk40t-0.9.3001.dist-info/RECORD +0 -437
  414. test/bootstrap.py +0 -63
  415. test/test_cli.py +0 -12
  416. test/test_core_cutcode.py +0 -418
  417. test/test_core_elements.py +0 -144
  418. test/test_core_plotplanner.py +0 -397
  419. test/test_core_viewports.py +0 -312
  420. test/test_drivers_grbl.py +0 -108
  421. test/test_drivers_lihuiyu.py +0 -443
  422. test/test_drivers_newly.py +0 -113
  423. test/test_element_degenerate_points.py +0 -43
  424. test/test_elements_classify.py +0 -97
  425. test/test_elements_penbox.py +0 -22
  426. test/test_file_svg.py +0 -176
  427. test/test_fill.py +0 -155
  428. test/test_geomstr.py +0 -1523
  429. test/test_geomstr_nodes.py +0 -18
  430. test/test_imagetools_actualize.py +0 -306
  431. test/test_imagetools_wizard.py +0 -258
  432. test/test_kernel.py +0 -200
  433. test/test_laser_speeds.py +0 -3303
  434. test/test_length.py +0 -57
  435. test/test_lifecycle.py +0 -66
  436. test/test_operations.py +0 -251
  437. test/test_operations_hatch.py +0 -57
  438. test/test_ruida.py +0 -19
  439. test/test_spooler.py +0 -22
  440. test/test_tools_rasterplotter.py +0 -29
  441. test/test_wobble.py +0 -133
  442. test/test_zingl.py +0 -124
  443. {test → meerk40t/cylinder}/__init__.py +0 -0
  444. /meerk40t/{core/element_commands.py → cylinder/gui/__init__.py} +0 -0
  445. {meerk40t-0.9.3001.dist-info → meerk40t-0.9.7010.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