1 # SPDX-FileCopyrightText: 2014-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Freestyle SVG Exporter",
7 "author": "Folkert de Vries",
10 "location": "Properties > Render > Freestyle SVG Export",
11 "description": "Exports Freestyle's stylized edges in SVG format",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/render/render_freestyle_svg.html",
14 "support": 'OFFICIAL',
19 import parameter_editor
23 import xml
.etree
.cElementTree
as et
25 from bpy
.app
.handlers
import persistent
26 from collections
import OrderedDict
27 from functools
import partial
28 from mathutils
import Vector
30 from freestyle
.types
import (
37 from freestyle
.utils
import (
45 from freestyle
.functions
import (
49 from freestyle
.predicates
import (
62 QuantitativeInvisibilityUP1D
,
67 from freestyle
.chainingiterators
import ChainPredicateIterator
68 from parameter_editor
import get_dashed_pattern
70 from bpy
.props
import (
77 # use utf-8 here to keep ElementTree happy, end result is utf-16
78 svg_primitive
= """<?xml version="1.0" encoding="ascii" standalone="no"?>
79 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
80 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
81 <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{:d}" height="{:d}">
87 "inkscape": "http://www.inkscape.org/namespaces/inkscape",
88 "svg": "http://www.w3.org/2000/svg",
89 "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
90 "": "http://www.w3.org/2000/svg",
94 # wrap XMLElem.find, so the namespaces don't need to be given as an argument
95 def find_xml_elem(obj
, search
, namespaces
, *, all
=False):
97 return obj
.findall(search
, namespaces
=namespaces
)
98 return obj
.find(search
, namespaces
=namespaces
)
100 find_svg_elem
= partial(find_xml_elem
, namespaces
=namespaces
)
103 def render_height(scene
):
104 return int(scene
.render
.resolution_y
* scene
.render
.resolution_percentage
/ 100)
107 def render_width(scene
):
108 return int(scene
.render
.resolution_x
* scene
.render
.resolution_percentage
/ 100)
111 def format_rgb(color
):
112 return 'rgb({}, {}, {})'.format(*(int(v
* 255) for v
in color
))
115 # stores the state of the render, used to differ between animation and single frame renders.
118 # Note that this flag is set to False only after the first frame
119 # has been written to file.
124 def render_init(scene
):
125 RenderState
.is_preview
= True
129 def render_write(scene
):
130 RenderState
.is_preview
= False
133 def is_preview_render(scene
):
134 return RenderState
.is_preview
or scene
.svg_export
.mode
== 'FRAME'
137 def create_path(scene
):
138 """Creates the output path for the svg file"""
139 path
= os
.path
.dirname(scene
.render
.frame_path())
140 file_dir_path
= os
.path
.dirname(bpy
.data
.filepath
)
142 # try to use the given path if it is absolute
143 if os
.path
.isabs(path
):
146 # otherwise, use current file's location as a start for the relative path
147 elif bpy
.data
.is_saved
and file_dir_path
:
148 dirname
= os
.path
.normpath(os
.path
.join(file_dir_path
, path
))
150 # otherwise, use the folder from which blender was called as the start
152 dirname
= os
.path
.abspath(bpy
.path
.abspath(path
))
155 basename
= bpy
.path
.basename(scene
.render
.filepath
)
156 if scene
.svg_export
.mode
== 'FRAME':
157 frame
= "{:04d}".format(scene
.frame_current
)
159 frame
= "{:04d}-{:04d}".format(scene
.frame_start
, scene
.frame_end
)
161 os
.makedirs(dirname
, exist_ok
=True)
163 return os
.path
.join(dirname
, basename
+ frame
+ ".svg")
166 class SVGExporterLinesetPanel(bpy
.types
.Panel
):
167 """Creates a panel in the View Layer context of the properties editor"""
168 bl_idname
= "RENDER_PT_SVGExporterLinesetPanel"
169 bl_space_type
= 'PROPERTIES'
170 bl_label
= "Freestyle Line Style SVG Export"
171 bl_region_type
= 'WINDOW'
172 bl_context
= "view_layer"
174 def draw(self
, context
):
177 scene
= context
.scene
178 svg
= scene
.svg_export
179 freestyle
= context
.window
.view_layer
.freestyle_settings
182 linestyle
= freestyle
.linesets
.active
.linestyle
184 except AttributeError:
185 # Linestyles can be removed, so 0 linestyles is possible.
186 # there is nothing to draw in those cases.
187 # see https://developer.blender.org/T49855
191 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
193 column
= row
.column()
194 column
.prop(linestyle
, 'use_export_strokes')
196 column
= row
.column()
197 column
.active
= svg
.object_fill
198 column
.prop(linestyle
, 'use_export_fills')
201 row
.prop(linestyle
, "stroke_color_mode", expand
=True)
204 class SVGExport(bpy
.types
.PropertyGroup
):
205 """Implements the properties for the SVG exporter"""
206 bl_idname
= "RENDER_PT_svg_export"
208 use_svg_export
: BoolProperty(
210 description
="Export Freestyle edges to an .svg format",
212 split_at_invisible
: BoolProperty(
213 name
="Split at Invisible",
214 description
="Split the stroke at an invisible vertex",
216 object_fill
: BoolProperty(
217 name
="Fill Contours",
218 description
="Fill the contour with the object's material color",
223 ('FRAME', "Frame", "Export a single frame", 0),
224 ('ANIMATION', "Animation", "Export an animation", 1),
228 line_join_type
: EnumProperty(
231 ('MITER', "Miter", "Corners are sharp", 0),
232 ('ROUND', "Round", "Corners are smoothed", 1),
233 ('BEVEL', "Bevel", "Corners are beveled", 2),
239 class SVGExporterPanel(bpy
.types
.Panel
):
240 """Creates a Panel in the render context of the properties editor"""
241 bl_idname
= "RENDER_PT_SVGExporterPanel"
242 bl_space_type
= 'PROPERTIES'
243 bl_label
= "Freestyle SVG Export"
244 bl_region_type
= 'WINDOW'
245 bl_context
= "render"
247 def draw_header(self
, context
):
248 self
.layout
.prop(context
.scene
.svg_export
, "use_svg_export", text
="")
250 def draw(self
, context
):
253 scene
= context
.scene
254 svg
= scene
.svg_export
255 freestyle
= context
.window
.view_layer
.freestyle_settings
257 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
260 row
.prop(svg
, "mode", expand
=True)
263 row
.prop(svg
, "split_at_invisible")
264 row
.prop(svg
, "object_fill")
267 row
.prop(svg
, "line_join_type", expand
=True)
271 def svg_export_header(scene
):
272 if not (scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
):
275 # write the header only for the first frame when animation is being rendered
276 if not is_preview_render(scene
) and scene
.frame_current
!= scene
.frame_start
:
279 # this may fail still. The error is printed to the console.
280 with
open(create_path(scene
), "w") as f
:
281 f
.write(svg_primitive
.format(render_width(scene
), render_height(scene
)))
285 def svg_export_animation(scene
):
286 """makes an animation of the exported SVG file """
287 render
= scene
.render
288 svg
= scene
.svg_export
290 if render
.use_freestyle
and svg
.use_svg_export
and not is_preview_render(scene
):
291 write_animation(create_path(scene
), scene
.frame_start
, render
.fps
)
294 def write_animation(filepath
, frame_begin
, fps
):
295 """Adds animate tags to the specified file."""
296 tree
= et
.parse(filepath
)
297 root
= tree
.getroot()
299 linesets
= find_svg_elem(tree
, ".//svg:g[@inkscape:groupmode='lineset']", all
=True)
300 for i
, lineset
in enumerate(linesets
):
301 name
= lineset
.get('id')
302 frames
= find_svg_elem(lineset
, ".//svg:g[@inkscape:groupmode='frame']", all
=True)
303 n_of_frames
= len(frames
)
304 keyTimes
= ";".join(str(round(x
/ n_of_frames
, 3)) for x
in range(n_of_frames
)) + ";1"
307 'attributeName': 'display',
308 'values': "none;" * (n_of_frames
- 1) + "inline;none",
309 'repeatCount': 'indefinite',
310 'keyTimes': keyTimes
,
311 'dur': "{:.3f}s".format(n_of_frames
/ fps
),
314 for j
, frame
in enumerate(frames
):
315 id = 'anim_{}_{:06n}'.format(name
, j
+ frame_begin
)
317 frame_anim
= et
.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j
- n_of_frames
) / fps
))
318 # add per-lineset style attributes
319 frame_anim
.attrib
.update(style
)
320 # add to the current frame
321 frame
.append(frame_anim
)
325 tree
.write(filepath
, encoding
='ascii', xml_declaration
=True)
328 # - StrokeShaders - #
329 class SVGPathShader(StrokeShader
):
330 """Stroke Shader for writing stroke data to a .svg file."""
331 def __init__(self
, name
, style
, filepath
, res_y
, split_at_invisible
, stroke_color_mode
, frame_current
):
332 StrokeShader
.__init
__(self
)
333 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
335 self
.filepath
= filepath
337 self
.frame_current
= frame_current
339 self
.split_at_invisible
= split_at_invisible
340 self
.stroke_color_mode
= stroke_color_mode
# BASE | FIRST | LAST
345 def from_lineset(cls
, lineset
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
, *, name
=""):
346 """Builds a SVGPathShader using data from the given lineset"""
347 name
= name
or lineset
.name
348 linestyle
= lineset
.linestyle
349 # extract style attributes from the linestyle and scene
350 svg
= getCurrentScene().svg_export
353 'stroke-width': linestyle
.thickness
,
354 'stroke-linecap': linestyle
.caps
.lower(),
355 'stroke-opacity': linestyle
.alpha
,
356 'stroke': format_rgb(linestyle
.color
),
357 'stroke-linejoin': svg
.line_join_type
.lower(),
359 # get dashed line pattern (if specified)
360 if linestyle
.use_dashed_line
:
361 style
['stroke-dasharray'] = ",".join(str(elem
) for elem
in get_dashed_pattern(linestyle
))
363 return cls(name
, style
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
)
367 def pathgen(stroke
, style
, height
, split_at_invisible
, stroke_color_mode
, f
=lambda v
: not v
.attribute
.visible
):
368 """Generator that creates SVG paths (as strings) from the current stroke """
372 if stroke_color_mode
!= 'BASE':
373 # try to use the color of the first or last vertex
375 index
= 0 if stroke_color_mode
== 'FIRST' else -1
376 color
= format_rgb(stroke
[index
].attribute
.color
)
377 style
["stroke"] = color
378 except (ValueError, IndexError):
379 # default is linestyle base color
382 # put style attributes into a single svg path definition
383 path
= '\n<path ' + "".join('{}="{}" '.format(k
, v
) for k
, v
in style
.items()) + 'd=" M '
390 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
391 if split_at_invisible
and v
.attribute
.visible
is False:
392 # end current and start new path;
394 # fast-forward till the next visible vertex
395 it
= itertools
.dropwhile(f
, it
)
396 # yield next visible vertex
397 svert
= next(it
, None)
401 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
405 def shade(self
, stroke
):
406 stroke_to_paths
= "".join(self
.pathgen(stroke
, self
.style
, self
.h
, self
.split_at_invisible
, self
.stroke_color_mode
)).split("\n")
407 # convert to actual XML. Empty strokes are empty strings; they are ignored.
408 self
.elements
.extend(et
.XML(elem
) for elem
in stroke_to_paths
if elem
) # if len(elem.strip()) > len(self.path))
411 """Write SVG data tree to file """
412 tree
= et
.parse(self
.filepath
)
413 root
= tree
.getroot()
415 scene
= bpy
.context
.scene
417 # create <g> for lineset as a whole (don't overwrite)
418 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
419 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(name
))
420 if lineset_group
is None:
421 lineset_group
= et
.XML('<g/>')
422 lineset_group
.attrib
= {
424 'xmlns:inkscape': namespaces
["inkscape"],
425 'inkscape:groupmode': 'lineset',
426 'inkscape:label': name
,
428 root
.append(lineset_group
)
430 # create <g> for the current frame
431 id = "frame_{:04n}".format(self
.frame_current
)
433 stroke_group
= et
.XML("<g/>")
434 stroke_group
.attrib
= {
435 'xmlns:inkscape': namespaces
["inkscape"],
436 'inkscape:groupmode': 'layer',
438 'inkscape:label': 'strokes'
441 stroke_group
.extend(self
.elements
)
442 if scene
.svg_export
.mode
== 'ANIMATION':
443 frame_group
= et
.XML("<g/>")
444 frame_group
.attrib
= {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
445 frame_group
.append(stroke_group
)
446 lineset_group
.append(frame_group
)
448 lineset_group
.append(stroke_group
)
451 print("SVG Export: writing to", self
.filepath
)
453 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
456 class SVGFillBuilder
:
457 def __init__(self
, filepath
, height
, name
):
458 self
.filepath
= filepath
460 self
.stroke_to_fill
= partial(self
.stroke_to_svg
, height
=height
)
463 def pathgen(vertices
, path
, height
):
465 for point
in vertices
:
467 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
468 yield ' z" />' # closes the path; connects the current to the first point
472 def get_merged_strokes(strokes
):
473 def extend_stroke(stroke
, vertices
):
474 for vert
in map(StrokeVertex
, vertices
):
475 stroke
.insert_vertex(vert
, stroke
.stroke_vertices_end())
478 base_strokes
= tuple(stroke
for stroke
in strokes
if not is_poly_clockwise(stroke
))
479 merged_strokes
= OrderedDict((s
, list()) for s
in base_strokes
)
481 for stroke
in filter(is_poly_clockwise
, strokes
):
482 for base
in base_strokes
:
483 # don't merge when diffuse colors don't match
484 if diffuse_from_stroke(stroke
) != diffuse_from_stroke(stroke
):
486 # only merge when the 'hole' is inside the base
487 elif stroke_inside_stroke(stroke
, base
):
488 merged_strokes
[base
].append(stroke
)
490 # if it isn't a hole, it is likely that there are two strokes belonging
491 # to the same object separated by another object. let's try to join them
492 elif (get_object_name(base
) == get_object_name(stroke
) and
493 diffuse_from_stroke(stroke
) == diffuse_from_stroke(stroke
)):
494 base
= extend_stroke(base
, (sv
for sv
in stroke
))
497 # if all else fails, treat this stroke as a base stroke
498 merged_strokes
.update({stroke
: []})
499 return merged_strokes
502 def stroke_to_svg(self
, stroke
, height
, parameters
=None):
503 if parameters
is None:
504 *color
, alpha
= diffuse_from_stroke(stroke
)
505 color
= tuple(int(255 * c
) for c
in color
)
507 'fill_rule': 'evenodd',
509 'fill-opacity': alpha
,
510 'fill': 'rgb' + repr(color
),
512 param_str
= " ".join('{}="{}"'.format(k
, v
) for k
, v
in parameters
.items())
513 path
= '<path {} d=" M '.format(param_str
)
514 vertices
= (svert
.point
for svert
in stroke
)
515 s
= "".join(self
.pathgen(vertices
, path
, height
))
519 def create_fill_elements(self
, strokes
):
520 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
521 merged_strokes
= self
.get_merged_strokes(strokes
)
522 for k
, v
in merged_strokes
.items():
523 base
= self
.stroke_to_fill(k
)
524 fills
= (self
.stroke_to_fill(stroke
).get("d") for stroke
in v
)
525 merged_points
= " ".join(fills
)
526 base
.attrib
['d'] += merged_points
529 def write(self
, strokes
):
530 """Write SVG data tree to file """
532 tree
= et
.parse(self
.filepath
)
533 root
= tree
.getroot()
534 scene
= bpy
.context
.scene
537 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(self
._name
))
538 if lineset_group
is None:
539 lineset_group
= et
.XML('<g/>')
540 lineset_group
.attrib
= {
542 'xmlns:inkscape': namespaces
["inkscape"],
543 'inkscape:groupmode': 'lineset',
544 'inkscape:label': name
,
546 root
.append(lineset_group
)
547 print('added new lineset group ', name
)
550 # <g> for the fills of the current frame
551 fill_group
= et
.XML('<g/>')
552 fill_group
.attrib
= {
553 'xmlns:inkscape': namespaces
["inkscape"],
554 'inkscape:groupmode': 'layer',
555 'inkscape:label': 'fills',
559 fill_elements
= self
.create_fill_elements(strokes
)
560 fill_group
.extend(reversed(tuple(fill_elements
)))
561 if scene
.svg_export
.mode
== 'ANIMATION':
562 # add the fills to the <g> of the current frame
563 frame_group
= find_svg_elem(lineset_group
, ".//svg:g[@id='frame_{:04n}']".format(scene
.frame_current
))
564 frame_group
.insert(0, fill_group
)
566 lineset_group
.insert(0, fill_group
)
570 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
573 def stroke_inside_stroke(a
, b
):
574 box_a
= BoundingBox
.from_sequence(svert
.point
for svert
in a
)
575 box_b
= BoundingBox
.from_sequence(svert
.point
for svert
in b
)
576 return box_a
.inside(box_b
)
579 def diffuse_from_stroke(stroke
, curvemat
=CurveMaterialF0D()):
580 material
= curvemat(Interface0DIterator(stroke
))
581 return material
.diffuse
584 class ParameterEditorCallback(object):
585 """Object to store callbacks for the Parameter Editor in"""
586 def lineset_pre(self
, scene
, layer
, lineset
):
587 raise NotImplementedError()
589 def modifier_post(self
, scene
, layer
, lineset
):
590 raise NotImplementedError()
592 def lineset_post(self
, scene
, layer
, lineset
):
593 raise NotImplementedError()
597 class SVGPathShaderCallback(ParameterEditorCallback
):
599 def poll(cls
, scene
, linestyle
):
600 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and linestyle
.use_export_strokes
603 def modifier_post(cls
, scene
, layer
, lineset
):
604 if not cls
.poll(scene
, lineset
.linestyle
):
607 split
= scene
.svg_export
.split_at_invisible
608 stroke_color_mode
= lineset
.linestyle
.stroke_color_mode
609 cls
.shader
= SVGPathShader
.from_lineset(
610 lineset
, create_path(scene
),
611 render_height(scene
), split
, stroke_color_mode
, scene
.frame_current
, name
=layer
.name
+ '_' + lineset
.name
)
615 def lineset_post(cls
, scene
, layer
, lineset
):
616 if not cls
.poll(scene
, lineset
.linestyle
):
621 class SVGFillShaderCallback(ParameterEditorCallback
):
623 def poll(cls
, scene
, linestyle
):
624 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and scene
.svg_export
.object_fill
and linestyle
.use_export_fills
627 def lineset_post(cls
, scene
, layer
, lineset
):
628 if not cls
.poll(scene
, lineset
.linestyle
):
631 # reset the stroke selection (but don't delete the already generated strokes)
632 Operators
.reset(delete_strokes
=False)
633 # Unary Predicates: visible and correct edge nature
635 QuantitativeInvisibilityUP1D(0),
636 OrUP1D(ExternalContourUP1D(),
637 pyNatureUP1D(Nature
.BORDER
)),
639 # select the new edges
640 Operators
.select(upred
)
644 NotBP1D(pyZDiscontinuityBP1D()),
646 bpred
= OrBP1D(bpred
, AndBP1D(NotBP1D(bpred
), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
648 Operators
.bidirectional_chain(ChainPredicateIterator(upred
, bpred
))
650 collector
= StrokeCollector()
651 Operators
.create(TrueUP1D(), [collector
])
653 builder
= SVGFillBuilder(create_path(scene
), render_height(scene
), layer
.name
+ '_' + lineset
.name
)
654 builder
.write(collector
.strokes
)
655 # make strokes used for filling invisible
656 for stroke
in collector
.strokes
:
658 svert
.attribute
.visible
= False
662 def indent_xml(elem
, level
=0, indentsize
=4):
663 """Prettifies XML code (used in SVG exporter) """
664 i
= "\n" + level
* " " * indentsize
666 if not elem
.text
or not elem
.text
.strip():
667 elem
.text
= i
+ " " * indentsize
668 if not elem
.tail
or not elem
.tail
.strip():
671 indent_xml(elem
, level
+ 1)
672 if not elem
.tail
or not elem
.tail
.strip():
674 elif level
and (not elem
.tail
or not elem
.tail
.strip()):
678 def register_namespaces(namespaces
=namespaces
):
679 for name
, url
in namespaces
.items():
680 if name
!= 'svg': # creates invalid xml
681 et
.register_namespace(name
, url
)
684 def handle_versions(self
):
685 # We don't modify startup file because it assumes to
686 # have all the default values only.
687 if not bpy
.data
.is_saved
:
690 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
691 # changed the default for fills.
692 # fix by Sergey https://developer.blender.org/T46150
693 if bpy
.data
.version
<= (2, 76, 0):
694 for linestyle
in bpy
.data
.linestyles
:
695 linestyle
.use_export_fills
= True
701 SVGExporterLinesetPanel
,
707 linestyle
= bpy
.types
.FreestyleLineStyle
708 linestyle
.use_export_strokes
= BoolProperty(
709 name
="Export Strokes",
710 description
="Export strokes for this Line Style",
713 linestyle
.stroke_color_mode
= EnumProperty(
714 name
="Stroke Color Mode",
716 ('BASE', "Base Color", "Use the linestyle's base color", 0),
717 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
718 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
722 linestyle
.use_export_fills
= BoolProperty(
724 description
="Export fills for this Line Style",
729 bpy
.utils
.register_class(cls
)
730 bpy
.types
.Scene
.svg_export
= PointerProperty(type=SVGExport
)
734 bpy
.app
.handlers
.render_init
.append(render_init
)
735 bpy
.app
.handlers
.render_write
.append(render_write
)
736 bpy
.app
.handlers
.render_pre
.append(svg_export_header
)
737 bpy
.app
.handlers
.render_complete
.append(svg_export_animation
)
739 # manipulate shaders list
740 parameter_editor
.callbacks_modifiers_post
.append(SVGPathShaderCallback
.modifier_post
)
741 parameter_editor
.callbacks_lineset_post
.append(SVGPathShaderCallback
.lineset_post
)
742 parameter_editor
.callbacks_lineset_post
.append(SVGFillShaderCallback
.lineset_post
)
744 # register namespaces
745 register_namespaces()
748 bpy
.app
.handlers
.version_update
.append(handle_versions
)
754 bpy
.utils
.unregister_class(cls
)
755 del bpy
.types
.Scene
.svg_export
756 linestyle
= bpy
.types
.FreestyleLineStyle
757 del linestyle
.use_export_strokes
758 del linestyle
.use_export_fills
761 bpy
.app
.handlers
.render_init
.remove(render_init
)
762 bpy
.app
.handlers
.render_write
.remove(render_write
)
763 bpy
.app
.handlers
.render_pre
.remove(svg_export_header
)
764 bpy
.app
.handlers
.render_complete
.remove(svg_export_animation
)
766 # manipulate shaders list
767 parameter_editor
.callbacks_modifiers_post
.remove(SVGPathShaderCallback
.modifier_post
)
768 parameter_editor
.callbacks_lineset_post
.remove(SVGPathShaderCallback
.lineset_post
)
769 parameter_editor
.callbacks_lineset_post
.remove(SVGFillShaderCallback
.lineset_post
)
771 bpy
.app
.handlers
.version_update
.remove(handle_versions
)
774 if __name__
== "__main__":