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 return os
.path
.join(dirname
, basename
+ frame
+ ".svg")
164 class SVGExporterLinesetPanel(bpy
.types
.Panel
):
165 """Creates a Panel in the Render Layers context of the properties editor"""
166 bl_idname
= "RENDER_PT_SVGExporterLinesetPanel"
167 bl_space_type
= 'PROPERTIES'
168 bl_label
= "Freestyle Line Style SVG Export"
169 bl_region_type
= 'WINDOW'
170 bl_context
= "view_layer"
172 def draw(self
, context
):
175 scene
= context
.scene
176 svg
= scene
.svg_export
177 freestyle
= context
.window
.view_layer
.freestyle_settings
180 linestyle
= freestyle
.linesets
.active
.linestyle
182 except AttributeError:
183 # Linestyles can be removed, so 0 linestyles is possible.
184 # there is nothing to draw in those cases.
185 # see https://developer.blender.org/T49855
189 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
191 column
= row
.column()
192 column
.prop(linestyle
, 'use_export_strokes')
194 column
= row
.column()
195 column
.active
= svg
.object_fill
196 column
.prop(linestyle
, 'use_export_fills')
199 row
.prop(linestyle
, "stroke_color_mode", expand
=True)
202 class SVGExport(bpy
.types
.PropertyGroup
):
203 """Implements the properties for the SVG exporter"""
204 bl_idname
= "RENDER_PT_svg_export"
206 use_svg_export
: BoolProperty(
208 description
="Export Freestyle edges to an .svg format",
210 split_at_invisible
: BoolProperty(
211 name
="Split at Invisible",
212 description
="Split the stroke at an invisible vertex",
214 object_fill
: BoolProperty(
215 name
="Fill Contours",
216 description
="Fill the contour with the object's material color",
221 ('FRAME', "Frame", "Export a single frame", 0),
222 ('ANIMATION', "Animation", "Export an animation", 1),
226 line_join_type
: EnumProperty(
229 ('MITER', "Miter", "Corners are sharp", 0),
230 ('ROUND', "Round", "Corners are smoothed", 1),
231 ('BEVEL', "Bevel", "Corners are beveled", 2),
237 class SVGExporterPanel(bpy
.types
.Panel
):
238 """Creates a Panel in the render context of the properties editor"""
239 bl_idname
= "RENDER_PT_SVGExporterPanel"
240 bl_space_type
= 'PROPERTIES'
241 bl_label
= "Freestyle SVG Export"
242 bl_region_type
= 'WINDOW'
243 bl_context
= "render"
245 def draw_header(self
, context
):
246 self
.layout
.prop(context
.scene
.svg_export
, "use_svg_export", text
="")
248 def draw(self
, context
):
251 scene
= context
.scene
252 svg
= scene
.svg_export
253 freestyle
= context
.window
.view_layer
.freestyle_settings
255 layout
.active
= (svg
.use_svg_export
and freestyle
.mode
!= 'SCRIPT')
258 row
.prop(svg
, "mode", expand
=True)
261 row
.prop(svg
, "split_at_invisible")
262 row
.prop(svg
, "object_fill")
265 row
.prop(svg
, "line_join_type", expand
=True)
269 def svg_export_header(scene
):
270 if not (scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
):
273 # write the header only for the first frame when animation is being rendered
274 if not is_preview_render(scene
) and scene
.frame_current
!= scene
.frame_start
:
277 # this may fail still. The error is printed to the console.
278 with
open(create_path(scene
), "w") as f
:
279 f
.write(svg_primitive
.format(render_width(scene
), render_height(scene
)))
283 def svg_export_animation(scene
):
284 """makes an animation of the exported SVG file """
285 render
= scene
.render
286 svg
= scene
.svg_export
288 if render
.use_freestyle
and svg
.use_svg_export
and not is_preview_render(scene
):
289 write_animation(create_path(scene
), scene
.frame_start
, render
.fps
)
292 def write_animation(filepath
, frame_begin
, fps
):
293 """Adds animate tags to the specified file."""
294 tree
= et
.parse(filepath
)
295 root
= tree
.getroot()
297 linesets
= find_svg_elem(tree
, ".//svg:g[@inkscape:groupmode='lineset']", all
=True)
298 for i
, lineset
in enumerate(linesets
):
299 name
= lineset
.get('id')
300 frames
= find_svg_elem(lineset
, ".//svg:g[@inkscape:groupmode='frame']", all
=True)
301 n_of_frames
= len(frames
)
302 keyTimes
= ";".join(str(round(x
/ n_of_frames
, 3)) for x
in range(n_of_frames
)) + ";1"
305 'attributeName': 'display',
306 'values': "none;" * (n_of_frames
- 1) + "inline;none",
307 'repeatCount': 'indefinite',
308 'keyTimes': keyTimes
,
309 'dur': "{:.3f}s".format(n_of_frames
/ fps
),
312 for j
, frame
in enumerate(frames
):
313 id = 'anim_{}_{:06n}'.format(name
, j
+ frame_begin
)
315 frame_anim
= et
.XML('<animate id="{}" begin="{:.3f}s" />'.format(id, (j
- n_of_frames
) / fps
))
316 # add per-lineset style attributes
317 frame_anim
.attrib
.update(style
)
318 # add to the current frame
319 frame
.append(frame_anim
)
323 tree
.write(filepath
, encoding
='ascii', xml_declaration
=True)
326 # - StrokeShaders - #
327 class SVGPathShader(StrokeShader
):
328 """Stroke Shader for writing stroke data to a .svg file."""
329 def __init__(self
, name
, style
, filepath
, res_y
, split_at_invisible
, stroke_color_mode
, frame_current
):
330 StrokeShader
.__init
__(self
)
331 # attribute 'name' of 'StrokeShader' objects is not writable, so _name is used
333 self
.filepath
= filepath
335 self
.frame_current
= frame_current
337 self
.split_at_invisible
= split_at_invisible
338 self
.stroke_color_mode
= stroke_color_mode
# BASE | FIRST | LAST
343 def from_lineset(cls
, lineset
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
, *, name
=""):
344 """Builds a SVGPathShader using data from the given lineset"""
345 name
= name
or lineset
.name
346 linestyle
= lineset
.linestyle
347 # extract style attributes from the linestyle and scene
348 svg
= getCurrentScene().svg_export
351 'stroke-width': linestyle
.thickness
,
352 'stroke-linecap': linestyle
.caps
.lower(),
353 'stroke-opacity': linestyle
.alpha
,
354 'stroke': format_rgb(linestyle
.color
),
355 'stroke-linejoin': svg
.line_join_type
.lower(),
357 # get dashed line pattern (if specified)
358 if linestyle
.use_dashed_line
:
359 style
['stroke-dasharray'] = ",".join(str(elem
) for elem
in get_dashed_pattern(linestyle
))
361 return cls(name
, style
, filepath
, res_y
, split_at_invisible
, use_stroke_color
, frame_current
)
365 def pathgen(stroke
, style
, height
, split_at_invisible
, stroke_color_mode
, f
=lambda v
: not v
.attribute
.visible
):
366 """Generator that creates SVG paths (as strings) from the current stroke """
370 if stroke_color_mode
!= 'BASE':
371 # try to use the color of the first or last vertex
373 index
= 0 if stroke_color_mode
== 'FIRST' else -1
374 color
= format_rgb(stroke
[index
].attribute
.color
)
375 style
["stroke"] = color
376 except (ValueError, IndexError):
377 # default is linestyle base color
380 # put style attributes into a single svg path definition
381 path
= '\n<path ' + "".join('{}="{}" '.format(k
, v
) for k
, v
in style
.items()) + 'd=" M '
388 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
389 if split_at_invisible
and v
.attribute
.visible
is False:
390 # end current and start new path;
392 # fast-forward till the next visible vertex
393 it
= itertools
.dropwhile(f
, it
)
394 # yield next visible vertex
395 svert
= next(it
, None)
399 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
403 def shade(self
, stroke
):
404 stroke_to_paths
= "".join(self
.pathgen(stroke
, self
.style
, self
.h
, self
.split_at_invisible
, self
.stroke_color_mode
)).split("\n")
405 # convert to actual XML. Empty strokes are empty strings; they are ignored.
406 self
.elements
.extend(et
.XML(elem
) for elem
in stroke_to_paths
if elem
) # if len(elem.strip()) > len(self.path))
409 """Write SVG data tree to file """
410 tree
= et
.parse(self
.filepath
)
411 root
= tree
.getroot()
413 scene
= bpy
.context
.scene
415 # create <g> for lineset as a whole (don't overwrite)
416 # when rendering an animation, frames will be nested in here, otherwise a group of strokes and optionally fills.
417 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(name
))
418 if lineset_group
is None:
419 lineset_group
= et
.XML('<g/>')
420 lineset_group
.attrib
= {
422 'xmlns:inkscape': namespaces
["inkscape"],
423 'inkscape:groupmode': 'lineset',
424 'inkscape:label': name
,
426 root
.append(lineset_group
)
428 # create <g> for the current frame
429 id = "frame_{:04n}".format(self
.frame_current
)
431 stroke_group
= et
.XML("<g/>")
432 stroke_group
.attrib
= {
433 'xmlns:inkscape': namespaces
["inkscape"],
434 'inkscape:groupmode': 'layer',
436 'inkscape:label': 'strokes'
439 stroke_group
.extend(self
.elements
)
440 if scene
.svg_export
.mode
== 'ANIMATION':
441 frame_group
= et
.XML("<g/>")
442 frame_group
.attrib
= {'id': id, 'inkscape:groupmode': 'frame', 'inkscape:label': id}
443 frame_group
.append(stroke_group
)
444 lineset_group
.append(frame_group
)
446 lineset_group
.append(stroke_group
)
449 print("SVG Export: writing to", self
.filepath
)
451 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
454 class SVGFillBuilder
:
455 def __init__(self
, filepath
, height
, name
):
456 self
.filepath
= filepath
458 self
.stroke_to_fill
= partial(self
.stroke_to_svg
, height
=height
)
461 def pathgen(vertices
, path
, height
):
463 for point
in vertices
:
465 yield '{:.3f}, {:.3f} '.format(x
, height
- y
)
466 yield ' z" />' # closes the path; connects the current to the first point
470 def get_merged_strokes(strokes
):
471 def extend_stroke(stroke
, vertices
):
472 for vert
in map(StrokeVertex
, vertices
):
473 stroke
.insert_vertex(vert
, stroke
.stroke_vertices_end())
476 base_strokes
= tuple(stroke
for stroke
in strokes
if not is_poly_clockwise(stroke
))
477 merged_strokes
= OrderedDict((s
, list()) for s
in base_strokes
)
479 for stroke
in filter(is_poly_clockwise
, strokes
):
480 for base
in base_strokes
:
481 # don't merge when diffuse colors don't match
482 if diffuse_from_stroke(stroke
) != diffuse_from_stroke(stroke
):
484 # only merge when the 'hole' is inside the base
485 elif stroke_inside_stroke(stroke
, base
):
486 merged_strokes
[base
].append(stroke
)
488 # if it isn't a hole, it is likely that there are two strokes belonging
489 # to the same object separated by another object. let's try to join them
490 elif (get_object_name(base
) == get_object_name(stroke
) and
491 diffuse_from_stroke(stroke
) == diffuse_from_stroke(stroke
)):
492 base
= extend_stroke(base
, (sv
for sv
in stroke
))
495 # if all else fails, treat this stroke as a base stroke
496 merged_strokes
.update({stroke
: []})
497 return merged_strokes
500 def stroke_to_svg(self
, stroke
, height
, parameters
=None):
501 if parameters
is None:
502 *color
, alpha
= diffuse_from_stroke(stroke
)
503 color
= tuple(int(255 * c
) for c
in color
)
505 'fill_rule': 'evenodd',
507 'fill-opacity': alpha
,
508 'fill': 'rgb' + repr(color
),
510 param_str
= " ".join('{}="{}"'.format(k
, v
) for k
, v
in parameters
.items())
511 path
= '<path {} d=" M '.format(param_str
)
512 vertices
= (svert
.point
for svert
in stroke
)
513 s
= "".join(self
.pathgen(vertices
, path
, height
))
517 def create_fill_elements(self
, strokes
):
518 """Creates ElementTree objects by merging stroke objects together and turning them into SVG paths."""
519 merged_strokes
= self
.get_merged_strokes(strokes
)
520 for k
, v
in merged_strokes
.items():
521 base
= self
.stroke_to_fill(k
)
522 fills
= (self
.stroke_to_fill(stroke
).get("d") for stroke
in v
)
523 merged_points
= " ".join(fills
)
524 base
.attrib
['d'] += merged_points
527 def write(self
, strokes
):
528 """Write SVG data tree to file """
530 tree
= et
.parse(self
.filepath
)
531 root
= tree
.getroot()
532 scene
= bpy
.context
.scene
535 lineset_group
= find_svg_elem(tree
, ".//svg:g[@id='{}']".format(self
._name
))
536 if lineset_group
is None:
537 lineset_group
= et
.XML('<g/>')
538 lineset_group
.attrib
= {
540 'xmlns:inkscape': namespaces
["inkscape"],
541 'inkscape:groupmode': 'lineset',
542 'inkscape:label': name
,
544 root
.append(lineset_group
)
545 print('added new lineset group ', name
)
548 # <g> for the fills of the current frame
549 fill_group
= et
.XML('<g/>')
550 fill_group
.attrib
= {
551 'xmlns:inkscape': namespaces
["inkscape"],
552 'inkscape:groupmode': 'layer',
553 'inkscape:label': 'fills',
557 fill_elements
= self
.create_fill_elements(strokes
)
558 fill_group
.extend(reversed(tuple(fill_elements
)))
559 if scene
.svg_export
.mode
== 'ANIMATION':
560 # add the fills to the <g> of the current frame
561 frame_group
= find_svg_elem(lineset_group
, ".//svg:g[@id='frame_{:04n}']".format(scene
.frame_current
))
562 frame_group
.insert(0, fill_group
)
564 lineset_group
.insert(0, fill_group
)
568 tree
.write(self
.filepath
, encoding
='ascii', xml_declaration
=True)
571 def stroke_inside_stroke(a
, b
):
572 box_a
= BoundingBox
.from_sequence(svert
.point
for svert
in a
)
573 box_b
= BoundingBox
.from_sequence(svert
.point
for svert
in b
)
574 return box_a
.inside(box_b
)
577 def diffuse_from_stroke(stroke
, curvemat
=CurveMaterialF0D()):
578 material
= curvemat(Interface0DIterator(stroke
))
579 return material
.diffuse
582 class ParameterEditorCallback(object):
583 """Object to store callbacks for the Parameter Editor in"""
584 def lineset_pre(self
, scene
, layer
, lineset
):
585 raise NotImplementedError()
587 def modifier_post(self
, scene
, layer
, lineset
):
588 raise NotImplementedError()
590 def lineset_post(self
, scene
, layer
, lineset
):
591 raise NotImplementedError()
595 class SVGPathShaderCallback(ParameterEditorCallback
):
597 def poll(cls
, scene
, linestyle
):
598 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and linestyle
.use_export_strokes
601 def modifier_post(cls
, scene
, layer
, lineset
):
602 if not cls
.poll(scene
, lineset
.linestyle
):
605 split
= scene
.svg_export
.split_at_invisible
606 stroke_color_mode
= lineset
.linestyle
.stroke_color_mode
607 cls
.shader
= SVGPathShader
.from_lineset(
608 lineset
, create_path(scene
),
609 render_height(scene
), split
, stroke_color_mode
, scene
.frame_current
, name
=layer
.name
+ '_' + lineset
.name
)
613 def lineset_post(cls
, scene
, layer
, lineset
):
614 if not cls
.poll(scene
, lineset
.linestyle
):
619 class SVGFillShaderCallback(ParameterEditorCallback
):
621 def poll(cls
, scene
, linestyle
):
622 return scene
.render
.use_freestyle
and scene
.svg_export
.use_svg_export
and scene
.svg_export
.object_fill
and linestyle
.use_export_fills
625 def lineset_post(cls
, scene
, layer
, lineset
):
626 if not cls
.poll(scene
, lineset
.linestyle
):
629 # reset the stroke selection (but don't delete the already generated strokes)
630 Operators
.reset(delete_strokes
=False)
631 # Unary Predicates: visible and correct edge nature
633 QuantitativeInvisibilityUP1D(0),
634 OrUP1D(ExternalContourUP1D(),
635 pyNatureUP1D(Nature
.BORDER
)),
637 # select the new edges
638 Operators
.select(upred
)
642 NotBP1D(pyZDiscontinuityBP1D()),
644 bpred
= OrBP1D(bpred
, AndBP1D(NotBP1D(bpred
), AndBP1D(SameShapeIdBP1D(), MaterialBP1D())))
646 Operators
.bidirectional_chain(ChainPredicateIterator(upred
, bpred
))
648 collector
= StrokeCollector()
649 Operators
.create(TrueUP1D(), [collector
])
651 builder
= SVGFillBuilder(create_path(scene
), render_height(scene
), layer
.name
+ '_' + lineset
.name
)
652 builder
.write(collector
.strokes
)
653 # make strokes used for filling invisible
654 for stroke
in collector
.strokes
:
656 svert
.attribute
.visible
= False
660 def indent_xml(elem
, level
=0, indentsize
=4):
661 """Prettifies XML code (used in SVG exporter) """
662 i
= "\n" + level
* " " * indentsize
664 if not elem
.text
or not elem
.text
.strip():
665 elem
.text
= i
+ " " * indentsize
666 if not elem
.tail
or not elem
.tail
.strip():
669 indent_xml(elem
, level
+ 1)
670 if not elem
.tail
or not elem
.tail
.strip():
672 elif level
and (not elem
.tail
or not elem
.tail
.strip()):
676 def register_namespaces(namespaces
=namespaces
):
677 for name
, url
in namespaces
.items():
678 if name
!= 'svg': # creates invalid xml
679 et
.register_namespace(name
, url
)
682 def handle_versions(self
):
683 # We don't modify startup file because it assumes to
684 # have all the default values only.
685 if not bpy
.data
.is_saved
:
688 # Revision https://developer.blender.org/rBA861519e44adc5674545fa18202dc43c4c20f2d1d
689 # changed the default for fills.
690 # fix by Sergey https://developer.blender.org/T46150
691 if bpy
.data
.version
<= (2, 76, 0):
692 for linestyle
in bpy
.data
.linestyles
:
693 linestyle
.use_export_fills
= True
699 SVGExporterLinesetPanel
,
705 linestyle
= bpy
.types
.FreestyleLineStyle
706 linestyle
.use_export_strokes
= BoolProperty(
707 name
="Export Strokes",
708 description
="Export strokes for this Line Style",
711 linestyle
.stroke_color_mode
= EnumProperty(
712 name
="Stroke Color Mode",
714 ('BASE', "Base Color", "Use the linestyle's base color", 0),
715 ('FIRST', "First Vertex", "Use the color of a stroke's first vertex", 1),
716 ('FINAL', "Final Vertex", "Use the color of a stroke's final vertex", 2),
720 linestyle
.use_export_fills
= BoolProperty(
722 description
="Export fills for this Line Style",
727 bpy
.utils
.register_class(cls
)
728 bpy
.types
.Scene
.svg_export
= PointerProperty(type=SVGExport
)
732 bpy
.app
.handlers
.render_init
.append(render_init
)
733 bpy
.app
.handlers
.render_write
.append(render_write
)
734 bpy
.app
.handlers
.render_pre
.append(svg_export_header
)
735 bpy
.app
.handlers
.render_complete
.append(svg_export_animation
)
737 # manipulate shaders list
738 parameter_editor
.callbacks_modifiers_post
.append(SVGPathShaderCallback
.modifier_post
)
739 parameter_editor
.callbacks_lineset_post
.append(SVGPathShaderCallback
.lineset_post
)
740 parameter_editor
.callbacks_lineset_post
.append(SVGFillShaderCallback
.lineset_post
)
742 # register namespaces
743 register_namespaces()
746 bpy
.app
.handlers
.version_update
.append(handle_versions
)
752 bpy
.utils
.unregister_class(cls
)
753 del bpy
.types
.Scene
.svg_export
754 linestyle
= bpy
.types
.FreestyleLineStyle
755 del linestyle
.use_export_strokes
756 del linestyle
.use_export_fills
759 bpy
.app
.handlers
.render_init
.remove(render_init
)
760 bpy
.app
.handlers
.render_write
.remove(render_write
)
761 bpy
.app
.handlers
.render_pre
.remove(svg_export_header
)
762 bpy
.app
.handlers
.render_complete
.remove(svg_export_animation
)
764 # manipulate shaders list
765 parameter_editor
.callbacks_modifiers_post
.remove(SVGPathShaderCallback
.modifier_post
)
766 parameter_editor
.callbacks_lineset_post
.remove(SVGPathShaderCallback
.lineset_post
)
767 parameter_editor
.callbacks_lineset_post
.remove(SVGFillShaderCallback
.lineset_post
)
769 bpy
.app
.handlers
.version_update
.remove(handle_versions
)
772 if __name__
== "__main__":