Blender V5.0
grease_pencil_io_export_svg.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include "BLI_bounds.hh"
6#include "BLI_color.hh"
7#include "BLI_string_utf8.h"
8#include "BLI_vector.hh"
9
10#include "BKE_curves.hh"
11#include "BKE_grease_pencil.hh"
12#include "BKE_scene.hh"
13
14#include "DNA_object_types.h"
15#include "DNA_scene_types.h"
16
18
20#include "GEO_set_curve_type.hh"
21
23
24#include <fmt/core.h>
25#include <fmt/format.h>
26#include <optional>
27#include <pugixml.hpp>
28
29#ifdef WIN32
30# include "utfconv.hh"
31#endif
32
36
38
39constexpr const char *svg_exporter_name = "SVG Export for Grease Pencil";
40constexpr const char *svg_exporter_version = "v2.0";
41
42static std::string rgb_to_hexstr(const float color[3])
43{
44 uint8_t r = color[0] * 255.0f;
45 uint8_t g = color[1] * 255.0f;
46 uint8_t b = color[2] * 255.0f;
47 return fmt::format("#{:02X}{:02X}{:02X}", r, g, b);
48}
49
50static void write_stroke_color_attribute(pugi::xml_node node,
51 const ColorGeometry4f &stroke_color,
52 const float stroke_opacity,
53 const bool round_cap)
54{
56 linearrgb_to_srgb_v3_v3(color, stroke_color);
57 std::string stroke_hex = rgb_to_hexstr(color);
58
59 node.append_attribute("stroke").set_value(stroke_hex.c_str());
60 node.append_attribute("stroke-opacity").set_value(stroke_color.a * stroke_opacity);
61
62 node.append_attribute("fill").set_value("none");
63 node.append_attribute("stroke-linecap").set_value(round_cap ? "round" : "square");
64}
65
66static void write_fill_color_attribute(pugi::xml_node node,
67 const ColorGeometry4f &fill_color,
68 const float layer_opacity)
69{
71 linearrgb_to_srgb_v3_v3(color, fill_color);
72 std::string stroke_hex = rgb_to_hexstr(color);
73
74 node.append_attribute("fill").set_value(stroke_hex.c_str());
75 node.append_attribute("stroke").set_value("none");
76 node.append_attribute("fill-opacity").set_value(fill_color.a * layer_opacity);
77}
78
79static void write_rect(pugi::xml_node node,
80 const float x,
81 const float y,
82 const float width,
83 const float height,
84 const float thickness,
85 const std::string &hexcolor)
86{
87 pugi::xml_node rect_node = node.append_child("rect");
88 rect_node.append_attribute("x").set_value(x);
89 rect_node.append_attribute("y").set_value(y);
90 rect_node.append_attribute("width").set_value(width);
91 rect_node.append_attribute("height").set_value(height);
92 rect_node.append_attribute("fill").set_value("none");
93 if (thickness > 0.0f) {
94 rect_node.append_attribute("stroke").set_value(hexcolor.c_str());
95 rect_node.append_attribute("stroke-width").set_value(thickness);
96 }
97}
98
100 uint64_t _node_uuid = 0;
101
102 std::string get_node_uuid_string();
103
104 public:
106
107 pugi::xml_document main_doc_;
108
110 void export_grease_pencil_objects(pugi::xml_node node, int frame_number);
111 void export_grease_pencil_layer(pugi::xml_node node,
112 const Object &object,
113 const bke::greasepencil::Layer &layer,
114 const bke::greasepencil::Drawing &drawing);
115
117 pugi::xml_node write_main_node();
118 pugi::xml_node write_animation_node(pugi::xml_node parent_node,
119 IndexMask frames,
120 float duration);
121 pugi::xml_node write_polygon(pugi::xml_node node,
122 const float4x4 &transform,
123 Span<float3> positions);
124 pugi::xml_node write_polyline(pugi::xml_node node,
125 const float4x4 &transform,
126 Span<float3> positions,
127 bool cyclic,
128 std::optional<float> width);
129 pugi::xml_node write_path(pugi::xml_node node,
130 const float4x4 &transform,
131 Span<float3> positions,
132 bool cyclic);
133 pugi::xml_node write_bezier_path(pugi::xml_node node,
134 const float4x4 &transform,
135 Span<float3> positions,
136 Span<float3> positions_left,
137 Span<float3> positions_right,
138 bool cyclic);
139
140 bool write_to_file(StringRefNull filepath);
141};
142
143std::string SVGExporter::get_node_uuid_string()
144{
145 std::string id = fmt::format(".uuid_{:#x}", this->_node_uuid++);
146 return id;
147}
148
150{
151 this->_node_uuid = 0;
152
153 switch (params_.frame_mode) {
155 const int frame_number = scene.r.cfra;
156 this->prepare_render_params(scene, frame_number);
157
158 this->write_document_header();
159 pugi::xml_node main_node = this->write_main_node();
160
161 this->export_grease_pencil_objects(main_node, frame_number);
162
163 const bool write_success = this->write_to_file(filepath);
164 return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
165 }
168 const bool selection_only = params_.frame_mode == ExportParams::FrameMode::Selected;
169 const int orig_frame = scene.r.cfra;
170
171 IndexMask frames = IndexMask(IndexRange(scene.r.sfra, scene.r.efra - scene.r.sfra + 1));
172
173 IndexMaskMemory memory;
174 if (selection_only) {
175 const Object &ob_eval = *DEG_get_evaluated(context_.depsgraph, params_.object);
176 if (ob_eval.type != OB_GREASE_PENCIL) {
178 }
179 const GreasePencil &grease_pencil = *static_cast<GreasePencil *>(ob_eval.data);
181 frames, GrainSize(1024), memory, [&](const int frame_number) {
182 return this->is_selected_frame(grease_pencil, frame_number);
183 });
184 }
185
186 if (frames.is_empty()) {
188 }
189
190 this->prepare_render_params(scene, frames.first());
191
192 this->write_document_header();
193 pugi::xml_node main_node = this->write_main_node();
194
195 /* Put frames in a hidden group. They are referenced later by a `<use>-node` that displays
196 * them in order. Use a group rather than a `<defs>-node` because some graphics applications
197 * don't expose those to users making it hard for them to work with the file.
198 */
199 pugi::xml_node frames_group_node = main_node.append_child("g");
200 frames_group_node.append_attribute("id").set_value("blender_frames");
201 frames_group_node.append_attribute("display").set_value("none");
202
203 const int frame_count = frames.size();
204 const float duration = scene.r.frs_sec_base * frame_count / scene.r.frs_sec;
205
206 frames.foreach_index([&](const int frame_number) {
207 scene.r.cfra = frame_number;
209 this->prepare_render_params(scene, frame_number);
210 this->export_grease_pencil_objects(frames_group_node, frame_number);
211 });
212
213 /* Back to original frame. */
214 scene.r.cfra = orig_frame;
217
218 this->write_animation_node(main_node, frames, duration);
219
220 const bool write_success = this->write_to_file(filepath);
221 return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
222 }
223 default:
226 }
227}
228
229static std::string frame_name(int frame_number)
230{
231 std::string frametxt = "blender_frame." + std::to_string(frame_number);
232 return frametxt;
233}
234
235void SVGExporter::export_grease_pencil_objects(pugi::xml_node node, const int frame_number)
236{
238
239 const bool is_clipping = camera_persmat_ && params_.use_clip_camera;
240
242
243 /* Camera clipping. */
244 if (is_clipping) {
245 pugi::xml_node clip_node = node.append_child("clipPath");
246 clip_node.append_attribute("id").set_value(
247 ("clip-path." + std::to_string(frame_number)).c_str());
248
249 write_rect(clip_node, 0, 0, camera_rect_.size().x, camera_rect_.size().y, 0.0f, "#000000");
250 }
251
252 pugi::xml_node frame_node = node.append_child("g");
253 frame_node.append_attribute("id").set_value(frame_name(frame_number).c_str());
254
255 /* Clip area. */
256 if (is_clipping) {
257 frame_node.append_attribute("clip-path")
258 .set_value(("url(#clip-path." + std::to_string(frame_number) + ")").c_str());
259 }
260
261 for (const ObjectInfo &info : objects) {
262 const Object *ob = info.object;
263
264 pugi::xml_node ob_node = frame_node.append_child("g");
265
266 char obtxt[15 + (MAX_ID_NAME - 2) + 1 + 11 + 1]; /* Final +1 for the null terminator. */
267 SNPRINTF_UTF8(obtxt, "blender_object.%s.%d", ob->id.name + 2, frame_number);
268 std::string object_id = std::string(obtxt) + this->get_node_uuid_string();
269 ob_node.append_attribute("id").set_value(object_id.c_str());
270
271 /* Use evaluated version to get strokes with modifiers. */
272 const Object *ob_eval = DEG_get_evaluated(context_.depsgraph, ob);
273 BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
274 const GreasePencil *grease_pencil_eval = static_cast<const GreasePencil *>(ob_eval->data);
275
276 for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
277 if (!layer->is_visible()) {
278 continue;
279 }
280 const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
281 if (drawing == nullptr) {
282 continue;
283 }
284
285 /* Layer node. */
286 pugi::xml_node layer_node = ob_node.append_child("g");
287 std::string layer_node_id = "layer." + layer->name() + this->get_node_uuid_string();
288 layer_node.append_attribute("id").set_value(layer_node_id.c_str());
289
290 const bke::CurvesGeometry &curves = drawing->strokes();
291 /* Convert NURBS and Catmull Rom to bezier then export. */
292 if (curves.has_curve_with_type({CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_NURBS})) {
293 IndexMaskMemory memory;
294 const IndexMask non_poly_selection = curves.indices_for_curve_type(CURVE_TYPE_POLY, memory)
295 .complement(curves.curves_range(), memory);
296
298 options.convert_bezier_handles_to_poly_points = false;
299 options.convert_bezier_handles_to_catmull_rom_points = false;
300 options.keep_bezier_shape_as_nurbs = true;
301 options.keep_catmull_rom_shape_as_nurbs = true;
302
303 Drawing export_drawing;
305 curves, non_poly_selection, CURVE_TYPE_BEZIER, {}, options);
306 export_drawing.tag_topology_changed();
307 export_grease_pencil_layer(layer_node, *ob_eval, *layer, export_drawing);
308 }
309 else {
310 export_grease_pencil_layer(layer_node, *ob_eval, *layer, *drawing);
311 }
312 }
313 }
314}
315
316void SVGExporter::export_grease_pencil_layer(pugi::xml_node layer_node,
317 const Object &object,
318 const bke::greasepencil::Layer &layer,
319 const bke::greasepencil::Drawing &drawing)
320{
322
323 const float4x4 layer_to_world = layer.to_world_space(object);
324
325 auto write_stroke = [&](const Span<float3> positions,
326 const Span<float3> positions_left,
327 const Span<float3> positions_right,
328 const bool cyclic,
329 const int8_t type,
330 const ColorGeometry4f &color,
331 const float opacity,
332 const std::optional<float> width,
333 const bool round_cap,
334 const bool is_outline) {
335 if (is_outline) {
336 pugi::xml_node element_node = write_path(layer_node, layer_to_world, positions, cyclic);
337 write_fill_color_attribute(element_node, color, opacity);
338 }
339 else {
340 pugi::xml_node element_node;
341 if (type == CURVE_TYPE_BEZIER) {
342 element_node = write_bezier_path(
343 layer_node, layer_to_world, positions, positions_left, positions_right, cyclic);
344 }
345 else {
346 /* Fill is always exported as polygon because the stroke of the fill is done
347 * in a different SVG command. */
348 element_node = write_polyline(layer_node, layer_to_world, positions, cyclic, width);
349 }
350
351 if (width) {
352 write_stroke_color_attribute(element_node, color, opacity, round_cap);
353 }
354 else {
355 write_fill_color_attribute(element_node, color, opacity);
356 }
357 }
358 };
359
360 foreach_stroke_in_layer(object, layer, drawing, write_stroke);
361}
362
364{
365 /* Add a custom document declaration node. */
366 pugi::xml_node decl = main_doc_.prepend_child(pugi::node_declaration);
367 decl.append_attribute("version") = "1.0";
368 decl.append_attribute("encoding") = "UTF-8";
369
370 pugi::xml_node comment = main_doc_.append_child(pugi::node_comment);
371 std::string txt = std::string(" Generator: Blender, ") + svg_exporter_name + " - " +
373 comment.set_value(txt.c_str());
374
375 pugi::xml_node doctype = main_doc_.append_child(pugi::node_doctype);
376 doctype.set_value(
377 "svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
378 "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"");
379}
380
382{
383 pugi::xml_node main_node = main_doc_.append_child("svg");
384 main_node.append_attribute("version").set_value("1.1");
385 main_node.append_attribute("x").set_value("0px");
386 main_node.append_attribute("y").set_value("0px");
387 main_node.append_attribute("xmlns").set_value("http://www.w3.org/2000/svg");
388
389 std::string width, height;
390
391 if (camera_persmat_) {
392 width = std::to_string(camera_rect_.size().x);
393 height = std::to_string(camera_rect_.size().y);
394 }
395 else {
396 width = std::to_string(screen_rect_.size().x);
397 height = std::to_string(screen_rect_.size().y);
398 }
399
400 main_node.append_attribute("width").set_value((width + "px").c_str());
401 main_node.append_attribute("height").set_value((height + "px").c_str());
402 std::string viewbox = "0 0 " + width + " " + height;
403 main_node.append_attribute("viewBox").set_value(viewbox.c_str());
404
405 return main_node;
406}
407
408pugi::xml_node SVGExporter::write_animation_node(pugi::xml_node parent_node,
409 IndexMask frames,
410 const float duration)
411{
412 pugi::xml_node use_node = parent_node.append_child("use");
413 use_node.append_attribute("id").set_value("blender_animation");
414 std::string href_text = "#" + frame_name(frames.first());
415 use_node.append_attribute("href").set_value(href_text.c_str());
416
417 pugi::xml_node animate_node = use_node.append_child("animate");
418 animate_node.append_attribute("id").set_value("frame-by-frame_animation");
419 animate_node.append_attribute("attributeName").set_value("href");
420
421 std::string duration_text = std::to_string(duration) + "s";
422 animate_node.append_attribute("dur").set_value(duration_text.c_str());
423 animate_node.append_attribute("repeatCount").set_value("indefinite");
424
425 std::string animated_frame_ids = [&]() {
426 std::string frame_ids_text = "";
427 frames.foreach_index([&](const int frame) {
428 std::string frame_url_entry = "#" + frame_name(frame) + ";";
429 frame_ids_text.append(frame_url_entry);
430 });
431 return frame_ids_text;
432 }();
433
434 animate_node.append_attribute("values").set_value(animated_frame_ids.c_str());
435
436 return use_node;
437}
438
439pugi::xml_node SVGExporter::write_polygon(pugi::xml_node node,
440 const float4x4 &transform,
441 const Span<float3> positions)
442{
443 pugi::xml_node element_node = node.append_child("polygon");
444
445 std::string txt;
446 for (const int i : positions.index_range()) {
447 const float2 screen_co = this->project_to_screen(transform, positions[i]);
448
449 if (i > 0) {
450 txt.append(" ");
451 }
452
453 txt.append(coord_to_svg_string(screen_co));
454 }
455
456 element_node.append_attribute("points").set_value(txt.c_str());
457
458 return element_node;
459}
460
461pugi::xml_node SVGExporter::write_polyline(pugi::xml_node node,
462 const float4x4 &transform,
463 const Span<float3> positions,
464 const bool cyclic,
465 const std::optional<float> width)
466{
467 pugi::xml_node element_node = node.append_child(cyclic ? "polygon" : "polyline");
468
469 if (width) {
470 element_node.append_attribute("stroke-width").set_value(*width);
471 }
472
473 std::string txt;
474 for (const int i : positions.index_range()) {
475 const float2 screen_co = this->project_to_screen(transform, positions[i]);
476
477 if (i > 0) {
478 txt.append(" ");
479 }
480
481 txt.append(coord_to_svg_string(screen_co));
482 }
483
484 element_node.append_attribute("points").set_value(txt.c_str());
485
486 return element_node;
487}
488
489pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
490 const float4x4 &transform,
491 const Span<float3> positions,
492 const bool cyclic)
493{
494 pugi::xml_node element_node = node.append_child("path");
495
496 std::string txt = "M";
497 for (const int i : positions.index_range()) {
498 const float2 screen_co = this->project_to_screen(transform, positions[i]);
499
500 if (i > 0) {
501 txt.append("L");
502 }
503
504 txt.append(coord_to_svg_string(screen_co));
505 }
506 /* Close patch (cyclic). */
507 if (cyclic) {
508 txt.append("z");
509 }
510
511 element_node.append_attribute("d").set_value(txt.c_str());
512
513 return element_node;
514}
515
516pugi::xml_node SVGExporter::write_bezier_path(pugi::xml_node node,
517 const float4x4 &transform,
518 const Span<float3> positions,
519 const Span<float3> positions_left,
520 const Span<float3> positions_right,
521 const bool cyclic)
522{
523 pugi::xml_node element_node = node.append_child("path");
524
525 std::string txt = "M";
526 for (const int i : positions.index_range().drop_back(1)) {
527 const float2 screen_co = this->project_to_screen(transform, positions[i]);
528 const float2 screen_co_right = this->project_to_screen(transform, positions_right[i]);
529 const float2 screen_co_left = this->project_to_screen(transform, positions_left[i + 1]);
530
531 txt.append(coord_to_svg_string(screen_co));
532 txt.append(" C ");
533 txt.append(coord_to_svg_string(screen_co_right));
534 txt.append(", ");
535 txt.append(coord_to_svg_string(screen_co_left));
536
537 if (i != positions.size() - 2) {
538 txt.append(", ");
539 }
540 }
541
542 {
543 txt.append(", ");
544 const float2 screen_co = this->project_to_screen(transform, positions.last());
545 txt.append(coord_to_svg_string(screen_co));
546 }
547
548 /* Close patch (cyclic). */
549 if (cyclic) {
550 const float2 screen_co_right = this->project_to_screen(transform, positions_right.last());
551 const float2 screen_co_left = this->project_to_screen(transform, positions_left.first());
552 const float2 screen_co = this->project_to_screen(transform, positions.first());
553
554 txt.append(" C ");
555 txt.append(coord_to_svg_string(screen_co_right));
556 txt.append(", ");
557 txt.append(coord_to_svg_string(screen_co_left));
558 txt.append(", ");
559 txt.append(coord_to_svg_string(screen_co));
560 txt.append("z");
561 }
562
563 element_node.append_attribute("d").set_value(txt.c_str());
564
565 return element_node;
566}
567
569{
570 bool result = true;
571 /* Support unicode character paths on Windows. */
572#ifdef WIN32
573 wchar_t *filepath_16 = alloc_utf16_from_8(filepath.c_str(), 0);
574 std::wstring wstr(filepath_16);
575 result = main_doc_.save_file(wstr.c_str());
576 free(filepath_16);
577#else
578 result = main_doc_.save_file(filepath.c_str());
579#endif
580
581 return result;
582}
583
585 const ExportParams &params,
586 Scene &scene,
587 StringRefNull filepath)
588{
589 SVGExporter exporter(context, params);
590 return exporter.export_scene(scene, filepath);
591}
592
593} // namespace blender::io::grease_pencil
Low-level operations for curves.
Low-level operations for grease pencil.
bool BKE_scene_camera_switch_update(Scene *scene)
Definition scene.cc:2265
void BKE_scene_graph_update_for_newframe(Depsgraph *depsgraph)
Definition scene.cc:2700
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
#define BLI_assert(a)
Definition BLI_assert.h:46
void BLI_kdtree_nd_ free(KDTree *tree)
void linearrgb_to_srgb_v3_v3(float srgb[3], const float linear[3])
#define SNPRINTF_UTF8(dst, format,...)
T * DEG_get_evaluated(const Depsgraph *depsgraph, T *id)
#define MAX_ID_NAME
Definition DNA_ID.h:373
Object is a sort of wrapper for general info.
@ OB_GREASE_PENCIL
SIMD_FORCE_INLINE btVector3 transform(const btVector3 &point) const
unsigned long long int uint64_t
ChannelStorageType a
static IndexMask from_predicate(const IndexMask &universe, GrainSize grain_size, IndexMaskMemory &memory, Fn &&predicate)
constexpr IndexRange drop_back(int64_t n) const
constexpr const T & first() const
Definition BLI_span.hh:315
constexpr int64_t size() const
Definition BLI_span.hh:252
constexpr const T & last(const int64_t n=0) const
Definition BLI_span.hh:325
constexpr IndexRange index_range() const
Definition BLI_span.hh:401
constexpr const char * c_str() const
IndexRange curves_range() const
bool has_curve_with_type(CurveType type) const
IndexMask indices_for_curve_type(CurveType type, IndexMaskMemory &memory) const
bke::CurvesGeometry & strokes_for_write()
const bke::CurvesGeometry & strokes() const
float4x4 to_world_space(const Object &object) const
IndexMask complement(const IndexMask &universe, IndexMaskMemory &memory) const
void foreach_index(Fn &&fn) const
GreasePencilExporter(const IOContext &context, const ExportParams &params)
std::string coord_to_svg_string(const float2 &screen_co) const
void prepare_render_params(Scene &scene, int frame_number)
void foreach_stroke_in_layer(const Object &object, const bke::greasepencil::Layer &layer, const bke::greasepencil::Drawing &drawing, WriteStrokeFn stroke_fn)
bool is_selected_frame(const GreasePencil &grease_pencil, int frame_number) const
float2 project_to_screen(const float4x4 &transform, const float3 &position) const
GreasePencilExporter(const IOContext &context, const ExportParams &params)
pugi::xml_node write_polygon(pugi::xml_node node, const float4x4 &transform, Span< float3 > positions)
void export_grease_pencil_layer(pugi::xml_node node, const Object &object, const bke::greasepencil::Layer &layer, const bke::greasepencil::Drawing &drawing)
void export_grease_pencil_objects(pugi::xml_node node, int frame_number)
pugi::xml_node write_bezier_path(pugi::xml_node node, const float4x4 &transform, Span< float3 > positions, Span< float3 > positions_left, Span< float3 > positions_right, bool cyclic)
pugi::xml_node write_animation_node(pugi::xml_node parent_node, IndexMask frames, float duration)
ExportStatus export_scene(Scene &scene, StringRefNull filepath)
pugi::xml_node write_path(pugi::xml_node node, const float4x4 &transform, Span< float3 > positions, bool cyclic)
pugi::xml_node write_polyline(pugi::xml_node node, const float4x4 &transform, Span< float3 > positions, bool cyclic, std::optional< float > width)
CCL_NAMESPACE_BEGIN struct Options options
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
bke::CurvesGeometry convert_curves(const bke::CurvesGeometry &src_curves, const IndexMask &selection, CurveType dst_type, const bke::AttributeFilter &attribute_filter, const ConvertCurvesOptions &options={})
static std::string rgb_to_hexstr(const float color[3])
static void write_fill_color_attribute(pugi::xml_node node, const ColorGeometry4f &fill_color, const float layer_opacity)
static void write_rect(pugi::xml_node node, const float x, const float y, const float width, const float height, const float thickness, const std::string &hexcolor)
ExportStatus export_svg(const IOContext &context, const ExportParams &params, Scene &scene, StringRefNull filepath)
static std::string frame_name(int frame_number)
static void write_stroke_color_attribute(pugi::xml_node node, const ColorGeometry4f &stroke_color, const float stroke_opacity, const bool round_cap)
constexpr const char * svg_exporter_version
MatBase< float, 4, 4 > float4x4
VecBase< float, 2 > float2
ColorSceneLinear4f< eAlpha::Premultiplied > ColorGeometry4f
char name[258]
Definition DNA_ID.h:432
struct RenderData r
i
Definition text_draw.cc:230
wchar_t * alloc_utf16_from_8(const char *in8, size_t add)
Definition utfconv.cc:294