Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntonio Vazquez <blendergit@gmail.com>2021-03-24 17:14:43 +0300
committerAntonio Vazquez <blendergit@gmail.com>2021-03-24 17:28:58 +0300
commita8a92cd15a5251377474fbfdcf9ff0298a8457a9 (patch)
treee706390707e62180be921b2f2de03fcb8793303e /source/blender/io/gpencil/intern/gpencil_io_export_svg.cc
parentce359da5b3ac98c9f7cfd6593cc1769ec3b7247b (diff)
GPencil: New modules for Import and Export
This patch adds support to export and import grease pencil in several formats. Inlude: * Export SVG * Export PDF (always from camera view) * Import SVG The import and export only support solid colors and not gradients or textures. Requires libharu and pugixml. For importing SVG, the NanoSVG lib is used, but this does not require installation (just a .h file embedded in the project folder) Example of PDF export: https://youtu.be/BMm0KeMJsI4 Reviewed By: #grease_pencil, HooglyBoogly Maniphest Tasks: T83190, T79875, T83191, T83192 Differential Revision: https://developer.blender.org/D10482
Diffstat (limited to 'source/blender/io/gpencil/intern/gpencil_io_export_svg.cc')
-rw-r--r--source/blender/io/gpencil/intern/gpencil_io_export_svg.cc464
1 files changed, 464 insertions, 0 deletions
diff --git a/source/blender/io/gpencil/intern/gpencil_io_export_svg.cc b/source/blender/io/gpencil/intern/gpencil_io_export_svg.cc
new file mode 100644
index 00000000000..89584cd242f
--- /dev/null
+++ b/source/blender/io/gpencil/intern/gpencil_io_export_svg.cc
@@ -0,0 +1,464 @@
+/*
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * The Original Code is Copyright (C) 2020 Blender Foundation
+ * All rights reserved.
+ */
+
+/** \file
+ * \ingroup bgpencil
+ */
+
+#include "BLI_math_vector.h"
+#include "BLI_string.h"
+#include "BLI_utildefines.h"
+
+#include "DNA_gpencil_types.h"
+#include "DNA_material_types.h"
+#include "DNA_object_types.h"
+#include "DNA_scene_types.h"
+#include "DNA_screen_types.h"
+
+#include "BKE_gpencil.h"
+#include "BKE_gpencil_geom.h"
+#include "BKE_main.h"
+#include "BKE_material.h"
+
+#include "DEG_depsgraph.h"
+#include "DEG_depsgraph_query.h"
+
+#include "ED_gpencil.h"
+#include "ED_view3d.h"
+
+#ifdef WIN32
+# include "utfconv.h"
+#endif
+
+#include "UI_view2d.h"
+
+#include "gpencil_io.h"
+#include "gpencil_io_export_svg.h"
+
+#include "pugixml.hpp"
+
+namespace blender ::io ::gpencil {
+
+/* Constructor. */
+GpencilExporterSVG::GpencilExporterSVG(const char *filename, const GpencilIOParams *iparams)
+ : GpencilExporter(iparams)
+{
+ filename_set(filename);
+
+ invert_axis_[0] = false;
+ invert_axis_[1] = true;
+}
+
+bool GpencilExporterSVG::add_newpage()
+{
+ create_document_header();
+ return true;
+}
+
+bool GpencilExporterSVG::add_body()
+{
+ export_gpencil_layers();
+ return true;
+}
+
+bool GpencilExporterSVG::write()
+{
+ bool result = true;
+/* Support unicode character paths on Windows. */
+#ifdef WIN32
+ char filename_cstr[FILE_MAX];
+ BLI_strncpy(filename_cstr, filename_, FILE_MAX);
+
+ UTF16_ENCODE(filename_cstr);
+ std::wstring wstr(filename_cstr_16);
+ result = main_doc_.save_file(wstr.c_str());
+
+ UTF16_UN_ENCODE(filename_cstr);
+#else
+ result = main_doc_.save_file(filename_);
+#endif
+
+ return result;
+}
+
+/* Create document header and main svg node. */
+void GpencilExporterSVG::create_document_header()
+{
+ /* Add a custom document declaration node. */
+ pugi::xml_node decl = main_doc_.prepend_child(pugi::node_declaration);
+ decl.append_attribute("version") = "1.0";
+ decl.append_attribute("encoding") = "UTF-8";
+
+ pugi::xml_node comment = main_doc_.append_child(pugi::node_comment);
+ char txt[128];
+ sprintf(txt, " Generator: Blender, %s - %s ", SVG_EXPORTER_NAME, SVG_EXPORTER_VERSION);
+ comment.set_value(txt);
+
+ pugi::xml_node doctype = main_doc_.append_child(pugi::node_doctype);
+ doctype.set_value(
+ "svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "
+ "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"");
+
+ main_node_ = main_doc_.append_child("svg");
+ main_node_.append_attribute("version").set_value("1.0");
+ main_node_.append_attribute("x").set_value("0px");
+ main_node_.append_attribute("y").set_value("0px");
+
+ std::string width;
+ std::string height;
+
+ width = std::to_string(render_x_);
+ height = std::to_string(render_y_);
+
+ main_node_.append_attribute("width").set_value((width + "px").c_str());
+ main_node_.append_attribute("height").set_value((height + "px").c_str());
+ std::string viewbox = "0 0 " + width + " " + height;
+ main_node_.append_attribute("viewBox").set_value(viewbox.c_str());
+}
+
+/* Main layer loop. */
+void GpencilExporterSVG::export_gpencil_layers()
+{
+ const bool is_clipping = is_camera_mode() && (params_.flag & GP_EXPORT_CLIP_CAMERA) != 0;
+
+ /* If is doing a set of frames, the list of objects can change for each frame. */
+ create_object_list();
+
+ for (ObjectZ &obz : ob_list_) {
+ Object *ob = obz.ob;
+
+ /* Camera clipping. */
+ if (is_clipping) {
+ pugi::xml_node clip_node = main_node_.append_child("clipPath");
+ clip_node.append_attribute("id").set_value(("clip-path" + std::to_string(cfra_)).c_str());
+
+ add_rect(clip_node, 0, 0, render_x_, render_y_, 0.0f, "#000000");
+ }
+
+ frame_node_ = main_node_.append_child("g");
+ std::string frametxt = "blender_frame_" + std::to_string(cfra_);
+ frame_node_.append_attribute("id").set_value(frametxt.c_str());
+
+ /* Clip area. */
+ if (is_clipping) {
+ frame_node_.append_attribute("clip-path")
+ .set_value(("url(#clip-path" + std::to_string(cfra_) + ")").c_str());
+ }
+
+ pugi::xml_node ob_node = frame_node_.append_child("g");
+
+ char obtxt[96];
+ sprintf(obtxt, "blender_object_%s", ob->id.name + 2);
+ ob_node.append_attribute("id").set_value(obtxt);
+
+ /* Use evaluated version to get strokes with modifiers. */
+ Object *ob_eval_ = (Object *)DEG_get_evaluated_id(depsgraph_, &ob->id);
+ bGPdata *gpd_eval = (bGPdata *)ob_eval_->data;
+
+ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd_eval->layers) {
+ if (gpl->flag & GP_LAYER_HIDE) {
+ continue;
+ }
+ prepare_layer_export_matrix(ob, gpl);
+
+ bGPDframe *gpf = gpl->actframe;
+ if ((gpf == nullptr) || (gpf->strokes.first == nullptr)) {
+ continue;
+ }
+
+ /* Layer node. */
+ std::string txt = "Layer: ";
+ txt.append(gpl->info);
+ ob_node.append_child(pugi::node_comment).set_value(txt.c_str());
+
+ pugi::xml_node node_gpl = ob_node.append_child("g");
+ node_gpl.append_attribute("id").set_value(gpl->info);
+
+ LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
+ if (gps->totpoints < 2) {
+ continue;
+ }
+ if (!ED_gpencil_stroke_material_visible(ob, gps)) {
+ continue;
+ }
+
+ /* Duplicate the stroke to apply any layer thickness change. */
+ bGPDstroke *gps_duplicate = BKE_gpencil_stroke_duplicate(gps, true, false);
+
+ MaterialGPencilStyle *gp_style = BKE_gpencil_material_settings(ob,
+ gps_duplicate->mat_nr + 1);
+
+ const bool is_stroke = ((gp_style->flag & GP_MATERIAL_STROKE_SHOW) &&
+ (gp_style->stroke_rgba[3] > GPENCIL_ALPHA_OPACITY_THRESH));
+ const bool is_fill = ((gp_style->flag & GP_MATERIAL_FILL_SHOW) &&
+ (gp_style->fill_rgba[3] > GPENCIL_ALPHA_OPACITY_THRESH));
+
+ prepare_stroke_export_colors(ob, gps_duplicate);
+
+ /* Apply layer thickness change. */
+ gps_duplicate->thickness += gpl->line_change;
+ /* Apply object scale to thickness. */
+ gps_duplicate->thickness *= mat4_to_scale(ob->obmat);
+ CLAMP_MIN(gps_duplicate->thickness, 1.0f);
+
+ const bool is_normalized = ((params_.flag & GP_EXPORT_NORM_THICKNESS) != 0) ||
+ BKE_gpencil_stroke_is_pressure_constant(gps);
+
+ /* Fill. */
+ if ((is_fill) && (params_.flag & GP_EXPORT_FILL)) {
+ /* Fill is always exported as polygon because the stroke of the fill is done
+ * in a different SVG command. */
+ export_stroke_to_polyline(gpl, gps_duplicate, node_gpl, is_stroke, true);
+ }
+
+ /* Stroke. */
+ if (is_stroke) {
+ if (is_normalized) {
+ export_stroke_to_polyline(gpl, gps_duplicate, node_gpl, is_stroke, false);
+ }
+ else {
+ bGPDstroke *gps_perimeter = BKE_gpencil_stroke_perimeter_from_view(
+ rv3d_, gpd_, gpl, gps_duplicate, 3, diff_mat_.values);
+
+ /* Sample stroke. */
+ if (params_.stroke_sample > 0.0f) {
+ BKE_gpencil_stroke_sample(gpd_eval, gps_perimeter, params_.stroke_sample, false);
+ }
+
+ export_stroke_to_path(gpl, gps_perimeter, node_gpl, false);
+
+ BKE_gpencil_free_stroke(gps_perimeter);
+ }
+ }
+
+ BKE_gpencil_free_stroke(gps_duplicate);
+ }
+ }
+ }
+}
+
+/**
+ * Export a stroke using SVG path
+ * \param node_gpl: Node of the layer.
+ * \param do_fill: True if the stroke is only fill
+ */
+void GpencilExporterSVG::export_stroke_to_path(bGPDlayer *gpl,
+ bGPDstroke *gps,
+ pugi::xml_node node_gpl,
+ const bool do_fill)
+{
+ pugi::xml_node node_gps = node_gpl.append_child("path");
+
+ float col[3];
+ std::string stroke_hex;
+ if (do_fill) {
+ node_gps.append_attribute("fill-opacity").set_value(fill_color_[3] * gpl->opacity);
+
+ interp_v3_v3v3(col, fill_color_, gpl->tintcolor, gpl->tintcolor[3]);
+ }
+ else {
+ node_gps.append_attribute("fill-opacity")
+ .set_value(stroke_color_[3] * stroke_average_opacity_get() * gpl->opacity);
+
+ interp_v3_v3v3(col, stroke_color_, gpl->tintcolor, gpl->tintcolor[3]);
+ }
+
+ linearrgb_to_srgb_v3_v3(col, col);
+ stroke_hex = rgb_to_hexstr(col);
+
+ node_gps.append_attribute("fill").set_value(stroke_hex.c_str());
+ node_gps.append_attribute("stroke").set_value("none");
+
+ std::string txt = "M";
+ for (const int i : IndexRange(gps->totpoints)) {
+ if (i > 0) {
+ txt.append("L");
+ }
+ bGPDspoint &pt = gps->points[i];
+ const float2 screen_co = gpencil_3D_point_to_2D(&pt.x);
+ txt.append(std::to_string(screen_co.x) + "," + std::to_string(screen_co.y));
+ }
+ /* Close patch (cyclic)*/
+ if (gps->flag & GP_STROKE_CYCLIC) {
+ txt.append("z");
+ }
+
+ node_gps.append_attribute("d").set_value(txt.c_str());
+}
+
+/**
+ * Export a stroke using polyline or polygon
+ * \param node_gpl: Node of the layer.
+ * \param do_fill: True if the stroke is only fill
+ */
+void GpencilExporterSVG::export_stroke_to_polyline(bGPDlayer *gpl,
+ bGPDstroke *gps,
+ pugi::xml_node node_gpl,
+ const bool is_stroke,
+ const bool do_fill)
+{
+ const bool cyclic = ((gps->flag & GP_STROKE_CYCLIC) != 0);
+ const float avg_pressure = BKE_gpencil_stroke_average_pressure_get(gps);
+
+ /* Get the thickness in pixels using a simple 1 point stroke. */
+ bGPDstroke *gps_temp = BKE_gpencil_stroke_duplicate(gps, false, false);
+ gps_temp->totpoints = 1;
+ gps_temp->points = (bGPDspoint *)MEM_callocN(sizeof(bGPDspoint), "gp_stroke_points");
+ bGPDspoint *pt_src = &gps->points[0];
+ bGPDspoint *pt_dst = &gps_temp->points[0];
+ copy_v3_v3(&pt_dst->x, &pt_src->x);
+ pt_dst->pressure = avg_pressure;
+
+ const float radius = stroke_point_radius_get(gpl, gps_temp);
+
+ BKE_gpencil_free_stroke(gps_temp);
+
+ pugi::xml_node node_gps = node_gpl.append_child(do_fill || cyclic ? "polygon" : "polyline");
+
+ color_string_set(gpl, gps, node_gps, do_fill);
+
+ if (is_stroke && !do_fill) {
+ node_gps.append_attribute("stroke-width").set_value((radius * 2.0f) - gpl->line_change);
+ }
+
+ std::string txt;
+ for (const int i : IndexRange(gps->totpoints)) {
+ if (i > 0) {
+ txt.append(" ");
+ }
+ bGPDspoint *pt = &gps->points[i];
+ const float2 screen_co = gpencil_3D_point_to_2D(&pt->x);
+ txt.append(std::to_string(screen_co.x) + "," + std::to_string(screen_co.y));
+ }
+
+ node_gps.append_attribute("points").set_value(txt.c_str());
+}
+
+/**
+ * Set color SVG string for stroke
+ * \param node_gps: Stroke node
+ * @param do_fill: True if the stroke is only fill
+ */
+void GpencilExporterSVG::color_string_set(bGPDlayer *gpl,
+ bGPDstroke *gps,
+ pugi::xml_node node_gps,
+ const bool do_fill)
+{
+ const bool round_cap = (gps->caps[0] == GP_STROKE_CAP_ROUND ||
+ gps->caps[1] == GP_STROKE_CAP_ROUND);
+
+ float col[3];
+ if (do_fill) {
+ interp_v3_v3v3(col, fill_color_, gpl->tintcolor, gpl->tintcolor[3]);
+ linearrgb_to_srgb_v3_v3(col, col);
+ std::string stroke_hex = rgb_to_hexstr(col);
+ node_gps.append_attribute("fill").set_value(stroke_hex.c_str());
+ node_gps.append_attribute("stroke").set_value("none");
+ node_gps.append_attribute("fill-opacity").set_value(fill_color_[3] * gpl->opacity);
+ }
+ else {
+ interp_v3_v3v3(col, stroke_color_, gpl->tintcolor, gpl->tintcolor[3]);
+ linearrgb_to_srgb_v3_v3(col, col);
+ std::string stroke_hex = rgb_to_hexstr(col);
+ node_gps.append_attribute("stroke").set_value(stroke_hex.c_str());
+ node_gps.append_attribute("stroke-opacity")
+ .set_value(stroke_color_[3] * stroke_average_opacity_get() * gpl->opacity);
+
+ if (gps->totpoints > 1) {
+ node_gps.append_attribute("fill").set_value("none");
+ node_gps.append_attribute("stroke-linecap").set_value(round_cap ? "round" : "square");
+ }
+ else {
+ node_gps.append_attribute("fill").set_value(stroke_hex.c_str());
+ node_gps.append_attribute("fill-opacity").set_value(fill_color_[3] * gpl->opacity);
+ }
+ }
+}
+
+/**
+ * Create a SVG rectangle
+ * \param node: Parent node
+ * \param x: X location
+ * \param y: Y location
+ * \param width: width of the recntagle
+ * \param height: Height of the rectangle
+ * \param thickness: Thickness of the line
+ * \param hexcolor: Color of the line
+ */
+void GpencilExporterSVG::add_rect(pugi::xml_node node,
+ float x,
+ float y,
+ float width,
+ float height,
+ float thickness,
+ std::string hexcolor)
+{
+ pugi::xml_node rect_node = node.append_child("rect");
+ rect_node.append_attribute("x").set_value(x);
+ rect_node.append_attribute("y").set_value(y);
+ rect_node.append_attribute("width").set_value(width);
+ rect_node.append_attribute("height").set_value(height);
+ rect_node.append_attribute("fill").set_value("none");
+ if (thickness > 0.0f) {
+ rect_node.append_attribute("stroke").set_value(hexcolor.c_str());
+ rect_node.append_attribute("stroke-width").set_value(thickness);
+ }
+}
+
+/**
+ * Create SVG text
+ * \param node: Parent node
+ * \param x: X location
+ * \param y: Y location
+ * \param text: Text to include
+ * \param size: Size of th etext
+ * \param hexcolor: Color of the text
+ */
+void GpencilExporterSVG::add_text(pugi::xml_node node,
+ float x,
+ float y,
+ std::string text,
+ const float size,
+ std::string hexcolor)
+{
+ pugi::xml_node nodetxt = node.append_child("text");
+
+ nodetxt.append_attribute("x").set_value(x);
+ nodetxt.append_attribute("y").set_value(y);
+ // nodetxt.append_attribute("font-family").set_value("'system-ui'");
+ nodetxt.append_attribute("font-size").set_value(size);
+ nodetxt.append_attribute("fill").set_value(hexcolor.c_str());
+ nodetxt.text().set(text.c_str());
+}
+
+/** Convert a color to Hex value (#FFFFFF). */
+std::string GpencilExporterSVG::rgb_to_hexstr(float color[3])
+{
+ uint8_t r = color[0] * 255.0f;
+ uint8_t g = color[1] * 255.0f;
+ uint8_t b = color[2] * 255.0f;
+ char hex_string[20];
+ sprintf(hex_string, "#%02X%02X%02X", r, g, b);
+
+ std::string hexstr = hex_string;
+
+ return hexstr;
+}
+
+} // namespace blender::io::gpencil