Blender V4.5
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.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
22
23#include <fmt/core.h>
24#include <fmt/format.h>
25#include <optional>
26#include <pugixml.hpp>
27
28#ifdef WIN32
29# include "utfconv.hh"
30#endif
31
35
37
38constexpr const char *svg_exporter_name = "SVG Export for Grease Pencil";
39constexpr const char *svg_exporter_version = "v2.0";
40
41static std::string rgb_to_hexstr(const float color[3])
42{
43 uint8_t r = color[0] * 255.0f;
44 uint8_t g = color[1] * 255.0f;
45 uint8_t b = color[2] * 255.0f;
46 return fmt::format("#{:02X}{:02X}{:02X}", r, g, b);
47}
48
49static void write_stroke_color_attribute(pugi::xml_node node,
50 const ColorGeometry4f &stroke_color,
51 const float stroke_opacity,
52 const bool round_cap)
53{
55 linearrgb_to_srgb_v3_v3(color, stroke_color);
56 std::string stroke_hex = rgb_to_hexstr(color);
57
58 node.append_attribute("stroke").set_value(stroke_hex.c_str());
59 node.append_attribute("stroke-opacity").set_value(stroke_color.a * stroke_opacity);
60
61 node.append_attribute("fill").set_value("none");
62 node.append_attribute("stroke-linecap").set_value(round_cap ? "round" : "square");
63}
64
65static void write_fill_color_attribute(pugi::xml_node node,
66 const ColorGeometry4f &fill_color,
67 const float layer_opacity)
68{
70 linearrgb_to_srgb_v3_v3(color, fill_color);
71 std::string stroke_hex = rgb_to_hexstr(color);
72
73 node.append_attribute("fill").set_value(stroke_hex.c_str());
74 node.append_attribute("stroke").set_value("none");
75 node.append_attribute("fill-opacity").set_value(fill_color.a * layer_opacity);
76}
77
78static void write_rect(pugi::xml_node node,
79 const float x,
80 const float y,
81 const float width,
82 const float height,
83 const float thickness,
84 const std::string &hexcolor)
85{
86 pugi::xml_node rect_node = node.append_child("rect");
87 rect_node.append_attribute("x").set_value(x);
88 rect_node.append_attribute("y").set_value(y);
89 rect_node.append_attribute("width").set_value(width);
90 rect_node.append_attribute("height").set_value(height);
91 rect_node.append_attribute("fill").set_value("none");
92 if (thickness > 0.0f) {
93 rect_node.append_attribute("stroke").set_value(hexcolor.c_str());
94 rect_node.append_attribute("stroke-width").set_value(thickness);
95 }
96}
97
99 uint64_t _node_uuid = 0;
100
101 std::string get_node_uuid_string();
102
103 public:
105
106 pugi::xml_document main_doc_;
107
109 void export_grease_pencil_objects(pugi::xml_node node, int frame_number);
110 void export_grease_pencil_layer(pugi::xml_node node,
111 const Object &object,
112 const bke::greasepencil::Layer &layer,
113 const bke::greasepencil::Drawing &drawing);
114
116 pugi::xml_node write_main_node();
117 pugi::xml_node write_animation_node(pugi::xml_node parent_node,
118 IndexMask frames,
119 float duration);
120 pugi::xml_node write_polygon(pugi::xml_node node,
121 const float4x4 &transform,
122 Span<float3> positions);
123 pugi::xml_node write_polyline(pugi::xml_node node,
124 const float4x4 &transform,
125 Span<float3> positions,
126 bool cyclic,
127 std::optional<float> width);
128 pugi::xml_node write_path(pugi::xml_node node,
129 const float4x4 &transform,
130 Span<float3> positions,
131 bool cyclic);
132
133 bool write_to_file(StringRefNull filepath);
134};
135
136std::string SVGExporter::get_node_uuid_string()
137{
138 std::string id = fmt::format(".uuid_{:#x}", this->_node_uuid++);
139 return id;
140}
141
143{
144 this->_node_uuid = 0;
145
146 switch (params_.frame_mode) {
148 const int frame_number = scene.r.cfra;
149 this->prepare_render_params(scene, frame_number);
150
151 this->write_document_header();
152 pugi::xml_node main_node = this->write_main_node();
153
154 this->export_grease_pencil_objects(main_node, frame_number);
155
156 const bool write_success = this->write_to_file(filepath);
157 return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
158 }
161 const bool selection_only = params_.frame_mode == ExportParams::FrameMode::Selected;
162 const int orig_frame = scene.r.cfra;
163
164 IndexMask frames = IndexMask(IndexRange(scene.r.sfra, scene.r.efra - scene.r.sfra + 1));
165
166 IndexMaskMemory memory;
167 if (selection_only) {
168 const Object &ob_eval = *DEG_get_evaluated(context_.depsgraph, params_.object);
169 if (ob_eval.type != OB_GREASE_PENCIL) {
171 }
172 const GreasePencil &grease_pencil = *static_cast<GreasePencil *>(ob_eval.data);
174 frames, GrainSize(1024), memory, [&](const int frame_number) {
175 return this->is_selected_frame(grease_pencil, frame_number);
176 });
177 }
178
179 if (frames.is_empty()) {
181 }
182
183 this->prepare_render_params(scene, frames.first());
184
185 this->write_document_header();
186 pugi::xml_node main_node = this->write_main_node();
187
188 /* Put frames in a hidden group. They are referenced later by a `<use>-node` that displays
189 * them in order. Use a group rather than a `<defs>-node` because some graphics applications
190 * don't expose those to users making it hard for them to work with the file.
191 */
192 pugi::xml_node frames_group_node = main_node.append_child("g");
193 frames_group_node.append_attribute("id").set_value("blender_frames");
194 frames_group_node.append_attribute("display").set_value("none");
195
196 const int frame_count = frames.size();
197 const float duration = scene.r.frs_sec_base * frame_count / scene.r.frs_sec;
198
199 frames.foreach_index([&](const int frame_number) {
200 scene.r.cfra = frame_number;
202 this->prepare_render_params(scene, frame_number);
203 this->export_grease_pencil_objects(frames_group_node, frame_number);
204 });
205
206 /* Back to original frame. */
207 scene.r.cfra = orig_frame;
210
211 this->write_animation_node(main_node, frames, duration);
212
213 const bool write_success = this->write_to_file(filepath);
214 return write_success ? ExportStatus::Ok : ExportStatus::FileWriteError;
215 }
216 default:
219 }
220}
221
222static std::string frame_name(int frame_number)
223{
224 std::string frametxt = "blender_frame." + std::to_string(frame_number);
225 return frametxt;
226}
227
228void SVGExporter::export_grease_pencil_objects(pugi::xml_node node, const int frame_number)
229{
231
232 const bool is_clipping = camera_persmat_ && params_.use_clip_camera;
233
235
236 /* Camera clipping. */
237 if (is_clipping) {
238 pugi::xml_node clip_node = node.append_child("clipPath");
239 clip_node.append_attribute("id").set_value(
240 ("clip-path." + std::to_string(frame_number)).c_str());
241
242 write_rect(clip_node, 0, 0, camera_rect_.size().x, camera_rect_.size().y, 0.0f, "#000000");
243 }
244
245 pugi::xml_node frame_node = node.append_child("g");
246 frame_node.append_attribute("id").set_value(frame_name(frame_number).c_str());
247
248 /* Clip area. */
249 if (is_clipping) {
250 frame_node.append_attribute("clip-path")
251 .set_value(("url(#clip-path." + std::to_string(frame_number) + ")").c_str());
252 }
253
254 for (const ObjectInfo &info : objects) {
255 const Object *ob = info.object;
256
257 pugi::xml_node ob_node = frame_node.append_child("g");
258
259 char obtxt[96];
260 SNPRINTF(obtxt, "blender_object.%s.%d", ob->id.name + 2, frame_number);
261 std::string object_id = std::string(obtxt) + this->get_node_uuid_string();
262 ob_node.append_attribute("id").set_value(object_id.c_str());
263
264 /* Use evaluated version to get strokes with modifiers. */
265 const Object *ob_eval = DEG_get_evaluated(context_.depsgraph, ob);
266 BLI_assert(ob_eval->type == OB_GREASE_PENCIL);
267 const GreasePencil *grease_pencil_eval = static_cast<const GreasePencil *>(ob_eval->data);
268
269 for (const bke::greasepencil::Layer *layer : grease_pencil_eval->layers()) {
270 if (!layer->is_visible()) {
271 continue;
272 }
273 const Drawing *drawing = grease_pencil_eval->get_drawing_at(*layer, frame_number);
274 if (drawing == nullptr) {
275 continue;
276 }
277
278 /* Layer node. */
279 pugi::xml_node layer_node = ob_node.append_child("g");
280 std::string layer_node_id = "layer." + layer->name() + this->get_node_uuid_string();
281 layer_node.append_attribute("id").set_value(layer_node_id.c_str());
282
283 const bke::CurvesGeometry &curves = drawing->strokes();
284 /* TODO: Instead of converting all the other curve types to poly curves, export them directly
285 * as curve paths to the SVG. */
286 if (curves.has_curve_with_type(
287 {CURVE_TYPE_CATMULL_ROM, CURVE_TYPE_BEZIER, CURVE_TYPE_NURBS}))
288 {
289 IndexMaskMemory memory;
290 const IndexMask non_poly_selection = curves.indices_for_curve_type(CURVE_TYPE_POLY, memory)
291 .complement(curves.curves_range(), memory);
292
293 Drawing export_drawing;
294 export_drawing.strokes_for_write() = geometry::resample_to_evaluated(curves,
295 non_poly_selection);
296 export_drawing.tag_topology_changed();
297
298 export_grease_pencil_layer(layer_node, *ob_eval, *layer, export_drawing);
299 }
300 else {
301 export_grease_pencil_layer(layer_node, *ob_eval, *layer, *drawing);
302 }
303 }
304 }
305}
306
307void SVGExporter::export_grease_pencil_layer(pugi::xml_node layer_node,
308 const Object &object,
309 const bke::greasepencil::Layer &layer,
310 const bke::greasepencil::Drawing &drawing)
311{
313
314 const float4x4 layer_to_world = layer.to_world_space(object);
315
316 auto write_stroke = [&](const Span<float3> positions,
317 const bool cyclic,
318 const ColorGeometry4f &color,
319 const float opacity,
320 const std::optional<float> width,
321 const bool round_cap,
322 const bool is_outline) {
323 if (is_outline) {
324 pugi::xml_node element_node = write_path(layer_node, layer_to_world, positions, cyclic);
325 write_fill_color_attribute(element_node, color, opacity);
326 }
327 else {
328 /* Fill is always exported as polygon because the stroke of the fill is done
329 * in a different SVG command. */
330 pugi::xml_node element_node = write_polyline(
331 layer_node, layer_to_world, positions, cyclic, width);
332
333 if (width) {
334 write_stroke_color_attribute(element_node, color, opacity, round_cap);
335 }
336 else {
337 write_fill_color_attribute(element_node, color, opacity);
338 }
339 }
340 };
341
342 foreach_stroke_in_layer(object, layer, drawing, write_stroke);
343}
344
346{
347 /* Add a custom document declaration node. */
348 pugi::xml_node decl = main_doc_.prepend_child(pugi::node_declaration);
349 decl.append_attribute("version") = "1.0";
350 decl.append_attribute("encoding") = "UTF-8";
351
352 pugi::xml_node comment = main_doc_.append_child(pugi::node_comment);
353 std::string txt = std::string(" Generator: Blender, ") + svg_exporter_name + " - " +
355 comment.set_value(txt.c_str());
356
357 pugi::xml_node doctype = main_doc_.append_child(pugi::node_doctype);
358 doctype.set_value(
359 "svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
360 "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"");
361}
362
364{
365 pugi::xml_node main_node = main_doc_.append_child("svg");
366 main_node.append_attribute("version").set_value("1.1");
367 main_node.append_attribute("x").set_value("0px");
368 main_node.append_attribute("y").set_value("0px");
369 main_node.append_attribute("xmlns").set_value("http://www.w3.org/2000/svg");
370
371 std::string width, height;
372
373 if (camera_persmat_) {
374 width = std::to_string(camera_rect_.size().x);
375 height = std::to_string(camera_rect_.size().y);
376 }
377 else {
378 width = std::to_string(screen_rect_.size().x);
379 height = std::to_string(screen_rect_.size().y);
380 }
381
382 main_node.append_attribute("width").set_value((width + "px").c_str());
383 main_node.append_attribute("height").set_value((height + "px").c_str());
384 std::string viewbox = "0 0 " + width + " " + height;
385 main_node.append_attribute("viewBox").set_value(viewbox.c_str());
386
387 return main_node;
388}
389
390pugi::xml_node SVGExporter::write_animation_node(pugi::xml_node parent_node,
391 IndexMask frames,
392 const float duration)
393{
394 pugi::xml_node use_node = parent_node.append_child("use");
395 use_node.append_attribute("id").set_value("blender_animation");
396 std::string href_text = "#" + frame_name(frames.first());
397 use_node.append_attribute("href").set_value(href_text.c_str());
398
399 pugi::xml_node animate_node = use_node.append_child("animate");
400 animate_node.append_attribute("id").set_value("frame-by-frame_animation");
401 animate_node.append_attribute("attributeName").set_value("href");
402
403 std::string duration_text = std::to_string(duration) + "s";
404 animate_node.append_attribute("dur").set_value(duration_text.c_str());
405 animate_node.append_attribute("repeatCount").set_value("indefinite");
406
407 std::string animated_frame_ids = [&]() {
408 std::string frame_ids_text = "";
409 frames.foreach_index([&](const int frame) {
410 std::string frame_url_entry = "#" + frame_name(frame) + ";";
411 frame_ids_text.append(frame_url_entry);
412 });
413 return frame_ids_text;
414 }();
415
416 animate_node.append_attribute("values").set_value(animated_frame_ids.c_str());
417
418 return use_node;
419}
420
421pugi::xml_node SVGExporter::write_polygon(pugi::xml_node node,
422 const float4x4 &transform,
423 const Span<float3> positions)
424{
425 pugi::xml_node element_node = node.append_child("polygon");
426
427 std::string txt;
428 for (const int i : positions.index_range()) {
429 if (i > 0) {
430 txt.append(" ");
431 }
432 /* SVG has inverted Y axis. */
433 const float2 screen_co = this->project_to_screen(transform, positions[i]);
434 if (camera_persmat_) {
435 txt.append(std::to_string(screen_co.x) + "," +
436 std::to_string(camera_rect_.size().y - screen_co.y));
437 }
438 else {
439 txt.append(std::to_string(screen_co.x) + "," +
440 std::to_string(screen_rect_.size().y - screen_co.y));
441 }
442 }
443
444 element_node.append_attribute("points").set_value(txt.c_str());
445
446 return element_node;
447}
448
449pugi::xml_node SVGExporter::write_polyline(pugi::xml_node node,
450 const float4x4 &transform,
451 const Span<float3> positions,
452 const bool cyclic,
453 const std::optional<float> width)
454{
455 pugi::xml_node element_node = node.append_child(cyclic ? "polygon" : "polyline");
456
457 if (width) {
458 element_node.append_attribute("stroke-width").set_value(*width);
459 }
460
461 std::string txt;
462 for (const int i : positions.index_range()) {
463 if (i > 0) {
464 txt.append(" ");
465 }
466 /* SVG has inverted Y axis. */
467 const float2 screen_co = this->project_to_screen(transform, positions[i]);
468 if (camera_persmat_) {
469 txt.append(std::to_string(screen_co.x) + "," +
470 std::to_string(camera_rect_.size().y - screen_co.y));
471 }
472 else {
473 txt.append(std::to_string(screen_co.x) + "," +
474 std::to_string(screen_rect_.size().y - screen_co.y));
475 }
476 }
477
478 element_node.append_attribute("points").set_value(txt.c_str());
479
480 return element_node;
481}
482
483pugi::xml_node SVGExporter::write_path(pugi::xml_node node,
484 const float4x4 &transform,
485 const Span<float3> positions,
486 const bool cyclic)
487{
488 pugi::xml_node element_node = node.append_child("path");
489
490 std::string txt = "M";
491 for (const int i : positions.index_range()) {
492 if (i > 0) {
493 txt.append("L");
494 }
495 const float2 screen_co = this->project_to_screen(transform, positions[i]);
496 /* SVG has inverted Y axis. */
497 if (camera_persmat_) {
498 txt.append(std::to_string(screen_co.x) + "," +
499 std::to_string(camera_rect_.size().y - screen_co.y));
500 }
501 else {
502 txt.append(std::to_string(screen_co.x) + "," +
503 std::to_string(screen_rect_.size().y - screen_co.y));
504 }
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
517{
518 bool result = true;
519 /* Support unicode character paths on Windows. */
520#ifdef WIN32
521 wchar_t *filepath_16 = alloc_utf16_from_8(filepath.c_str(), 0);
522 std::wstring wstr(filepath_16);
523 result = main_doc_.save_file(wstr.c_str());
524 free(filepath_16);
525#else
526 result = main_doc_.save_file(filepath.c_str());
527#endif
528
529 return result;
530}
531
533 const ExportParams &params,
534 Scene &scene,
535 StringRefNull filepath)
536{
537 SVGExporter exporter(context, params);
538 return exporter.export_scene(scene, filepath);
539}
540
541} // 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:2267
void BKE_scene_graph_update_for_newframe(Depsgraph *depsgraph)
Definition scene.cc:2697
#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(dst, format,...)
Definition BLI_string.h:599
T * DEG_get_evaluated(const Depsgraph *depsgraph, T *id)
@ CURVE_TYPE_POLY
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
Definition BLI_color.hh:93
static IndexMask from_predicate(const IndexMask &universe, GrainSize grain_size, IndexMaskMemory &memory, Fn &&predicate)
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)
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_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)
uiWidgetBaseParameters params[MAX_WIDGET_BASE_BATCH]
CurvesGeometry resample_to_evaluated(const CurvesGeometry &src_curves, const IndexMask &selection, const ResampleCurvesOutputAttributeIDs &output_ids={})
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
Definition BLI_color.hh:342
char name[66]
Definition DNA_ID.h:415
struct RenderData r
i
Definition text_draw.cc:230
wchar_t * alloc_utf16_from_8(const char *in8, size_t add)
Definition utfconv.cc:292