1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Freestyle SVG Exporter",
5 "author": "Folkert de Vries",
8 "location": "Properties > Render > Freestyle SVG Export",
9 "description": "Exports Freestyle's stylized edges in SVG format",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/render_freestyle_svg.html",
12 "support": 'OFFICIAL',
17 import parameter_editor
21 import xml
.etree
.cElementTree
as et
23 from bpy
.app
.handlers
import persistent
24 from collections
import OrderedDict
25 from functools
import partial
26 from mathutils
import Vector
28 from freestyle
.types
import (
35 from freestyle
.utils
import (
43 from freestyle
.functions
import (
47 from freestyle
.predicates
import (
60 QuantitativeInvisibilityUP1D
,
65 from freestyle
.chainingiterators
import ChainPredicateIterator
66 from parameter_editor
import get_dashed_pattern
68 from bpy
.props
import (
75 # use utf-8 here to keep ElementTree happy, end result is utf-16
76 svg_primitive
= """<?xml version="1.0" encoding="ascii" standalone="no"?>
77 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
78 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
79 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
85 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
86 "svg": "http://www.w3.org/2000/svg",
87 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
88 "": "http://www.w3.org/2000/svg",
92 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
93 def find_xml_elem(obj
, search
, namespaces
, *, all
=False):
95 return obj
.findall(search
, namespaces
=namespaces
)
96 return obj
.find(search
, namespaces
=namespaces
)
98 find_svg_elem
= partial(find_xml_elem
, namespaces
=namespaces
)
101 def render_height(scene
):
102 return int(scene
.render
.resolution_y
* scene
.render
.resolution_percentage
/ 100)
105 def render_width(scene
):
106 return int(scene
.render
.resolution_x
* scene
.render
.resolution_percentage
/ 100)
109 def format_rgb(color
):
110 return 'rgb({}, {}, {})'.format(*(int(v
* 255) for v
in color
))
113 # stores the state of the render, used to differ between animation and single frame renders.
116 # Note that this flag is set to False only after the first frame
117 # has been written to file.
122 def render_init(scene
):
123 RenderState
.is_preview
= True
127 def render_write(scene
):
128 RenderState
.is_preview
= False
131 def is_preview_render(scene
):
132 return RenderState
.is_preview
or scene
.svg_export
.mode
== 'FRAME'
135 def create_path(scene
):
136 """Creates the output path for the svg file"""
137 path
= os
.path
.dirname(scene
.render
.frame_path())
138 file_dir_path
= os
.path
.dirname(bpy
.data
.filepath
)
140 # try to use the given path if it is absolute
141 if os
.path
.isabs(path
):
144 # otherwise, use current file's location as a start for the relative path
145 elif bpy
.data
.is_saved
and file_dir_path
:
146 dirname
= os
.path
.normpath(os
.path
.join(file_dir_path
, path
))
148 # otherwise, use the folder from which blender was called as the start
150 dirname
= os
.path
.abspath(bpy
.path
.abspath(path
))
153 basename
= bpy
.path
.basename(scene
.render
.filepath
)
154 if scene
.svg_export
.mode
== 'FRAME':
155 frame
= "{:04d}".format(scene
.frame_current
)
157 frame
= "{:04d}-{:04d}".format(scene
.frame_start
, scene
.frame_end
)
159 return os
.path
.join(dirname
, basename
+ frame
+ ".svg")
162 class SVGExporterLinesetPanel(bpy
.types
.Panel
):
163 """Creates a Panel in the Render Layers context of the properties editor"""
164 bl_idname
= "RENDER_PT_SVGExporterLinesetPanel"
165 bl_space_type
= 'PROPERTIES'
166 bl_label
= "Freestyle Line Style SVG Export"
167 bl_region_type
= 'WINDOW'
168 bl_context
= "view_layer"
170 def draw(self
, context
):
173 scene
= context
.scene
174 svg
= scene
.svg_export
175 freestyle
= context
.window
.view_layer
.freestyle_settings
178 linestyle
= freestyle
.linesets
.active
.linestyle
180 except AttributeError:
181 # Linestyles can be removed, so 0 linestyles is possible.
182 # there is nothing to draw in those cases.
183 # see https://developer.blender.org/T49855
187 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
189 column
= row
.column()
190 column
.prop(linestyle
, 'use_export_strokes')
192 column
= row
.column()
193 column
.active
= svg
.object_fill
194 column
.prop(linestyle
, 'use_export_fills')
197 row
.prop(linestyle
, "stroke_color_mode", expand
=True)
200 class SVGExport(bpy
.types
.PropertyGroup
):
201 """Implements the properties for the SVG exporter"""
202 bl_idname
= "RENDER_PT_svg_export"
204 use_svg_export
: BoolProperty(
206 description
="Export Freestyle edges to an .svg format",
208 split_at_invisible
: BoolProperty(
209 name
="Split at Invisible",
210 description
="Split the stroke at an invisible vertex",
212 object_fill
: BoolProperty(
213 name
="Fill Contours",
214 description
="Fill the contour with the object's material color",
219 ('FRAME', "Frame", "Export a single frame", 0),
220 ('ANIMATION', "Animation", "Export an animation", 1),
224 line_join_type
: EnumProperty(
227 ('MITER', "Miter", "Corners are sharp", 0),
228 ('ROUND', "Round", "Corners are smoothed", 1),
229 ('BEVEL', "Bevel", "Corners are beveled", 2),
235 class SVGExporterPanel(bpy
.types
.Panel
):
236 """Creates a Panel in the render context of the properties editor"""
237 bl_idname
= "RENDER_PT_SVGExporterPanel"
238 bl_space_type
= 'PROPERTIES'
239 bl_label
= "Freestyle SVG Export"
240 bl_region_type
= 'WINDOW'
241 bl_context
= "render"
243 def draw_header(self
, context
):
244 self
.layout
.prop(context
.scene
.svg_export
, "use_svg_export", text
="")
246 def draw(self
, context
):
249 scene
= context
.scene
250 svg
= scene
.svg_export
251 freestyle
= context
.window
.view_layer
.freestyle_settings
253 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
256 row
.prop(svg
, "mode", expand
=True)
259 row
.prop(svg
, "split_at_invisible")
260 row
.prop(svg
, "object_fill")
263 row
.prop(svg
, "line_join_type", expand
=True)
267 def svg_export_header(scene
):
268 if not (scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
):
271 # write the header only for the first frame when animation is being rendered
272 if not is_preview_render(scene
) and scene
.frame_current
!= scene
.frame_start
:
275 # this may fail still. The error is printed to the console.
276 with
open(create_path(scene
), "w") as f
:
277 f
.write(svg_primitive
.format(render_width(scene
), render_height(scene
)))
281 def svg_export_animation(scene
):
282 """makes an animation of the exported SVG file """
283 render
= scene
.render
284 svg
= scene
.svg_export
286 if render
.use_freestyle
and svg
.use_svg_export
and not is_preview_render(scene
):
287 write_animation(create_path(scene
), scene
.frame_start
, render
.fps
)
290 def write_animation(filepath
, frame_begin
, fps
):
291 """Adds animate tags to the specified file."""
292 tree
= et
.parse(filepath
)
293 root
= tree
.getroot()
295 linesets
= find_svg_elem(tree
, ".//svg:g[@inkscape:groupmode='lineset']", all
=True)
296 for i
, lineset
in enumerate(linesets
):
297 name
= lineset
.get('id')
298 frames
= find_svg_elem(lineset
, ".//svg:g[@inkscape:groupmode='frame']", all
=True)
299 n_of_frames
= len(frames
)
300 keyTimes
= ";".join(str(round(x
/ n_of_frames
, 3)) for x
in range(n_of_frames
)) + ";1"
303 'attributeName': 'display',
304 'values': "none;" * (n_of_frames
- 1) + "inline;none",
305 'repeatCount': 'indefinite',
306 'keyTimes': keyTimes
,
307 'dur': "{:.3f}s".format(n_of_frames
/ fps
),
310 for j
, frame
in enumerate(frames
):
311 id = 'anim_{}_{:06n}'.format(name
, j
+ frame_begin
)
313 frame_anim
= et
.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j
- n_of_frames
) / fps
))
314 # add per-lineset style attributes
315 frame_anim
.attrib
.update(style
)
316 # add to the current frame
317 frame
.append(frame_anim
)
321 tree
.write(filepath
, encoding
='ascii', xml_declaration
=True)
324 # - StrokeShaders - #
325 class SVGPathShader(StrokeShader
):
326 """Stroke Shader for writing stroke data to a .svg file."""
327 def __init__(self
, name
, style
, filepath
, res_y
, split_at_invisible
, stroke_color_mode
, frame_current
):
328 StrokeShader
.__init
__(self
)
329 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
331 self
.filepath
= filepath
333 self
.frame_current
= frame_current
335 self
.split_at_invisible
= split_at_invisible
336 self
.stroke_color_mode
= stroke_color_mode
# BASE | FIRST | LAST
341 def from_lineset(cls
, lineset
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
, *, name
=""):
342 """Builds a SVGPathShader using data from the given lineset"""
343 name
= name
or lineset
.name
344 linestyle
= lineset
.linestyle
345 # extract style attributes from the linestyle and scene
346 svg
= getCurrentScene().svg_export
349 'stroke-width': linestyle
.thickness
,
350 'stroke-linecap': linestyle
.caps
.lower(),
351 'stroke-opacity': linestyle
.alpha
,
352 'stroke': format_rgb(linestyle
.color
),
353 'stroke-linejoin': svg
.line_join_type
.lower(),
355 # get dashed line pattern (if specified)
356 if linestyle
.use_dashed_line
:
357 style
['stroke-dasharray'] = ",".join(str(elem
) for elem
in get_dashed_pattern(linestyle
))
359 return cls(name
, style
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
)
363 def pathgen(stroke
, style
, height
, split_at_invisible
, stroke_color_mode
, f
=lambda v
: not v
.attribute
.visible
):
364 """Generator that creates SVG paths (as strings) from the current stroke """
368 if stroke_color_mode
!= 'BASE':
369 # try to use the color of the first or last vertex
371 index
= 0 if stroke_color_mode
== 'FIRST' else -1
372 color
= format_rgb(stroke
[index
].attribute
.color
)
373 style
["stroke"] = color
374 except (ValueError, IndexError):
375 # default is linestyle base color
378 # put style attributes into a single svg path definition
379 path
= '\n<path ' + "".join('{}="{}" '.format(k
, v
) for k
, v
in style
.items()) + 'd=" M '
386 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
387 if split_at_invisible
and v
.attribute
.visible
is False:
388 # end current and start new path;
390 # fast-forward till the next visible vertex
391 it
= itertools
.dropwhile(f
, it
)
392 # yield next visible vertex
393 svert
= next(it
, None)
397 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
401 def shade(self
, stroke
):
402 stroke_to_paths
= "".join(self
.pathgen(stroke
, self
.style
, self
.h
, self
.split_at_invisible
, self
.stroke_color_mode
)).split("\n")
403 # convert to actual XML. Empty strokes are empty strings; they are ignored.
404 self
.elements
.extend(et
.XML(elem
) for elem
in stroke_to_paths
if elem
) # if len(elem.strip()) > len(self.path))
407 """Write SVG data tree to file """
408 tree
= et
.parse(self
.filepath
)
409 root
= tree
.getroot()
411 scene
= bpy
.context
.scene
413 # create <g> for lineset as a whole (don't overwrite)
414 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
415 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(name
))
416 if lineset_group
is None:
417 lineset_group
= et
.XML('<g/>')
418 lineset_group
.attrib
= {
420 'xmlns:inkscape': namespaces
["inkscape"],
421 'inkscape:groupmode': 'lineset',
422 'inkscape:label': name
,
424 root
.append(lineset_group
)
426 # create <g> for the current frame
427 id = "frame_{:04n}".format(self
.frame_current
)
429 stroke_group
= et
.XML("<g/>")
430 stroke_group
.attrib
= {
431 'xmlns:inkscape': namespaces
["inkscape"],
432 'inkscape:groupmode': 'layer',
434 'inkscape:label': 'strokes'
437 stroke_group
.extend(self
.elements
)
438 if scene
.svg_export
.mode
== 'ANIMATION':
439 frame_group
= et
.XML("<g/>")
440 frame_group
.attrib
= {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
441 frame_group
.append(stroke_group
)
442 lineset_group
.append(frame_group
)
444 lineset_group
.append(stroke_group
)
447 print("SVG Export: writing to", self
.filepath
)
449 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
452 class SVGFillBuilder
:
453 def __init__(self
, filepath
, height
, name
):
454 self
.filepath
= filepath
456 self
.stroke_to_fill
= partial(self
.stroke_to_svg
, height
=height
)
459 def pathgen(vertices
, path
, height
):
461 for point
in vertices
:
463 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
464 yield ' z" />' # closes the path; connects the current to the first point
468 def get_merged_strokes(strokes
):
469 def extend_stroke(stroke
, vertices
):
470 for vert
in map(StrokeVertex
, vertices
):
471 stroke
.insert_vertex(vert
, stroke
.stroke_vertices_end())
474 base_strokes
= tuple(stroke
for stroke
in strokes
if not is_poly_clockwise(stroke
))
475 merged_strokes
= OrderedDict((s
, list()) for s
in base_strokes
)
477 for stroke
in filter(is_poly_clockwise
, strokes
):
478 for base
in base_strokes
:
479 # don't merge when diffuse colors don't match
480 if diffuse_from_stroke(stroke
) != diffuse_from_stroke(stroke
):
482 # only merge when the 'hole' is inside the base
483 elif stroke_inside_stroke(stroke
, base
):
484 merged_strokes
[base
].append(stroke
)
486 # if it isn't a hole, it is likely that there are two strokes belonging
487 # to the same object separated by another object. let's try to join them
488 elif (get_object_name(base
) == get_object_name(stroke
) and
489 diffuse_from_stroke(stroke
) == diffuse_from_stroke(stroke
)):
490 base
= extend_stroke(base
, (sv
for sv
in stroke
))
493 # if all else fails, treat this stroke as a base stroke
494 merged_strokes
.update({stroke
: []})
495 return merged_strokes
498 def stroke_to_svg(self
, stroke
, height
, parameters
=None):
499 if parameters
is None:
500 *color
, alpha
= diffuse_from_stroke(stroke
)
501 color
= tuple(int(255 * c
) for c
in color
)
503 'fill_rule': 'evenodd',
505 'fill-opacity': alpha
,
506 'fill': 'rgb' + repr(color
),
508 param_str
= " ".join('{}="{}"'.format(k
, v
) for k
, v
in parameters
.items())
509 path
= '<path {} d=" M '.format(param_str
)
510 vertices
= (svert
.point
for svert
in stroke
)
511 s
= "".join(self
.pathgen(vertices
, path
, height
))
515 def create_fill_elements(self
, strokes
):
516 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
517 merged_strokes
= self
.get_merged_strokes(strokes
)
518 for k
, v
in merged_strokes
.items():
519 base
= self
.stroke_to_fill(k
)
520 fills
= (self
.stroke_to_fill(stroke
).get("d") for stroke
in v
)
521 merged_points
= " ".join(fills
)
522 base
.attrib
['d'] += merged_points
525 def write(self
, strokes
):
526 """Write SVG data tree to file """
528 tree
= et
.parse(self
.filepath
)
529 root
= tree
.getroot()
530 scene
= bpy
.context
.scene
533 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(self
._name
))
534 if lineset_group
is None:
535 lineset_group
= et
.XML('<g/>')
536 lineset_group
.attrib
= {
538 'xmlns:inkscape': namespaces
["inkscape"],
539 'inkscape:groupmode': 'lineset',
540 'inkscape:label': name
,
542 root
.append(lineset_group
)
543 print('added new lineset group ', name
)
546 # <g> for the fills of the current frame
547 fill_group
= et
.XML('<g/>')
548 fill_group
.attrib
= {
549 'xmlns:inkscape': namespaces
["inkscape"],
550 'inkscape:groupmode': 'layer',
551 'inkscape:label': 'fills',
555 fill_elements
= self
.create_fill_elements(strokes
)
556 fill_group
.extend(reversed(tuple(fill_elements
)))
557 if scene
.svg_export
.mode
== 'ANIMATION':
558 # add the fills to the <g> of the current frame
559 frame_group
= find_svg_elem(lineset_group
, ".//svg:g[@id='frame_{:04n}']".format(scene
.frame_current
))
560 frame_group
.insert(0, fill_group
)
562 lineset_group
.insert(0, fill_group
)
566 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
569 def stroke_inside_stroke(a
, b
):
570 box_a
= BoundingBox
.from_sequence(svert
.point
for svert
in a
)
571 box_b
= BoundingBox
.from_sequence(svert
.point
for svert
in b
)
572 return box_a
.inside(box_b
)
575 def diffuse_from_stroke(stroke
, curvemat
=CurveMaterialF0D()):
576 material
= curvemat(Interface0DIterator(stroke
))
577 return material
.diffuse
580 class ParameterEditorCallback(object):
581 """Object to store callbacks for the Parameter Editor in"""
582 def lineset_pre(self
, scene
, layer
, lineset
):
583 raise NotImplementedError()
585 def modifier_post(self
, scene
, layer
, lineset
):
586 raise NotImplementedError()
588 def lineset_post(self
, scene
, layer
, lineset
):
589 raise NotImplementedError()
593 class SVGPathShaderCallback(ParameterEditorCallback
):
595 def poll(cls
, scene
, linestyle
):
596 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and linestyle
.use_export_strokes
599 def modifier_post(cls
, scene
, layer
, lineset
):
600 if not cls
.poll(scene
, lineset
.linestyle
):
603 split
= scene
.svg_export
.split_at_invisible
604 stroke_color_mode
= lineset
.linestyle
.stroke_color_mode
605 cls
.shader
= SVGPathShader
.from_lineset(
606 lineset
, create_path(scene
),
607 render_height(scene
), split
, stroke_color_mode
, scene
.frame_current
, name
=layer
.name
+ '_' + lineset
.name
)
611 def lineset_post(cls
, scene
, layer
, lineset
):
612 if not cls
.poll(scene
, lineset
.linestyle
):
617 class SVGFillShaderCallback(ParameterEditorCallback
):
619 def poll(cls
, scene
, linestyle
):
620 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and scene
.svg_export
.object_fill
and linestyle
.use_export_fills
623 def lineset_post(cls
, scene
, layer
, lineset
):
624 if not cls
.poll(scene
, lineset
.linestyle
):
627 # reset the stroke selection (but don't delete the already generated strokes)
628 Operators
.reset(delete_strokes
=False)
629 # Unary Predicates: visible and correct edge nature
631 QuantitativeInvisibilityUP1D(0),
632 OrUP1D(ExternalContourUP1D(),
633 pyNatureUP1D(Nature
.BORDER
)),
635 # select the new edges
636 Operators
.select(upred
)
640 NotBP1D(pyZDiscontinuityBP1D()),
642 bpred
= OrBP1D(bpred
, AndBP1D(NotBP1D(bpred
), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
644 Operators
.bidirectional_chain(ChainPredicateIterator(upred
, bpred
))
646 collector
= StrokeCollector()
647 Operators
.create(TrueUP1D(), [collector
])
649 builder
= SVGFillBuilder(create_path(scene
), render_height(scene
), layer
.name
+ '_' + lineset
.name
)
650 builder
.write(collector
.strokes
)
651 # make strokes used for filling invisible
652 for stroke
in collector
.strokes
:
654 svert
.attribute
.visible
= False
658 def indent_xml(elem
, level
=0, indentsize
=4):
659 """Prettifies XML code (used in SVG exporter) """
660 i
= "\n" + level
* " " * indentsize
662 if not elem
.text
or not elem
.text
.strip():
663 elem
.text
= i
+ " " * indentsize
664 if not elem
.tail
or not elem
.tail
.strip():
667 indent_xml(elem
, level
+ 1)
668 if not elem
.tail
or not elem
.tail
.strip():
670 elif level
and (not elem
.tail
or not elem
.tail
.strip()):
674 def register_namespaces(namespaces
=namespaces
):
675 for name
, url
in namespaces
.items():
676 if name
!= 'svg': # creates invalid xml
677 et
.register_namespace(name
, url
)
680 def handle_versions(self
):
681 # We don't modify startup file because it assumes to
682 # have all the default values only.
683 if not bpy
.data
.is_saved
:
686 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
687 # changed the default for fills.
688 # fix by Sergey https://developer.blender.org/T46150
689 if bpy
.data
.version
<= (2, 76, 0):
690 for linestyle
in bpy
.data
.linestyles
:
691 linestyle
.use_export_fills
= True
697 SVGExporterLinesetPanel
,
703 linestyle
= bpy
.types
.FreestyleLineStyle
704 linestyle
.use_export_strokes
= BoolProperty(
705 name
="Export Strokes",
706 description
="Export strokes for this Line Style",
709 linestyle
.stroke_color_mode
= EnumProperty(
710 name
="Stroke Color Mode",
712 ('BASE', "Base Color", "Use the linestyle's base color", 0),
713 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
714 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
718 linestyle
.use_export_fills
= BoolProperty(
720 description
="Export fills for this Line Style",
725 bpy
.utils
.register_class(cls
)
726 bpy
.types
.Scene
.svg_export
= PointerProperty(type=SVGExport
)
730 bpy
.app
.handlers
.render_init
.append(render_init
)
731 bpy
.app
.handlers
.render_write
.append(render_write
)
732 bpy
.app
.handlers
.render_pre
.append(svg_export_header
)
733 bpy
.app
.handlers
.render_complete
.append(svg_export_animation
)
735 # manipulate shaders list
736 parameter_editor
.callbacks_modifiers_post
.append(SVGPathShaderCallback
.modifier_post
)
737 parameter_editor
.callbacks_lineset_post
.append(SVGPathShaderCallback
.lineset_post
)
738 parameter_editor
.callbacks_lineset_post
.append(SVGFillShaderCallback
.lineset_post
)
740 # register namespaces
741 register_namespaces()
744 bpy
.app
.handlers
.version_update
.append(handle_versions
)
750 bpy
.utils
.unregister_class(cls
)
751 del bpy
.types
.Scene
.svg_export
752 linestyle
= bpy
.types
.FreestyleLineStyle
753 del linestyle
.use_export_strokes
754 del linestyle
.use_export_fills
757 bpy
.app
.handlers
.render_init
.remove(render_init
)
758 bpy
.app
.handlers
.render_write
.remove(render_write
)
759 bpy
.app
.handlers
.render_pre
.remove(svg_export_header
)
760 bpy
.app
.handlers
.render_complete
.remove(svg_export_animation
)
762 # manipulate shaders list
763 parameter_editor
.callbacks_modifiers_post
.remove(SVGPathShaderCallback
.modifier_post
)
764 parameter_editor
.callbacks_lineset_post
.remove(SVGPathShaderCallback
.lineset_post
)
765 parameter_editor
.callbacks_lineset_post
.remove(SVGFillShaderCallback
.lineset_post
)
767 bpy
.app
.handlers
.version_update
.remove(handle_versions
)
770 if __name__
== "__main__":