diff options
Diffstat (limited to 'src/extension')
231 files changed, 72366 insertions, 0 deletions
diff --git a/src/extension/CMakeLists.txt b/src/extension/CMakeLists.txt new file mode 100644 index 0000000..867d235 --- /dev/null +++ b/src/extension/CMakeLists.txt @@ -0,0 +1,259 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +set(extension_SRC + db.cpp + dependency.cpp + effect.cpp + execution-env.cpp + extension.cpp + init.cpp + input.cpp + output.cpp + patheffect.cpp + print.cpp + system.cpp + timer.cpp + loader.cpp + + implementation/implementation.cpp + implementation/xslt.cpp + implementation/script.cpp + + internal/bluredge.cpp + internal/cairo-ps-out.cpp + internal/cairo-render-context.cpp + internal/cairo-renderer.cpp + internal/cairo-renderer-pdf-out.cpp + internal/emf-inout.cpp + internal/emf-print.cpp + internal/gdkpixbuf-input.cpp + internal/gimpgrad.cpp + internal/grid.cpp + internal/image-resolution.cpp + internal/latex-pstricks.cpp + internal/latex-pstricks-out.cpp + internal/metafile-inout.cpp + internal/metafile-print.cpp + internal/odf.cpp + internal/latex-text-renderer.cpp + internal/png-output.cpp + internal/pov-out.cpp + internal/svg.cpp + internal/svgz.cpp + internal/text_reassemble.c + internal/wmf-inout.cpp + internal/wmf-print.cpp + + internal/filter/filter-all.cpp + internal/filter/filter-file.cpp + internal/filter/filter.cpp + + internal/pdfinput/pdf-input.cpp + internal/pdfinput/pdf-parser.cpp + internal/pdfinput/svg-builder.cpp + + prefdialog/prefdialog.cpp + prefdialog/parameter.cpp + prefdialog/parameter-bool.cpp + prefdialog/parameter-color.cpp + prefdialog/parameter-float.cpp + prefdialog/parameter-int.cpp + prefdialog/parameter-notebook.cpp + prefdialog/parameter-optiongroup.cpp + prefdialog/parameter-path.cpp + prefdialog/parameter-string.cpp + prefdialog/widget.cpp + prefdialog/widget-box.cpp + prefdialog/widget-image.cpp + prefdialog/widget-label.cpp + prefdialog/widget-separator.cpp + prefdialog/widget-spacer.cpp + + # ------ + # Header + db.h + dependency.h + effect.h + execution-env.h + extension.h + init.h + input.h + output.h + patheffect.h + print.h + system.h + timer.h + loader.h + + implementation/implementation.h + implementation/script.h + implementation/xslt.h + + internal/bluredge.h + internal/cairo-ps-out.h + internal/cairo-render-context.h + internal/cairo-renderer-pdf-out.h + internal/cairo-renderer.h + internal/clear-n_.h + internal/emf-inout.h + internal/emf-print.h + internal/filter/bevels.h + internal/filter/blurs.h + internal/filter/bumps.h + internal/filter/color.h + internal/filter/distort.h + internal/filter/filter.h + internal/filter/image.h + internal/filter/morphology.h + internal/filter/overlays.h + internal/filter/paint.h + internal/filter/protrusions.h + internal/filter/shadows.h + internal/filter/textures.h + internal/filter/transparency.h + internal/gdkpixbuf-input.h + internal/gimpgrad.h + internal/grid.h + internal/image-resolution.h + internal/latex-pstricks-out.h + internal/latex-pstricks.h + internal/latex-text-renderer.h + internal/metafile-inout.h + internal/metafile-print.h + internal/odf.h + internal/pdfinput/pdf-input.h + internal/pdfinput/pdf-parser.h + internal/pdfinput/svg-builder.h + internal/png-output.h + internal/pov-out.h + internal/svg.h + internal/svgz.h + internal/text_reassemble.h + internal/wmf-inout.h + internal/wmf-print.h + + prefdialog/prefdialog.h + prefdialog/parameter.h + prefdialog/parameter-bool.h + prefdialog/parameter-color.h + prefdialog/parameter-float.h + prefdialog/parameter-int.h + prefdialog/parameter-notebook.h + prefdialog/parameter-optiongroup.h + prefdialog/parameter-path.h + prefdialog/parameter-string.h + prefdialog/widget.h + prefdialog/widget-box.h + prefdialog/widget-image.h + prefdialog/widget-label.h + prefdialog/widget-separator.h + prefdialog/widget-spacer.h +) + +if(WIN32) + list(APPEND extension_SRC + ) +endif() + +if(WITH_LIBCDR) + list(APPEND extension_SRC + internal/cdr-input.cpp + internal/cdr-input.h + ) +endif() + +if(WITH_LIBVISIO) + list(APPEND extension_SRC + internal/vsd-input.cpp + internal/vsd-input.h + ) +endif() + +if(WITH_LIBWPG) + list(APPEND extension_SRC + internal/wpg-input.cpp + internal/wpg-input.h + ) +endif() + +if(WITH_MAGICK) + list(APPEND extension_SRC + internal/bitmap/adaptiveThreshold.cpp + internal/bitmap/adaptiveThreshold.h + internal/bitmap/addNoise.cpp + internal/bitmap/addNoise.h + internal/bitmap/blur.cpp + internal/bitmap/blur.h + internal/bitmap/channel.cpp + internal/bitmap/channel.h + internal/bitmap/charcoal.cpp + internal/bitmap/charcoal.h + internal/bitmap/colorize.cpp + internal/bitmap/colorize.h + internal/bitmap/contrast.cpp + internal/bitmap/contrast.h + internal/bitmap/crop.cpp + internal/bitmap/crop.h + internal/bitmap/cycleColormap.cpp + internal/bitmap/cycleColormap.h + internal/bitmap/despeckle.cpp + internal/bitmap/despeckle.h + internal/bitmap/edge.cpp + internal/bitmap/edge.h + internal/bitmap/emboss.cpp + internal/bitmap/emboss.h + internal/bitmap/enhance.cpp + internal/bitmap/enhance.h + internal/bitmap/equalize.cpp + internal/bitmap/equalize.h + internal/bitmap/gaussianBlur.cpp + internal/bitmap/gaussianBlur.h + internal/bitmap/imagemagick.cpp + internal/bitmap/imagemagick.h + internal/bitmap/implode.cpp + internal/bitmap/implode.h + internal/bitmap/level.cpp + internal/bitmap/level.h + internal/bitmap/levelChannel.cpp + internal/bitmap/levelChannel.h + internal/bitmap/medianFilter.cpp + internal/bitmap/medianFilter.h + internal/bitmap/modulate.cpp + internal/bitmap/modulate.h + internal/bitmap/negate.cpp + internal/bitmap/negate.h + internal/bitmap/normalize.cpp + internal/bitmap/normalize.h + internal/bitmap/oilPaint.cpp + internal/bitmap/oilPaint.h + internal/bitmap/opacity.cpp + internal/bitmap/opacity.h + internal/bitmap/raise.cpp + internal/bitmap/raise.h + internal/bitmap/reduceNoise.cpp + internal/bitmap/reduceNoise.h + internal/bitmap/sample.cpp + internal/bitmap/sample.h + internal/bitmap/shade.cpp + internal/bitmap/shade.h + internal/bitmap/sharpen.cpp + internal/bitmap/sharpen.h + internal/bitmap/solarize.cpp + internal/bitmap/solarize.h + internal/bitmap/spread.cpp + internal/bitmap/spread.h + internal/bitmap/swirl.cpp + internal/bitmap/swirl.h + internal/bitmap/threshold.cpp + internal/bitmap/threshold.h + internal/bitmap/unsharpmask.cpp + internal/bitmap/unsharpmask.h + internal/bitmap/wave.cpp + internal/bitmap/wave.h + ) +endif() + +# add_inkscape_lib(extension_LIB "${extension_SRC}") +add_inkscape_source("${extension_SRC}") + +add_subdirectory( plugins ) diff --git a/src/extension/db.cpp b/src/extension/db.cpp new file mode 100644 index 0000000..1fb8763 --- /dev/null +++ b/src/extension/db.cpp @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Functions to keep a listing of all modules in the system. Has its + * own file mostly for abstraction reasons, but is pretty simple + * otherwise. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "db.h" + +#include "implementation/script.h" +#include "input.h" +#include "output.h" +#include "effect.h" + +/* Globals */ + +/* Namespaces */ + +namespace Inkscape { +namespace Extension { + +/** This is the actual database object. There is only one of these */ +DB db; + +/* Types */ + +DB::DB (void) = default; + + +struct ModuleInputCmp { + bool operator()(Input* module1, Input* module2) const { + + // Ensure SVG files are at top + int n1 = 0; + int n2 = 0; + // 12345678901234567890123456789012 + if (strncmp(module1->get_id(),"org.inkscape.input.svg", 22) == 0) n1 = 1; + if (strncmp(module2->get_id(),"org.inkscape.input.svg", 22) == 0) n2 = 1; + if (strncmp(module1->get_id(),"org.inkscape.input.svgz", 23) == 0) n1 = 2; + if (strncmp(module2->get_id(),"org.inkscape.input.svgz", 23) == 0) n2 = 2; + + if (n1 != 0 && n2 != 0) return (n1 < n2); + if (n1 != 0) return true; + if (n2 != 0) return false; + + // GDK filetypenames begin with lower case letters and thus are sorted at the end. + // Special case "sK1" which starts with a lower case letter to keep it out of GDK region. + if (strncmp(module1->get_id(),"org.inkscape.input.sk1", 22) == 0) { + return ( strcmp("SK1", module2->get_filetypename()) <= 0 ); + } + if (strncmp(module2->get_id(),"org.inkscape.input.sk1", 22) == 0) { + return ( strcmp(module1->get_filetypename(), "SK1" ) <= 0 ); + } + + return ( strcmp(module1->get_filetypename(), module2->get_filetypename()) <= 0 ); + } +}; + + +struct ModuleOutputCmp { + bool operator()(Output* module1, Output* module2) const { + + // Ensure SVG files are at top + int n1 = 0; + int n2 = 0; + // 12345678901234567890123456789012 + if (strncmp(module1->get_id(),"org.inkscape.output.png.inkscape", 32) == 0) n1 = 1; + if (strncmp(module2->get_id(),"org.inkscape.output.png.inkscape", 32) == 0) n2 = 1; + if (strncmp(module1->get_id(),"org.inkscape.output.svg.inkscape", 32) == 0) n1 = 1; + if (strncmp(module2->get_id(),"org.inkscape.output.svg.inkscape", 32) == 0) n2 = 1; + if (strncmp(module1->get_id(),"org.inkscape.output.svg.plain", 29) == 0) n1 = 2; + if (strncmp(module2->get_id(),"org.inkscape.output.svg.plain", 29) == 0) n2 = 2; + if (strncmp(module1->get_id(),"org.inkscape.output.svgz.inkscape", 33) == 0) n1 = 3; + if (strncmp(module2->get_id(),"org.inkscape.output.svgz.inkscape", 33) == 0) n2 = 3; + if (strncmp(module1->get_id(),"org.inkscape.output.svgz.plain", 30) == 0) n1 = 4; + if (strncmp(module2->get_id(),"org.inkscape.output.svgz.plain", 30) == 0) n2 = 4; + if (strncmp(module1->get_id(),"org.inkscape.output.scour", 25) == 0) n1 = 5; + if (strncmp(module2->get_id(),"org.inkscape.output.scour", 25) == 0) n2 = 5; + if (strncmp(module1->get_id(),"org.inkscape.output.ZIP", 23) == 0) n1 = 6; + if (strncmp(module2->get_id(),"org.inkscape.output.ZIP", 23) == 0) n2 = 6; + if (strncmp(module1->get_id(),"org.inkscape.output.LAYERS", 26) == 0) n1 = 7; + if (strncmp(module2->get_id(),"org.inkscape.output.LAYERS", 26) == 0) n2 = 7; + + if (n1 != 0 && n2 != 0) return (n1 < n2); + if (n1 != 0) return true; + if (n2 != 0) return false; + + // Special case "sK1" which starts with a lower case letter. + if (strncmp(module1->get_id(),"org.inkscape.output.sk1", 23) == 0) { + return ( strcmp("SK1", module2->get_filetypename()) <= 0 ); + } + if (strncmp(module2->get_id(),"org.inkscape.output.sk1", 23) == 0) { + return ( strcmp(module1->get_filetypename(), "SK1" ) <= 0 ); + } + // special case: two extensions for the same file type. I only one of them is a script, prefer the other one + if (Glib::ustring(module1->get_extension()).lowercase() == Glib::ustring(module2->get_extension()).lowercase()) { + bool module1_is_script = dynamic_cast<Inkscape::Extension::Implementation::Script *>(module1->get_imp()); + bool module2_is_script = dynamic_cast<Inkscape::Extension::Implementation::Script *>(module2->get_imp()); + if (module1_is_script != module2_is_script) { + return module1_is_script ? false : true; + } + } + return ( strcmp(module1->get_filetypename(), module2->get_filetypename()) <= 0 ); + } +}; + + +/** + \brief Add a module to the module database + \param module The module to be registered. +*/ +void +DB::register_ext (Extension *module) +{ + g_return_if_fail(module != nullptr); + g_return_if_fail(module->get_id() != nullptr); + + // only add to list if it's a never-before-seen module + bool add_to_list = + ( moduledict.find(module->get_id()) == moduledict.end()); + + //printf("Registering: '%s' '%s' add:%d\n", module->get_id(), module->get_name(), add_to_list); + moduledict[module->get_id()] = module; + + if (add_to_list) { + modulelist.push_back( module ); + } +} + +/** + \brief This function removes a module from the database + \param module The module to be removed. +*/ +void +DB::unregister_ext (Extension * module) +{ + g_return_if_fail(module != nullptr); + g_return_if_fail(module->get_id() != nullptr); + + // printf("Extension DB: removing %s\n", module->get_id()); + moduledict.erase(moduledict.find(module->get_id())); + // only remove if it's not there any more + if ( moduledict.find(module->get_id()) != moduledict.end()) + modulelist.remove(module); +} + +/** + \return A reference to the Inkscape::Extension::Extension specified by the input key. + \brief This function looks up a Inkscape::Extension::Extension by using its unique + id. It then returns a reference to that module. + \param key The unique ID of the module + + Retrieves a module by name; if non-NULL, it refs the returned + module; the caller is responsible for releasing that reference + when it is no longer needed. +*/ +Extension * +DB::get (const gchar *key) const +{ + if (key == nullptr) return nullptr; + + auto it = moduledict.find(key); + if (it == moduledict.end()) + return nullptr; + + Extension *mod = it->second; + assert(mod); + + if ( !mod || mod->deactivated() ) + return nullptr; + + return mod; +} + +/** + \return none + \brief A function to execute another function with every entry + in the database as a parameter. + \param in_func The function to execute for every module + \param in_data A data pointer that is also passed to in_func + + Enumerates the modules currently in the database, calling a given + callback for each one. +*/ +void +DB::foreach (void (*in_func)(Extension * in_plug, gpointer in_data), gpointer in_data) +{ + std::list <Extension *>::iterator cur; + + for (cur = modulelist.begin(); cur != modulelist.end(); ++cur) { + // printf("foreach: %s\n", (*cur)->get_id()); + in_func((*cur), in_data); + } +} + +/** + \return none + \brief The function to look at each module and see if it is + an input module, then add it to the list. + \param in_plug Module to be examined + \param data The list to be attached to + + The first thing that is checked is if this module is an input + module. If it is, then it is added to the list which is passed + in through \c data. +*/ +void +DB::input_internal (Extension * in_plug, gpointer data) +{ + if (dynamic_cast<Input *>(in_plug)) { + InputList * ilist; + Input * imod; + + imod = dynamic_cast<Input *>(in_plug); + ilist = reinterpret_cast<InputList *>(data); + + ilist->push_back(imod); + // printf("Added to input list: %s\n", imod->get_id()); + } +} + +/** + \return none + \brief The function to look at each module and see if it is + an output module, then add it to the list. + \param in_plug Module to be examined + \param data The list to be attached to + + The first thing that is checked is if this module is an output + module. If it is, then it is added to the list which is passed + in through \c data. +*/ +void +DB::output_internal (Extension * in_plug, gpointer data) +{ + if (dynamic_cast<Output *>(in_plug)) { + OutputList * olist; + Output * omod; + + omod = dynamic_cast<Output *>(in_plug); + olist = reinterpret_cast<OutputList *>(data); + + olist->push_back(omod); + // printf("Added to output list: %s\n", omod->get_id()); + } + + return; +} + +/** + \return none + \brief The function to look at each module and see if it is + an effect module, then add it to the list. + \param in_plug Module to be examined + \param data The list to be attached to + + The first thing that is checked is if this module is an effect + module. If it is, then it is added to the list which is passed + in through \c data. +*/ +void +DB::effect_internal (Extension * in_plug, gpointer data) +{ + if (dynamic_cast<Effect *>(in_plug)) { + EffectList * elist; + Effect * emod; + + emod = dynamic_cast<Effect *>(in_plug); + elist = reinterpret_cast<EffectList *>(data); + + elist->push_back(emod); + // printf("Added to effect list: %s\n", emod->get_id()); + } + + return; +} + +/** + \brief Creates a list of all the Input extensions + \param ou_list The list that is used to put all the extensions in + + Calls the database \c foreach function with \c input_internal. +*/ +DB::InputList & +DB::get_input_list (DB::InputList &ou_list) +{ + foreach(input_internal, (gpointer)&ou_list); + ou_list.sort( ModuleInputCmp() ); + return ou_list; +} + +/** + \brief Creates a list of all the Output extensions + \param ou_list The list that is used to put all the extensions in + + Calls the database \c foreach function with \c output_internal. +*/ +DB::OutputList & +DB::get_output_list (DB::OutputList &ou_list) +{ + foreach(output_internal, (gpointer)&ou_list); + ou_list.sort( ModuleOutputCmp() ); + return ou_list; +} + +/** + \brief Creates a list of all the Effect extensions + \param ou_list The list that is used to put all the extensions in + + Calls the database \c foreach function with \c effect_internal. +*/ +DB::EffectList & +DB::get_effect_list (DB::EffectList &ou_list) +{ + foreach(effect_internal, (gpointer)&ou_list); + return ou_list; +} + +} } /* namespace Extension, Inkscape */ diff --git a/src/extension/db.h b/src/extension/db.h new file mode 100644 index 0000000..d585180 --- /dev/null +++ b/src/extension/db.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Functions to keep a listing of all modules in the system. Has its + * own file mostly for abstraction reasons, but is pretty simple + * otherwise. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_MODULES_DB_H +#define SEEN_MODULES_DB_H + +#include <map> +#include <list> +#include <cstring> + +#include <glib.h> + + +namespace Inkscape { +namespace Extension { + +class Input; +class Output; +class Effect; +class Extension; + +class DB { +private: + /** A string comparison function to be used in the moduledict + to find the different extensions in the hash map. */ + struct ltstr { + bool operator()(const char* s1, const char* s2) const { + if ( (s1 == nullptr) && (s2 != nullptr) ) { + return true; + } else if (s1 == nullptr || s2 == nullptr) { + return false; + } else { + return strcmp(s1, s2) < 0; + } + } + }; + /** This is the actual database. It has all of the modules in it, + indexed by their ids. It's a hash table for faster lookups */ + std::map <const char *, Extension *, ltstr> moduledict; + /** Maintain an ordered list of modules for generating the extension + lists via "foreach" */ + std::list <Extension *> modulelist; + + static void foreach_internal (gpointer in_key, gpointer in_value, gpointer in_data); + +public: + DB (); + Extension * get (const gchar *key) const; + void register_ext (Extension *module); + void unregister_ext (Extension *module); + void foreach (void (*in_func)(Extension * in_plug, gpointer in_data), gpointer in_data); + +private: + static void input_internal (Extension * in_plug, gpointer data); + static void output_internal (Extension * in_plug, gpointer data); + static void effect_internal (Extension * in_plug, gpointer data); + +public: + typedef std::list<Output *> OutputList; + typedef std::list<Input *> InputList; + typedef std::list<Effect *> EffectList; + + InputList &get_input_list (InputList &ou_list); + OutputList &get_output_list (OutputList &ou_list); + EffectList &get_effect_list (EffectList &ou_list); +}; /* class DB */ + +extern DB db; + +} } /* namespace Extension, Inkscape */ + +#endif // SEEN_MODULES_DB_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/dependency.cpp b/src/extension/dependency.cpp new file mode 100644 index 0000000..09928c6 --- /dev/null +++ b/src/extension/dependency.cpp @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Johan Engelen, johan@shouraizou.nl + * Copyright (C) 2004 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> +#include "dependency.h" +#include "db.h" +#include "extension.h" +#include "io/resource.h" + +namespace Inkscape { +namespace Extension { + +// These strings are for XML attribute comparisons and should not be translated; +// make sure to keep in sync with enum defined in dependency.h +gchar const * Dependency::_type_str[] = { + "executable", + "file", + "extension", +}; + +// These strings are for XML attribute comparisons and should not be translated +// make sure to keep in sync with enum defined in dependency.h +gchar const * Dependency::_location_str[] = { + "path", + "extensions", + "inx", + "absolute", +}; + +/** + \brief Create a dependency using an XML definition + \param in_repr XML definition of the dependency + \param extension Reference to the extension requesting this dependency + \param default_type Default file type of the dependency (unless overridden by XML definition's "type" attribute) + + This function mostly looks for the 'location' and 'type' attributes + and turns them into the enums of the same name. This makes things + a little bit easier to use later. Also, a pointer to the core + content is pulled out -- also to make things easier. +*/ +Dependency::Dependency (Inkscape::XML::Node * in_repr, const Extension *extension, type_t default_type) + : _repr(in_repr) + , _extension(extension) + , _type(default_type) +{ + Inkscape::GC::anchor(_repr); + + if (const gchar * location = _repr->attribute("location")) { + for (int i = 0; i < LOCATION_CNT && location != nullptr; i++) { + if (!strcmp(location, _location_str[i])) { + _location = (location_t)i; + break; + } + } + } else if (const gchar * location = _repr->attribute("reldir")) { // backwards-compatibility + for (int i = 0; i < LOCATION_CNT && location != nullptr; i++) { + if (!strcmp(location, _location_str[i])) { + _location = (location_t)i; + break; + } + } + } + + const gchar * type = _repr->attribute("type"); + for (int i = 0; i < TYPE_CNT && type != nullptr; i++) { + if (!strcmp(type, _type_str[i])) { + _type = (type_t)i; + break; + } + } + + _string = _repr->firstChild()->content(); + + _description = _repr->attribute("description"); + if (_description == nullptr) + _description = _repr->attribute("_description"); + + return; +} + +/** + \brief This dependency is not longer needed + + Unreference the XML structure. +*/ +Dependency::~Dependency () +{ + Inkscape::GC::release(_repr); +} + +/** + \brief Check if the dependency passes. + \return Whether or not the dependency passes. + + This function depends largely on all of the enums. The first level + that is evaluated is the \c _type. + + If the type is \c TYPE_EXTENSION then the id for the extension is + looked up in the database. If the extension is found, and it is + not deactivated, the dependency passes. + + If the type is \c TYPE_EXECUTABLE or \c TYPE_FILE things are getting + even more interesting because now the \c _location variable is also + taken into account. First, the difference between the two is that + the file test for \c TYPE_EXECUTABLE also tests to make sure the + file is executable, besides checking that it exists. + + If the \c _location is \c LOCATION_EXTENSIONS then the \c INKSCAPE_EXTENSIONDIR + is put on the front of the string with \c build_filename. Then the + appropriate filetest is run. + + If the \c _location is \c LOCATION_ABSOLUTE then the file test is + run directly on the string. + + If the \c _location is \c LOCATION_PATH or not specified then the + path is used to find the file. Each entry in the path is stepped + through, attached to the string, and then tested. If the file is + found then a TRUE is returned. If we get all the way through the + path then a FALSE is returned, the command could not be found. +*/ +bool Dependency::check () +{ + if (_string == nullptr) { + return false; + } + + _absolute_location = ""; + + switch (_type) { + case TYPE_EXTENSION: { + Extension * myext = db.get(_string); + if (myext == nullptr) return false; + if (myext->deactivated()) return false; + break; + } + case TYPE_EXECUTABLE: + case TYPE_FILE: { + Glib::FileTest filetest = Glib::FILE_TEST_EXISTS; + + std::string location(_string); + + // get potential file extension for later usage + std::string extension; + size_t index = location.find_last_of("."); + if (index != std::string::npos) { + extension = location.substr(index); + } + + // check interpreted scripts as "file" for backwards-compatibility, even if "executable" was requested + static const std::vector<std::string> interpreted = {".py", ".pl", ".rb"}; + if (!extension.empty() && + std::find(interpreted.begin(), interpreted.end(), extension) != interpreted.end()) + { + _type = TYPE_FILE; + } + +#ifndef _WIN32 + // There's no executable bit on Windows, so this is unreliable + // glib would search for "executable types" instead, which are only {".exe", ".cmd", ".bat", ".com"}, + // and would therefore miss files without extension and other script files (like .py files) + if (_type == TYPE_EXECUTABLE) { + filetest = Glib::FILE_TEST_IS_EXECUTABLE; + } +#endif + + switch (_location) { + // backwards-compatibility: location="extensions" will be deprecated as of Inkscape 1.1, + // use location="inx" instead + case LOCATION_EXTENSIONS: { + // get_filename will warn if the resource isn't found, while returning an empty string. + std::string temploc = + Inkscape::IO::Resource::get_filename_string(Inkscape::IO::Resource::EXTENSIONS, location.c_str()); + if (!temploc.empty()) { + location = temploc; + _absolute_location = temploc; + break; + } + /* Look for deprecated locations next */ + auto deprloc = g_build_filename("inkex", "deprecated-simple", location.c_str(), nullptr); + std::string tempdepr = + Inkscape::IO::Resource::get_filename_string(Inkscape::IO::Resource::EXTENSIONS, deprloc, false, true); + g_free(deprloc); + if (!tempdepr.empty()) { + location = tempdepr; + _absolute_location = tempdepr; + break; + } + // PASS THROUGH!!! - also check inx location for backwards-compatibility, + // notably to make extension manager work + // (installs into subfolders of "extensions" directory) + } + case LOCATION_INX: { + std::string base_directory = _extension->get_base_directory(); + if (base_directory.empty()) { + g_warning("Dependency '%s' requests location relative to .inx file, " + "which is unknown for extension '%s'", _string, _extension->get_id()); + } + std::string absolute_location = Glib::build_filename(base_directory, location); + if (!Glib::file_test(absolute_location, filetest)) { + return false; + } + _absolute_location = absolute_location; + break; + } + case LOCATION_ABSOLUTE: { + // TODO: should we check if the directory actually is absolute and/or sanitize the filename somehow? + if (!Glib::file_test(location, filetest)) { + return false; + } + _absolute_location = location; + break; + } + /* The default case is to look in the path */ + case LOCATION_PATH: + default: { + // TODO: we can likely use g_find_program_in_path (or its glibmm equivalent) for executable types + + gchar * path = g_strdup(g_getenv("PATH")); + + if (path == nullptr) { + /* There is no `PATH' in the environment. + The default search path is the current directory */ + path = g_strdup(G_SEARCHPATH_SEPARATOR_S); + } + + gchar * orig_path = path; + + for (; path != nullptr;) { + gchar * local_path; // to have the path after detection of the separator + std::string final_name; + + local_path = path; + path = g_utf8_strchr(path, -1, G_SEARCHPATH_SEPARATOR); + /* Not sure whether this is UTF8 happy, but it would seem + like it considering that I'm searching (and finding) + the ':' character */ + if (path != nullptr) { + path[0] = '\0'; + path++; + } + + if (*local_path == '\0') { + final_name = _string; + } else { + final_name = Glib::build_filename(local_path, _string); + } + + if (Glib::file_test(final_name, filetest)) { + g_free(orig_path); + _absolute_location = final_name; + return true; + } + +#ifdef _WIN32 + // Unfortunately file extensions tend to be different on Windows and we can't know + // which one it is, so try all extensions glib assumes to be executable. + // As we can only guess here, return the version without extension if either one is found, + // so that we don't accidentally override (or conflict with) some g_spawn_* magic. + if (_type == TYPE_EXECUTABLE) { + static const std::vector<std::string> extensions = {".exe", ".cmd", ".bat", ".com"}; + if (extension.empty() || + std::find(extensions.begin(), extensions.end(), extension) == extensions.end()) + { + for (auto extension : extensions) { + if (Glib::file_test(final_name + extension, filetest)) { + g_free(orig_path); + _absolute_location = final_name; + return true; + } + } + } + } +#endif + } + + g_free(orig_path); + return false; /* Reverse logic in this one */ + } + } /* switch _location */ + break; + } /* TYPE_FILE, TYPE_EXECUTABLE */ + default: + return false; + } /* switch _type */ + + return true; +} + +/** + \brief Accessor to the name attribute. + \return A string containing the name of the dependency. + + Returns the name of the dependency as found in the configuration file. + +*/ +const gchar* Dependency::get_name() +{ + return _string; +} + +/** + \brief Path of this dependency + \return Absolute path to the dependency file + (or an empty string if dependency was not found or is of TYPE_EXTENSION) + + Returns the verified absolute path of the dependency file. + This value is only available after checking the Dependency by calling Dependency::check(). +*/ +std::string Dependency::get_path() +{ + if (_type == TYPE_EXTENSION) { + g_warning("Requested absolute path of dependency '%s' which is of 'extension' type.", _string); + return ""; + } + if (_absolute_location == UNCHECKED) { + g_warning("Requested absolute path of dependency '%s' which is unchecked.", _string); + return ""; + } + + return _absolute_location; +} + +/** + \brief Print out a dependency to a string. +*/ +Glib::ustring Dependency::info_string() +{ + Glib::ustring str = Glib::ustring::compose("%1:\n\t%2: %3\n\t%4: %5\n\t%6: %7", + _("Dependency"), + _("type"), _(_type_str[_type]), + _("location"), _(_location_str[_location]), + _("string"), _string); + + if (_description) { + str += Glib::ustring::compose("\n\t%1: %2\n", _(" description: "), _(_description)); + } + + return str; +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/dependency.h b/src/extension/dependency.h new file mode 100644 index 0000000..e2c2791 --- /dev/null +++ b/src/extension/dependency.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_DEPENDENCY_H__ +#define INKSCAPE_EXTENSION_DEPENDENCY_H__ + +#include <glibmm/ustring.h> +#include "xml/repr.h" + +namespace Inkscape { +namespace Extension { + +class Extension; + +/** \brief A class to represent a dependency for an extension. There + are different things that can be done in a dependency, and + this class takes care of all of them. */ +class Dependency { + +public: + /** \brief All the possible types of dependencies. */ + enum type_t { + TYPE_EXECUTABLE, /**< Look for an executable */ + TYPE_FILE, /**< Look to make sure a file exists */ + TYPE_EXTENSION, /**< Make sure a specific extension is loaded and functional */ + TYPE_CNT /**< Number of types */ + }; + + /** \brief All of the possible locations to look for the dependency. */ + enum location_t { + LOCATION_PATH, /**< Look in the PATH for this dependency - historically this is the default + (it's a bit odd for interpreted script files but makes sense for other executables) */ + LOCATION_EXTENSIONS, /**< Look in the extensions directory + (note: this can be in both, user and system locations!) */ + LOCATION_INX, /**< Look relative to the inx file's location */ + LOCATION_ABSOLUTE, /**< This dependency is already defined in absolute terms */ + LOCATION_CNT /**< Number of locations to look */ + }; + +private: + static constexpr const char *UNCHECKED = "---unchecked---"; + + /** \brief The XML representation of the dependency. */ + Inkscape::XML::Node * _repr; + /** \brief The string that is in the XML tags pulled out. */ + const gchar * _string = nullptr; + /** \brief The description of the dependency for the users. */ + const gchar * _description = nullptr; + /** \brief The absolute path to the dependency file determined while checking this dependency. */ + std::string _absolute_location = UNCHECKED; + + /** \brief Storing the type of this particular dependency. */ + type_t _type = TYPE_FILE; + /** \brief The location to look for this particular dependency. */ + location_t _location = LOCATION_PATH; + + /** \brief Strings to represent the different enum values in + \c type_t in the XML */ + static gchar const * _type_str[TYPE_CNT]; + /** \brief Strings to represent the different enum values in + \c location_t in the XML */ + static gchar const * _location_str[LOCATION_CNT]; + + /** \brief Reference to the extension requesting this dependency. */ + const Extension *_extension; + +public: + Dependency (Inkscape::XML::Node *in_repr, const Extension *extension, type_t type=TYPE_FILE); + virtual ~Dependency (); + bool check(); + const gchar* get_name(); + std::string get_path(); + + Glib::ustring info_string(); +}; /* class Dependency */ + + +} } /* namespace Extension, Inkscape */ + +#endif /* INKSCAPE_EXTENSION_DEPENDENCY_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/effect.cpp b/src/extension/effect.cpp new file mode 100644 index 0000000..5d78381 --- /dev/null +++ b/src/extension/effect.cpp @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * Abhishek Sharma + * Sushant A.A. <sushant.co19@gmail.com> + * + * Copyright (C) 2002-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "effect.h" + +#include "execution-env.h" +#include "inkscape.h" +#include "timer.h" + +#include "implementation/implementation.h" +#include "prefdialog/prefdialog.h" +#include "ui/view/view.h" +#include "inkscape-application.h" + + +/* Inkscape::Extension::Effect */ + +namespace Inkscape { +namespace Extension { + +Effect * Effect::_last_effect = nullptr; + +/** + * Adds effect to Gio::Actions + * + * \c effect is Filter or Extension + * \c show_prefs is used to show preferences dialog +*/ +void +action_effect (Effect* effect, bool show_prefs) +{ + if (effect->_workingDialog && show_prefs) { + effect->prefs(InkscapeApplication::instance()->get_active_view()); + } else { + effect->effect(InkscapeApplication::instance()->get_active_view()); + } +} + +// Modifying string to get submenu id +std::string +action_menu_name (std::string menu) +{ + transform(menu.begin(), menu.end(), menu.begin(), ::tolower); + for (auto &x:menu) { + if (x==' ') { + x = '-'; + } + } + return menu; +} + +Effect::Effect (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) + , _menu_node(nullptr), _workingDialog(true) + , _prefDialog(nullptr) +{ + Inkscape::XML::Node * local_effects_menu = nullptr; + + // can't use document level because it is not defined + static auto app = InkscapeApplication::instance(); + + if (!app) { + // This happens during tests. + // std::cerr << "Effect::Effect:: no app!" << std::endl; + return; + } + + if (!Inkscape::Application::exists()) { + return; + } + + // This is a weird hack + if (!strcmp(this->get_id(), "org.inkscape.filter.dropshadow")) + return; + + bool hidden = false; + + no_doc = false; + no_live_preview = false; + + // Setting initial value of description to name of action incase if there is no description + Glib::ustring description = get_name(); + + if (repr != nullptr) { + + for (Inkscape::XML::Node *child = repr->firstChild(); child != nullptr; child = child->next()) { + if (!strcmp(child->name(), INKSCAPE_EXTENSION_NS "effect")) { + if (child->attribute("needs-document") && !strcmp(child->attribute("needs-document"), "false")) { + no_doc = true; + } + if (child->attribute("needs-live-preview") && !strcmp(child->attribute("needs-live-preview"), "false")) { + no_live_preview = true; + } + if (child->attribute("implements-custom-gui") && !strcmp(child->attribute("implements-custom-gui"), "true")) { + _workingDialog = false; + } + for (Inkscape::XML::Node *effect_child = child->firstChild(); effect_child != nullptr; effect_child = effect_child->next()) { + if (!strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "effects-menu")) { + // printf("Found local effects menu in %s\n", this->get_name()); + local_effects_menu = effect_child->firstChild(); + if (effect_child->attribute("hidden") && !strcmp(effect_child->attribute("hidden"), "true")) { + hidden = true; + } + } + if (!strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "menu-tip") || + !strcmp(effect_child->name(), INKSCAPE_EXTENSION_NS "_menu-tip")) { + // printf("Found local effects menu in %s\n", this->get_name()); + description = effect_child->firstChild()->content(); + } + } // children of "effect" + break; // there can only be one effect + } // find "effect" + } // children of "inkscape-extension" + } // if we have an XML file + + // Replace any dashes with underscores + std::string aid = std::string(get_id()); + std::replace(aid.begin(), aid.end(), '_', '-'); + std::string action_id = "app." + aid; + + static auto gapp = InkscapeApplication::instance()->gtk_app(); + if (gapp) { + // Might be in command line mode without GUI (testing). + gapp->add_action( aid, sigc::bind<Effect*>(sigc::ptr_fun(&action_effect), this, true)); + gapp->add_action( aid + ".noprefs", sigc::bind<Effect*>(sigc::ptr_fun(&action_effect), this, false)); + } + + if (!hidden) { + // Submenu retrieval as a list of strings (to handle nested menus). + std::list<Glib::ustring> sub_menu_list; + get_menu(local_effects_menu, sub_menu_list); + + if (local_effects_menu && local_effects_menu->attribute("name") && !strcmp(local_effects_menu->attribute("name"), ("Filters"))) { + + std::vector<std::vector<Glib::ustring>>raw_data_filter = + {{ action_id, get_name(), "Filter", description }, + { action_id + ".noprefs", Glib::ustring(get_name()) + " " + _("(No preferences)"), "Filter", description }}; + app->get_action_extra_data().add_data(raw_data_filter); + + } else { + + std::vector<std::vector<Glib::ustring>>raw_data_effect = + {{ action_id, get_name(), "Effect", description }, + { action_id + ".noprefs", Glib::ustring(get_name()) + " " + _("(No preferences)"), "Effect", description }}; + app->get_action_extra_data().add_data(raw_data_effect); + + sub_menu_list.push_front("Effects"); + } + + // std::cout << " Effect: name: " << get_name(); + // std::cout << " id: " << aid.c_str(); + // std::cout << " menu: "; + // for (auto sub_menu : sub_menu_list) { + // std::cout << "|" << sub_menu.raw(); // Must use raw() as somebody has messed up encoding. + // } + // std::cout << "|" << std::endl; + + // Add submenu to effect data + gchar *ellipsized_name = widget_visible_count() ? g_strdup_printf(_("%s..."), get_name()) : nullptr; + Glib::ustring menu_name = ellipsized_name ? ellipsized_name : get_name(); + app->get_action_effect_data().add_data(aid, sub_menu_list, menu_name); + g_free(ellipsized_name); + } +} + +void +Effect::get_menu (Inkscape::XML::Node * pattern, std::list<Glib::ustring>& sub_menu_list) +{ + if (!pattern) { + return; + } + + Glib::ustring merge_name; + + gchar const *menu_name = pattern->attribute("name"); + if (!menu_name) { + menu_name = pattern->attribute("_name"); + } + if (!menu_name) { + return; + } + + if (_translation_enabled) { + merge_name = get_translation(menu_name); + } else { + merge_name = _(menu_name); + } + + // Making sub menu string + sub_menu_list.push_back(merge_name); + + get_menu(pattern->firstChild(), sub_menu_list); +} + +Effect::~Effect () +{ + if (get_last_effect() == this) + set_last_effect(nullptr); + if (_menu_node) { + if (_menu_node->parent()) { + _menu_node->parent()->removeChild(_menu_node); + } + Inkscape::GC::release(_menu_node); + } + return; +} + +bool +Effect::prefs (Inkscape::UI::View::View * doc) +{ + if (_prefDialog != nullptr) { + _prefDialog->raise(); + return true; + } + + if (widget_visible_count() == 0) { + effect(doc); + return true; + } + + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return false; + + Glib::ustring name = this->get_name(); + _prefDialog = new PrefDialog(name, nullptr, this); + _prefDialog->show(); + + return true; +} + +/** + \brief The function that 'does' the effect itself + \param doc The Inkscape::UI::View::View to do the effect on + + This function first insures that the extension is loaded, and if not, + loads it. It then calls the implementation to do the actual work. It + also resets the last effect pointer to be this effect. Finally, it + executes a \c SPDocumentUndo::done to commit the changes to the undo + stack. +*/ +void +Effect::effect (Inkscape::UI::View::View * doc) +{ + //printf("Execute effect\n"); + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return; + ExecutionEnv executionEnv(this, doc, nullptr, _workingDialog, true); + execution_env = &executionEnv; + timer->lock(); + executionEnv.run(); + if (executionEnv.wait()) { + executionEnv.commit(); + } else { + executionEnv.cancel(); + } + timer->unlock(); + + return; +} + +/** \brief Sets which effect was called last + \param in_effect The effect that has been called + + This function sets the static variable \c _last_effect + + If the \c in_effect variable is \c NULL then the last effect + verb is made insensitive. +*/ +void +Effect::set_last_effect (Effect * in_effect) +{ + _last_effect = in_effect; + return; +} + +Inkscape::XML::Node * +Effect::find_menu (Inkscape::XML::Node * menustruct, const gchar *name) +{ + if (menustruct == nullptr) return nullptr; + for (Inkscape::XML::Node * child = menustruct; + child != nullptr; + child = child->next()) { + if (!strcmp(child->name(), name)) { + return child; + } + Inkscape::XML::Node * firstchild = child->firstChild(); + if (firstchild != nullptr) { + Inkscape::XML::Node *found = find_menu (firstchild, name); + if (found) { + return found; + } + } + } + return nullptr; +} + + +Gtk::Box * +Effect::get_info_widget() +{ + return Extension::get_info_widget(); +} + +PrefDialog * +Effect::get_pref_dialog () +{ + return _prefDialog; +} + +void +Effect::set_pref_dialog (PrefDialog * prefdialog) +{ + _prefDialog = prefdialog; + return; +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/effect.h b/src/extension/effect.h new file mode 100644 index 0000000..b5e5b1f --- /dev/null +++ b/src/extension/effect.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_EXTENSION_EFFECT_H__ +#define INKSCAPE_EXTENSION_EFFECT_H__ + +#include <list> + +#include <glibmm/i18n.h> +#include "extension.h" +#include "inkscape-application.h" + +namespace Gtk { + class Box; +} + +class SPDocument; + +namespace Inkscape { + + +namespace Extension { +class PrefDialog; + +/** \brief Effects are extensions that take a document and do something + to it in place. This class adds the extra functions required + to make extensions effects. +*/ +class Effect : public Extension { + /** \brief This is the last effect that was used. This is used in + a menu item to rapidly recall the same effect. */ + static Effect * _last_effect; + + Inkscape::XML::Node *find_menu (Inkscape::XML::Node * menustruct, const gchar *name); + void get_menu (Inkscape::XML::Node * pattern, std::list<Glib::ustring>& sub_menu_list); + + /** \brief Menu node created for this effect */ + Inkscape::XML::Node * _menu_node; + + /** \brief The preference dialog if it is shown */ + PrefDialog * _prefDialog; + +public: + Effect(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Effect () override; + + bool prefs (Inkscape::UI::View::View * doc); + void effect (Inkscape::UI::View::View * doc); + + /** \brief Whether a working dialog should be shown */ + bool _workingDialog; + + /** \brief Static function to get the last effect used */ + static Effect * get_last_effect () { return _last_effect; }; + static void set_last_effect (Effect * in_effect); + + static void place_menus (); + void place_menu (Inkscape::XML::Node * menus); + + Gtk::Box * get_info_widget(); + + bool no_doc; // if true, the effect does not process SVG document at all, so no need to save, read, and watch for errors + bool no_live_preview; // if true, the effect does not need "live preview" checkbox in its dialog + + PrefDialog *get_pref_dialog (); + void set_pref_dialog (PrefDialog * prefdialog); +private: + static gchar * remove_ (gchar * instr); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_EFFECT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/execution-env.cpp b/src/extension/execution-env.cpp new file mode 100644 index 0000000..9d5cd3a --- /dev/null +++ b/src/extension/execution-env.cpp @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * Abhishek Sharma + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/dialog.h> +#include <gtkmm/messagedialog.h> + +#include "execution-env.h" +#include "prefdialog/prefdialog.h" +#include "implementation/implementation.h" + +#include "selection.h" +#include "effect.h" +#include "document.h" +#include "desktop.h" +#include "inkscape.h" +#include "document-undo.h" +#include "desktop.h" +#include "object/sp-namedview.h" + +#include "ui/widget/canvas.h" // To get window (perverse!) + +namespace Inkscape { +namespace Extension { + +/** \brief Create an execution environment that will allow the effect + to execute independently. + \param effect The effect that we should execute + \param doc The Document to execute on + \param docCache The cache created for that document + \param show_working Show the working dialog + \param show_error Show the error dialog (not working) + + Grabs the selection of the current document so that it can get + restored. Will generate a document cache if one isn't provided. +*/ +ExecutionEnv::ExecutionEnv (Effect * effect, Inkscape::UI::View::View * doc, Implementation::ImplementationDocumentCache * docCache, bool show_working, bool show_errors) : + _state(ExecutionEnv::INIT), + _visibleDialog(nullptr), + _mainloop(nullptr), + _doc(doc), + _docCache(docCache), + _effect(effect), + _show_working(show_working) +{ + SPDesktop *desktop = (SPDesktop *)_doc; + SPDocument *document = _doc->doc(); + if (document && desktop) { + // Temporarily prevent undo in this scope + Inkscape::DocumentUndo::ScopedInsensitive pauseUndo(document); + Inkscape::Selection *selection = desktop->getSelection(); + if (selection) { + // Make sure all selected objects have an ID attribute + selection->enforceIds(); + } + } + + genDocCache(); + + return; +} + +/** \brief Destroy an execution environment + + Destroys the dialog if created and the document cache. +*/ +ExecutionEnv::~ExecutionEnv () { + if (_visibleDialog != nullptr) { + _visibleDialog->hide(); + delete _visibleDialog; + _visibleDialog = nullptr; + } + killDocCache(); + return; +} + +/** \brief Generate a document cache if needed + + If there isn't one we create a new one from the implementation + from the effect's implementation. +*/ +void +ExecutionEnv::genDocCache () { + if (_docCache == nullptr) { + // printf("Gen Doc Cache\n"); + _docCache = _effect->get_imp()->newDocCache(_effect, _doc); + } + return; +} + +/** \brief Destroy a document cache + + Just delete it. +*/ +void +ExecutionEnv::killDocCache () { + if (_docCache != nullptr) { + // printf("Killed Doc Cache\n"); + delete _docCache; + _docCache = nullptr; + } + return; +} + +/** \brief Create the working dialog + + Builds the dialog with a message saying that the effect is working. + And make sure to connect to the cancel. +*/ +void +ExecutionEnv::createWorkingDialog () { + if (_visibleDialog != nullptr) { + _visibleDialog->hide(); + delete _visibleDialog; + _visibleDialog = nullptr; + } + + SPDesktop *desktop = (SPDesktop *)_doc; + Gtk::Widget *toplevel = desktop->getCanvas()->get_toplevel(); + Gtk::Window *window = dynamic_cast<Gtk::Window *>(toplevel); + if (!window) { + return; + } + + gchar * dlgmessage = g_strdup_printf(_("'%s' working, please wait..."), _effect->get_name()); + _visibleDialog = new Gtk::MessageDialog(*window, + dlgmessage, + false, // use markup + Gtk::MESSAGE_INFO, + Gtk::BUTTONS_CANCEL, + true); // modal + _visibleDialog->signal_response().connect(sigc::mem_fun(this, &ExecutionEnv::workingCanceled)); + g_free(dlgmessage); + + Gtk::Dialog *dlg = _effect->get_pref_dialog(); + if (dlg) { + _visibleDialog->set_transient_for(*dlg); + } else { + // ToDo: Do we need to make the window transient for the main window here? + // Currently imossible to test because of GUI freezing during save, + // see https://bugs.launchpad.net/inkscape/+bug/967416 + } + _visibleDialog->show_now(); + + return; +} + +void +ExecutionEnv::workingCanceled( const int /*resp*/) { + cancel(); + undo(); + return; +} + +void +ExecutionEnv::cancel () { + SPDesktop *desktop = (SPDesktop *)_doc; + desktop->clearWaitingCursor(); + _effect->get_imp()->cancelProcessing(); + return; +} + +void +ExecutionEnv::undo () { + DocumentUndo::cancel(_doc->doc()); + return; +} + +void +ExecutionEnv::commit () { + DocumentUndo::done(_doc->doc(), _effect->get_name(), ""); + Effect::set_last_effect(_effect); + _effect->get_imp()->commitDocument(); + killDocCache(); + return; +} + +void +ExecutionEnv::reselect () { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if(desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + if (selection) { + selection->restoreBackup(); + } + } + return; +} + +void +ExecutionEnv::run () { + _state = ExecutionEnv::RUNNING; + if (_show_working) { + createWorkingDialog(); + } + SPDesktop *desktop = (SPDesktop *)_doc; + Inkscape::Selection *selection = desktop->getSelection(); + selection->setBackup(); + desktop->setWaitingCursor(); + _effect->get_imp()->effect(_effect, _doc, _docCache); + desktop->clearWaitingCursor(); + _state = ExecutionEnv::COMPLETE; + selection->restoreBackup(); + // _runComplete.signal(); + return; +} + +void +ExecutionEnv::runComplete () { + _mainloop->quit(); +} + +bool +ExecutionEnv::wait () { + if (_state != ExecutionEnv::COMPLETE) { + if (_mainloop) { + _mainloop = Glib::MainLoop::create(false); + } + + sigc::connection conn = _runComplete.connect(sigc::mem_fun(this, &ExecutionEnv::runComplete)); + _mainloop->run(); + + conn.disconnect(); + } + + return true; +} + + + +} } /* namespace Inkscape, Extension */ + + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/execution-env.h b/src/extension/execution-env.h new file mode 100644 index 0000000..d33bdc4 --- /dev/null +++ b/src/extension/execution-env.h @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_EXECUTION_ENV_H__ +#define INKSCAPE_EXTENSION_EXECUTION_ENV_H__ + +#include <glibmm/main.h> +#include <glibmm/ustring.h> + +#include <gtkmm/dialog.h> + +namespace Inkscape { + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI + +namespace Extension { + +class Effect; + +namespace Implementation +{ +class ImplementationDocumentCache; +} + +class ExecutionEnv { +private: + enum state_t { + INIT, //< The context has been initialized + COMPLETE, //< We've completed atleast once + RUNNING //< The effect is currently running + }; + /** \brief What state the execution engine is in. */ + state_t _state; + + /** \brief If there is a working dialog it'll be referenced + right here. */ + Gtk::Dialog * _visibleDialog; + /** \brief Signal that the run is complete. */ + sigc::signal<void> _runComplete; + /** \brief In some cases we need a mainLoop, when we do, this is + a pointer to it. */ + Glib::RefPtr<Glib::MainLoop> _mainloop; + /** \brief The document that we're working on. */ + Inkscape::UI::View::View * _doc; + /** \brief A document cache if we were passed one. */ + Implementation::ImplementationDocumentCache * _docCache; + + /** \brief The effect that we're executing in this context. */ + Effect * _effect; + + /** \brief Show the working dialog when the effect is executing. */ + bool _show_working; +public: + + /** \brief Create a new context for execution of an effect + \param effect The effect to execute + \param doc The document to execute the effect on + \param docCache The implementation cache of the document. May be + NULL in which case it'll be created by the execution + environment. + \prarm show_working Show a small dialog signaling the effect + is working. Allows for user canceling. + \param show_errors If the effect has an error, show it or not. + */ + ExecutionEnv (Effect * effect, + Inkscape::UI::View::View * doc, + Implementation::ImplementationDocumentCache * docCache = nullptr, + bool show_working = true, + bool show_errors = true); + virtual ~ExecutionEnv (); + + /** \brief Starts the execution of the effect + \return Returns whether the effect was executed to completion */ + void run (); + /** \brief Cancel the execution of the effect */ + void cancel (); + /** \brief Commit the changes to the document */ + void commit (); + /** \brief Undoes what the effect completed. */ + void undo (); + /** \brief Wait for the effect to complete if it hasn't. */ + bool wait (); + void reselect (); + + /** \brief Return reference to working dialog (if any) */ + Gtk::Dialog *get_working_dialog () { return _visibleDialog; }; + +private: + void runComplete (); + void createWorkingDialog (); + void workingCanceled (const int resp); + void genDocCache (); + void killDocCache (); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_EXECUTION_ENV_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/extension.cpp b/src/extension/extension.cpp new file mode 100644 index 0000000..b95607f --- /dev/null +++ b/src/extension/extension.cpp @@ -0,0 +1,1111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * + * Inkscape::Extension::Extension: + * the ability to have features that are more modular so that they + * can be added and removed easily. This is the basis for defining + * those actions. + */ + +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension.h" +#include "implementation/implementation.h" +#include "implementation/script.h" +#include "implementation/xslt.h" + +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> + +#include <glib/gstdio.h> +#include <glib/gprintf.h> + +#include <glibmm/i18n.h> +#include <gtkmm/box.h> +#include <gtkmm/frame.h> +#include <gtkmm/grid.h> +#include <gtkmm/label.h> + +#include "db.h" +#include "dependency.h" +#include "inkscape.h" +#include "timer.h" + +#include "io/resource.h" +#include "io/sys.h" + +#include "prefdialog/parameter.h" +#include "prefdialog/widget.h" + +#include "xml/repr.h" + + +namespace Inkscape { +namespace Extension { + +/* Inkscape::Extension::Extension */ + +FILE *Extension::error_file = nullptr; + +/** + \return none + \brief Constructs an Extension from a Inkscape::XML::Node + \param in_repr The repr that should be used to build it + \param base_directory Base directory of extensions that were loaded from a file (.inx file's location) + + This function is the basis of building an extension for Inkscape. It + currently extracts the fields from the Repr that are used in the + extension. The Repr will likely include other children that are + not related to the module directly. If the Repr does not include + a name and an ID the module will be left in an errored state. +*/ +Extension::Extension(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : _gui(true) + , execution_env(nullptr) +{ + g_return_if_fail(in_repr); // should be ensured in system.cpp + repr = in_repr; + Inkscape::GC::anchor(repr); + + if (in_imp == nullptr) { + imp = new Implementation::Implementation(); + } else { + imp = in_imp; + } + + if (base_directory) { + _base_directory = *base_directory; + } + + // get name of the translation catalog ("gettext textdomain") that the extension wants to use for translations + // and lookup the locale directory for it + const char *translationdomain = repr->attribute("translationdomain"); + if (translationdomain) { + _translationdomain = translationdomain; + } else { + _translationdomain = "inkscape"; // default to the Inkscape catalog + } + if (!strcmp(_translationdomain, "none")) { + // special keyword "none" means the extension author does not want translation of extension strings + _translation_enabled = false; + _translationdomain = nullptr; + } else if (!strcmp(_translationdomain, "inkscape")) { + // this is our default domain; we know the location already (also respects INKSCAPE_LOCALEDIR) + _gettext_catalog_dir = bindtextdomain("inkscape", nullptr); + } else { + lookup_translation_catalog(); + } + + // Read XML tree and parse extension + Inkscape::XML::Node *child_repr = repr->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + chname++; + } + + if (!strcmp(chname, "id")) { + const char *id = child_repr->firstChild() ? child_repr->firstChild()->content() : nullptr; + if (id) { + _id = g_strdup(id); + } else { + throw extension_no_id(); + } + } else if (!strcmp(chname, "name")) { + const char *name = child_repr->firstChild() ? child_repr->firstChild()->content() : nullptr; + if (name) { + _name = g_strdup(name); + } else { + throw extension_no_name(); + } + } else if (InxWidget::is_valid_widget_name(chname)) { + InxWidget *widget = InxWidget::make(child_repr, this); + if (widget) { + _widgets.push_back(widget); + } + } else if (!strcmp(chname, "dependency")) { + _deps.push_back(new Dependency(child_repr, this)); + } else if (!strcmp(chname, "script")) { // TODO: should these be parsed in their respective Implementation? + for (Inkscape::XML::Node *child = child_repr->firstChild(); child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { // skip non-element nodes (see LP #1372200) + const char *interpreted = child->attribute("interpreter"); + Dependency::type_t type = interpreted ? Dependency::TYPE_FILE : Dependency::TYPE_EXECUTABLE; + _deps.push_back(new Dependency(child, this, type)); + break; + } + } + } else if (!strcmp(chname, "xslt")) { // TODO: should these be parsed in their respective Implementation? + for (Inkscape::XML::Node *child = child_repr->firstChild(); child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::NodeType::ELEMENT_NODE) { // skip non-element nodes (see LP #1372200) + _deps.push_back(new Dependency(child, this, Dependency::TYPE_FILE)); + break; + } + } + } else { + // We could do some sanity checking here. + // However, we don't really know which additional elements Extension subclasses might need... + } + + child_repr = child_repr->next(); + } + + // all extensions need an ID and a name + if (!_id) { + throw extension_no_id(); + } + if (!_name) { + throw extension_no_name(); + } + + // filter out extensions that are not compatible with the current platform +#ifndef _WIN32 + if (strstr(_id, "win32")) { + throw extension_not_compatible(); + } +#endif + + // finally register the extension if all checks passed + db.register_ext (this); +} + +/** + \return none + \brief Destroys the Extension + + This function frees all of the strings that could be attached + to the extension and also unreferences the repr. This is better + than freeing it because it may (I wouldn't know why) be referenced + in another place. +*/ +Extension::~Extension () +{ + set_state(STATE_UNLOADED); + + db.unregister_ext(this); + + Inkscape::GC::release(repr); + + g_free(_id); + g_free(_name); + + delete timer; + timer = nullptr; + + for (auto widget : _widgets) { + delete widget; + } + + for (auto & _dep : _deps) { + delete _dep; + } + _deps.clear(); +} + +/** + \return none + \brief A function to set whether the extension should be loaded + or unloaded + \param in_state Which state should the extension be in? + + It checks to see if this is a state change or not. If we're changing + states it will call the appropriate function in the implementation, + load or unload. Currently, there is no error checking in this + function. There should be. +*/ +void +Extension::set_state (state_t in_state) +{ + if (_state == STATE_DEACTIVATED) return; + if (in_state != _state) { + /** \todo Need some more error checking here! */ + switch (in_state) { + case STATE_LOADED: + if (imp->load(this)) + _state = STATE_LOADED; + + if (timer != nullptr) { + delete timer; + } + timer = new ExpirationTimer(this); + + break; + case STATE_UNLOADED: + imp->unload(this); + _state = STATE_UNLOADED; + + if (timer != nullptr) { + delete timer; + timer = nullptr; + } + break; + case STATE_DEACTIVATED: + _state = STATE_DEACTIVATED; + + if (timer != nullptr) { + delete timer; + timer = nullptr; + } + break; + default: + break; + } + } + + return; +} + +/** + \return The state the extension is in + \brief A getter for the state variable. +*/ +Extension::state_t +Extension::get_state () +{ + return _state; +} + +/** + \return Whether the extension is loaded or not + \brief A quick function to test the state of the extension +*/ +bool +Extension::loaded () +{ + return get_state() == STATE_LOADED; +} + +/** + \return A boolean saying whether the extension passed the checks + \brief A function to check the validity of the extension + + This function chekcs to make sure that there is an id, a name, a + repr and an implementation for this extension. Then it checks all + of the dependencies to see if they pass. Finally, it asks the + implementation to do a check of itself. + + On each check, if there is a failure, it will print a message to the + error log for that failure. It is important to note that the function + keeps executing if it finds an error, to try and get as many of them + into the error log as possible. This should help people debug + installations, and figure out what they need to get for the full + functionality of Inkscape to be available. +*/ +bool +Extension::check () +{ + const char * inx_failure = _(" This is caused by an improper .inx file for this extension." + " An improper .inx file could have been caused by a faulty installation of Inkscape."); + + if (repr == nullptr) { + printFailure(Glib::ustring(_("the XML description of it got lost.")) + inx_failure); + return false; + } + if (imp == nullptr) { + printFailure(Glib::ustring(_("no implementation was defined for the extension.")) + inx_failure); + return false; + } + + bool retval = true; + for (auto _dep : _deps) { + if (_dep->check() == false) { + printFailure(Glib::ustring(_("a dependency was not met."))); + error_file_write(_dep->info_string()); + retval = false; + } + } + + if (retval) { + return imp->check(this); + } + + error_file_write(""); + return retval; +} + +/** \brief A quick function to print out a standard start of extension + errors in the log. + \param reason A string explaining why this failed + + Real simple, just put everything into \c error_file. +*/ +void +Extension::printFailure (Glib::ustring reason) +{ + _error_reason = Glib::ustring::compose(_("Extension \"%1\" failed to load because %2"), _name, reason); + error_file_write(_error_reason); +} + +/** + \return The XML tree that is used to define the extension + \brief A getter for the internal Repr, does not add a reference. +*/ +Inkscape::XML::Node * +Extension::get_repr () +{ + return repr; +} + +/** + \return The textual id of this extension + \brief Get the ID of this extension - not a copy don't delete! +*/ +gchar * +Extension::get_id () const +{ + return _id; +} + +/** + \return The textual name of this extension + \brief Get the name of this extension - not a copy don't delete! +*/ +const gchar * +Extension::get_name () const +{ + return get_translation(_name, nullptr); +} + +/** + \return None + \brief This function diactivates the extension (which makes it + unusable, but not deleted) + + This function is used to removed an extension from functioning, but + not delete it completely. It sets the state to \c STATE_DEACTIVATED to + mark to the world that it has been deactivated. It also removes + the current implementation and replaces it with a standard one. This + makes it so that we don't have to continually check if there is an + implementation, but we are guaranteed to have a benign one. + + \warning It is important to note that there is no 'activate' function. + Running this function is irreversible. +*/ +void +Extension::deactivate () +{ + set_state(STATE_DEACTIVATED); + + /* Removing the old implementation, and making this use the default. */ + /* This should save some memory */ + delete imp; + imp = new Implementation::Implementation(); + + return; +} + +/** + \return Whether the extension has been deactivated + \brief Find out the status of the extension +*/ +bool +Extension::deactivated () +{ + return get_state() == STATE_DEACTIVATED; +} + +/** Gets the location of the dependency file as an absolute path + * + * Iterates over all dependencies of this extension and finds the one with matching name, + * then returns the absolute path to this dependency file as determined previously. + * + * TODO: This function should not be necessary, but we parse script dependencies twice: + * - Once here in the Extension::Extension() constructor + * - A second time in Script::load() in "script.cpp" when determining the script location + * Theoretically we could return the wrong path if an extension depends on two files with the same name + * in different relative locations. In practice this risk should be close to zero, though. + * + * @return Absolute path of the dependency file + */ +std::string Extension::get_dependency_location(const char *name) +{ + for (auto dep : _deps) { + if (!strcmp(name, dep->get_name())) { + return dep->get_path(); + } + } + + return ""; +} + +/** recursively searches directory for a file named filename; returns true if found */ +static bool _find_filename_recursive(std::string directory, std::string const &filename) { + Glib::Dir dir(directory); + + std::string name = dir.read_name(); + while (!name.empty()) { + std::string fullpath = Glib::build_filename(directory, name); + // g_message("%s", fullpath.c_str()); + + if (Glib::file_test(fullpath, Glib::FILE_TEST_IS_DIR)) { + if (_find_filename_recursive(fullpath, filename)) { + return true; + } + } else if (name == filename) { + return true; + } + name = dir.read_name(); + } + + return false; +} + +/** Searches for a gettext catalog matching the extension's translationdomain + * + * This function will attempt to find the correct gettext catalog for the translationdomain + * requested by the extension. + * + * For this the following three locations are recursively searched for "${translationdomain}.mo": + * - the 'locale' directory in the .inx file's folder + * - the 'locale' directory in the "extensions" folder containing the .inx + * - the system location for gettext catalogs, i.e. where Inkscape's own catalog is located + * + * If one matching file is found, the directory is assumed to be the correct location and registered with gettext + */ +void Extension::lookup_translation_catalog() { + g_assert(!_base_directory.empty()); + + // get locale folder locations + std::string locale_dir_current_extension; + std::string locale_dir_extensions; + std::string locale_dir_system; + + locale_dir_current_extension = Glib::build_filename(_base_directory, "locale"); + + size_t index = _base_directory.find_last_of("extensions"); + if (index != std::string::npos) { + locale_dir_extensions = Glib::build_filename(_base_directory.substr(0, index+1), "locale"); + } + + locale_dir_system = bindtextdomain("inkscape", nullptr); + + // collect unique locations into vector + std::vector<std::string> locale_dirs; + if (locale_dir_current_extension != locale_dir_extensions) { + locale_dirs.push_back(std::move(locale_dir_current_extension)); + } + locale_dirs.push_back(std::move(locale_dir_extensions)); + locale_dirs.push_back(std::move(locale_dir_system)); + + // iterate over locations and look for the one that has the correct catalog + std::string search_name; + search_name += _translationdomain; + search_name += ".mo"; + for (auto locale_dir : locale_dirs) { + if (!Glib::file_test(locale_dir, Glib::FILE_TEST_IS_DIR)) { + continue; + } + + if (_find_filename_recursive(locale_dir, search_name)) { + _gettext_catalog_dir = locale_dir; + break; + } + } + + // register catalog with gettext if found, disable translation for this extension otherwise + if (!_gettext_catalog_dir.empty()) { + const char *current_dir = bindtextdomain(_translationdomain, nullptr); + if (_gettext_catalog_dir != current_dir) { + g_info("Binding textdomain '%s' to '%s'.", _translationdomain, _gettext_catalog_dir.c_str()); + bindtextdomain(_translationdomain, _gettext_catalog_dir.c_str()); + bind_textdomain_codeset(_translationdomain, "UTF-8"); + } + } else { + g_warning("Failed to locate message catalog for textdomain '%s'.", _translationdomain); + _translation_enabled = false; + _translationdomain = nullptr; + } +} + +/** Gets a translation within the context of the current extension + * + * Query gettext for the translated version of the input string, + * handling the preferred translation domain of the extension internally. + * + * @param msgid String to translate + * @param msgctxt Context for the translation + * + * @return Translated string (or original string if extension is not supposed to be translated) + */ +const char *Extension::get_translation(const char *msgid, const char *msgctxt) const { + if (!_translation_enabled) { + return msgid; + } + + if (!strcmp(msgid, "")) { + g_warning("Attempting to translate an empty string in extension '%s', which is not supported.", _id); + return msgid; + } + + if (msgctxt) { + return g_dpgettext2(_translationdomain, msgctxt, msgid); + } else { + return g_dgettext(_translationdomain, msgid); + } +} + +/** Sets environment suitable for executing this Extension + * + * Currently sets the environment variables INKEX_GETTEXT_DOMAIN and INKEX_GETTEXT_DIRECTORY + * to make the "translationdomain" accessible to child processes spawned by this extension's Implementation. + * + * @param doc Optional document, if provided sets the DOCUMENT_PATH from the document's save location. + */ +void Extension::set_environment(const SPDocument *doc) { + Glib::unsetenv("INKEX_GETTEXT_DOMAIN"); + Glib::unsetenv("INKEX_GETTEXT_DIRECTORY"); + + // This is needed so extensions can interact with the user's profile, keep settings etc. + Glib::setenv("INKSCAPE_PROFILE_DIR", std::string(Inkscape::IO::Resource::profile_path())); + + // This is needed so files can be saved relative to their document location (see image-extract) + if (doc) { + auto path = doc->getDocumentFilename(); + if (!path) { + path = ""; // Set to blank string so extensions know the difference between old inkscape and not-saved document. + } + Glib::setenv("DOCUMENT_PATH", std::string(path)); + } + + if (_translationdomain) { + Glib::setenv("INKEX_GETTEXT_DOMAIN", std::string(_translationdomain)); + } + if (!_gettext_catalog_dir.empty()) { + Glib::setenv("INKEX_GETTEXT_DIRECTORY", _gettext_catalog_dir); + } +} + +/** Uses the object's type to figure out what the type is. + * + * @return Returns the type of extension that this object is. + */ +ModuleImpType Extension::get_implementation_type() +{ + if (dynamic_cast<Implementation::Script *>(imp)) { + return MODULE_EXTENSION; + } else if (dynamic_cast<Implementation::XSLT *>(imp)) { + return MODULE_XSLT; + } + // MODULE_UNKNOWN_IMP is not required because it never results in an + // object being created. Thus this function wouldn't be available. + return MODULE_PLUGIN; +} + +/** + \brief A function to get the parameters in a string form + \return An array with all the parameters in it. + +*/ +void +Extension::paramListString (std::list <std::string> &retlist) +{ + // first collect all widgets in the current extension + std::vector<InxWidget *> widget_list; + for (auto widget : _widgets) { + widget->get_widgets(widget_list); + } + + // then build a list of parameter strings from parameter names and values, as '--name=value' + for (auto widget : widget_list) { + InxParameter *parameter = dynamic_cast<InxParameter *>(widget); // filter InxParameters from InxWidgets + if (parameter) { + const char *name = parameter->name(); + std::string value = parameter->value_to_string(); + + if (name && !value.empty()) { // TODO: Shouldn't empty string values be allowed? + std::string parameter_string; + parameter_string += "--"; + parameter_string += name; + parameter_string += "="; + parameter_string += value; + retlist.push_back(parameter_string); + } + } + } + + return; +} + +InxParameter *Extension::get_param(const gchar *name) +{ + if (!name || _widgets.empty()) { + throw Extension::param_not_exist(); + } + + // first collect all widgets in the current extension + std::vector<InxWidget *> widget_list; + for (auto widget : _widgets) { + widget->get_widgets(widget_list); + } + + // then search for a parameter with a matching name + for (auto widget : widget_list) { + InxParameter *parameter = dynamic_cast<InxParameter *>(widget); // filter InxParameters from InxWidgets + if (parameter && !strcmp(parameter->name(), name)) { + return parameter; + } + } + + // if execution reaches here, no parameter matching 'name' was found + throw Extension::param_not_exist(); +} + +InxParameter const *Extension::get_param(const gchar *name) const +{ + return const_cast<Extension *>(this)->get_param(name); +} + + +/** + \return The value of the parameter identified by the name + \brief Gets a parameter identified by name with the bool placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +bool +Extension::get_param_bool(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_bool(); +} + +/** + * \return The value of the param or the alternate if the param doesn't exist. + * \brief Like get_param_bool but with a default on param_not_exist error. + */ +bool Extension::get_param_bool(const gchar *name, bool alt) const +{ + try { + return get_param_bool(name); + } catch (Extension::param_not_exist) { + return alt; + } +} + +/** + \return The integer value for the parameter specified + \brief Gets a parameter identified by name with the integer placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +int +Extension::get_param_int(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_int(); +} + +/** + * \return The value of the param or the alternate if the param doesn't exist. + * \brief Like get_param_int but with a default on param_not_exist error. + */ +int Extension::get_param_int(const gchar *name, int alt) const +{ + try { + return get_param_int(name); + } catch (Extension::param_not_exist) { + return alt; + } +} + + +/** + \return The double value for the float parameter specified + \brief Gets a float parameter identified by name with the double placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +double +Extension::get_param_float(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_float(); +} + +/** + * \return The value of the param or the alternate if the param doesn't exist. + * \brief Like get_param_float but with a default on param_not_exist error. + */ +double Extension::get_param_float(const gchar *name, double alt) const +{ + try { + return get_param_float(name); + } catch (Extension::param_not_exist) { + return alt; + } +} + +/** + \return The string value for the parameter specified + \brief Gets a parameter identified by name with the string placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::get_param_string(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_string(); +} + +/** + * \return The value of the param or the alternate if the param doesn't exist. + * \brief Like get_param_string but with a default on param_not_exist error. + */ +const char *Extension::get_param_string(const gchar *name, const char *alt) const +{ + try { + return get_param_string(name); + } catch (Extension::param_not_exist) { + return alt; + } +} + +/** + \return The string value for the parameter specified + \brief Gets a parameter identified by name with the string placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::get_param_optiongroup(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_optiongroup(); +} + +/** + * \return The value of the param or the alternate if the param doesn't exist. + * \brief Like get_param_optiongroup but with a default on param_not_exist error. + */ +const char *Extension::get_param_optiongroup(const gchar *name, const char *alt) const +{ + try { + return get_param_optiongroup(name); + } catch (Extension::param_not_exist) { + return alt; + } +} + +/** + * This is useful to find out, if a given string \c value is selectable in a optiongroup named \cname. + * + * @param name The name of the optiongroup parameter to get. + * @return true if value exists, false if not + */ +bool +Extension::get_param_optiongroup_contains(const gchar *name, const char *value) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_optiongroup_contains(value); +} + +/** + \return The unsigned integer RGBA value for the parameter specified + \brief Gets a parameter identified by name with the unsigned int placed in value. + \param name The name of the parameter to get + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +guint32 +Extension::get_param_color(const gchar *name) const +{ + const InxParameter *param; + param = get_param(name); + return param->get_color(); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the boolean in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +bool +Extension::set_param_bool(const gchar *name, const bool value) +{ + InxParameter *param; + param = get_param(name); + return param->set_bool(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the integer in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +int +Extension::set_param_int(const gchar *name, const int value) +{ + InxParameter *param; + param = get_param(name); + return param->set_int(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the double in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +double +Extension::set_param_float(const gchar *name, const double value) +{ + InxParameter *param; + param = get_param(name); + return param->set_float(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the string in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::set_param_string(const gchar *name, const char *value) +{ + InxParameter *param; + param = get_param(name); + return param->set_string(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the string in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + + Look up in the parameters list, const then execute the function on that found parameter. +*/ +const char * +Extension::set_param_optiongroup(const gchar *name, const char *value) +{ + InxParameter *param; + param = get_param(name); + return param->set_optiongroup(value); +} + +/** + \return The passed in value + \brief Sets a parameter identified by name with the unsigned integer RGBA value in the parameter value. + \param name The name of the parameter to set + \param value The value to set the parameter to + +Look up in the parameters list, const then execute the function on that found parameter. +*/ +guint32 +Extension::set_param_color(const gchar *name, const guint32 color) +{ + InxParameter *param; + param = get_param(name); + return param->set_color(color); +} + + +/** \brief A function to open the error log file. */ +void +Extension::error_file_open () +{ + gchar *ext_error_file = Inkscape::IO::Resource::log_path(EXTENSION_ERROR_LOG_FILENAME); + error_file = Inkscape::IO::fopen_utf8name(ext_error_file, "w+"); + if (!error_file) { + g_warning(_("Could not create extension error log file '%s'"), ext_error_file); + } + g_free(ext_error_file); +}; + +/** \brief A function to close the error log file. */ +void +Extension::error_file_close () +{ + if (error_file) { + fclose(error_file); + } +}; + +/** \brief A function to write to the error log file. */ +void +Extension::error_file_write (Glib::ustring text) +{ + if (error_file) { + g_fprintf(error_file, "%s\n", text.c_str()); + } +}; + +/** \brief A widget to represent the inside of an AutoGUI widget */ +class AutoGUI : public Gtk::Box { +public: + /** \brief Create an AutoGUI object */ + AutoGUI () : Gtk::Box(Gtk::ORIENTATION_VERTICAL) {}; + + /** + * Adds a widget with a tool tip into the autogui. + * + * If there is no widget, nothing happens. Otherwise it is just + * added into the VBox. If there is a tooltip (non-NULL) then it + * is placed on the widget. + * + * @param widg Widget to add. + * @param tooltip Tooltip for the widget. + */ + void addWidget(Gtk::Widget *widg, gchar const *tooltip, int indent) { + if (widg) { + widg->set_margin_start(indent * InxParameter::GUI_INDENTATION); + this->pack_start(*widg, false, true, 0); // fill=true does not have an effect here, but allows the + // child to choose to expand by setting hexpand/vexpand + if (tooltip) { + widg->set_tooltip_text(tooltip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + }; +}; + +/** \brief A function to automatically generate a GUI from the extensions' widgets + \return Generated widget + + This function just goes through each widget, and calls it's 'get_widget'. + Then, each of those is placed into a Gtk::VBox, which is then returned to the calling function. + + If there are no visible parameters, this function just returns NULL. +*/ +Gtk::Widget * +Extension::autogui (SPDocument *doc, Inkscape::XML::Node *node, sigc::signal<void> *changeSignal) +{ + if (!_gui || widget_visible_count() == 0) { + return nullptr; + } + + AutoGUI * agui = Gtk::manage(new AutoGUI()); + agui->set_border_width(InxParameter::GUI_BOX_MARGIN); + agui->set_spacing(InxParameter::GUI_BOX_SPACING); + + // go through the list of widgets and add the all non-hidden ones + for (auto widget : _widgets) { + if (widget->get_hidden()) { + continue; + } + + Gtk::Widget *widg = widget->get_widget(changeSignal); + gchar const *tip = widget->get_tooltip(); + int indent = widget->get_indent(); + + agui->addWidget(widg, tip, indent); + } + + agui->show(); + return agui; +}; + +/* Extension editor dialog stuff */ + +Gtk::Box * +Extension::get_info_widget() +{ + Gtk::Box * retval = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + retval->set_border_width(4); + + Gtk::Frame * info = Gtk::manage(new Gtk::Frame("General Extension Information")); + retval->pack_start(*info, true, true, 4); + + auto table = Gtk::manage(new Gtk::Grid()); + table->set_border_width(4); + table->set_column_spacing(4); + + info->add(*table); + + int row = 0; + add_val(_("Name:"), get_translation(_name), table, &row); + add_val(_("ID:"), _id, table, &row); + add_val(_("State:"), _state == STATE_LOADED ? _("Loaded") : _state == STATE_UNLOADED ? _("Unloaded") : _("Deactivated"), table, &row); + + retval->show_all(); + return retval; +} + +void Extension::add_val(Glib::ustring labelstr, Glib::ustring valuestr, Gtk::Grid * table, int * row) +{ + Gtk::Label * label; + Gtk::Label * value; + + (*row)++; + label = Gtk::manage(new Gtk::Label(labelstr, Gtk::ALIGN_START)); + value = Gtk::manage(new Gtk::Label(valuestr, Gtk::ALIGN_START)); + + table->attach(*label, 0, (*row) - 1, 1, 1); + table->attach(*value, 1, (*row) - 1, 1, 1); + + label->show(); + value->show(); + + return; +} + +Gtk::Box * +Extension::get_params_widget() +{ + Gtk::Box * retval = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Widget * content = Gtk::manage(new Gtk::Label("Params")); + retval->pack_start(*content, true, true, 4); + content->show(); + retval->show(); + return retval; +} + +unsigned int Extension::widget_visible_count ( ) +{ + unsigned int _visible_count = 0; + for (auto widget : _widgets) { + if (!widget->get_hidden()) { + _visible_count++; + } + } + return _visible_count; +} + +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/extension.h b/src/extension/extension.h new file mode 100644 index 0000000..7ac64a8 --- /dev/null +++ b/src/extension/extension.h @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_H +#define INK_EXTENSION_H + +/** \file + * Frontend to certain, possibly pluggable, actions. + */ + +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <fstream> +#include <ostream> +#include <vector> + +#include <glib.h> +#include <sigc++/signal.h> + +namespace Glib { + class ustring; +} + +namespace Gtk { + class Grid; + class Box; + class Widget; +} + +/** The key that is used to identify that the I/O should be autodetected */ +#define SP_MODULE_KEY_AUTODETECT "autodetect" +/** This is the key for the SVG input module */ +#define SP_MODULE_KEY_INPUT_SVG "org.inkscape.input.svg" +#define SP_MODULE_KEY_INPUT_SVGZ "org.inkscape.input.svgz" +/** Specifies the input module that should be used if none are selected */ +#define SP_MODULE_KEY_INPUT_DEFAULT SP_MODULE_KEY_AUTODETECT +/** The key for outputting standard W3C SVG */ +#define SP_MODULE_KEY_OUTPUT_SVG "org.inkscape.output.svg.plain" +#define SP_MODULE_KEY_OUTPUT_SVGZ "org.inkscape.output.svgz.plain" +/** This is an output file that has SVG data with the Sodipodi namespace extensions */ +#define SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "org.inkscape.output.svg.inkscape" +#define SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE "org.inkscape.output.svgz.inkscape" +/** Which output module should be used? */ +#define SP_MODULE_KEY_OUTPUT_DEFAULT SP_MODULE_KEY_AUTODETECT + +/** Internal raster extensions */ +#define SP_MODULE_KEY_RASTER_PNG "org.inkscape.output.png.inkscape" + +/** Defines the key for Postscript printing */ +#define SP_MODULE_KEY_PRINT_PS "org.inkscape.print.ps" +#define SP_MODULE_KEY_PRINT_CAIRO_PS "org.inkscape.print.ps.cairo" +#define SP_MODULE_KEY_PRINT_CAIRO_EPS "org.inkscape.print.eps.cairo" +/** Defines the key for PDF printing */ +#define SP_MODULE_KEY_PRINT_PDF "org.inkscape.print.pdf" +#define SP_MODULE_KEY_PRINT_CAIRO_PDF "org.inkscape.print.pdf.cairo" +/** Defines the key for LaTeX printing */ +#define SP_MODULE_KEY_PRINT_LATEX "org.inkscape.print.latex" +/** Defines the key for printing with GNOME Print */ +#define SP_MODULE_KEY_PRINT_GNOME "org.inkscape.print.gnome" + +/** Mime type for SVG */ +#define MIME_SVG "image/svg+xml" + +/** Name of the extension error file */ +#define EXTENSION_ERROR_LOG_FILENAME "extension-errors.log" + + +#define INKSCAPE_EXTENSION_URI "http://www.inkscape.org/namespace/inkscape/extension" +#define INKSCAPE_EXTENSION_NS_NC "extension" +#define INKSCAPE_EXTENSION_NS "extension:" + +enum ModuleImpType +{ + MODULE_EXTENSION, // implementation/script.h python extensions + MODULE_XSLT, // implementation/xslt.h xml transform extensions + MODULE_PLUGIN, // plugins/*/*.h C++ extensions + MODULE_UNKNOWN_IMP // No implementation, so nothing created. +}; +enum ModuleFuncType +{ + MODULE_INPUT, + MODULE_OUTPUT, + MODULE_FILTER, + MODULE_PRINT, + MODULE_PATH_EFFECT, + MODULE_UNKNOWN_FUNC +}; + +class SPDocument; + +namespace Inkscape { + +namespace XML { +class Node; +} + +namespace Extension { + +class ExecutionEnv; +class Dependency; +class ExpirationTimer; +class ExpirationTimer; +class InxParameter; +class InxWidget; + +namespace Implementation +{ +class Implementation; +} + + +/** The object that is the basis for the Extension system. This object + contains all of the information that all Extension have. The + individual items are detailed within. This is the interface that + those who want to _use_ the extensions system should use. This + is most likely to be those who are inside the Inkscape program. */ +class Extension { +public: + /** An enumeration to identify if the Extension has been loaded or not. */ + enum state_t { + STATE_LOADED, /**< The extension has been loaded successfully */ + STATE_UNLOADED, /**< The extension has not been loaded */ + STATE_DEACTIVATED /**< The extension is missing something which makes it unusable */ + }; + +private: + gchar *_id = nullptr; /**< The unique identifier for the Extension */ + gchar *_name = nullptr; /**< A user friendly name for the Extension */ + state_t _state = STATE_UNLOADED; /**< Which state the Extension is currently in */ + std::vector<Dependency *> _deps; /**< Dependencies for this extension */ + static FILE *error_file; /**< This is the place where errors get reported */ + bool _gui; + std::string _error_reason; /**< Short, textual explanation for the latest error */ + +protected: + Inkscape::XML::Node *repr; /**< The XML description of the Extension */ + Implementation::Implementation * imp; /**< An object that holds all the functions for making this work */ + ExecutionEnv * execution_env; /**< Execution environment of the extension + * (currently only used by Effects) */ + std::string _base_directory; /**< Directory containing the .inx file, + * relative paths in the extension should usually be relative to it */ + ExpirationTimer * timer = nullptr; /**< Timeout to unload after a given time */ + bool _translation_enabled = true; /**< Attempt translation of strings provided by the extension? */ + +private: + const char *_translationdomain = nullptr; /**< Domainname of gettext textdomain that should + * be used for translation of the extension's strings */ + std::string _gettext_catalog_dir; /**< Directory containing the gettext catalog for _translationdomain */ + + void lookup_translation_catalog(); + +public: + Extension(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + virtual ~Extension(); + + void set_state (state_t in_state); + state_t get_state (); + bool loaded (); + virtual bool check (); + Inkscape::XML::Node * get_repr (); + gchar * get_id () const; + const gchar * get_name () const; + void deactivate (); + bool deactivated (); + void printFailure (Glib::ustring reason); + std::string const &getErrorReason() { return _error_reason; }; + Implementation::Implementation * get_imp () { return imp; }; + void set_execution_env (ExecutionEnv * env) { execution_env = env; }; + ExecutionEnv *get_execution_env () { return execution_env; }; + std::string get_base_directory() const { return _base_directory; }; + void set_base_directory(std::string const &base_directory) { _base_directory = base_directory; }; + std::string get_dependency_location(const char *name); + const char *get_translation(const char* msgid, const char *msgctxt=nullptr) const; + void set_environment(const SPDocument *doc=nullptr); + ModuleImpType get_implementation_type(); + +/* Parameter Stuff */ +private: + std::vector<InxWidget *> _widgets; /**< A list of widgets for this extension. */ + +public: + /** \brief A function to get the number of visible parameters of the extension. + \return The number of visible parameters. */ + unsigned int widget_visible_count ( ); + +public: + /** An error class for when a parameter is looked for that just + * simply doesn't exist */ + class param_not_exist {}; + + /** no valid ID found while parsing XML representation */ + class extension_no_id{}; + + /** no valid name found while parsing XML representation */ + class extension_no_name{}; + + /** extension is not compatible with the current system and should not be loaded */ + class extension_not_compatible{}; + + /** An error class for when a filename already exists, but the user + * doesn't want to overwrite it */ + class no_overwrite {}; + +private: + void make_param (Inkscape::XML::Node * paramrepr); + + /** + * Looks up the parameter with the specified name. + * + * Searches the list of parameters attached to this extension, + * looking for a parameter with a matching name. + * + * This function can throw a 'param_not_exist' exception if the + * name is not found. + * + * @param name Name of the parameter to search for. + * @return Parameter with matching name. + */ + InxParameter *get_param(const gchar *name); + + InxParameter const *get_param(const gchar *name) const; + +public: + bool get_param_bool (const gchar *name) const; + bool get_param_bool (const gchar *name, bool alt) const; + int get_param_int (const gchar *name) const; + int get_param_int (const gchar *name, int alt) const; + double get_param_float (const gchar *name) const; + double get_param_float (const gchar *name, double alt) const; + const char *get_param_string (const gchar *name, const char *alt) const; + const char *get_param_string (const gchar *name) const; + const char *get_param_optiongroup (const gchar *name, const char *alt) const; + const char *get_param_optiongroup (const gchar *name) const; + guint32 get_param_color (const gchar *name) const; + + bool get_param_optiongroup_contains (const gchar *name, const char *value) const; + + bool set_param_bool (const gchar *name, const bool value); + int set_param_int (const gchar *name, const int value); + double set_param_float (const gchar *name, const double value); + const char *set_param_string (const gchar *name, const char *value); + const char *set_param_optiongroup (const gchar *name, const char *value); + guint32 set_param_color (const gchar *name, const guint32 color); + + + /* Error file handling */ +public: + static void error_file_open (); + static void error_file_close (); + static void error_file_write (Glib::ustring text); + +public: + Gtk::Widget *autogui (SPDocument *doc, Inkscape::XML::Node *node, sigc::signal<void> *changeSignal = nullptr); + void paramListString(std::list <std::string> &retlist); + void set_gui(bool s) { _gui = s; } + bool get_gui() { return _gui; } + + /* Extension editor dialog stuff */ +public: + Gtk::Box *get_info_widget(); + Gtk::Box *get_params_widget(); +protected: + inline static void add_val(Glib::ustring labelstr, Glib::ustring valuestr, Gtk::Grid * table, int * row); +}; + + + +/* + +This is a prototype for how collections should work. Whoever gets +around to implementing this gets to decide what a 'folder' and an +'item' really is. That is the joy of implementing it, eh? + +class Collection : public Extension { + +public: + folder get_root (void); + int get_count (folder); + thumbnail get_thumbnail(item); + item[] get_items(folder); + folder[] get_folders(folder); + metadata get_metadata(item); + image get_image(item); + +}; +*/ + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif // INK_EXTENSION_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/find_extension_by_mime.h b/src/extension/find_extension_by_mime.h new file mode 100644 index 0000000..f58580e --- /dev/null +++ b/src/extension/find_extension_by_mime.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Find an extension by its mime type. + */ +/* Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Frank Felfe <innerspace@iname.com> + * bulia byak <buliabyak@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * + * Copyright (C) 2012 Kris De Gussem + * Copyright (C) 2010 authors + * Copyright (C) 1999-2005 authors + * Copyright (C) 2004 David Turner + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "input.h" + +namespace Inkscape { +namespace Extension { +static inline Inkscape::Extension::Extension *find_by_mime(const char *const mime) +{ + + Inkscape::Extension::DB::InputList o; + Inkscape::Extension::db.get_input_list(o); + Inkscape::Extension::DB::InputList::const_iterator i = o.begin(); + while (i != o.end() && strcmp((*i)->get_mimetype(), mime) != 0) { + ++i; + } + return *i; +} +} +} diff --git a/src/extension/implementation/implementation.cpp b/src/extension/implementation/implementation.cpp new file mode 100644 index 0000000..36cd299 --- /dev/null +++ b/src/extension/implementation/implementation.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Author: Ted Gould <ted@gould.cx> + Copyright (c) 2003-2005,2007 + + Released under GNU GPL v2+, read the file 'COPYING' for more information. + + This file is the backend to the extensions system. These are + the parts of the system that most users will never see, but are + important for implementing the extensions themselves. This file + contains the base class for all of that. +*/ + +#include "implementation.h" + +#include <extension/output.h> +#include <extension/input.h> +#include <extension/effect.h> + +#include "selection.h" +#include "desktop.h" + + +namespace Inkscape { +namespace Extension { +namespace Implementation { + +Gtk::Widget * +Implementation::prefs_input(Inkscape::Extension::Input *module, gchar const */*filename*/) { + return module->autogui(nullptr, nullptr); +} + +Gtk::Widget * +Implementation::prefs_output(Inkscape::Extension::Output *module) { + return module->autogui(nullptr, nullptr); +} + +Gtk::Widget *Implementation::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, ImplementationDocumentCache * /*docCache*/) +{ + if (module->widget_visible_count() == 0) { + return nullptr; + } + + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node const* first_select = nullptr; + if (!selected.empty()) { + const SPItem * item = selected.front(); + first_select = item->getRepr(); + } + + // TODO deal with this broken const correctness: + return module->autogui(current_document, const_cast<Inkscape::XML::Node *>(first_select), changeSignal); +} // Implementation::prefs_effect + +} /* namespace Implementation */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/implementation/implementation.h b/src/extension/implementation/implementation.h new file mode 100644 index 0000000..edd9e72 --- /dev/null +++ b/src/extension/implementation/implementation.h @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + Author: Ted Gould <ted@gould.cx> + Copyright (c) 2003-2005,2007 + + Released under GNU GPL v2+, read the file 'COPYING' for more information. + + This file is the backend to the extensions system. These are + the parts of the system that most users will never see, but are + important for implementing the extensions themselves. This file + contains the base class for all of that. +*/ +#ifndef SEEN_INKSCAPE_EXTENSION_IMPLEMENTATION_H +#define SEEN_INKSCAPE_EXTENSION_IMPLEMENTATION_H + +#include <vector> +#include <sigc++/signal.h> +#include <glibmm/value.h> +#include <2geom/forward.h> + +namespace Gtk { + class Widget; +} + +class SPDocument; +class SPStyle; + +namespace Inkscape { + +namespace UI { +namespace View { +class View; +} +} + +namespace XML { + class Node; +} + +namespace Extension { + +class Effect; +class Extension; +class Input; +class Output; +class Print; + +namespace Implementation { + +/** + * A cache for the document and this implementation. + */ +class ImplementationDocumentCache { + + /** + * The document that this instance is working on. + */ + Inkscape::UI::View::View * _view; +public: + explicit ImplementationDocumentCache (Inkscape::UI::View::View * view) { _view = view;}; + + virtual ~ImplementationDocumentCache ( ) { return; }; + Inkscape::UI::View::View const * view ( ) { return _view; }; +}; + +/** + * Base class for all implementations of modules. This is whether they are done systematically by + * having something like the scripting system, or they are implemented internally they all derive + * from this class. + */ +class Implementation { +public: + // ----- Constructor / destructor ----- + Implementation() = default; + + virtual ~Implementation() = default; + + // ----- Basic functions for all Extension ----- + virtual bool load(Inkscape::Extension::Extension * /*module*/) { return true; } + + virtual void unload(Inkscape::Extension::Extension * /*module*/) {} + + /** + * Create a new document cache object. + * This function just returns \c NULL. Subclasses are likely + * to reimplement it to do something useful. + * @param ext The extension that is referencing us + * @param doc The document to create the cache of + * @return A new document cache that is valid as long as the document + * is not changed. + */ + virtual ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * /*doc*/) { return nullptr; } + + /** Verify any dependencies. */ + virtual bool check(Inkscape::Extension::Extension * /*module*/) { return true; } + + virtual bool cancelProcessing () { return true; } + virtual void commitDocument () {} + + // ----- Input functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget *prefs_input(Inkscape::Extension::Input *module, + gchar const *filename); + + virtual SPDocument *open(Inkscape::Extension::Input * /*module*/, + gchar const * /*filename*/) { return nullptr; } + + // ----- Output functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget *prefs_output(Inkscape::Extension::Output *module); + virtual void save(Inkscape::Extension::Output * /*module*/, SPDocument * /*doc*/, gchar const * /*filename*/) {} + virtual void export_raster( + Inkscape::Extension::Output * /*module*/, + const SPDocument * /*doc*/, + std::string const &/*png_file*/, + gchar const * /*filename*/) {} + + // ----- Effect functions ----- + /** Find out information about the file. */ + virtual Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, + Inkscape::UI::View::View *view, + sigc::signal<void> *changeSignal, + ImplementationDocumentCache *docCache); + virtual void effect(Inkscape::Extension::Effect * /*module*/, + Inkscape::UI::View::View * /*document*/, + ImplementationDocumentCache * /*docCache*/) {} + + // ----- Print functions ----- + virtual unsigned setup(Inkscape::Extension::Print * /*module*/) { return 0; } + virtual unsigned set_preview(Inkscape::Extension::Print * /*module*/) { return 0; } + + virtual unsigned begin(Inkscape::Extension::Print * /*module*/, + SPDocument * /*doc*/) { return 0; } + virtual unsigned finish(Inkscape::Extension::Print * /*module*/) { return 0; } + + /** + * Tell the printing engine whether text should be text or path. + * Default value is false because most printing engines will support + * paths more than they'll support text. (at least they do today) + * \retval true Render the text as a path + * \retval false Render text using the text function (above) + */ + virtual bool textToPath(Inkscape::Extension::Print * /*ext*/) { return false; } + + /** + * Get "fontEmbedded" param, i.e. tell the printing engine whether fonts should be embedded. + * Only available for Adobe Type 1 fonts in EPS output as of now + * \retval true Fonts have to be embedded in the output so that the user might not need + * to install fonts to have the interpreter read the document correctly + * \retval false Do not embed fonts + */ + virtual bool fontEmbedded(Inkscape::Extension::Print * /*ext*/) { return false; } + + // ----- Rendering methods ----- + virtual unsigned bind(Inkscape::Extension::Print * /*module*/, + Geom::Affine const & /*transform*/, + float /*opacity*/) { return 0; } + virtual unsigned release(Inkscape::Extension::Print * /*module*/) { return 0; } + virtual unsigned fill(Inkscape::Extension::Print * /*module*/, + Geom::PathVector const & /*pathv*/, + Geom::Affine const & /*ctm*/, + SPStyle const * /*style*/, + Geom::OptRect const & /*pbox*/, + Geom::OptRect const & /*dbox*/, + Geom::OptRect const & /*bbox*/) { return 0; } + virtual unsigned stroke(Inkscape::Extension::Print * /*module*/, + Geom::PathVector const & /*pathv*/, + Geom::Affine const & /*transform*/, + SPStyle const * /*style*/, + Geom::OptRect const & /*pbox*/, + Geom::OptRect const & /*dbox*/, + Geom::OptRect const & /*bbox*/) { return 0; } + virtual unsigned image(Inkscape::Extension::Print * /*module*/, + unsigned char * /*px*/, + unsigned int /*w*/, + unsigned int /*h*/, + unsigned int /*rs*/, + Geom::Affine const & /*transform*/, + SPStyle const * /*style*/) { return 0; } + virtual unsigned text(Inkscape::Extension::Print * /*module*/, + char const * /*text*/, + Geom::Point const & /*p*/, + SPStyle const * /*style*/) { return 0; } + virtual void processPath(Inkscape::XML::Node * /*node*/) {} + + /** + * If detach = true, when saving to a file, don't store URIs relative to the filename + */ + virtual void setDetachBase(bool detach) {} +}; + + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // __INKSCAPE_EXTENSION_IMPLEMENTATION_H__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/implementation/script.cpp b/src/extension/implementation/script.cpp new file mode 100644 index 0000000..0d0a07e --- /dev/null +++ b/src/extension/implementation/script.cpp @@ -0,0 +1,1022 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Code for handling extensions (i.e. scripts). + * + * Authors: + * Bryce Harrington <bryce@osdl.org> + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2002-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib/gstdio.h> +#include <glibmm.h> +#include <glibmm/convert.h> +#include <glibmm/miscutils.h> +#include <gtkmm/main.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> + +#include "desktop.h" +#include "inkscape.h" +#include "inkscape-window.h" +#include "path-prefix.h" +#include "preferences.h" +#include "script.h" +#include "selection.h" + +#include "extension/db.h" +#include "extension/effect.h" +#include "extension/execution-env.h" +#include "extension/init.h" +#include "extension/input.h" +#include "extension/output.h" +#include "extension/system.h" +#include "io/resource.h" +#include "layer-manager.h" +#include "object/sp-namedview.h" +#include "object/sp-path.h" +#include "ui/desktop/menubar.h" +#include "ui/dialog-events.h" +#include "ui/tool/control-point-selection.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/path-manipulator.h" +#include "ui/tools/node-tool.h" +#include "ui/view/view.h" +#include "widgets/desktop-widget.h" +#include "xml/attribute-record.h" +#include "xml/node.h" + +/* Namespaces */ +namespace Inkscape { +namespace Extension { +namespace Implementation { + +/** \brief Make GTK+ events continue to come through a little bit + + This just keeps coming the events through so that we'll make the GUI + update and look pretty. +*/ +void Script::pump_events () { + while ( Gtk::Main::events_pending() ) { + Gtk::Main::iteration(); + } + return; +} + + +/** \brief A table of what interpreters to call for a given language + + This table is used to keep track of all the programs to execute a + given script. It also tracks the preference to use to overwrite + the given interpreter to a custom one per user. +*/ +const std::map<std::string, Script::interpreter_t> Script::interpreterTab = { + // clang-format off +#ifdef _WIN32 + { "perl", {"perl-interpreter", {"wperl" }}}, + { "python", {"python-interpreter", {"pythonw" }}}, +#elif defined __APPLE__ + { "perl", {"perl-interpreter", {"perl" }}}, + { "python", {"python-interpreter", {"python3" }}}, +#else + { "perl", {"perl-interpreter", {"perl" }}}, + { "python", {"python-interpreter", {"python3", "python" }}}, +#endif + { "python2", {"python2-interpreter", {"python2", "python" }}}, + { "ruby", {"ruby-interpreter", {"ruby" }}}, + { "shell", {"shell-interpreter", {"sh" }}}, + // clang-format on +}; + + + +/** \brief Look up an interpreter name, and translate to something that + is executable + \param interpNameArg The name of the interpreter that we're looking + for, should be an entry in interpreterTab +*/ +std::string Script::resolveInterpreterExecutable(const Glib::ustring &interpNameArg) +{ + // 0. Do we have a supported interpreter type? + auto interp = interpreterTab.find(interpNameArg); + if (interp == interpreterTab.end()) { + g_critical("Script::resolveInterpreterExecutable(): unknown script interpreter '%s'", interpNameArg.c_str()); + return ""; + } + + std::list<Glib::ustring> searchList; + std::copy(interp->second.defaultvals.begin(), interp->second.defaultvals.end(), std::back_inserter(searchList)); + + // 1. Check preferences for an override. + auto prefs = Inkscape::Preferences::get(); + auto prefInterp = prefs->getString("/extensions/" + Glib::ustring(interp->second.prefstring)); + + if (!prefInterp.empty()) { + searchList.push_front(prefInterp); + } + + // 2. Search for things in the path if they're there or an absolute + for (const auto& binname : searchList) { + auto interpreter_path = Glib::filename_from_utf8(binname); + + if (!Glib::path_is_absolute(interpreter_path)) { + auto found_path = Glib::find_program_in_path(interpreter_path); + if (!found_path.empty()) { + return found_path; + } + } else { + return interpreter_path; + } + } + + // 3. Error + g_critical("Script::resolveInterpreterExecutable(): failed to locate script interpreter '%s'", interpNameArg.c_str()); + return ""; +} + +/** \brief This function creates a script object and sets up the + variables. + \return A script object + + This function just sets the command to NULL. It should get built + officially in the load function. This allows for less allocation + of memory in the unloaded state. +*/ +Script::Script() + : Implementation() + , _canceled(false) + , parent_window(nullptr) +{ +} + +/** + * \brief Destructor + */ +Script::~Script() += default; + + +/** + \return none + \brief This function 'loads' an extension, basically it determines + the full command for the extension and stores that. + \param module The extension to be loaded. + + The most difficult part about this function is finding the actual + command through all of the Reprs. Basically it is hidden down a + couple of layers, and so the code has to move down too. When + the command is actually found, it has its relative directory + solved. + + At that point all of the loops are exited, and there is an + if statement to make sure they didn't exit because of not finding + the command. If that's the case, the extension doesn't get loaded + and should error out at a higher level. +*/ + +bool Script::load(Inkscape::Extension::Extension *module) +{ + if (module->loaded()) { + return true; + } + + helper_extension = ""; + + /* This should probably check to find the executable... */ + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "script")) { + for (child_repr = child_repr->firstChild(); child_repr != nullptr; child_repr = child_repr->next()) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "command")) { + const gchar *interpretstr = child_repr->attribute("interpreter"); + if (interpretstr != nullptr) { + std::string interpString = resolveInterpreterExecutable(interpretstr); + if (interpString.empty()) { + continue; // can't have a script extension with empty interpreter + } + command.push_back(interpString); + } + // TODO: we already parse commands as dependencies in extension.cpp + // can can we optimize this to be less indirect? + const char *script_name = child_repr->firstChild()->content(); + std::string script_location = module->get_dependency_location(script_name); + command.push_back(std::move(script_location)); + } else if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "helper_extension")) { + helper_extension = child_repr->firstChild()->content(); + } + } + + break; + } + child_repr = child_repr->next(); + } + + // TODO: Currently this causes extensions to fail silently, see comment in Extension::set_state() + g_return_val_if_fail(command.size() > 0, false); + + return true; +} + + +/** + \return None. + \brief Unload this puppy! + \param module Extension to be unloaded. + + This function just sets the module to unloaded. It free's the + command if it has been allocated. +*/ +void Script::unload(Inkscape::Extension::Extension */*module*/) +{ + command.clear(); + helper_extension = ""; +} + + + + +/** + \return Whether the check passed or not + \brief Check every dependency that was given to make sure we should keep this extension + \param module The Extension in question + +*/ +bool Script::check(Inkscape::Extension::Extension *module) +{ + int script_count = 0; + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "script")) { + script_count++; + + // check if all helper_extensions attached to this script were registered + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "helper_extension")) { + gchar const *helper = child_repr->firstChild()->content(); + if (Inkscape::Extension::db.get(helper) == nullptr) { + return false; + } + } + + child_repr = child_repr->next(); + } + + break; + } + child_repr = child_repr->next(); + } + + if (script_count == 0) { + return false; + } + + return true; +} + +class ScriptDocCache : public ImplementationDocumentCache { + friend class Script; +protected: + std::string _filename; + int _tempfd; +public: + ScriptDocCache (Inkscape::UI::View::View * view); + ~ScriptDocCache ( ) override; +}; + +ScriptDocCache::ScriptDocCache (Inkscape::UI::View::View * view) : + ImplementationDocumentCache(view), + _filename(""), + _tempfd(0) +{ + try { + _tempfd = Glib::file_open_tmp(_filename, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + SPDesktop *desktop = (SPDesktop *) view; + sp_namedview_document_from_window(desktop); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool("/options/svgoutput/disable_optimizations", true); + Inkscape::Extension::save( + Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE), + view->doc(), _filename.c_str(), false, false, Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + prefs->setBool("/options/svgoutput/disable_optimizations", false); + return; +} + +ScriptDocCache::~ScriptDocCache ( ) +{ + close(_tempfd); + unlink(_filename.c_str()); +} + +ImplementationDocumentCache *Script::newDocCache( Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * view ) { + return new ScriptDocCache(view); +} + + +/** + \return A dialog for preferences + \brief A stub function right now + \param module Module who's preferences need getting + \param filename Hey, the file you're getting might be important + + This function should really do something, right now it doesn't. +*/ +Gtk::Widget *Script::prefs_input(Inkscape::Extension::Input *module, + const gchar */*filename*/) +{ + return module->autogui(nullptr, nullptr); +} + + + +/** + \return A dialog for preferences + \brief A stub function right now + \param module Module whose preferences need getting + + This function should really do something, right now it doesn't. +*/ +Gtk::Widget *Script::prefs_output(Inkscape::Extension::Output *module) +{ + return module->autogui(nullptr, nullptr); +} + +/** + \return A new document that has been opened + \brief This function uses a filename that is put in, and calls + the extension's command to create an SVG file which is + returned. + \param module Extension to use. + \param filename File to open. + + First things first, this function needs a temporary file name. To + create one of those the function Glib::file_open_tmp is used with + the header of ink_ext_. + + The extension is then executed using the 'execute' function + with the filename assigned and then the temporary filename. + After execution the SVG should be in the temporary file. + + Finally, the temporary file is opened using the SVG input module and + a document is returned. That document has its filename set to + the incoming filename (so that it's not the temporary filename). + That document is then returned from this function. +*/ +SPDocument *Script::open(Inkscape::Extension::Input *module, + const gchar *filenameArg) +{ + std::list<std::string> params; + module->paramListString(params); + module->set_environment(); + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return nullptr; + } + + std::string lfilename = Glib::filename_from_utf8(filenameArg); + + file_listener fileout; + int data_read = execute(command, params, lfilename, fileout); + fileout.toFile(tempfilename_out); + + SPDocument * mydoc = nullptr; + if (data_read > 10) { + if (helper_extension.size()==0) { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(SP_MODULE_KEY_INPUT_SVG), + tempfilename_out.c_str()); + } else { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(helper_extension.c_str()), + tempfilename_out.c_str()); + } + } // data_read + + if (mydoc != nullptr) { + mydoc->setDocumentBase(nullptr); + mydoc->changeFilenameAndHrefs(filenameArg); + } + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_out); + + unlink(tempfilename_out.c_str()); + + return mydoc; +} // open + + + +/** + \return none + \brief This function uses an extension to save a document. It first + creates an SVG file of the document, and then runs it through + the script. + \param module Extension to be used + \param doc Document to be saved + \param filename The name to save the final file as + \return false in case of any failure writing the file, otherwise true + + Well, at some point people need to save - it is really what makes + the entire application useful. And, it is possible that someone + would want to use an extension for this, so we need a function to + do that, eh? + + First things first, the document is saved to a temporary file that + is an SVG file. To get the temporary filename Glib::file_open_tmp is used with + ink_ext_ as a prefix. Don't worry, this file gets deleted at the + end of the function. + + After we have the SVG file, then Script::execute is called with + the temporary file name and the final output filename. This should + put the output of the script into the final output file. We then + delete the temporary file. +*/ +void Script::save(Inkscape::Extension::Output *module, + SPDocument *doc, + const gchar *filenameArg) +{ + std::list<std::string> params; + module->paramListString(params); + module->set_environment(doc); + + std::string tempfilename_in; + int tempfd_in = 0; + try { + tempfd_in = Glib::file_open_tmp(tempfilename_in, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + throw Inkscape::Extension::Output::save_failed(); + } + + if (helper_extension.size() == 0) { + Inkscape::Extension::save( + Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE), + doc, tempfilename_in.c_str(), false, false, + Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + } else { + Inkscape::Extension::save( + Inkscape::Extension::db.get(helper_extension.c_str()), + doc, tempfilename_in.c_str(), false, false, + Inkscape::Extension::FILE_SAVE_METHOD_TEMPORARY); + } + + + file_listener fileout; + int data_read = execute(command, params, tempfilename_in, fileout); + + bool success = false; + + if (data_read > 0) { + std::string lfilename = Glib::filename_from_utf8(filenameArg); + success = fileout.toFile(lfilename); + } + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_in); + // FIXME: convert to utf8 (from "filename encoding") and unlink_utf8name + unlink(tempfilename_in.c_str()); + + if (success == false) { + throw Inkscape::Extension::Output::save_failed(); + } + + return; +} + + +void Script::export_raster(Inkscape::Extension::Output *module, + const SPDocument *doc, + const std::string &png_file, + const gchar *filenameArg) +{ + if(!module->is_raster()) { + g_error("Can not export raster to non-raster extension."); + return; + } + + std::list<std::string> params; + module->paramListString(params); + module->set_environment(doc); + + file_listener fileout; + int data_read = execute(command, params, png_file, fileout); + + bool success = false; + if (data_read > 0) { + std::string lfilename = Glib::filename_from_utf8(filenameArg); + success = fileout.toFile(lfilename); + } + if (success == false) { + throw Inkscape::Extension::Output::save_failed(); + } + return; +} + +/** + \return none + \brief This function uses an extension as an effect on a document. + \param module Extension to effect with. + \param doc Document to run through the effect. + + This function is a little bit trickier than the previous two. It + needs two temporary files to get its work done. Both of these + files have random names created for them using the Glib::file_open_temp function + with the ink_ext_ prefix in the temporary directory. Like the other + functions, the temporary files are deleted at the end. + + To save/load the two temporary documents (both are SVG) the internal + modules for SVG load and save are used. They are both used through + the module system function by passing their keys into the functions. + + The command itself is built a little bit differently than in other + functions because the effect support selections. So on the command + line a list of all the ids that are selected is included. Currently, + this only works for a single selected object, but there will be more. + The command string is filled with the data, and then after the execution + it is freed. + + The execute function is used at the core of this function + to execute the Script on the two SVG documents (actually only one + exists at the time, the other is created by that script). At that + point both should be full, and the second one is loaded. +*/ +void Script::effect(Inkscape::Extension::Effect *module, + Inkscape::UI::View::View *doc, + ImplementationDocumentCache * docCache) +{ + if (docCache == nullptr) { + docCache = newDocCache(module, doc); + } + ScriptDocCache * dc = dynamic_cast<ScriptDocCache *>(docCache); + if (dc == nullptr) { + printf("TOO BAD TO LIVE!!!"); + exit(1); + } + if (doc == nullptr) + { + g_warning("Script::effect: View not defined"); + return; + } + + SPDesktop *desktop = reinterpret_cast<SPDesktop *>(doc); + sp_namedview_document_from_window(desktop); + + std::list<std::string> params; + module->paramListString(params); + module->set_environment(desktop->getDocument()); + + parent_window = module->get_execution_env()->get_working_dialog(); + + if (module->no_doc) { + // this is a no-doc extension, e.g. a Help menu command; + // just run the command without any files, ignoring errors + + Glib::ustring empty; + file_listener outfile; + execute(command, params, empty, outfile); + + // Hack to allow for extension manager to reload extensions + // TODO: Find a better way to do this, e.g. implement an action and have extensions (or users) + // call that instead when there's a change that requires extensions to reload + if (!g_strcmp0(module->get_id(), "org.inkscape.extension.manager")) { + Inkscape::Extension::refresh_user_extensions(); + build_menu(); // Rebuild main menubar. + } + + return; + } + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX.svg"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + if (desktop) { + Inkscape::Selection * selection = desktop->getSelection(); + if (selection) { + params = selection->params; + module->paramListString(params); + selection->clear(); + } + } + + file_listener fileout; + int data_read = execute(command, params, dc->_filename, fileout); + fileout.toFile(tempfilename_out); + + pump_events(); + + SPDocument * mydoc = nullptr; + if (data_read > 10) { + try { + mydoc = Inkscape::Extension::open( + Inkscape::Extension::db.get(SP_MODULE_KEY_INPUT_SVG), + tempfilename_out.c_str()); + } catch (const Inkscape::Extension::Input::open_failed &e) { + g_warning("Extension returned output that could not be parsed: %s", e.what()); + Gtk::MessageDialog warning( + _("The output from the extension could not be parsed."), + false, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_OK, true); + warning.set_transient_for( parent_window ? *parent_window : *(INKSCAPE.active_desktop()->getToplevel()) ); + warning.run(); + } + } // data_read + + pump_events(); + + // make sure we don't leak file descriptors from Glib::file_open_tmp + close(tempfd_out); + + g_unlink(tempfilename_out.c_str()); + + if (mydoc) { + SPDocument* vd=doc->doc(); + if (vd != nullptr) + { + mydoc->changeFilenameAndHrefs(vd->getDocumentFilename()); + + vd->emitReconstructionStart(); + copy_doc(vd->getReprRoot(), mydoc->getReprRoot()); + vd->emitReconstructionFinish(); + + // Getting the named view from the document generated by the extension + SPNamedView *nv = mydoc->getNamedView(); + + //Check if it has a default layer set up + SPObject *layer = nullptr; + if ( nv != nullptr) + { + SPDocument *document = desktop->doc(); + if (document != nullptr) { + //If so, get that layer + if( nv->default_layer_id != 0 ) { + layer = document->getObjectById(g_quark_to_string(nv->default_layer_id)); + } + desktop->showGrids(nv->grids_visible); + } + } + + sp_namedview_update_layers_from_document(desktop); + //If that layer exists, + if (layer) { + //set the current layer + desktop->layerManager().setCurrentLayer(layer); + } + } + mydoc->release(); + } + + return; +} + + + +/** + \brief A function to replace all the elements in an old document + by those from a new document. + document and repinserts them into an emptied old document. + \param oldroot The root node of the old (destination) document. + \param newroot The root node of the new (source) document. + + This function first deletes all the root attributes in the old document followed + by copying all the root attributes from the new document to the old document. + + It then deletes all the elements in the old document by + making two passes, the first to create a list of the old elements and + the second to actually delete them. This two pass approach removes issues + with the list being changed while parsing through it... lots of nasty bugs. + + Then, it copies all the element in the new document into the old document. + + Finally, it copies the attributes in namedview. +*/ +void Script::copy_doc (Inkscape::XML::Node * oldroot, Inkscape::XML::Node * newroot) +{ + if ((oldroot == nullptr) ||(newroot == nullptr)) + { + g_warning("Error on copy_doc: NULL pointer input."); + return; + } + + // For copying attributes in root and in namedview + std::vector<gchar const *> attribs; + + // Must explicitly copy root attributes. This must be done first since + // copying grid lines calls "SPGuide::set()" which needs to know the + // width, height, and viewBox of the root element. + + // Make a list of all attributes of the old root node. + for (const auto & iter : oldroot->attributeList()) { + attribs.push_back(g_quark_to_string(iter.key)); + } + + // Delete the attributes of the old root node. + for (auto attrib : attribs) { + oldroot->removeAttribute(attrib); + } + + // Set the new attributes. + for (const auto & iter : newroot->attributeList()) { + gchar const *name = g_quark_to_string(iter.key); + oldroot->setAttribute(name, newroot->attribute(name)); + } + + // Question: Why is the "sodipodi:namedview" special? Treating it as a normal + // element results in crashes. + // Seems to be a bug: + // http://inkscape.13.x6.nabble.com/Effect-that-modifies-the-document-properties-tt2822126.html + + std::vector<Inkscape::XML::Node *> delete_list; + + // Make list + for (Inkscape::XML::Node * child = oldroot->firstChild(); + child != nullptr; + child = child->next()) { + if (!strcmp("sodipodi:namedview", child->name())) { + for (Inkscape::XML::Node * oldroot_namedview_child = child->firstChild(); + oldroot_namedview_child != nullptr; + oldroot_namedview_child = oldroot_namedview_child->next()) { + delete_list.push_back(oldroot_namedview_child); + } + break; + } + } + + // Unparent (delete) + for (auto & i : delete_list) { + sp_repr_unparent(i); + } + attribs.clear(); + oldroot->mergeFrom(newroot, "id", true, true); +} + +/** \brief This function checks the stderr file, and if it has data, + shows it in a warning dialog to the user + \param filename Filename of the stderr file +*/ +void Script::checkStderr (const Glib::ustring &data, + Gtk::MessageType type, + const Glib::ustring &message) +{ + Gtk::MessageDialog warning(message, false, type, Gtk::BUTTONS_OK, true); + warning.set_resizable(true); + GtkWidget *dlg = GTK_WIDGET(warning.gobj()); + if (parent_window) { + warning.set_transient_for(*parent_window); + } else { + sp_transientize(dlg); + } + + auto vbox = warning.get_content_area(); + + /* Gtk::TextView * textview = new Gtk::TextView(Gtk::TextBuffer::create()); */ + Gtk::TextView * textview = new Gtk::TextView(); + textview->set_editable(false); + textview->set_wrap_mode(Gtk::WRAP_WORD); + textview->show(); + + textview->get_buffer()->set_text(data.c_str()); + + Gtk::ScrolledWindow * scrollwindow = new Gtk::ScrolledWindow(); + scrollwindow->add(*textview); + scrollwindow->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollwindow->set_shadow_type(Gtk::SHADOW_IN); + scrollwindow->show(); + scrollwindow->set_size_request(0, 60); + + vbox->pack_start(*scrollwindow, true, true, 5 /* fix these */); + + warning.run(); + + delete textview; + delete scrollwindow; + + return; +} + +bool Script::cancelProcessing () { + _canceled = true; + if (_main_loop) { + _main_loop->quit(); + } + Glib::spawn_close_pid(_pid); + + return true; +} + + +/** \brief This is the core of the extension file as it actually does + the execution of the extension. + \param in_command The command to be executed + \param filein Filename coming in + \param fileout Filename of the out file + \return Number of bytes that were read into the output file. + + The first thing that this function does is build the command to be + executed. This consists of the first string (in_command) and then + the filename for input (filein). This file is put on the command + line. + + The next thing that this function does is open a pipe to the + command and get the file handle in the ppipe variable. It then + opens the output file with the output file handle. Both of these + operations are checked extensively for errors. + + After both are opened, then the data is copied from the output + of the pipe into the file out using \a fread and \a fwrite. These two + functions are used because of their primitive nature - they make + no assumptions about the data. A buffer is used in the transfer, + but the output of \a fread is stored so the exact number of bytes + is handled gracefully. + + At the very end (after the data has been copied) both of the files + are closed, and we return to what we were doing. +*/ +int Script::execute (const std::list<std::string> &in_command, + const std::list<std::string> &in_params, + const Glib::ustring &filein, + file_listener &fileout) +{ + g_return_val_if_fail(!in_command.empty(), 0); + + std::vector<std::string> argv; + + bool interpreted = (in_command.size() == 2); + std::string program = in_command.front(); + std::string script = interpreted ? in_command.back() : ""; + std::string working_directory = ""; + + // We should always have an absolute path here: + // - For interpreted scripts, see Script::resolveInterpreterExecutable() + // - For "normal" scripts this should be done as part of the dependency checking, see Dependency::check() + if (!Glib::path_is_absolute(program)) { + g_critical("Script::execute(): Got unexpected relative path '%s'. Please report a bug.", program.c_str()); + return 0; + } + argv.push_back(program); + + if (interpreted) { + // On Windows, Python garbles Unicode command line parameters + // in an useless way. This means extensions fail when Inkscape + // is run from an Unicode directory. + // As a workaround, we set the working directory to the one + // containing the script. + working_directory = Glib::path_get_dirname(script); + script = Glib::path_get_basename(script); + argv.push_back(script); + } + + // assemble the rest of argv + std::copy(in_params.begin(), in_params.end(), std::back_inserter(argv)); + if (!filein.empty()) { + auto filein_native = Glib::filename_from_utf8(filein); + if (!Glib::path_is_absolute(filein_native)) + filein_native = Glib::build_filename(Glib::get_current_dir(), filein_native); + argv.push_back(filein_native); + } + + //for(int i=0;i<argv.size(); ++i){printf("%s ",argv[i].c_str());}printf("\n"); + + int stdout_pipe, stderr_pipe; + + try { + Glib::spawn_async_with_pipes(working_directory, // working directory + argv, // arg v + static_cast<Glib::SpawnFlags>(0), // no flags + sigc::slot<void>(), + &_pid, // Pid + nullptr, // STDIN + &stdout_pipe, // STDOUT + &stderr_pipe); // STDERR + } catch (Glib::Error &e) { + g_critical("Script::execute(): failed to execute program '%s'.\n\tReason: %s", program.c_str(), e.what().data()); + return 0; + } + + // Create a new MainContext for the loop so that the original context sources are not run here, + // this enforces that only the file_listeners should be read in this new MainLoop + Glib::RefPtr<Glib::MainContext> _main_context = Glib::MainContext::create(); + _main_loop = Glib::MainLoop::create(_main_context, false); + + file_listener fileerr; + fileout.init(stdout_pipe, _main_loop); + fileerr.init(stderr_pipe, _main_loop); + + _canceled = false; + _main_loop->run(); + + // Ensure all the data is out of the pipe + while (!fileout.isDead()) { + fileout.read(Glib::IO_IN); + } + while (!fileerr.isDead()) { + fileerr.read(Glib::IO_IN); + } + + _main_loop.reset(); + + if (_canceled) { + // std::cout << "Script Canceled" << std::endl; + return 0; + } + + Glib::ustring stderr_data = fileerr.string(); + if (stderr_data.length() != 0 && + INKSCAPE.use_gui() + ) { + checkStderr(stderr_data, Gtk::MESSAGE_INFO, + _("Inkscape has received additional data from the script executed. " + "The script did not return an error, but this may indicate the results will not be as expected.")); + } + + Glib::ustring stdout_data = fileout.string(); + if (stdout_data.length() == 0) { + return 0; + } + + // std::cout << "Finishing Execution." << std::endl; + return stdout_data.length(); +} + + +void Script::file_listener::init(int fd, Glib::RefPtr<Glib::MainLoop> main) { + _channel = Glib::IOChannel::create_from_fd(fd); + _channel->set_close_on_unref(true); + _channel->set_encoding(); + _conn = main->get_context()->signal_io().connect(sigc::mem_fun(*this, &file_listener::read), _channel, Glib::IO_IN | Glib::IO_HUP | Glib::IO_ERR); + _main_loop = main; + + return; +} + +bool Script::file_listener::read(Glib::IOCondition condition) { + if (condition != Glib::IO_IN) { + _main_loop->quit(); + return false; + } + + Glib::IOStatus status; + Glib::ustring out; + status = _channel->read_line(out); + _string += out; + + if (status != Glib::IO_STATUS_NORMAL) { + _main_loop->quit(); + _dead = true; + return false; + } + + return true; +} + +bool Script::file_listener::toFile(const Glib::ustring &name) { + return toFile(Glib::filename_from_utf8(name)); +} + +bool Script::file_listener::toFile(const std::string &name) { + try { + Glib::RefPtr<Glib::IOChannel> stdout_file = Glib::IOChannel::create_from_file(name, "w"); + stdout_file->set_encoding(); + stdout_file->write(_string); + } catch (Glib::FileError &e) { + return false; + } + return true; +} + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 : diff --git a/src/extension/implementation/script.h b/src/extension/implementation/script.h new file mode 100644 index 0000000..bc865f4 --- /dev/null +++ b/src/extension/implementation/script.h @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code for handling extensions (i.e., scripts) + * + * Authors: + * Bryce Harrington <bryce@osdl.org> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN +#define INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN + +#include "implementation.h" +#include <gtkmm/enums.h> +#include <gtkmm/window.h> +#include <glibmm/main.h> +#include <glibmm/spawn.h> +#include <glibmm/fileutils.h> + +namespace Inkscape { +namespace XML { +class Node; +} // namespace XML + +namespace Extension { +namespace Implementation { + +/** + * Utility class used for loading and launching script extensions + */ +class Script : public Implementation { +public: + + Script(); + ~Script() override; + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + bool check(Inkscape::Extension::Extension *module) override; + + ImplementationDocumentCache * newDocCache(Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * view) override; + + Gtk::Widget *prefs_input(Inkscape::Extension::Input *module, gchar const *filename) override; + SPDocument *open(Inkscape::Extension::Input *module, gchar const *filename) override; + Gtk::Widget *prefs_output(Inkscape::Extension::Output *module) override; + void save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) override; + void export_raster(Inkscape::Extension::Output *module, + const SPDocument *doc, std::string const &png_file, gchar const *filename) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *doc, ImplementationDocumentCache * docCache) override; + bool cancelProcessing () override; + +private: + bool _canceled; + Glib::Pid _pid; + Glib::RefPtr<Glib::MainLoop> _main_loop; + + /** + * The command that has been derived from + * the configuration file with appropriate directories + */ + std::list<std::string> command; + + /** + * This is the extension that will be used + * as the helper to read in or write out the + * data + */ + Glib::ustring helper_extension; + + /** + * The window which should be considered as "parent window" of the script execution, + * e.g. when showin warning messages + * + * If set to NULL the main window of the currently active document is used. + */ + Gtk::Window *parent_window; + + void copy_doc(Inkscape::XML::Node * olddoc, Inkscape::XML::Node * newdoc); + void checkStderr (Glib::ustring const& filename, Gtk::MessageType type, Glib::ustring const& message); + + class file_listener { + Glib::ustring _string; + sigc::connection _conn; + Glib::RefPtr<Glib::IOChannel> _channel; + Glib::RefPtr<Glib::MainLoop> _main_loop; + bool _dead; + + public: + file_listener () : _dead(false) { }; + virtual ~file_listener () { + _conn.disconnect(); + }; + + bool isDead () { return _dead; } + void init(int fd, Glib::RefPtr<Glib::MainLoop> main); + bool read(Glib::IOCondition condition); + Glib::ustring string () { return _string; }; + bool toFile(const Glib::ustring &name); + bool toFile(const std::string &name); + }; + + int execute (const std::list<std::string> &in_command, + const std::list<std::string> &in_params, + const Glib::ustring &filein, + file_listener &fileout); + + void pump_events(); + + /** \brief A definition of an interpreter, which can be specified + in the INX file, but we need to know what to call */ + struct interpreter_t { + std::string prefstring; /**< The preferences key that can override the default */ + std::vector<std::string> defaultvals; /**< The default values to check if the preferences are wrong */ + }; + static const std::map<std::string, interpreter_t> interpreterTab; + + std::string resolveInterpreterExecutable(const Glib::ustring &interpNameArg); + +}; // class Script +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // INKSCAPE_EXTENSION_IMPEMENTATION_SCRIPT_H_SEEN + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/implementation/xslt.cpp b/src/extension/implementation/xslt.cpp new file mode 100644 index 0000000..dced1dd --- /dev/null +++ b/src/extension/implementation/xslt.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Code for handling XSLT extensions. + */ +/* + * Authors: + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "xslt.h" + +#include <unistd.h> +#include <cstring> + +#include <glibmm/fileutils.h> +#include <libxslt/transform.h> +#include <libxslt/xsltutils.h> + +#include "document.h" +#include "file.h" + +#include "extension/extension.h" +#include "extension/output.h" +#include "extension/input.h" + +#include "io/resource.h" + +#include "xml/node.h" +#include "xml/repr.h" + +#include <clocale> + +Inkscape::XML::Document * sp_repr_do_read (xmlDocPtr doc, const gchar * default_ns); + +/* Namespaces */ +namespace Inkscape { +namespace Extension { +namespace Implementation { + +/* Real functions */ +/** + \return A XSLT object + \brief This function creates a XSLT object and sets up the + variables. + +*/ +XSLT::XSLT() : + Implementation(), + _filename(""), + _parsedDoc(nullptr), + _stylesheet(nullptr) +{ +} + +bool XSLT::check(Inkscape::Extension::Extension *module) +{ + if (load(module)) { + unload(module); + return true; + } else { + return false; + } +} + +bool XSLT::load(Inkscape::Extension::Extension *module) +{ + if (module->loaded()) { return true; } + + Inkscape::XML::Node *child_repr = module->get_repr()->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "xslt")) { + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "file")) { + // TODO: we already parse xslt files as dependencies in extension.cpp + // can can we optimize this to be less indirect? + const char *filename = child_repr->firstChild()->content(); + _filename = module->get_dependency_location(filename); + } + child_repr = child_repr->next(); + } + + break; + } + child_repr = child_repr->next(); + } + + _parsedDoc = xmlParseFile(_filename.c_str()); + if (_parsedDoc == nullptr) { return false; } + + _stylesheet = xsltParseStylesheetDoc(_parsedDoc); + + return true; +} + +void XSLT::unload(Inkscape::Extension::Extension *module) +{ + if (!module->loaded()) { return; } + xsltFreeStylesheet(_stylesheet); + // No need to use xmlfreedoc(_parsedDoc), it's handled by xsltFreeStylesheet(_stylesheet); + return; +} + +SPDocument * XSLT::open(Inkscape::Extension::Input */*module*/, + gchar const *filename) +{ + xmlDocPtr filein = xmlParseFile(filename); + if (filein == nullptr) { return nullptr; } + + const char * params[1]; + params[0] = nullptr; + char *oldlocale = g_strdup(std::setlocale(LC_NUMERIC, nullptr)); + std::setlocale(LC_NUMERIC, "C"); + + xmlDocPtr result = xsltApplyStylesheet(_stylesheet, filein, params); + xmlFreeDoc(filein); + + Inkscape::XML::Document * rdoc = sp_repr_do_read( result, SP_SVG_NS_URI); + xmlFreeDoc(result); + std::setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + if (rdoc == nullptr) { + return nullptr; + } + + if (strcmp(rdoc->root()->name(), "svg:svg") != 0) { + return nullptr; + } + + gchar * base = nullptr; + gchar * name = nullptr; + gchar * s = nullptr, * p = nullptr; + s = g_strdup(filename); + p = strrchr(s, '/'); + if (p) { + name = g_strdup(p + 1); + p[1] = '\0'; + base = g_strdup(s); + } else { + base = nullptr; + name = g_strdup(filename); + } + g_free(s); + + SPDocument * doc = SPDocument::createDoc(rdoc, filename, base, name, true, nullptr); + + g_free(base); g_free(name); + + return doc; +} + +void XSLT::save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) +{ + /* TODO: Should we assume filename to be in utf8 or to be a raw filename? + * See JavaFXOutput::save for discussion. + * + * From JavaFXOutput::save (now removed): + * --- + * N.B. The name `filename_utf8' represents the fact that we want it to be in utf8; whereas in + * fact we know that some callers of Extension::save pass something in the filesystem's + * encoding, while others do g_filename_to_utf8 before calling. + * + * In terms of safety, it's best to make all callers pass actual filenames, since in general + * one can't round-trip from filename to utf8 back to the same filename. Whereas the argument + * for passing utf8 filenames is one of convenience: we often want to pass to g_warning or + * store as a string (rather than a byte stream) in XML, or the like. */ + g_return_if_fail(doc != nullptr); + g_return_if_fail(filename != nullptr); + + Inkscape::XML::Node *repr = doc->getReprRoot(); + + std::string tempfilename_out; + int tempfd_out = 0; + try { + tempfd_out = Glib::file_open_tmp(tempfilename_out, "ink_ext_XXXXXX"); + } catch (...) { + /// \todo Popup dialog here + return; + } + + if (!sp_repr_save_rebased_file(repr->document(), tempfilename_out.c_str(), SP_SVG_NS_URI, + doc->getDocumentBase(), filename)) { + throw Inkscape::Extension::Output::save_failed(); + } + + xmlDocPtr svgdoc = xmlParseFile(tempfilename_out.c_str()); + close(tempfd_out); + if (svgdoc == nullptr) { + return; + } + + std::list<std::string> params; + module->paramListString(params); + const int max_parameters = params.size() * 2; + const char * xslt_params[max_parameters+1] ; + + int count = 0; + for(auto & param : params) { + std::size_t pos = param.find("="); + std::ostringstream parameter; + std::ostringstream value; + parameter << param.substr(2,pos-2); + value << param.substr(pos+1); + xslt_params[count++] = g_strdup_printf("%s", parameter.str().c_str()); + xslt_params[count++] = g_strdup_printf("'%s'", value.str().c_str()); + } + xslt_params[count] = nullptr; + + // workaround for inbox#2208 + char *oldlocale = g_strdup(std::setlocale(LC_NUMERIC, nullptr)); + std::setlocale(LC_NUMERIC, "C"); + xmlDocPtr newdoc = xsltApplyStylesheet(_stylesheet, svgdoc, xslt_params); + //xmlSaveFile(filename, newdoc); + int success = xsltSaveResultToFilename(filename, newdoc, _stylesheet, 0); + std::setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + xmlFreeDoc(newdoc); + xmlFreeDoc(svgdoc); + + xsltCleanupGlobals(); + xmlCleanupParser(); + + if (success < 1) { + throw Inkscape::Extension::Output::save_failed(); + } + + return; +} + + +} /* Implementation */ +} /* module */ +} /* Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/implementation/xslt.h b/src/extension/implementation/xslt.h new file mode 100644 index 0000000..745d7f5 --- /dev/null +++ b/src/extension/implementation/xslt.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code for handling XSLT extensions + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ +#define __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ + +#include "implementation.h" + +#include "libxml/tree.h" +#include "libxslt/xslt.h" +#include "libxslt/xsltInternals.h" + +namespace Inkscape { +namespace XML { +class Node; +} +} + + +namespace Inkscape { +namespace Extension { +namespace Implementation { + +class XSLT : public Implementation { +private: + std::string _filename; + xmlDocPtr _parsedDoc; + xsltStylesheetPtr _stylesheet; + +public: + XSLT (); + + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + + bool check(Inkscape::Extension::Extension *module) override; + + SPDocument *open(Inkscape::Extension::Input *module, + gchar const *filename) override; + void save(Inkscape::Extension::Output *module, SPDocument *doc, gchar const *filename) override; +}; + +} /* Inkscape */ +} /* Extension */ +} /* Implementation */ +#endif /* __INKSCAPE_EXTENSION_IMPEMENTATION_XSLT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/init.cpp b/src/extension/init.cpp new file mode 100644 index 0000000..5e291c1 --- /dev/null +++ b/src/extension/init.cpp @@ -0,0 +1,328 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is what gets executed to initialize all of the modules. For + * the internal modules this involves executing their initialization + * functions, for external ones it involves reading their .spmodule + * files and bringing them into Sodipodi. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +# include "internal/pdfinput/pdf-input.h" +#endif + +#include "path-prefix.h" + +#include "inkscape.h" + +#include <glibmm/fileutils.h> +#include <glibmm/i18n.h> +#include <glibmm/ustring.h> + +#include "system.h" +#include "db.h" +#include "internal/svgz.h" +# include "internal/emf-inout.h" +# include "internal/emf-print.h" +# include "internal/wmf-inout.h" +# include "internal/wmf-print.h" + +#include <cairo.h> +#ifdef CAIRO_HAS_PDF_SURFACE +# include "internal/cairo-renderer-pdf-out.h" +#endif +#ifdef CAIRO_HAS_PS_SURFACE +# include "internal/cairo-ps-out.h" +#endif +#include "internal/png-output.h" +#include "internal/pov-out.h" +#include "internal/odf.h" +#include "internal/latex-pstricks-out.h" +#include "internal/latex-pstricks.h" +#include "internal/gdkpixbuf-input.h" +#include "internal/bluredge.h" +#include "internal/gimpgrad.h" +#include "internal/grid.h" +#ifdef WITH_LIBWPG +#include "internal/wpg-input.h" +#endif +#ifdef WITH_LIBVISIO +#include "internal/vsd-input.h" +#endif +#ifdef WITH_LIBCDR +#include "internal/cdr-input.h" +#endif +#include "preferences.h" +#include "io/sys.h" +#include "io/resource.h" + +#ifdef WITH_MAGICK +#include <Magick++.h> +#include "internal/bitmap/adaptiveThreshold.h" +#include "internal/bitmap/addNoise.h" +#include "internal/bitmap/blur.h" +#include "internal/bitmap/channel.h" +#include "internal/bitmap/charcoal.h" +#include "internal/bitmap/colorize.h" +#include "internal/bitmap/contrast.h" +#include "internal/bitmap/crop.h" +#include "internal/bitmap/cycleColormap.h" +#include "internal/bitmap/despeckle.h" +#include "internal/bitmap/edge.h" +#include "internal/bitmap/emboss.h" +#include "internal/bitmap/enhance.h" +#include "internal/bitmap/equalize.h" +#include "internal/bitmap/gaussianBlur.h" +#include "internal/bitmap/implode.h" +#include "internal/bitmap/level.h" +#include "internal/bitmap/levelChannel.h" +#include "internal/bitmap/medianFilter.h" +#include "internal/bitmap/modulate.h" +#include "internal/bitmap/negate.h" +#include "internal/bitmap/normalize.h" +#include "internal/bitmap/oilPaint.h" +#include "internal/bitmap/opacity.h" +#include "internal/bitmap/raise.h" +#include "internal/bitmap/reduceNoise.h" +#include "internal/bitmap/sample.h" +#include "internal/bitmap/shade.h" +#include "internal/bitmap/sharpen.h" +#include "internal/bitmap/solarize.h" +#include "internal/bitmap/spread.h" +#include "internal/bitmap/swirl.h" +//#include "internal/bitmap/threshold.h" +#include "internal/bitmap/unsharpmask.h" +#include "internal/bitmap/wave.h" +#endif /* WITH_MAGICK */ + +#include "internal/filter/filter.h" + +#include "init.h" + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace Extension { + +/** This is the extension that all files are that are pulled from + the extension directory and parsed */ +#define SP_MODULE_EXTENSION "inx" + +static void check_extensions(); + +/** + * \return none + * \brief Examines the given string preference and checks to see + * that at least one of the registered extensions matches + * it. If not, a default is assigned. + * \param pref_path Preference path to update + * \param pref_default Default string to set + * \param extension_family List of extensions to search + */ +static void +update_pref(Glib::ustring const &pref_path, + gchar const *pref_default) +{ + Glib::ustring pref = Inkscape::Preferences::get()->getString(pref_path); + if (!Inkscape::Extension::db.get( pref.data() ) /*missing*/) { + Inkscape::Preferences::get()->setString(pref_path, pref_default); + } +} + +// A list of user extensions loaded, used for refreshing +static std::vector<Glib::ustring> user_extensions; + +/** + * Invokes the init routines for internal modules. + * + * This should be a list of all the internal modules that need to initialized. This is just a + * convenient place to put them. + */ +void +init() +{ + /* TODO: Change to Internal */ + Internal::Svg::init(); + Internal::Svgz::init(); + +#ifdef CAIRO_HAS_PDF_SURFACE + Internal::CairoRendererPdfOutput::init(); +#endif +#ifdef CAIRO_HAS_PS_SURFACE + Internal::CairoPsOutput::init(); + Internal::CairoEpsOutput::init(); +#endif +#ifdef HAVE_POPPLER + Internal::PdfInput::init(); +#endif + Internal::PrintEmf::init(); + Internal::Emf::init(); + Internal::PrintWmf::init(); + Internal::Wmf::init(); + Internal::PngOutput::init(); + Internal::PovOutput::init(); + Internal::OdfOutput::init(); + Internal::PrintLatex::init(); + Internal::LatexOutput::init(); +#ifdef WITH_LIBWPG + Internal::WpgInput::init(); +#endif +#ifdef WITH_LIBVISIO + Internal::VsdInput::init(); +#endif +#ifdef WITH_LIBCDR + Internal::CdrInput::init(); +#endif + + /* Effects */ + Internal::BlurEdge::init(); + Internal::GimpGrad::init(); + Internal::Grid::init(); + + /* Raster Effects */ +#ifdef WITH_MAGICK + Magick::InitializeMagick(NULL); + + Internal::Bitmap::AdaptiveThreshold::init(); + Internal::Bitmap::AddNoise::init(); + Internal::Bitmap::Blur::init(); + Internal::Bitmap::Channel::init(); + Internal::Bitmap::Charcoal::init(); + Internal::Bitmap::Colorize::init(); + Internal::Bitmap::Contrast::init(); + Internal::Bitmap::Crop::init(); + Internal::Bitmap::CycleColormap::init(); + Internal::Bitmap::Edge::init(); + Internal::Bitmap::Despeckle::init(); + Internal::Bitmap::Emboss::init(); + Internal::Bitmap::Enhance::init(); + Internal::Bitmap::Equalize::init(); + Internal::Bitmap::GaussianBlur::init(); + Internal::Bitmap::Implode::init(); + Internal::Bitmap::Level::init(); + Internal::Bitmap::LevelChannel::init(); + Internal::Bitmap::MedianFilter::init(); + Internal::Bitmap::Modulate::init(); + Internal::Bitmap::Negate::init(); + Internal::Bitmap::Normalize::init(); + Internal::Bitmap::OilPaint::init(); + Internal::Bitmap::Opacity::init(); + Internal::Bitmap::Raise::init(); + Internal::Bitmap::ReduceNoise::init(); + Internal::Bitmap::Sample::init(); + Internal::Bitmap::Shade::init(); + Internal::Bitmap::Sharpen::init(); + Internal::Bitmap::Solarize::init(); + Internal::Bitmap::Spread::init(); + Internal::Bitmap::Swirl::init(); + //Internal::Bitmap::Threshold::init(); + Internal::Bitmap::Unsharpmask::init(); + Internal::Bitmap::Wave::init(); +#endif /* WITH_MAGICK */ + + Internal::Filter::Filter::filters_all(); + + // User extensions first so they can over-ride + load_user_extensions(); + + for(auto &filename: get_filenames(SYSTEM, EXTENSIONS, {SP_MODULE_EXTENSION})) { + build_from_file(filename.c_str()); + } + + /* this is at the very end because it has several catch-alls + * that are possibly over-ridden by other extensions (such as + * svgz) + */ + Internal::GdkpixbufInput::init(); + + /* now we need to check and make sure everyone is happy */ + check_extensions(); + + /* This is a hack to deal with updating saved outdated module + * names in the prefs... + */ + update_pref("/dialogs/save_as/default", + SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE + // Inkscape::Extension::db.get_output_list() + ); +} + +void +load_user_extensions() +{ + // There's no need to ask for SYSTEM extensions, just ask for user extensions. + for(auto &filename: get_filenames(USER, EXTENSIONS, {SP_MODULE_EXTENSION})) { + bool exist = false; + for(auto &filename2: user_extensions) { + if (filename == filename2) { + exist = true; + break; + } + } + if (!exist) { + build_from_file(filename.c_str()); + user_extensions.push_back(filename); + } + } +} + +/** + * Refresh user extensions + * + * Remember to call check_extensions() once completed. + */ +void +refresh_user_extensions() +{ + load_user_extensions(); + check_extensions(); +} + + +static void +check_extensions_internal(Extension *in_plug, gpointer in_data) +{ + int *count = (int *)in_data; + + if (in_plug == nullptr) return; + if (!in_plug->deactivated() && !in_plug->check()) { + in_plug->deactivate(); + (*count)++; + } +} + +static void check_extensions() +{ + int count = 1; + + Inkscape::Extension::Extension::error_file_open(); + while (count != 0) { + count = 0; + db.foreach(check_extensions_internal, (gpointer)&count); + } + Inkscape::Extension::Extension::error_file_close(); +} + +} } /* namespace Inkscape::Extension */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/init.h b/src/extension/init.h new file mode 100644 index 0000000..71de5ac --- /dev/null +++ b/src/extension/init.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is what gets executed to initialize all of the modules. For + * the internal modules this invovles executing their initialization + * functions, for external ones it involves reading their .spmodule + * files and bringing them into Sodipodi. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_INIT_H__ +#define INKSCAPE_EXTENSION_INIT_H__ + +namespace Inkscape { +namespace Extension { + +void init (); +void load_user_extensions(); +void refresh_user_extensions(); +} } /* namespace Inkscape::Extension */ + +#endif /* INKSCAPE_EXTENSION_INIT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/input.cpp b/src/extension/input.cpp new file mode 100644 index 0000000..b82d44a --- /dev/null +++ b/src/extension/input.cpp @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "input.h" + +#include "timer.h" + +#include "implementation/implementation.h" + +#include "prefdialog/prefdialog.h" + +#include "xml/repr.h" + + +/* Inkscape::Extension::Input */ + +namespace Inkscape { +namespace Extension { + +/** + \return None + \brief Builds a SPModuleInput object from a XML description + \param module The module to be initialized + \param repr The XML description in a Inkscape::XML::Node tree + + Okay, so you want to build a SPModuleInput object. + + This function first takes and does the build of the parent class, + which is SPModule. Then, it looks for the <input> section of the + XML description. Under there should be several fields which + describe the input module to excruciating detail. Those are parsed, + copied, and put into the structure that is passed in as module. + Overall, there are many levels of indentation, just to handle the + levels of indentation in the XML file. +*/ +Input::Input (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + mimetype = nullptr; + extension = nullptr; + filetypename = nullptr; + filetypetooltip = nullptr; + + if (repr != nullptr) { + Inkscape::XML::Node * child_repr; + + child_repr = repr->firstChild(); + + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "input")) { + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + char const * chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') /* Allow _ for translation of tags */ + chname++; + if (!strcmp(chname, "extension")) { + g_free (extension); + extension = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "mimetype")) { + g_free (mimetype); + mimetype = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypename")) { + g_free (filetypename); + filetypename = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypetooltip")) { + g_free (filetypetooltip); + filetypetooltip = g_strdup(child_repr->firstChild()->content()); + } + + child_repr = child_repr->next(); + } + + break; + } + + child_repr = child_repr->next(); + } + + } + + return; +} + +/** + \return None + \brief Destroys an Input extension +*/ +Input::~Input () +{ + g_free(mimetype); + g_free(extension); + g_free(filetypename); + g_free(filetypetooltip); + return; +} + +/** + \return Whether this extension checks out + \brief Validate this extension + + This function checks to make sure that the input extension has + a filename extension and a MIME type. Then it calls the parent + class' check function which also checks out the implementation. +*/ +bool +Input::check () +{ + if (extension == nullptr) + return FALSE; + if (mimetype == nullptr) + return FALSE; + + return Extension::check(); +} + +/** + \return A new document + \brief This function creates a document from a file + \param uri The filename to create the document from + + This function acts as the first step in creating a new document + from a file. The first thing that this does is make sure that the + file actually exists. If it doesn't, a NULL is returned. If the + file exits, then it is opened using the implementation of this extension. +*/ +SPDocument * +Input::open (const gchar *uri) +{ + if (!loaded()) { + set_state(Extension::STATE_LOADED); + } + if (!loaded()) { + return nullptr; + } + timer->touch(); + + SPDocument *const doc = imp->open(this, uri); + + return doc; +} + +/** + \return IETF mime-type for the extension + \brief Get the mime-type that describes this extension +*/ +gchar * +Input::get_mimetype() +{ + return mimetype; +} + +/** + \return Filename extension for the extension + \brief Get the filename extension for this extension +*/ +gchar * +Input::get_extension() +{ + return extension; +} + +/** + \return The name of the filetype supported + \brief Get the name of the filetype supported +*/ +const char * +Input::get_filetypename(bool translated) +{ + const char *name; + + if (filetypename) + name = filetypename; + else + name = get_name(); + + if (name && translated && filetypename) { + return get_translation(name); + } else { + return name; + } +} + +/** + \return Tooltip giving more information on the filetype + \brief Get the tooltip for more information on the filetype +*/ +const char * +Input::get_filetypetooltip(bool translated) +{ + if (filetypetooltip && translated) { + return get_translation(filetypetooltip); + } else { + return filetypetooltip; + } +} + +/** + \return A dialog to get settings for this extension + \brief Create a dialog for preference for this extension + + Calls the implementation to get the preferences. +*/ +bool +Input::prefs (const gchar *uri) +{ + if (!loaded()) { + set_state(Extension::STATE_LOADED); + } + if (!loaded()) { + return false; + } + + Gtk::Widget * controls; + controls = imp->prefs_input(this, uri); + if (controls == nullptr) { + // std::cout << "No preferences for Input" << std::endl; + return true; + } + + Glib::ustring name = this->get_name(); + PrefDialog *dialog = new PrefDialog(name, controls); + int response = dialog->run(); + dialog->hide(); + + delete dialog; + + return (response == Gtk::RESPONSE_OK); +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/input.h b/src/extension/input.h new file mode 100644 index 0000000..64cce94 --- /dev/null +++ b/src/extension/input.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef INKSCAPE_EXTENSION_INPUT_H__ +#define INKSCAPE_EXTENSION_INPUT_H__ + +#include <exception> +#include <glib.h> +#include "extension.h" + +class SPDocument; + +namespace Inkscape { +namespace Extension { + +class Input : public Extension { + gchar *mimetype; /**< What is the mime type this inputs? */ + gchar *extension; /**< The extension of the input files */ + gchar *filetypename; /**< A userfriendly name for the file type */ + gchar *filetypetooltip; /**< A more detailed description of the filetype */ + +public: + struct open_failed : public std::exception { + ~open_failed() noexcept override = default; + const char *what() const noexcept override { return "Open failed"; } + }; + struct no_extension_found : public std::exception { + ~no_extension_found() noexcept override = default; + const char *what() const noexcept override { return "No suitable input extension found"; } + }; + struct open_cancelled : public std::exception { + ~open_cancelled() noexcept override = default; + const char *what() const noexcept override { return "Open was cancelled"; } + }; + + Input(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Input() override; + + bool check() override; + + SPDocument * open (gchar const *uri); + gchar * get_mimetype (); + gchar * get_extension (); + const char * get_filetypename (bool translated=false); + const char * get_filetypetooltip (bool translated=false); + bool prefs (gchar const *uri); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_INPUT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/bitmap/adaptiveThreshold.cpp b/src/extension/internal/bitmap/adaptiveThreshold.cpp new file mode 100644 index 0000000..7c85d84 --- /dev/null +++ b/src/extension/internal/bitmap/adaptiveThreshold.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "adaptiveThreshold.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +AdaptiveThreshold::applyEffect(Magick::Image *image) { + image->adaptiveThreshold(_width, _height); +} + +void +AdaptiveThreshold::refreshParameters(Inkscape::Extension::Effect *module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); + _offset = module->get_param_int("offset"); +} + +#include "../clear-n_.h" + +void +AdaptiveThreshold::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Adaptive Threshold") "</name>\n" + "<id>org.inkscape.effect.bitmap.adaptiveThreshold</id>\n" + "<param name=\"width\" gui-text=\"" N_("Width:") "\" type=\"int\" min=\"-100\" max=\"100\">5</param>\n" + "<param name=\"height\" gui-text=\"" N_("Height:") "\" type=\"int\" min=\"-100\" max=\"100\">5</param>\n" + "<param name=\"offset\" gui-text=\"" N_("Offset:") "\" type=\"int\" min=\"0\" max=\"100\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Apply adaptive thresholding to selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new AdaptiveThreshold()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/adaptiveThreshold.h b/src/extension/internal/bitmap/adaptiveThreshold.h new file mode 100644 index 0000000..066f13b --- /dev/null +++ b/src/extension/internal/bitmap/adaptiveThreshold.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class AdaptiveThreshold : public ImageMagick +{ +private: + unsigned int _width; + unsigned int _height; + unsigned _offset; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/addNoise.cpp b/src/extension/internal/bitmap/addNoise.cpp new file mode 100644 index 0000000..0bedb3c --- /dev/null +++ b/src/extension/internal/bitmap/addNoise.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "addNoise.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +AddNoise::applyEffect(Magick::Image *image) { + Magick::NoiseType noiseType = Magick::UniformNoise; + if (!strcmp(_noiseTypeName, "Uniform Noise")) noiseType = Magick::UniformNoise; + else if (!strcmp(_noiseTypeName, "Gaussian Noise")) noiseType = Magick::GaussianNoise; + else if (!strcmp(_noiseTypeName, "Multiplicative Gaussian Noise")) noiseType = Magick::MultiplicativeGaussianNoise; + else if (!strcmp(_noiseTypeName, "Impulse Noise")) noiseType = Magick::ImpulseNoise; + else if (!strcmp(_noiseTypeName, "Laplacian Noise")) noiseType = Magick::LaplacianNoise; + else if (!strcmp(_noiseTypeName, "Poisson Noise")) noiseType = Magick::PoissonNoise; + + image->addNoise(noiseType); +} + +void +AddNoise::refreshParameters(Inkscape::Extension::Effect *module) { + _noiseTypeName = module->get_param_optiongroup("noiseType"); +} + +#include "../clear-n_.h" + +void +AddNoise::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Add Noise") "</name>\n" + "<id>org.inkscape.effect.bitmap.addNoise</id>\n" + "<param name=\"noiseType\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='Uniform Noise'>" N_("Uniform Noise") "</option>\n" + "<option value='Gaussian Noise'>" N_("Gaussian Noise") "</option>\n" + "<option value='Multiplicative Gaussian Noise'>" N_("Multiplicative Gaussian Noise") "</option>\n" + "<option value='Impulse Noise'>" N_("Impulse Noise") "</option>\n" + "<option value='Laplacian Noise'>" N_("Laplacian Noise") "</option>\n" + "<option value='Poisson Noise'>" N_("Poisson Noise") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Add random noise to selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new AddNoise()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/addNoise.h b/src/extension/internal/bitmap/addNoise.h new file mode 100644 index 0000000..06ce8c3 --- /dev/null +++ b/src/extension/internal/bitmap/addNoise.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class AddNoise : public ImageMagick +{ +private: + const gchar* _noiseTypeName; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/blur.cpp b/src/extension/internal/bitmap/blur.cpp new file mode 100644 index 0000000..ba9d523 --- /dev/null +++ b/src/extension/internal/bitmap/blur.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "blur.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Blur::applyEffect(Magick::Image *image) { + image->blur(_radius, _sigma); +} + +void +Blur::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Blur::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Blur") "</name>\n" + "<id>org.inkscape.effect.bitmap.blur</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0\" max=\"100\">1</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"0\" max=\"100\">0.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blur selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Blur()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/blur.h b/src/extension/internal/bitmap/blur.h new file mode 100644 index 0000000..0ed158a --- /dev/null +++ b/src/extension/internal/bitmap/blur.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Blur : public ImageMagick +{ +private: + double _radius; + double _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/channel.cpp b/src/extension/internal/bitmap/channel.cpp new file mode 100644 index 0000000..38ba8f1 --- /dev/null +++ b/src/extension/internal/bitmap/channel.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "channel.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Channel::applyEffect(Magick::Image *image) { + Magick::ChannelType layer = Magick::UndefinedChannel; + if (!strcmp(_layerName, "Red Channel")) layer = Magick::RedChannel; + else if (!strcmp(_layerName, "Green Channel")) layer = Magick::GreenChannel; + else if (!strcmp(_layerName, "Blue Channel")) layer = Magick::BlueChannel; + else if (!strcmp(_layerName, "Cyan Channel")) layer = Magick::CyanChannel; + else if (!strcmp(_layerName, "Magenta Channel")) layer = Magick::MagentaChannel; + else if (!strcmp(_layerName, "Yellow Channel")) layer = Magick::YellowChannel; + else if (!strcmp(_layerName, "Black Channel")) layer = Magick::BlackChannel; + else if (!strcmp(_layerName, "Opacity Channel")) layer = Magick::OpacityChannel; + else if (!strcmp(_layerName, "Matte Channel")) layer = Magick::MatteChannel; + + image->channel(layer); +} + +void +Channel::refreshParameters(Inkscape::Extension::Effect *module) { + _layerName = module->get_param_optiongroup("layer"); +} + +#include "../clear-n_.h" + +void +Channel::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Channel") "</name>\n" + "<id>org.inkscape.effect.bitmap.channel</id>\n" + "<param name=\"layer\" gui-text=\"" N_("Layer:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='Red Channel'>" N_("Red Channel") "</option>\n" + "<option value='Green Channel'>" N_("Green Channel") "</option>\n" + "<option value='Blue Channel'>" N_("Blue Channel") "</option>\n" + "<option value='Cyan Channel'>" N_("Cyan Channel") "</option>\n" + "<option value='Magenta Channel'>" N_("Magenta Channel") "</option>\n" + "<option value='Yellow Channel'>" N_("Yellow Channel") "</option>\n" + "<option value='Black Channel'>" N_("Black Channel") "</option>\n" + "<option value='Opacity Channel'>" N_("Opacity Channel") "</option>\n" + "<option value='Matte Channel'>" N_("Matte Channel") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Extract specific channel from image") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Channel()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/channel.h b/src/extension/internal/bitmap/channel.h new file mode 100644 index 0000000..e215344 --- /dev/null +++ b/src/extension/internal/bitmap/channel.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Channel : public ImageMagick { + +private: + const gchar * _layerName; + +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/charcoal.cpp b/src/extension/internal/bitmap/charcoal.cpp new file mode 100644 index 0000000..6343399 --- /dev/null +++ b/src/extension/internal/bitmap/charcoal.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "charcoal.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Charcoal::applyEffect(Magick::Image* image) { + image->charcoal(_radius, _sigma); +} + +void +Charcoal::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Charcoal::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Charcoal") "</name>\n" + "<id>org.inkscape.effect.bitmap.charcoal</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0\" max=\"100\">1</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"0\" max=\"100\">0.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Apply charcoal stylization to selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Charcoal()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/charcoal.h b/src/extension/internal/bitmap/charcoal.h new file mode 100644 index 0000000..da381d9 --- /dev/null +++ b/src/extension/internal/bitmap/charcoal.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Charcoal : public ImageMagick +{ +private: + double _radius; + double _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/colorize.cpp b/src/extension/internal/bitmap/colorize.cpp new file mode 100644 index 0000000..ea9d748 --- /dev/null +++ b/src/extension/internal/bitmap/colorize.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "colorize.h" + +#include "color.h" + +#include <iostream> +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Colorize::applyEffect(Magick::Image *image) { + float r = ((_color >> 24) & 0xff) / 255.0F; + float g = ((_color >> 16) & 0xff) / 255.0F; + float b = ((_color >> 8) & 0xff) / 255.0F; + float a = ((_color ) & 0xff) / 255.0F; + + Magick::ColorRGB mc(r,g,b); + + image->colorize(a * 100, mc); +} + +void +Colorize::refreshParameters(Inkscape::Extension::Effect *module) { + _color = module->get_param_color("color"); +} + +#include "../clear-n_.h" + +void +Colorize::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Colorize") "</name>\n" + "<id>org.inkscape.effect.bitmap.colorize</id>\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Colorize selected bitmap(s) with specified color, using given opacity") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Colorize()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/colorize.h b/src/extension/internal/bitmap/colorize.h new file mode 100644 index 0000000..7c16ef5 --- /dev/null +++ b/src/extension/internal/bitmap/colorize.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Colorize : public ImageMagick { +private: + unsigned int _opacity; + guint32 _color; + +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/contrast.cpp b/src/extension/internal/bitmap/contrast.cpp new file mode 100644 index 0000000..2f43923 --- /dev/null +++ b/src/extension/internal/bitmap/contrast.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "contrast.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Contrast::applyEffect(Magick::Image *image) { + // the contrast method's argument seems to be binary, so we perform it multiple times + // to get the desired level of effect + for (unsigned int i = 0; i < _sharpen; i ++) + image->contrast(1); +} + +void +Contrast::refreshParameters(Inkscape::Extension::Effect *module) { + _sharpen = module->get_param_int("sharpen"); +} + +#include "../clear-n_.h" + +void +Contrast::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Contrast") "</name>\n" + "<id>org.inkscape.effect.bitmap.contrast</id>\n" + "<param name=\"sharpen\" gui-text=\"" N_("Adjust:") "\" type=\"int\" min=\"0\" max=\"10\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Increase or decrease contrast in bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Contrast()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/contrast.h b/src/extension/internal/bitmap/contrast.h new file mode 100644 index 0000000..c0e95e4 --- /dev/null +++ b/src/extension/internal/bitmap/contrast.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Contrast : public ImageMagick +{ +private: + unsigned int _sharpen; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/crop.cpp b/src/extension/internal/bitmap/crop.cpp new file mode 100644 index 0000000..785c32a --- /dev/null +++ b/src/extension/internal/bitmap/crop.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2011 Authors: + * Nicolas Dufour <nicoduf@yahoo.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "2geom/transforms.h" +#include "extension/effect.h" +#include "extension/system.h" + +#include "crop.h" +#include "selection-chemistry.h" +#include "object/sp-item.h" +#include "object/sp-item-transform.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Crop::applyEffect(Magick::Image *image) { + int width = image->baseColumns() - (_left + _right); + int height = image->baseRows() - (_top + _bottom); + if (width > 0 and height > 0) { + image->crop(Magick::Geometry(width, height, _left, _top, false, false)); + image->page("+0+0"); + } +} + +void +Crop::postEffect(Magick::Image *image, SPItem *item) { + + // Scale bbox + Geom::Scale scale (0,0); + scale = Geom::Scale(image->columns() / (double) image->baseColumns(), + image->rows() / (double) image->baseRows()); + item->scale_rel(scale); + + // Translate proportionaly to the image/bbox ratio + Geom::OptRect bbox(item->desktopGeometricBounds()); + //g_warning("bbox. W:%f, H:%f, X:%f, Y:%f", bbox->dimensions()[Geom::X], bbox->dimensions()[Geom::Y], bbox->min()[Geom::X], bbox->min()[Geom::Y]); + + Geom::Translate translate (0,0); + translate = Geom::Translate(((_left - _right) / 2.0) * (bbox->dimensions()[Geom::X] / (double) image->columns()), + ((_bottom - _top) / 2.0) * (bbox->dimensions()[Geom::Y] / (double) image->rows())); + item->move_rel(translate); +} + +void +Crop::refreshParameters(Inkscape::Extension::Effect *module) { + _top = module->get_param_int("top"); + _bottom = module->get_param_int("bottom"); + _left = module->get_param_int("left"); + _right = module->get_param_int("right"); +} + +#include "../clear-n_.h" + +void +Crop::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Crop") "</name>\n" + "<id>org.inkscape.effect.bitmap.crop</id>\n" + "<param name=\"top\" gui-text=\"" N_("Top (px):") "\" type=\"int\" min=\"0\" max=\"100000\">0</param>\n" + "<param name=\"bottom\" gui-text=\"" N_("Bottom (px):") "\" type=\"int\" min=\"0\" max=\"100000\">0</param>\n" + "<param name=\"left\" gui-text=\"" N_("Left (px):") "\" type=\"int\" min=\"0\" max=\"100000\">0</param>\n" + "<param name=\"right\" gui-text=\"" N_("Right (px):") "\" type=\"int\" min=\"0\" max=\"100000\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Crop selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Crop()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/crop.h b/src/extension/internal/bitmap/crop.h new file mode 100644 index 0000000..da53878 --- /dev/null +++ b/src/extension/internal/bitmap/crop.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2010 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * Nicolas Dufour <nicoduf@yahoo.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Crop : public ImageMagick +{ +private: + int _top; + int _bottom; + int _left; + int _right; +public: + void applyEffect(Magick::Image *image) override; + void postEffect(Magick::Image *image, SPItem *item) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/cycleColormap.cpp b/src/extension/internal/bitmap/cycleColormap.cpp new file mode 100644 index 0000000..c28e5e6 --- /dev/null +++ b/src/extension/internal/bitmap/cycleColormap.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "cycleColormap.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +CycleColormap::applyEffect(Magick::Image *image) { + image->cycleColormap(_amount); +} + +void +CycleColormap::refreshParameters(Inkscape::Extension::Effect *module) { + _amount = module->get_param_int("amount"); +} + +#include "../clear-n_.h" + +void +CycleColormap::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Cycle Colormap") "</name>\n" + "<id>org.inkscape.effect.bitmap.cycleColormap</id>\n" + "<param name=\"amount\" gui-text=\"" N_("Amount:") "\" type=\"int\" min=\"0\" max=\"360\">180</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Cycle colormap(s) of selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new CycleColormap()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/cycleColormap.h b/src/extension/internal/bitmap/cycleColormap.h new file mode 100644 index 0000000..0d66b15 --- /dev/null +++ b/src/extension/internal/bitmap/cycleColormap.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class CycleColormap : public ImageMagick { +private: + int _amount; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/despeckle.cpp b/src/extension/internal/bitmap/despeckle.cpp new file mode 100644 index 0000000..46a1baf --- /dev/null +++ b/src/extension/internal/bitmap/despeckle.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "despeckle.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Despeckle::applyEffect(Magick::Image *image) { + image->despeckle(); +} + +void +Despeckle::refreshParameters(Inkscape::Extension::Effect */*module*/) { +} + +#include "../clear-n_.h" + +void +Despeckle::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Despeckle") "</name>\n" + "<id>org.inkscape.effect.bitmap.despeckle</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Reduce speckle noise of selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Despeckle()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/despeckle.h b/src/extension/internal/bitmap/despeckle.h new file mode 100644 index 0000000..0c731ee --- /dev/null +++ b/src/extension/internal/bitmap/despeckle.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Despeckle : public ImageMagick { +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/edge.cpp b/src/extension/internal/bitmap/edge.cpp new file mode 100644 index 0000000..93b7394 --- /dev/null +++ b/src/extension/internal/bitmap/edge.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "edge.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Edge::applyEffect(Magick::Image *image) { + image->edge(_radius); +} + +void +Edge::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_int("radius"); +} + +#include "../clear-n_.h" + +void +Edge::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Edge") "</name>\n" + "<id>org.inkscape.effect.bitmap.edge</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"int\" min=\"0\" max=\"100\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Highlight edges of selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Edge()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/edge.h b/src/extension/internal/bitmap/edge.h new file mode 100644 index 0000000..2c5fe14 --- /dev/null +++ b/src/extension/internal/bitmap/edge.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Edge : public ImageMagick { +private: + unsigned int _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/emboss.cpp b/src/extension/internal/bitmap/emboss.cpp new file mode 100644 index 0000000..86988c9 --- /dev/null +++ b/src/extension/internal/bitmap/emboss.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "emboss.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Emboss::applyEffect(Magick::Image *image) { + image->emboss(_radius, _sigma); +} + +void +Emboss::refreshParameters(Inkscape::Extension::Effect *module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Emboss::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Emboss") "</name>\n" + "<id>org.inkscape.effect.bitmap.emboss</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0\" max=\"100\">1.0</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"-50\" max=\"50\">0.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Emboss selected bitmap(s); highlight edges with 3D effect") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Emboss()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/emboss.h b/src/extension/internal/bitmap/emboss.h new file mode 100644 index 0000000..51d04d6 --- /dev/null +++ b/src/extension/internal/bitmap/emboss.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Emboss : public ImageMagick +{ +private: + double _radius; + double _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/enhance.cpp b/src/extension/internal/bitmap/enhance.cpp new file mode 100644 index 0000000..391d1f1 --- /dev/null +++ b/src/extension/internal/bitmap/enhance.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "enhance.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Enhance::applyEffect(Magick::Image *image) { + image->enhance(); +} + +void +Enhance::refreshParameters(Inkscape::Extension::Effect */*module*/) { } + +#include "../clear-n_.h" + +void +Enhance::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Enhance") "</name>\n" + "<id>org.inkscape.effect.bitmap.enhance</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Enhance selected bitmap(s); minimize noise") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Enhance()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/enhance.h b/src/extension/internal/bitmap/enhance.h new file mode 100644 index 0000000..dd3d9ff --- /dev/null +++ b/src/extension/internal/bitmap/enhance.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Enhance : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/equalize.cpp b/src/extension/internal/bitmap/equalize.cpp new file mode 100644 index 0000000..df0575e --- /dev/null +++ b/src/extension/internal/bitmap/equalize.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "equalize.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Equalize::applyEffect(Magick::Image *image) { + image->equalize(); +} + +void +Equalize::refreshParameters(Inkscape::Extension::Effect */*module*/) { } + +#include "../clear-n_.h" + +void +Equalize::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Equalize") "</name>\n" + "<id>org.inkscape.effect.bitmap.equalize</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Equalize selected bitmap(s); histogram equalization") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Equalize()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/equalize.h b/src/extension/internal/bitmap/equalize.h new file mode 100644 index 0000000..8259ffb --- /dev/null +++ b/src/extension/internal/bitmap/equalize.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Equalize : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init (); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/gaussianBlur.cpp b/src/extension/internal/bitmap/gaussianBlur.cpp new file mode 100644 index 0000000..1b8396b --- /dev/null +++ b/src/extension/internal/bitmap/gaussianBlur.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "gaussianBlur.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +GaussianBlur::applyEffect(Magick::Image* image) { + image->gaussianBlur(_width, _sigma); +} + +void +GaussianBlur::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_float("width"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +GaussianBlur::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Gaussian Blur") "</name>\n" + "<id>org.inkscape.effect.bitmap.gaussianBlur</id>\n" + "<param name=\"width\" gui-text=\"" N_("Factor:") "\" type=\"float\" min=\"0\" max=\"100\">5.0</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"0\" max=\"100\">5.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Gaussian blur selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new GaussianBlur()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/gaussianBlur.h b/src/extension/internal/bitmap/gaussianBlur.h new file mode 100644 index 0000000..9c9c500 --- /dev/null +++ b/src/extension/internal/bitmap/gaussianBlur.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class GaussianBlur : public ImageMagick +{ +private: + double _width; + double _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/imagemagick.cpp b/src/extension/internal/bitmap/imagemagick.cpp new file mode 100644 index 0000000..36176bc --- /dev/null +++ b/src/extension/internal/bitmap/imagemagick.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <libintl.h> + +#include <gtkmm/box.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/spinbutton.h> + +#include <glib/gstdio.h> + +#include "desktop.h" + +#include "selection.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "imagemagick.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class ImageMagickDocCache: public Inkscape::Extension::Implementation::ImplementationDocumentCache { + friend class ImageMagick; +private: + void readImage(char const *xlink, char const *id, Magick::Image *image); +protected: + Inkscape::XML::Node** _nodes; + + Magick::Image** _images; + int _imageCount; + char** _caches; + unsigned* _cacheLengths; + const char** _originals; + SPItem** _imageItems; +public: + ImageMagickDocCache(Inkscape::UI::View::View * view); + ~ImageMagickDocCache ( ) override; +}; + +ImageMagickDocCache::ImageMagickDocCache(Inkscape::UI::View::View * view) : + Inkscape::Extension::Implementation::ImplementationDocumentCache(view), + _nodes(NULL), + _images(NULL), + _imageCount(0), + _caches(NULL), + _cacheLengths(NULL), + _originals(NULL), + _imageItems(NULL) +{ + SPDesktop *desktop = (SPDesktop*)view; + auto selectedItemList = desktop->selection->items(); + int selectCount = (int) boost::distance(selectedItemList); + + // Init the data-holders + _nodes = new Inkscape::XML::Node*[selectCount]; + _originals = new const char*[selectCount]; + _caches = new char*[selectCount]; + _cacheLengths = new unsigned int[selectCount]; + _images = new Magick::Image*[selectCount]; + _imageCount = 0; + _imageItems = new SPItem*[selectCount]; + + // Loop through selected items + for (auto i = selectedItemList.begin(); i != selectedItemList.end(); ++i) { + SPItem *item = *i; + Inkscape::XML::Node *node = reinterpret_cast<Inkscape::XML::Node *>(item->getRepr()); + if (!strcmp(node->name(), "image") || !strcmp(node->name(), "svg:image")) + { + _nodes[_imageCount] = node; + char const *xlink = node->attribute("xlink:href"); + char const *id = node->attribute("id"); + _originals[_imageCount] = xlink; + _caches[_imageCount] = (char*)""; + _cacheLengths[_imageCount] = 0; + _images[_imageCount] = new Magick::Image(); + readImage(xlink, id, _images[_imageCount]); + _imageItems[_imageCount] = item; + _imageCount++; + } + } +} + +ImageMagickDocCache::~ImageMagickDocCache ( ) { + if (_nodes) + delete _nodes; + if (_originals) + delete _originals; + if (_caches) + delete _caches; + if (_cacheLengths) + delete _cacheLengths; + if (_images) + delete _images; + if (_imageItems) + delete _imageItems; + return; +} + +void +ImageMagickDocCache::readImage(const char *xlink, const char *id, Magick::Image *image) +{ + // Find if the xlink:href is base64 data, i.e. if the image is embedded + gchar *search = g_strndup(xlink, 30); + if (strstr(search, "base64") != (char*)NULL) { + // 7 = strlen("base64") + strlen(",") + const char* pureBase64 = strstr(xlink, "base64") + 7; + Magick::Blob blob; + blob.base64(pureBase64); + try { + image->read(blob); + } catch (Magick::Exception &error_) { + g_warning("ImageMagick could not read '%s'\nDetails: %s", id, error_.what()); + } + } else { + gchar *path; + if (strncmp (xlink,"file:", 5) == 0) { + path = g_filename_from_uri(xlink, NULL, NULL); + } else { + path = g_strdup(xlink); + } + try { + image->read(path); + } catch (Magick::Exception &error_) { + g_warning("ImageMagick could not read '%s' from '%s'\nDetails: %s", id, path, error_.what()); + } + g_free(path); + } + g_free(search); +} + +bool +ImageMagick::load(Inkscape::Extension::Extension */*module*/) +{ + return true; +} + +Inkscape::Extension::Implementation::ImplementationDocumentCache * +ImageMagick::newDocCache (Inkscape::Extension::Extension * /*ext*/, Inkscape::UI::View::View * view) { + return new ImageMagickDocCache(view); +} + +void +ImageMagick::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) +{ + refreshParameters(module); + + if (docCache == NULL) { // should never happen + docCache = newDocCache(module, document); + } + ImageMagickDocCache * dc = dynamic_cast<ImageMagickDocCache *>(docCache); + if (dc == NULL) { // should really never happen + printf("AHHHHHHHHH!!!!!"); + exit(1); + } + + for (int i = 0; i < dc->_imageCount; i++) + { + try + { + Magick::Image effectedImage = *dc->_images[i]; // make a copy + + applyEffect(&effectedImage); + + // postEffect can be used to change things on the item itself + // e.g. resize the image element, after the effecti is applied + postEffect(&effectedImage, dc->_imageItems[i]); + +// dc->_nodes[i]->setAttribute("xlink:href", dc->_caches[i]); + + Magick::Blob *blob = new Magick::Blob(); + effectedImage.write(blob); + + std::string raw_string = blob->base64(); + const int raw_len = raw_string.length(); + const char *raw_i = raw_string.c_str(); + + unsigned new_len = (int)(raw_len * (77.0 / 76.0) + 100); + if (new_len > dc->_cacheLengths[i]) { + dc->_cacheLengths[i] = (int)(new_len * 1.2); + dc->_caches[i] = new char[dc->_cacheLengths[i]]; + } + char *formatted_i = dc->_caches[i]; + const char *src; + + for (src = "data:image/"; *src; ) + *formatted_i++ = *src++; + for (src = effectedImage.magick().c_str(); *src ; ) + *formatted_i++ = *src++; + for (src = ";base64, \n" ; *src; ) + *formatted_i++ = *src++; + + int col = 0; + while (*raw_i) { + *formatted_i++ = *raw_i++; + if (col++ > 76) { + *formatted_i++ = '\n'; + col = 0; + } + } + if (col) { + *formatted_i++ = '\n'; + } + *formatted_i = '\0'; + + dc->_nodes[i]->setAttribute("xlink:href", dc->_caches[i]); + dc->_nodes[i]->removeAttribute("sodipodi:absref"); + delete blob; + } + catch (Magick::Exception &error_) { + printf("Caught exception: %s \n", error_.what()); + } + + //while(Gtk::Main::events_pending()) { + // Gtk::Main::iteration(); + //} + } +} + +/** \brief A function to get the preferences for the grid + \param module Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +ImageMagick::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = NULL; + if (!selected.empty()) { + first_select = (selected.front())->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/imagemagick.h b/src/extension/internal/bitmap/imagemagick.h new file mode 100644 index 0000000..754e195 --- /dev/null +++ b/src/extension/internal/bitmap/imagemagick.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H +#define INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H + +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +class SPItem; + +namespace Magick { +class Image; +} + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { +namespace Bitmap { + +class ImageMagick : public Inkscape::Extension::Implementation::Implementation { +public: + /* Functions to be implemented by subclasses */ + virtual void applyEffect(Magick::Image */*image*/) { }; + virtual void refreshParameters(Inkscape::Extension::Effect */*module*/) { }; + virtual void postEffect(Magick::Image */*image*/, SPItem */*item*/) { }; + + /* Functions implemented from ::Implementation */ + bool load(Inkscape::Extension::Extension *module) override; + Inkscape::Extension::Implementation::ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * doc) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget* prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +#endif // INKSCAPE_EXTENSION_INTERNAL_BITMAP_IMAGEMAGICK_H diff --git a/src/extension/internal/bitmap/implode.cpp b/src/extension/internal/bitmap/implode.cpp new file mode 100644 index 0000000..4395e2a --- /dev/null +++ b/src/extension/internal/bitmap/implode.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "implode.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Implode::applyEffect(Magick::Image* image) { + image->implode(_factor); +} + +void +Implode::refreshParameters(Inkscape::Extension::Effect* module) { + _factor = module->get_param_float("factor"); +} + +#include "../clear-n_.h" + +void +Implode::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Implode") "</name>\n" + "<id>org.inkscape.effect.bitmap.implode</id>\n" + "<param name=\"factor\" gui-text=\"" N_("Factor:") "\" type=\"float\" min=\"0\" max=\"100\">10</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Implode selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Implode()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/implode.h b/src/extension/internal/bitmap/implode.h new file mode 100644 index 0000000..d9c5adb --- /dev/null +++ b/src/extension/internal/bitmap/implode.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Implode : public ImageMagick +{ +private: + float _factor; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/level.cpp b/src/extension/internal/bitmap/level.cpp new file mode 100644 index 0000000..cf31744 --- /dev/null +++ b/src/extension/internal/bitmap/level.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "level.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Level::applyEffect(Magick::Image* image) { + Magick::Quantum black_point = Magick::Color::scaleDoubleToQuantum(_black_point / 100.0); + Magick::Quantum white_point = Magick::Color::scaleDoubleToQuantum(_white_point / 100.0); + image->level(black_point, white_point, _mid_point); +} + +void +Level::refreshParameters(Inkscape::Extension::Effect* module) { + _black_point = module->get_param_float("blackPoint"); + _white_point = module->get_param_float("whitePoint"); + _mid_point = module->get_param_float("midPoint"); +} + +#include "../clear-n_.h" + +void +Level::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Level") "</name>\n" + "<id>org.inkscape.effect.bitmap.level</id>\n" + "<param name=\"blackPoint\" gui-text=\"" N_("Black Point:") "\" type=\"float\" min=\"0\" max=\"100\">0</param>\n" + "<param name=\"whitePoint\" gui-text=\"" N_("White Point:") "\" type=\"float\" min=\"0\" max=\"100\">100</param>\n" + "<param name=\"midPoint\" gui-text=\"" N_("Gamma Correction:") "\" type=\"float\" min=\"0\" max=\"10\">1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Level selected bitmap(s) by scaling values falling between the given ranges to the full color range") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Level()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/level.h b/src/extension/internal/bitmap/level.h new file mode 100644 index 0000000..a09f189 --- /dev/null +++ b/src/extension/internal/bitmap/level.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Level : public ImageMagick +{ +private: + double _black_point; + double _white_point; + double _mid_point; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/levelChannel.cpp b/src/extension/internal/bitmap/levelChannel.cpp new file mode 100644 index 0000000..7280fb1 --- /dev/null +++ b/src/extension/internal/bitmap/levelChannel.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "levelChannel.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +LevelChannel::applyEffect(Magick::Image* image) { + Magick::ChannelType channel = Magick::UndefinedChannel; + if (!strcmp(_channelName, "Red Channel")) channel = Magick::RedChannel; + else if (!strcmp(_channelName, "Green Channel")) channel = Magick::GreenChannel; + else if (!strcmp(_channelName, "Blue Channel")) channel = Magick::BlueChannel; + else if (!strcmp(_channelName, "Cyan Channel")) channel = Magick::CyanChannel; + else if (!strcmp(_channelName, "Magenta Channel")) channel = Magick::MagentaChannel; + else if (!strcmp(_channelName, "Yellow Channel")) channel = Magick::YellowChannel; + else if (!strcmp(_channelName, "Black Channel")) channel = Magick::BlackChannel; + else if (!strcmp(_channelName, "Opacity Channel")) channel = Magick::OpacityChannel; + else if (!strcmp(_channelName, "Matte Channel")) channel = Magick::MatteChannel; + Magick::Quantum black_point = Magick::Color::scaleDoubleToQuantum(_black_point / 100.0); + Magick::Quantum white_point = Magick::Color::scaleDoubleToQuantum(_white_point / 100.0); + image->levelChannel(channel, black_point, white_point, _mid_point); +} + +void +LevelChannel::refreshParameters(Inkscape::Extension::Effect* module) { + _channelName = module->get_param_optiongroup("channel"); + _black_point = module->get_param_float("blackPoint"); + _white_point = module->get_param_float("whitePoint"); + _mid_point = module->get_param_float("midPoint"); +} + +#include "../clear-n_.h" + +void +LevelChannel::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Level (with Channel)") "</name>\n" + "<id>org.inkscape.effect.bitmap.levelChannel</id>\n" + "<param name=\"channel\" gui-text=\"" N_("Channel:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='Red Channel'>" N_("Red Channel") "</option>\n" + "<option value='Green Channel'>" N_("Green Channel") "</option>\n" + "<option value='Blue Channel'>" N_("Blue Channel") "</option>\n" + "<option value='Cyan Channel'>" N_("Cyan Channel") "</option>\n" + "<option value='Magenta Channel'>" N_("Magenta Channel") "</option>\n" + "<option value='Yellow Channel'>" N_("Yellow Channel") "</option>\n" + "<option value='Black Channel'>" N_("Black Channel") "</option>\n" + "<option value='Opacity Channel'>" N_("Opacity Channel") "</option>\n" + "<option value='Matte Channel'>" N_("Matte Channel") "</option>\n" + "</param>\n" + "<param name=\"blackPoint\" gui-text=\"" N_("Black Point:") "\" type=\"float\" min=\"0.0\" max=\"100.0\">0.0</param>\n" + "<param name=\"whitePoint\" gui-text=\"" N_("White Point:") "\" type=\"float\" min=\"0.0\" max=\"100.0\">100.0</param>\n" + "<param name=\"midPoint\" gui-text=\"" N_("Gamma Correction:") "\" type=\"float\" min=\"0.0\" max=\"10.0\">1.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Level the specified channel of selected bitmap(s) by scaling values falling between the given ranges to the full color range") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new LevelChannel()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/levelChannel.h b/src/extension/internal/bitmap/levelChannel.h new file mode 100644 index 0000000..d1cc82a --- /dev/null +++ b/src/extension/internal/bitmap/levelChannel.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class LevelChannel : public ImageMagick +{ +private: + double _black_point; + double _white_point; + double _mid_point; + const gchar * _channelName; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/medianFilter.cpp b/src/extension/internal/bitmap/medianFilter.cpp new file mode 100644 index 0000000..80385ac --- /dev/null +++ b/src/extension/internal/bitmap/medianFilter.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "medianFilter.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +MedianFilter::applyEffect(Magick::Image* image) { + image->medianFilter(_radius); +} + +void +MedianFilter::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); +} + +#include "../clear-n_.h" + +void +MedianFilter::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Median") "</name>\n" + "<id>org.inkscape.effect.bitmap.medianFilter</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0\" max=\"100\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Replace each pixel component with the median color in a circular neighborhood") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new MedianFilter()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/medianFilter.h b/src/extension/internal/bitmap/medianFilter.h new file mode 100644 index 0000000..214ed2b --- /dev/null +++ b/src/extension/internal/bitmap/medianFilter.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class MedianFilter : public ImageMagick +{ +private: + double _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/modulate.cpp b/src/extension/internal/bitmap/modulate.cpp new file mode 100644 index 0000000..a570da4 --- /dev/null +++ b/src/extension/internal/bitmap/modulate.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "modulate.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Modulate::applyEffect(Magick::Image* image) { + double hue = (_hue * 200 / 360.0) + 100; + image->modulate(_brightness, _saturation, hue); +} + +void +Modulate::refreshParameters(Inkscape::Extension::Effect* module) { + _brightness = module->get_param_float("brightness"); + _saturation = module->get_param_float("saturation"); + _hue = module->get_param_float("hue"); +} + +#include "../clear-n_.h" + +void +Modulate::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("HSB Adjust") "</name>\n" + "<id>org.inkscape.effect.bitmap.modulate</id>\n" + "<param name=\"hue\" gui-text=\"" N_("Hue:") "\" type=\"float\" min=\"-360\" max=\"360\">0</param>\n" + "<param name=\"saturation\" gui-text=\"" N_("Saturation:") "\" type=\"float\" min=\"0\" max=\"200\">100</param>\n" + "<param name=\"brightness\" gui-text=\"" N_("Brightness:") "\" type=\"float\" min=\"0\" max=\"200\">100</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Adjust the amount of hue, saturation, and brightness in selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Modulate()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/modulate.h b/src/extension/internal/bitmap/modulate.h new file mode 100644 index 0000000..327d3c4 --- /dev/null +++ b/src/extension/internal/bitmap/modulate.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Modulate : public ImageMagick +{ +private: + double _brightness; + double _saturation; + double _hue; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/negate.cpp b/src/extension/internal/bitmap/negate.cpp new file mode 100644 index 0000000..6470556 --- /dev/null +++ b/src/extension/internal/bitmap/negate.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "negate.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Negate::applyEffect(Magick::Image* image) { + image->negate(); +} + +void +Negate::refreshParameters(Inkscape::Extension::Effect* /*module*/) { +} + +#include "../clear-n_.h" + +void +Negate::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Negate") "</name>\n" + "<id>org.inkscape.effect.bitmap.negate</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Negate (take inverse) selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Negate()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/negate.h b/src/extension/internal/bitmap/negate.h new file mode 100644 index 0000000..4cde402 --- /dev/null +++ b/src/extension/internal/bitmap/negate.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Negate : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/normalize.cpp b/src/extension/internal/bitmap/normalize.cpp new file mode 100644 index 0000000..96cfe13 --- /dev/null +++ b/src/extension/internal/bitmap/normalize.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "normalize.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Normalize::applyEffect(Magick::Image* image) { + image->normalize(); +} + +void +Normalize::refreshParameters(Inkscape::Extension::Effect* /*module*/) { +} + +#include "../clear-n_.h" + +void +Normalize::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Normalize") "</name>\n" + "<id>org.inkscape.effect.bitmap.normalize</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Normalize selected bitmap(s), expanding color range to the full possible range of color") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Normalize()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/normalize.h b/src/extension/internal/bitmap/normalize.h new file mode 100644 index 0000000..2d4a9c2 --- /dev/null +++ b/src/extension/internal/bitmap/normalize.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Normalize : public ImageMagick +{ +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/oilPaint.cpp b/src/extension/internal/bitmap/oilPaint.cpp new file mode 100644 index 0000000..ee17964 --- /dev/null +++ b/src/extension/internal/bitmap/oilPaint.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "oilPaint.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +OilPaint::applyEffect(Magick::Image* image) { + image->oilPaint(_radius); +} + +void +OilPaint::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_int("radius"); +} + +#include "../clear-n_.h" + +void +OilPaint::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Oil Paint") "</name>\n" + "<id>org.inkscape.effect.bitmap.oilPaint</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"int\" min=\"0\" max=\"50\">3</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Stylize selected bitmap(s) so that they appear to be painted with oils") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new OilPaint()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/oilPaint.h b/src/extension/internal/bitmap/oilPaint.h new file mode 100644 index 0000000..58b94b2 --- /dev/null +++ b/src/extension/internal/bitmap/oilPaint.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class OilPaint : public ImageMagick +{ +private: + int _radius; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/opacity.cpp b/src/extension/internal/bitmap/opacity.cpp new file mode 100644 index 0000000..024dcb9 --- /dev/null +++ b/src/extension/internal/bitmap/opacity.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "opacity.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Opacity::applyEffect(Magick::Image* image) { + Magick::Quantum opacity = Magick::Color::scaleDoubleToQuantum((100 - _opacity) / 100.0); + image->opacity(opacity); +} + +void +Opacity::refreshParameters(Inkscape::Extension::Effect* module) { + _opacity = module->get_param_float("opacity"); +} + +#include "../clear-n_.h" + +void +Opacity::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Opacity") "</name>\n" + "<id>org.inkscape.effect.bitmap.opacity</id>\n" + "<param name=\"opacity\" gui-text=\"" N_("Opacity:") "\" type=\"float\" min=\"0.0\" max=\"100.0\">80.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Modify opacity channel(s) of selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Opacity()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/opacity.h b/src/extension/internal/bitmap/opacity.h new file mode 100644 index 0000000..e8cb548 --- /dev/null +++ b/src/extension/internal/bitmap/opacity.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Opacity : public ImageMagick +{ +private: + double _opacity; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/raise.cpp b/src/extension/internal/bitmap/raise.cpp new file mode 100644 index 0000000..e209f2d --- /dev/null +++ b/src/extension/internal/bitmap/raise.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "raise.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Raise::applyEffect(Magick::Image* image) { + Magick::Geometry geometry(_width, _height, 0, 0); + image->raise(geometry, _raisedFlag); +} + +void +Raise::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); + _raisedFlag = module->get_param_bool("raisedFlag"); +} + +#include "../clear-n_.h" + +void +Raise::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Raise") "</name>\n" + "<id>org.inkscape.effect.bitmap.raise</id>\n" + "<param name=\"width\" gui-text=\"" N_("Width:") "\" type=\"int\" min=\"0\" max=\"800\">6</param>\n" + "<param name=\"height\" gui-text=\"" N_("Height:") "\" type=\"int\" min=\"0\" max=\"800\">6</param>\n" + "<param name=\"raisedFlag\" gui-text=\"" N_("Raised") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Alter lightness the edges of selected bitmap(s) to create a raised appearance") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Raise()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/raise.h b/src/extension/internal/bitmap/raise.h new file mode 100644 index 0000000..90023fb --- /dev/null +++ b/src/extension/internal/bitmap/raise.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Raise : public ImageMagick +{ +private: + int _width; + int _height; + bool _raisedFlag; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/reduceNoise.cpp b/src/extension/internal/bitmap/reduceNoise.cpp new file mode 100644 index 0000000..e1e5e83 --- /dev/null +++ b/src/extension/internal/bitmap/reduceNoise.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "reduceNoise.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +ReduceNoise::applyEffect(Magick::Image* image) { + if (_order > -1) + image->reduceNoise(_order); + else + image->reduceNoise(); +} + +void +ReduceNoise::refreshParameters(Inkscape::Extension::Effect* module) { + _order = module->get_param_int("order"); +} + +#include "../clear-n_.h" + +void +ReduceNoise::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Reduce Noise") "</name>\n" + "<id>org.inkscape.effect.bitmap.reduceNoise</id>\n" + "<param name=\"order\" gui-text=\"" N_("Order:") "\" type=\"int\" min=\"-1\" max=\"100\">-1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Reduce noise in selected bitmap(s) using a noise peak elimination filter") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ReduceNoise()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/reduceNoise.h b/src/extension/internal/bitmap/reduceNoise.h new file mode 100644 index 0000000..3c9d2d6 --- /dev/null +++ b/src/extension/internal/bitmap/reduceNoise.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class ReduceNoise : public ImageMagick +{ +private: + int _order; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sample.cpp b/src/extension/internal/bitmap/sample.cpp new file mode 100644 index 0000000..47b0147 --- /dev/null +++ b/src/extension/internal/bitmap/sample.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "sample.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Sample::applyEffect(Magick::Image* image) { + Magick::Geometry geometry(_width, _height, 0, 0); + image->sample(geometry); +} + +void +Sample::refreshParameters(Inkscape::Extension::Effect* module) { + _width = module->get_param_int("width"); + _height = module->get_param_int("height"); +} + +#include "../clear-n_.h" + +void +Sample::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Resample") "</name>\n" + "<id>org.inkscape.effect.bitmap.sample</id>\n" + "<param name=\"width\" gui-text=\"" N_("Width:") "\" type=\"int\" min=\"0\" max=\"6400\">100</param>\n" + "<param name=\"height\" gui-text=\"" N_("Height:") "\" type=\"int\" min=\"0\" max=\"6400\">100</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Alter the resolution of selected image by resizing it to the given pixel size") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Sample()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sample.h b/src/extension/internal/bitmap/sample.h new file mode 100644 index 0000000..c93ab0a --- /dev/null +++ b/src/extension/internal/bitmap/sample.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Sample : public ImageMagick +{ +private: + int _width; + int _height; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/shade.cpp b/src/extension/internal/bitmap/shade.cpp new file mode 100644 index 0000000..4a4f907 --- /dev/null +++ b/src/extension/internal/bitmap/shade.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "shade.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Shade::applyEffect(Magick::Image* image) { + image->shade(_azimuth, _elevation, !_colorShading); + // I don't know why, but I have to invert colorShading here +} + +void +Shade::refreshParameters(Inkscape::Extension::Effect* module) { + _azimuth = module->get_param_float("azimuth"); + _elevation = module->get_param_float("elevation"); + _colorShading = module->get_param_bool("colorShading"); +} + +#include "../clear-n_.h" + +void +Shade::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Shade") "</name>\n" + "<id>org.inkscape.effect.bitmap.shade</id>\n" + "<param name=\"azimuth\" gui-text=\"" N_("Azimuth:") "\" type=\"float\" min=\"-180\" max=\"180\">30</param>\n" + "<param name=\"elevation\" gui-text=\"" N_("Elevation:") "\" type=\"float\" min=\"-180\" max=\"180\">30</param>\n" + "<param name=\"colorShading\" gui-text=\"" N_("Colored Shading") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Shade selected bitmap(s) simulating distant light source") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Shade()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/shade.h b/src/extension/internal/bitmap/shade.h new file mode 100644 index 0000000..928dc87 --- /dev/null +++ b/src/extension/internal/bitmap/shade.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Shade : public ImageMagick +{ +private: + double _azimuth; + double _elevation; + bool _colorShading; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sharpen.cpp b/src/extension/internal/bitmap/sharpen.cpp new file mode 100644 index 0000000..40e159c --- /dev/null +++ b/src/extension/internal/bitmap/sharpen.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "sharpen.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Sharpen::applyEffect(Magick::Image* image) { + image->sharpen(_radius, _sigma); +} + +void +Sharpen::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); +} + +#include "../clear-n_.h" + +void +Sharpen::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Sharpen") "</name>\n" + "<id>org.inkscape.effect.bitmap.sharpen</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0\" max=\"50\">1.0</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"0\" max=\"10\">0.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Sharpen selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Sharpen()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/sharpen.h b/src/extension/internal/bitmap/sharpen.h new file mode 100644 index 0000000..6bbae73 --- /dev/null +++ b/src/extension/internal/bitmap/sharpen.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Sharpen : public ImageMagick +{ +private: + double _radius; + double _sigma; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/solarize.cpp b/src/extension/internal/bitmap/solarize.cpp new file mode 100644 index 0000000..41952d8 --- /dev/null +++ b/src/extension/internal/bitmap/solarize.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "solarize.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Solarize::applyEffect(Magick::Image* image) { + // Image Magick Quantum depth = 16 + // 655.35 = (2^16 - 1) / 100 + image->solarize(_factor * 655.35); +} + +void +Solarize::refreshParameters(Inkscape::Extension::Effect* module) { + _factor = module->get_param_float("factor"); +} + +#include "../clear-n_.h" + +void +Solarize::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Solarize") "</name>\n" + "<id>org.inkscape.effect.bitmap.solarize</id>\n" + "<param name=\"factor\" gui-text=\"" N_("Factor:") "\" type=\"float\" min=\"0\" max=\"100\">50</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Solarize selected bitmap(s), like overexposing photographic film") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Solarize()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/solarize.h b/src/extension/internal/bitmap/solarize.h new file mode 100644 index 0000000..0e6bbea --- /dev/null +++ b/src/extension/internal/bitmap/solarize.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Solarize : public ImageMagick +{ +private: + double _factor; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/spread.cpp b/src/extension/internal/bitmap/spread.cpp new file mode 100644 index 0000000..cb7aa5d --- /dev/null +++ b/src/extension/internal/bitmap/spread.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "spread.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Spread::applyEffect(Magick::Image* image) { + image->spread(_amount); +} + +void +Spread::refreshParameters(Inkscape::Extension::Effect* module) { + _amount = module->get_param_int("amount"); +} + +#include "../clear-n_.h" + +void +Spread::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Dither") "</name>\n" + "<id>org.inkscape.effect.bitmap.spread</id>\n" + "<param name=\"amount\" gui-text=\"" N_("Amount:") "\" type=\"int\" min=\"0\" max=\"100\">3</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Randomly scatter pixels in selected bitmap(s), within the given radius of the original position") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Spread()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/spread.h b/src/extension/internal/bitmap/spread.h new file mode 100644 index 0000000..ad77544 --- /dev/null +++ b/src/extension/internal/bitmap/spread.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Spread : public ImageMagick +{ +private: + int _amount; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/swirl.cpp b/src/extension/internal/bitmap/swirl.cpp new file mode 100644 index 0000000..50dc75d --- /dev/null +++ b/src/extension/internal/bitmap/swirl.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "swirl.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Swirl::applyEffect(Magick::Image* image) { + image->swirl(_degrees); +} + +void +Swirl::refreshParameters(Inkscape::Extension::Effect* module) { + _degrees = module->get_param_int("degrees"); +} + +#include "../clear-n_.h" + +void +Swirl::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Swirl") "</name>\n" + "<id>org.inkscape.effect.bitmap.swirl</id>\n" + "<param name=\"degrees\" gui-text=\"" N_("Degrees:") "\" type=\"int\" min=\"-360\" max=\"360\">30</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Swirl selected bitmap(s) around center point") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Swirl()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/swirl.h b/src/extension/internal/bitmap/swirl.h new file mode 100644 index 0000000..d95c0c9 --- /dev/null +++ b/src/extension/internal/bitmap/swirl.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Swirl : public ImageMagick +{ +private: + int _degrees; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/threshold.cpp b/src/extension/internal/bitmap/threshold.cpp new file mode 100644 index 0000000..235cf48 --- /dev/null +++ b/src/extension/internal/bitmap/threshold.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "threshold.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Threshold::applyEffect(Magick::Image* image) { + image->threshold(_threshold); +} + +void +Threshold::refreshParameters(Inkscape::Extension::Effect* module) { + _threshold = module->get_param_float("threshold"); +} + +#include "../clear-n_.h" + +void +Threshold::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + // TRANSLATORS: see http://docs.gimp.org/en/gimp-tool-threshold.html + "<name>" N_("Threshold") "</name>\n" + "<id>org.inkscape.effect.bitmap.threshold</id>\n" + "<param name=\"threshold\" gui-text=\"" N_("Threshold:") "\" type=\"float\" min=\"-100.0\" max=\"100.0\"></param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Threshold selected bitmap(s)") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Threshold()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/threshold.h b/src/extension/internal/bitmap/threshold.h new file mode 100644 index 0000000..93e15bc --- /dev/null +++ b/src/extension/internal/bitmap/threshold.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Threshold : public ImageMagick +{ +private: + double _threshold; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/unsharpmask.cpp b/src/extension/internal/bitmap/unsharpmask.cpp new file mode 100644 index 0000000..73940d7 --- /dev/null +++ b/src/extension/internal/bitmap/unsharpmask.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "unsharpmask.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Unsharpmask::applyEffect(Magick::Image* image) { + double amount = _amount / 100.0; + image->unsharpmask(_radius, _sigma, amount, _threshold); +} + +void +Unsharpmask::refreshParameters(Inkscape::Extension::Effect* module) { + _radius = module->get_param_float("radius"); + _sigma = module->get_param_float("sigma"); + _amount = module->get_param_float("amount"); + _threshold = module->get_param_float("threshold"); +} + +#include "../clear-n_.h" + +void +Unsharpmask::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Unsharp Mask") "</name>\n" + "<id>org.inkscape.effect.bitmap.unsharpmask</id>\n" + "<param name=\"radius\" gui-text=\"" N_("Radius:") "\" type=\"float\" min=\"0.0\" max=\"50.0\">5.0</param>\n" + "<param name=\"sigma\" gui-text=\"" N_("Sigma:") "\" type=\"float\" min=\"0.0\" max=\"50.0\">5.0</param>\n" + "<param name=\"amount\" gui-text=\"" N_("Amount:") "\" type=\"float\" min=\"0.0\" max=\"100.0\">50.0</param>\n" + "<param name=\"threshold\" gui-text=\"" N_("Threshold:") "\" type=\"float\" min=\"0.0\" max=\"50.0\">5.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Sharpen selected bitmap(s) using unsharp mask algorithms") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Unsharpmask()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/unsharpmask.h b/src/extension/internal/bitmap/unsharpmask.h new file mode 100644 index 0000000..2fb4679 --- /dev/null +++ b/src/extension/internal/bitmap/unsharpmask.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Unsharpmask : public ImageMagick +{ +private: + double _radius; + double _sigma; + double _amount; + double _threshold; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/wave.cpp b/src/extension/internal/bitmap/wave.cpp new file mode 100644 index 0000000..80ae768 --- /dev/null +++ b/src/extension/internal/bitmap/wave.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/effect.h" +#include "extension/system.h" + +#include "wave.h" +#include <Magick++.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +void +Wave::applyEffect(Magick::Image* image) { + image->wave(_amplitude, _wavelength); +} + +void +Wave::refreshParameters(Inkscape::Extension::Effect* module) { + _amplitude = module->get_param_float("amplitude"); + _wavelength = module->get_param_float("wavelength"); +} + +#include "../clear-n_.h" + +void +Wave::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Wave") "</name>\n" + "<id>org.inkscape.effect.bitmap.wave</id>\n" + "<param name=\"amplitude\" gui-text=\"" N_("Amplitude:") "\" type=\"float\" min=\"-720.0\" max=\"720.0\">25</param>\n" + "<param name=\"wavelength\" gui-text=\"" N_("Wavelength:") "\" type=\"float\" min=\"-720.0\" max=\"720.0\">150</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Raster") "\" />\n" + "</effects-menu>\n" + "<menu-tip>" N_("Alter selected bitmap(s) along sine wave") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Wave()); + // clang-format on +} + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bitmap/wave.h b/src/extension/internal/bitmap/wave.h new file mode 100644 index 0000000..65342ce --- /dev/null +++ b/src/extension/internal/bitmap/wave.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2007 Authors: + * Christopher Brown <audiere@gmail.com> + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "imagemagick.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Bitmap { + +class Wave : public ImageMagick +{ +private: + double _amplitude; + double _wavelength; +public: + void applyEffect(Magick::Image *image) override; + void refreshParameters(Inkscape::Extension::Effect *module) override; + static void init(); +}; + +}; /* namespace Bitmap */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/bluredge.cpp b/src/extension/internal/bluredge.cpp new file mode 100644 index 0000000..ed7f15c --- /dev/null +++ b/src/extension/internal/bluredge.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file bluredge.cpp + + A plug-in to add an effect to blur the edges of an object. +*/ +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "bluredge.h" + +#include <vector> +#include "desktop.h" +#include "document.h" +#include "selection.h" + +#include "preferences.h" +#include "path-chemistry.h" +#include "object/sp-item.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "path/path-offset.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +BlurEdge::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Blur Edge, I'm loading!" << std::endl; + return TRUE; +} + +/** + \brief This actually blurs the edge. + \param module The effect that was called (unused) + \param desktop What should be edited. +*/ +void +BlurEdge::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *view, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + auto desktop = dynamic_cast<SPDesktop *>(view); + if (!desktop) { + std::cerr << "BlurEdge::effect: view is not desktop!" << std::endl; + return; + } + Inkscape::Selection * selection = desktop->selection; + + double width = module->get_param_float("blur-width"); + int steps = module->get_param_int("num-steps"); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double old_offset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + // TODO need to properly refcount the items, at least + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + selection->clear(); + + for(auto spitem : items) { + std::vector<Inkscape::XML::Node *> new_items(steps); + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node * new_group = xml_doc->createElement("svg:g"); + spitem->getRepr()->parent()->appendChild(new_group); + + double orig_opacity = sp_repr_css_double_property(sp_repr_css_attr(spitem->getRepr(), "style"), "opacity", 1.0); + char opacity_string[64]; + g_ascii_formatd(opacity_string, sizeof(opacity_string), "%f", + orig_opacity / (steps)); + + for (int i = 0; i < steps; i++) { + double offset = (width / (float)(steps - 1) * (float)i) - (width / 2.0); + + new_items[i] = spitem->getRepr()->duplicate(xml_doc); + + SPCSSAttr * css = sp_repr_css_attr(new_items[i], "style"); + sp_repr_css_set_property(css, "opacity", opacity_string); + sp_repr_css_change(new_items[i], css, "style"); + + new_group->appendChild(new_items[i]); + selection->add(new_items[i]); + selection->toCurves(); + selection->removeLPESRecursive(true); + selection->unlinkRecursive(true); + + if (offset < 0.0) { + /* Doing an inset here folks */ + offset *= -1.0; + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", offset, "px"); + sp_selected_path_inset(desktop); + } else if (offset > 0.0) { + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", offset, "px"); + sp_selected_path_offset(desktop); + } + + selection->clear(); + } + + Inkscape::GC::release(new_group); + } + + prefs->setDoubleUnit("/options/defaultoffsetwidth/value", old_offset, "px"); + + selection->clear(); + selection->add(items.begin(), items.end()); + + return; +} + +Gtk::Widget * +BlurEdge::prefs_effect(Inkscape::Extension::Effect * module, Inkscape::UI::View::View * /*view*/, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + return module->autogui(nullptr, nullptr, changeSignal); +} + +#include "clear-n_.h" + +void +BlurEdge::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Inset/Outset Halo") "</name>\n" + "<id>org.inkscape.effect.bluredge</id>\n" + "<param name=\"blur-width\" gui-text=\"" N_("Width:") "\" gui-description=\"" N_("Width in px of the halo") "\" type=\"float\" min=\"1.0\" max=\"50.0\">1.0</param>\n" + "<param name=\"num-steps\" gui-text=\"" N_("Number of steps:") "\" gui-description=\"" N_("Number of inset/outset copies of the object to make") "\" type=\"int\" min=\"5\" max=\"100\">11</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Generate from Path") "\" />\n" + "</effects-menu>\n" + "</effect>\n" + "</inkscape-extension>\n" , new BlurEdge()); + // clang-format on + return; +} + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/bluredge.h b/src/extension/internal/bluredge.h new file mode 100644 index 0000000..b74b753 --- /dev/null +++ b/src/extension/internal/bluredge.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class BlurEdge : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect * module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void init (); +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/cairo-ps-out.cpp b/src/extension/internal/cairo-ps-out.cpp new file mode 100644 index 0000000..abf76a9 --- /dev/null +++ b/src/extension/internal/cairo-ps-out.cpp @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PS. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Ulf Erikson <ulferikson@users.sf.net> + * Adib Taraben <theAdib@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairo.h> +#ifdef CAIRO_HAS_PS_SURFACE + +#include "cairo-ps.h" +#include "cairo-ps-out.h" +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "latex-text-renderer.h" +#include "path-chemistry.h" +#include <print.h> +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/output.h" +#include "display/drawing.h" + +#include "display/curve.h" + +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "io/sys.h" +#include "document.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool CairoPsOutput::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_PS)) { + return FALSE; + } else { + return TRUE; + } +} + +bool CairoEpsOutput::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_EPS)) { + return FALSE; + } else { + return TRUE; + } +} + +static bool +ps_print_document_to_file(SPDocument *doc, gchar const *filename, unsigned int level, bool texttopath, bool omittext, + bool filtertobitmap, int resolution, const gchar * const exportId, bool exportDrawing, bool exportCanvas, double bleedmargin_px, bool eps = false) +{ + if (texttopath) { + assert(!omittext); + // Cairo's text-to-path method has numerical precision and font matching + // issues (https://gitlab.com/inkscape/inkscape/-/issues/1979). + // We get better results by using Inkscape's Object-to-Path method. + Inkscape::convert_text_to_curves(doc); + } + + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = TRUE; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = SP_ITEM(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) + return false; + + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + root->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY); + + /* Create renderer and context */ + CairoRenderer *renderer = new CairoRenderer(); + CairoRenderContext *ctx = renderer->createContext(); + ctx->setPSLevel(level); + ctx->setEPS(eps); + ctx->setTextToPath(texttopath); + ctx->setOmitText(omittext); + ctx->setFilterToBitmap(filtertobitmap); + ctx->setBitmapResolution(resolution); + + bool ret = ctx->setPsTarget(filename); + if(ret) { + /* Render document */ + ret = renderer->setupDocument(ctx, doc, pageBoundingBox, bleedmargin_px, base); + if (ret) { + renderer->renderItem(ctx, root); + ret = ctx->finish(); + } + } + + root->invoke_hide(dkey); + + renderer->destroyContext(ctx); + delete renderer; + + return ret; +} + + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .ps) +*/ +void +CairoPsOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_PS); + if (ext == nullptr) + return; + + int level = CAIRO_PS_LEVEL_2; + try { + const gchar *new_level = mod->get_param_optiongroup("PSlevel"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PS3", new_level) == 0)) { + level = CAIRO_PS_LEVEL_3; + } + } catch(...) {} + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } catch(...) {} + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter <textToLaTeX> might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } catch(...) {} + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } catch(...) {} + + bool new_areaPage = true; + try { + new_areaPage = (strcmp(mod->get_param_optiongroup("area"), "page") == 0); + } catch(...) {} + + bool new_areaDrawing = !new_areaPage; + + double bleedmargin_px = 0.; + try { + bleedmargin_px = mod->get_param_float("bleed"); + } catch(...) {} + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } catch(...) {} + + // Create PS + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = ps_print_document_to_file(doc, final_name, level, new_textToPath, + new_textToLaTeX, new_blurToBitmap, + new_bitmapResolution, new_exportId, + new_areaDrawing, new_areaPage, + bleedmargin_px); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_areaDrawing, new_areaPage, 0., false); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .ps) +*/ +void +CairoEpsOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get(SP_MODULE_KEY_PRINT_CAIRO_EPS); + if (ext == nullptr) + return; + + int level = CAIRO_PS_LEVEL_2; + try { + const gchar *new_level = mod->get_param_optiongroup("PSlevel"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PS3", new_level) == 0)) { + level = CAIRO_PS_LEVEL_3; + } + } catch(...) {} + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } catch(...) {} + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter <textToLaTeX> might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } catch(...) {} + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } catch(...) {} + + bool new_areaPage = true; + try { + new_areaPage = (strcmp(mod->get_param_optiongroup("area"), "page") == 0); + } catch(...) {} + + bool new_areaDrawing = !new_areaPage; + + double bleedmargin_px = 0.; + try { + bleedmargin_px = mod->get_param_float("bleed"); + } catch(...) {} + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } catch(...) {} + + // Create EPS + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = ps_print_document_to_file(doc, final_name, level, new_textToPath, + new_textToLaTeX, new_blurToBitmap, + new_bitmapResolution, new_exportId, + new_areaDrawing, new_areaPage, + bleedmargin_px, true); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_areaDrawing, new_areaPage, 0., false); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + + +bool +CairoPsOutput::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +bool +CairoEpsOutput::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo PS out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoPsOutput::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("PostScript") "</name>\n" + "<id>" SP_MODULE_KEY_PRINT_CAIRO_PS "</id>\n" + "<param name=\"PSlevel\" gui-text=\"" N_("Restrict to PS level:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='PS3'>" N_("PostScript level 3") "</option>\n" + "<option value='PS2'>" N_("PostScript level 2") "</option>\n" + "</param>\n" + "<param name=\"textToPath\" gui-text=\"" N_("Text output options:") "\" type=\"optiongroup\" appearance=\"radio\">\n" + "<option value=\"embed\">" N_("Embed fonts") "</option>\n" + "<option value=\"paths\">" N_("Convert text to paths") "</option>\n" + "<option value=\"LaTeX\">" N_("Omit text in PDF and create LaTeX file") "</option>\n" + "</param>\n" + "<param name=\"blurToBitmap\" gui-text=\"" N_("Rasterize filter effects") "\" type=\"bool\">true</param>\n" + "<param name=\"resolution\" gui-text=\"" N_("Resolution for rasterization (dpi):") "\" type=\"int\" min=\"1\" max=\"10000\">96</param>\n" + "<param name=\"area\" gui-text=\"" N_("Output page size") "\" type=\"optiongroup\" appearance=\"radio\" >\n" + "<option value=\"page\">" N_("Use document's page size") "</option>" + "<option value=\"drawing\">" N_("Use exported object's size") "</option>" + "</param>" + "<param name=\"bleed\" gui-text=\"" N_("Bleed/margin (mm):") "\" type=\"float\" min=\"-10000\" max=\"10000\">0</param>\n" + "<param name=\"exportId\" gui-text=\"" N_("Limit export to the object with ID:") "\" type=\"string\"></param>\n" + "<output>\n" + "<extension>.ps</extension>\n" + "<mimetype>image/x-postscript</mimetype>\n" + "<filetypename>" N_("PostScript (*.ps)") "</filetypename>\n" + "<filetypetooltip>" N_("PostScript File") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new CairoPsOutput()); + // clang-format on + + return; +} + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo EPS out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoEpsOutput::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Encapsulated PostScript") "</name>\n" + "<id>" SP_MODULE_KEY_PRINT_CAIRO_EPS "</id>\n" + "<param name=\"PSlevel\" gui-text=\"" N_("Restrict to PS level:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='PS3'>" N_("PostScript level 3") "</option>\n" + "<option value='PS2'>" N_("PostScript level 2") "</option>\n" + "</param>\n" + "<param name=\"textToPath\" gui-text=\"" N_("Text output options:") "\" type=\"optiongroup\" appearance=\"radio\">\n" + "<option value=\"embed\">" N_("Embed fonts") "</option>\n" + "<option value=\"paths\">" N_("Convert text to paths") "</option>\n" + "<option value=\"LaTeX\">" N_("Omit text in PDF and create LaTeX file") "</option>\n" + "</param>\n" + "<param name=\"blurToBitmap\" gui-text=\"" N_("Rasterize filter effects") "\" type=\"bool\">true</param>\n" + "<param name=\"resolution\" gui-text=\"" N_("Resolution for rasterization (dpi):") "\" type=\"int\" min=\"1\" max=\"10000\">96</param>\n" + "<param name=\"area\" gui-text=\"" N_("Output page size") "\" type=\"optiongroup\" appearance=\"radio\" >\n" + "<option value=\"page\">" N_("Use document's page size") "</option>" + "<option value=\"drawing\">" N_("Use exported object's size") "</option>" + "</param>" + "<param name=\"bleed\" gui-text=\"" N_("Bleed/margin (mm)") "\" type=\"float\" min=\"-10000\" max=\"10000\">0</param>\n" + "<param name=\"exportId\" gui-text=\"" N_("Limit export to the object with ID:") "\" type=\"string\"></param>\n" + "<output>\n" + "<extension>.eps</extension>\n" + "<mimetype>image/x-e-postscript</mimetype>\n" + "<filetypename>" N_("Encapsulated PostScript (*.eps)") "</filetypename>\n" + "<filetypetooltip>" N_("Encapsulated PostScript File") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new CairoEpsOutput()); + // clang-format on + + return; +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* HAVE_CAIRO_PDF */ diff --git a/src/extension/internal/cairo-ps-out.h b/src/extension/internal/cairo-ps-out.h new file mode 100644 index 0000000..ee58179 --- /dev/null +++ b/src/extension/internal/cairo-ps-out.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the print output to write out a file. This + * then makes 'save as...' PS. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Ulf Erikson <ulferikson@users.sf.net> + * Adib Taraben <theAdib@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_CAIRO_PS_OUT_H +#define EXTENSION_INTERNAL_CAIRO_PS_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoPsOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + static void init(); + bool textToPath(Inkscape::Extension::Print *ext) override; + +}; + +class CairoEpsOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *uri) override; + static void init(); + bool textToPath(Inkscape::Extension::Print *ext) override; + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_PS_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-render-context.cpp b/src/extension/internal/cairo-render-context.cpp new file mode 100644 index 0000000..063938b --- /dev/null +++ b/src/extension/internal/cairo-render-context.cpp @@ -0,0 +1,2040 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering with Cairo. + */ +/* + * Author: + * Miklos Erdelyi <erdelyim@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_BACKEND +#define PANGO_ENABLE_BACKEND +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + +#include "cairo-render-context.h" + +#include <csignal> +#include <cerrno> +#include <2geom/pathvector.h> + +#include <glib.h> +#include <glibmm/i18n.h> + +#include "display/drawing.h" +#include "display/curve.h" +#include "display/cairo-utils.h" + +#include "object/sp-item.h" +#include "object/sp-item-group.h" +#include "object/sp-hatch.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-mesh-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-mask.h" +#include "object/sp-clippath.h" + +#include "util/units.h" +#ifdef _WIN32 +#include "libnrtype/FontFactory.h" // USE_PANGO_WIN32 +#endif + +#include "cairo-renderer.h" +#include "extension/system.h" + +#include "io/sys.h" + +#include "svg/stringstream.h" + +#include <cairo.h> + +// include support for only the compiled-in surface types +#ifdef CAIRO_HAS_PDF_SURFACE +#include <cairo-pdf.h> +#endif +#ifdef CAIRO_HAS_PS_SURFACE +#include <cairo-ps.h> +#endif + + +#ifdef CAIRO_HAS_FT_FONT +#include <cairo-ft.h> +#endif +#ifdef CAIRO_HAS_WIN32_FONT +#include <pango/pangowin32.h> +#include <cairo-win32.h> +#endif + +#include <pango/pangofc-fontmap.h> + +//#define TRACE(_args) g_printf _args +//#define TRACE(_args) g_message _args +#define TRACE(_args) +//#define TEST(_args) _args +#define TEST(_args) + +// FIXME: expose these from sp-clippath/mask.cpp +/*struct SPClipPathView { + SPClipPathView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +}; + +struct SPMaskView { + SPMaskView *next; + unsigned int key; + Inkscape::DrawingItem *arenaitem; + Geom::OptRect bbox; +};*/ + +namespace Inkscape { +namespace Extension { +namespace Internal { + +static cairo_status_t _write_callback(void *closure, const unsigned char *data, unsigned int length); + +CairoRenderContext::CairoRenderContext(CairoRenderer *parent) : + _dpi(72), + _pdf_level(1), + _ps_level(1), + _eps(false), + _is_texttopath(FALSE), + _is_omittext(FALSE), + _is_filtertobitmap(FALSE), + _is_show_page(false), + _bitmapresolution(72), + _stream(nullptr), + _is_valid(FALSE), + _vector_based_target(FALSE), + _cr(nullptr), // Cairo context + _surface(nullptr), + _target(CAIRO_SURFACE_TYPE_IMAGE), + _target_format(CAIRO_FORMAT_ARGB32), + _layout(nullptr), + _state(nullptr), + _renderer(parent), + _render_mode(RENDER_MODE_NORMAL), + _clip_mode(CLIP_MODE_MASK), + _omittext_state(EMPTY) +{ +} + +CairoRenderContext::~CairoRenderContext() +{ + for (std::map<gpointer, cairo_font_face_t *>::const_iterator iter = font_table.begin(); iter != font_table.end(); ++iter) + font_data_free(iter->second); + + if (_cr) cairo_destroy(_cr); + if (_surface) cairo_surface_destroy(_surface); + if (_layout) g_object_unref(_layout); +} +void CairoRenderContext::font_data_free(gpointer data) +{ + cairo_font_face_t *font_face = (cairo_font_face_t *)data; + if (font_face) { + cairo_font_face_destroy(font_face); + } +} + +CairoRenderer* CairoRenderContext::getRenderer() const +{ + return _renderer; +} + +CairoRenderState* CairoRenderContext::getCurrentState() const +{ + return _state; +} + +CairoRenderState* CairoRenderContext::getParentState() const +{ + // if this is the root node just return it + if (_state_stack.size() == 1) { + return _state; + } else { + return _state_stack[_state_stack.size()-2]; + } +} + +void CairoRenderContext::setStateForStyle(SPStyle const *style) +{ + // only opacity & overflow is stored for now + _state->opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + _state->has_overflow = (style->overflow.set && style->overflow.value != SP_CSS_OVERFLOW_VISIBLE); + _state->has_filtereffect = (style->filter.set != 0) ? TRUE : FALSE; + + if (style->fill.isPaintserver() || style->stroke.isPaintserver()) + _state->merge_opacity = FALSE; + + // disable rendering of opacity if there's a stroke on the fill + if (_state->merge_opacity + && !style->fill.isNone() + && !style->stroke.isNone()) + _state->merge_opacity = FALSE; +} + +/** + * \brief Creates a new render context which will be compatible with the given context's Cairo surface + * + * \param width width of the surface to be created + * \param height height of the surface to be created + */ +CairoRenderContext* +CairoRenderContext::cloneMe(double width, double height) const +{ + g_assert( _is_valid ); + g_assert( width > 0.0 && height > 0.0 ); + + CairoRenderContext *new_context = _renderer->createContext(); + cairo_surface_t *surface = cairo_surface_create_similar(cairo_get_target(_cr), CAIRO_CONTENT_COLOR_ALPHA, + (int)ceil(width), (int)ceil(height)); + new_context->_cr = cairo_create(surface); + new_context->_surface = surface; + new_context->_width = width; + new_context->_height = height; + new_context->_is_valid = TRUE; + + return new_context; +} + +CairoRenderContext* CairoRenderContext::cloneMe() const +{ + g_assert( _is_valid ); + + return cloneMe(_width, _height); +} + +bool CairoRenderContext::setImageTarget(cairo_format_t format) +{ + // format cannot be set on an already initialized surface + if (_is_valid) + return false; + + switch (format) { + case CAIRO_FORMAT_ARGB32: + case CAIRO_FORMAT_RGB24: + case CAIRO_FORMAT_A8: + case CAIRO_FORMAT_A1: + _target_format = format; + _target = CAIRO_SURFACE_TYPE_IMAGE; + return true; + break; + default: + break; + } + + return false; +} + +bool CairoRenderContext::setPdfTarget(gchar const *utf8_fn) +{ +#ifndef CAIRO_HAS_PDF_SURFACE + return false; +#else + _target = CAIRO_SURFACE_TYPE_PDF; + _vector_based_target = TRUE; +#endif + + FILE *osf = nullptr; + FILE *osp = nullptr; + + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError *error = nullptr; + gchar *local_fn = g_filename_from_utf8(utf8_fn, + -1, &bytesRead, &bytesWritten, &error); + gchar const *fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + if (*fn == '|') { + fn += 1; + while (isspace(*fn)) fn += 1; +#ifndef _WIN32 + osp = popen(fn, "w"); +#else + osp = _popen(fn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osp; + } else if (*fn == '>') { + fn += 1; + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osf; + } else { + /* put cwd stuff in here */ + gchar *qn = ( *fn + ? g_strdup_printf("lpr -P %s", fn) /* FIXME: quote fn */ + : g_strdup("lpr") ); +#ifndef _WIN32 + osp = popen(qn, "w"); +#else + osp = _popen(qn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + qn, strerror(errno)); + return false; + } + g_free(qn); + _stream = osp; + } + } + + g_free(local_fn); + + if (_stream) { + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + } + + return true; +} + +bool CairoRenderContext::setPsTarget(gchar const *utf8_fn) +{ +#ifndef CAIRO_HAS_PS_SURFACE + return false; +#else + _target = CAIRO_SURFACE_TYPE_PS; + _vector_based_target = TRUE; +#endif + + FILE *osf = nullptr; + FILE *osp = nullptr; + + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError *error = nullptr; + gchar *local_fn = g_filename_from_utf8(utf8_fn, + -1, &bytesRead, &bytesWritten, &error); + gchar const *fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + if (*fn == '|') { + fn += 1; + while (isspace(*fn)) fn += 1; +#ifndef _WIN32 + osp = popen(fn, "w"); +#else + osp = _popen(fn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osp; + } else if (*fn == '>') { + fn += 1; + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", + fn, strerror(errno)); + return false; + } + _stream = osf; + } else { + /* put cwd stuff in here */ + gchar *qn = ( *fn + ? g_strdup_printf("lpr -P %s", fn) /* FIXME: quote fn */ + : g_strdup("lpr") ); +#ifndef _WIN32 + osp = popen(qn, "w"); +#else + osp = _popen(qn, "w"); +#endif + if (!osp) { + fprintf(stderr, "inkscape: popen(%s): %s\n", + qn, strerror(errno)); + return false; + } + g_free(qn); + _stream = osp; + } + } + + g_free(local_fn); + + if (_stream) { + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + } + + return true; +} + +void CairoRenderContext::setPSLevel(unsigned int level) +{ + _ps_level = level; +} + +void CairoRenderContext::setEPS(bool eps) +{ + _eps = eps; +} + +unsigned int CairoRenderContext::getPSLevel() +{ + return _ps_level; +} + +void CairoRenderContext::setPDFLevel(unsigned int level) +{ + _pdf_level = level; +} + +void CairoRenderContext::setTextToPath(bool texttopath) +{ + _is_texttopath = texttopath; +} + +void CairoRenderContext::setOmitText(bool omittext) +{ + _is_omittext = omittext; +} + +bool CairoRenderContext::getOmitText() +{ + return _is_omittext; +} + +void CairoRenderContext::setFilterToBitmap(bool filtertobitmap) +{ + _is_filtertobitmap = filtertobitmap; +} + +bool CairoRenderContext::getFilterToBitmap() +{ + return _is_filtertobitmap; +} + +void CairoRenderContext::setBitmapResolution(int resolution) +{ + _bitmapresolution = resolution; +} + +int CairoRenderContext::getBitmapResolution() +{ + return _bitmapresolution; +} + +cairo_surface_t* +CairoRenderContext::getSurface() +{ + g_assert( _is_valid ); + + return _surface; +} + +bool +CairoRenderContext::saveAsPng(const char *file_name) +{ + cairo_status_t status = cairo_surface_write_to_png(_surface, file_name); + if (status) + return false; + else + return true; +} + +void +CairoRenderContext::setRenderMode(CairoRenderMode mode) +{ + switch (mode) { + case RENDER_MODE_NORMAL: + case RENDER_MODE_CLIP: + _render_mode = mode; + break; + default: + _render_mode = RENDER_MODE_NORMAL; + break; + } +} + +CairoRenderContext::CairoRenderMode +CairoRenderContext::getRenderMode() const +{ + return _render_mode; +} + +void +CairoRenderContext::setClipMode(CairoClipMode mode) +{ + switch (mode) { + case CLIP_MODE_PATH: // Clip is rendered as a path for vector output + case CLIP_MODE_MASK: // Clip is rendered as a bitmap for raster output. + _clip_mode = mode; + break; + default: + _clip_mode = CLIP_MODE_PATH; + break; + } +} + +CairoRenderContext::CairoClipMode +CairoRenderContext::getClipMode() const +{ + return _clip_mode; +} + +CairoRenderState* CairoRenderContext::_createState() +{ + CairoRenderState *state = static_cast<CairoRenderState*>(g_try_malloc(sizeof(CairoRenderState))); + g_assert( state != nullptr ); + + state->has_filtereffect = FALSE; + state->merge_opacity = TRUE; + state->opacity = 1.0; + state->need_layer = FALSE; + state->has_overflow = FALSE; + state->parent_has_userspace = FALSE; + state->clip_path = nullptr; + state->mask = nullptr; + + return state; +} + +void CairoRenderContext::pushLayer() +{ + g_assert( _is_valid ); + + TRACE(("--pushLayer\n")); + cairo_push_group(_cr); + + // clear buffer + if (!_vector_based_target) { + cairo_save(_cr); + cairo_set_operator(_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(_cr); + cairo_restore(_cr); + } +} + +void +CairoRenderContext::popLayer(cairo_operator_t composite) +{ + g_assert( _is_valid ); + + float opacity = _state->opacity; + TRACE(("--popLayer w/ opacity %f\n", opacity)); + + /* + At this point, the Cairo source is ready. A Cairo mask must be created if required. + Care must be taken of transformatons as Cairo, like PS and PDF, treats clip paths and + masks independently of the objects they effect while in SVG the clip paths and masks + are defined relative to the objects they are attached to. + Notes: + 1. An SVG object may have both a clip path and a mask! + 2. An SVG clip path can be composed of an object with a clip path. This is not handled properly. + 3. An SVG clipped or masked object may be first drawn off the page and then translated onto + the page (document). This is also not handled properly. + 4. The code converts all SVG masks to bitmaps. This shouldn't be necessary. + 5. Cairo expects a mask to use only the alpha channel. SVG masks combine the RGB luminance with + alpha. This is handled here by doing a pixel by pixel conversion. + */ + + SPClipPath *clip_path = _state->clip_path; + SPMask *mask = _state->mask; + if (clip_path || mask) { + + CairoRenderContext *clip_ctx = nullptr; + cairo_surface_t *clip_mask = nullptr; + + // Apply any clip path first + if (clip_path) { + TRACE((" Applying clip\n")); + if (_render_mode == RENDER_MODE_CLIP) + mask = nullptr; // disable mask when performing nested clipping + + if (_vector_based_target) { + setClipMode(CLIP_MODE_PATH); // Vector + if (!mask) { + cairo_pop_group_to_source(_cr); + _renderer->applyClipPath(this, clip_path); // Uses cairo_clip() + if (opacity == 1.0) + cairo_paint(_cr); + else + cairo_paint_with_alpha(_cr, opacity); + + } else { + // the clipPath will be applied before masking + } + } else { + + // setup a new rendering context + clip_ctx = _renderer->createContext(); + clip_ctx->setImageTarget(CAIRO_FORMAT_A8); + clip_ctx->setClipMode(CLIP_MODE_MASK); // Raster + // This code ties the clipping to the document coordinates. It doesn't allow + // for a clipped object initially drawn off the page and then translated onto + // the page. + if (!clip_ctx->setupSurface(_width, _height)) { + TRACE(("clip: setupSurface failed\n")); + _renderer->destroyContext(clip_ctx); + return; + } + + // clear buffer + cairo_save(clip_ctx->_cr); + cairo_set_operator(clip_ctx->_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(clip_ctx->_cr); + cairo_restore(clip_ctx->_cr); + + // If a mask won't be applied set opacity too. (The clip is represented by a solid Cairo mask.) + if (!mask) + cairo_set_source_rgba(clip_ctx->_cr, 1.0, 1.0, 1.0, opacity); + else + cairo_set_source_rgba(clip_ctx->_cr, 1.0, 1.0, 1.0, 1.0); + + // copy over the correct CTM + // It must be stored in item_transform of current state after pushState. + Geom::Affine item_transform; + if (_state->parent_has_userspace) + item_transform = getParentState()->transform * _state->item_transform; + else + item_transform = _state->item_transform; + + // apply the clip path + clip_ctx->pushState(); + clip_ctx->getCurrentState()->item_transform = item_transform; + _renderer->applyClipPath(clip_ctx, clip_path); + clip_ctx->popState(); + + clip_mask = clip_ctx->getSurface(); + TEST(clip_ctx->saveAsPng("clip_mask.png")); + + if (!mask) { + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + cairo_mask_surface(_cr, clip_mask, 0, 0); + _renderer->destroyContext(clip_ctx); + } + } + } + + // Apply any mask second + if (mask) { + TRACE((" Applying mask\n")); + // create rendering context for mask + CairoRenderContext *mask_ctx = _renderer->createContext(); + + // Fix Me: This is a kludge. PDF and PS output is set to 72 dpi but the + // Cairo surface is expecting the mask to be 96 dpi. + float surface_width = _width; + float surface_height = _height; + if( _vector_based_target ) { + surface_width *= 4.0/3.0; + surface_height *= 4.0/3.0; + } + if (!mask_ctx->setupSurface( surface_width, surface_height )) { + TRACE(("mask: setupSurface failed\n")); + _renderer->destroyContext(mask_ctx); + return; + } + TRACE(("mask surface: %f x %f at %i dpi\n", surface_width, surface_height, _dpi )); + + // Mask should start black, but it is created white. + cairo_set_source_rgba(mask_ctx->_cr, 0.0, 0.0, 0.0, 1.0); + cairo_rectangle(mask_ctx->_cr, 0, 0, surface_width, surface_height); + cairo_fill(mask_ctx->_cr); + + // set rendering mode to normal + setRenderMode(RENDER_MODE_NORMAL); + + // copy the correct CTM to mask context + /* + if (_state->parent_has_userspace) + mask_ctx->setTransform(getParentState()->transform); + else + mask_ctx->setTransform(_state->transform); + */ + // This is probably not correct... but it seems to do the trick. + mask_ctx->setTransform(_state->item_transform); + + // render mask contents to mask_ctx + _renderer->applyMask(mask_ctx, mask); + + TEST(mask_ctx->saveAsPng("mask.png")); + + // composite with clip mask + if (clip_path && _clip_mode == CLIP_MODE_MASK) { + cairo_mask_surface(mask_ctx->_cr, clip_mask, 0, 0); + _renderer->destroyContext(clip_ctx); + } + + cairo_surface_t *mask_image = mask_ctx->getSurface(); + int width = cairo_image_surface_get_width(mask_image); + int height = cairo_image_surface_get_height(mask_image); + int stride = cairo_image_surface_get_stride(mask_image); + unsigned char *pixels = cairo_image_surface_get_data(mask_image); + + // In SVG, the rgb channels as well as the alpha channel is used in masking. + // In Cairo, only the alpha channel is used thus requiring this conversion. + // SVG specifies that RGB be converted to alpha using luminance-to-alpha. + // Notes: This calculation assumes linear RGB values. VERIFY COLOR SPACE! + // The incoming pixel values already include alpha, fill-opacity, etc., + // however, opacity must still be applied. + TRACE(("premul w/ %f\n", opacity)); + const float coeff_r = 0.2125 / 255.0; + const float coeff_g = 0.7154 / 255.0; + const float coeff_b = 0.0721 / 255.0; + for (int row = 0 ; row < height; row++) { + unsigned char *row_data = pixels + (row * stride); + for (int i = 0 ; i < width; i++) { + guint32 *pixel = reinterpret_cast<guint32 *>(row_data) + i; + float lum_alpha = (((*pixel & 0x00ff0000) >> 16) * coeff_r + + ((*pixel & 0x0000ff00) >> 8) * coeff_g + + ((*pixel & 0x000000ff) ) * coeff_b ); + // lum_alpha can be slightly greater than 1 due to rounding errors... + // but this should be OK since it doesn't matter what the lower + // six hexadecimal numbers of *pixel are. + *pixel = (guint32)(0xff000000 * lum_alpha * opacity); + } + } + + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + if (_clip_mode == CLIP_MODE_PATH) { + // we have to do the clipping after cairo_pop_group_to_source + _renderer->applyClipPath(this, clip_path); + } + // apply the mask onto the layer + cairo_mask_surface(_cr, mask_image, 0, 0); + _renderer->destroyContext(mask_ctx); + } + } else { + // No clip path or mask + cairo_pop_group_to_source(_cr); + if (composite != CAIRO_OPERATOR_CLEAR){ + cairo_set_operator(_cr, composite); + } + if (opacity == 1.0) + cairo_paint(_cr); + else + cairo_paint_with_alpha(_cr, opacity); + } +} +void CairoRenderContext::tagBegin(const char* l){ +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + char* link = g_strdup_printf("uri='%s'", l); + cairo_tag_begin(_cr, CAIRO_TAG_LINK, link); + g_free(link); +#endif +} + +void CairoRenderContext::tagEnd(){ +#if CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + cairo_tag_end(_cr, CAIRO_TAG_LINK); +#endif +} + + +void +CairoRenderContext::addClipPath(Geom::PathVector const &pv, SPIEnum<SPWindRule> const *fill_rule) +{ + g_assert( _is_valid ); + + // here it should be checked whether the current clip winding changed + // so we could switch back to masked clipping + if (fill_rule->value == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + addPathVector(pv); +} + +void +CairoRenderContext::addClippingRect(double x, double y, double width, double height) +{ + g_assert( _is_valid ); + + cairo_rectangle(_cr, x, y, width, height); + cairo_clip(_cr); +} + +bool +CairoRenderContext::setupSurface(double width, double height) +{ + // Is the surface already set up? + if (_is_valid) + return true; + + if (_vector_based_target && _stream == nullptr) + return false; + + _width = width; + _height = height; + + cairo_surface_t *surface = nullptr; + cairo_matrix_t ctm; + cairo_matrix_init_identity (&ctm); + switch (_target) { + case CAIRO_SURFACE_TYPE_IMAGE: + surface = cairo_image_surface_create(_target_format, (int)ceil(width), (int)ceil(height)); + break; +#ifdef CAIRO_HAS_PDF_SURFACE + case CAIRO_SURFACE_TYPE_PDF: + surface = cairo_pdf_surface_create_for_stream(Inkscape::Extension::Internal::_write_callback, _stream, width, height); + cairo_pdf_surface_restrict_to_version(surface, (cairo_pdf_version_t)_pdf_level); + break; +#endif +#ifdef CAIRO_HAS_PS_SURFACE + case CAIRO_SURFACE_TYPE_PS: + surface = cairo_ps_surface_create_for_stream(Inkscape::Extension::Internal::_write_callback, _stream, width, height); + if(CAIRO_STATUS_SUCCESS != cairo_surface_status(surface)) { + return FALSE; + } + cairo_ps_surface_restrict_to_level(surface, (cairo_ps_level_t)_ps_level); + cairo_ps_surface_set_eps(surface, (cairo_bool_t) _eps); + break; +#endif + default: + return false; + break; + } + + _setSurfaceMetadata(surface); + + return _finishSurfaceSetup (surface, &ctm); +} + +bool +CairoRenderContext::setSurfaceTarget(cairo_surface_t *surface, bool is_vector, cairo_matrix_t *ctm) +{ + if (_is_valid || !surface) + return false; + + _vector_based_target = is_vector; + bool ret = _finishSurfaceSetup (surface, ctm); + if (ret) + cairo_surface_reference (surface); + return ret; +} + +bool +CairoRenderContext::_finishSurfaceSetup(cairo_surface_t *surface, cairo_matrix_t *ctm) +{ + if(surface == nullptr) { + return false; + } + if(CAIRO_STATUS_SUCCESS != cairo_surface_status(surface)) { + return false; + } + + _cr = cairo_create(surface); + if(CAIRO_STATUS_SUCCESS != cairo_status(_cr)) { + return false; + } + if (ctm) + cairo_set_matrix(_cr, ctm); + _surface = surface; + + if (_vector_based_target) { + cairo_scale(_cr, Inkscape::Util::Quantity::convert(1, "px", "pt"), Inkscape::Util::Quantity::convert(1, "px", "pt")); + } else if (cairo_surface_get_content(_surface) != CAIRO_CONTENT_ALPHA) { + // set background color on non-alpha surfaces + // TODO: bgcolor should be derived from SPDocument (see IconImpl) + cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0); + cairo_rectangle(_cr, 0, 0, _width, _height); + cairo_fill(_cr); + } + + _is_valid = TRUE; + + return true; +} + +void +CairoRenderContext::_setSurfaceMetadata(cairo_surface_t *surface) +{ + switch (_target) { +#if defined CAIRO_HAS_PDF_SURFACE && CAIRO_VERSION >= CAIRO_VERSION_ENCODE(1, 15, 4) + case CAIRO_SURFACE_TYPE_PDF: + if (!_metadata.title.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_TITLE, _metadata.title.c_str()); + } + if (!_metadata.author.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_AUTHOR, _metadata.author.c_str()); + } + if (!_metadata.subject.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_SUBJECT, _metadata.subject.c_str()); + } + if (!_metadata.keywords.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_KEYWORDS, _metadata.keywords.c_str()); + } + if (!_metadata.creator.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_CREATOR, _metadata.creator.c_str()); + } + if (!_metadata.cdate.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_CREATE_DATE, _metadata.cdate.c_str()); + } + if (!_metadata.mdate.empty()) { + cairo_pdf_surface_set_metadata(surface, CAIRO_PDF_METADATA_MOD_DATE, _metadata.mdate.c_str()); + } + break; +#endif +#if defined CAIRO_HAS_PS_SURFACE + case CAIRO_SURFACE_TYPE_PS: + if (!_metadata.title.empty()) { + cairo_ps_surface_dsc_comment(surface, (Glib::ustring("%%Title: ") + _metadata.title).c_str()); + } + if (!_metadata.copyright.empty()) { + cairo_ps_surface_dsc_comment(surface, (Glib::ustring("%%Copyright: ") + _metadata.copyright).c_str()); + } + break; +#endif + default: + g_warning("unsupported target %d\n", _target); + } +} + +/** + * Each page that's made should call finishPage to complete it. + */ +bool +CairoRenderContext::finishPage() +{ + g_assert(_is_valid); + if (!_vector_based_target) + return false; + + // Protect against finish() showing one too many pages. + if (!_is_show_page) { + cairo_show_page(_cr); + _is_show_page = true; + } + + auto status = cairo_status(_cr); + if (status != CAIRO_STATUS_SUCCESS) { + g_critical("error while rendering page: %s", cairo_status_to_string(status)); + return false; + } + return true; +} + +/** + * When writing multiple pages, resize the next page. + */ +bool +CairoRenderContext::nextPage(double width, double height, char const *label) +{ + g_assert(_is_valid); + if (!_vector_based_target) + return false; + + _width = width; + _height = height; + _is_show_page = false; + cairo_pdf_surface_set_size(_surface, width, height); + + if (label) { + cairo_pdf_surface_set_page_label(_surface, label); + } + + auto status = cairo_surface_status(_surface); + if (status != CAIRO_STATUS_SUCCESS) { + g_critical("error while sizing page: %s", cairo_status_to_string(status)); + return false; + } + return true; +} + + +bool +CairoRenderContext::finish(bool finish_surface) +{ + g_assert( _is_valid ); + + if (_vector_based_target && !_is_show_page && finish_surface) + cairo_show_page(_cr); + + cairo_status_t status = cairo_status(_cr); + if (status != CAIRO_STATUS_SUCCESS) + g_critical("error while rendering output: %s", cairo_status_to_string(status)); + + cairo_destroy(_cr); + _cr = nullptr; + + if (finish_surface) + cairo_surface_finish(_surface); + status = cairo_surface_status(_surface); + cairo_surface_destroy(_surface); + _surface = nullptr; + + if (_layout) + g_object_unref(_layout); + + _is_valid = FALSE; + + if (_vector_based_target && _stream) { + /* Flush stream to be sure. */ + (void) fflush(_stream); + + fclose(_stream); + _stream = nullptr; + } + + if (status == CAIRO_STATUS_SUCCESS) + return true; + else + return false; +} + +void +CairoRenderContext::transform(Geom::Affine const &transform) +{ + g_assert( _is_valid ); + + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, transform); + cairo_transform(_cr, &matrix); + + // store new CTM + _state->transform = getTransform(); +} + +void +CairoRenderContext::setTransform(Geom::Affine const &transform) +{ + g_assert( _is_valid ); + + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, transform); + cairo_set_matrix(_cr, &matrix); + _state->transform = transform; +} + +Geom::Affine CairoRenderContext::getTransform() const +{ + g_assert( _is_valid ); + + cairo_matrix_t ctm; + cairo_get_matrix(_cr, &ctm); + Geom::Affine ret; + ret[0] = ctm.xx; + ret[1] = ctm.yx; + ret[2] = ctm.xy; + ret[3] = ctm.yy; + ret[4] = ctm.x0; + ret[5] = ctm.y0; + return ret; +} + +Geom::Affine CairoRenderContext::getParentTransform() const +{ + g_assert( _is_valid ); + + CairoRenderState *parent_state = getParentState(); + return parent_state->transform; +} + +void CairoRenderContext::pushState() +{ + g_assert( _is_valid ); + + cairo_save(_cr); + + CairoRenderState *new_state = _createState(); + // copy current state's transform + new_state->transform = _state->transform; + _state_stack.push_back(new_state); + _state = new_state; +} + +void CairoRenderContext::popState() +{ + g_assert( _is_valid ); + + cairo_restore(_cr); + + g_free(_state_stack.back()); + _state_stack.pop_back(); + + g_assert( !_state_stack.empty()); + _state = _state_stack.back(); +} + +static bool pattern_hasItemChildren(SPPattern *pat) +{ + for (auto& child: pat->children) { + if (SP_IS_ITEM (&child)) { + return true; + } + } + return false; +} + +cairo_pattern_t* +CairoRenderContext::_createPatternPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox) +{ + g_assert( SP_IS_PATTERN(paintserver) ); + + SPPattern *pat = SP_PATTERN (paintserver); + + Geom::Affine ps2user, pcs2dev; + ps2user = Geom::identity(); + pcs2dev = Geom::identity(); + + double x = pat->x(); + double y = pat->y(); + double width = pat->width(); + double height = pat->height(); + double bbox_width_scaler; + double bbox_height_scaler; + + TRACE(("%f x %f pattern\n", width, height)); + + if (pbox && pat->patternUnits() == SPPattern::UNITS_OBJECTBOUNDINGBOX) { + bbox_width_scaler = pbox->width(); + bbox_height_scaler = pbox->height(); + ps2user[4] = x * bbox_width_scaler + pbox->left(); + ps2user[5] = y * bbox_height_scaler + pbox->top(); + } else { + bbox_width_scaler = 1.0; + bbox_height_scaler = 1.0; + ps2user[4] = x; + ps2user[5] = y; + } + + // apply pattern transformation + Geom::Affine pattern_transform(pat->getTransform()); + ps2user *= pattern_transform; + Geom::Point ori (ps2user[4], ps2user[5]); + + // create pattern contents coordinate system + if (pat->viewBox_set) { + Geom::Rect view_box = *pat->viewbox(); + + double x, y, w, h; + x = 0; + y = 0; + w = width * bbox_width_scaler; + h = height * bbox_height_scaler; + + //calculatePreserveAspectRatio(pat->aspect_align, pat->aspect_clip, view_width, view_height, &x, &y, &w, &h); + pcs2dev[0] = w / view_box.width(); + pcs2dev[3] = h / view_box.height(); + pcs2dev[4] = x - view_box.left() * pcs2dev[0]; + pcs2dev[5] = y - view_box.top() * pcs2dev[3]; + } else if (pbox && pat->patternContentUnits() == SPPattern::UNITS_OBJECTBOUNDINGBOX) { + pcs2dev[0] = pbox->width(); + pcs2dev[3] = pbox->height(); + } + + // Calculate the size of the surface which has to be created +#define SUBPIX_SCALE 100 + // Cairo requires an integer pattern surface width/height. + // Subtract 0.5 to prevent small rounding errors from increasing pattern size by one pixel. + // Multiply by SUBPIX_SCALE to allow for less than a pixel precision + double surface_width = MAX(ceil(SUBPIX_SCALE * bbox_width_scaler * width - 0.5), 1); + double surface_height = MAX(ceil(SUBPIX_SCALE * bbox_height_scaler * height - 0.5), 1); + TRACE(("pattern surface size: %f x %f\n", surface_width, surface_height)); + // create new rendering context + CairoRenderContext *pattern_ctx = cloneMe(surface_width, surface_height); + + // adjust the size of the painted pattern to fit exactly the created surface + // this has to be done because of the rounding to obtain an integer pattern surface width/height + double scale_width = surface_width / (bbox_width_scaler * width); + double scale_height = surface_height / (bbox_height_scaler * height); + if (scale_width != 1.0 || scale_height != 1.0 || _vector_based_target) { + TRACE(("needed to scale with %f %f\n", scale_width, scale_height)); + pcs2dev *= Geom::Scale(SUBPIX_SCALE,SUBPIX_SCALE); + ps2user *= Geom::Scale(1.0/SUBPIX_SCALE,1.0/SUBPIX_SCALE); + } + + // despite scaling up/down by subpixel scaler, the origin point of the pattern must be the same + ps2user[4] = ori[Geom::X]; + ps2user[5] = ori[Geom::Y]; + + pattern_ctx->setTransform(pcs2dev); + pattern_ctx->pushState(); + + // create drawing and group + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + + // show items and render them + for (SPPattern *pat_i = pat; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i && pattern_hasItemChildren(pat_i)) { // find the first one with item children + for (auto& child: pat_i->children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_show(drawing, dkey, SP_ITEM_REFERENCE_FLAGS); + _renderer->renderItem(pattern_ctx, SP_ITEM(&child)); + } + } + break; // do not go further up the chain if children are found + } + } + + pattern_ctx->popState(); + + // setup a cairo_pattern_t + cairo_surface_t *pattern_surface = pattern_ctx->getSurface(); + TEST(pattern_ctx->saveAsPng("pattern.png")); + cairo_pattern_t *result = cairo_pattern_create_for_surface(pattern_surface); + cairo_pattern_set_extend(result, CAIRO_EXTEND_REPEAT); + + // set pattern transformation + cairo_matrix_t pattern_matrix; + _initCairoMatrix(&pattern_matrix, ps2user); + cairo_matrix_invert(&pattern_matrix); + cairo_pattern_set_matrix(result, &pattern_matrix); + + delete pattern_ctx; + + // hide all items + for (SPPattern *pat_i = pat; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (pat_i && pattern_hasItemChildren(pat_i)) { // find the first one with item children + for (auto& child: pat_i->children) { + if (SP_IS_ITEM(&child)) { + SP_ITEM(&child)->invoke_hide(dkey); + } + } + break; // do not go further up the chain if children are found + } + } + + return result; +} + +cairo_pattern_t* +CairoRenderContext::_createHatchPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox) { + SPHatch const *hatch = dynamic_cast<SPHatch const *>(paintserver); + g_assert( hatch ); + + g_assert(hatch->pitch() > 0); + + // create drawing and group + Inkscape::Drawing drawing; + unsigned dkey = SPItem::display_key_new(1); + + // TODO need to refactor 'evil' referenced code for const correctness. + SPHatch *evil = const_cast<SPHatch *>(hatch); + evil->show(drawing, dkey, pbox); + + SPHatch::RenderInfo render_info = hatch->calculateRenderInfo(dkey); + Geom::Rect tile_rect = render_info.tile_rect; + + // Cairo requires an integer pattern surface width/height. + // Subtract 0.5 to prevent small rounding errors from increasing pattern size by one pixel. + // Multiply by SUBPIX_SCALE to allow for less than a pixel precision + const int subpix_scale = 10; + double surface_width = MAX(ceil(subpix_scale * tile_rect.width() - 0.5), 1); + double surface_height = MAX(ceil(subpix_scale * tile_rect.height() - 0.5), 1); + Geom::Affine drawing_scale = Geom::Scale(surface_width / tile_rect.width(), surface_height / tile_rect.height()); + Geom::Affine drawing_transform = Geom::Translate(-tile_rect.min()) * drawing_scale; + + Geom::Affine child_transform = render_info.child_transform; + child_transform *= drawing_transform; + + //The rendering of hatch overflow is implemented by repeated drawing + //of hatch paths over one strip. Within each iteration paths are moved by pitch value. + //The movement progresses from right to left. This gives the same result + //as drawing whole strips in left-to-right order. + gdouble overflow_right_strip = 0.0; + int overflow_steps = 1; + Geom::Affine overflow_transform; + if (hatch->style->overflow.computed == SP_CSS_OVERFLOW_VISIBLE) { + Geom::Interval bounds = hatch->bounds(); + overflow_right_strip = floor(bounds.max() / hatch->pitch()) * hatch->pitch(); + overflow_steps = ceil((overflow_right_strip - bounds.min()) / hatch->pitch()) + 1; + overflow_transform = Geom::Translate(hatch->pitch(), 0.0); + } + + CairoRenderContext *pattern_ctx = cloneMe(surface_width, surface_height); + pattern_ctx->setTransform(child_transform); + pattern_ctx->transform(Geom::Translate(-overflow_right_strip, 0.0)); + pattern_ctx->pushState(); + + std::vector<SPHatchPath *> children(evil->hatchPaths()); + + for (int i = 0; i < overflow_steps; i++) { + for (auto path : children) { + _renderer->renderHatchPath(pattern_ctx, *path, dkey); + } + pattern_ctx->transform(overflow_transform); + } + + pattern_ctx->popState(); + + // setup a cairo_pattern_t + cairo_surface_t *pattern_surface = pattern_ctx->getSurface(); + TEST(pattern_ctx->saveAsPng("hatch.png")); + cairo_pattern_t *result = cairo_pattern_create_for_surface(pattern_surface); + cairo_pattern_set_extend(result, CAIRO_EXTEND_REPEAT); + + Geom::Affine pattern_transform; + pattern_transform = render_info.pattern_to_user_transform.inverse() * drawing_transform; + ink_cairo_pattern_set_matrix(result, pattern_transform); + + evil->hide(dkey); + + delete pattern_ctx; + return result; +} + +cairo_pattern_t* +CairoRenderContext::_createPatternForPaintServer(SPPaintServer const *const paintserver, + Geom::OptRect const &pbox, float alpha) +{ + cairo_pattern_t *pattern = nullptr; + bool apply_bbox2user = FALSE; + + auto const paintserver_mutable = const_cast<SPPaintServer *>(paintserver); + + if (auto lg = dynamic_cast<SPLinearGradient *>(paintserver_mutable)) { + + lg->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point p1 (lg->x1.computed, lg->y1.computed); + Geom::Point p2 (lg->x2.computed, lg->y2.computed); + if (pbox && lg->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) { + // convert to userspace + Geom::Affine bbox2user(pbox->width(), 0, 0, pbox->height(), pbox->left(), pbox->top()); + p1 *= bbox2user; + p2 *= bbox2user; + } + + // create linear gradient pattern + pattern = cairo_pattern_create_linear(p1[Geom::X], p1[Geom::Y], p2[Geom::X], p2[Geom::Y]); + + // add stops + for (gint i = 0; unsigned(i) < lg->vector.stops.size(); i++) { + float rgb[3]; + lg->vector.stops[i].color.get_rgb_floatv(rgb); + cairo_pattern_add_color_stop_rgba(pattern, lg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], lg->vector.stops[i].opacity * alpha); + } + } else if (auto rg = dynamic_cast<SPRadialGradient *>(paintserver_mutable)) { + + rg->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point c (rg->cx.computed, rg->cy.computed); + Geom::Point f (rg->fx.computed, rg->fy.computed); + double r = rg->r.computed; + double fr = rg->fr.computed; + if (pbox && rg->getUnits() == SP_GRADIENT_UNITS_OBJECTBOUNDINGBOX) + apply_bbox2user = true; + + // create radial gradient pattern + pattern = cairo_pattern_create_radial(f[Geom::X], f[Geom::Y], fr, c[Geom::X], c[Geom::Y], r); + + // add stops + for (gint i = 0; unsigned(i) < rg->vector.stops.size(); i++) { + float rgb[3]; + rg->vector.stops[i].color.get_rgb_floatv(rgb); + cairo_pattern_add_color_stop_rgba(pattern, rg->vector.stops[i].offset, rgb[0], rgb[1], rgb[2], rg->vector.stops[i].opacity * alpha); + } + } else if (auto mg = dynamic_cast<SPMeshGradient *>(paintserver_mutable)) { + pattern = mg->pattern_new(_cr, pbox, 1.0); + } else if (SP_IS_PATTERN (paintserver)) { + pattern = _createPatternPainter(paintserver, pbox); + } else if ( dynamic_cast<SPHatch const *>(paintserver) ) { + pattern = _createHatchPainter(paintserver, pbox); + } else { + return nullptr; + } + + if (pattern && SP_IS_GRADIENT(paintserver)) { + auto g = dynamic_cast<SPGradient *>(paintserver_mutable); + + // set extend type + SPGradientSpread spread = g->fetchSpread(); + switch (spread) { + case SP_GRADIENT_SPREAD_REPEAT: { + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT); + break; + } + case SP_GRADIENT_SPREAD_REFLECT: { // not supported by cairo-pdf yet + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REFLECT); + break; + } + case SP_GRADIENT_SPREAD_PAD: { // not supported by cairo-pdf yet + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD); + break; + } + default: { + cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE); + break; + } + } + + cairo_matrix_t pattern_matrix; + if (g->gradientTransform_set) { + // apply gradient transformation + cairo_matrix_init(&pattern_matrix, + g->gradientTransform[0], g->gradientTransform[1], + g->gradientTransform[2], g->gradientTransform[3], + g->gradientTransform[4], g->gradientTransform[5]); + } else { + cairo_matrix_init_identity (&pattern_matrix); + } + + if (apply_bbox2user) { + // convert to userspace + cairo_matrix_t bbox2user; + cairo_matrix_init (&bbox2user, pbox->width(), 0, 0, pbox->height(), pbox->left(), pbox->top()); + cairo_matrix_multiply (&pattern_matrix, &bbox2user, &pattern_matrix); + } + cairo_matrix_invert(&pattern_matrix); // because Cairo expects a userspace->patternspace matrix + cairo_pattern_set_matrix(pattern, &pattern_matrix); + } + + return pattern; +} + +void +CairoRenderContext::_setFillStyle(SPStyle const *const style, Geom::OptRect const &pbox) +{ + g_return_if_fail( !style->fill.set + || style->fill.isColor() + || style->fill.isPaintserver() ); + + float alpha = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (_state->merge_opacity) { + alpha *= _state->opacity; + TRACE(("merged op=%f\n", alpha)); + } + + SPPaintServer const *paint_server = style->getFillPaintServer(); + if (paint_server && paint_server->isValid()) { + + g_assert(SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style)) + || SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style)) + || dynamic_cast<SPHatch *>(SP_STYLE_FILL_SERVER(style))); + + cairo_pattern_t *pattern = _createPatternForPaintServer(paint_server, pbox, alpha); + if (pattern) { + cairo_set_source(_cr, pattern); + cairo_pattern_destroy(pattern); + } + } else if (style->fill.colorSet) { + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + + cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); + + } else { // unset fill is black + g_assert(!style->fill.set + || (paint_server && !paint_server->isValid())); + + cairo_set_source_rgba(_cr, 0, 0, 0, alpha); + } +} + +void +CairoRenderContext::_setStrokeStyle(SPStyle const *style, Geom::OptRect const &pbox) +{ + float alpha = SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + if (_state->merge_opacity) + alpha *= _state->opacity; + + if (style->stroke.isColor() || (style->stroke.isPaintserver() && !style->getStrokePaintServer()->isValid())) { + float rgb[3]; + style->stroke.value.color.get_rgb_floatv(rgb); + + cairo_set_source_rgba(_cr, rgb[0], rgb[1], rgb[2], alpha); + } else { + g_assert( style->stroke.isPaintserver() + || SP_IS_GRADIENT(SP_STYLE_STROKE_SERVER(style)) + || SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style)) + || dynamic_cast<SPHatch *>(SP_STYLE_STROKE_SERVER(style))); + + cairo_pattern_t *pattern = _createPatternForPaintServer(SP_STYLE_STROKE_SERVER(style), pbox, alpha); + + if (pattern) { + cairo_set_source(_cr, pattern); + cairo_pattern_destroy(pattern); + } + } + + if (!style->stroke_dasharray.values.empty()) + { + size_t ndashes = style->stroke_dasharray.values.size(); + double* dashes =(double*)malloc(ndashes*sizeof(double)); + for( unsigned i = 0; i < ndashes; ++i ) { + dashes[i] = style->stroke_dasharray.values[i].value; + } + cairo_set_dash(_cr, dashes, ndashes, style->stroke_dashoffset.value); + free(dashes); + } else { + cairo_set_dash(_cr, nullptr, 0, 0.0); // disable dashing + } + + // This allows hairlines to be drawn properly in PDF, PS, Win32-Print, etc. + // It requires the following pull request in Cairo: + // https://gitlab.freedesktop.org/cairo/cairo/merge_requests/21 + if (style->stroke_extensions.hairline) { + ink_cairo_set_hairline(_cr); + } else { + cairo_set_line_width(_cr, style->stroke_width.computed); + } + + // set line join type + cairo_line_join_t join = CAIRO_LINE_JOIN_MITER; + switch (style->stroke_linejoin.computed) { + case SP_STROKE_LINEJOIN_MITER: + join = CAIRO_LINE_JOIN_MITER; + break; + case SP_STROKE_LINEJOIN_ROUND: + join = CAIRO_LINE_JOIN_ROUND; + break; + case SP_STROKE_LINEJOIN_BEVEL: + join = CAIRO_LINE_JOIN_BEVEL; + break; + } + cairo_set_line_join(_cr, join); + + // set line cap type + cairo_line_cap_t cap = CAIRO_LINE_CAP_BUTT; + switch (style->stroke_linecap.computed) { + case SP_STROKE_LINECAP_BUTT: + cap = CAIRO_LINE_CAP_BUTT; + break; + case SP_STROKE_LINECAP_ROUND: + cap = CAIRO_LINE_CAP_ROUND; + break; + case SP_STROKE_LINECAP_SQUARE: + cap = CAIRO_LINE_CAP_SQUARE; + break; + } + cairo_set_line_cap(_cr, cap); + cairo_set_miter_limit(_cr, MAX(1, style->stroke_miterlimit.value)); +} + +void +CairoRenderContext::_prepareRenderGraphic() +{ + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_is_omittext && _target == CAIRO_SURFACE_TYPE_PDF && _render_mode != RENDER_MODE_CLIP) { + if (_omittext_state == NEW_PAGE_ON_GRAPHIC) { + // better set this immediately (not sure if masks applied during "popLayer" could call + // this function, too, triggering the same code again in error + _omittext_state = GRAPHIC_ON_TOP; + + // As we can not emit the page in the middle of a layer (aka group) - it will not be fully painted yet! - + // the following basically mirrors the calls in CairoRenderer::renderItem (but in reversed order) + // - first traverse all saved states in reversed order (i.e. from deepest nesting to the top) + // and apply clipping / masking to layers on the way (this is done in popLayer) + // - then emit the page using cairo_show_page() + // - finally restore the previous state with proper transforms and appropriate layers again + // + // TODO: While this appears to be an ugly hack it seems to work + // Somebody with a more intimate understanding of cairo and the renderer implementation might + // be able to implement this in a cleaner way, though. + int stack_size = _state_stack.size(); + for (int i = stack_size-1; i > 0; i--) { + if (_state_stack[i]->need_layer) + popLayer(); + cairo_restore(_cr); + _state = _state_stack[i-1]; + } + + cairo_show_page(_cr); + + for (int i = 1; i < stack_size; i++) { + cairo_save(_cr); + _state = _state_stack[i]; + if (_state->need_layer) + pushLayer(); + setTransform(_state->transform); + } + } + _omittext_state = GRAPHIC_ON_TOP; + } +} + +void +CairoRenderContext::_prepareRenderText() +{ + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_is_omittext && _target == CAIRO_SURFACE_TYPE_PDF) { + if (_omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + } +} + +/* We need CairoPaintOrder as markers are rendered in a separate step and may be rendered + * in between fill and stroke. + */ +bool +CairoRenderContext::renderPathVector(Geom::PathVector const & pathv, SPStyle const *style, Geom::OptRect const &pbox, CairoPaintOrder order) +{ + g_assert( _is_valid ); + + _prepareRenderGraphic(); + + if (_render_mode == RENDER_MODE_CLIP) { + if (_clip_mode == CLIP_MODE_PATH) { + addClipPath(pathv, &style->fill_rule); + } else { + setPathVector(pathv); + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + cairo_fill(_cr); + TEST(cairo_surface_write_to_png (_surface, "pathmask.png")); + } + return true; + } + + bool no_fill = style->fill.isNone() || style->fill_opacity.value == 0 || + order == STROKE_ONLY; + bool no_stroke = style->stroke.isNone() || (!style->stroke_extensions.hairline && style->stroke_width.computed < 1e-9) || + style->stroke_opacity.value == 0 || order == FILL_ONLY; + + if (no_fill && no_stroke) + return true; + + bool need_layer = ( !_state->merge_opacity && !_state->need_layer && + ( _state->opacity != 1.0 || _state->clip_path != nullptr || _state->mask != nullptr ) ); + bool blend = false; + if (style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) { + need_layer = true; + blend = true; + } + if (!need_layer) + cairo_save(_cr); + else + pushLayer(); + + if (!no_fill) { + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + } + + setPathVector(pathv); + + if (!no_fill && (order == STROKE_OVER_FILL || order == FILL_ONLY)) { + _setFillStyle(style, pbox); + + if (no_stroke) + cairo_fill(_cr); + else + cairo_fill_preserve(_cr); + } + + if (!no_stroke) { + _setStrokeStyle(style, pbox); + + if (no_fill || order == STROKE_OVER_FILL) + cairo_stroke(_cr); + else + cairo_stroke_preserve(_cr); + } + + if (!no_fill && order == FILL_OVER_STROKE) { + _setFillStyle(style, pbox); + + cairo_fill(_cr); + } + + if (need_layer) { + if (blend) { + popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } else { + popLayer(); + } + } else { + cairo_restore(_cr); + } + + return true; +} + +bool CairoRenderContext::renderImage(Inkscape::Pixbuf *pb, + Geom::Affine const &image_transform, SPStyle const *style) +{ + g_assert( _is_valid ); + + if (_render_mode == RENDER_MODE_CLIP) { + return true; + } + + _prepareRenderGraphic(); + + int w = pb->width(); + int h = pb->height(); + + // TODO: reenable merge_opacity if useful + + cairo_surface_t *image_surface = pb->getSurfaceRaw(); + if (cairo_surface_status(image_surface)) { + TRACE(("Image surface creation failed:\n%s\n", cairo_status_to_string(cairo_surface_status(image_surface)))); + return false; + } + + cairo_save(_cr); + + // scaling by width & height is not needed because it will be done by Cairo + transform(image_transform); + + cairo_set_source_surface(_cr, image_surface, 0.0, 0.0); + + // set clip region so that the pattern will not be repeated (bug in Cairo-PDF) + if (_vector_based_target) { + cairo_new_path(_cr); + cairo_rectangle(_cr, 0, 0, w, h); + cairo_clip(_cr); + } + + // Cairo filter method will be mapped to PS/PDF 'interpolate' true/false). + // See cairo-pdf-surface.c + if (style) { + // See: http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty + // https://drafts.csswg.org/css-images-3/#the-image-rendering + // style.h/style.cpp, drawing-image.cpp + // + // CSS 3 defines: + // 'optimizeSpeed' as alias for "pixelated" + // 'optimizeQuality' as alias for "smooth" + switch (style->image_rendering.computed) { + case SP_CSS_IMAGE_RENDERING_OPTIMIZESPEED: + case SP_CSS_IMAGE_RENDERING_PIXELATED: + // we don't have an implementation for crisp-edges, but it should *not* smooth or blur + case SP_CSS_IMAGE_RENDERING_CRISPEDGES: + cairo_pattern_set_filter(cairo_get_source(_cr), CAIRO_FILTER_NEAREST); + break; + case SP_CSS_IMAGE_RENDERING_OPTIMIZEQUALITY: + case SP_CSS_IMAGE_RENDERING_AUTO: + default: + cairo_pattern_set_filter(cairo_get_source(_cr), CAIRO_FILTER_BEST); + break; + } + } + + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + + cairo_paint(_cr); + + cairo_restore(_cr); + return true; +} + +#define GLYPH_ARRAY_SIZE 64 + +// TODO investigate why the font is being ignored: +unsigned int CairoRenderContext::_showGlyphs(cairo_t *cr, PangoFont * /*font*/, std::vector<CairoGlyphInfo> const &glyphtext, bool path) +{ + cairo_glyph_t glyph_array[GLYPH_ARRAY_SIZE]; + cairo_glyph_t *glyphs = glyph_array; + unsigned int num_glyphs = glyphtext.size(); + if (num_glyphs > GLYPH_ARRAY_SIZE) { + glyphs = (cairo_glyph_t*)g_try_malloc(sizeof(cairo_glyph_t) * num_glyphs); + if(glyphs == nullptr) { + g_warning("CairorenderContext::_showGlyphs: can not allocate memory for %d glyphs.", num_glyphs); + return 0; + } + } + + unsigned int num_invalid_glyphs = 0; + unsigned int i = 0; // is a counter for indexing the glyphs array, only counts the valid glyphs + for (const auto & it_info : glyphtext) { + // skip glyphs which are PANGO_GLYPH_EMPTY (0x0FFFFFFF) + // or have the PANGO_GLYPH_UNKNOWN_FLAG (0x10000000) set + if (it_info.index == 0x0FFFFFFF || it_info.index & 0x10000000) { + TRACE(("INVALID GLYPH found\n")); + g_message("Invalid glyph found, continuing..."); + num_invalid_glyphs++; + continue; + } + glyphs[i].index = it_info.index; + glyphs[i].x = it_info.x; + glyphs[i].y = it_info.y; + i++; + } + + if (path) { + cairo_glyph_path(cr, glyphs, num_glyphs - num_invalid_glyphs); + } else { + cairo_show_glyphs(cr, glyphs, num_glyphs - num_invalid_glyphs); + } + + if (num_glyphs > GLYPH_ARRAY_SIZE) { + g_free(glyphs); + } + + return num_glyphs - num_invalid_glyphs; +} + +bool +CairoRenderContext::renderGlyphtext(PangoFont *font, Geom::Affine const &font_matrix, + std::vector<CairoGlyphInfo> const &glyphtext, SPStyle const *style) +{ + + _prepareRenderText(); + if (_is_omittext) + return true; + + // create a cairo_font_face from PangoFont + // double size = style->font_size.computed; /// \fixme why is this variable never used? + gpointer fonthash = (gpointer)font; + cairo_font_face_t *font_face = nullptr; + if(font_table.find(fonthash)!=font_table.end()) + font_face = font_table[fonthash]; + + FcPattern *fc_pattern = nullptr; + +#ifdef USE_PANGO_WIN32 +# ifdef CAIRO_HAS_WIN32_FONT + LOGFONTA *lfa = pango_win32_font_logfont(font); + LOGFONTW lfw; + + ZeroMemory(&lfw, sizeof(LOGFONTW)); + memcpy(&lfw, lfa, sizeof(LOGFONTA)); + MultiByteToWideChar(CP_OEMCP, MB_PRECOMPOSED, lfa->lfFaceName, LF_FACESIZE, lfw.lfFaceName, LF_FACESIZE); + + if(font_face == NULL) { + font_face = cairo_win32_font_face_create_for_logfontw(&lfw); + font_table[fonthash] = font_face; + } +# endif +#else +# ifdef CAIRO_HAS_FT_FONT + PangoFcFont *fc_font = PANGO_FC_FONT(font); + fc_pattern = fc_font->font_pattern; + if(font_face == nullptr) { + font_face = cairo_ft_font_face_create_for_pattern(fc_pattern); + font_table[fonthash] = font_face; + } +# endif +#endif + + cairo_save(_cr); + cairo_set_font_face(_cr, font_face); + + //if (fc_pattern && FcPatternGetDouble(fc_pattern, FC_PIXEL_SIZE, 0, &size) != FcResultMatch) + // size = 12.0; + + // set the given font matrix + cairo_matrix_t matrix; + _initCairoMatrix(&matrix, font_matrix); + cairo_set_font_matrix(_cr, &matrix); + + if (_render_mode == RENDER_MODE_CLIP) { + if (_clip_mode == CLIP_MODE_MASK) { + if (style->fill_rule.computed == SP_WIND_RULE_EVENODD) { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_EVEN_ODD); + } else { + cairo_set_fill_rule(_cr, CAIRO_FILL_RULE_WINDING); + } + _showGlyphs(_cr, font, glyphtext, FALSE); + } else { + // just add the glyph paths to the current context + _showGlyphs(_cr, font, glyphtext, TRUE); + } + } else { + + bool fill = false; + if (style->fill.isColor() || style->fill.isPaintserver()) { + fill = true; + } + + bool stroke = false; + if (style->stroke.isColor() || style->stroke.isPaintserver()) { + stroke = true; + } + + if (style->mix_blend_mode.set && style->mix_blend_mode.value) { + cairo_set_operator(_cr, ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); + } + + // Text never has markers + bool stroke_over_fill = true; + if ( (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL) || + + (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL) || + + (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL) ) { + stroke_over_fill = false; + } + + bool have_path = false; + if (fill && stroke_over_fill) { + _setFillStyle(style, Geom::OptRect()); + if (_is_texttopath) { + _showGlyphs(_cr, font, glyphtext, true); + if (stroke) { + cairo_fill_preserve(_cr); + have_path = true; + } else { + cairo_fill(_cr); + } + } else { + _showGlyphs(_cr, font, glyphtext, false); + } + } + + if (stroke) { + _setStrokeStyle(style, Geom::OptRect()); + if (!have_path) { + _showGlyphs(_cr, font, glyphtext, true); + } + if (fill && _is_texttopath && !stroke_over_fill) { + cairo_stroke_preserve(_cr); + have_path = true; + } else { + cairo_stroke(_cr); + } + } + + if (fill && !stroke_over_fill) { + _setFillStyle(style, Geom::OptRect()); + if (_is_texttopath) { + if (!have_path) { + // Could happen if both 'stroke' and 'stroke_over_fill' are false + _showGlyphs(_cr, font, glyphtext, true); + } + cairo_fill(_cr); + } else { + _showGlyphs(_cr, font, glyphtext, false); + } + } + + } + + cairo_restore(_cr); + +// if (font_face) +// cairo_font_face_destroy(font_face); + + return true; +} + +/* Helper functions */ + +void +CairoRenderContext::setPathVector(Geom::PathVector const &pv) +{ + cairo_new_path(_cr); + addPathVector(pv); +} + +void +CairoRenderContext::addPathVector(Geom::PathVector const &pv) +{ + feed_pathvector_to_cairo(_cr, pv); +} + +void +CairoRenderContext::_concatTransform(cairo_t *cr, double xx, double yx, double xy, double yy, double x0, double y0) +{ + cairo_matrix_t matrix; + + cairo_matrix_init(&matrix, xx, yx, xy, yy, x0, y0); + cairo_transform(cr, &matrix); +} + +void +CairoRenderContext::_initCairoMatrix(cairo_matrix_t *matrix, Geom::Affine const &transform) +{ + matrix->xx = transform[0]; + matrix->yx = transform[1]; + matrix->xy = transform[2]; + matrix->yy = transform[3]; + matrix->x0 = transform[4]; + matrix->y0 = transform[5]; +} + +void +CairoRenderContext::_concatTransform(cairo_t *cr, Geom::Affine const &transform) +{ + _concatTransform(cr, transform[0], transform[1], + transform[2], transform[3], + transform[4], transform[5]); +} + +static cairo_status_t +_write_callback(void *closure, const unsigned char *data, unsigned int length) +{ + size_t written; + FILE *file = (FILE*)closure; + + written = fwrite (data, 1, length, file); + + if (written == length) + return CAIRO_STATUS_SUCCESS; + else + return CAIRO_STATUS_WRITE_ERROR; +} + +#include "clear-n_.h" + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#undef TRACE +#undef TEST + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-render-context.h b/src/extension/internal/cairo-render-context.h new file mode 100644 index 0000000..ccb46db --- /dev/null +++ b/src/extension/internal/cairo-render-context.h @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN +#define EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN + +/** \file + * Declaration of CairoRenderContext, a class used for rendering with Cairo. + */ +/* + * Authors: + * Miklos Erdelyi <erdelyim@gmail.com> + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include <set> +#include <string> + +#include <2geom/forward.h> +#include <2geom/affine.h> + +#include "style-internal.h" // SPIEnum + +#include <cairo.h> + +class SPClipPath; +class SPMask; + +typedef struct _PangoFont PangoFont; +typedef struct _PangoLayout PangoLayout; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +class CairoRenderer; +class CairoRenderContext; +struct CairoRenderState; +struct CairoGlyphInfo; + +// Holds info for rendering a glyph +struct CairoGlyphInfo { + unsigned long index; + double x; + double y; +}; + +struct CairoRenderState { + unsigned int merge_opacity : 1; // whether fill/stroke opacity can be mul'd with item opacity + unsigned int need_layer : 1; // whether object is masked, clipped, and/or has a non-zero opacity + unsigned int has_overflow : 1; + unsigned int parent_has_userspace : 1; // whether the parent's ctm should be applied + float opacity; + bool has_filtereffect; + Geom::Affine item_transform; // this item's item->transform, for correct clipping + + SPClipPath *clip_path; + SPMask* mask; + + Geom::Affine transform; // the CTM +}; + +// Metadata to set on the cairo surface (if the surface supports it) +struct CairoRenderContextMetadata { + Glib::ustring title = ""; + Glib::ustring author = ""; + Glib::ustring subject = ""; + Glib::ustring keywords = ""; + Glib::ustring copyright = ""; + Glib::ustring creator = ""; + Glib::ustring cdate = ""; // currently unused + Glib::ustring mdate = ""; // currently unused +}; + +class CairoRenderContext { + friend class CairoRenderer; +public: + CairoRenderContext *cloneMe() const; + CairoRenderContext *cloneMe(double width, double height) const; + bool finish(bool finish_surface = true); + bool finishPage(); + bool nextPage(double width, double height, char const *label); + + CairoRenderer *getRenderer() const; + cairo_t *getCairoContext() const; + + enum CairoRenderMode { + RENDER_MODE_NORMAL, + RENDER_MODE_CLIP + }; + + enum CairoClipMode { + CLIP_MODE_PATH, + CLIP_MODE_MASK + }; + + bool setImageTarget(cairo_format_t format); + bool setPdfTarget(gchar const *utf8_fn); + bool setPsTarget(gchar const *utf8_fn); + /** Set the cairo_surface_t from an external source */ + bool setSurfaceTarget(cairo_surface_t *surface, bool is_vector, cairo_matrix_t *ctm=nullptr); + + void setPSLevel(unsigned int level); + void setEPS(bool eps); + unsigned int getPSLevel(); + void setPDFLevel(unsigned int level); + void setTextToPath(bool texttopath); + bool getTextToPath(); + void setOmitText(bool omittext); + bool getOmitText(); + void setFilterToBitmap(bool filtertobitmap); + bool getFilterToBitmap(); + void setBitmapResolution(int resolution); + int getBitmapResolution(); + + /** Creates the cairo_surface_t for the context with the + given width, height and with the currently set target + surface type. Also sets supported metadata on the surface. */ + bool setupSurface(double width, double height); + + cairo_surface_t *getSurface(); + + /** Saves the contents of the context to a PNG file. */ + bool saveAsPng(const char *file_name); + + /** On targets supporting multiple pages, sends subsequent rendering to a new page*/ + void newPage(); + + /* Render/clip mode setting/query */ + void setRenderMode(CairoRenderMode mode); + CairoRenderMode getRenderMode() const; + void setClipMode(CairoClipMode mode); + CairoClipMode getClipMode() const; + + void addPathVector(Geom::PathVector const &pv); + void setPathVector(Geom::PathVector const &pv); + + void pushLayer(); + void popLayer(cairo_operator_t composite = CAIRO_OPERATOR_CLEAR); + + void tagBegin(const char* link); + void tagEnd(); + + /* Graphics state manipulation */ + void pushState(); + void popState(); + CairoRenderState *getCurrentState() const; + CairoRenderState *getParentState() const; + void setStateForStyle(SPStyle const *style); + + void transform(Geom::Affine const &transform); + void setTransform(Geom::Affine const &transform); + Geom::Affine getTransform() const; + Geom::Affine getParentTransform() const; + + /* Clipping methods */ + void addClipPath(Geom::PathVector const &pv, SPIEnum<SPWindRule> const *fill_rule); + void addClippingRect(double x, double y, double width, double height); + + /* Rendering methods */ + enum CairoPaintOrder { + STROKE_OVER_FILL, + FILL_OVER_STROKE, + FILL_ONLY, + STROKE_ONLY + }; + + bool renderPathVector(Geom::PathVector const &pathv, SPStyle const *style, Geom::OptRect const &pbox, CairoPaintOrder order = STROKE_OVER_FILL); + bool renderImage(Inkscape::Pixbuf *pb, + Geom::Affine const &image_transform, SPStyle const *style); + bool renderGlyphtext(PangoFont *font, Geom::Affine const &font_matrix, + std::vector<CairoGlyphInfo> const &glyphtext, SPStyle const *style); + + /* More general rendering methods will have to be added (like fill, stroke) */ + +protected: + CairoRenderContext(CairoRenderer *renderer); + virtual ~CairoRenderContext(); + + enum CairoOmitTextPageState { + EMPTY, + GRAPHIC_ON_TOP, + NEW_PAGE_ON_GRAPHIC + }; + + float _width; + float _height; + unsigned short _dpi; + unsigned int _pdf_level; + unsigned int _ps_level; + bool _eps; + bool _is_texttopath; + bool _is_omittext; + bool _is_filtertobitmap; + bool _is_show_page; + int _bitmapresolution; + + FILE *_stream; + + unsigned int _is_valid : 1; + unsigned int _vector_based_target : 1; + + cairo_t *_cr; // Cairo context + cairo_surface_t *_surface; + cairo_surface_type_t _target; + cairo_format_t _target_format; + PangoLayout *_layout; + + unsigned int _clip_rule : 8; + unsigned int _clip_winding_failed : 1; + + std::vector<CairoRenderState *> _state_stack; + CairoRenderState *_state; // the current state + + CairoRenderer *_renderer; + + CairoRenderMode _render_mode; + CairoClipMode _clip_mode; + + CairoOmitTextPageState _omittext_state; + + CairoRenderContextMetadata _metadata; + + cairo_pattern_t *_createPatternForPaintServer(SPPaintServer const *const paintserver, + Geom::OptRect const &pbox, float alpha); + cairo_pattern_t *_createPatternPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox); + cairo_pattern_t *_createHatchPainter(SPPaintServer const *const paintserver, Geom::OptRect const &pbox); + + unsigned int _showGlyphs(cairo_t *cr, PangoFont *font, std::vector<CairoGlyphInfo> const &glyphtext, bool is_stroke); + + bool _finishSurfaceSetup(cairo_surface_t *surface, cairo_matrix_t *ctm = nullptr); + void _setSurfaceMetadata(cairo_surface_t *surface); + + void _setFillStyle(SPStyle const *style, Geom::OptRect const &pbox); + void _setStrokeStyle(SPStyle const *style, Geom::OptRect const &pbox); + + void _initCairoMatrix(cairo_matrix_t *matrix, Geom::Affine const &transform); + void _concatTransform(cairo_t *cr, double xx, double yx, double xy, double yy, double x0, double y0); + void _concatTransform(cairo_t *cr, Geom::Affine const &transform); + + void _prepareRenderGraphic(); + void _prepareRenderText(); + + std::map<gpointer, cairo_font_face_t *> font_table; + static void font_data_free(gpointer data); + + CairoRenderState *_createState(); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDER_CONTEXT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer-pdf-out.cpp b/src/extension/internal/cairo-renderer-pdf-out.cpp new file mode 100644 index 0000000..f5adc09 --- /dev/null +++ b/src/extension/internal/cairo-renderer-pdf-out.cpp @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PDF. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Ulf Erikson <ulferikson@users.sf.net> + * Johan Engelen <goejendaagh@zonnet.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairo.h> +#ifdef CAIRO_HAS_PDF_SURFACE + +#include "cairo-renderer-pdf-out.h" +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "latex-text-renderer.h" +#include "path-chemistry.h" +#include <print.h> +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/output.h" + +#include "display/drawing.h" +#include "display/curve.h" + +#include "object/sp-item.h" +#include "object/sp-root.h" +#include "object/sp-page.h" + +#include <2geom/affine.h> +#include "page-manager.h" +#include "document.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool CairoRendererPdfOutput::check(Inkscape::Extension::Extension * /*module*/) +{ + bool result = true; + + if (nullptr == Inkscape::Extension::db.get("org.inkscape.output.pdf.cairorenderer")) { + result = false; + } + + return result; +} + +static bool +pdf_render_document_to_file(SPDocument *doc, gchar const *filename, unsigned int level, + bool texttopath, bool omittext, bool filtertobitmap, int resolution, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, double bleedmargin_px) +{ + if (texttopath) { + assert(!omittext); + // Cairo's text-to-path method has numerical precision and font matching + // issues (https://gitlab.com/inkscape/inkscape/-/issues/1979). + // We get better results by using Inkscape's Object-to-Path method. + Inkscape::convert_text_to_curves(doc); + } + + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = TRUE; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = SP_ITEM(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) { + return false; + } + + /* Create new arena */ + Inkscape::Drawing drawing; + drawing.setExact(true); + unsigned dkey = SPItem::display_key_new(1); + root->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY); + + /* Create renderer and context */ + CairoRenderer *renderer = new CairoRenderer(); + CairoRenderContext *ctx = renderer->createContext(); + ctx->setPDFLevel(level); + ctx->setTextToPath(texttopath); + ctx->setOmitText(omittext); + ctx->setFilterToBitmap(filtertobitmap); + ctx->setBitmapResolution(resolution); + + bool ret = ctx->setPdfTarget (filename); + if(ret) { + /* Render document */ + ret = renderer->setupDocument(ctx, doc, pageBoundingBox, bleedmargin_px, base); + + auto pages = doc->getPageManager().getPages(); + if (pages.size() == 0) { + // Output the page bounding box as already set up in the initial setupDocument. + renderer->renderItem(ctx, root); + ret = ctx->finish(); + } else { + auto scale = doc->getDocumentScale(); + + // Set root transformation, which copies a bit of what happens above the + // reason for this manual process is because we need to slide in the page + // offset transformations so need fine-control over placing the children. + ctx->transform(scale); + ctx->transform(root->transform); + + int index = 1; + for (auto &page : pages) { + ctx->pushState(); + + auto pt = Inkscape::Util::Quantity::convert(1, "px", "pt"); + auto rect = page->getRect(); + // Conclude previous page and set new page width and height. + auto big_rect = rect * scale; + ctx->nextPage(big_rect.width() * pt, big_rect.height() * pt, page->label()); + + // Set up page transformation which pushes objects back into the 0,0 location + ctx->transform(Geom::Translate(rect.corner(0)).inverse()); + + for (auto &child : page->getOverlappingItems(false)) { + ctx->pushState(); + + // This process does not return layers, so those affines are added manually. + for (auto anc : child->ancestorList(true)) { + if (auto layer = dynamic_cast<SPItem *>(anc)) { + if (layer != child && layer != root) { + ctx->transform(layer->transform); + } + } + } + + // Render the page into the context in the new location. + renderer->renderItem(ctx, child, nullptr, page); + ctx->popState(); + } + ret = ctx->finishPage(); + index += 1; + + ctx->popState(); + } + ret = ctx->finish(); + } + } + + root->invoke_hide(dkey); + + renderer->destroyContext(ctx); + delete renderer; + + return ret; +} + +/** + \brief This function calls the output module with the filename + \param mod unused + \param doc Document to be saved + \param filename Filename to save to (probably will end in .pdf) + + The most interesting thing that this function does is just attach + an '>' on the front of the filename. This is the syntax used to + tell the printing system to save to file. +*/ +void +CairoRendererPdfOutput::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + unsigned int ret; + + ext = Inkscape::Extension::db.get("org.inkscape.output.pdf.cairorenderer"); + if (ext == nullptr) + return; + + int level = 0; + try { + const gchar *new_level = mod->get_param_optiongroup("PDFversion"); + if((new_level != nullptr) && (g_ascii_strcasecmp("PDF-1.5", new_level) == 0)) { + level = 1; + } + } + catch(...) { + g_warning("Parameter <PDFversion> might not exist"); + } + + bool new_textToPath = FALSE; + try { + new_textToPath = (strcmp(mod->get_param_optiongroup("textToPath"), "paths") == 0); + } + catch(...) { + g_warning("Parameter <textToPath> might not exist"); + } + + bool new_textToLaTeX = FALSE; + try { + new_textToLaTeX = (strcmp(mod->get_param_optiongroup("textToPath"), "LaTeX") == 0); + } + catch(...) { + g_warning("Parameter <textToLaTeX> might not exist"); + } + + bool new_blurToBitmap = FALSE; + try { + new_blurToBitmap = mod->get_param_bool("blurToBitmap"); + } + catch(...) { + g_warning("Parameter <blurToBitmap> might not exist"); + } + + int new_bitmapResolution = 72; + try { + new_bitmapResolution = mod->get_param_int("resolution"); + } + catch(...) { + g_warning("Parameter <resolution> might not exist"); + } + + const gchar *new_exportId = nullptr; + try { + new_exportId = mod->get_param_string("exportId"); + } + catch(...) { + g_warning("Parameter <exportId> might not exist"); + } + + bool new_exportCanvas = true; + try { + new_exportCanvas = (strcmp(ext->get_param_optiongroup("area"), "page") == 0); + } catch(...) { + g_warning("Parameter <area> might not exist"); + } + bool new_exportDrawing = !new_exportCanvas; + + double new_bleedmargin_px = 0.; + try { + new_bleedmargin_px = Inkscape::Util::Quantity::convert(mod->get_param_float("bleed"), "mm", "px"); + } + catch(...) { + g_warning("Parameter <bleed> might not exist"); + } + + // Create PDF file + { + gchar * final_name; + final_name = g_strdup_printf("> %s", filename); + ret = pdf_render_document_to_file(doc, final_name, level, + new_textToPath, new_textToLaTeX, new_blurToBitmap, new_bitmapResolution, + new_exportId, new_exportDrawing, new_exportCanvas, new_bleedmargin_px); + g_free(final_name); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } + + // Create LaTeX file (if requested) + if (new_textToLaTeX) { + ret = latex_render_document_text_to_file(doc, filename, new_exportId, new_exportDrawing, new_exportCanvas, new_bleedmargin_px, true); + + if (!ret) + throw Inkscape::Extension::Output::save_failed(); + } +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of Cairo PDF out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +CairoRendererPdfOutput::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>Portable Document Format</name>\n" + "<id>org.inkscape.output.pdf.cairorenderer</id>\n" + "<param name=\"PDFversion\" gui-text=\"" N_("Restrict to PDF version:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value='PDF-1.5'>" N_("PDF 1.5") "</option>\n" + "<option value='PDF-1.4'>" N_("PDF 1.4") "</option>\n" + "</param>\n" + "<param name=\"textToPath\" gui-text=\"" N_("Text output options:") "\" type=\"optiongroup\" appearance=\"radio\">\n" + "<option value=\"embed\">" N_("Embed fonts") "</option>\n" + "<option value=\"paths\">" N_("Convert text to paths") "</option>\n" + "<option value=\"LaTeX\">" N_("Omit text in PDF and create LaTeX file") "</option>\n" + "</param>\n" + "<param name=\"blurToBitmap\" gui-text=\"" N_("Rasterize filter effects") "\" type=\"bool\">true</param>\n" + "<param name=\"resolution\" gui-text=\"" N_("Resolution for rasterization (dpi):") "\" type=\"int\" min=\"1\" max=\"10000\">96</param>\n" + "<param name=\"area\" gui-text=\"" N_("Output page size:") "\" type=\"optiongroup\" appearance=\"radio\" >\n" + "<option value=\"page\">" N_("Use document's page size") "</option>" + "<option value=\"drawing\">" N_("Use exported object's size") "</option>" + "</param>" + "<param name=\"bleed\" gui-text=\"" N_("Bleed/margin (mm):") "\" type=\"float\" min=\"-10000\" max=\"10000\">0</param>\n" + "<param name=\"exportId\" gui-text=\"" N_("Limit export to the object with ID:") "\" type=\"string\"></param>\n" + "<output is_exported='true'>\n" + "<extension>.pdf</extension>\n" + "<mimetype>application/pdf</mimetype>\n" + "<filetypename>Portable Document Format (*.pdf)</filetypename>\n" + "<filetypetooltip>PDF File</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new CairoRendererPdfOutput()); + // clang-format on + + return; +} + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* HAVE_CAIRO_PDF */ diff --git a/src/extension/internal/cairo-renderer-pdf-out.h b/src/extension/internal/cairo-renderer-pdf-out.h new file mode 100644 index 0000000..9f99012 --- /dev/null +++ b/src/extension/internal/cairo-renderer-pdf-out.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A quick hack to use the Cairo renderer to write out a file. This + * then makes 'save as...' PDF. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Ulf Erikson <ulferikson@users.sf.net> + * + * Copyright (C) 2004-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H +#define EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoRendererPdfOutput : Inkscape::Extension::Implementation::Implementation { + +public: + bool check(Inkscape::Extension::Extension *module) override; + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + static void init(); +}; + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDERER_PDF_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer.cpp b/src/extension/internal/cairo-renderer.cpp new file mode 100644 index 0000000..5898b8e --- /dev/null +++ b/src/extension/internal/cairo-renderer.cpp @@ -0,0 +1,1012 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering with Cairo. + */ +/* + * Author: + * Miklos Erdelyi <erdelyim@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_BACKEND +#define PANGO_ENABLE_BACKEND +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + + +#include <csignal> +#include <cerrno> + + +#include <2geom/transforms.h> +#include <2geom/pathvector.h> +#include <cairo.h> +#include <glib.h> +#include <glibmm/i18n.h> + +// include support for only the compiled-in surface types +#ifdef CAIRO_HAS_PDF_SURFACE +#include <cairo-pdf.h> +#endif +#ifdef CAIRO_HAS_PS_SURFACE +#include <cairo-ps.h> +#endif + +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "document.h" +#include "inkscape-version.h" +#include "rdf.h" +#include "style-internal.h" +#include "display/cairo-utils.h" +#include "display/curve.h" +#include "extension/system.h" +#include "filter-chemistry.h" +#include "helper/pixbuf-ops.h" +#include "helper/png-write.h" + +#include "io/sys.h" + +#include "include/source_date_epoch.h" + +#include "libnrtype/Layout-TNG.h" + +#include "object/sp-anchor.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-hatch-path.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-page.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-symbol.h" +#include "object/sp-text.h" +#include "object/sp-use.h" + +#include "util/units.h" + +//#define TRACE(_args) g_printf _args +#define TRACE(_args) +//#define TEST(_args) _args +#define TEST(_args) + +namespace Inkscape { +namespace Extension { +namespace Internal { + +CairoRenderer::CairoRenderer(void) += default; + +CairoRenderer::~CairoRenderer() +{ + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + + return; +} + +CairoRenderContext* +CairoRenderer::createContext() +{ + CairoRenderContext *new_context = new CairoRenderContext(this); + g_assert( new_context != nullptr ); + + new_context->_state = nullptr; + + // create initial render state + CairoRenderState *state = new_context->_createState(); + state->transform = Geom::identity(); + new_context->_state_stack.push_back(state); + new_context->_state = state; + + return new_context; +} + +void +CairoRenderer::destroyContext(CairoRenderContext *ctx) +{ + delete ctx; +} + +/* + +Here comes the rendering part which could be put into the 'render' methods of SPItems' + +*/ + +/* The below functions are copy&pasted plus slightly modified from *_invoke_print functions. */ +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr); +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr); +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx); +static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page = nullptr); +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin = nullptr); +static void sp_text_render(SPText *text, CairoRenderContext *ctx); +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx); +static void sp_image_render(SPImage *image, CairoRenderContext *ctx); +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page); +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page = nullptr); + +static void sp_shape_render_invoke_marker_rendering(SPMarker* marker, Geom::Affine tr, SPStyle* style, CairoRenderContext *ctx, SPItem *origin) +{ + bool render = true; + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + if (style->stroke_width.computed > 1e-9) { + tr = Geom::Scale(style->stroke_width.computed) * tr; + } else { + render = false; // stroke width zero and marker is thus scaled down to zero, skip + } + } + + if (render) { + SPItem* marker_item = sp_item_first_item_child(marker); + if (marker_item) { + tr = (Geom::Affine)marker_item->transform * (Geom::Affine)marker->c2p * tr; + Geom::Affine old_tr = marker_item->transform; + marker_item->transform = tr; + ctx->getRenderer()->renderItem (ctx, marker_item, origin); + marker_item->transform = old_tr; + } + } +} + +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin) +{ + if (!shape->curve()) { + return; + } + + Geom::OptRect pbox = shape->geometricBounds(); + + SPStyle* style = shape->style; + auto fill_origin = style->fill.paintOrigin; + auto stroke_origin = style->stroke.paintOrigin; + + if (origin) { + // If the shape is a child of a marker, we must set styles from the origin. + SPMarker *marker = nullptr; + auto parentobj = shape->parent; + while (parentobj) { + if (marker = dynamic_cast<SPMarker *>(parentobj)) { + break; + } + parentobj = parentobj->parent; + } + if (marker) { + // Origin will ultimately be either the original object the marker + // was added to OR a use tag. See sp_use_render for where this object + // comes from when it's a clone. + SPStyle *styleorig = origin->style; + bool iscolorfill = styleorig->fill.isColor() || (styleorig->fill.isPaintserver() && !styleorig->getFillPaintServer()->isValid()); + bool iscolorstroke = styleorig->stroke.isColor() || (styleorig->stroke.isPaintserver() && !styleorig->getStrokePaintServer()->isValid()); + bool fillctxfill = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool fillctxstroke = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + bool strokectxfill = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool strokectxstroke = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + + if (fillctxfill || fillctxstroke) { + if (fillctxfill ? iscolorfill : iscolorstroke) { + style->fill.setColor(fillctxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (fillctxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->fill.value.href = fillctxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->fill.setNone(); + } + } + if (strokectxfill || strokectxstroke) { + if (strokectxfill ? iscolorfill : iscolorstroke) { + style->stroke.setColor(strokectxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (strokectxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->stroke.value.href = strokectxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->stroke.setNone(); + } + } + style->fill.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + style->stroke.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + } + } + + Geom::PathVector const &pathv = shape->curve()->get_pathvector(); + if (pathv.empty()) { + return; + } + + if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_NORMAL || + (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE)) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + Geom::Affine tr; + if (marker->orient_mode == MARKER_ORIENT_AUTO) { + tr = sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else if (marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + tr = Geom::Rotate::from_degrees( 180.0 ) * sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(pathv.begin()->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if ( !shape->_marker[i] ) continue; + SPMarker* marker = shape->_marker[i]; + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_start(path_it->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(path_it->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment */ + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform(*curve_it1, *curve_it2); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(curve_it1->pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + } + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + + if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + + // Put the style's paint origin back, in case this shape is a marker + // which is rendered multple times. + style->fill.paintOrigin = fill_origin; + style->stroke.paintOrigin = stroke_origin; +} + +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + CairoRenderer *renderer = ctx->getRenderer(); + for (auto obj : group->childList(false)) { + if (SPItem *item = dynamic_cast<SPItem *>(obj)) { + renderer->renderItem(ctx, item, origin, page); + } + } +} + +static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page) +{ + bool translated = false; + CairoRenderer *renderer = ctx->getRenderer(); + + if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { + // FIXME: This translation sometimes isn't in the correct units; e.g. + // x="0" y="42" has a different effect than transform="translate(0,42)". + Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); + ctx->pushState(); + ctx->transform(tp); + translated = true; + } + + if (use->child) { + // Padding in the use object as the origin here ensures markers + // are rendered with their correct context-fill. + renderer->renderItem(ctx, use->child, use, page); + } + + if (translated) { + ctx->popState(); + } +} + +static void sp_text_render(SPText *text, CairoRenderContext *ctx) +{ + text->layout.showGlyphs(ctx); +} + +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx) +{ + flowtext->layout.showGlyphs(ctx); +} + +static void sp_image_render(SPImage *image, CairoRenderContext *ctx) +{ + if (!image->pixbuf) { + return; + } + if ((image->width.computed <= 0.0) || (image->height.computed <= 0.0)) { + return; + } + + int w = image->pixbuf->width(); + int h = image->pixbuf->height(); + + double x = image->x.computed; + double y = image->y.computed; + double width = image->width.computed; + double height = image->height.computed; + + if (image->aspect_align != SP_ASPECT_NONE) { + calculatePreserveAspectRatio (image->aspect_align, image->aspect_clip, (double)w, (double)h, + &x, &y, &width, &height); + } + + if (image->aspect_clip == SP_ASPECT_SLICE && !ctx->getCurrentState()->has_overflow) { + ctx->addClippingRect(image->x.computed, image->y.computed, image->width.computed, image->height.computed); + } + + Geom::Translate tp(x, y); + Geom::Scale s(width / (double)w, height / (double)h); + Geom::Affine t(s * tp); + + ctx->renderImage(image->pixbuf, t, image->style); +} + +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + std::vector<SPObject*> l(a->childList(false)); + if (a->href) + ctx->tagBegin(a->href); + for(auto x : l){ + SPItem *item = dynamic_cast<SPItem*>(x); + if (item) { + renderer->renderItem(ctx, item); + } + } + if (a->href) + ctx->tagEnd(); +} + +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + if (!symbol->cloned) { + return; + } + + /* Cloned <symbol> is actually renderable */ + ctx->pushState(); + ctx->transform(symbol->c2p); + + // apply viewbox if set + if (false /*symbol->viewBox_set*/) { + Geom::Affine vb2user; + double x, y, width, height; + double view_width, view_height; + x = 0.0; + y = 0.0; + width = 1.0; + height = 1.0; + + view_width = symbol->viewBox.width(); + view_height = symbol->viewBox.height(); + + calculatePreserveAspectRatio(symbol->aspect_align, symbol->aspect_clip, view_width, view_height, + &x, &y,&width, &height); + + // [itemTransform *] translate(x, y) * scale(w/vw, h/vh) * translate(-vx, -vy); + vb2user = Geom::identity(); + vb2user[0] = width / view_width; + vb2user[3] = height / view_height; + vb2user[4] = x - symbol->viewBox.left() * vb2user[0]; + vb2user[5] = y - symbol->viewBox.top() * vb2user[3]; + + ctx->transform(vb2user); + } + + sp_group_render(symbol, ctx, origin, page); + ctx->popState(); +} + +static void sp_root_render(SPRoot *root, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + if (!ctx->getCurrentState()->has_overflow && root->parent) + ctx->addClippingRect(root->x.computed, root->y.computed, root->width.computed, root->height.computed); + + ctx->pushState(); + renderer->setStateForItem(ctx, root); + ctx->transform(root->c2p); + sp_group_render(root, ctx); + ctx->popState(); +} + +/** + This function converts the item to a raster image and includes the image into the cairo renderer. + It is only used for filters and then only when rendering filters as bitmaps is requested. +*/ +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page) +{ + + // The code was adapted from sp_selection_create_bitmap_copy in selection-chemistry.cpp + + // Calculate resolution + double res; + /** @TODO reimplement the resolution stuff (WHY?) + */ + res = ctx->getBitmapResolution(); + if(res == 0) { + res = Inkscape::Util::Quantity::convert(1, "in", "px"); + } + TRACE(("sp_asbitmap_render: resolution: %f\n", res )); + + // Get the bounding box of the selection in document coordinates. + Geom::OptRect bbox = item->documentVisualBounds(); + + bbox &= (page ? page->getDocumentRect() : item->document->preferredBounds()); + + // no bbox, e.g. empty group or item not overlapping its page + if (!bbox) { + return; + } + + // The width and height of the bitmap in pixels + unsigned width = ceil(bbox->width() * Inkscape::Util::Quantity::convert(res, "px", "in")); + unsigned height = ceil(bbox->height() * Inkscape::Util::Quantity::convert(res, "px", "in")); + + if (width == 0 || height == 0) return; + + // Scale to exactly fit integer bitmap inside bounding box + double scale_x = bbox->width() / width; + double scale_y = bbox->height() / height; + + // Location of bounding box in document coordinates. + double shift_x = bbox->min()[Geom::X]; + double shift_y = bbox->top(); + + // For default 96 dpi, snap bitmap to pixel grid + if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) { + shift_x = round (shift_x); + shift_y = round (shift_y); + } + + // Calculate the matrix that will be applied to the image so that it exactly overlaps the source objects + + // Matrix to put bitmap in correct place on document + Geom::Affine t_on_document = (Geom::Affine)(Geom::Scale (scale_x, scale_y)) * + (Geom::Affine)(Geom::Translate (shift_x, shift_y)); + + // ctx matrix already includes item transformation. We must substract. + Geom::Affine t_item = item->i2doc_affine(); + Geom::Affine t = t_on_document * t_item.inverse(); + + // Do the export + SPDocument *document = item->document; + + std::vector<SPItem*> items; + items.push_back(item); + + std::unique_ptr<Inkscape::Pixbuf> pb(sp_generate_internal_bitmap(document, *bbox, res, items, true)); + + if (pb) { + //TEST(gdk_pixbuf_save( pb, "bitmap.png", "png", NULL, NULL )); + + ctx->renderImage(pb.get(), t, item->style); + } +} + + +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + SPRoot *root = dynamic_cast<SPRoot *>(item); + if (root) { + TRACE(("root\n")); + sp_root_render(root, ctx); + } else { + SPSymbol *symbol = dynamic_cast<SPSymbol *>(item); + if (symbol) { + TRACE(("symbol\n")); + sp_symbol_render(symbol, ctx, origin, page); + } else { + SPAnchor *anchor = dynamic_cast<SPAnchor *>(item); + if (anchor) { + TRACE(("<a>\n")); + sp_anchor_render(anchor, ctx); + } else { + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + TRACE(("shape\n")); + sp_shape_render(shape, ctx, origin); + } else { + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + TRACE(("use begin---\n")); + sp_use_render(use, ctx, page); + TRACE(("---use end\n")); + } else { + SPText *text = dynamic_cast<SPText *>(item); + if (text) { + TRACE(("text\n")); + sp_text_render(text, ctx); + } else { + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item); + if (flowtext) { + TRACE(("flowtext\n")); + sp_flowtext_render(flowtext, ctx); + } else { + SPImage *image = dynamic_cast<SPImage *>(item); + if (image) { + TRACE(("image\n")); + sp_image_render(image, ctx); + } else if (dynamic_cast<SPMarker *>(item)) { + // Marker contents shouldn't be rendered, even outside of <defs>. + return; + } else { + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + TRACE(("<g>\n")); + sp_group_render(group, ctx, origin, page); + } + } + } + } + } + } + } + } + } +} + +void +CairoRenderer::setStateForItem(CairoRenderContext *ctx, SPItem const *item) +{ + ctx->setStateForStyle(item->style); + + CairoRenderState *state = ctx->getCurrentState(); + state->clip_path = item->getClipObject(); + state->mask = item->getMaskObject(); + state->item_transform = Geom::Affine (item->transform); + + // If parent_has_userspace is true the parent state's transform + // has to be used for the mask's/clippath's context. + // This is so because we use the image's/(flow)text's transform for positioning + // instead of explicitly specifying it and letting the renderer do the + // transformation before rendering the item. + if (dynamic_cast<SPText const *>(item) || dynamic_cast<SPFlowtext const *>(item) || dynamic_cast<SPImage const *>(item)) { + state->parent_has_userspace = TRUE; + } + TRACE(("setStateForItem opacity: %f\n", state->opacity)); +} + +bool CairoRenderer::_shouldRasterize(CairoRenderContext *ctx, SPItem const *item) +{ + // rasterize filtered items as per user setting + // however, clipPaths ignore any filters, so do *not* rasterize + // TODO: might apply to some degree to masks with filtered elements as well; + // we need to figure out where in the stack it would be safe to rasterize + if (ctx->getFilterToBitmap() && !item->isInClipPath()) { + if (auto const *clone = dynamic_cast<SPUse const *>(item)) { + return clone->anyInChain([](SPItem const *i) { return i && i->isFiltered(); }); + } else { + return item->isFiltered(); + } + } + return false; +} + +void CairoRenderer::_doRender(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + // Check item's visibility + if (item->isHidden() || has_hidder_filter(item)) { + return; + } + + if (_shouldRasterize(ctx, item)) { + sp_asbitmap_render(item, ctx, page); + } else { + sp_item_invoke_render(item, ctx, origin, page); + } +} + +// TODO change this to accept a const SPItem: +void CairoRenderer::renderItem(CairoRenderContext *ctx, SPItem *item, SPItem *origin, SPPage *page) +{ + ctx->pushState(); + setStateForItem(ctx, item); + + CairoRenderState *state = ctx->getCurrentState(); + state->need_layer = ( state->mask || state->clip_path || state->opacity != 1.0 ); + SPStyle* style = item->style; + SPGroup * group = dynamic_cast<SPGroup *>(item); + bool blend = false; + if (group && style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) { + state->need_layer = true; + blend = true; + } + // Draw item on a temporary surface so a mask, clip-path, or opacity can be applied to it. + if (state->need_layer) { + state->merge_opacity = FALSE; + ctx->pushLayer(); + } + + ctx->transform(item->transform); + + _doRender(item, ctx, origin, page); + + if (state->need_layer) { + if (blend) { + ctx->popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); // This applies clipping/masking + } else { + ctx->popLayer(); // This applies clipping/masking + } + } + ctx->popState(); +} + +void CairoRenderer::renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key) { + ctx->pushState(); + ctx->setStateForStyle(hatchPath.style); + ctx->transform(Geom::Translate(hatchPath.offset.computed, 0)); + + std::unique_ptr<SPCurve> curve = hatchPath.calculateRenderCurve(key); + Geom::PathVector const & pathv =curve->get_pathvector(); + if (!pathv.empty()) { + ctx->renderPathVector(pathv, hatchPath.style, Geom::OptRect()); + } + + ctx->popState(); +} + +void CairoRenderer::setMetadata(CairoRenderContext *ctx, SPDocument *doc) { + // title + const gchar *title = rdf_get_work_entity(doc, rdf_find_entity("title")); + if (title) { + ctx->_metadata.title = title; + } + + // author + const gchar *author = rdf_get_work_entity(doc, rdf_find_entity("creator")); + if (author) { + ctx->_metadata.author = author; + } + + // subject + const gchar *subject = rdf_get_work_entity(doc, rdf_find_entity("description")); + if (subject) { + ctx->_metadata.subject = subject; + } + + // keywords + const gchar *keywords = rdf_get_work_entity(doc, rdf_find_entity("subject")); + if (keywords) { + ctx->_metadata.keywords = keywords; + } + + // copyright + const gchar *copyright = rdf_get_work_entity(doc, rdf_find_entity("rights")); + if (copyright) { + ctx->_metadata.copyright = copyright; + } + + // creator + ctx->_metadata.creator = Glib::ustring::compose("Inkscape %1 (https://inkscape.org)", + Inkscape::version_string_without_revision); + + // cdate (only used for for reproducible builds hack) + Glib::ustring cdate = ReproducibleBuilds::now_iso_8601(); + if (!cdate.empty()) { + ctx->_metadata.cdate = cdate; + } + + // mdate (currently unused) +} + +bool +CairoRenderer::setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base) +{ +// PLEASE note when making changes to the boundingbox and transform calculation, corresponding changes should be made to LaTeXTextRenderer::setupDocument !!! + + g_assert( ctx != nullptr ); + + if (!base) { + base = doc->getRoot(); + } + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); + } else { + Geom::OptRect bbox = base->documentVisualBounds(); + if (!bbox) { + g_message("CairoRenderer: empty bounding box."); + return false; + } + d = *bbox; + } + d.expandBy(bleedmargin_px); + + double px_to_ctx_units = 1.0; + if (ctx->_vector_based_target) { + // convert from px to pt + px_to_ctx_units = Inkscape::Util::Quantity::convert(1, "px", "pt"); + } + + auto width = d.width() * px_to_ctx_units; + auto height = d.height() * px_to_ctx_units; + + setMetadata(ctx, doc); + + TRACE(("setupDocument: %f x %f\n", width, height)); + bool ret = ctx->setupSurface(width, height); + + if (ret) { + if (pageBoundingBox) { + // translate to set bleed/margin + Geom::Affine tp( Geom::Translate( bleedmargin_px, bleedmargin_px ) ); + ctx->transform(tp); + } else { + // this transform translates the export drawing to a virtual page (0,0)-(width,height) + Geom::Affine tp(Geom::Translate(-d.min())); + ctx->transform(tp); + } + } + + return ret; +} + +// Apply an SVG clip path +void +CairoRenderer::applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (cp == nullptr) + return; + + CairoRenderContext::CairoRenderMode saved_mode = ctx->getRenderMode(); + ctx->setRenderMode(CairoRenderContext::RENDER_MODE_CLIP); + + // FIXME: the access to the first clippath view to obtain the bbox is completely bogus + Geom::Affine saved_ctm; + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && cp->display->bbox) { + Geom::Rect clip_bbox = *cp->display->bbox; + Geom::Affine t(Geom::Scale(clip_bbox.dimensions())); + t[4] = clip_bbox.left(); + t[5] = clip_bbox.top(); + t *= ctx->getCurrentState()->transform; + saved_ctm = ctx->getTransform(); + ctx->setTransform(t); + } + + TRACE(("BEGIN clip\n")); + SPObject const *co = cp; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast<SPItem const *>(&child); + if (item) { + + // combine transform of the item in clippath and the item using clippath: + Geom::Affine tempmat = item->transform * ctx->getCurrentState()->item_transform; + + // render this item in clippath + ctx->pushState(); + ctx->transform(tempmat); + setStateForItem(ctx, item); + // TODO fix this call to accept const items + _doRender(const_cast<SPItem *>(item), ctx); + ctx->popState(); + } + } + TRACE(("END clip\n")); + + // do clipping only if this was the first call to applyClipPath + if (ctx->getClipMode() == CairoRenderContext::CLIP_MODE_PATH + && saved_mode == CairoRenderContext::RENDER_MODE_NORMAL) + cairo_clip(ctx->_cr); + + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX) + ctx->setTransform(saved_ctm); + + ctx->setRenderMode(saved_mode); +} + +// Apply an SVG mask +void +CairoRenderer::applyMask(CairoRenderContext *ctx, SPMask const *mask) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (mask == nullptr) + return; + + // FIXME: the access to the first mask view to obtain the bbox is completely bogus + // TODO: should the bbox be transformed if maskUnits != userSpaceOnUse ? + if (mask->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && mask->display->bbox) { + Geom::Rect mask_bbox = *mask->display->bbox; + Geom::Affine t(Geom::Scale(mask_bbox.dimensions())); + t[4] = mask_bbox.left(); + t[5] = mask_bbox.top(); + t *= ctx->getCurrentState()->transform; + ctx->setTransform(t); + } + + // Clip mask contents... but... + // The mask's bounding box is the "geometric bounding box" which doesn't allow for + // filters which extend outside the bounding box. So don't clip. + // ctx->addClippingRect(mask_bbox.x0, mask_bbox.y0, mask_bbox.x1 - mask_bbox.x0, mask_bbox.y1 - mask_bbox.y0); + + ctx->pushState(); + + TRACE(("BEGIN mask\n")); + SPObject const *co = mask; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast<SPItem const *>(&child); + if (item) { + // TODO fix const correctness: + renderItem(ctx, const_cast<SPItem*>(item)); + } + } + TRACE(("END mask\n")); + + ctx->popState(); +} + +void +calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, double vp_height, + double *x, double *y, double *width, double *height) +{ + if (aspect_align == SP_ASPECT_NONE) + return; + + double scalex, scaley, scale; + double new_width, new_height; + scalex = *width / vp_width; + scaley = *height / vp_height; + scale = (aspect_clip == SP_ASPECT_MEET) ? MIN(scalex, scaley) : MAX(scalex, scaley); + new_width = vp_width * scale; + new_height = vp_height * scale; + /* Now place viewbox to requested position */ + switch (aspect_align) { + case SP_ASPECT_XMIN_YMIN: + break; + case SP_ASPECT_XMID_YMIN: + *x -= 0.5 * (new_width - *width); + break; + case SP_ASPECT_XMAX_YMIN: + *x -= 1.0 * (new_width - *width); + break; + case SP_ASPECT_XMIN_YMID: + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMID: + *x -= 0.5 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMID: + *x -= 1.0 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMIN_YMAX: + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMAX: + *x -= 0.5 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMAX: + *x -= 1.0 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + default: + break; + } + *width = new_width; + *height = new_height; +} + +#include "clear-n_.h" + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#undef TRACE + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cairo-renderer.h b/src/extension/internal/cairo-renderer.h new file mode 100644 index 0000000..934fa45 --- /dev/null +++ b/src/extension/internal/cairo-renderer.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN +#define EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN + +/** \file + * Declaration of CairoRenderer, a class used for rendering via a CairoRenderContext. + */ +/* + * Authors: + * Miklos Erdelyi <erdelyim@gmail.com> + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include <set> +#include <string> + +//#include "libnrtype/font-instance.h" +#include <cairo.h> + +class SPItem; +class SPClipPath; +class SPMask; +class SPHatchPath; +class SPPage; + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CairoRenderer; +class CairoRenderContext; + +class CairoRenderer { +public: + CairoRenderer(); + virtual ~CairoRenderer(); + + CairoRenderContext *createContext(); + void destroyContext(CairoRenderContext *ctx); + + void setStateForItem(CairoRenderContext *ctx, SPItem const *item); + + void applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp); + void applyMask(CairoRenderContext *ctx, SPMask const *mask); + + /** Initializes the CairoRenderContext according to the specified + SPDocument. A set*Target function can only be called on the context + before setupDocument. */ + bool setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base); + + /** Traverses the object tree and invokes the render methods. */ + void renderItem(CairoRenderContext *ctx, SPItem *item, SPItem *clone = nullptr, SPPage *page = nullptr); + void renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key); + +private: + /** Extract metadata from doc and set it on ctx. */ + void setMetadata(CairoRenderContext *ctx, SPDocument *doc); + + /** Decide whether the given item should be rendered as a bitmap. */ + static bool _shouldRasterize(CairoRenderContext *ctx, SPItem const *item); + + /** Render a single item in a fully set up context. */ + static void _doRender(SPItem *item, CairoRenderContext *ctx, SPItem *origin = nullptr, + SPPage *page = nullptr); + +}; + +// FIXME: this should be a static method of CairoRenderer +void calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, + double vp_height, double *x, double *y, double *width, double *height); + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_CAIRO_RENDERER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/cdr-input.cpp b/src/extension/internal/cdr-input.cpp new file mode 100644 index 0000000..521ff0e --- /dev/null +++ b/src/extension/internal/cdr-input.cpp @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstdio> + +#include "cdr-input.h" + +#ifdef WITH_LIBCDR + +#include <string> +#include <cstring> + +#include <libcdr/libcdr.h> + +#include <librevenge-stream/librevenge-stream.h> + +using librevenge::RVNGString; +using librevenge::RVNGFileStream; +using librevenge::RVNGStringVector; + +#include <gtkmm/grid.h> +#include <gtkmm/spinbutton.h> + +#include "extension/system.h" +#include "extension/input.h" + +#include "document.h" +#include "inkscape.h" + +#include "ui/dialog-events.h" +#include <glibmm/i18n.h> + +#include "ui/view/svg-view-widget.h" + +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +class CdrImportDialog : public Gtk::Dialog { +public: + CdrImportDialog(const std::vector<RVNGString> &vec); + ~CdrImportDialog() override; + + bool showDialog(); + unsigned getSelectedPage(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(); + + // Signal handlers + void _onPageNumberChanged(); + void _onSpinButtonPress(GdkEventButton* button_event); + void _onSpinButtonRelease(GdkEventButton* button_event); + + class Gtk::Box * vbox1; + class Inkscape::UI::View::SVGViewWidget * _previewArea; + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + + class Gtk::Box * _page_selector_box; + class Gtk::Label * _labelSelect; + class Gtk::Label * _labelTotalPages; + class Gtk::SpinButton * _pageNumberSpin; + + const std::vector<RVNGString> &_vec; // Document to be imported + unsigned _current_page; // Current selected page + bool _spinning; // whether SpinButton is pressed (i.e. we're "spinning") +}; + +CdrImportDialog::CdrImportDialog(const std::vector<RVNGString> &vec) + : _previewArea(nullptr) + , _vec(vec) + , _current_page(1) + , _spinning(false) +{ + int num_pages = _vec.size(); + if ( num_pages <= 1 ) + return; + + // Dialog settings + this->set_title(_("Page Selector")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + + // Preview area + vbox1 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + this->get_content_area()->pack_start(*vbox1); + + // CONTROLS + _page_selector_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + // "Select page:" label + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _page_selector_box->pack_start(*_labelSelect, Gtk::PACK_SHRINK); + + // Adjustment + spinner + auto pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _vec.size(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Gtk::SpinButton(pageNumberSpin_adj, 1, 0)); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_wrap(false); + _page_selector_box->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK); + + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + _page_selector_box->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK); + + vbox1->pack_end(*_page_selector_box, Gtk::PACK_SHRINK); + + // Buttons + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + this->add_action_widget(*cancelbutton, Gtk::RESPONSE_CANCEL); + this->add_action_widget(*okbutton, Gtk::RESPONSE_OK); + + // Show all widgets in dialog + this->show_all(); + + // Connect signals + _pageNumberSpin->signal_value_changed().connect(sigc::mem_fun(*this, &CdrImportDialog::_onPageNumberChanged)); + _pageNumberSpin->signal_button_press_event().connect_notify(sigc::mem_fun(*this, &CdrImportDialog::_onSpinButtonPress)); + _pageNumberSpin->signal_button_release_event().connect_notify(sigc::mem_fun(*this, &CdrImportDialog::_onSpinButtonRelease)); + + _setPreviewPage(); +} + +CdrImportDialog::~CdrImportDialog() = default; + +bool CdrImportDialog::showDialog() +{ + show(); + gint b = run(); + hide(); + if (b == Gtk::RESPONSE_OK || b == Gtk::RESPONSE_ACCEPT) { + return TRUE; + } else { + return FALSE; + } +} + +unsigned CdrImportDialog::getSelectedPage() +{ + return _current_page; +} + +void CdrImportDialog::_onPageNumberChanged() +{ + unsigned page = static_cast<unsigned>(_pageNumberSpin->get_value_as_int()); + _current_page = CLAMP(page, 1U, _vec.size()); + _setPreviewPage(); +} + +void CdrImportDialog::_onSpinButtonPress(GdkEventButton* /*button_event*/) +{ + _spinning = true; +} + +void CdrImportDialog::_onSpinButtonRelease(GdkEventButton* /*button_event*/) +{ + _spinning = false; + _setPreviewPage(); +} + +/** + * \brief Renders the given page's thumbnail + */ +void CdrImportDialog::_setPreviewPage() +{ + if (_spinning) { + return; + } + + SPDocument *doc = SPDocument::createNewDocFromMem(_vec[_current_page-1].cstr(), strlen(_vec[_current_page-1].cstr()), false); + if(!doc) { + g_warning("CDR import: Could not create preview for page %d", _current_page); + gchar const *no_preview_template = R"A( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'> + <path d='M 82,10 18,74 m 0,-64 64,64' style='fill:none;stroke:#ff0000;stroke-width:2px;'/> + <rect x='18' y='10' width='64' height='64' style='fill:none;stroke:#000000;stroke-width:1.5px;'/> + <text x='50' y='92' style='font-size:10px;text-anchor:middle;font-family:sans-serif;'>%s</text> + </svg> + )A"; + gchar * no_preview = g_strdup_printf(no_preview_template, _("No preview")); + doc = SPDocument::createNewDocFromMem(no_preview, strlen(no_preview), false); + g_free(no_preview); + } + + if (!doc) { + std::cerr << "CdrImportDialog::_setPreviewPage: No document!" << std::endl; + return; + } + + if (_previewArea) { + _previewArea->setDocument(doc); + } else { + _previewArea = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + vbox1->pack_start(*_previewArea, Gtk::PACK_EXPAND_WIDGET, 0); + } + + _previewArea->setResize(400, 400); + _previewArea->show_all(); +} + +SPDocument *CdrInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGFileStream input(converted_uri); + g_free(converted_uri); + #else + RVNGFileStream input(uri); + #endif + + if (!libcdr::CDRDocument::isSupported(&input)) { + return nullptr; + } + + RVNGStringVector output; + librevenge::RVNGSVGDrawingGenerator generator(output, "svg"); + + if (!libcdr::CDRDocument::parse(&input, &generator)) { + return nullptr; + } + + if (output.empty()) { + return nullptr; + } + + std::vector<RVNGString> tmpSVGOutput; + for (unsigned i=0; i<output.size(); ++i) { + RVNGString tmpString("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"); + tmpString.append(output[i]); + tmpSVGOutput.push_back(tmpString); + } + + unsigned page_num = 1; + + // If only one page is present, import that one without bothering user + if (tmpSVGOutput.size() > 1) { + CdrImportDialog *dlg = nullptr; + if (INKSCAPE.use_gui()) { + dlg = new CdrImportDialog(tmpSVGOutput); + if (!dlg->showDialog()) { + delete dlg; + throw Input::open_cancelled(); + } + } + + // Get needed page + if (dlg) { + page_num = dlg->getSelectedPage(); + if (page_num < 1) + page_num = 1; + if (page_num > tmpSVGOutput.size()) + page_num = tmpSVGOutput.size(); + } + } + + SPDocument * doc = SPDocument::createNewDocFromMem(tmpSVGOutput[page_num-1].cstr(), strlen(tmpSVGOutput[page_num-1].cstr()), TRUE); + + if (doc && !doc->getRoot()->viewBox_set) { + // Scales the document to account for 72dpi scaling in librevenge(<=0.0.4) + doc->setWidth(Inkscape::Util::Quantity(doc->getWidth().quantity, "pt"), false); + doc->setHeight(Inkscape::Util::Quantity(doc->getHeight().quantity, "pt"), false); + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value("pt"), doc->getHeight().value("pt"))); + } + return doc; +} + +#include "clear-n_.h" + +void CdrInput::init() +{ + // clang-format off + /* CDR */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Corel DRAW Input") "</name>\n" + "<id>org.inkscape.input.cdr</id>\n" + "<input>\n" + "<extension>.cdr</extension>\n" + "<mimetype>image/x-xcdr</mimetype>\n" + "<filetypename>" N_("Corel DRAW 7-X4 files (*.cdr)") "</filetypename>\n" + "<filetypetooltip>" N_("Open files saved in Corel DRAW 7-X4") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new CdrInput()); + + /* CDT */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Corel DRAW templates input") "</name>\n" + "<id>org.inkscape.input.cdt</id>\n" + "<input>\n" + "<extension>.cdt</extension>\n" + "<mimetype>application/x-xcdt</mimetype>\n" + "<filetypename>" N_("Corel DRAW 7-13 template files (*.cdt)") "</filetypename>\n" + "<filetypetooltip>" N_("Open files saved in Corel DRAW 7-13") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new CdrInput()); + + /* CCX */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Corel DRAW Compressed Exchange files input") "</name>\n" + "<id>org.inkscape.input.ccx</id>\n" + "<input>\n" + "<extension>.ccx</extension>\n" + "<mimetype>application/x-xccx</mimetype>\n" + "<filetypename>" N_("Corel DRAW Compressed Exchange files (*.ccx)") "</filetypename>\n" + "<filetypetooltip>" N_("Open compressed exchange files saved in Corel DRAW") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new CdrInput()); + + /* CMX */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Corel DRAW Presentation Exchange files input") "</name>\n" + "<id>org.inkscape.input.cmx</id>\n" + "<input>\n" + "<extension>.cmx</extension>\n" + "<mimetype>application/x-xcmx</mimetype>\n" + "<filetypename>" N_("Corel DRAW Presentation Exchange files (*.cmx)") "</filetypename>\n" + "<filetypetooltip>" N_("Open presentation exchange files saved in Corel DRAW") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new CdrInput()); + // clang-format on + + return; + +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBCDR */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/cdr-input.h b/src/extension/internal/cdr-input.h new file mode 100644 index 0000000..546151f --- /dev/null +++ b/src/extension/internal/cdr-input.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_CDROUTPUT_H__ +#define __EXTENSION_INTERNAL_CDROUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBCDR + +#include <gtkmm/dialog.h> + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class CdrInput : public Inkscape::Extension::Implementation::Implementation { + CdrInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBCDR */ +#endif /* __EXTENSION_INTERNAL_CDROUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/clear-n_.h b/src/extension/internal/clear-n_.h new file mode 100644 index 0000000..ecd8eaa --- /dev/null +++ b/src/extension/internal/clear-n_.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file clear-n_.h + + A way to clear the N_ macro, which is defined as an inline function. + Unfortunately, this makes it so it is hard to use in static strings + where you only want to translate a small part. Including this + turns it back into a a macro. +*/ +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef N_ +#undef N_ +#endif +#define N_(x) x + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/emf-inout.cpp b/src/extension/internal/emf-inout.cpp new file mode 100644 index 0000000..71a5869 --- /dev/null +++ b/src/extension/internal/emf-inout.cpp @@ -0,0 +1,3688 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows-only Enhanced Metafile input and output. + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * David Mathog + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include <cstdio> +#include <cstdlib> +#include <cstdint> +#include <3rdparty/libuemf/symbol_convert.h> + +#include "emf-inout.h" + +#include "clear-n_.h" +#include "display/drawing-item.h" +#include "display/drawing.h" +#include "document.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "extension/print.h" +#include "extension/system.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "path/path-boolop.h" +#include "print.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "util/units.h" + +#include "emf-print.h" + +#define PRINT_EMF "org.inkscape.print.emf" + +#ifndef U_PS_JOIN_MASK +#define U_PS_JOIN_MASK (U_PS_JOIN_BEVEL|U_PS_JOIN_MITER|U_PS_JOIN_ROUND) +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + +static uint32_t ICMmode = 0; // not used yet, but code to read it from EMF implemented +static uint32_t BLTmode = 0; +float faraway = 10000000; // used in "exclude" clips, hopefully well outside any real drawing! + +Emf::Emf () // The null constructor +{ + return; +} + + +Emf::~Emf () //The destructor +{ + return; +} + + +bool +Emf::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(PRINT_EMF)) + return FALSE; + return TRUE; +} + + +void +Emf::print_document_to_file(SPDocument *doc, const gchar *filename) +{ + Inkscape::Extension::Print *mod; + SPPrintContext context; + const gchar *oldconst; + gchar *oldoutput; + unsigned int ret; + + doc->ensureUpToDate(); + + mod = Inkscape::Extension::get_print(PRINT_EMF); + oldconst = mod->get_param_string("destination"); + oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + +/* Start */ + context.module = mod; + /* fixme: This has to go into module constructor somehow */ + /* Create new arena */ + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = mod->base->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + /* Print document */ + ret = mod->begin(doc); + if (ret) { + g_free(oldoutput); + throw Inkscape::Extension::Output::save_failed(); + } + mod->base->invoke_print(&context); + (void) mod->finish(); + /* Release arena */ + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // deleted by invoke_hide +/* end */ + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); + + return; +} + + +void +Emf::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + + ext = Inkscape::Extension::db.get(PRINT_EMF); + if (ext == nullptr) + return; + + bool new_val = mod->get_param_bool("textToPath"); + bool new_FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); // character position bug + // reserve FixPPT2 for opacity bug. Currently EMF does not export opacity values + bool new_FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); // dashed line bug + bool new_FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); // gradient bug + bool new_FixPPTLinGrad = mod->get_param_bool("FixPPTLinGrad"); // allow native rectangular linear gradient + bool new_FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); // force all patterns as standard EMF hatch + bool new_FixImageRot = mod->get_param_bool("FixImageRot"); // remove rotations on images + + TableGen( //possibly regenerate the unicode-convert tables + mod->get_param_bool("TnrToSymbol"), + mod->get_param_bool("TnrToWingdings"), + mod->get_param_bool("TnrToZapfDingbats"), + mod->get_param_bool("UsePUA") + ); + + ext->set_param_bool("FixPPTCharPos",new_FixPPTCharPos); // Remember to add any new ones to PrintEmf::init or a mysterious failure will result! + ext->set_param_bool("FixPPTDashLine",new_FixPPTDashLine); + ext->set_param_bool("FixPPTGrad2Polys",new_FixPPTGrad2Polys); + ext->set_param_bool("FixPPTLinGrad",new_FixPPTLinGrad); + ext->set_param_bool("FixPPTPatternAsHatch",new_FixPPTPatternAsHatch); + ext->set_param_bool("FixImageRot",new_FixImageRot); + ext->set_param_bool("textToPath", new_val); + + // ensure usage of dot as decimal separator in scanf/printf functions (independently of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + print_document_to_file(doc, filename); + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return; +} + + +/* given the transformation matrix from worldTransform return the scale in the matrix part. Assumes that the + matrix is not used to skew, invert, or make another distorting transformation. */ +double Emf::current_scale(PEMF_CALLBACK_DATA d){ + double scale = + d->dc[d->level].worldTransform.eM11 * d->dc[d->level].worldTransform.eM22 - + d->dc[d->level].worldTransform.eM12 * d->dc[d->level].worldTransform.eM21; + if(scale <= 0.0)scale=1.0; /* something is dreadfully wrong with the matrix, but do not crash over it */ + scale=sqrt(scale); + return(scale); +} + +/* given the transformation matrix from worldTransform and the current x,y position in inkscape coordinates, + generate an SVG transform that gives the same amount of rotation, no scaling, and maps x,y back onto x,y. This is used for + rotating objects when the location of at least one point in that object is known. Returns: + "matrix(a,b,c,d,e,f)" (WITH the double quotes) +*/ +std::string Emf::current_matrix(PEMF_CALLBACK_DATA d, double x, double y, int useoffset){ + SVGOStringStream cxform; + double scale = current_scale(d); + cxform << "\"matrix("; + cxform << d->dc[d->level].worldTransform.eM11/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM12/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM21/scale; cxform << ","; + cxform << d->dc[d->level].worldTransform.eM22/scale; cxform << ","; + if(useoffset){ + /* for the "new" coordinates drop the worldtransform translations, not used here */ + double newx = x * d->dc[d->level].worldTransform.eM11/scale + y * d->dc[d->level].worldTransform.eM21/scale; + double newy = x * d->dc[d->level].worldTransform.eM12/scale + y * d->dc[d->level].worldTransform.eM22/scale; + cxform << x - newx; cxform << ","; + cxform << y - newy; + } + else { + cxform << "0,0"; + } + cxform << ")\""; + return(cxform.str()); +} + +/* given the transformation matrix from worldTransform return the rotation angle in radians. + counter clockwise from the x axis. */ +double Emf::current_rotation(PEMF_CALLBACK_DATA d){ + return -std::atan2(d->dc[d->level].worldTransform.eM12, d->dc[d->level].worldTransform.eM11); +} + +/* Add another 100 blank slots to the hatches array. +*/ +void Emf::enlarge_hatches(PEMF_CALLBACK_DATA d){ + d->hatches.size += 100; + d->hatches.strings = (char **) realloc(d->hatches.strings,d->hatches.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_hatches(PEMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; i<d->hatches.count; i++){ + if(strcmp(test,d->hatches.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a hatch. If a matching hatch already exists nothing happens. If one + does not exist it is added to the hatches list and also entered into <defs>. + This is also used to add the path part of the hatches, which they reference with a xlink:href +*/ +uint32_t Emf::add_hatch(PEMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor){ + char hatchname[64]; // big enough + char hpathname[64]; // big enough + char hbkname[64]; // big enough + char tmpcolor[8]; + char bkcolor[8]; + uint32_t idx; + + switch(hatchType){ + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].textColor)); + break; + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + break; + default: + sprintf(tmpcolor,"%6.6X",sethexcolor(hatchColor)); + break; + } + + /* For both bkMode types set the PATH + FOREGROUND COLOR for the indicated standard hatch. + This will be used late to compose, or recompose the transparent or opaque final hatch.*/ + + std::string refpath; // used to reference later the path pieces which are about to be created + sprintf(hpathname,"EMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hpathname); + auto & defs = d->defs; + if(!idx){ // add path/color if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hpathname); + + defs += "\n"; + switch(hatchType){ + case U_HS_HORIZONTAL: + defs += " <path id=\""; + defs += hpathname; + defs += "\" d=\"M 0 0 6 0\" style=\"fill:none;stroke:#"; + defs += tmpcolor; + defs += "\" />\n"; + break; + case U_HS_VERTICAL: + defs += " <path id=\""; + defs += hpathname; + defs += "\" d=\"M 0 0 0 6\" style=\"fill:none;stroke:#"; + defs += tmpcolor; + defs += "\" />\n"; + break; + case U_HS_FDIAGONAL: + defs += " <line id=\"sub"; + defs += hpathname; + defs += "\" x1=\"-1\" y1=\"-1\" x2=\"7\" y2=\"7\" stroke=\"#"; + defs += tmpcolor; + defs += "\"/>\n"; + break; + case U_HS_BDIAGONAL: + defs += " <line id=\"sub"; + defs += hpathname; + defs += "\" x1=\"-1\" y1=\"7\" x2=\"7\" y2=\"-1\" stroke=\"#"; + defs += tmpcolor; + defs += "\"/>\n"; + break; + case U_HS_CROSS: + defs += " <path id=\""; + defs += hpathname; + defs += "\" d=\"M 0 0 6 0 M 0 0 0 6\" style=\"fill:none;stroke:#"; + defs += tmpcolor; + defs += "\" />\n"; + break; + case U_HS_DIAGCROSS: + defs += " <line id=\"subfd"; + defs += hpathname; + defs += "\" x1=\"-1\" y1=\"-1\" x2=\"7\" y2=\"7\" stroke=\"#"; + defs += tmpcolor; + defs += "\"/>\n"; + defs += " <line id=\"subbd"; + defs += hpathname; + defs += "\" x1=\"-1\" y1=\"7\" x2=\"7\" y2=\"-1\" stroke=\"#"; + defs += tmpcolor; + defs += "\"/>\n"; + break; + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + defs += " <path id=\""; + defs += hpathname; + defs += "\" d=\"M 0 0 6 0 6 6 0 6 z\" style=\"fill:#"; + defs += tmpcolor; + defs += ";stroke:none"; + defs += "\" />\n"; + break; + } + } + + // References to paths possibly just created above. These will be used in the actual patterns. + switch(hatchType){ + case U_HS_HORIZONTAL: + case U_HS_VERTICAL: + case U_HS_CROSS: + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + refpath += " <use xlink:href=\"#"; + refpath += hpathname; + refpath += "\" />\n"; + break; + case U_HS_FDIAGONAL: + case U_HS_BDIAGONAL: + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\" />\n"; + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\" />\n"; + break; + case U_HS_DIAGCROSS: + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\"/>\n"; + break; + } + + if(d->dc[d->level].bkMode == U_TRANSPARENT || hatchType >= U_HS_SOLIDCLR){ + sprintf(hatchname,"EMFhatch%d_%s",hatchType,tmpcolor); + sprintf(hpathname,"EMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " <pattern id=\""; + defs += hatchname; + defs += "\" xlink:href=\"#EMFhbasepattern\">\n"; + defs += refpath; + defs += " </pattern>\n"; + idx = d->hatches.count; + } + } + else { // bkMode==U_OPAQUE + /* Set up an object in the defs for this background, if there is not one already there */ + sprintf(bkcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + sprintf(hbkname,"EMFhbkclr_%s",bkcolor); + idx = in_hatches(d,hbkname); + if(!idx){ // add path/color if not already present. Hatchtype is not needed in the name. + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hbkname); + + defs += "\n"; + defs += " <rect id=\""; + defs += hbkname; + defs += "\" x=\"0\" y=\"0\" width=\"6\" height=\"6\" fill=\"#"; + defs += bkcolor; + defs += "\" />\n"; + } + + // this is the pattern, its name will show up in Inkscape's pattern selector + sprintf(hatchname,"EMFhatch%d_%s_%s",hatchType,tmpcolor,bkcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " <pattern id=\""; + defs += hatchname; + defs += "\" xlink:href=\"#EMFhbasepattern\">\n"; + defs += " <use xlink:href=\"#"; + defs += hbkname; + defs += "\" />\n"; + defs += refpath; + defs += " </pattern>\n"; + idx = d->hatches.count; + } + } + return(idx-1); +} + +/* Add another 100 blank slots to the images array. +*/ +void Emf::enlarge_images(PEMF_CALLBACK_DATA d){ + d->images.size += 100; + d->images.strings = (char **) realloc(d->images.strings,d->images.size * sizeof(char *)); +} + +/* See if the image string is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_images(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; i<d->images.count; i++){ + if(strcmp(test,d->images.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add an image. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into <defs>. + + U_EMRCREATEMONOBRUSH records only work when the bitmap is monochrome. If we hit one that isn't + set idx to 2^32-1 and let the caller handle it. +*/ +uint32_t Emf::add_image(PEMF_CALLBACK_DATA d, void *pEmr, uint32_t cbBits, uint32_t cbBmi, + uint32_t iUsage, uint32_t offBits, uint32_t offBmi){ + + uint32_t idx; + char imagename[64]; // big enough + char imrotname[64]; // big enough + char xywh[64]; // big enough + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + U_RGBQUAD ct2[2]; + uint32_t width, height, colortype, numCt, invert; // if needed these values will be set in get_DIB_params + if(cbBits && cbBmi && (iUsage == U_DIB_RGB_COLORS)){ + // next call returns pointers and values, but allocates no memory + dibparams = get_DIB_params((const char *)pEmr, offBits, offBmi, &px, (const U_RGBQUAD **) &ct, + &numCt, &width, &height, &colortype, &invert); + if(dibparams ==U_BI_RGB){ + // U_EMRCREATEMONOBRUSH uses text/bk colors instead of what is in the color map. + if(((PU_EMR)pEmr)->iType == U_EMR_CREATEMONOBRUSH){ + if(numCt==2){ + ct2[0] = U_RGB2BGR(d->dc[d->level].textColor); + ct2[1] = U_RGB2BGR(d->dc[d->level].bkColor); + ct = &ct2[0]; + } + else { // This record is invalid, nothing more to do here, let caller handle it + return(U_EMR_INVALID); + } + } + + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array in record + height, // Height of pixel array in record + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG || dibparams==U_BI_PNG){ // image was binary png or jpg in source file + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + + sprintf(imagename,"EMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " <image id=\""; + defs += imagename; + defs += "\"\n "; + defs += xywh; + defs += "\n"; + if(dibparams == U_BI_JPEG){ defs += " xlink:href=\"data:image/jpeg;base64,"; } + else { defs += " xlink:href=\"data:image/png;base64,"; } + defs += base64String; + defs += "\"\n"; + defs += " preserveAspectRatio=\"none\"\n"; + defs += " />\n"; + + + defs += "\n"; + defs += " <pattern id=\""; + defs += imagename; + defs += "_ref\"\n "; + defs += xywh; + defs += "\n patternUnits=\"userSpaceOnUse\""; + defs += " >\n"; + defs += " <use id=\""; + defs += imagename; + defs += "_ign\" "; + defs += " xlink:href=\"#"; + defs += imagename; + defs += "\" />\n"; + defs += " "; + defs += " </pattern>\n"; + } + g_free(base64String);//wait until this point to free because it might be a duplicate image + + /* image allows the inner image to be rotated nicely, load this one second only if needed + imagename retained from above + Here comes a dreadful hack. How do we determine if this rotation of the base image has already + been loaded? The image names contain no identifying information, they are just numbered sequentially. + So the rotated name is EMFrotimage###_XXXXXX, where ### is the number of the referred to image, and + XXXX is the rotation in radians x 1000000 and truncated. That is then stored in BASE64 as the "image". + The corresponding SVG generated though is not for an image, but a reference to an image. + The name of the pattern MUST still be EMFimage###_ref or output_style() will not be able to use it. + */ + if(current_rotation(d) >= 0.00001 || current_rotation(d) <= -0.00001){ /* some rotation, allow a little rounding error around 0 degrees */ + int tangle = round(current_rotation(d)*1000000.0); + sprintf(imrotname,"EMFrotimage%d_%d",idx-1,tangle); + base64String = g_base64_encode((guchar*) imrotname, strlen(imrotname) ); + idx = in_images(d, (char *) base64String); // scan for this "image" + if(!idx){ + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + sprintf(imrotname,"EMFimage%d",idx++); + + defs += "\n"; + defs += " <pattern\n"; + defs += " id=\""; + defs += imrotname; + defs += "_ref\"\n"; + defs += " xlink:href=\"#"; + defs += imagename; + defs += "_ref\"\n"; + defs += " patternTransform="; + defs += current_matrix(d, 0.0, 0.0, 0); //j use offset 0,0 + defs += " />\n"; + } + g_free(base64String); + } + + return(idx-1); +} + +/* Add another 100 blank slots to the gradients array. +*/ +void Emf::enlarge_gradients(PEMF_CALLBACK_DATA d){ + d->gradients.size += 100; + d->gradients.strings = (char **) realloc(d->gradients.strings,d->gradients.size * sizeof(char *)); +} + +/* See if the gradient name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_gradients(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; i<d->gradients.count; i++){ + if(strcmp(test,d->gradients.strings[i])==0)return(i+1); + } + return(0); +} + +U_COLORREF trivertex_to_colorref(U_TRIVERTEX tv){ + U_COLORREF uc; + uc.Red = tv.Red >> 8; + uc.Green = tv.Green >> 8; + uc.Blue = tv.Blue >> 8; + uc.Reserved = tv.Alpha >> 8; // Not used + return(uc); +} + +/* (Conditionally) add a gradient. If a matching gradient already exists nothing happens. If one + does not exist it is added to the gradients list and also entered into <defs>. + Only call this with H or V gradient, not a triangle. +*/ +uint32_t Emf::add_gradient(PEMF_CALLBACK_DATA d, uint32_t gradientType, U_TRIVERTEX tv1, U_TRIVERTEX tv2){ + char hgradname[64]; // big enough + char tmpcolor1[8]; + char tmpcolor2[8]; + char gradc; + uint32_t idx; + std::string x2,y2; + + U_COLORREF gradientColor1 = trivertex_to_colorref(tv1); + U_COLORREF gradientColor2 = trivertex_to_colorref(tv2); + + + sprintf(tmpcolor1,"%6.6X",sethexcolor(gradientColor1)); + sprintf(tmpcolor2,"%6.6X",sethexcolor(gradientColor2)); + switch(gradientType){ + case U_GRADIENT_FILL_RECT_H: + gradc='H'; + x2="100"; + y2="0"; + break; + case U_GRADIENT_FILL_RECT_V: + gradc='V'; + x2="0"; + y2="100"; + break; + default: // this should never happen, but fill these in to avoid compiler warnings + gradc='!'; + x2="0"; + y2="0"; + break; + } + + /* Even though the gradient was defined as Horizontal or Vertical if the rectangle is rotated it needs to + be at some other alignment, and that needs gradienttransform. Set the name using the same sort of hack + as for add_image. + */ + int tangle = round(current_rotation(d)*1000000.0); + sprintf(hgradname,"LinGrd%c_%s_%s_%d",gradc,tmpcolor1,tmpcolor2,tangle); + + idx = in_gradients(d,hgradname); + if(!idx){ // gradient does not yet exist + if(d->gradients.count == d->gradients.size){ enlarge_gradients(d); } + d->gradients.strings[d->gradients.count++]=strdup(hgradname); + idx = d->gradients.count; + SVGOStringStream stmp; + stmp << " <linearGradient id=\""; + stmp << hgradname; + stmp << "\" x1=\""; + stmp << pix_to_x_point(d, tv1.x , tv1.y); + stmp << "\" y1=\""; + stmp << pix_to_y_point(d, tv1.x , tv1.y); + stmp << "\" x2=\""; + if(gradc=='H'){ // UR corner + stmp << pix_to_x_point(d, tv2.x , tv1.y); + stmp << "\" y2=\""; + stmp << pix_to_y_point(d, tv2.x , tv1.y); + } + else { // LL corner + stmp << pix_to_x_point(d, tv1.x , tv2.y); + stmp << "\" y2=\""; + stmp << pix_to_y_point(d, tv1.x , tv2.y); + } + stmp << "\" gradientTransform=\"(1,0,0,1,0,0)\""; + stmp << " gradientUnits=\"userSpaceOnUse\"\n"; + stmp << ">\n"; + stmp << " <stop offset=\"0\" style=\"stop-color:#"; + stmp << tmpcolor1; + stmp << ";stop-opacity:1\" />\n"; + stmp << " <stop offset=\"1\" style=\"stop-color:#"; + stmp << tmpcolor2; + stmp << ";stop-opacity:1\" />\n"; + stmp << " </linearGradient>\n"; + d->defs += stmp.str().c_str(); + } + + return(idx-1); +} + +/* Add another 100 blank slots to the clips array. +*/ +void Emf::enlarge_clips(PEMF_CALLBACK_DATA d){ + d->clips.size += 100; + d->clips.strings = (char **) realloc(d->clips.strings,d->clips.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Emf::in_clips(PEMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; i<d->clips.count; i++){ + if(strcmp(test,d->clips.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a clip. + If a matching clip already exists nothing happens + If one does exist it is added to the clips list, entered into <defs>. +*/ +void Emf::add_clips(PEMF_CALLBACK_DATA d, const char *clippath, unsigned int logic){ + int op = combine_ops_to_livarot(logic); + Geom::PathVector combined_vect; + std::string combined; + if (op >= 0 && d->dc[d->level].clip_id) { + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector old_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + Geom::PathVector new_vect = sp_svg_read_pathv(clippath); + combined_vect = sp_pathvector_boolop(new_vect, old_vect, (bool_op) op , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + combined = sp_svg_write_path(combined_vect); + } + else { + combined = clippath; // COPY operation, erases everything and starts a new one + } + + uint32_t idx = in_clips(d, combined.c_str()); + if(!idx){ // add clip if not already present + if(d->clips.count == d->clips.size){ enlarge_clips(d); } + d->clips.strings[d->clips.count++] = strdup(combined.c_str()); + d->dc[d->level].clip_id = d->clips.count; // one more than the slot where it is actually stored + SVGOStringStream tmp_clippath; + tmp_clippath << "\n<clipPath"; + tmp_clippath << "\n\tclipPathUnits=\"userSpaceOnUse\" "; + tmp_clippath << "\n\tid=\"clipEmfPath" << d->dc[d->level].clip_id << "\""; + tmp_clippath << " >"; + tmp_clippath << "\n\t<path d=\""; + tmp_clippath << combined; + tmp_clippath << "\""; + tmp_clippath << "\n\t/>"; + tmp_clippath << "\n</clipPath>"; + d->outdef += tmp_clippath.str().c_str(); + } + else { + d->dc[d->level].clip_id = idx; + } +} + + + +void +Emf::output_style(PEMF_CALLBACK_DATA d, int iType) +{ +// SVGOStringStream tmp_id; + SVGOStringStream tmp_style; + char tmp[1024] = {0}; + + float fill_rgb[3]; + d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); + float stroke_rgb[3]; + d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + + // for U_EMR_BITBLT with no image, try to approximate some of these operations/ + // Assume src color is "white" + if(d->dwRop3){ + switch(d->dwRop3){ + case U_PATINVERT: // invert pattern + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + break; + case U_SRCINVERT: // treat all of these as black + case U_DSTINVERT: + case U_BLACKNESS: + case U_SRCERASE: + case U_NOTSRCCOPY: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + break; + case U_SRCCOPY: // treat all of these as white + case U_NOTSRCERASE: + case U_WHITENESS: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + break; + case U_SRCPAINT: // use the existing color + case U_SRCAND: + case U_MERGECOPY: + case U_MERGEPAINT: + case U_PATPAINT: + case U_PATCOPY: + default: + break; + } + d->dwRop3 = 0; // might as well reset it here, it must be set for each BITBLT + } + + // Implement some of these, the ones where the original screen color does not matter. + // The options that merge screen and pen colors cannot be done correctly because we + // have no way of knowing what color is already on the screen. For those just pass the + // pen color through. + switch(d->dwRop2){ + case U_R2_BLACK: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + break; + case U_R2_NOTMERGEPEN: + case U_R2_MASKNOTPEN: + break; + case U_R2_NOTCOPYPEN: + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + stroke_rgb[0] = 1.0 - stroke_rgb[0]; + stroke_rgb[1] = 1.0 - stroke_rgb[1]; + stroke_rgb[2] = 1.0 - stroke_rgb[2]; + break; + case U_R2_MASKPENNOT: + case U_R2_NOT: + case U_R2_XORPEN: + case U_R2_NOTMASKPEN: + case U_R2_NOTXORPEN: + case U_R2_NOP: + case U_R2_MERGENOTPEN: + case U_R2_COPYPEN: + case U_R2_MASKPEN: + case U_R2_MERGEPENNOT: + case U_R2_MERGEPEN: + break; + case U_R2_WHITE: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + break; + default: + break; + } + + +// tmp_id << "\n\tid=\"" << (d->id++) << "\""; +// d->outsvg += tmp_id.str().c_str(); + d->outsvg += "\n\tstyle=\""; + if (iType == U_EMR_STROKEPATH || !d->dc[d->level].fill_set) { + tmp_style << "fill:none;"; + } else { + switch(d->dc[d->level].fill_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "fill:url(#%s); ",d->hatches.strings[d->dc[d->level].fill_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "fill:url(#EMFimage%d_ref); ",d->dc[d->level].fill_idx); + tmp_style << tmp; + break; + case DRAW_LINEAR_GRADIENT: + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "fill:#%02x%02x%02x;", + SP_COLOR_F_TO_U(fill_rgb[0]), + SP_COLOR_F_TO_U(fill_rgb[1]), + SP_COLOR_F_TO_U(fill_rgb[2]) + ); + tmp_style << tmp; + break; + } + snprintf( + tmp, 1023, + "fill-rule:%s;", + (d->dc[d->level].style.fill_rule.value == SP_WIND_RULE_NONZERO ? "evenodd" : "nonzero") + ); + tmp_style << tmp; + tmp_style << "fill-opacity:1;"; + + // if the stroke is the same as the fill, and the right size not to change the end size of the object, do not do it separately + if( + (d->dc[d->level].fill_set ) && + (d->dc[d->level].stroke_set ) && + (d->dc[d->level].style.stroke_width.value == 1 ) && + (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && + ( + (d->dc[d->level].fill_mode != DRAW_PAINT) || + ( + (fill_rgb[0]==stroke_rgb[0]) && + (fill_rgb[1]==stroke_rgb[1]) && + (fill_rgb[2]==stroke_rgb[2]) + ) + ) + ){ + d->dc[d->level].stroke_set = false; + } + } + + if (iType == U_EMR_FILLPATH || !d->dc[d->level].stroke_set) { + tmp_style << "stroke:none;"; + } else { + switch(d->dc[d->level].stroke_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "stroke:url(#%s); ",d->hatches.strings[d->dc[d->level].stroke_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "stroke:url(#EMFimage%d_ref); ",d->dc[d->level].stroke_idx); + tmp_style << tmp; + break; + case DRAW_LINEAR_GRADIENT: + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "stroke:#%02x%02x%02x;", + SP_COLOR_F_TO_U(stroke_rgb[0]), + SP_COLOR_F_TO_U(stroke_rgb[1]), + SP_COLOR_F_TO_U(stroke_rgb[2]) + ); + tmp_style << tmp; + break; + } + tmp_style << "stroke-width:" << + MAX( 0.001, d->dc[d->level].style.stroke_width.value ) << "px;"; + + tmp_style << "stroke-linecap:" << + ( + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_BUTT ? "butt" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_ROUND ? "round" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_SQUARE ? "square" : + "unknown" + ) << ";"; + + tmp_style << "stroke-linejoin:" << + ( + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_MITER ? "miter" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_ROUND ? "round" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_BEVEL ? "bevel" : + "unknown" + ) << ";"; + + // Set miter limit if known, even if it is not needed immediately (not miter) + tmp_style << "stroke-miterlimit:" << + MAX( 2.0, d->dc[d->level].style.stroke_miterlimit.value ) << ";"; + + if (d->dc[d->level].style.stroke_dasharray.set && + !d->dc[d->level].style.stroke_dasharray.values.empty() ) + { + tmp_style << "stroke-dasharray:"; + for (unsigned i=0; i<d->dc[d->level].style.stroke_dasharray.values.size(); i++) { + if (i) + tmp_style << ","; + tmp_style << d->dc[d->level].style.stroke_dasharray.values[i].value; + } + tmp_style << ";"; + tmp_style << "stroke-dashoffset:0;"; + } else { + tmp_style << "stroke-dasharray:none;"; + } + tmp_style << "stroke-opacity:1;"; + } + tmp_style << "\" "; + if (d->dc[d->level].clip_id) + tmp_style << "\n\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\" "; + + d->outsvg += tmp_style.str().c_str(); +} + + +double +Emf::_pix_x_to_point(PEMF_CALLBACK_DATA d, double px) +{ + double scale = (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0); + double tmp; + tmp = ((((double) (px - d->dc[d->level].winorg.x))*scale) + d->dc[d->level].vieworg.x) * d->D2PscaleX; + tmp -= d->ulCornerOutX; //The EMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + +double +Emf::_pix_y_to_point(PEMF_CALLBACK_DATA d, double py) +{ + double scale = (d->dc[d->level].ScaleInY ? d->dc[d->level].ScaleInY : 1.0); + double tmp; + tmp = ((((double) (py - d->dc[d->level].winorg.y))*scale) * d->E2IdirY + d->dc[d->level].vieworg.y) * d->D2PscaleY; + tmp -= d->ulCornerOutY; //The EMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + + +double +Emf::pix_to_x_point(PEMF_CALLBACK_DATA d, double px, double py) +{ + double wpx = px * d->dc[d->level].worldTransform.eM11 + py * d->dc[d->level].worldTransform.eM21 + d->dc[d->level].worldTransform.eDx; + double x = _pix_x_to_point(d, wpx); + + return x; +} + +double +Emf::pix_to_y_point(PEMF_CALLBACK_DATA d, double px, double py) +{ + + double wpy = px * d->dc[d->level].worldTransform.eM12 + py * d->dc[d->level].worldTransform.eM22 + d->dc[d->level].worldTransform.eDy; + double y = _pix_y_to_point(d, wpy); + + return y; + +} + +double +Emf::pix_to_abs_size(PEMF_CALLBACK_DATA d, double px) +{ + double ppx = fabs(px * (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0) * d->D2PscaleX * current_scale(d)); + return ppx; +} + +/* snaps coordinate pairs made up of values near +/-faraway, +/-faraway to exactly faraway. + This eliminates coordinate drift on repeated clipping cycles which use exclude. + It should not affect internals of normal drawings because the value of faraway is so large. +*/ +void +Emf::snap_to_faraway_pair(double *x, double *y) +{ + if((std::abs(std::abs(*x) - faraway)/faraway <= 1e-4) && (std::abs(std::abs(*y) - faraway)/faraway <= 1e-4)){ + *x = (*x > 0 ? faraway : -faraway); + *y = (*y > 0 ? faraway : -faraway); + } +} + +/* returns "x,y" (without the quotes) in inkscape coordinates for a pair of EMF x,y coordinates. + Since exclude clip can go through here, it calls snap_to_faraway_pair for numerical stability. +*/ +std::string Emf::pix_to_xy(PEMF_CALLBACK_DATA d, double x, double y){ + SVGOStringStream cxform; + double tx = pix_to_x_point(d,x,y); + double ty = pix_to_y_point(d,x,y); + snap_to_faraway_pair(&tx,&ty); + cxform << tx; + cxform << ","; + cxform << ty; + return(cxform.str()); +} + + +void +Emf::select_pen(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMRCREATEPEN pEmr = nullptr; + + if (index >= 0 && index < d->n_obj){ + pEmr = (PU_EMRCREATEPEN) d->emf_obj[index].lpEMFR; + } + + if (!pEmr){ return; } + + switch (pEmr->lopn.lopnStyle & U_PS_STYLE_MASK) { + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + SPILength spilength(1.f); + int penstyle = (pEmr->lopn.lopnStyle & U_PS_STYLE_MASK); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + + case U_PS_SOLID: + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (pEmr->lopn.lopnStyle & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + + switch (pEmr->lopn.lopnStyle & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + + d->dc[d->level].stroke_set = true; + + if (pEmr->lopn.lopnStyle == U_PS_NULL) { + d->dc[d->level].style.stroke_width.value = 0; + d->dc[d->level].stroke_set = false; + } else if (pEmr->lopn.lopnWidth.x) { + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, pEmr->lopn.lopnWidth.x ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + //d->dc[d->level].style.stroke_width.value = 1.0; + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } + + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lopn.lopnColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lopn.lopnColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lopn.lopnColor) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); +} + + +void +Emf::select_extpen(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMREXTCREATEPEN pEmr = nullptr; + + if (index >= 0 && index < d->n_obj) + pEmr = (PU_EMREXTCREATEPEN) d->emf_obj[index].lpEMFR; + + if (!pEmr) + return; + + switch (pEmr->elp.elpPenStyle & U_PS_STYLE_MASK) { + case U_PS_USERSTYLE: + { + if (pEmr->elp.elpNumEntries) { + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + for (unsigned int i=0; i<pEmr->elp.elpNumEntries; i++) { + double dash_length = pix_to_abs_size( d, pEmr->elp.elpStyleEntry[i] ); + d->dc[d->level].style.stroke_dasharray.values.emplace_back(dash_length); + } + d->dc[d->level].style.stroke_dasharray.set = true; + } else { + d->dc[d->level].style.stroke_dasharray.set = false; + } + break; + } + + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + int penstyle = (pEmr->elp.elpPenStyle & U_PS_STYLE_MASK); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + SPILength spilength; + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(2); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + case U_PS_SOLID: +/* includes these for now, some should maybe not be in here + case U_PS_NULL: + case U_PS_INSIDEFRAME: + case U_PS_ALTERNATE: + case U_PS_STYLE_MASK: +*/ + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (pEmr->elp.elpPenStyle & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; + break; + } + case U_PS_ENDCAP_SQUARE: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; + break; + } + case U_PS_ENDCAP_FLAT: + default: + { + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; + break; + } + } + + switch (pEmr->elp.elpPenStyle & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; + break; + } + case U_PS_JOIN_MITER: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; + break; + } + case U_PS_JOIN_ROUND: + default: + { + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; + break; + } + } + + d->dc[d->level].stroke_set = true; + + if (pEmr->elp.elpPenStyle == U_PS_NULL) { // draw nothing, but fill out all the values with something + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].style.stroke_width.value = 0; + d->dc[d->level].stroke_set = false; + d->dc[d->level].stroke_mode = DRAW_PAINT; + } + else { + if (pEmr->elp.elpWidth) { + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, pEmr->elp.elpWidth ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + //d->dc[d->level].style.stroke_width.value = 1.0; + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + d->dc[d->level].style.stroke_width.value = pen_width; + } + + if( pEmr->elp.elpBrushStyle == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->elp.elpColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->elp.elpColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->elp.elpColor) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + } + else if(pEmr->elp.elpBrushStyle == U_BS_HATCHED){ + d->dc[d->level].stroke_idx = add_hatch(d, pEmr->elp.elpHatch, pEmr->elp.elpColor); + d->dc[d->level].stroke_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].stroke_mode = DRAW_PATTERN; + d->dc[d->level].stroke_set = true; + } + else if(pEmr->elp.elpBrushStyle == U_BS_DIBPATTERN || pEmr->elp.elpBrushStyle == U_BS_DIBPATTERNPT){ + d->dc[d->level].stroke_idx = add_image(d, (void *)pEmr, pEmr->cbBits, pEmr->cbBmi, *(uint32_t *) &(pEmr->elp.elpColor), pEmr->offBits, pEmr->offBmi); + d->dc[d->level].stroke_mode = DRAW_IMAGE; + d->dc[d->level].stroke_set = true; + } + else { // U_BS_PATTERN and anything strange that falls in, stroke is solid textColor + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + } + } +} + + +void +Emf::select_brush(PEMF_CALLBACK_DATA d, int index) +{ + uint32_t tidx; + uint32_t iType; + + if (index >= 0 && index < d->n_obj){ + iType = ((PU_EMR) (d->emf_obj[index].lpEMFR))->iType; + if(iType == U_EMR_CREATEBRUSHINDIRECT){ + PU_EMRCREATEBRUSHINDIRECT pEmr = (PU_EMRCREATEBRUSHINDIRECT) d->emf_obj[index].lpEMFR; + if( pEmr->lb.lbStyle == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(pEmr->lb.lbColor) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(pEmr->lb.lbColor) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(pEmr->lb.lbColor) ); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + } + else if(pEmr->lb.lbStyle == U_BS_HATCHED){ + d->dc[d->level].fill_idx = add_hatch(d, pEmr->lb.lbHatch, pEmr->lb.lbColor); + d->dc[d->level].fill_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].fill_mode = DRAW_PATTERN; + d->dc[d->level].fill_set = true; + } + } + else if(iType == U_EMR_CREATEDIBPATTERNBRUSHPT || iType == U_EMR_CREATEMONOBRUSH){ + PU_EMRCREATEDIBPATTERNBRUSHPT pEmr = (PU_EMRCREATEDIBPATTERNBRUSHPT) d->emf_obj[index].lpEMFR; + tidx = add_image(d, (void *) pEmr, pEmr->cbBits, pEmr->cbBmi, pEmr->iUsage, pEmr->offBits, pEmr->offBmi); + if(tidx == U_EMR_INVALID){ // This happens if createmonobrush has a DIB that isn't monochrome + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + } +} + + +void +Emf::select_font(PEMF_CALLBACK_DATA d, int index) +{ + PU_EMREXTCREATEFONTINDIRECTW pEmr = nullptr; + + if (index >= 0 && index < d->n_obj) + pEmr = (PU_EMREXTCREATEFONTINDIRECTW) d->emf_obj[index].lpEMFR; + + if (!pEmr)return; + + + /* The logfont information always starts with a U_LOGFONT structure but the U_EMREXTCREATEFONTINDIRECTW + is defined as U_LOGFONT_PANOSE so it can handle one of those if that is actually present. Currently only logfont + is supported, and the remainder, it it really is a U_LOGFONT_PANOSE record, is ignored + */ + int cur_level = d->level; + d->level = d->emf_obj[index].level; + double font_size = pix_to_abs_size( d, pEmr->elfw.elfLogFont.lfHeight ); + /* snap the font_size to the nearest 1/32nd of a point. + (The size is converted from Pixels to points, snapped, and converted back.) + See the notes where d->D2Pscale[XY] are set for the reason why. + Typically this will set the font to the desired exact size. If some peculiar size + was intended this will, at worst, make it .03125 off, which is unlikely to be a problem. */ + font_size = round(20.0 * 0.8 * font_size)/(20.0 * 0.8); + d->level = cur_level; + d->dc[d->level].style.font_size.computed = font_size; + d->dc[d->level].style.font_weight.value = + pEmr->elfw.elfLogFont.lfWeight == U_FW_THIN ? SP_CSS_FONT_WEIGHT_100 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_200 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_LIGHT ? SP_CSS_FONT_WEIGHT_300 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_400 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_MEDIUM ? SP_CSS_FONT_WEIGHT_500 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_SEMIBOLD ? SP_CSS_FONT_WEIGHT_600 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_700 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_800 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_HEAVY ? SP_CSS_FONT_WEIGHT_900 : + pEmr->elfw.elfLogFont.lfWeight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_NORMAL : + pEmr->elfw.elfLogFont.lfWeight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_BOLD : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_LIGHTER : + pEmr->elfw.elfLogFont.lfWeight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_BOLDER : + SP_CSS_FONT_WEIGHT_NORMAL; + d->dc[d->level].style.font_style.value = (pEmr->elfw.elfLogFont.lfItalic ? SP_CSS_FONT_STYLE_ITALIC : SP_CSS_FONT_STYLE_NORMAL); + d->dc[d->level].style.text_decoration_line.underline = pEmr->elfw.elfLogFont.lfUnderline; + d->dc[d->level].style.text_decoration_line.line_through = pEmr->elfw.elfLogFont.lfStrikeOut; + d->dc[d->level].style.text_decoration_line.set = true; + d->dc[d->level].style.text_decoration_line.inherit = false; + // malformed EMF with empty filename may exist, ignore font change if encountered + char *ctmp = U_Utf16leToUtf8((uint16_t *) (pEmr->elfw.elfLogFont.lfFaceName), U_LF_FACESIZE, nullptr); + if(ctmp){ + if (d->dc[d->level].font_name){ free(d->dc[d->level].font_name); } + if(*ctmp){ + d->dc[d->level].font_name = ctmp; + } + else { // Malformed EMF might specify an empty font name + free(ctmp); + d->dc[d->level].font_name = strdup("Arial"); // Default font, EMF spec says device can pick whatever it wants + } + } + d->dc[d->level].style.baseline_shift.value = round((double)((pEmr->elfw.elfLogFont.lfEscapement + 3600) % 3600)) / 10.0; // use baseline_shift instead of text_transform to avoid overflow +} + +void +Emf::delete_object(PEMF_CALLBACK_DATA d, int index) +{ + if (index >= 0 && index < d->n_obj) { + d->emf_obj[index].type = 0; +// We are keeping a copy of the EMR rather than just a structure. Currently that is not necessary as the entire +// EMF is read in at once and is stored in a big malloc. However, in past versions it was handled +// record by record, and we might need to do that again at some point in the future if we start running into EMF +// files too big to fit into memory. + if (d->emf_obj[index].lpEMFR) + free(d->emf_obj[index].lpEMFR); + d->emf_obj[index].lpEMFR = nullptr; + } +} + + +void +Emf::insert_object(PEMF_CALLBACK_DATA d, int index, int type, PU_ENHMETARECORD pObj) +{ + if (index >= 0 && index < d->n_obj) { + delete_object(d, index); + d->emf_obj[index].type = type; + d->emf_obj[index].level = d->level; + d->emf_obj[index].lpEMFR = emr_dup((char *) pObj); + } +} + +/* Identify probable Adobe Illustrator produced EMF files, which do strange things with the scaling. + The few so far observed all had this format. +*/ +int Emf::AI_hack(PU_EMRHEADER pEmr){ + int ret=0; + char *ptr; + ptr = (char *)pEmr; + PU_EMRSETMAPMODE nEmr = (PU_EMRSETMAPMODE) (ptr + pEmr->emr.nSize); + char *string = nullptr; + if(pEmr->nDescription)string = U_Utf16leToUtf8((uint16_t *)((char *) pEmr + pEmr->offDescription), pEmr->nDescription, nullptr); + if(string){ + if((pEmr->nDescription >= 13) && + (0==strcmp("Adobe Systems",string)) && + (nEmr->emr.iType == U_EMR_SETMAPMODE) && + (nEmr->iMode == U_MM_ANISOTROPIC)){ ret=1; } + free(string); + } + return(ret); +} + +/** + \fn create a UTF-32LE buffer and fill it with UNICODE unknown character + \param count number of copies of the Unicode unknown character to fill with +*/ +uint32_t *Emf::unknown_chars(size_t count){ + uint32_t *res = (uint32_t *) malloc(sizeof(uint32_t) * (count + 1)); + if(!res)throw "Inkscape fatal memory allocation error - cannot continue"; + for(uint32_t i=0; i<count; i++){ res[i] = 0xFFFD; } + res[count]=0; + return res; +} + +/** + \fn store SVG for an image given the pixmap and various coordinate information + \param d + \param pEmr + \param dx (double) destination x in inkscape pixels + \param dy (double) destination y in inkscape pixels + \param dw (double) destination width in inkscape pixels + \param dh (double) destination height in inkscape pixels + \param sx (int) source x in src image pixels + \param sy (int) source y in src image pixels + \param iUsage + \param offBits + \param cbBits + \param offBmi + \param cbBmi +*/ +void Emf::common_image_extraction(PEMF_CALLBACK_DATA d, void *pEmr, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, + uint32_t iUsage, uint32_t offBits, uint32_t cbBits, uint32_t offBmi, uint32_t cbBmi){ + + SVGOStringStream tmp_image; + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + tmp_image << "\n\t <image\n"; + if (d->dc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + uint32_t width, height, colortype, numCt, invert; // if needed these values will be set in get_DIB_params + if(cbBits && cbBmi && (iUsage == U_DIB_RGB_COLORS)){ + // next call returns pointers and values, but allocates no memory + dibparams = get_DIB_params((const char *)pEmr, offBits, offBmi, &px, (const U_RGBQUAD **) &ct, + &numCt, &width, &height, &colortype, &invert); + if(dibparams ==U_BI_RGB){ + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG){ // image was binary jpg in source file + tmp_image << " xlink:href=\"data:image/jpeg;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(dibparams==U_BI_PNG){ // image was binary png in source file + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + + tmp_image << " transform=" << current_matrix(d, dx, dy, 1); // calculate appropriate offset + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \fn myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA lpData) + \param contents binary contents of an EMF file + \param length length in bytes of contents + \param d Inkscape data structures returned by this call +*/ +//THis was a callback, just build it into a normal function +int Emf::myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA d) +{ + uint32_t off=0; + uint32_t emr_mask; + int OK =1; + int file_status=1; + uint32_t nSize; + uint32_t iType; + const char *blimit = contents + length; + PU_ENHMETARECORD lpEMFR; + TCHUNK_SPECS tsp; + uint32_t tbkMode = U_TRANSPARENT; // holds proposed change to bkMode, if text is involved saving these to the DC must wait until the text is written + U_COLORREF tbkColor = U_RGB(255, 255, 255); // holds proposed change to bkColor + + // code for end user debugging + int eDbgRecord=0; + int eDbgComment=0; + int eDbgFinal=0; + char const* eDbgString = getenv( "INKSCAPE_DBG_EMF" ); + if ( eDbgString != nullptr ) { + if(strstr(eDbgString,"RECORD")){ eDbgRecord = 1; } + if(strstr(eDbgString,"COMMENT")){ eDbgComment = 1; } + if(strstr(eDbgString,"FINAL")){ eDbgFinal = 1; } + } + + /* initialize the tsp for text reassembly */ + tsp.string = nullptr; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.spaces = 0; // this field is only used for debugging + tsp.color.Red = 0; /* RGB Black */ + tsp.color.Green = 0; /* RGB Black */ + tsp.color.Blue = 0; /* RGB Black */ + tsp.color.Reserved = 0; /* not used */ + tsp.italics = 0; + tsp.weight = 80; + tsp.decoration = TXTDECOR_NONE; + tsp.condensed = 100; + tsp.co = 0; + tsp.fi_idx = -1; /* set to an invalid */ + + while(OK){ + if(off>=length)return(0); //normally should exit from while after EMREOF sets OK to false. + + // check record sizes and types thoroughly + int badrec = 0; + if (!U_emf_record_sizeok(contents + off, blimit, &nSize, &iType, 1) || + !U_emf_record_safe(contents + off)){ + badrec = 1; + } + else { + emr_mask = emr_properties(iType); + if (emr_mask == U_EMR_INVALID) { badrec = 1; } + } + if (badrec) { + file_status = 0; + break; + } + + lpEMFR = (PU_ENHMETARECORD)(contents + off); + +// At run time define environment variable INKSCAPE_DBG_EMF to include string RECORD. +// Users may employ this to track down toxic records + if(eDbgRecord){ + std::cout << "record type: " << iType << " name " << U_emr_names(iType) << " length: " << nSize << " offset: " << off <<std::endl; + } + off += nSize; + + SVGOStringStream tmp_outsvg; + SVGOStringStream tmp_path; + SVGOStringStream tmp_str; + SVGOStringStream dbg_str; + +/* Uncomment the following to track down text problems */ +//std::cout << "tri->dirty:"<< d->tri->dirty << " emr_mask: " << std::hex << emr_mask << std::dec << std::endl; + + // incompatible change to text drawing detected (color or background change) forces out existing text + // OR + // next record is valid type and forces pending text to be drawn immediately + if ((d->dc[d->level].dirty & DIRTY_TEXT) || ((emr_mask != U_EMR_INVALID) && (emr_mask & U_DRAW_TEXT) && d->tri->dirty)){ + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\n<g\n\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + SVGOStringStream ts; + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + if (d->dc[d->level].clip_id){ + d->outsvg += "\n</g>\n"; + } + } + if(d->dc[d->level].dirty){ //Apply the delayed background changes, clear the flag + d->dc[d->level].bkMode = tbkMode; + memcpy(&(d->dc[d->level].bkColor),&tbkColor, sizeof(U_COLORREF)); + + if(d->dc[d->level].dirty & DIRTY_TEXT){ + // U_COLORREF and TRCOLORREF are exactly the same in memory, but the compiler needs some convincing... + if(tbkMode == U_TRANSPARENT){ (void) trinfo_load_bk(d->tri, BKCLR_NONE, *(TRCOLORREF *) &tbkColor); } + else { (void) trinfo_load_bk(d->tri, BKCLR_LINE, *(TRCOLORREF *) &tbkColor); } // Opaque + } + + /* It is possible to have a series of EMF records that would result in + the following creating hash patterns which are never used. For instance, if + there were a series of records that changed the background color but did nothing + else. + */ + if((d->dc[d->level].stroke_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_STROKE)){ + select_extpen(d, d->dc[d->level].stroke_recidx); + } + + if((d->dc[d->level].fill_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_FILL)){ + select_brush(d, d->dc[d->level].fill_recidx); + } + + d->dc[d->level].dirty = 0; + } + +// std::cout << "BEFORE DRAW logic d->mask: " << std::hex << d->mask << " emr_mask: " << emr_mask << std::dec << std::endl; +/* +std::cout << "BEFORE DRAW" + << " test0 " << ( d->mask & U_DRAW_VISIBLE) + << " test1 " << ( d->mask & U_DRAW_FORCE) + << " test2 " << (emr_mask & U_DRAW_ALTERS) + << " test3 " << (emr_mask & U_DRAW_VISIBLE) + << " test4 " << !(d->mask & U_DRAW_ONLYTO) + << " test5 " << ((d->mask & U_DRAW_ONLYTO) && !(emr_mask & U_DRAW_ONLYTO) ) + << std::endl; +*/ + + if( + (emr_mask != U_EMR_INVALID) && // next record is valid type + (d->mask & U_DRAW_VISIBLE) && // Current set of objects are drawable + ( + (d->mask & U_DRAW_FORCE) || // This draw is forced by STROKE/FILL/STROKEANDFILL PATH + (emr_mask & U_DRAW_ALTERS) || // Next record would alter the drawing environment in some way + ( + (emr_mask & U_DRAW_VISIBLE) && // Next record is visible... + ( + ( !(d->mask & U_DRAW_ONLYTO) ) || // Non *TO records cannot be followed by any Visible + ((d->mask & U_DRAW_ONLYTO) && !(emr_mask & U_DRAW_ONLYTO) )// *TO records can only be followed by other *TO records + ) + ) + ) + ){ +// std::cout << "PATH DRAW at TOP path" << *(d->path) << std::endl; + if(!(d->path.empty())){ + d->outsvg += " <path "; // this is the ONLY place <path should be used!!! One exception, gradientfill. + if(d->drawtype){ // explicit draw type EMR record + output_style(d, d->drawtype); + } + else if(d->mask & U_DRAW_CLOSED){ // implicit draw type + output_style(d, U_EMR_STROKEANDFILLPATH); + } + else { + output_style(d, U_EMR_STROKEPATH); + } + d->outsvg += "\n\t"; + d->outsvg += "\n\td=\""; // this is the ONLY place d=" should be used!!!! One exception, gradientfill. + d->outsvg += d->path; + d->outsvg += " \" /> \n"; + d->path = ""; + } + // reset the flags + d->mask = 0; + d->drawtype = 0; + } +// std::cout << "AFTER DRAW logic d->mask: " << std::hex << d->mask << " emr_mask: " << emr_mask << std::dec << std::endl; + + switch (iType) + { + case U_EMR_HEADER: + { + dbg_str << "<!-- U_EMR_HEADER -->\n"; + + d->outdef += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"; + + if (d->pDesc) { + d->outdef += "<!-- "; + d->outdef += d->pDesc; + d->outdef += " -->\n"; + } + + PU_EMRHEADER pEmr = (PU_EMRHEADER) lpEMFR; + SVGOStringStream tmp_outdef; + tmp_outdef << "<svg\n"; + tmp_outdef << " xmlns:svg=\"http://www.w3.org/2000/svg\"\n"; + tmp_outdef << " xmlns=\"http://www.w3.org/2000/svg\"\n"; + tmp_outdef << " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"; + tmp_outdef << " xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\n"; // needed for sodipodi:role + tmp_outdef << " version=\"1.0\"\n"; + + + /* inclusive-inclusive, so the size is 1 more than the difference */ + d->MM100InX = pEmr->rclFrame.right - pEmr->rclFrame.left + 1; + d->MM100InY = pEmr->rclFrame.bottom - pEmr->rclFrame.top + 1; + d->PixelsInX = pEmr->rclBounds.right - pEmr->rclBounds.left + 1; + d->PixelsInY = pEmr->rclBounds.bottom - pEmr->rclBounds.top + 1; + + /* + calculate ratio of Inkscape dpi/EMF device dpi + This can cause problems later due to accuracy limits in the EMF. A high resolution + EMF might have a final D2Pscale[XY] of 0.074998, and adjusting the (integer) device size + by 1 will still not get it exactly to 0.075. Later when the font size is calculated it + can end up as 29.9992 or 22.4994 instead of the intended 30 or 22.5. This is handled by + snapping font sizes to the nearest .01. The best estimate is made by using both values. + */ + if ((pEmr->szlMillimeters.cx + pEmr->szlMillimeters.cy) && ( pEmr->szlDevice.cx + pEmr->szlDevice.cy)){ + d->E2IdirY = 1.0; // assume MM_TEXT, if not, this will be changed later + d->D2PscaleX = d->D2PscaleY = Inkscape::Util::Quantity::convert(1, "mm", "px") * + (double)(pEmr->szlMillimeters.cx + pEmr->szlMillimeters.cy)/ + (double)( pEmr->szlDevice.cx + pEmr->szlDevice.cy); + } + trinfo_load_qe(d->tri, d->D2PscaleX); /* quantization error that will affect text positions */ + + /* Adobe Illustrator files set mapmode to MM_ANISOTROPIC and somehow or other this + converts the rclFrame values from MM_HIMETRIC to MM_HIENGLISH, with another factor of 3 thrown + in for good measure. Ours not to question why... + */ + if(AI_hack(pEmr)){ + d->MM100InX *= 25.4/(10.0*3.0); + d->MM100InY *= 25.4/(10.0*3.0); + d->D2PscaleX *= 25.4/(10.0*3.0); + d->D2PscaleY *= 25.4/(10.0*3.0); + } + + d->MMX = d->MM100InX / 100.0; + d->MMY = d->MM100InY / 100.0; + + d->PixelsOutX = Inkscape::Util::Quantity::convert(d->MMX, "mm", "px"); + d->PixelsOutY = Inkscape::Util::Quantity::convert(d->MMY, "mm", "px"); + + // Upper left corner, from header rclBounds, in device units, usually both 0, but not always + d->ulCornerInX = pEmr->rclBounds.left; + d->ulCornerInY = pEmr->rclBounds.top; + d->ulCornerOutX = d->ulCornerInX * d->D2PscaleX; + d->ulCornerOutY = d->ulCornerInY * d->E2IdirY * d->D2PscaleY; + + tmp_outdef << + " width=\"" << d->MMX << "mm\"\n" << + " height=\"" << d->MMY << "mm\">\n"; + d->outdef += tmp_outdef.str().c_str(); + d->outdef += "<defs>"; // temporary end of header + + // d->defs holds any defines which are read in. + + tmp_outsvg << "\n</defs>\n\n"; // start of main body + + if (pEmr->nHandles) { + d->n_obj = pEmr->nHandles; + d->emf_obj = new EMF_OBJECT[d->n_obj]; + + // Init the new emf_obj list elements to null, provided the + // dynamic allocation succeeded. + if ( d->emf_obj != nullptr ) + { + for( int i=0; i < d->n_obj; ++i ) + d->emf_obj[i].lpEMFR = nullptr; + } //if + + } else { + d->emf_obj = nullptr; + } + + break; + } + case U_EMR_POLYBEZIER: + { + dbg_str << "<!-- U_EMR_POLYBEZIER -->\n"; + + PU_EMRPOLYBEZIER pEmr = (PU_EMRPOLYBEZIER) lpEMFR; + uint32_t i,j; + + if (pEmr->cptl<4) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y) << " "; + + for (i=1; i<pEmr->cptl; ) { + tmp_str << "\n\tC "; + for (j=0; j<3 && i<pEmr->cptl; j++,i++) { + tmp_str << pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y) << " "; + } + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYGON: + { + dbg_str << "<!-- U_EMR_POLYGON -->\n"; + + PU_EMRPOLYGON pEmr = (PU_EMRPOLYGON) lpEMFR; + uint32_t i; + + if (pEmr->cptl < 2) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y ) << " "; + + for (i=1; i<pEmr->cptl; i++) { + tmp_str << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + tmp_path << " z"; + + break; + } + case U_EMR_POLYLINE: + { + dbg_str << "<!-- U_EMR_POLYLINE -->\n"; + + PU_EMRPOLYLINE pEmr = (PU_EMRPOLYLINE) lpEMFR; + uint32_t i; + + if (pEmr->cptl<2) + break; + + d->mask |= emr_mask; + + tmp_str << + "\n\tM " << + pix_to_xy( d, pEmr->aptl[0].x, pEmr->aptl[0].y ) << " "; + + for (i=1; i<pEmr->cptl; i++) { + tmp_str << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYBEZIERTO: + { + dbg_str << "<!-- U_EMR_POLYBEZIERTO -->\n"; + + PU_EMRPOLYBEZIERTO pEmr = (PU_EMRPOLYBEZIERTO) lpEMFR; + uint32_t i,j; + + d->mask |= emr_mask; + + for (i=0; i<pEmr->cptl;) { + tmp_path << "\n\tC "; + for (j=0; j<3 && i<pEmr->cptl; j++,i++) { + tmp_path << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + } + + break; + } + case U_EMR_POLYLINETO: + { + dbg_str << "<!-- U_EMR_POLYLINETO -->\n"; + + PU_EMRPOLYLINETO pEmr = (PU_EMRPOLYLINETO) lpEMFR; + uint32_t i; + + d->mask |= emr_mask; + + for (i=0; i<pEmr->cptl;i++) { + tmp_path << + "\n\tL " << + pix_to_xy( d, pEmr->aptl[i].x, pEmr->aptl[i].y ) << " "; + } + + break; + } + case U_EMR_POLYPOLYLINE: + case U_EMR_POLYPOLYGON: + { + if (lpEMFR->iType == U_EMR_POLYPOLYLINE) + dbg_str << "<!-- U_EMR_POLYPOLYLINE -->\n"; + if (lpEMFR->iType == U_EMR_POLYPOLYGON) + dbg_str << "<!-- U_EMR_POLYPOLYGON -->\n"; + + PU_EMRPOLYPOLYGON pEmr = (PU_EMRPOLYPOLYGON) lpEMFR; + unsigned int n, i, j; + + d->mask |= emr_mask; + + U_POINTL *aptl = (PU_POINTL) &pEmr->aPolyCounts[pEmr->nPolys]; + + i = 0; + for (n=0; n<pEmr->nPolys && i<pEmr->cptl; n++) { + SVGOStringStream poly_path; + + poly_path << "\n\tM " << pix_to_xy( d, aptl[i].x, aptl[i].y) << " "; + i++; + + for (j=1; j<pEmr->aPolyCounts[n] && i<pEmr->cptl; j++) { + poly_path << "\n\tL " << pix_to_xy( d, aptl[i].x, aptl[i].y) << " "; + i++; + } + + tmp_str << poly_path.str().c_str(); + if (lpEMFR->iType == U_EMR_POLYPOLYGON) + tmp_str << " z"; + tmp_str << " \n"; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_SETWINDOWEXTEX: + { + dbg_str << "<!-- U_EMR_SETWINDOWEXTEX -->\n"; + + PU_EMRSETWINDOWEXTEX pEmr = (PU_EMRSETWINDOWEXTEX) lpEMFR; + + d->dc[d->level].sizeWnd = pEmr->szlExtent; + + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd.cx = d->PixelsOutX; + d->dc[d->level].sizeWnd.cy = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + } + + /* scales logical to EMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.cx && d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.cx / (double) d->dc[d->level].sizeWnd.cx; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.cy / (double) d->dc[d->level].sizeWnd.cy; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_EMR_SETWINDOWORGEX: + { + dbg_str << "<!-- U_EMR_SETWINDOWORGEX -->\n"; + + PU_EMRSETWINDOWORGEX pEmr = (PU_EMRSETWINDOWORGEX) lpEMFR; + d->dc[d->level].winorg = pEmr->ptlOrigin; + break; + } + case U_EMR_SETVIEWPORTEXTEX: + { + dbg_str << "<!-- U_EMR_SETVIEWPORTEXTEX -->\n"; + + PU_EMRSETVIEWPORTEXTEX pEmr = (PU_EMRSETVIEWPORTEXTEX) lpEMFR; + + d->dc[d->level].sizeView = pEmr->szlExtent; + + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + if (!d->dc[d->level].sizeView.cx || !d->dc[d->level].sizeView.cy) { + d->dc[d->level].sizeView.cx = d->PixelsOutX; + d->dc[d->level].sizeView.cy = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeWnd.cx || !d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + } + + /* scales logical to EMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.cx && d->dc[d->level].sizeWnd.cy) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.cx / (double) d->dc[d->level].sizeWnd.cx; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.cy / (double) d->dc[d->level].sizeWnd.cy; + if( d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_EMR_SETVIEWPORTORGEX: + { + dbg_str << "<!-- U_EMR_SETVIEWPORTORGEX -->\n"; + + PU_EMRSETVIEWPORTORGEX pEmr = (PU_EMRSETVIEWPORTORGEX) lpEMFR; + d->dc[d->level].vieworg = pEmr->ptlOrigin; + break; + } + case U_EMR_SETBRUSHORGEX: dbg_str << "<!-- U_EMR_SETBRUSHORGEX -->\n"; break; + case U_EMR_EOF: + { + dbg_str << "<!-- U_EMR_EOF -->\n"; + + tmp_outsvg << "</svg>\n"; + d->outsvg = d->outdef + d->defs + d->outsvg; + OK=0; + break; + } + case U_EMR_SETPIXELV: dbg_str << "<!-- U_EMR_SETPIXELV -->\n"; break; + case U_EMR_SETMAPPERFLAGS: dbg_str << "<!-- U_EMR_SETMAPPERFLAGS -->\n"; break; + case U_EMR_SETMAPMODE: + { + dbg_str << "<!-- U_EMR_SETMAPMODE -->\n"; + PU_EMRSETMAPMODE pEmr = (PU_EMRSETMAPMODE) lpEMFR; + switch (pEmr->iMode){ + case U_MM_TEXT: + default: + // Use all values from the header. + break; + /* For all of the following the indicated scale this will be encoded in WindowExtEx/ViewportExtex + and show up in ScaleIn[XY] + */ + case U_MM_LOMETRIC: // 1 LU = 0.1 mm, + case U_MM_HIMETRIC: // 1 LU = 0.01 mm + case U_MM_LOENGLISH: // 1 LU = 0.1 in + case U_MM_HIENGLISH: // 1 LU = 0.01 in + case U_MM_TWIPS: // 1 LU = 1/1440 in + d->E2IdirY = -1.0; + // Use d->D2Pscale[XY] values from the header. + break; + case U_MM_ISOTROPIC: // ScaleIn[XY] should be set elsewhere by SETVIEWPORTEXTEX and SETWINDOWEXTEX + case U_MM_ANISOTROPIC: + break; + } + break; + } + case U_EMR_SETBKMODE: + { + dbg_str << "<!-- U_EMR_SETBKMODE -->\n"; + PU_EMRSETBKMODE pEmr = (PU_EMRSETBKMODE) lpEMFR; + tbkMode = pEmr->iMode; + if(tbkMode != d->dc[d->level].bkMode){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + } + memcpy(&tbkColor,&(d->dc[d->level].bkColor),sizeof(U_COLORREF)); + } + break; + } + case U_EMR_SETPOLYFILLMODE: + { + dbg_str << "<!-- U_EMR_SETPOLYFILLMODE -->\n"; + + PU_EMRSETPOLYFILLMODE pEmr = (PU_EMRSETPOLYFILLMODE) lpEMFR; + d->dc[d->level].style.fill_rule.value = + (pEmr->iMode == U_ALTERNATE + ? SP_WIND_RULE_NONZERO + : (pEmr->iMode == U_WINDING ? SP_WIND_RULE_INTERSECT : SP_WIND_RULE_NONZERO)); + break; + } + case U_EMR_SETROP2: + { + dbg_str << "<!-- U_EMR_SETROP2 -->\n"; + PU_EMRSETROP2 pEmr = (PU_EMRSETROP2) lpEMFR; + d->dwRop2 = pEmr->iMode; + break; + } + case U_EMR_SETSTRETCHBLTMODE: + { + PU_EMRSETSTRETCHBLTMODE pEmr = (PU_EMRSETSTRETCHBLTMODE) lpEMFR; // from wingdi.h + BLTmode = pEmr->iMode; + dbg_str << "<!-- U_EMR_SETSTRETCHBLTMODE -->\n"; + break; + } + case U_EMR_SETTEXTALIGN: + { + dbg_str << "<!-- U_EMR_SETTEXTALIGN -->\n"; + + PU_EMRSETTEXTALIGN pEmr = (PU_EMRSETTEXTALIGN) lpEMFR; + d->dc[d->level].textAlign = pEmr->iMode; + break; + } + case U_EMR_SETCOLORADJUSTMENT: + dbg_str << "<!-- U_EMR_SETCOLORADJUSTMENT -->\n"; + break; + case U_EMR_SETTEXTCOLOR: + { + dbg_str << "<!-- U_EMR_SETTEXTCOLOR -->\n"; + + PU_EMRSETTEXTCOLOR pEmr = (PU_EMRSETTEXTCOLOR) lpEMFR; + d->dc[d->level].textColor = pEmr->crColor; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + } + // not text_dirty, because multicolored complex text is supported in libTERE + break; + } + case U_EMR_SETBKCOLOR: + { + dbg_str << "<!-- U_EMR_SETBKCOLOR -->\n"; + + PU_EMRSETBKCOLOR pEmr = (PU_EMRSETBKCOLOR) lpEMFR; + tbkColor = pEmr->crColor; + if(memcmp(&tbkColor, &(d->dc[d->level].bkColor), sizeof(U_COLORREF))){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + if(d->dc[d->level].stroke_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_STROKE; } + tbkMode = d->dc[d->level].bkMode; + } + break; + } + case U_EMR_OFFSETCLIPRGN: + { + dbg_str << "<!-- U_EMR_OFFSETCLIPRGN -->\n"; + if (d->dc[d->level].clip_id) { // can only offsetan existing clipping path + PU_EMROFFSETCLIPRGN pEmr = (PU_EMROFFSETCLIPRGN) lpEMFR; + U_POINTL off = pEmr->ptlOffset; + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector tmp_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + double ox = pix_to_x_point(d, off.x, off.y) - pix_to_x_point(d, 0, 0); // take into account all active transforms + double oy = pix_to_y_point(d, off.x, off.y) - pix_to_y_point(d, 0, 0); + Geom::Affine tf = Geom::Translate(ox,oy); + tmp_vect *= tf; + add_clips(d, sp_svg_write_path(tmp_vect).c_str(), U_RGN_COPY); + } + break; + } + case U_EMR_MOVETOEX: + { + dbg_str << "<!-- U_EMR_MOVETOEX -->\n"; + + PU_EMRMOVETOEX pEmr = (PU_EMRMOVETOEX) lpEMFR; + + d->mask |= emr_mask; + + d->dc[d->level].cur = pEmr->ptl; + + tmp_path << + "\n\tM " << pix_to_xy( d, pEmr->ptl.x, pEmr->ptl.y ) << " "; + break; + } + case U_EMR_SETMETARGN: dbg_str << "<!-- U_EMR_SETMETARGN -->\n"; break; + case U_EMR_EXCLUDECLIPRECT: + { + dbg_str << "<!-- U_EMR_EXCLUDECLIPRECT -->\n"; + + PU_EMREXCLUDECLIPRECT pEmr = (PU_EMREXCLUDECLIPRECT) lpEMFR; + U_RECTL rc = pEmr->rclClip; + + SVGOStringStream tmp_path; + //outer rect, clockwise + tmp_path << "M " << faraway << "," << faraway << " "; + tmp_path << "L " << faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << faraway << " "; + tmp_path << "z "; + //inner rect, counterclockwise (sign of Y is reversed) + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_INTERSECTCLIPRECT: + { + dbg_str << "<!-- U_EMR_INTERSECTCLIPRECT -->\n"; + + PU_EMRINTERSECTCLIPRECT pEmr = (PU_EMRINTERSECTCLIPRECT) lpEMFR; + U_RECTL rc = pEmr->rclClip; + + SVGOStringStream tmp_path; + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_SCALEVIEWPORTEXTEX: dbg_str << "<!-- U_EMR_SCALEVIEWPORTEXTEX -->\n"; break; + case U_EMR_SCALEWINDOWEXTEX: dbg_str << "<!-- U_EMR_SCALEWINDOWEXTEX -->\n"; break; + case U_EMR_SAVEDC: + dbg_str << "<!-- U_EMR_SAVEDC -->\n"; + + if (d->level < EMF_MAX_DC) { + d->dc[d->level + 1] = d->dc[d->level]; + if(d->dc[d->level].font_name){ + d->dc[d->level + 1].font_name = strdup(d->dc[d->level].font_name); // or memory access problems because font name pointer duplicated + } + d->level = d->level + 1; + } + break; + case U_EMR_RESTOREDC: + { + dbg_str << "<!-- U_EMR_RESTOREDC -->\n"; + + PU_EMRRESTOREDC pEmr = (PU_EMRRESTOREDC) lpEMFR; + int old_level = d->level; + if (pEmr->iRelative >= 0) { + if (pEmr->iRelative < d->level) + d->level = pEmr->iRelative; + } + else { + if (d->level + pEmr->iRelative >= 0) + d->level = d->level + pEmr->iRelative; + } + while (old_level > d->level) { + if (!d->dc[old_level].style.stroke_dasharray.values.empty() && + (old_level == 0 || (old_level > 0 && d->dc[old_level].style.stroke_dasharray != + d->dc[old_level - 1].style.stroke_dasharray))) { + d->dc[old_level].style.stroke_dasharray.values.clear(); + } + if(d->dc[old_level].font_name){ + free(d->dc[old_level].font_name); // else memory leak + d->dc[old_level].font_name = nullptr; + } + old_level--; + } + break; + } + case U_EMR_SETWORLDTRANSFORM: + { + dbg_str << "<!-- U_EMR_SETWORLDTRANSFORM -->\n"; + + PU_EMRSETWORLDTRANSFORM pEmr = (PU_EMRSETWORLDTRANSFORM) lpEMFR; + d->dc[d->level].worldTransform = pEmr->xform; + break; + } + case U_EMR_MODIFYWORLDTRANSFORM: + { + dbg_str << "<!-- U_EMR_MODIFYWORLDTRANSFORM -->\n"; + + PU_EMRMODIFYWORLDTRANSFORM pEmr = (PU_EMRMODIFYWORLDTRANSFORM) lpEMFR; + switch (pEmr->iMode) + { + case U_MWT_IDENTITY: + d->dc[d->level].worldTransform.eM11 = 1.0; + d->dc[d->level].worldTransform.eM12 = 0.0; + d->dc[d->level].worldTransform.eM21 = 0.0; + d->dc[d->level].worldTransform.eM22 = 1.0; + d->dc[d->level].worldTransform.eDx = 0.0; + d->dc[d->level].worldTransform.eDy = 0.0; + break; + case U_MWT_LEFTMULTIPLY: + { +// d->dc[d->level].worldTransform = pEmr->xform * worldTransform; + + float a11 = pEmr->xform.eM11; + float a12 = pEmr->xform.eM12; + float a13 = 0.0; + float a21 = pEmr->xform.eM21; + float a22 = pEmr->xform.eM22; + float a23 = 0.0; + float a31 = pEmr->xform.eDx; + float a32 = pEmr->xform.eDy; + float a33 = 1.0; + + float b11 = d->dc[d->level].worldTransform.eM11; + float b12 = d->dc[d->level].worldTransform.eM12; + //float b13 = 0.0; + float b21 = d->dc[d->level].worldTransform.eM21; + float b22 = d->dc[d->level].worldTransform.eM22; + //float b23 = 0.0; + float b31 = d->dc[d->level].worldTransform.eDx; + float b32 = d->dc[d->level].worldTransform.eDy; + //float b33 = 1.0; + + float c11 = a11*b11 + a12*b21 + a13*b31;; + float c12 = a11*b12 + a12*b22 + a13*b32;; + //float c13 = a11*b13 + a12*b23 + a13*b33;; + float c21 = a21*b11 + a22*b21 + a23*b31;; + float c22 = a21*b12 + a22*b22 + a23*b32;; + //float c23 = a21*b13 + a22*b23 + a23*b33;; + float c31 = a31*b11 + a32*b21 + a33*b31;; + float c32 = a31*b12 + a32*b22 + a33*b32;; + //float c33 = a31*b13 + a32*b23 + a33*b33;; + + d->dc[d->level].worldTransform.eM11 = c11;; + d->dc[d->level].worldTransform.eM12 = c12;; + d->dc[d->level].worldTransform.eM21 = c21;; + d->dc[d->level].worldTransform.eM22 = c22;; + d->dc[d->level].worldTransform.eDx = c31; + d->dc[d->level].worldTransform.eDy = c32; + + break; + } + case U_MWT_RIGHTMULTIPLY: + { +// d->dc[d->level].worldTransform = worldTransform * pEmr->xform; + + float a11 = d->dc[d->level].worldTransform.eM11; + float a12 = d->dc[d->level].worldTransform.eM12; + float a13 = 0.0; + float a21 = d->dc[d->level].worldTransform.eM21; + float a22 = d->dc[d->level].worldTransform.eM22; + float a23 = 0.0; + float a31 = d->dc[d->level].worldTransform.eDx; + float a32 = d->dc[d->level].worldTransform.eDy; + float a33 = 1.0; + + float b11 = pEmr->xform.eM11; + float b12 = pEmr->xform.eM12; + //float b13 = 0.0; + float b21 = pEmr->xform.eM21; + float b22 = pEmr->xform.eM22; + //float b23 = 0.0; + float b31 = pEmr->xform.eDx; + float b32 = pEmr->xform.eDy; + //float b33 = 1.0; + + float c11 = a11*b11 + a12*b21 + a13*b31;; + float c12 = a11*b12 + a12*b22 + a13*b32;; + //float c13 = a11*b13 + a12*b23 + a13*b33;; + float c21 = a21*b11 + a22*b21 + a23*b31;; + float c22 = a21*b12 + a22*b22 + a23*b32;; + //float c23 = a21*b13 + a22*b23 + a23*b33;; + float c31 = a31*b11 + a32*b21 + a33*b31;; + float c32 = a31*b12 + a32*b22 + a33*b32;; + //float c33 = a31*b13 + a32*b23 + a33*b33;; + + d->dc[d->level].worldTransform.eM11 = c11;; + d->dc[d->level].worldTransform.eM12 = c12;; + d->dc[d->level].worldTransform.eM21 = c21;; + d->dc[d->level].worldTransform.eM22 = c22;; + d->dc[d->level].worldTransform.eDx = c31; + d->dc[d->level].worldTransform.eDy = c32; + + break; + } +// case MWT_SET: + default: + d->dc[d->level].worldTransform = pEmr->xform; + break; + } + break; + } + case U_EMR_SELECTOBJECT: + { + dbg_str << "<!-- U_EMR_SELECTOBJECT -->\n"; + + PU_EMRSELECTOBJECT pEmr = (PU_EMRSELECTOBJECT) lpEMFR; + unsigned int index = pEmr->ihObject; + + if (index & U_STOCK_OBJECT) { + switch (index) { + case U_NULL_BRUSH: + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = false; + break; + case U_BLACK_BRUSH: + case U_DKGRAY_BRUSH: + case U_GRAY_BRUSH: + case U_LTGRAY_BRUSH: + case U_WHITE_BRUSH: + { + float val = 0; + switch (index) { + case U_BLACK_BRUSH: + val = 0.0 / 255.0; + break; + case U_DKGRAY_BRUSH: + val = 64.0 / 255.0; + break; + case U_GRAY_BRUSH: + val = 128.0 / 255.0; + break; + case U_LTGRAY_BRUSH: + val = 192.0 / 255.0; + break; + case U_WHITE_BRUSH: + val = 255.0 / 255.0; + break; + } + d->dc[d->level].style.fill.value.color.set( val, val, val ); + + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + break; + } + case U_NULL_PEN: + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = false; + break; + case U_BLACK_PEN: + case U_WHITE_PEN: + { + float val = index == U_BLACK_PEN ? 0 : 1; + d->dc[d->level].style.stroke_dasharray.set = false; + d->dc[d->level].style.stroke_width.value = 1.0; + d->dc[d->level].style.stroke.value.color.set( val, val, val ); + + d->dc[d->level].stroke_mode = DRAW_PAINT; + d->dc[d->level].stroke_set = true; + + break; + } + } + } else { + if ( /*index >= 0 &&*/ index < (unsigned int) d->n_obj) { + switch (d->emf_obj[index].type) + { + case U_EMR_CREATEPEN: + select_pen(d, index); + break; + case U_EMR_CREATEBRUSHINDIRECT: + case U_EMR_CREATEDIBPATTERNBRUSHPT: + case U_EMR_CREATEMONOBRUSH: + select_brush(d, index); + break; + case U_EMR_EXTCREATEPEN: + select_extpen(d, index); + break; + case U_EMR_EXTCREATEFONTINDIRECTW: + select_font(d, index); + break; + } + } + } + break; + } + case U_EMR_CREATEPEN: + { + dbg_str << "<!-- U_EMR_CREATEPEN -->\n"; + + PU_EMRCREATEPEN pEmr = (PU_EMRCREATEPEN) lpEMFR; + insert_object(d, pEmr->ihPen, U_EMR_CREATEPEN, lpEMFR); + break; + } + case U_EMR_CREATEBRUSHINDIRECT: + { + dbg_str << "<!-- U_EMR_CREATEBRUSHINDIRECT -->\n"; + + PU_EMRCREATEBRUSHINDIRECT pEmr = (PU_EMRCREATEBRUSHINDIRECT) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEBRUSHINDIRECT, lpEMFR); + break; + } + case U_EMR_DELETEOBJECT: + dbg_str << "<!-- U_EMR_DELETEOBJECT -->\n"; + // Objects here are not deleted until the draw completes, new ones may write over an existing one. + break; + case U_EMR_ANGLEARC: + dbg_str << "<!-- U_EMR_ANGLEARC -->\n"; + break; + case U_EMR_ELLIPSE: + { + dbg_str << "<!-- U_EMR_ELLIPSE -->\n"; + + PU_EMRELLIPSE pEmr = (PU_EMRELLIPSE) lpEMFR; + U_RECTL rclBox = pEmr->rclBox; + + double cx = pix_to_x_point( d, (rclBox.left + rclBox.right)/2.0, (rclBox.bottom + rclBox.top)/2.0 ); + double cy = pix_to_y_point( d, (rclBox.left + rclBox.right)/2.0, (rclBox.bottom + rclBox.top)/2.0 ); + double rx = pix_to_abs_size( d, std::abs(rclBox.right - rclBox.left )/2.0 ); + double ry = pix_to_abs_size( d, std::abs(rclBox.top - rclBox.bottom)/2.0 ); + + SVGOStringStream tmp_ellipse; + tmp_ellipse << "cx=\"" << cx << "\" "; + tmp_ellipse << "cy=\"" << cy << "\" "; + tmp_ellipse << "rx=\"" << rx << "\" "; + tmp_ellipse << "ry=\"" << ry << "\" "; + + d->mask |= emr_mask; + + d->outsvg += " <ellipse "; + output_style(d, lpEMFR->iType); // + d->outsvg += "\n\t"; + d->outsvg += tmp_ellipse.str().c_str(); + d->outsvg += "/> \n"; + d->path = ""; + break; + } + case U_EMR_RECTANGLE: + { + dbg_str << "<!-- U_EMR_RECTANGLE -->\n"; + + PU_EMRRECTANGLE pEmr = (PU_EMRRECTANGLE) lpEMFR; + U_RECTL rc = pEmr->rclBox; + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= emr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_EMR_ROUNDRECT: + { + dbg_str << "<!-- U_EMR_ROUNDRECT -->\n"; + + PU_EMRROUNDRECT pEmr = (PU_EMRROUNDRECT) lpEMFR; + U_RECTL rc = pEmr->rclBox; + U_SIZEL corner = pEmr->szlCorner; + double f = 4.*(sqrt(2) - 1)/3; + double f1 = 1.0 - f; + double cnx = corner.cx/2; + double cny = corner.cy/2; + + // clang-format off + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n" + << " M " + << pix_to_xy(d, rc.left , rc.top + cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.left , rc.top + cny*f1 ) + << " " + << pix_to_xy(d, rc.left + cnx*f1 , rc.top ) + << " " + << pix_to_xy(d, rc.left + cnx , rc.top ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.right - cnx , rc.top ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.right - cnx*f1 , rc.top ) + << " " + << pix_to_xy(d, rc.right , rc.top + cny*f1 ) + << " " + << pix_to_xy(d, rc.right , rc.top + cny ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.right , rc.bottom - cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.right , rc.bottom - cny*f1 ) + << " " + << pix_to_xy(d, rc.right - cnx*f1 , rc.bottom ) + << " " + << pix_to_xy(d, rc.right - cnx , rc.bottom ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, rc.left + cnx , rc.bottom ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, rc.left + cnx*f1 , rc.bottom ) + << " " + << pix_to_xy(d, rc.left , rc.bottom - cny*f1 ) + << " " + << pix_to_xy(d, rc.left , rc.bottom - cny ) + << "\n"; + tmp_rectangle << " z\n"; + // clang-format on + + d->mask |= emr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_EMR_ARC: + { + dbg_str << "<!-- U_EMR_ARC -->\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + int stat = emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size); + if(!stat){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + d->mask |= emr_mask; + } + else { + dbg_str << "<!-- ARC record is invalid -->\n"; + } + break; + } + case U_EMR_CHORD: + { + dbg_str << "<!-- U_EMR_CHORD -->\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= emr_mask; + } + else { + dbg_str << "<!-- CHORD record is invalid -->\n"; + } + break; + } + case U_EMR_PIE: + { + dbg_str << "<!-- U_EMR_PIE -->\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, center.x, center.y); + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= emr_mask; + } + else { + dbg_str << "<!-- PIE record is invalid -->\n"; + } + break; + } + case U_EMR_SELECTPALETTE: dbg_str << "<!-- U_EMR_SELECTPALETTE -->\n"; break; + case U_EMR_CREATEPALETTE: dbg_str << "<!-- U_EMR_CREATEPALETTE -->\n"; break; + case U_EMR_SETPALETTEENTRIES: dbg_str << "<!-- U_EMR_SETPALETTEENTRIES -->\n"; break; + case U_EMR_RESIZEPALETTE: dbg_str << "<!-- U_EMR_RESIZEPALETTE -->\n"; break; + case U_EMR_REALIZEPALETTE: dbg_str << "<!-- U_EMR_REALIZEPALETTE -->\n"; break; + case U_EMR_EXTFLOODFILL: dbg_str << "<!-- U_EMR_EXTFLOODFILL -->\n"; break; + case U_EMR_LINETO: + { + dbg_str << "<!-- U_EMR_LINETO -->\n"; + + PU_EMRLINETO pEmr = (PU_EMRLINETO) lpEMFR; + + d->mask |= emr_mask; + + tmp_path << + "\n\tL " << pix_to_xy( d, pEmr->ptl.x, pEmr->ptl.y) << " "; + break; + } + case U_EMR_ARCTO: + { + dbg_str << "<!-- U_EMR_ARCTO -->\n"; + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!emr_arc_points( lpEMFR, &f1, f2, ¢er, &start, &end, &size)){ + // draw a line from current position to start, arc from there + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y)<< " "; + + d->mask |= emr_mask; + } + else { + dbg_str << "<!-- ARCTO record is invalid -->\n"; + } + break; + } + case U_EMR_POLYDRAW: dbg_str << "<!-- U_EMR_POLYDRAW -->\n"; break; + case U_EMR_SETARCDIRECTION: + { + dbg_str << "<!-- U_EMR_SETARCDIRECTION -->\n"; + PU_EMRSETARCDIRECTION pEmr = (PU_EMRSETARCDIRECTION) lpEMFR; + if(d->arcdir == U_AD_CLOCKWISE || d->arcdir == U_AD_COUNTERCLOCKWISE){ // EMF file could be corrupt + d->arcdir = pEmr->iArcDirection; + } + break; + } + case U_EMR_SETMITERLIMIT: + { + dbg_str << "<!-- U_EMR_SETMITERLIMIT -->\n"; + + PU_EMRSETMITERLIMIT pEmr = (PU_EMRSETMITERLIMIT) lpEMFR; + + //The function takes a float but saves a 32 bit int in the U_EMR_SETMITERLIMIT record. + float miterlimit = *((int32_t *) &(pEmr->eMiterLimit)); + d->dc[d->level].style.stroke_miterlimit.value = miterlimit; //ratio, not a pt size + if (d->dc[d->level].style.stroke_miterlimit.value < 2) + d->dc[d->level].style.stroke_miterlimit.value = 2.0; + break; + } + case U_EMR_BEGINPATH: + { + dbg_str << "<!-- U_EMR_BEGINPATH -->\n"; + // The next line should never be needed, should have been handled before main switch + // qualifier added because EMF's encountered where moveto preceded beginpath followed by lineto + if(d->mask & U_DRAW_VISIBLE){ + d->path = ""; + } + d->mask |= emr_mask; + break; + } + case U_EMR_ENDPATH: + { + dbg_str << "<!-- U_EMR_ENDPATH -->\n"; + d->mask &= (0xFFFFFFFF - U_DRAW_ONLYTO); // clear the OnlyTo bit (it might not have been set), prevents any further path extension + break; + } + case U_EMR_CLOSEFIGURE: + { + dbg_str << "<!-- U_EMR_CLOSEFIGURE -->\n"; + // EMF may contain multiple closefigures on one path + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + break; + } + case U_EMR_FILLPATH: + { + dbg_str << "<!-- U_EMR_FILLPATH -->\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + if(!(d->mask & U_DRAW_CLOSED)){ // Close a path not explicitly closed by an EMRCLOSEFIGURE, otherwise fill makes no sense + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + } + d->mask |= emr_mask; + d->drawtype = U_EMR_FILLPATH; + } + break; + } + case U_EMR_STROKEANDFILLPATH: + { + dbg_str << "<!-- U_EMR_STROKEANDFILLPATH -->\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + if(!(d->mask & U_DRAW_CLOSED)){ // Close a path not explicitly closed by an EMRCLOSEFIGURE, otherwise fill makes no sense + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + } + d->mask |= emr_mask; + d->drawtype = U_EMR_STROKEANDFILLPATH; + } + break; + } + case U_EMR_STROKEPATH: + { + dbg_str << "<!-- U_EMR_STROKEPATH -->\n"; + if(d->mask & U_DRAW_PATH){ // Operation only effects declared paths + d->mask |= emr_mask; + d->drawtype = U_EMR_STROKEPATH; + } + break; + } + case U_EMR_FLATTENPATH: dbg_str << "<!-- U_EMR_FLATTENPATH -->\n"; break; + case U_EMR_WIDENPATH: dbg_str << "<!-- U_EMR_WIDENPATH -->\n"; break; + case U_EMR_SELECTCLIPPATH: + { + dbg_str << "<!-- U_EMR_SELECTCLIPPATH -->\n"; + PU_EMRSELECTCLIPPATH pEmr = (PU_EMRSELECTCLIPPATH) lpEMFR; + int logic = pEmr->iMode; + + if ((logic < U_RGN_MIN) || (logic > U_RGN_MAX)){ break; } + add_clips(d, d->path.c_str(), logic); // finds an existing one or stores this, sets clip_id + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_ABORTPATH: + { + dbg_str << "<!-- U_EMR_ABORTPATH -->\n"; + d->path = ""; + d->drawtype = 0; + break; + } + case U_EMR_UNDEF69: dbg_str << "<!-- U_EMR_UNDEF69 -->\n"; break; + case U_EMR_COMMENT: + { + dbg_str << "<!-- U_EMR_COMMENT -->\n"; + + PU_EMRCOMMENT pEmr = (PU_EMRCOMMENT) lpEMFR; + + char *szTxt = (char *) pEmr->Data; + + for (uint32_t i = 0; i < pEmr->cbData; i++) { + if ( *szTxt) { + if ( *szTxt >= ' ' && *szTxt < 'z' && *szTxt != '<' && *szTxt != '>' ) { + tmp_str << *szTxt; + } + szTxt++; + } + } + + if (false && strlen(tmp_str.str().c_str())) { + tmp_outsvg << " <!-- \""; + tmp_outsvg << tmp_str.str().c_str(); + tmp_outsvg << "\" -->\n"; + } + + break; + } + case U_EMR_FILLRGN: dbg_str << "<!-- U_EMR_FILLRGN -->\n"; break; + case U_EMR_FRAMERGN: dbg_str << "<!-- U_EMR_FRAMERGN -->\n"; break; + case U_EMR_INVERTRGN: dbg_str << "<!-- U_EMR_INVERTRGN -->\n"; break; + case U_EMR_PAINTRGN: dbg_str << "<!-- U_EMR_PAINTRGN -->\n"; break; + case U_EMR_EXTSELECTCLIPRGN: + { + dbg_str << "<!-- U_EMR_EXTSELECTCLIPRGN -->\n"; + + PU_EMREXTSELECTCLIPRGN pEmr = (PU_EMREXTSELECTCLIPRGN) lpEMFR; + // the only mode we implement - this clears the clipping region + if (pEmr->iMode == U_RGN_COPY) { + d->dc[d->level].clip_id = 0; + } + break; + } + case U_EMR_BITBLT: + { + dbg_str << "<!-- U_EMR_BITBLT -->\n"; + + PU_EMRBITBLT pEmr = (PU_EMRBITBLT) lpEMFR; + // Treat all nonImage bitblts as a rectangular write. Definitely not correct, but at + // least it leaves objects where the operations should have been. + if (!pEmr->cbBmiSrc) { + // should be an application of a DIBPATTERNBRUSHPT, use a solid color instead + + if(pEmr->dwRop == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = pEmr->Dest.x; + int32_t dy = pEmr->Dest.y; + int32_t dw = pEmr->cDest.x; + int32_t dh = pEmr->cDest.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= emr_mask; + d->dwRop3 = pEmr->dwRop; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + //source position within the bitmap, in pixels + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_STRETCHBLT: + { + dbg_str << "<!-- U_EMR_STRETCHBLT -->\n"; + PU_EMRSTRETCHBLT pEmr = (PU_EMRSTRETCHBLT) lpEMFR; + // Always grab image, ignore modes. + if (pEmr->cbBmiSrc) { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + //source position within the bitmap, in pixels + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = pEmr->cSrc.x; // extract the specified amount of the image + int sh = pEmr->cSrc.y; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_MASKBLT: + { + dbg_str << "<!-- U_EMR_MASKBLT -->\n"; + PU_EMRMASKBLT pEmr = (PU_EMRMASKBLT) lpEMFR; + // Always grab image, ignore masks and modes. + if (pEmr->cbBmiSrc) { + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + int sx = pEmr->Src.x + pEmr->xformSrc.eDx; //source position within the bitmap, in pixels + int sy = pEmr->Src.y + pEmr->xformSrc.eDy; + int sw = 0; // extract all of the image + int sh = 0; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + } + break; + } + case U_EMR_PLGBLT: dbg_str << "<!-- U_EMR_PLGBLT -->\n"; break; + case U_EMR_SETDIBITSTODEVICE: dbg_str << "<!-- U_EMR_SETDIBITSTODEVICE -->\n"; break; + case U_EMR_STRETCHDIBITS: + { + // Some applications use multiple EMF operations, including multiple STRETCHDIBITS to create + // images with transparent regions. PowerPoint does this with rotated images, for instance. + // Parsing all of that to derive a single resultant image object is left for a later version + // of this code. In the meantime, every STRETCHDIBITS goes directly to an image. The Inkscape + // user can sort out transparency later using Gimp, if need be. + + PU_EMRSTRETCHDIBITS pEmr = (PU_EMRSTRETCHDIBITS) lpEMFR; + double dx = pix_to_x_point( d, pEmr->Dest.x, pEmr->Dest.y ); + double dy = pix_to_y_point( d, pEmr->Dest.x, pEmr->Dest.y ); + double dw = pix_to_abs_size( d, pEmr->cDest.x); + double dh = pix_to_abs_size( d, pEmr->cDest.y); + int sx = pEmr->Src.x; //source position within the bitmap, in pixels + int sy = pEmr->Src.y; + int sw = pEmr->cSrc.x; // extract the specified amount of the image + int sh = pEmr->cSrc.y; + common_image_extraction(d,pEmr,dx,dy,dw,dh,sx,sy,sw,sh, + pEmr->iUsageSrc, pEmr->offBitsSrc, pEmr->cbBitsSrc, pEmr->offBmiSrc, pEmr->cbBmiSrc); + + dbg_str << "<!-- U_EMR_STRETCHDIBITS -->\n"; + break; + } + case U_EMR_EXTCREATEFONTINDIRECTW: + { + dbg_str << "<!-- U_EMR_EXTCREATEFONTINDIRECTW -->\n"; + + PU_EMREXTCREATEFONTINDIRECTW pEmr = (PU_EMREXTCREATEFONTINDIRECTW) lpEMFR; + insert_object(d, pEmr->ihFont, U_EMR_EXTCREATEFONTINDIRECTW, lpEMFR); + break; + } + case U_EMR_EXTTEXTOUTA: + case U_EMR_EXTTEXTOUTW: + case U_EMR_SMALLTEXTOUT: + { + dbg_str << "<!-- U_EMR_EXTTEXTOUTA/W -->\n"; + + PU_EMREXTTEXTOUTW pEmr = (PU_EMREXTTEXTOUTW) lpEMFR; + PU_EMRSMALLTEXTOUT pEmrS = (PU_EMRSMALLTEXTOUT) lpEMFR; + + double x1,y1; + int roff = sizeof(U_EMRSMALLTEXTOUT); //offset to the start of the variable fields, only used with U_EMR_SMALLTEXTOUT + int cChars; + if(lpEMFR->iType==U_EMR_SMALLTEXTOUT){ + x1 = pEmrS->Dest.x; + y1 = pEmrS->Dest.y; + cChars = pEmrS->cChars; + if(!(pEmrS->fuOptions & U_ETO_NO_RECT)){ roff += sizeof(U_RECTL); } + } + else { + x1 = pEmr->emrtext.ptlReference.x; + y1 = pEmr->emrtext.ptlReference.y; + cChars = 0; + } + uint32_t fOptions = pEmr->emrtext.fOptions; + + if (d->dc[d->level].textAlign & U_TA_UPDATECP) { + x1 = d->dc[d->level].cur.x; + y1 = d->dc[d->level].cur.y; + } + + double x = pix_to_x_point(d, x1, y1); + double y = pix_to_y_point(d, x1, y1); + + /* Rotation issues are handled entirely in libTERE now */ + + uint32_t *dup_wt = nullptr; + + if( lpEMFR->iType==U_EMR_EXTTEXTOUTA){ + /* These should be JUST ASCII, but they might not be... + If it holds Utf-8 or plain ASCII the first call will succeed. + If not, assume that it holds Latin1. + If that fails then something is really screwed up! + */ + dup_wt = U_Utf8ToUtf32le((char *) pEmr + pEmr->emrtext.offString, pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = U_Latin1ToUtf32le((char *) pEmr + pEmr->emrtext.offString, pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(pEmr->emrtext.nChars); + } + else if( lpEMFR->iType==U_EMR_EXTTEXTOUTW){ + dup_wt = U_Utf16leToUtf32le((uint16_t *)((char *) pEmr + pEmr->emrtext.offString), pEmr->emrtext.nChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(pEmr->emrtext.nChars); + } + else { // U_EMR_SMALLTEXTOUT + if(pEmrS->fuOptions & U_ETO_SMALL_CHARS){ + dup_wt = U_Utf8ToUtf32le((char *) pEmrS + roff, cChars, nullptr); + } + else { + dup_wt = U_Utf16leToUtf32le((uint16_t *)((char *) pEmrS + roff), cChars, nullptr); + } + if(!dup_wt)dup_wt = unknown_chars(cChars); + } + + msdepua(dup_wt); //convert everything in Microsoft's private use area. For Symbol, Wingdings, Dingbats + + if(NonToUnicode(dup_wt, d->dc[d->level].font_name)){ + free(d->dc[d->level].font_name); + d->dc[d->level].font_name = strdup("Times New Roman"); + } + + char *ansi_text; + ansi_text = (char *) U_Utf32leToUtf8((uint32_t *)dup_wt, 0, nullptr); + free(dup_wt); + // Empty string or starts with an invalid escape/control sequence, which is bogus text. Throw it out before g_markup_escape_text can make things worse + if(*((uint8_t *)ansi_text) <= 0x1F){ + free(ansi_text); + ansi_text=nullptr; + } + + if (ansi_text) { + + SVGOStringStream ts; + + gchar *escaped_text = g_markup_escape_text(ansi_text, -1); + + tsp.x = x*0.8; // TERE expects sizes in points. + tsp.y = y*0.8; + tsp.color.Red = d->dc[d->level].textColor.Red; + tsp.color.Green = d->dc[d->level].textColor.Green; + tsp.color.Blue = d->dc[d->level].textColor.Blue; + tsp.color.Reserved = 0; + switch(d->dc[d->level].style.font_style.value){ + case SP_CSS_FONT_STYLE_OBLIQUE: + tsp.italics = FC_SLANT_OBLIQUE; break; + case SP_CSS_FONT_STYLE_ITALIC: + tsp.italics = FC_SLANT_ITALIC; break; + default: + case SP_CSS_FONT_STYLE_NORMAL: + tsp.italics = FC_SLANT_ROMAN; break; + } + switch(d->dc[d->level].style.font_weight.value){ + case SP_CSS_FONT_WEIGHT_100: tsp.weight = FC_WEIGHT_THIN ; break; + case SP_CSS_FONT_WEIGHT_200: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_300: tsp.weight = FC_WEIGHT_LIGHT ; break; + case SP_CSS_FONT_WEIGHT_400: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_500: tsp.weight = FC_WEIGHT_MEDIUM ; break; + case SP_CSS_FONT_WEIGHT_600: tsp.weight = FC_WEIGHT_SEMIBOLD ; break; + case SP_CSS_FONT_WEIGHT_700: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_800: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + case SP_CSS_FONT_WEIGHT_900: tsp.weight = FC_WEIGHT_HEAVY ; break; + case SP_CSS_FONT_WEIGHT_NORMAL: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_BOLD: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_LIGHTER: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_BOLDER: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + default: tsp.weight = FC_WEIGHT_NORMAL ; break; + } + // EMF only supports two types of text decoration + tsp.decoration = TXTDECOR_NONE; + if(d->dc[d->level].style.text_decoration_line.underline){ tsp.decoration |= TXTDECOR_UNDER; } + if(d->dc[d->level].style.text_decoration_line.line_through){ tsp.decoration |= TXTDECOR_STRIKE;} + + // EMF textalignment is a bit strange: 0x6 is center, 0x2 is right, 0x0 is left, the value 0x4 is also drawn left + tsp.taln = ((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_CENTER) ? ALICENTER : + (((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_LEFT) ? ALILEFT : + ALIRIGHT); + tsp.taln |= ((d->dc[d->level].textAlign & U_TA_BASEBIT) ? ALIBASE : + ((d->dc[d->level].textAlign & U_TA_BOTTOM) ? ALIBOT : + ALITOP)); + + // language direction can be encoded two ways, U_TA_RTLREADING is preferred + if( (fOptions & U_ETO_RTLREADING) || (d->dc[d->level].textAlign & U_TA_RTLREADING) ){ tsp.ldir = LDIR_RL; } + else{ tsp.ldir = LDIR_LR; } + + tsp.condensed = FC_WIDTH_NORMAL; // Not implemented well in libTERE (yet) + tsp.ori = d->dc[d->level].style.baseline_shift.value; // For now orientation is always the same as escapement + tsp.ori += 180.0 * current_rotation(d)/ M_PI; // radians to degrees + tsp.string = (uint8_t *) U_strdup(escaped_text); // this will be free'd much later at a trinfo_clear(). + tsp.fs = d->dc[d->level].style.font_size.computed * 0.8; // Font size in points + char *fontspec = TR_construct_fontspec(&tsp, d->dc[d->level].font_name); + tsp.fi_idx = ftinfo_load_fontname(d->tri->fti,fontspec); + free(fontspec); + // when font name includes narrow it may not be set to "condensed". Narrow fonts do not work well anyway though + // as the metrics from fontconfig may not match, or the font may not be present. + if(0<= TR_findcasesub(d->dc[d->level].font_name, (char *) "Narrow")){ tsp.co=1; } + else { tsp.co=0; } + + int status = trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ori is actually escapement + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\n<g\n\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + (void) trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ignore return status, it must work + if (d->dc[d->level].clip_id){ + d->outsvg += "\n</g>\n"; + } + } + + g_free(escaped_text); + free(ansi_text); + } + + break; + } + case U_EMR_POLYBEZIER16: + { + dbg_str << "<!-- U_EMR_POLYBEZIER16 -->\n"; + + PU_EMRPOLYBEZIER16 pEmr = (PU_EMRPOLYBEZIER16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i,j; + + if (pEmr->cpts<4) + break; + + d->mask |= emr_mask; + + tmp_str << "\n\tM " << pix_to_xy( d, apts[0].x, apts[0].y ) << " "; + + for (i=1; i<pEmr->cpts; ) { + tmp_str << "\n\tC "; + for (j=0; j<3 && i<pEmr->cpts; j++,i++) { + tmp_str << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYGON16: + { + dbg_str << "<!-- U_EMR_POLYGON16 -->\n"; + + PU_EMRPOLYGON16 pEmr = (PU_EMRPOLYGON16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + SVGOStringStream tmp_poly; + unsigned int i; + unsigned int first = 0; + + d->mask |= emr_mask; + + // skip the first point? + tmp_poly << "\n\tM " << pix_to_xy( d, apts[first].x, apts[first].y ) << " "; + + for (i=first+1; i<pEmr->cpts; i++) { + tmp_poly << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + + tmp_path << tmp_poly.str().c_str(); + tmp_path << "\n\tz"; + d->mask |= U_DRAW_CLOSED; + + break; + } + case U_EMR_POLYLINE16: + { + dbg_str << "<!-- U_EMR_POLYLINE16 -->\n"; + + PU_EMRPOLYLINE16 pEmr = (PU_EMRPOLYLINE16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i; + + if (pEmr->cpts<2) + break; + + d->mask |= emr_mask; + + tmp_str << "\n\tM " << pix_to_xy( d, apts[0].x, apts[0].y ) << " "; + + for (i=1; i<pEmr->cpts; i++) { + tmp_str << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y ) << " "; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYBEZIERTO16: + { + dbg_str << "<!-- U_EMR_POLYBEZIERTO16 -->\n"; + + PU_EMRPOLYBEZIERTO16 pEmr = (PU_EMRPOLYBEZIERTO16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i,j; + + d->mask |= emr_mask; + + for (i=0; i<pEmr->cpts;) { + tmp_path << "\n\tC "; + for (j=0; j<3 && i<pEmr->cpts; j++,i++) { + tmp_path << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + } + } + + break; + } + case U_EMR_POLYLINETO16: + { + dbg_str << "<!-- U_EMR_POLYLINETO16 -->\n"; + + PU_EMRPOLYLINETO16 pEmr = (PU_EMRPOLYLINETO16) lpEMFR; + PU_POINT16 apts = (PU_POINT16) pEmr->apts; // Bug in MinGW wingdi.h ? + uint32_t i; + + d->mask |= emr_mask; + + for (i=0; i<pEmr->cpts;i++) { + tmp_path << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + } + + break; + } + case U_EMR_POLYPOLYLINE16: + case U_EMR_POLYPOLYGON16: + { + if (lpEMFR->iType == U_EMR_POLYPOLYLINE16) + dbg_str << "<!-- U_EMR_POLYPOLYLINE16 -->\n"; + if (lpEMFR->iType == U_EMR_POLYPOLYGON16) + dbg_str << "<!-- U_EMR_POLYPOLYGON16 -->\n"; + + PU_EMRPOLYPOLYGON16 pEmr = (PU_EMRPOLYPOLYGON16) lpEMFR; + unsigned int n, i, j; + + d->mask |= emr_mask; + + PU_POINT16 apts = (PU_POINT16) &pEmr->aPolyCounts[pEmr->nPolys]; + + i = 0; + for (n=0; n<pEmr->nPolys && i<pEmr->cpts; n++) { + SVGOStringStream poly_path; + + poly_path << "\n\tM " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + i++; + + for (j=1; j<pEmr->aPolyCounts[n] && i<pEmr->cpts; j++) { + poly_path << "\n\tL " << pix_to_xy( d, apts[i].x, apts[i].y) << " "; + i++; + } + + tmp_str << poly_path.str().c_str(); + if (lpEMFR->iType == U_EMR_POLYPOLYGON16) + tmp_str << " z"; + tmp_str << " \n"; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_EMR_POLYDRAW16: dbg_str << "<!-- U_EMR_POLYDRAW16 -->\n"; break; + case U_EMR_CREATEMONOBRUSH: + { + dbg_str << "<!-- U_EMR_CREATEDIBPATTERNBRUSHPT -->\n"; + + PU_EMRCREATEMONOBRUSH pEmr = (PU_EMRCREATEMONOBRUSH) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEMONOBRUSH, lpEMFR); + break; + } + case U_EMR_CREATEDIBPATTERNBRUSHPT: + { + dbg_str << "<!-- U_EMR_CREATEDIBPATTERNBRUSHPT -->\n"; + + PU_EMRCREATEDIBPATTERNBRUSHPT pEmr = (PU_EMRCREATEDIBPATTERNBRUSHPT) lpEMFR; + insert_object(d, pEmr->ihBrush, U_EMR_CREATEDIBPATTERNBRUSHPT, lpEMFR); + break; + } + case U_EMR_EXTCREATEPEN: + { + dbg_str << "<!-- U_EMR_EXTCREATEPEN -->\n"; + + PU_EMREXTCREATEPEN pEmr = (PU_EMREXTCREATEPEN) lpEMFR; + insert_object(d, pEmr->ihPen, U_EMR_EXTCREATEPEN, lpEMFR); + break; + } + case U_EMR_POLYTEXTOUTA: dbg_str << "<!-- U_EMR_POLYTEXTOUTA -->\n"; break; + case U_EMR_POLYTEXTOUTW: dbg_str << "<!-- U_EMR_POLYTEXTOUTW -->\n"; break; + case U_EMR_SETICMMODE: + { + dbg_str << "<!-- U_EMR_SETICMMODE -->\n"; + PU_EMRSETICMMODE pEmr = (PU_EMRSETICMMODE) lpEMFR; + ICMmode= pEmr->iMode; + break; + } + case U_EMR_CREATECOLORSPACE: dbg_str << "<!-- U_EMR_CREATECOLORSPACE -->\n"; break; + case U_EMR_SETCOLORSPACE: dbg_str << "<!-- U_EMR_SETCOLORSPACE -->\n"; break; + case U_EMR_DELETECOLORSPACE: dbg_str << "<!-- U_EMR_DELETECOLORSPACE -->\n"; break; + case U_EMR_GLSRECORD: dbg_str << "<!-- U_EMR_GLSRECORD -->\n"; break; + case U_EMR_GLSBOUNDEDRECORD: dbg_str << "<!-- U_EMR_GLSBOUNDEDRECORD -->\n"; break; + case U_EMR_PIXELFORMAT: dbg_str << "<!-- U_EMR_PIXELFORMAT -->\n"; break; + case U_EMR_DRAWESCAPE: dbg_str << "<!-- U_EMR_DRAWESCAPE -->\n"; break; + case U_EMR_EXTESCAPE: dbg_str << "<!-- U_EMR_EXTESCAPE -->\n"; break; + case U_EMR_UNDEF107: dbg_str << "<!-- U_EMR_UNDEF107 -->\n"; break; + // U_EMR_SMALLTEXTOUT is handled with U_EMR_EXTTEXTOUTA/W above + case U_EMR_FORCEUFIMAPPING: dbg_str << "<!-- U_EMR_FORCEUFIMAPPING -->\n"; break; + case U_EMR_NAMEDESCAPE: dbg_str << "<!-- U_EMR_NAMEDESCAPE -->\n"; break; + case U_EMR_COLORCORRECTPALETTE: dbg_str << "<!-- U_EMR_COLORCORRECTPALETTE -->\n"; break; + case U_EMR_SETICMPROFILEA: dbg_str << "<!-- U_EMR_SETICMPROFILEA -->\n"; break; + case U_EMR_SETICMPROFILEW: dbg_str << "<!-- U_EMR_SETICMPROFILEW -->\n"; break; + case U_EMR_ALPHABLEND: dbg_str << "<!-- U_EMR_ALPHABLEND -->\n"; break; + case U_EMR_SETLAYOUT: dbg_str << "<!-- U_EMR_SETLAYOUT -->\n"; break; + case U_EMR_TRANSPARENTBLT: dbg_str << "<!-- U_EMR_TRANSPARENTBLT -->\n"; break; + case U_EMR_UNDEF117: dbg_str << "<!-- U_EMR_UNDEF117 -->\n"; break; + case U_EMR_GRADIENTFILL: + { + /* Gradient fill is doable for rectangles because those correspond to linear gradients. However, + the general case for the triangle fill, with a different color in each corner of the triangle, + has no SVG equivalent and cannot be easily emulated with SVG gradients. So the linear gradient + is implemented, and the triangle fill just paints with the color of the first corner. + + This record can hold a series of gradients so we are forced to add path elements directly here, + it cannot wait for the top of the main loop. Any existing path is erased. + + */ + dbg_str << "<!-- U_EMR_GRADIENTFILL -->\n"; + PU_EMRGRADIENTFILL pEmr = (PU_EMRGRADIENTFILL) lpEMFR; + int nV = pEmr->nTriVert; // Number of TriVertex objects + int nG = pEmr->nGradObj; // Number of gradient triangle/rectangle objects + U_TRIVERTEX *tv = (U_TRIVERTEX *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL)); + if( pEmr->ulMode == U_GRADIENT_FILL_RECT_H || + pEmr->ulMode == U_GRADIENT_FILL_RECT_V + ){ + SVGOStringStream tmp_rectangle; + int i,fill_idx; + U_GRADIENT4 *rcs = (U_GRADIENT4 *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL) + sizeof(U_TRIVERTEX)*nV); + for(i=0;i<nG;i++){ + tmp_rectangle << "\n<path d=\""; + fill_idx = add_gradient(d, pEmr->ulMode, tv[rcs[i].UpperLeft], tv[rcs[i].LowerRight]); + tmp_rectangle << "\n\tM " << pix_to_xy( d, tv[rcs[i].UpperLeft ].x , tv[rcs[i].UpperLeft ].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].LowerRight].x , tv[rcs[i].UpperLeft ].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].LowerRight].x , tv[rcs[i].LowerRight].y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, tv[rcs[i].UpperLeft ].x , tv[rcs[i].LowerRight].y ) << " "; + tmp_rectangle << "\n\tz\""; + tmp_rectangle << "\n\tstyle=\"stroke:none;fill:url(#"; + tmp_rectangle << d->gradients.strings[fill_idx]; + tmp_rectangle << ");\"\n"; + if (d->dc[d->level].clip_id){ + tmp_rectangle << "\tclip-path=\"url(#clipEmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_rectangle << "/>\n"; + } + d->outsvg += tmp_rectangle.str().c_str(); + } + else if(pEmr->ulMode == U_GRADIENT_FILL_TRIANGLE){ + SVGOStringStream tmp_triangle; + char tmpcolor[8]; + int i; + U_GRADIENT3 *tris = (U_GRADIENT3 *)(((char *)lpEMFR) + sizeof(U_EMRGRADIENTFILL) + sizeof(U_TRIVERTEX)*nV); + for(i=0;i<nG;i++){ + tmp_triangle << "\n<path d=\""; + sprintf(tmpcolor,"%6.6X",sethexcolor(trivertex_to_colorref(tv[tris[i].Vertex1]))); + tmp_triangle << "\n\tM " << pix_to_xy( d, tv[tris[i].Vertex1].x , tv[tris[i].Vertex1].y ) << " "; + tmp_triangle << "\n\tL " << pix_to_xy( d, tv[tris[i].Vertex2].x , tv[tris[i].Vertex2].y ) << " "; + tmp_triangle << "\n\tL " << pix_to_xy( d, tv[tris[i].Vertex3].x , tv[tris[i].Vertex3].y ) << " "; + tmp_triangle << "\n\tz\""; + tmp_triangle << "\n\tstyle=\"stroke:none;fill:#"; + tmp_triangle << tmpcolor; + tmp_triangle << ";\"\n/>\n"; + } + d->outsvg += tmp_triangle.str().c_str(); + } + d->path = ""; + // if it is anything else the record is bogus, so ignore it + break; + } + case U_EMR_SETLINKEDUFIS: dbg_str << "<!-- U_EMR_SETLINKEDUFIS -->\n"; break; + case U_EMR_SETTEXTJUSTIFICATION: dbg_str << "<!-- U_EMR_SETTEXTJUSTIFICATION -->\n"; break; + case U_EMR_COLORMATCHTOTARGETW: dbg_str << "<!-- U_EMR_COLORMATCHTOTARGETW -->\n"; break; + case U_EMR_CREATECOLORSPACEW: dbg_str << "<!-- U_EMR_CREATECOLORSPACEW -->\n"; break; + default: + dbg_str << "<!-- U_EMR_??? -->\n"; + break; + } //end of switch +// At run time define environment variable INKSCAPE_DBG_EMF to include string COMMENT. +// Users may employ this to to place a comment for each processed EMR record in the SVG + if(eDbgComment){ + d->outsvg += dbg_str.str().c_str(); + } + d->outsvg += tmp_outsvg.str().c_str(); + d->path += tmp_path.str().c_str(); + + } //end of while +// At run time define environment variable INKSCAPE_DBG_EMF to include string FINAL +// Users may employ this to to show the final SVG derived from the EMF + if(eDbgFinal){ + std::cout << d->outsvg << std::endl; + } + (void) emr_properties(U_EMR_INVALID); // force the release of the lookup table memory, returned value is irrelevant + + return(file_status); +} + +void Emf::free_emf_strings(EMF_STRINGS name){ + if(name.count){ + for(int i=0; i< name.count; i++){ free(name.strings[i]); } + free(name.strings); + } + name.count = 0; + name.size = 0; +} + +SPDocument * +Emf::open( Inkscape::Extension::Input * /*mod*/, const gchar *uri ) +{ + if (uri == nullptr) { + return nullptr; + } + + // ensure usage of dot as decimal separator in scanf/printf functions (indepentendly of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + EMF_CALLBACK_DATA d; + + d.n_obj = 0; //these might not be set otherwise if the input file is corrupt + d.emf_obj = nullptr; + d.dc[0].font_name = strdup("Arial"); // Default font, set only on lowest level, it copies up from there EMF spec says device can pick whatever it wants + + // set up the size default for patterns in defs. This might not be referenced if there are no patterns defined in the drawing. + + d.defs += "\n"; + d.defs += " <pattern id=\"EMFhbasepattern\" \n"; + d.defs += " patternUnits=\"userSpaceOnUse\"\n"; + d.defs += " width=\"6\" \n"; + d.defs += " height=\"6\" \n"; + d.defs += " x=\"0\" \n"; + d.defs += " y=\"0\"> \n"; + d.defs += " </pattern> \n"; + + + size_t length; + char *contents; + if(emf_readdata(uri, &contents, &length))return(nullptr); + + d.pDesc = nullptr; + + // set up the text reassembly system + if(!(d.tri = trinfo_init(nullptr)))return(nullptr); + (void) trinfo_load_ft_opts(d.tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + int good = myEnhMetaFileProc(contents,length, &d); + free(contents); + + if (d.pDesc){ free( d.pDesc ); } + +// std::cout << "SVG Output: " << std::endl << d.outsvg << std::endl; + + SPDocument *doc = nullptr; + if (good) { + doc = SPDocument::createNewDocFromMem(d.outsvg.c_str(), strlen(d.outsvg.c_str()), TRUE); + } + + free_emf_strings(d.hatches); + free_emf_strings(d.images); + free_emf_strings(d.gradients); + free_emf_strings(d.clips); + + if (d.emf_obj) { + int i; + for (i=0; i<d.n_obj; i++) + delete_object(&d, i); + delete[] d.emf_obj; + } + + d.dc[0].style.stroke_dasharray.values.clear(); + + for(int i=0; i<=EMF_MAX_DC; i++){ + if(d.dc[i].font_name)free(d.dc[i].font_name); + } + + d.tri = trinfo_release_except_FC(d.tri); + + // in earlier versions no viewbox was generated and a call to setViewBoxIfMissing() was needed here. + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return doc; +} + + +void +Emf::init () +{ + /* EMF in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("EMF Input") "</name>\n" + "<id>org.inkscape.input.emf</id>\n" + "<input>\n" + "<extension>.emf</extension>\n" + "<mimetype>image/x-emf</mimetype>\n" + "<filetypename>" N_("Enhanced Metafiles (*.emf)") "</filetypename>\n" + "<filetypetooltip>" N_("Enhanced Metafiles") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new Emf()); + // clang-format on + + /* EMF out */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("EMF Output") "</name>\n" + "<id>org.inkscape.output.emf</id>\n" + "<param name=\"textToPath\" gui-text=\"" N_("Convert texts to paths") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToSymbol\" gui-text=\"" N_("Map Unicode to Symbol font") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToWingdings\" gui-text=\"" N_("Map Unicode to Wingdings") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToZapfDingbats\" gui-text=\"" N_("Map Unicode to Zapf Dingbats") "\" type=\"bool\">true</param>\n" + "<param name=\"UsePUA\" gui-text=\"" N_("Use MS Unicode PUA (0xF020-0xF0FF) for converted characters") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTCharPos\" gui-text=\"" N_("Compensate for PPT font bug") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTDashLine\" gui-text=\"" N_("Convert dashed/dotted lines to single lines") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTGrad2Polys\" gui-text=\"" N_("Convert gradients to colored polygon series") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTLinGrad\" gui-text=\"" N_("Use native rectangular linear gradients") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTPatternAsHatch\" gui-text=\"" N_("Map all fill patterns to standard EMF hatches") "\" type=\"bool\">false</param>\n" + "<param name=\"FixImageRot\" gui-text=\"" N_("Ignore image rotations") "\" type=\"bool\">false</param>\n" + "<output>\n" + "<extension>.emf</extension>\n" + "<mimetype>image/x-emf</mimetype>\n" + "<filetypename>" N_("Enhanced Metafile (*.emf)") "</filetypename>\n" + "<filetypetooltip>" N_("Enhanced Metafile") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new Emf()); + // clang-format on + + return; +} + + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-inout.h b/src/extension/internal/emf-inout.h new file mode 100644 index 0000000..74c8053 --- /dev/null +++ b/src/extension/internal/emf-inout.h @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile Input/Output + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * David Mathog + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_EXTENSION_INTERNAL_EMF_H +#define SEEN_EXTENSION_INTERNAL_EMF_H + +#include <3rdparty/libuemf/uemf.h> +#include <3rdparty/libuemf/uemf_safe.h> +#include <3rdparty/libuemf/uemf_endian.h> // for U_emf_record_sizeok() +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/implementation/implementation.h" +#include "style.h" +#include "text_reassemble.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define DIRTY_NONE 0x00 +#define DIRTY_TEXT 0x01 +#define DIRTY_FILL 0x02 +#define DIRTY_STROKE 0x04 + +struct EMF_OBJECT { + int type = 0; + int level = 0; + char *lpEMFR = nullptr; +}; +using PEMF_OBJECT = EMF_OBJECT *; + +struct EMF_STRINGS { + int size = 0; // number of slots allocated in strings + int count = 0; // number of slots used in strings + char **strings = nullptr; // place to store strings +}; +using PEMF_STRINGS = EMF_STRINGS *; + +struct EMF_DEVICE_CONTEXT { + EMF_DEVICE_CONTEXT() : + // SPStyle: class with constructor + font_name(nullptr), + clip_id(0), + stroke_set(false), stroke_mode(0), stroke_idx(0), stroke_recidx(0), + fill_set(false), fill_mode(0), fill_idx(0), fill_recidx(0), + dirty(0), + // sizeWnd, sizeView, winorg, vieworg, + ScaleInX(0), ScaleInY(0), + ScaleOutX(0), ScaleOutY(0), + bkMode(U_TRANSPARENT), + // bkColor, textColor + textAlign(0) + // worldTransform, cur + { + sizeWnd = sizel_set( 0.0, 0.0 ); + sizeView = sizel_set( 0.0, 0.0 ); + winorg = point32_set( 0.0, 0.0 ); + vieworg = point32_set( 0.0, 0.0 ); + bkColor = U_RGB(255, 255, 255); // default foreground color (white) + textColor = U_RGB(0, 0, 0); // default foreground color (black) + worldTransform.eM11 = 1.0; + worldTransform.eM12 = 0.0; + worldTransform.eM21 = 0.0; + worldTransform.eM22 = 1.0; + worldTransform.eDx = 0.0; + worldTransform.eDy = 0.0; + cur = point32_set( 0, 0 ); + }; + SPStyle style; + char *font_name; + int clip_id; // 0 if none, else 1 + index into clips + bool stroke_set; + int stroke_mode; // enumeration from drawmode, not used if fill_set is not True + int stroke_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int stroke_recidx;// record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + bool fill_set; + int fill_mode; // enumeration from drawmode, not used if fill_set is not True + int fill_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int fill_recidx; // record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + int dirty; // holds the dirty bits for text, stroke, fill + U_SIZEL sizeWnd; + U_SIZEL sizeView; + U_POINTL winorg; + U_POINTL vieworg; + double ScaleInX, ScaleInY; + double ScaleOutX, ScaleOutY; + uint16_t bkMode; + U_COLORREF bkColor; + U_COLORREF textColor; + uint32_t textAlign; + U_XFORM worldTransform; + U_POINTL cur; +}; +using PEMF_DEVICE_CONTEXT = EMF_DEVICE_CONTEXT *; + +#define EMF_MAX_DC 128 + +struct EMF_CALLBACK_DATA { + + EMF_CALLBACK_DATA() : + // dc: array, structure w/ constructor + level(0), + E2IdirY(1.0), + D2PscaleX(1.0), D2PscaleY(1.0), + MM100InX(0), MM100InY(0), + PixelsInX(0), PixelsInY(0), + PixelsOutX(0), PixelsOutY(0), + ulCornerInX(0), ulCornerInY(0), + ulCornerOutX(0), ulCornerOutY(0), + mask(0), + arcdir(U_AD_COUNTERCLOCKWISE), + dwRop2(U_R2_COPYPEN), dwRop3(0), + MMX(0),MMY(0), + drawtype(0), + pDesc(nullptr), + // hatches, images, gradients, struct w/ constructor + tri(nullptr), + n_obj(0) + // emf_obj; + {}; + + Glib::ustring outsvg; + Glib::ustring path; + Glib::ustring outdef; + Glib::ustring defs; + + EMF_DEVICE_CONTEXT dc[EMF_MAX_DC+1]; // FIXME: This should be dynamic.. + int level; + + double E2IdirY; // EMF Y direction relative to Inkscape Y direction. Will be negative for MM_LOMETRIC etc. + double D2PscaleX,D2PscaleY; // EMF device to Inkscape Page scale. + float MM100InX, MM100InY; // size of the drawing in hundredths of a millimeter + float PixelsInX, PixelsInY; // size of the drawing, in EMF device pixels + float PixelsOutX, PixelsOutY; // size of the drawing, in Inkscape pixels + double ulCornerInX,ulCornerInY; // Upper left corner, from header rclBounds, in logical units + double ulCornerOutX,ulCornerOutY; // Upper left corner, in Inkscape pixels + uint32_t mask; // Draw properties + int arcdir; //U_AD_COUNTERCLOCKWISE 1 or U_AD_CLOCKWISE 2 + + uint32_t dwRop2; // Binary raster operation, 0 if none (use brush/pen unmolested) + uint32_t dwRop3; // Ternary raster operation, 0 if none (use brush/pen unmolested) + + float MMX; + float MMY; + + unsigned int drawtype; // one of 0 or U_EMR_FILLPATH, U_EMR_STROKEPATH, U_EMR_STROKEANDFILLPATH + char *pDesc; + // both of these end up in <defs> under the names shown here. These structures allow duplicates to be avoided. + EMF_STRINGS hatches; // hold pattern names, all like EMFhatch#_$$$$$$ where # is the EMF hatch code and $$$$$$ is the color + EMF_STRINGS images; // hold images, all like Image#, where # is the slot the image lives. + EMF_STRINGS gradients; // hold gradient names, all like EMF[HV]_$$$$$$_$$$$$$ where $$$$$$ are the colors + EMF_STRINGS clips; // hold clipping paths, referred to be the slot where the clipping path lives + TR_INFO *tri; // Text Reassembly data structure + + + int n_obj; + PEMF_OBJECT emf_obj; +}; +using PEMF_CALLBACK_DATA = EMF_CALLBACK_DATA *; + +class Emf : public Metafile +{ + +public: + + Emf(); // Empty constructor + + ~Emf() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + + static void init();//Initialize the class + +private: + +protected: + static void print_document_to_file(SPDocument *doc, const gchar *filename); + static double current_scale(PEMF_CALLBACK_DATA d); + static std::string current_matrix(PEMF_CALLBACK_DATA d, double x, double y, int useoffset); + static double current_rotation(PEMF_CALLBACK_DATA d); + static void enlarge_hatches(PEMF_CALLBACK_DATA d); + static int in_hatches(PEMF_CALLBACK_DATA d, char *test); + static uint32_t add_hatch(PEMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor); + static void enlarge_images(PEMF_CALLBACK_DATA d); + static int in_images(PEMF_CALLBACK_DATA d, const char *test); + static uint32_t add_image(PEMF_CALLBACK_DATA d, void *pEmr, uint32_t cbBits, uint32_t cbBmi, + uint32_t iUsage, uint32_t offBits, uint32_t offBmi); + static void enlarge_gradients(PEMF_CALLBACK_DATA d); + static int in_gradients(PEMF_CALLBACK_DATA d, const char *test); + static uint32_t add_gradient(PEMF_CALLBACK_DATA d, uint32_t gradientType, U_TRIVERTEX tv1, U_TRIVERTEX tv2); + + static void enlarge_clips(PEMF_CALLBACK_DATA d); + static int in_clips(PEMF_CALLBACK_DATA d, const char *test); + static void add_clips(PEMF_CALLBACK_DATA d, const char *clippath, unsigned int logic); + + static void output_style(PEMF_CALLBACK_DATA d, int iType); + static double _pix_x_to_point(PEMF_CALLBACK_DATA d, double px); + static double _pix_y_to_point(PEMF_CALLBACK_DATA d, double py); + static double pix_to_x_point(PEMF_CALLBACK_DATA d, double px, double py); + static double pix_to_y_point(PEMF_CALLBACK_DATA d, double px, double py); + static double pix_to_abs_size(PEMF_CALLBACK_DATA d, double px); + static void snap_to_faraway_pair(double *x, double *y); + static std::string pix_to_xy(PEMF_CALLBACK_DATA d, double x, double y); + static void select_pen(PEMF_CALLBACK_DATA d, int index); + static void select_extpen(PEMF_CALLBACK_DATA d, int index); + static void select_brush(PEMF_CALLBACK_DATA d, int index); + static void select_font(PEMF_CALLBACK_DATA d, int index); + static void delete_object(PEMF_CALLBACK_DATA d, int index); + static void insert_object(PEMF_CALLBACK_DATA d, int index, int type, PU_ENHMETARECORD pObj); + static int AI_hack(PU_EMRHEADER pEmr); + static uint32_t *unknown_chars(size_t count); + static void common_image_extraction(PEMF_CALLBACK_DATA d, void *pEmr, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, + uint32_t iUsage, uint32_t offBits, uint32_t cbBits, uint32_t offBmi, uint32_t cbBmi); + static int myEnhMetaFileProc(char *contents, unsigned int length, PEMF_CALLBACK_DATA d); + static void free_emf_strings(EMF_STRINGS name); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* EXTENSION_INTERNAL_EMF_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-print.cpp b/src/extension/internal/emf-print.cpp new file mode 100644 index 0000000..6d1a3ef --- /dev/null +++ b/src/extension/internal/emf-print.cpp @@ -0,0 +1,2210 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile printing + *//* + * Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * David Mathog + * + * Copyright (C) 2006-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include "emf-print.h" + +#include <cstring> +#include <glibmm/miscutils.h> +#include <3rdparty/libuemf/symbol_convert.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include <2geom/svg-path-parser.h> // to get from SVG text to Geom::Path + +#include "inkscape-version.h" + +#include "document.h" +#include "path-prefix.h" +#include "style.h" +#include "style-enums.h" // Fill rules + +#include "display/cairo-utils.h" // for Inkscape::Pixbuf::PF_CAIRO +#include "display/curve.h" + +#include "extension/system.h" +#include "extension/print.h" + +#include "helper/geom.h" +#include "helper/geom-curves.h" + +#include "object/sp-pattern.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-item.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-clippath.h" + +#include "path/path-boolop.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define PXPERMETER 2835 + +/* globals */ +static double PX2WORLD; +static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTLinGrad, FixPPTPatternAsHatch, FixImageRot; +static EMFTRACK *et = nullptr; +static EMFHANDLES *eht = nullptr; + +void PrintEmf::smuggle_adxkyrtl_out(const char *string, uint32_t **adx, double *ky, int *rtl, int *ndx, float scale) +{ + float fdx; + int i; + uint32_t *ladx; + const char *cptr = &string[strlen(string) + 1]; // this works because of the first fake terminator + + *adx = nullptr; + *ky = 0.0; // set a default value + sscanf(cptr, "%7d", ndx); + if (!*ndx) { + return; // this could happen with an empty string + } + cptr += 7; + ladx = (uint32_t *) malloc(*ndx * sizeof(uint32_t)); + if (!ladx) { + g_message("Out of memory"); + } + *adx = ladx; + for (i = 0; i < *ndx; i++, cptr += 7, ladx++) { + sscanf(cptr, "%7f", &fdx); + *ladx = (uint32_t) round(fdx * scale); + } + cptr++; // skip 2nd fake terminator + sscanf(cptr, "%7f", &fdx); + *ky = fdx; + cptr += 7; // advance over ky and its space + sscanf(cptr, "%07d", rtl); +} + +PrintEmf::PrintEmf() +{ + // all of the class variables are initialized elsewhere, many in PrintEmf::Begin, +} + + +unsigned int PrintEmf::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + + +unsigned int PrintEmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) +{ + U_SIZEL szlDev, szlMm; + U_RECTL rclBounds, rclFrame; + char *rec; + gchar const *utf8_fn = mod->get_param_string("destination"); + + // Typically PX2WORLD is 1200/90, using inkscape's default dpi + PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); + FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); + FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); + FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); + FixPPTLinGrad = mod->get_param_bool("FixPPTLinGrad"); + FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); + FixImageRot = mod->get_param_bool("FixImageRot"); + + (void) emf_start(utf8_fn, 1000000, 250000, &et); // Initialize the et structure + (void) htable_create(128, 128, &eht); // Initialize the eht structure + + char *ansi_uri = (char *) utf8_fn; + + + // width and height in px + _width = doc->getWidth().value("px"); + _height = doc->getHeight().value("px"); + _doc_unit_scale = doc->getDocumentScale()[Geom::X]; + + // initialize a few global variables + hbrush = hbrushOld = hpen = 0; + htextalignment = U_TA_BASELINE | U_TA_LEFT; + use_stroke = use_fill = simple_shape = usebk = false; + + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv) { + const char *p1 = nv->attribute("pagecolor"); + char *p2; + uint32_t lc = strtoul(&p1[1], &p2, 16); // it looks like "#ABC123" + if (*p2) { + lc = 0; + } + gv.bgc = _gethexcolor(lc); + gv.rgb[0] = (float) U_RGBAGetR(gv.bgc) / 255.0; + gv.rgb[1] = (float) U_RGBAGetG(gv.bgc) / 255.0; + gv.rgb[2] = (float) U_RGBAGetB(gv.bgc) / 255.0; + } + + bool pageBoundingBox; + pageBoundingBox = mod->get_param_bool("pageBoundingBox"); + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(0, 0, _width, _height); + } else { + SPItem *doc_item = doc->getRoot(); + Geom::OptRect bbox = doc_item->desktopVisualBounds(); + if (bbox) { + d = *bbox; + } + } + + d *= Geom::Scale(Inkscape::Util::Quantity::convert(1, "px", "in")); + + float dwInchesX = d.width(); + float dwInchesY = d.height(); + + // dwInchesX x dwInchesY in micrometer units, 1200 dpi/25.4 -> dpmm + (void) drawing_size((int) ceil(dwInchesX * 25.4), (int) ceil(dwInchesY * 25.4),1200.0/25.4, &rclBounds, &rclFrame); + + // set up the reference device as 100 X A4 horizontal, (1200 dpi/25.4 -> dpmm). Extra digits maintain dpi better in EMF + int MMX = 216; + int MMY = 279; + (void) device_size(MMX, MMY, 1200.0 / 25.4, &szlDev, &szlMm); + int PixelsX = szlDev.cx; + int PixelsY = szlDev.cy; + + // set up the description: (version string)0(file)00 + char buff[1024]; + memset(buff, 0, sizeof(buff)); + char *p1 = strrchr(ansi_uri, '\\'); + char *p2 = strrchr(ansi_uri, '/'); + char *p = MAX(p1, p2); + if (p) { + p++; + } else { + p = ansi_uri; + } + snprintf(buff, sizeof(buff) - 1, "Inkscape %s \1%s\1", Inkscape::version_string, p); + uint16_t *Description = U_Utf8ToUtf16le(buff, 0, nullptr); + int cbDesc = 2 + wchar16len(Description); // also count the final terminator + (void) U_Utf16leEdit(Description, '\1', '\0'); // swap the temporary \1 characters for nulls + + // construct the EMRHEADER record and append it to the EMF in memory + rec = U_EMRHEADER_set(rclBounds, rclFrame, nullptr, cbDesc, Description, szlDev, szlMm, 0); + free(Description); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRHEADER"); + } + + + // Simplest mapping mode, supply all coordinates in pixels + rec = U_EMRSETMAPMODE_set(U_MM_TEXT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRSETMAPMODE"); + } + + + // In earlier versions this was used to scale from inkscape's dpi of 90 to + // the files 1200 dpi, taking into account PX2WORLD which was 20. Now PX2WORLD + // is set so that this matrix is unitary. The usual value of PX2WORLD is 1200/90, + // but might be different if the internal dpi is changed. + + U_XFORM worldTransform; + worldTransform.eM11 = 1.0; + worldTransform.eM12 = 0.0; + worldTransform.eM21 = 0.0; + worldTransform.eM22 = 1.0; + worldTransform.eDx = 0; + worldTransform.eDy = 0; + + rec = U_EMRMODIFYWORLDTRANSFORM_set(worldTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at EMRMODIFYWORLDTRANSFORM"); + } + + + if (true) { + snprintf(buff, sizeof(buff) - 1, "Screen=%dx%dpx, %dx%dmm", PixelsX, PixelsY, MMX, MMY); + rec = textcomment_set(buff); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at textcomment_set 1"); + } + + snprintf(buff, sizeof(buff) - 1, "Drawing=%.1fx%.1fpx, %.1fx%.1fmm", _width, _height, Inkscape::Util::Quantity::convert(dwInchesX, "in", "mm"), Inkscape::Util::Quantity::convert(dwInchesY, "in", "mm")); + rec = textcomment_set(buff); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at textcomment_set 1"); + } + } + + /* set some parameters, else the program that reads the EMF may default to other values */ + + rec = U_EMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETBKMODE_set"); + } + + hpolyfillmode = U_WINDING; + rec = U_EMRSETPOLYFILLMODE_set(U_WINDING); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETPOLYFILLMODE_set"); + } + + // Text alignment: (only changed if RTL text is encountered ) + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the EMF text alignment must always be TA_BASELINE|TA_LEFT. + htextalignment = U_TA_BASELINE | U_TA_LEFT; + rec = U_EMRSETTEXTALIGN_set(U_TA_BASELINE | U_TA_LEFT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTALIGN_set"); + } + + htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + rec = U_EMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETTEXTCOLOR_set"); + } + + rec = U_EMRSETROP2_set(U_R2_COPYPEN); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::begin at U_EMRSETROP2_set"); + } + + /* miterlimit is set with eah pen, so no need to check for it changes as in WMF */ + + return 0; +} + + +unsigned int PrintEmf::finish(Inkscape::Extension::Print * /*mod*/) +{ + do_clip_if_present(nullptr); // Terminate any open clip. + char *rec; + if (!et) { + return 0; + } + + + // earlier versions had flush of fill here, but it never executed and was removed + + rec = U_EMREOF_set(0, nullptr, et); // generate the EOF record + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::finish"); + } + (void) emf_finish(et, eht); // Finalize and write out the EMF + emf_free(&et); // clean up + htable_free(&eht); // clean up + + return 0; +} + +// fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. +int PrintEmf::create_brush(SPStyle const *style, PU_COLORREF fcolor) +{ + float rgb[3]; + char *rec; + U_LOGBRUSH lb; + uint32_t brush, fmode; + MFDrawMode fill_mode; + Inkscape::Pixbuf *pixbuf; + uint32_t brushStyle; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width = 0; // quiets a harmless compiler warning, initialization not otherwise required. + uint32_t height = 0; + + if (!et) { + return 0; + } + + // set a default fill in case we can't figure out a better way to do it + fmode = U_ALTERNATE; + fill_mode = DRAW_PAINT; + brushStyle = U_BS_SOLID; + hatchType = U_HS_SOLIDCLR; + bkColor = U_RGB(0, 0, 0); + if (fcolor) { + hatchColor = *fcolor; + } else { + hatchColor = U_RGB(0, 0, 0); + } + + if (!fcolor && style) { + if (style->fill.isColor()) { + fill_mode = DRAW_PAINT; +#if 0 +// opacity not supported by EMF + float opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (opacity <= 0.0) { + opacity = 0.0; // basically the same as no fill + } +#endif + style->fill.value.color.get_rgb_floatv(rgb); + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); + } else if (SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + fill_mode = DRAW_IMAGE; + } else { // pattern + fill_mode = DRAW_PATTERN; + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + fill_mode = DRAW_PATTERN; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + brushStyle = U_BS_HATCHED; + } else if (SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below just sets the color to the average of the stops + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPLinearGradient *lg = nullptr; + SPRadialGradient *rg = nullptr; + + if (SP_IS_LINEARGRADIENT(paintserver)) { + lg = SP_LINEARGRADIENT(paintserver); + lg->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_LINEAR_GRADIENT; + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + rg = SP_RADIALGRADIENT(paintserver); + rg->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_RADIAL_GRADIENT; + } else { + // default fill + } + + if (rg) { + if (FixPPTGrad2Polys) { + return hold_gradient(rg, fill_mode); + } else { + hatchColor = avg_stop_color(rg); + } + } else if (lg) { + if (FixPPTGrad2Polys || FixPPTLinGrad) { + return hold_gradient(lg, fill_mode); + } else { + hatchColor = avg_stop_color(lg); + } + } + } + } else { // if (!style) + // default fill + } + + lb = logbrush_set(brushStyle, hatchColor, hatchType); + + switch (fill_mode) { + case DRAW_LINEAR_GRADIENT: // fill with average color unless gradients are converted to slices + case DRAW_RADIAL_GRADIENT: // ditto + case DRAW_PAINT: + case DRAW_PATTERN: + // SVG text has no background attribute, so OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + if (usebk) { + rec = U_EMRSETBKCOLOR_set(bkColor); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETBKCOLOR_set"); + } + rec = U_EMRSETBKMODE_set(U_OPAQUE); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETBKMODE_set"); + } + } + rec = createbrushindirect_set(&brush, eht, lb); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at createbrushindirect_set"); + } + break; + case DRAW_IMAGE: + char *px; + char *rgba_px; + uint32_t cbPx; + uint32_t colortype; + PU_RGBQUAD ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + rec = createdibpatternbrushpt_set(&brush, eht, U_DIB_RGB_COLORS, Bmi, cbPx, px); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at createdibpatternbrushpt_set"); + } + free(px); + free(Bmi); // ct will be NULL because of colortype + break; + } + + hbrush = brush; // need this later for destroy_brush + rec = selectobject_set(brush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at selectobject_set"); + } + + if (fmode != hpolyfillmode) { + hpolyfillmode = fmode; + rec = U_EMRSETPOLYFILLMODE_set(fmode); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_brush at U_EMRSETPOLYdrawmode_set"); + } + } + + return 0; +} + + +void PrintEmf::destroy_brush() +{ + char *rec; + // before an object may be safely deleted it must no longer be selected + // select in a stock object to deselect this one, the stock object should + // never be used because we always select in a new one before drawing anythingrestore previous brush, necessary??? Would using a default stock object not work? + rec = selectobject_set(U_NULL_BRUSH, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_brush at selectobject_set"); + } + if (hbrush) { + rec = deleteobject_set(&hbrush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_brush"); + } + hbrush = 0; + } +} + + +int PrintEmf::create_pen(SPStyle const *style, const Geom::Affine &transform) +{ + U_EXTLOGPEN *elp; + U_NUM_STYLEENTRY n_dash = 0; + U_STYLEENTRY *dash = nullptr; + char *rec = nullptr; + int linestyle = U_PS_SOLID; + int linecap = 0; + int linejoin = 0; + uint32_t pen; + uint32_t brushStyle; + Inkscape::Pixbuf *pixbuf; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width, height; + char *px = nullptr; + char *rgba_px; + uint32_t cbPx = 0; + uint32_t colortype; + PU_RGBQUAD ct = nullptr; + int numCt = 0; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi = nullptr; + + if (!et) { + return 0; + } + + // set a default stroke in case we can't figure out a better way to do it + brushStyle = U_BS_SOLID; + hatchColor = U_RGB(0, 0, 0); + hatchType = U_HS_HORIZONTAL; + bkColor = U_RGB(0, 0, 0); + + if (style) { + float rgb[3]; + + if (SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->stroke.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + brushStyle = U_BS_DIBPATTERN; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + } else { // pattern + brushStyle = U_BS_HATCHED; + if (usebk) { // OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + rec = U_EMRSETBKCOLOR_set(bkColor); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETBKCOLOR_set"); + } + rec = U_EMRSETBKMODE_set(U_OPAQUE); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETBKMODE_set"); + } + } + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + brushStyle = U_BS_HATCHED; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + } else if (SP_IS_GRADIENT(SP_STYLE_STROKE_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below has no net effect. + + SPPaintServer *paintserver = style->stroke.value.href->getObject(); + if (SP_IS_LINEARGRADIENT(paintserver)) { + SPLinearGradient *lg = SP_LINEARGRADIENT(paintserver); + + lg->ensureVector(); // when exporting from commandline, vector is not built + + Geom::Point p1(lg->x1.computed, lg->y1.computed); + Geom::Point p2(lg->x2.computed, lg->y2.computed); + + if (lg->gradientTransform_set) { + p1 = p1 * lg->gradientTransform; + p2 = p2 * lg->gradientTransform; + } + hatchColor = avg_stop_color(lg); + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + SPRadialGradient *rg = SP_RADIALGRADIENT(paintserver); + + rg->ensureVector(); // when exporting from commandline, vector is not built + double r = rg->r.computed; + + Geom::Point c(rg->cx.computed, rg->cy.computed); + Geom::Point xhandle_point(r, 0); + Geom::Point yhandle_point(0, -r); + yhandle_point += c; + xhandle_point += c; + if (rg->gradientTransform_set) { + c = c * rg->gradientTransform; + yhandle_point = yhandle_point * rg->gradientTransform; + xhandle_point = xhandle_point * rg->gradientTransform; + } + hatchColor = avg_stop_color(rg); + } else { + // default fill + } + } else if (style->stroke.isColor()) { // test last, always seems to be set, even for other types above + style->stroke.value.color.get_rgb_floatv(rgb); + brushStyle = U_BS_SOLID; + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + hatchType = U_HS_SOLIDCLR; + } else { + // default fill + } + + + + using Geom::X; + using Geom::Y; + + Geom::Point zero(0, 0); + Geom::Point one(1, 1); + Geom::Point p0(zero * transform); + Geom::Point p1(one * transform); + Geom::Point p(p1 - p0); + + double scale = sqrt((p[X] * p[X]) + (p[Y] * p[Y])) / sqrt(2); + + if (!style->stroke_width.computed) { + return 0; //if width is 0 do not (reset) the pen, it should already be NULL_PEN + } + uint32_t linewidth = MAX(1, (uint32_t) round(scale * style->stroke_width.computed * PX2WORLD)); + + if (style->stroke_linecap.computed == 0) { + linecap = U_PS_ENDCAP_FLAT; + } else if (style->stroke_linecap.computed == 1) { + linecap = U_PS_ENDCAP_ROUND; + } else if (style->stroke_linecap.computed == 2) { + linecap = U_PS_ENDCAP_SQUARE; + } + + if (style->stroke_linejoin.computed == 0) { + linejoin = U_PS_JOIN_MITER; + } else if (style->stroke_linejoin.computed == 1) { + linejoin = U_PS_JOIN_ROUND; + } else if (style->stroke_linejoin.computed == 2) { + linejoin = U_PS_JOIN_BEVEL; + } + + if (!style->stroke_dasharray.values.empty()) { + if (FixPPTDashLine) { // will break up line into many smaller lines. Override gradient if that was set, cannot do both. + brushStyle = U_BS_SOLID; + hatchType = U_HS_HORIZONTAL; + } else { + unsigned i = 0; + while ((linestyle != U_PS_USERSTYLE) && (i < style->stroke_dasharray.values.size())) { + if (style->stroke_dasharray.values[i].value > 0.00000001) { + linestyle = U_PS_USERSTYLE; + } + i++; + } + + if (linestyle == U_PS_USERSTYLE) { + n_dash = style->stroke_dasharray.values.size(); + dash = new uint32_t[n_dash]; + for (i = 0; i < n_dash; i++) { + dash[i] = MAX(1, (uint32_t)round(scale * style->stroke_dasharray.values[i].value * PX2WORLD)); + } + } + } + } + + elp = extlogpen_set( + U_PS_GEOMETRIC | linestyle | linecap | linejoin, + linewidth, + brushStyle, + hatchColor, + hatchType, + n_dash, + dash); + + } else { // if (!style) + linejoin = 0; + elp = extlogpen_set( + linestyle, + 1, + U_BS_SOLID, + U_RGB(0, 0, 0), + U_HS_HORIZONTAL, + 0, + nullptr); + } + + rec = extcreatepen_set(&pen, eht, Bmi, cbPx, px, elp); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at extcreatepen_set"); + } + free(elp); + if (Bmi) { + free(Bmi); + } + if (px) { + free(px); // ct will always be NULL + } + + rec = selectobject_set(pen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at selectobject_set"); + } + hpen = pen; // need this later for destroy_pen + + if (linejoin == U_PS_JOIN_MITER) { + float miterlimit = style->stroke_miterlimit.value; // This is a ratio. + + if (miterlimit < 1) { + miterlimit = 1; + } + + rec = U_EMRSETMITERLIMIT_set((uint32_t) miterlimit); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::create_pen at U_EMRSETMITERLIMIT_set"); + } + } + + if (n_dash) { + delete[] dash; + } + return 0; +} + +// set the current pen to the stock object NULL_PEN and then delete the defined pen object, if there is one. +void PrintEmf::destroy_pen() +{ + char *rec = nullptr; + // before an object may be safely deleted it must no longer be selected + // select in a stock object to deselect this one, the stock object should + // never be used because we always select in a new one before drawing anythingrestore previous brush, necessary??? Would using a default stock object not work? + rec = selectobject_set(U_NULL_PEN, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_pen at selectobject_set"); + } + if (hpen) { + rec = deleteobject_set(&hpen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::destroy_pen"); + } + hpen = 0; + } +} + +/* Return a Path consisting of just the corner points of the single path in a PathVector. If the +PathVector has more than one path, or that one path is open, or any of its segments are curved, then the +returned PathVector is an empty path. If the input path is already just straight lines and vertices the output will be the +same as the sole path in the input. */ + +Geom::Path PrintEmf::pathv_to_simple_polygon(Geom::PathVector const &pathv, int *vertices) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point P1_lead; + Geom::Point v1,v2; + Geom::Path output; + Geom::Path bad; + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv); + Geom::PathVector::const_iterator pit = pv.begin(); + Geom::PathVector::const_iterator pit2 = pv.begin(); + int first_seg=1; + ++pit2; + *vertices = 0; + if(pit->end_closed() != pit->end_default())return(bad); // path must be closed + if(pit2 != pv.end())return(bad); // there may only be one path + P1_trail = pit->finalPoint(); + Geom::Path::const_iterator cit = pit->begin(); + P1 = cit->initialPoint(); + for(;cit != pit->end_closed();++cit) { + if (!is_straight_curve(*cit)) { + *vertices = 0; + return(bad); + } + P1_lead = cit->finalPoint(); + if(Geom::are_near(P1_lead, P1, 1e-5))continue; // duplicate points at the same coordinate + v1 = unit_vector(P1 - P1_trail); + v2 = unit_vector(P1_lead - P1 ); + if(Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ // P1 is within a straight line + P1 = P1_lead; + continue; + } + // P1 is the center point of a turn of some angle + if(!*vertices){ + output.start( P1 ); + output.close( pit->closed() ); + } + if(!Geom::are_near(P1, P1_trail, 1e-5)){ // possible for P1 to start on the end point + Geom::LineSegment ls(P1_trail, P1); + output.append(ls); + if(first_seg){ + *vertices += 2; + first_seg=0; + } + else { + *vertices += 1; + } + } + P1_trail = P1; + P1 = P1_lead; + } + return(output); +} + +/* Returns the simplified PathVector (no matter what). + Sets is_rect if it is a rectangle. + Sets angle that will rotate side closest to horizontal onto horizontal. +*/ +Geom::Path PrintEmf::pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, double *angle) +{ + Geom::Point P1_trail; + Geom::Point P1; + Geom::Point v1,v2; + int vertices; + Geom::Path pR = pathv_to_simple_polygon(pathv, &vertices); + *is_rect = false; + if(vertices==4){ // or else it cannot be a rectangle + int vertex_count=0; + /* Get the ends of the LAST line segment. + Find minimum rotation to align rectangle with X,Y axes. (Very degenerate if it is rotated 45 degrees.) */ + *angle = 10.0; /* must be > than the actual angle in radians. */ + for(Geom::Path::iterator cit = pR.begin();; ++cit){ + P1_trail = cit->initialPoint(); + P1 = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + if(v1[Geom::X] > 0){ // only check the 1 or 2 points on vectors aimed the same direction as unit X + double ang = asin(v1[Geom::Y]); // because component is rotation by ang of {1,0| vector + if(fabs(ang) < fabs(*angle))*angle = -ang; // y increases down, flips sign on angle + } + if(cit == pR.end_open())break; + } + + /* For increased numerical stability, snap the angle to the nearest 1/100th of a degree. */ + double convert = 36000.0/ (2.0 * M_PI); + *angle = round(*angle * convert)/convert; + + // at this stage v1 holds the last vector in the path, whichever direction it points. + for(Geom::Path::iterator cit = pR.begin(); ;++cit) { + v2 = v1; + P1_trail = cit->initialPoint(); + P1 = cit->finalPoint(); + v1 = unit_vector(P1 - P1_trail); + // P1 is center of a turn that is not 90 degrees. Limit comes from cos(89.9) = .001745 + if(!Geom::are_near(dot(v1,v2), 0.0, 2e-3))break; + vertex_count++; + if(cit == pR.end_open())break; + } + if(vertex_count == 4){ + *is_rect=true; + } + } + return(pR); +} + +/* Compare a vector with a rectangle's orientation (angle needed to rotate side(s) + closest to horizontal to exactly horizontal) and return: + 0 none of the following + 1 parallel to horizontal + 2 parallel to vertical + 3 antiparallel to horizontal + 4 antiparallel to vertical +*/ +int PrintEmf::vector_rect_alignment(double angle, Geom::Point vtest){ + int stat = 0; + Geom::Point v1 = Geom::unit_vector(vtest); // unit vector to test alignment + Geom::Point v2 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v3 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + if( Geom::are_near(dot(v1,v2), 1.0, 1e-5)){ stat = 1; } + else if(Geom::are_near(dot(v1,v2),-1.0, 1e-5)){ stat = 2; } + else if(Geom::are_near(dot(v1,v3), 1.0, 1e-5)){ stat = 3; } + else if(Geom::are_near(dot(v1,v3),-1.0, 1e-5)){ stat = 4; } + return(stat); +} + +/* retrieve the point at the indicated corner: + 0 UL (and default) + 1 UR + 2 LR + 3 LL + Needed because the start can be any point, and the direction could run either + clockwise or counterclockwise. This should work even if the corners of the rectangle + are slightly displaced. +*/ +Geom::Point PrintEmf::get_pathrect_corner(Geom::Path pathRect, double angle, int corner){ + Geom::Point center(0,0); + for(Geom::Path::iterator cit = pathRect.begin(); ; ++cit) { + center += cit->initialPoint()/4.0; + if(cit == pathRect.end_open())break; + } + + int LR; // 1 if Left, 0 if Right + int UL; // 1 if Lower, 0 if Upper (as viewed on screen, y coordinates increase downwards) + switch(corner){ + case 1: //UR + LR = 0; + UL = 0; + break; + case 2: //LR + LR = 0; + UL = 1; + break; + case 3: //LL + LR = 1; + UL = 1; + break; + default: //UL + LR = 1; + UL = 0; + break; + } + + Geom::Point v1 = Geom::Point(1,0) * Geom::Rotate(-angle); // unit horizontal side (sign change because Y increases DOWN) + Geom::Point v2 = Geom::Point(0,1) * Geom::Rotate(-angle); // unit vertical side (sign change because Y increases DOWN) + Geom::Point P1; + for(Geom::Path::iterator cit = pathRect.begin(); ; ++cit) { + P1 = cit->initialPoint(); + + if ( ( LR == (dot(P1 - center,v1) > 0 ? 0 : 1) ) + && ( UL == (dot(P1 - center,v2) > 0 ? 1 : 0) ) ) break; + if(cit == pathRect.end_open())break; + } + return(P1); +} + +U_TRIVERTEX PrintEmf::make_trivertex(Geom::Point Pt, U_COLORREF uc){ + U_TRIVERTEX tv; + using Geom::X; + using Geom::Y; + tv.x = (int32_t) round(Pt[X]); + tv.y = (int32_t) round(Pt[Y]); + tv.Red = uc.Red << 8; + tv.Green = uc.Green << 8; + tv.Blue = uc.Blue << 8; + tv.Alpha = uc.Reserved << 8; // EMF will ignore this + return(tv); +} + +/* Examine clip. If there is a (new) one then apply it. If there is one and it is the + same as the preceding one, leave the preceding one active. If style is NULL + terminate the current clip, if any, and return. +*/ +void PrintEmf::do_clip_if_present(SPStyle const *style){ + char *rec; + static SPClipPath *scpActive = nullptr; + if(!style){ + if(scpActive){ // clear the existing clip + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + scpActive=nullptr; + } + } else { + /* The current implementation converts only one level of clipping. If there were more + clips further up the stack they should be combined with the pathvector using "and". Since this + comes up rarely, and would involve a lot of searching (all the way up the stack for every + draw operation), it has not yet been implemented. + + Note, to debug this section of code use print statements on sp_svg_write_path(combined_pathvector). + */ + /* find the first clip_ref at object or up the stack. There may not be one. */ + SPClipPath *scp = nullptr; + SPItem *item = SP_ITEM(style->object); + while(true) { + scp = item->getClipObject(); + if(scp)break; + item = SP_ITEM(item->parent); + if(!item || SP_IS_ROOT(item))break; // this will never be a clipping path + } + + if(scp != scpActive){ // change or remove the clipping + if(scpActive){ // clear the existing clip + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + scpActive = nullptr; + } + + if (scp) { // set the new clip + /* because of units and who knows what other transforms that might be applied above we + need the full transform all the way to the root. + */ + Geom::Affine tf = item->transform; + SPItem *scan_item = item; + while(true) { + scan_item = SP_ITEM(scan_item->parent); + if(!scan_item)break; + tf *= scan_item->transform; + } + tf *= Geom::Scale(_doc_unit_scale);; // Transform must be in PIXELS, no matter what the document unit is. + + /* find the clipping path */ + Geom::PathVector combined_pathvector; + Geom::Affine tfc; // clipping transform, generally not the same as item transform + for (auto& child: scp->children) { + item = SP_ITEM(&child); + if (!item) { + break; + } + if (SP_IS_GROUP(item)) { // not implemented + // return sp_group_render(item); + combined_pathvector = merge_PathVector_with_group(combined_pathvector, item, tfc); + } else if (SP_IS_SHAPE(item)) { + combined_pathvector = merge_PathVector_with_shape(combined_pathvector, item, tfc); + } else { // not implemented + } + } + + if (!combined_pathvector.empty()) { // if clipping path isn't empty, define EMF clipping record + scpActive = scp; // remember for next time + // the sole purpose of this SAVEDC is to let us clear the clipping region later. + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + (void) draw_pathv_to_EMF(combined_pathvector, tf); + rec = U_EMRSELECTCLIPPATH_set(U_RGN_COPY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::do_clip_if_present at U_EMRSELECTCLIPPATH_set"); + } + } + else { + scpActive = nullptr; // no valid path available to draw, so no DC was saved, so no signal to restore + } + } // change or remove clipping + } // scp exists + } // style exists +} + +Geom::PathVector PrintEmf::merge_PathVector_with_group(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform) +{ + // sanity test, only a group should be passed in, return empty if something else happens + auto group = dynamic_cast<SPGroup const *>(item); + if (!group) + return {}; + + Geom::PathVector new_combined_pathvector = combined_pathvector; + Geom::Affine tfc = item->transform * transform; + for (auto& child: group->children) { + item = SP_ITEM(&child); + if (!item) { + break; + } + if (SP_IS_GROUP(item)) { + new_combined_pathvector = merge_PathVector_with_group(new_combined_pathvector, item, tfc); // could be endlessly recursive on a badly formed SVG + } else if (SP_IS_SHAPE(item)) { + new_combined_pathvector = merge_PathVector_with_shape(new_combined_pathvector, item, tfc); + } else { // not implemented + } + } + return new_combined_pathvector; +} + +Geom::PathVector PrintEmf::merge_PathVector_with_shape(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform) +{ + Geom::PathVector new_combined_pathvector; + auto shape = dynamic_cast<SPShape const *>(item); + + // sanity test, only a shape should be passed in, return empty if something else happens + if (!shape) + return new_combined_pathvector; + + Geom::Affine tfc = item->transform * transform; + if (shape->curve()) { + Geom::PathVector const &new_vect = shape->curve()->get_pathvector(); + if(combined_pathvector.empty()){ + new_combined_pathvector = new_vect * tfc; + } + else { + new_combined_pathvector = sp_pathvector_boolop(new_vect * tfc, combined_pathvector, bool_op_union , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + } + } + return new_combined_pathvector; +} + +unsigned int PrintEmf::fill( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const & /*transform*/, SPStyle const *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + char *rec; + using Geom::X; + using Geom::Y; + Geom::Affine tf = m_tr_stack.top(); + + do_clip_if_present(style); // If clipping is needed set it up + + use_fill = true; + use_stroke = false; + + fill_transform = tf; + + int brush_stat = create_brush(style, nullptr); + + /* native linear gradients are only used if the object is a rectangle AND the gradient is parallel to the sides of the object */ + bool is_Rect = false; + double angle; + int rectDir=0; + Geom::Path pathRect; + if(FixPPTLinGrad && brush_stat && gv.mode == DRAW_LINEAR_GRADIENT){ + Geom::PathVector pvr = pathv * fill_transform; + pathRect = pathv_to_rect(pvr, &is_Rect, &angle); + if(is_Rect){ + /* Gradientfill records can only be used if the gradient is parallel to the sides of the rectangle. + That must be checked here so that we can fall back to another form of gradient fill if it is not + the case. */ + rectDir = vector_rect_alignment(angle, (gv.p2 - gv.p1) * fill_transform); + if(!rectDir)is_Rect = false; + } + if(!is_Rect && !FixPPTGrad2Polys)brush_stat=0; // fall all the way back to a solid fill + } + + if (brush_stat) { // only happens if the style is a gradient + /* + Handle gradients. Uses modified livarot as 2geom boolops is currently broken. + Can handle gradients with multiple stops. + + The overlap is needed to avoid antialiasing artifacts when edges are not strictly aligned on pixel boundaries. + There is an inevitable loss of accuracy saving through an EMF file because of the integer coordinate system. + Keep the overlap quite large so that loss of accuracy does not remove an overlap. + */ + destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below + Geom::Path cutter; + float rgb[3]; + U_COLORREF wc, c1, c2; + FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); + double doff, doff_base, doff_range; + double divisions = 128.0; + int nstops; + int istop = 1; + float opa; // opacity at stop + + SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here + nstops = tg->vector.stops.size(); + tg->vector.stops[0].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[0].opacity; // first stop + c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[nstops - 1].opacity; // last stop + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + doff = 0.0; + doff_base = 0.0; + doff_range = tg->vector.stops[1].offset; // next or last stop + + if (gv.mode == DRAW_RADIAL_GRADIENT) { + Geom::Point xv = gv.p2 - gv.p1; // X' vector + Geom::Point yv = gv.p3 - gv.p1; // Y' vector + Geom::Point xuv = Geom::unit_vector(xv); // X' unit vector + double rx = hypot(xv[X], xv[Y]); + double ry = hypot(yv[X], yv[Y]); + double range = fmax(rx, ry); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* radial gradient might stop part way through the shape, fill with outer color from there to "infinity". + Do this first so that outer colored ring will overlay it. + */ + pathvc = center_elliptical_hole_as_SVG_PathV(gv.p1, rx * (1.0 - overlap / range), ry * (1.0 - overlap / range), asin(xuv[Y])); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_oddEven, frb); + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + + pathvc = center_elliptical_ring_as_SVG_PathV(gv.p1, rx * start / range, ry * start / range, rx * stop / range, ry * stop / range, asin(xuv[Y])); + + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range) { + istop++; + if (istop >= nstops) { + istop = nstops - 1; + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else if (gv.mode == DRAW_LINEAR_GRADIENT) { + if(is_Rect){ + int gMode; + Geom::Point ul, ur, lr; + Geom::Point outUL, outLR; // UL,LR corners of a stop rectangle, in OUTPUT coordinates + U_TRIVERTEX ut[2]; + U_GRADIENT4 ug4; + U_RECTL rcb; + U_XFORM tmpTransform; + double wRect, hRect; + + /* coordinates: upper left, upper right, and lower right corners of the rectangle. + inkscape transform already applied, but needs to be scaled to EMF coordinates. */ + ul = get_pathrect_corner(pathRect, angle, 0) * PX2WORLD; + ur = get_pathrect_corner(pathRect, angle, 1) * PX2WORLD; + lr = get_pathrect_corner(pathRect, angle, 2) * PX2WORLD; + wRect = Geom::distance(ul,ur); + hRect = Geom::distance(ur,lr); + + /* The basic rectangle for all of these is placed with its UL corner at 0,0 with a size wRect,hRect. + Apply a world transform to place/scale it into the appropriate position on the drawing. + Actual gradientfill records are either this entire rectangle or slices of it as defined by the stops. + This rectangle has already been transformed by tf (whatever rotation/scale) Inkscape had applied to it. + */ + + Geom::Affine tf2 = Geom::Rotate(-angle); // the rectangle may be drawn skewed to the coordinate system + tmpTransform.eM11 = tf2[0]; + tmpTransform.eM12 = tf2[1]; + tmpTransform.eM21 = tf2[2]; + tmpTransform.eM22 = tf2[3]; + tmpTransform.eDx = round((ul)[Geom::X]); // use explicit round for better stability + tmpTransform.eDy = round((ul)[Geom::Y]); + + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + + rec = U_EMRMODIFYWORLDTRANSFORM_set(tmpTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRMODIFYWORLDTRANSFORM"); + } + + for(;istop<nstops;istop++){ + doff_range = tg->vector.stops[istop].offset; // next or last stop + if(rectDir == 1 || rectDir == 2){ + outUL = Geom::Point(doff_base *wRect, 0 ); + outLR = Geom::Point(doff_range*wRect, hRect); + gMode = U_GRADIENT_FILL_RECT_H; + } + else { + outUL = Geom::Point(0, doff_base *hRect); + outLR = Geom::Point(wRect,doff_range*hRect); + gMode = U_GRADIENT_FILL_RECT_V; + } + + doff_base = doff_range; + rcb.left = round(outUL[X]); // use explicit round for better stability + rcb.top = round(outUL[Y]); + rcb.right = round(outLR[X]); + rcb.bottom = round(outLR[Y]); + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + if(rectDir == 2 || rectDir == 4){ // gradient is reversed, so swap colors + ut[0] = make_trivertex(outUL, c2); + ut[1] = make_trivertex(outLR, c1); + } + else { + ut[0] = make_trivertex(outUL, c1); + ut[1] = make_trivertex(outLR, c2); + } + c1 = c2; // for next stop + ug4.UpperLeft = 0; + ug4.LowerRight= 1; + rec = U_EMRGRADIENTFILL_set(rcb, 2, 1, gMode, ut, (uint32_t *) &ug4 ); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRGRADIENTFILL_set"); + } + } + + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRRESTOREDC_set"); + } + } + else { + Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector + Geom::Point puv = uv.cw(); // perp. to unit vector + double range = Geom::distance(gv.p1, gv.p2); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* before lower end of gradient, overlap first slice position */ + wc = weight_opacity(c1); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + /* after high end of gradient, overlap last slice position */ + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range) { + istop++; + if (istop >= nstops) { + istop = nstops - 1; + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } + } else { + g_error("Fatal programming error in PrintEmf::fill, invalid gradient type detected"); + } + use_fill = false; // gradients handled, be sure stroke does not use stroke and fill + } else { + /* + Inkscape was not calling create_pen for objects with no border. + This was because it never called stroke() (next method). + PPT, and presumably others, pick whatever they want for the border if it is not specified, so no border can + become a visible border. + To avoid this force the pen to NULL_PEN if we can determine that no pen will be needed after the fill. + */ + if (style->stroke.noneSet || style->stroke_width.computed == 0.0) { + destroy_pen(); //this sets the NULL_PEN + } + + /* postpone fill in case stroke also required AND all stroke paths closed + Dashes converted to line segments will "open" a closed path. + */ + bool all_closed = true; + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (pit.end_default() != pit.end_closed()) { + all_closed = false; + } + } + } + if ( + (style->stroke.isNone() || style->stroke.noneSet || style->stroke_width.computed == 0.0) || + (!style->stroke_dasharray.values.empty() && FixPPTDashLine) || + !all_closed + ) { + print_pathv(pathv, fill_transform); // do any fills. side effect: clears fill_pathv + use_fill = false; + } + } + + return 0; +} + + +unsigned int PrintEmf::stroke( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, const Geom::Affine &/*transform*/, const SPStyle *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + do_clip_if_present(style); // If clipping is needed set it up + + use_stroke = true; + // use_fill was set in ::fill, if it is needed + + if (create_pen(style, tf)) { + return 0; + } + + if (!style->stroke_dasharray.values.empty() && FixPPTDashLine) { + // convert the path, gets its complete length, and then make a new path with parameter length instead of t + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw; // pathv-> sbasis + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw2; // sbasis using arc length parameter + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw3; // new (discontinuous) path, composed of dots/dashes + Geom::Piecewise<Geom::D2<Geom::SBasis> > first_frag; // first fragment, will be appended at end + int n_dash = style->stroke_dasharray.values.size(); + int i = 0; //dash index + double tlength; // length of tmp_pathpw + double slength = 0.0; // start of gragment + double elength; // end of gragment + for (const auto & i : pathv) { + tmp_pathpw.concat(i.toPwSb()); + } + tlength = length(tmp_pathpw, 0.1); + tmp_pathpw2 = arc_length_parametrization(tmp_pathpw); + + // go around the dash array repeatedly until the entire path is consumed (but not beyond). + while (slength < tlength) { + elength = slength + style->stroke_dasharray.values[i++].value; + if (elength > tlength) { + elength = tlength; + } + Geom::Piecewise<Geom::D2<Geom::SBasis> > fragment(portion(tmp_pathpw2, slength, elength)); + if (slength) { + tmp_pathpw3.concat(fragment); + } else { + first_frag = fragment; + } + slength = elength; + slength += style->stroke_dasharray.values[i++].value; // the gap + if (i >= n_dash) { + i = 0; + } + } + tmp_pathpw3.concat(first_frag); // may merge line around start point + Geom::PathVector out_pathv = Geom::path_from_piecewise(tmp_pathpw3, 0.01); + print_pathv(out_pathv, tf); + } else { + print_pathv(pathv, tf); + } + + use_stroke = false; + use_fill = false; + + if (usebk) { // OPAQUE was set, revert to TRANSPARENT + usebk = false; + rec = U_EMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSETBKMODE_set"); + } + } + + return 0; +} + + +// Draws simple_shapes, those with closed EMR_* primitives, like polygons, rectangles and ellipses. +// These use whatever the current pen/brush are and need not be followed by a FILLPATH or STROKEPATH. +// For other paths it sets a few flags and returns. +bool PrintEmf::print_simple_shape(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * transform); + + int nodes = 0; + int moves = 0; + int lines = 0; + int curves = 0; + char *rec = nullptr; + + for (auto & pit : pv) { + moves++; + nodes++; + + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + nodes++; + + if (is_straight_curve(*cit)) { + lines++; + } else if (dynamic_cast<Geom::CubicBezier const *>(&*cit)) { + curves++; + } + } + } + + if (!nodes) { + return false; + } + + U_POINT *lpPoints = new U_POINT[moves + lines + curves * 3]; + int i = 0; + + /** + * For all Subpaths in the <path> + */ + for (auto & pit : pv) { + using Geom::X; + using Geom::Y; + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + int32_t const x0 = (int32_t) round(p0[X]); + int32_t const y0 = (int32_t) round(p0[Y]); + + lpPoints[i].x = x0; + lpPoints[i].y = y0; + i = i + 1; + + /** + * For all segments in the subpath + */ + for (Geom::Path::iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + i = i + 1; + } else if (Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*cit)) { + std::vector<Geom::Point> points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + lpPoints[i + 1].x = x2; + lpPoints[i + 1].y = y2; + lpPoints[i + 2].x = x3; + lpPoints[i + 2].y = y3; + i = i + 3; + } + } + } + + bool done = false; + bool closed = (lpPoints[0].x == lpPoints[i - 1].x) && (lpPoints[0].y == lpPoints[i - 1].y); + bool polygon = false; + bool rectangle = false; + bool ellipse = false; + + if (moves == 1 && moves + lines == nodes && closed) { + polygon = true; + // if (nodes==5) { // disable due to LP Bug 407394 + // if (lpPoints[0].x == lpPoints[3].x && lpPoints[1].x == lpPoints[2].x && + // lpPoints[0].y == lpPoints[1].y && lpPoints[2].y == lpPoints[3].y) + // { + // rectangle = true; + // } + // } + } else if (moves == 1 && nodes == 5 && moves + curves == nodes && closed) { + // if (lpPoints[0].x == lpPoints[1].x && lpPoints[1].x == lpPoints[11].x && + // lpPoints[5].x == lpPoints[6].x && lpPoints[6].x == lpPoints[7].x && + // lpPoints[2].x == lpPoints[10].x && lpPoints[3].x == lpPoints[9].x && lpPoints[4].x == lpPoints[8].x && + // lpPoints[2].y == lpPoints[3].y && lpPoints[3].y == lpPoints[4].y && + // lpPoints[8].y == lpPoints[9].y && lpPoints[9].y == lpPoints[10].y && + // lpPoints[5].y == lpPoints[1].y && lpPoints[6].y == lpPoints[0].y && lpPoints[7].y == lpPoints[11].y) + // { // disable due to LP Bug 407394 + // ellipse = true; + // } + } + + if (polygon || ellipse) { + + if (use_fill && !use_stroke) { // only fill + rec = selectobject_set(U_NULL_PEN, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set pen"); + } + } else if (!use_fill && use_stroke) { // only stroke + rec = selectobject_set(U_NULL_BRUSH, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set brush"); + } + } + + if (polygon) { + if (rectangle) { + U_RECTL rcl = rectl_set((U_POINTL) { + lpPoints[0].x, lpPoints[0].y + }, (U_POINTL) { + lpPoints[2].x, lpPoints[2].y + }); + rec = U_EMRRECTANGLE_set(rcl); + } else { + rec = U_EMRPOLYGON_set(U_RCL_DEF, nodes, lpPoints); + } + } else if (ellipse) { + U_RECTL rcl = rectl_set((U_POINTL) { + lpPoints[6].x, lpPoints[3].y + }, (U_POINTL) { + lpPoints[0].x, lpPoints[9].y + }); + rec = U_EMRELLIPSE_set(rcl); + } + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at retangle/ellipse/polygon"); + } + + done = true; + + // replace the handle we moved above, assuming there was something set already + if (use_fill && !use_stroke && hpen) { // only fill + rec = selectobject_set(hpen, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set pen"); + } + } else if (!use_fill && use_stroke && hbrush) { // only stroke + rec = selectobject_set(hbrush, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_simple_shape at selectobject_set brush"); + } + } + } + + delete[] lpPoints; + + return done; +} + +/** Some parts based on win32.cpp by Lauris Kaplinski <lauris@kaplinski.com>. Was a part of Inkscape + in the past (or will be in the future?) Not in current trunk. (4/19/2012) + + Limitations of this code: + 1. Transparency is lost on export. (Apparently a limitation of the EMF format.) + 2. Probably messes up if row stride != w*4 + 3. There is still a small memory leak somewhere, possibly in a pixbuf created in a routine + that calls this one and passes px, but never removes the rest of the pixbuf. The first time + this is called it leaked 5M (in one test) and each subsequent call leaked around 200K more. + If this routine is reduced to + if(1)return(0); + and called for a single 1280 x 1024 image then the program leaks 11M per call, or roughly the + size of two bitmaps. +*/ + +unsigned int PrintEmf::image( + Inkscape::Extension::Print * /* module */, /** not used */ + unsigned char *rgba_px, /** array of pixel values, Gdk::Pixbuf bitmap format */ + unsigned int w, /** width of bitmap */ + unsigned int h, /** height of bitmap */ + unsigned int rs, /** row stride (normally w*4) */ + Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other transforms, use the one from m_tr_stack */ + SPStyle const *style) /** provides indirect link to image object */ +{ + double x1, y1, dw, dh; + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + do_clip_if_present(style); // If clipping is needed set it up + + rec = U_EMRSETSTRETCHBLTMODE_set(U_COLORONCOLOR); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRHEADER"); + } + + x1 = tf_rect[4]; + y1 = tf_rect[5]; + dw = ((double) w) * tf_rect[0]; + dh = ((double) h) * tf_rect[3]; + Geom::Point pLL(x1, y1); + Geom::Point pLL2 = pLL * tf; //location of LL corner in Inkscape coordinates + + char *px; + uint32_t cbPx; + uint32_t colortype; + PU_RGBQUAD ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + PU_BITMAPINFO Bmi; + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, (char *) rgba_px, w, h, w * 4, colortype, 0, 1); + Bmih = bitmapinfoheader_set(w, h, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + + U_POINTL Dest = pointl_set(round(pLL2[Geom::X] * PX2WORLD), round(pLL2[Geom::Y] * PX2WORLD)); + U_POINTL cDest = pointl_set(round(dw * PX2WORLD), round(dh * PX2WORLD)); + U_POINTL Src = pointl_set(0, 0); + U_POINTL cSrc = pointl_set(w, h); + /* map the integer Dest coordinates back into pLL2, so that the rounded part does not destabilize the transform offset below */ + pLL2[Geom::X] = Dest.x; + pLL2[Geom::Y] = Dest.y; + pLL2 /= PX2WORLD; + if (!FixImageRot) { /* Rotate images - some programs cannot read them in correctly if they are rotated */ + tf[4] = tf[5] = 0.0; // get rid of the offset in the transform + Geom::Point pLL2prime = pLL2 * tf; + U_XFORM tmpTransform; + tmpTransform.eM11 = tf[0]; + tmpTransform.eM12 = tf[1]; + tmpTransform.eM21 = tf[2]; + tmpTransform.eM22 = tf[3]; + tmpTransform.eDx = (pLL2[Geom::X] - pLL2prime[Geom::X]) * PX2WORLD; + tmpTransform.eDy = (pLL2[Geom::Y] - pLL2prime[Geom::Y]) * PX2WORLD; + + rec = U_EMRSAVEDC_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSAVEDC_set"); + } + + rec = U_EMRMODIFYWORLDTRANSFORM_set(tmpTransform, U_MWT_LEFTMULTIPLY); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at EMRMODIFYWORLDTRANSFORM"); + } + } + rec = U_EMRSTRETCHDIBITS_set( + U_RCL_DEF, //! Bounding rectangle in device units + Dest, //! Destination UL corner in logical units + cDest, //! Destination W & H in logical units + Src, //! Source UL corner in logical units + cSrc, //! Source W & H in logical units + U_DIB_RGB_COLORS, //! DIBColors Enumeration + U_SRCCOPY, //! RasterOPeration Enumeration + Bmi, //! (Optional) bitmapbuffer (U_BITMAPINFO section) + h * rs, //! size in bytes of px + px //! (Optional) bitmapbuffer (U_BITMAPINFO section) + ); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRSTRETCHDIBITS_set"); + } + free(px); + free(Bmi); + if (numCt) { + free(ct); + } + + if (!FixImageRot) { + rec = U_EMRRESTOREDC_set(-1); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::image at U_EMRRESTOREDC_set"); + } + } + + return 0; +} + +unsigned int PrintEmf::draw_pathv_to_EMF(Geom::PathVector const &pathv, const Geom::Affine &transform) { + char *rec; + + /* inkscape to EMF scaling is done below, but NOT the rotation/translation transform, + that is handled by the EMF MODIFYWORLDTRANSFORM record + */ + + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * transform); + + rec = U_EMRBEGINPATH_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRBEGINPATH_set"); + } + + /** + * For all Subpaths in the <path> + */ + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + U_POINTL ptl = pointl_set((int32_t) round(p0[X]), (int32_t) round(p0[Y])); + rec = U_EMRMOVETOEX_set(ptl); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRMOVETOEX_set"); + } + + /** + * For all segments in the subpath + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + + ptl = pointl_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + rec = U_EMRLINETO_set(ptl); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRLINETO_set"); + } + } else if (Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*cit)) { + std::vector<Geom::Point> points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + U_POINTL pt[3]; + pt[0].x = x1; + pt[0].y = y1; + pt[1].x = x2; + pt[1].y = y2; + pt[2].x = x3; + pt[2].y = y3; + + rec = U_EMRPOLYBEZIERTO_set(U_RCL_DEF, 3, pt); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRPOLYBEZIERTO_set"); + } + } else { + g_warning("logical error, because pathv_to_linear_and_cubic_beziers was used"); + } + } + + if (pit.end_default() == pit.end_closed()) { // there may be multiples of this on a single path + rec = U_EMRCLOSEFIGURE_set(); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRCLOSEFIGURE_set"); + } + } + + } + + rec = U_EMRENDPATH_set(); // there may be only be one of these on a single path + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::print_pathv at U_EMRENDPATH_set"); + } + return(0); +} + +// may also be called with a simple_shape or an empty path, whereupon it just returns without doing anything +unsigned int PrintEmf::print_pathv(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + Geom::Affine tf = transform; + char *rec = nullptr; + + simple_shape = print_simple_shape(pathv, tf); + if (simple_shape || pathv.empty()) { + if (use_fill) { + destroy_brush(); // these must be cleared even if nothing is drawn or hbrush,hpen fill up + } + if (use_stroke) { + destroy_pen(); + } + return TRUE; + } + + (void) draw_pathv_to_EMF(pathv, tf); + + // explicit FILL/STROKE commands are needed for each sub section of the path + if (use_fill && !use_stroke) { + rec = U_EMRFILLPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::fill at U_EMRFILLPATH_set"); + } + } else if (use_fill && use_stroke) { + rec = U_EMRSTROKEANDFILLPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSTROKEANDFILLPATH_set"); + } + } else if (!use_fill && use_stroke) { + rec = U_EMRSTROKEPATH_set(U_RCL_DEF); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::stroke at U_EMRSTROKEPATH_set"); + } + } + + // clean out brush and pen, but only after all parts of the draw complete + if (use_fill) { + destroy_brush(); + } + if (use_stroke) { + destroy_pen(); + } + + return TRUE; +} + + +unsigned int PrintEmf::text(Inkscape::Extension::Print * /*mod*/, char const *text, Geom::Point const &p, + SPStyle const *const style) +{ + if (!et || !text) { + return 0; + } + + do_clip_if_present(style); // If clipping is needed set it up + char *rec = nullptr; + int ccount, newfont; + int fix90n = 0; + uint32_t hfont = 0; + Geom::Affine tf = m_tr_stack.top(); + double rot = -1800.0 * std::atan2(tf[1], tf[0]) / M_PI; // 0.1 degree rotation, - sign for MM_TEXT + double rotb = -std::atan2(tf[1], tf[0]); // rotation for baseline offset for superscript/subscript, used below + double dx, dy; + double ky; + + // the dx array is smuggled in like: text<nul>w1 w2 w3 ...wn<nul><nul>, where the widths are floats 7 characters wide, including the space + int ndx, rtl; + uint32_t *adx; + smuggle_adxkyrtl_out(text, &adx, &ky, &rtl, &ndx, PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); // side effect: free() adx + + uint32_t textalignment; + if (rtl > 0) { + textalignment = U_TA_BASELINE | U_TA_LEFT; + } else { + textalignment = U_TA_BASELINE | U_TA_RIGHT | U_TA_RTLREADING; + } + if (textalignment != htextalignment) { + htextalignment = textalignment; + rec = U_EMRSETTEXTALIGN_set(textalignment); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMRSETTEXTALIGN_set"); + } + } + + char *text2 = strdup(text); // because U_Utf8ToUtf16le calls iconv which does not like a const char * + uint16_t *unicode_text = U_Utf8ToUtf16le(text2, 0, nullptr); + free(text2); + //translates Unicode to NonUnicode, if possible. If any translate, all will, and all to + //the same font, because of code in Layout::print + UnicodeToNon(unicode_text, &ccount, &newfont); + + //PPT gets funky with text within +-1 degree of a multiple of 90, but only for SOME fonts.Snap those to the central value + //Some funky ones: Arial, Times New Roman + //Some not funky ones: Symbol and Verdana. + //Without a huge table we cannot catch them all, so just the most common problem ones. + FontfixParams params; + + if (FixPPTCharPos) { + switch (newfont) { + case CVTSYM: + _lookup_ppt_fontfix("Convert To Symbol", params); + break; + case CVTZDG: + _lookup_ppt_fontfix("Convert To Zapf Dingbats", params); + break; + case CVTWDG: + _lookup_ppt_fontfix("Convert To Wingdings", params); + break; + default: //also CVTNON + _lookup_ppt_fontfix(style->font_family.value(), params); + break; + } + if (params.f2 != 0 || params.f3 != 0) { + int irem = ((int) round(rot)) % 900 ; + if (irem <= 9 && irem >= -9) { + fix90n = 1; //assume vertical + rot = (double)(((int) round(rot)) - irem); + rotb = rot * M_PI / 1800.0; + if (std::abs(rot) == 900.0) { + fix90n = 2; + } + } + } + } + + /* Note that text font sizes are stored into the EMF as fairly small integers and that limits their precision. + The EMF output files produced here have been designed so that the integer valued pt sizes + land right on an integer value in the EMF file, so those are exact. However, something like 18.1 pt will be + somewhat off, so that when it is read back in it becomes 18.11 pt. (For instance.) + */ + int textheight = round(-style->font_size.computed * PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); + + if (!hfont) { + // Get font face name. Use changed font name if unicode mapped to one + // of the special fonts. + uint16_t *wfacename; + if (!newfont) { + wfacename = U_Utf8ToUtf16le(style->font_family.value(), 0, nullptr); + } else { + wfacename = U_Utf8ToUtf16le(FontName(newfont), 0, nullptr); + } + + // Scale the text to the minimum stretch. (It tends to stay within bounding rectangles even if + // it was streteched asymmetrically.) Few applications support text from EMF which is scaled + // differently by height/width, so leave lfWidth alone. + + U_LOGFONT lf = logfont_set( + textheight, + 0, + round(rot), + round(rot), + _translate_weight(style->font_weight.computed), + (style->font_style.computed == SP_CSS_FONT_STYLE_ITALIC), + style->text_decoration_line.underline, + style->text_decoration_line.line_through, + U_DEFAULT_CHARSET, + U_OUT_DEFAULT_PRECIS, + U_CLIP_DEFAULT_PRECIS, + U_DEFAULT_QUALITY, + U_DEFAULT_PITCH | U_FF_DONTCARE, + wfacename); + free(wfacename); + + rec = extcreatefontindirectw_set(&hfont, eht, (char *) &lf, nullptr); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at extcreatefontindirectw_set"); + } + } + + rec = selectobject_set(hfont, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at selectobject_set"); + } + + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + // only change the text color when it needs to be changed + if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { + memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); + rec = U_EMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMRSETTEXTCOLOR_set"); + } + } + + Geom::Point p2 = p * tf; + + //Handle super/subscripts and vertical kerning + /* Previously used this, but vertical kerning was not supported + p2[Geom::X] -= style->baseline_shift.computed * std::sin( rotb ); + p2[Geom::Y] -= style->baseline_shift.computed * std::cos( rotb ); + */ + p2[Geom::X] += ky * std::sin(rotb); + p2[Geom::Y] += ky * std::cos(rotb); + + //Conditionally handle compensation for PPT EMF import bug (affects PPT 2003-2010, at least) + if (FixPPTCharPos) { + if (fix90n == 1) { //vertical + dx = 0.0; + dy = params.f3 * style->font_size.computed * std::cos(rotb); + } else if (fix90n == 2) { //horizontal + dx = params.f2 * style->font_size.computed * std::sin(rotb); + dy = 0.0; + } else { + dx = params.f1 * style->font_size.computed * std::sin(rotb); + dy = params.f1 * style->font_size.computed * std::cos(rotb); + } + p2[Geom::X] += dx; + p2[Geom::Y] += dy; + } + + p2[Geom::X] = (p2[Geom::X] * PX2WORLD); + p2[Geom::Y] = (p2[Geom::Y] * PX2WORLD); + + int32_t const xpos = (int32_t) round(p2[Geom::X]); + int32_t const ypos = (int32_t) round(p2[Geom::Y]); + + + // The number of characters in the string is a bit fuzzy. ndx, the number of entries in adx is + // the number of VISIBLE characters, since some may combine from the UTF (8 originally, + // now 16) encoding. Conversely strlen() or wchar16len() would give the absolute number of + // encoding characters. Unclear if emrtext wants the former or the latter but for now assume the former. + + // This is currently being smuggled in from caller as part of text, works + // MUCH better than the fallback hack below + // uint32_t *adx = dx_set(textheight, U_FW_NORMAL, slen); // dx is needed, this makes one up + char *rec2; + if (rtl > 0) { + rec2 = emrtext_set((U_POINTL) { + xpos, ypos + }, ndx, 2, unicode_text, U_ETO_NONE, U_RCL_DEF, adx); + } else { // RTL text, U_TA_RTLREADING should be enough, but set this one too just in case + rec2 = emrtext_set((U_POINTL) { + xpos, ypos + }, ndx, 2, unicode_text, U_ETO_RTLREADING, U_RCL_DEF, adx); + } + free(unicode_text); + free(adx); + rec = U_EMREXTTEXTOUTW_set(U_RCL_DEF, U_GM_COMPATIBLE, 1.0, 1.0, (PU_EMRTEXT)rec2); + free(rec2); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at U_EMREXTTEXTOUTW_set"); + } + + // Must deselect an object before deleting it. Put the default font (back) in. + rec = selectobject_set(U_DEVICE_DEFAULT_FONT, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at selectobject_set"); + } + + if (hfont) { + rec = deleteobject_set(&hfont, eht); + if (!rec || emf_append((PU_ENHMETARECORD)rec, et, U_REC_FREE)) { + g_error("Fatal programming error in PrintEmf::text at deleteobject_set"); + } + } + + return 0; +} + +void PrintEmf::init() +{ + /* EMF print */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>Enhanced Metafile Print</name>\n" + "<id>org.inkscape.print.emf</id>\n" + "<param gui-hidden=\"true\" name=\"destination\" type=\"string\"></param>\n" + "<param gui-hidden=\"true\" name=\"textToPath\" type=\"bool\">true</param>\n" + "<param gui-hidden=\"true\" name=\"pageBoundingBox\" type=\"bool\">true</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTCharPos\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTDashLine\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTGrad2Polys\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTLinGrad\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTPatternAsHatch\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixImageRot\" type=\"bool\">false</param>\n" + "<print/>\n" + "</inkscape-extension>", new PrintEmf()); + // clang-format on + + return; +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/emf-print.h b/src/extension/internal/emf-print.h new file mode 100644 index 0000000..9370ef2 --- /dev/null +++ b/src/extension/internal/emf-print.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Enhanced Metafile printing - implementation + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * David Mathog + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_EMF_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_EMF_PRINT_H + +#include <3rdparty/libuemf/uemf.h> +#include "extension/internal/metafile-print.h" + +class SPItem; + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintEmf : public PrintMetafile +{ + uint32_t hbrush, hbrushOld, hpen; + + unsigned int print_pathv (Geom::PathVector const &pathv, const Geom::Affine &transform); + bool print_simple_shape (Geom::PathVector const &pathv, const Geom::Affine &transform); + +public: + PrintEmf(); + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int fill (Inkscape::Extension::Print *module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print * module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int image(Inkscape::Extension::Print *module, + unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style) override; + unsigned int text(Inkscape::Extension::Print *module, char const *text, + Geom::Point const &p, SPStyle const *style) override; + + static void init (); +protected: + static void smuggle_adxkyrtl_out(const char *string, uint32_t **adx, double *ky, int *rtl, int *ndx, float scale); + + void do_clip_if_present(SPStyle const *style); + Geom::PathVector merge_PathVector_with_group(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform); + Geom::PathVector merge_PathVector_with_shape(Geom::PathVector const &combined_pathvector, SPItem const *item, const Geom::Affine &transform); + unsigned int draw_pathv_to_EMF(Geom::PathVector const &pathv, const Geom::Affine &transform); + Geom::Path pathv_to_simple_polygon(Geom::PathVector const &pathv, int *vertices); + Geom::Path pathv_to_rect(Geom::PathVector const &pathv, bool *is_rect, double *angle); + Geom::Point get_pathrect_corner(Geom::Path pathRect, double angle, int corner); + U_TRIVERTEX make_trivertex(Geom::Point Pt, U_COLORREF uc); + int vector_rect_alignment(double angle, Geom::Point vtest); + int create_brush(SPStyle const *style, PU_COLORREF fcolor) override; + void destroy_brush() override; + int create_pen(SPStyle const *style, const Geom::Affine &transform) override; + void destroy_pen() override; +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_EMF_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/filter/BUILD_YOUR_OWN b/src/extension/internal/filter/BUILD_YOUR_OWN new file mode 100644 index 0000000..7598499 --- /dev/null +++ b/src/extension/internal/filter/BUILD_YOUR_OWN @@ -0,0 +1,2 @@ +This directory contains filter effects. They're designed to be simple. +Very, very simple. Here is how to build your own. diff --git a/src/extension/internal/filter/bevels.h b/src/extension/internal/filter/bevels.h new file mode 100644 index 0000000..a8382ba --- /dev/null +++ b/src/extension/internal/filter/bevels.h @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ +/* Change the 'BEVELS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Bevel filters + * Diffuse light + * Matte jelly + * Specular light + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Diffuse light filter. + + Basic diffuse bevel to use for building textures + + Filter's parameters: + * Smoothness (0.->10., default 6.) -> blur (stdDeviation) + * Elevation (0->360, default 25) -> feDistantLight (elevation) + * Azimuth (0->360, default 235) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> diffuse (lighting-color) +*/ + +class DiffuseLight : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + DiffuseLight ( ) : Filter() { }; + ~DiffuseLight ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Diffuse Light") "</name>\n" + "<id>org.inkscape.effect.filter.DiffuseLight</id>\n" + "<param name=\"smooth\" gui-text=\"" N_("Smoothness") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"10\">6</param>\n" + "<param name=\"elevation\" gui-text=\"" N_("Elevation (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">25</param>\n" + "<param name=\"azimuth\" gui-text=\"" N_("Azimuth (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">235</param>\n" + "<param name=\"color\" gui-text=\"" N_("Lighting color") "\" type=\"color\">-1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Bevels") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Basic diffuse bevel to use for building textures") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new DiffuseLight()); + // clang-format on + }; + +}; + +gchar const * +DiffuseLight::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Diffuse Light\">\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s\" result=\"blur\" />\n" + "<feDiffuseLighting diffuseConstant=\"1\" surfaceScale=\"10\" lighting-color=\"rgb(%s,%s,%s)\" result=\"diffuse\">\n" + "<feDistantLight elevation=\"%s\" azimuth=\"%s\" />\n" + "</feDiffuseLighting>\n" + "<feComposite in=\"diffuse\" in2=\"diffuse\" operator=\"arithmetic\" k1=\"1\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"SourceGraphic\" k1=\"%s\" operator=\"arithmetic\" k3=\"1\" result=\"composite2\" />\n" + "</filter>\n", smooth.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* DiffuseLight filter */ + +/** + \brief Custom predefined Matte jelly filter. + + Bulging, matte jelly covering + + Filter's parameters: + * Smoothness (0.0->10., default 7.) -> blur (stdDeviation) + * Brightness (0.0->5., default .9) -> specular (specularConstant) + * Elevation (0->360, default 60) -> feDistantLight (elevation) + * Azimuth (0->360, default 225) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> specular (lighting-color) +*/ + +class MatteJelly : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + MatteJelly ( ) : Filter() { }; + ~MatteJelly ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Matte Jelly") "</name>\n" + "<id>org.inkscape.effect.filter.MatteJelly</id>\n" + "<param name=\"smooth\" gui-text=\"" N_("Smoothness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"10.00\">7</param>\n" + "<param name=\"bright\" gui-text=\"" N_("Brightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"5.00\">0.9</param>\n" + "<param name=\"elevation\" gui-text=\"" N_("Elevation (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">60</param>\n" + "<param name=\"azimuth\" gui-text=\"" N_("Azimuth (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">225</param>\n" + "<param name=\"color\" gui-text=\"" N_("Lighting color") "\" type=\"color\">-1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Bevels") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Bulging, matte jelly covering") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new MatteJelly()); + // clang-format on + }; + +}; + +gchar const * +MatteJelly::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream bright; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + bright << ext->get_param_float("bright"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Matte Jelly\">\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.85 0\" result=\"color\" in=\"SourceGraphic\" />\n" + "<feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"%s\" result=\"blur\" />\n" + "<feSpecularLighting in=\"blur\" specularExponent=\"25\" specularConstant=\"%s\" surfaceScale=\"5\" lighting-color=\"rgb(%s,%s,%s)\" result=\"specular\">\n" + "<feDistantLight elevation=\"%s\" azimuth=\"%s\" />\n" + "</feSpecularLighting>\n" + "<feComposite in=\"specular\" in2=\"SourceGraphic\" k3=\"1\" k2=\"%s\" operator=\"arithmetic\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"color\" operator=\"atop\" result=\"composite2\" />\n" + "</filter>\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* MatteJelly filter */ + +/** + \brief Custom predefined Specular light filter. + + Basic specular bevel to use for building textures + + Filter's parameters: + * Smoothness (0.0->10., default 6.) -> blur (stdDeviation) + * Brightness (0.0->5., default 1.) -> specular (specularConstant) + * Elevation (0->360, default 45) -> feDistantLight (elevation) + * Azimuth (0->360, default 235) -> feDistantLight (azimuth) + * Lighting color (guint, default -1 [white]) -> specular (lighting-color) +*/ + +class SpecularLight : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + SpecularLight ( ) : Filter() { }; + ~SpecularLight ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Specular Light") "</name>\n" + "<id>org.inkscape.effect.filter.SpecularLight</id>\n" + "<param name=\"smooth\" gui-text=\"" N_("Smoothness") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"10\">6</param>\n" + "<param name=\"bright\" gui-text=\"" N_("Brightness") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"5\">1</param>\n" + "<param name=\"elevation\" gui-text=\"" N_("Elevation (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">45</param>\n" + "<param name=\"azimuth\" gui-text=\"" N_("Azimuth (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">235</param>\n" + "<param name=\"color\" gui-text=\"" N_("Lighting color") "\" type=\"color\">-1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Bevels") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Basic specular bevel to use for building textures") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new SpecularLight()); + // clang-format on + }; + +}; + +gchar const * +SpecularLight::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream smooth; + std::ostringstream bright; + std::ostringstream elevation; + std::ostringstream azimuth; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + + smooth << ext->get_param_float("smooth"); + bright << ext->get_param_float("bright"); + elevation << ext->get_param_int("elevation"); + azimuth << ext->get_param_int("azimuth"); + guint32 color = ext->get_param_color("color"); + + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Specular Light\">\n" + "<feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"%s\" result=\"blur\" />\n" + "<feSpecularLighting in=\"blur\" specularExponent=\"25\" specularConstant=\"%s\" surfaceScale=\"10\" lighting-color=\"rgb(%s,%s,%s)\" result=\"specular\">\n" + "<feDistantLight elevation=\"%s\" azimuth=\"%s\" />\n" + "</feSpecularLighting>\n" + "<feComposite in=\"specular\" in2=\"SourceGraphic\" k3=\"1\" k2=\"%s\" operator=\"arithmetic\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"SourceAlpha\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", smooth.str().c_str(), bright.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), elevation.str().c_str(), azimuth.str().c_str(), a.str().c_str()); + + return _filter; +}; /* SpecularLight filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BEVELS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BEVELS_H__ */ diff --git a/src/extension/internal/filter/blurs.h b/src/extension/internal/filter/blurs.h new file mode 100644 index 0000000..85f99fd --- /dev/null +++ b/src/extension/internal/filter/blurs.h @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ +/* Change the 'BLURS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Blur filters + * Blur + * Clean edges + * Cross blur + * Feather + * Out of focus + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Blur filter. + + Simple horizontal and vertical blur + + Filter's parameters: + * Horizontal blur (0.01->100., default 2) -> blur (stdDeviation) + * Vertical blur (0.01->100., default 2) -> blur (stdDeviation) + * Blur content only (boolean, default false) -> +*/ + +class Blur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Blur ( ) : Filter() { }; + ~Blur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Blur") "</name>\n" + "<id>org.inkscape.effect.filter.Blur</id>\n" + "<param name=\"hblur\" gui-text=\"" N_("Horizontal blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">2</param>\n" + "<param name=\"vblur\" gui-text=\"" N_("Vertical blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">2</param>\n" + "<param name=\"content\" gui-text=\"" N_("Blur content only") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Blurs") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Simple vertical and horizontal blur effect") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Blur()); + // clang-format on + }; + +}; + +gchar const * +Blur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream bbox; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream content; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + + if (ext->get_param_bool("content")) { + bbox << "height=\"1\" width=\"1\" y=\"0\" x=\"0\""; + content << "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 50 0 \" result=\"colormatrix\" />\n" + << "<feComposite in=\"colormatrix\" in2=\"SourceGraphic\" operator=\"in\" />\n"; + } else { + bbox << "" ; + content << "" ; + } + + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" %s style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Blur\">\n" + "<feGaussianBlur stdDeviation=\"%s %s\" result=\"blur\" />\n" + "%s" + "</filter>\n", bbox.str().c_str(), hblur.str().c_str(), vblur.str().c_str(), content.str().c_str() ); + // clang-format on + + return _filter; +}; /* Blur filter */ + +/** + \brief Custom predefined Clean edges filter. + + Removes or decreases glows and jaggeries around objects edges after applying some filters + + Filter's parameters: + * Strength (0.01->2., default 0.4) -> blur (stdDeviation) +*/ + +class CleanEdges : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CleanEdges ( ) : Filter() { }; + ~CleanEdges ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Clean Edges") "</name>\n" + "<id>org.inkscape.effect.filter.CleanEdges</id>\n" + "<param name=\"blur\" gui-text=\"" N_("Strength") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"2.00\">0.4</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Blurs") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Removes or decreases glows and jaggeries around objects edges after applying some filters") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new CleanEdges()); + // clang-format on + }; + +}; + +gchar const * +CleanEdges::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + + blur << ext->get_param_float("blur"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Clean Edges\">\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur\" />\n" + "<feComposite in=\"SourceGraphic\" in2=\"blur\" operator=\"in\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"composite1\" k2=\"1\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", blur.str().c_str()); + // clang-format on + + return _filter; +}; /* CleanEdges filter */ + +/** + \brief Custom predefined Cross blur filter. + + Combine vertical and horizontal blur + + Filter's parameters: + * Brightness (0.->10., default 0) -> composite (k3) + * Fading (0.->1., default 0) -> composite (k4) + * Horizontal blur (0.01->20., default 5) -> blur (stdDeviation) + * Vertical blur (0.01->20., default 5) -> blur (stdDeviation) + * Blend mode (enum, default Darken) -> blend (mode) +*/ + +class CrossBlur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CrossBlur ( ) : Filter() { }; + ~CrossBlur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Cross Blur") "</name>\n" + "<id>org.inkscape.effect.filter.CrossBlur</id>\n" + "<param name=\"bright\" gui-text=\"" N_("Brightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"10.00\">0</param>\n" + "<param name=\"fade\" gui-text=\"" N_("Fading") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1.00\">0</param>\n" + "<param name=\"hblur\" gui-text=\"" N_("Horizontal blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">5</param>\n" + "<param name=\"vblur\" gui-text=\"" N_("Vertical blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">5</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Blurs") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Combine vertical and horizontal blur") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new CrossBlur()); + // clang-format on + }; + +}; + +gchar const * +CrossBlur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream bright; + std::ostringstream fade; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream blend; + + bright << ext->get_param_float("bright"); + fade << ext->get_param_float("fade"); + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + blend << ext->get_param_optiongroup("blend"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Cross Blur\">\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"colormatrix\" />\n" + "<feComposite in=\"SourceGraphic\" in2=\"colormatrix\" operator=\"arithmetic\" k2=\"1\" k3=\"%s\" k4=\"%s\" result=\"composite\" />\n" + "<feGaussianBlur stdDeviation=\"%s 0.01\" result=\"blur1\" />\n" + "<feGaussianBlur in=\"composite\" stdDeviation=\"0.01 %s\" result=\"blur2\" />\n" + "<feBlend in=\"blur2\" in2=\"blur1\" mode=\"%s\" result=\"blend\" />\n" + "</filter>\n", bright.str().c_str(), fade.str().c_str(), hblur.str().c_str(), vblur.str().c_str(), blend.str().c_str()); + // clang-format on + + return _filter; +}; /* Cross blur filter */ + +/** + \brief Custom predefined Feather filter. + + Blurred mask on the edge without altering the contents + + Filter's parameters: + * Strength (0.01->100., default 5) -> blur (stdDeviation) +*/ + +class Feather : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Feather ( ) : Filter() { }; + ~Feather ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Feather") "</name>\n" + "<id>org.inkscape.effect.filter.Feather</id>\n" + "<param name=\"blur\" gui-text=\"" N_("Strength") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Blurs") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blurred mask on the edge without altering the contents") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Feather()); + // clang-format on + }; + +}; + +gchar const * +Feather::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + + blur << ext->get_param_float("blur"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Feather\">\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur\" />\n" + "<feComposite in=\"SourceGraphic\" in2=\"blur\" operator=\"atop\" result=\"composite1\" />\n" + "<feComposite in2=\"composite1\" operator=\"in\" result=\"composite2\" />\n" + "<feComposite in2=\"composite2\" operator=\"in\" result=\"composite3\" />\n" + "</filter>\n", blur.str().c_str()); + // clang-format on + + return _filter; +}; /* Feather filter */ + +/** + \brief Custom predefined Out of Focus filter. + + Blur eroded by white or transparency + + Filter's parameters: + * Horizontal blur (0.01->10., default 3) -> blur (stdDeviation) + * Vertical blur (0.01->10., default 3) -> blur (stdDeviation) + * Dilatation (n-1th value, 0.->100., default 6) -> colormatrix2 (matrix) + * Erosion (nth value, 0.->100., default 2) -> colormatrix2 (matrix) + * Opacity (0.->1., default 1.) -> composite1 (k2) + * Background color (guint, default -1) -> flood (flood-opacity, flood-color) + * Blend type (enum, default normal) -> blend (mode) + * Blend to background (boolean, default false) -> blend (false: in2="flood", true: in2="BackgroundImage") + +*/ + +class ImageBlur : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ImageBlur ( ) : Filter() { }; + ~ImageBlur ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Out of Focus") "</name>\n" + "<id>org.inkscape.effect.filter.ImageBlur</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"hblur\" gui-text=\"" N_("Horizontal blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"50.00\">3</param>\n" + "<param name=\"vblur\" gui-text=\"" N_("Vertical blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"50.00\">3</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">6</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">2</param>\n" + "<param name=\"opacity\" gui-text=\"" N_("Opacity") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1\">1</param>\n" + "</page>\n" + "<page name=\"backgroundtab\" gui-text=\"Background\">\n" + "<param name=\"color\" gui-text=\"" N_("Background color") "\" type=\"color\">-1</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "<param name=\"background\" gui-text=\"" N_("Blend to background") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Blurs") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blur eroded by white or transparency") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ImageBlur()); + // clang-format on + }; + +}; + +gchar const * +ImageBlur::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream opacity; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream blend; + std::ostringstream background; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + dilat << ext->get_param_float("dilat"); + erosion << -ext->get_param_float("erosion"); + opacity << ext->get_param_float("opacity"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + blend << ext->get_param_optiongroup("blend"); + + if (ext->get_param_bool("background")) { + background << "BackgroundImage" ; + } else { + background << "flood" ; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Out of Focus\">\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"colormatrix1\" />\n" + "<feGaussianBlur in=\"colormatrix1\" stdDeviation=\"%s %s\" result=\"blur\" />\n" + "<feColorMatrix in=\"blur\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix2\" />\n" + "<feBlend in=\"colormatrix2\" in2=\"%s\" mode=\"%s\" result=\"blend\" />\n" + "<feComposite in=\"blend\" in2=\"blend\" operator=\"arithmetic\" k2=\"%s\" result=\"composite1\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" />\n" + "</filter>\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + hblur.str().c_str(), vblur.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), + background.str().c_str(), blend.str().c_str(), opacity.str().c_str()); + // clang-format on + + return _filter; +}; /* Out of Focus filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BLURS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BLURS_H__ */ diff --git a/src/extension/internal/filter/bumps.h b/src/extension/internal/filter/bumps.h new file mode 100644 index 0000000..4db33d6 --- /dev/null +++ b/src/extension/internal/filter/bumps.h @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ +/* Change the 'BUMPS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Bump filters + * Bump + * Wax bump + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Bump filter. + + All purpose bump filter + + Filter's parameters: + Options + * Image simplification (0.01->10., default 0.01) -> blur1 (stdDeviation) + * Bump simplification (0.01->10., default 0.01) -> blur2 (stdDeviation) + * Crop (-50.->50., default 0.) -> composite1 (k3) + * Red (-50.->50., default 0.) -> colormatrix1 (values) + * Green (-50.->50., default 0.) -> colormatrix1 (values) + * Blue (-50.->50., default 0.) -> colormatrix1 (values) + * Bump from background (boolean, default false) -> colormatrix1 (false: in="SourceGraphic", true: in="BackgroundImage") + Lighting + * Lighting type (enum, default specular) -> lighting block + * Height (0.->50., default 5.) -> lighting (surfaceScale) + * Lightness (0.->5., default 1.) -> lighting [diffuselighting (diffuseConstant)|specularlighting (specularConstant)] + * Precision (1->128, default 15) -> lighting (specularExponent) + * Color (guint, default -1 (RGB:255,255,255))-> lighting (lighting-color) + Light source + * Azimuth (0->360, default 225) -> lightsOptions (distantAzimuth) + * Elevation (0->180, default 45) -> lightsOptions (distantElevation) + * X location [point] (-5000->5000, default 526) -> lightsOptions (x) + * Y location [point] (-5000->5000, default 372) -> lightsOptions (y) + * Z location [point] (0->5000, default 150) -> lightsOptions (z) + * X location [spot] (-5000->5000, default 526) -> lightsOptions (x) + * Y location [spot] (-5000->5000, default 372) -> lightsOptions (y) + * Z location [spot] (-5000->5000, default 150) -> lightsOptions (z) + * X target (-5000->5000, default 0) -> lightsOptions (pointsAtX) + * Y target (-5000->5000, default 0) -> lightsOptions (pointsAtX) + * Z target (-5000->0, default -1000) -> lightsOptions (pointsAtX) + * Specular exponent (1->100, default 1) -> lightsOptions (specularExponent) + * Cone angle (0->100, default 50) -> lightsOptions (limitingConeAngle) + Color bump + * Blend type (enum, default normal) -> blend (mode) + * Image color (guint, default -987158017 (RGB:197,41,41)) -> flood (flood-color) + * Color bump (boolean, default false) -> composite2 (false: in="diffuselighting", true in="flood") +*/ + +class Bump : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Bump ( ) : Filter() { }; + ~Bump ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Bump") "</name>\n" + "<id>org.inkscape.effect.filter.Bump</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"simplifyImage\" gui-text=\"" N_("Image simplification") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"10.00\">0.01</param>\n" + "<param name=\"simplifyBump\" gui-text=\"" N_("Bump simplification") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"10.00\">0.01</param>\n" + "<param name=\"crop\" gui-text=\"" N_("Crop") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-50.\" max=\"50.\">0</param>\n" + "<label appearance=\"header\">" N_("Bump source") "</label>\n" + "<param name=\"red\" gui-text=\"" N_("Red") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-50.\" max=\"50.\">0</param>\n" + "<param name=\"green\" gui-text=\"" N_("Green") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-50.\" max=\"50.\">0</param>\n" + "<param name=\"blue\" gui-text=\"" N_("Blue") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-50.\" max=\"50.\">0</param>\n" + "<param name=\"background\" gui-text=\"" N_("Bump from background") "\" indent=\"1\" type=\"bool\">false</param>\n" + "</page>\n" + "<page name=\"lightingtab\" gui-text=\"Lighting\">\n" + "<param name=\"lightType\" gui-text=\"" N_("Lighting type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"specular\">" N_("Specular") "</option>\n" + "<option value=\"diffuse\">" N_("Diffuse") "</option>\n" + "</param>\n" + "<param name=\"height\" gui-text=\"" N_("Height") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"50.\">5</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"5.\">1</param>\n" + "<param name=\"precision\" gui-text=\"" N_("Precision") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"128\">15</param>\n" + "<param name=\"lightingColor\" gui-text=\"" N_("Color") "\" type=\"color\">-1</param>\n" + "</page>\n" + "<page name=\"lightsourcetab\" gui-text=\"" N_("Light source") "\">\n" + "<param name=\"lightSource\" gui-text=\"" N_("Light source:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"distant\">" N_("Distant") "</option>\n" + "<option value=\"point\">" N_("Point") "</option>\n" + "<option value=\"spot\">" N_("Spot") "</option>\n" + "</param>\n" + "<label appearance=\"header\">" N_("Distant light options") "</label>\n" + "<param name=\"distantAzimuth\" gui-text=\"" N_("Azimuth") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"360\">225</param>\n" + "<param name=\"distantElevation\" gui-text=\"" N_("Elevation") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"180\">45</param>\n" + "<label appearance=\"header\">" N_("Point light options") "</label>\n" + "<param name=\"pointX\" gui-text=\"" N_("X location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">526</param>\n" + "<param name=\"pointY\" gui-text=\"" N_("Y location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">372</param>\n" + "<param name=\"pointZ\" gui-text=\"" N_("Z location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"5000\">150</param>\n" + "<label appearance=\"header\">" N_("Spot light options") "</label>\n" + "<param name=\"spotX\" gui-text=\"" N_("X location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">526</param>\n" + "<param name=\"spotY\" gui-text=\"" N_("Y location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">372</param>\n" + "<param name=\"spotZ\" gui-text=\"" N_("Z location") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">150</param>\n" + "<param name=\"spotAtX\" gui-text=\"" N_("X target") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">0</param>\n" + "<param name=\"spotAtY\" gui-text=\"" N_("Y target") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"5000\">0</param>\n" + "<param name=\"spotAtZ\" gui-text=\"" N_("Z target") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"-5000\" max=\"0\">-1000</param>\n" + "<param name=\"spotExponent\" gui-text=\"" N_("Specular exponent") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"1\" max=\"100\">1</param>\n" + "<param name=\"spotConeAngle\" gui-text=\"" N_("Cone angle") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"100\">50</param>\n" + "</page>\n" + "<page name=\"colortab\" gui-text=\"Color bump\">\n" + "<param name=\"imageColor\" gui-text=\"" N_("Image color") "\" type=\"color\">-987158017</param>\n" + "<param name=\"colorize\" gui-text=\"" N_("Color bump") "\" type=\"bool\" >false</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Bumps") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("All purposes bump filter") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Bump()); + // clang-format on + }; + +}; + +gchar const * +Bump::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simplifyImage; + std::ostringstream simplifyBump; + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream crop; + std::ostringstream bumpSource; + std::ostringstream blend; + + std::ostringstream lightStart; + std::ostringstream lightOptions; + std::ostringstream lightEnd; + + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + std::ostringstream colorize; + + + simplifyImage << ext->get_param_float("simplifyImage"); + simplifyBump << ext->get_param_float("simplifyBump"); + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + crop << ext->get_param_float("crop"); + blend << ext->get_param_optiongroup("blend"); + + guint32 lightingColor = ext->get_param_color("lightingColor"); + guint32 imageColor = ext->get_param_color("imageColor"); + + if (ext->get_param_bool("background")) { + bumpSource << "BackgroundImage" ; + } else { + bumpSource << "blur1" ; + } + + const gchar *lightType = ext->get_param_optiongroup("lightType"); + if ((g_ascii_strcasecmp("specular", lightType) == 0)) { + // Specular + lightStart << "<feSpecularLighting lighting-color=\"rgb(" << ((lightingColor >> 24) & 0xff) << "," + << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + << ext->get_param_float("height") << "\" specularConstant=\"" << ext->get_param_float("lightness") + << "\" specularExponent=\"" << ext->get_param_int("precision") << "\" result=\"lighting\">"; + lightEnd << "</feSpecularLighting>"; + } else { + // Diffuse + lightStart << "<feDiffuseLighting lighting-color=\"rgb(" << ((lightingColor >> 24) & 0xff) << "," + << ((lightingColor >> 16) & 0xff) << "," << ((lightingColor >> 8) & 0xff) << ")\" surfaceScale=\"" + << ext->get_param_float("height") << "\" diffuseConstant=\"" << ext->get_param_float("lightness") + << "\" result=\"lighting\">"; + lightEnd << "</feDiffuseLighting>"; + } + + const gchar *lightSource = ext->get_param_optiongroup("lightSource"); + if ((g_ascii_strcasecmp("distant", lightSource) == 0)) { + // Distant + lightOptions << "<feDistantLight azimuth=\"" << ext->get_param_int("distantAzimuth") << "\" elevation=\"" + << ext->get_param_int("distantElevation") << "\" />"; + } else if ((g_ascii_strcasecmp("point", lightSource) == 0)) { + // Point + lightOptions << "<fePointLight z=\"" << ext->get_param_int("pointX") << "\" y=\"" << ext->get_param_int("pointY") + << "\" x=\"" << ext->get_param_int("pointZ") << "\" />"; + } else { + // Spot + lightOptions << "<feSpotLight x=\"" << ext->get_param_int("pointX") << "\" y=\"" << ext->get_param_int("pointY") + << "\" z=\"" << ext->get_param_int("pointZ") << "\" pointsAtX=\"" << ext->get_param_int("spotAtX") + << "\" pointsAtY=\"" << ext->get_param_int("spotAtY") << "\" pointsAtZ=\"" << ext->get_param_int("spotAtZ") + << "\" specularExponent=\"" << ext->get_param_int("spotExponent") + << "\" limitingConeAngle=\"" << ext->get_param_int("spotConeAngle") + << "\" />"; + } + + floodRed << ((imageColor >> 24) & 0xff); + floodGreen << ((imageColor >> 16) & 0xff); + floodBlue << ((imageColor >> 8) & 0xff); + floodAlpha << (imageColor & 0xff) / 255.0F; + + if (ext->get_param_bool("colorize")) { + colorize << "flood" ; + } else { + colorize << "blur1" ; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Bump\">\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feColorMatrix in=\"%s\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 %s %s %s 1 0 \" result=\"colormatrix1\" />\n" + "<feColorMatrix in=\"colormatrix1\" type=\"luminanceToAlpha\" result=\"colormatrix2\" />\n" + "<feComposite in2=\"blur1\" operator=\"arithmetic\" k2=\"1\" k3=\"%s\" result=\"composite1\" />\n" + "<feGaussianBlur in=\"composite1\" stdDeviation=\"%s\" result=\"blur2\" />\n" + "%s\n" + "%s\n" + "%s\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" result=\"flood\" />\n" + "<feComposite in=\"lighting\" in2=\"%s\" operator=\"arithmetic\" k3=\"1\" k2=\"1\" result=\"composite2\" />\n" + "<feBlend in2=\"SourceGraphic\" mode=\"%s\" result=\"blend\" />\n" + "<feComposite in=\"blend\" in2=\"SourceGraphic\" operator=\"in\" k2=\"1\" result=\"composite3\" />\n" + "</filter>\n", simplifyImage.str().c_str(), bumpSource.str().c_str(), red.str().c_str(), green.str().c_str(), blue.str().c_str(), + crop.str().c_str(), simplifyBump.str().c_str(), + lightStart.str().c_str(), lightOptions.str().c_str(), lightEnd.str().c_str(), + floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + colorize.str().c_str(), blend.str().c_str()); + // clang-format on + + return _filter; + +}; /* Bump filter */ + +/** + \brief Custom predefined Wax Bump filter. + + Turns an image to jelly + + Filter's parameters: + Options + * Image simplification (0.01->10., default 1.5) -> blur1 (stdDeviation) + * Bump simplification (0.01->10., default 1) -> blur2 (stdDeviation) + * Crop (-10.->10., default 1.) -> colormatrix2 (4th value of the last line) + * Red (-10.->10., default 0.) -> colormatrix2 (values, substract 0.21) + * Green (-10.->10., default 0.) -> colormatrix2 (values, substract 0.72) + * Blue (-10.->10., default 0.) -> colormatrix2 (values, substract 0.07) + * Background (enum, default color) -> + * color: colormatrix1 (in="flood1") + * image: colormatrix1 (in="SourceGraphic") + * blurred image: colormatrix1 (in="blur1") + * Background opacity (0.->1., default 0) -> colormatrix1 (last value) + Lighting (specular, distant light) + * Color (guint, default -1 (RGB:255,255,255))-> lighting (lighting-color) + * Height (-50.->50., default 5.) -> lighting (surfaceScale) + * Lightness (0.->10., default 1.4) -> lighting [diffuselighting (diffuseConstant)|specularlighting (specularConstant)] + * Precision (0->50, default 35) -> lighting (specularExponent) + * Azimuth (0->360, default 225) -> lightsOptions (distantAzimuth) + * Elevation (0->180, default 60) -> lightsOptions (distantElevation) + * Lighting blend (enum, default screen) -> blend1 (mode) + * Highlight blend (enum, default screen) -> blend2 (mode) + Bump + * Transparency type (enum [in,atop], default atop) -> composite2 (operator) + * Color (guint, default -520083713 (RGB:225,0,38)) -> flood2 (flood-color) + * Revert bump (boolean, default false) -> composite1 (false: operator="out", true operator="in") +*/ + +class WaxBump : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + WaxBump ( ) : Filter() { }; + ~WaxBump ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Wax Bump") "</name>\n" + "<id>org.inkscape.effect.filter.WaxBump</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"simplifyImage\" gui-text=\"" N_("Image simplification") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"10.00\">1.5</param>\n" + "<param name=\"simplifyBump\" gui-text=\"" N_("Bump simplification") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"10.00\">1</param>\n" + "<param name=\"crop\" gui-text=\"" N_("Crop") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">1</param>\n" + "<label appearance=\"header\">" N_("Bump source") "</label>\n" + "<param name=\"red\" gui-text=\"" N_("Red") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0</param>\n" + "<param name=\"green\" gui-text=\"" N_("Green") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0</param>\n" + "<param name=\"blue\" gui-text=\"" N_("Blue") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0</param>\n" + "<param name=\"background\" gui-text=\"" N_("Background:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"flood1\">" N_("Color") "</option>\n" + "<option value=\"SourceGraphic\">" N_("Image") "</option>\n" + "<option value=\"blur1\">" N_("Blurred image") "</option>\n" + "</param>\n" + "<param name=\"bgopacity\" gui-text=\"" N_("Background opacity") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1.\">0</param>\n" + "</page>\n" + "<page name=\"lightingtab\" gui-text=\"" N_("Lighting") "\">\n" + "<param name=\"lightingColor\" gui-text=\"" N_("Color") "\" type=\"color\">-1</param>\n" + "<param name=\"height\" gui-text=\"" N_("Height") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-50.\" max=\"50.\">5</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"10.\">1.4</param>\n" + "<param name=\"precision\" gui-text=\"" N_("Precision") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"50\">35</param>\n" + "<param name=\"distantAzimuth\" gui-text=\"" N_("Azimuth") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"360\">225</param>\n" + "<param name=\"distantElevation\" gui-text=\"" N_("Elevation") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"180\">60</param>\n" + "<param name=\"lightingblend\" gui-text=\"" N_("Lighting blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "<param name=\"highlightblend\" gui-text=\"" N_("Highlight blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "</page>\n" + "<page name=\"colortab\" gui-text=\"Bump\">\n" + "<param name=\"imageColor\" gui-text=\"" N_("Bump color") "\" type=\"color\">-520083713</param>\n" + "<param name=\"revert\" gui-text=\"" N_("Revert bump") "\" type=\"bool\" >false</param>\n" + "<param name=\"transparency\" gui-text=\"" N_("Transparency type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"atop\">" N_("Atop") "</option>\n" + "<option value=\"in\">" N_("In") "</option>\n" + "</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Bumps") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Turns an image to jelly") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new WaxBump()); + // clang-format on + }; + +}; + +gchar const * +WaxBump::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simplifyImage; + std::ostringstream simplifyBump; + std::ostringstream crop; + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + + std::ostringstream background; + std::ostringstream bgopacity; + + std::ostringstream height; + std::ostringstream lightness; + std::ostringstream precision; + std::ostringstream distantAzimuth; + std::ostringstream distantElevation; + + std::ostringstream lightRed; + std::ostringstream lightGreen; + std::ostringstream lightBlue; + + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + + std::ostringstream revert; + std::ostringstream lightingblend; + std::ostringstream highlightblend; + std::ostringstream transparency; + + simplifyImage << ext->get_param_float("simplifyImage"); + simplifyBump << ext->get_param_float("simplifyBump"); + crop << ext->get_param_float("crop"); + + red << ext->get_param_float("red") - 0.21; + green << ext->get_param_float("green") - 0.72; + blue << ext->get_param_float("blue") - 0.07; + + background << ext->get_param_optiongroup("background"); + bgopacity << ext->get_param_float("bgopacity"); + + height << ext->get_param_float("height"); + lightness << ext->get_param_float("lightness"); + precision << ext->get_param_int("precision"); + distantAzimuth << ext->get_param_int("distantAzimuth"); + distantElevation << ext->get_param_int("distantElevation"); + + guint32 lightingColor = ext->get_param_color("lightingColor"); + lightRed << ((lightingColor >> 24) & 0xff); + lightGreen << ((lightingColor >> 16) & 0xff); + lightBlue << ((lightingColor >> 8) & 0xff); + + guint32 imageColor = ext->get_param_color("imageColor"); + floodRed << ((imageColor >> 24) & 0xff); + floodGreen << ((imageColor >> 16) & 0xff); + floodBlue << ((imageColor >> 8) & 0xff); + floodAlpha << (imageColor & 0xff) / 255.0F; + + if (ext->get_param_bool("revert")) { + revert << "in" ; + } else { + revert << "out" ; + } + + lightingblend << ext->get_param_optiongroup("lightingblend"); + highlightblend << ext->get_param_optiongroup("highlightblend"); + transparency << ext->get_param_optiongroup("transparency"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Wax Bump\">\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feFlood flood-opacity=\"1\" flood-color=\"rgb(255,255,255)\" result=\"flood1\" />\n" + "<feColorMatrix in=\"%s\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 %s \" result=\"colormatrix1\" />\n" + "<feColorMatrix in=\"blur1\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 %s %s %s %s 0 \" result=\"colormatrix2\" />\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" result=\"flood2\" />\n" + "<feComposite in=\"flood2\" in2=\"colormatrix2\" operator=\"%s\" result=\"composite1\" />\n" + "<feGaussianBlur in=\"composite1\" stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feSpecularLighting in=\"blur2\" lighting-color=\"rgb(%s,%s,%s)\" specularConstant=\"%s\" surfaceScale=\"%s\" specularExponent=\"%s\" result=\"specular\">\n" + "<feDistantLight elevation=\"%s\" azimuth=\"%s\" />\n" + "</feSpecularLighting>\n" + "<feBlend in=\"specular\" in2=\"blur2\" specularConstant=\"1\" mode=\"%s\" result=\"blend1\" />\n" + "<feComposite in=\"blend1\" in2=\"blur2\" k2=\"0\" operator=\"%s\" k1=\"0.5\" k3=\"0.5\" k4=\"0\" result=\"composite2\" />\n" + "<feMerge result=\"merge\">\n" + "<feMergeNode in=\"colormatrix1\" />\n" + "<feMergeNode in=\"composite2\" />\n" + "</feMerge>\n" + "<feBlend in2=\"composite2\" mode=\"%s\" result=\"blend2\" />\n" + "<feComposite in=\"blend2\" in2=\"SourceGraphic\" operator=\"in\" result=\"composite3\" />\n" + "</filter>\n", simplifyImage.str().c_str(), background.str().c_str(), bgopacity.str().c_str(), + red.str().c_str(), green.str().c_str(), blue.str().c_str(), crop.str().c_str(), + floodRed.str().c_str(), floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + revert.str().c_str(), simplifyBump.str().c_str(), + lightRed.str().c_str(), lightGreen.str().c_str(), lightBlue.str().c_str(), + lightness.str().c_str(), height.str().c_str(), precision.str().c_str(), + distantElevation.str().c_str(), distantAzimuth.str().c_str(), + lightingblend.str().c_str(), transparency.str().c_str(), highlightblend.str().c_str() ); + // clang-format on + + return _filter; + +}; /* Wax bump filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'BUMPS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_BUMPS_H__ */ diff --git a/src/extension/internal/filter/color.h b/src/extension/internal/filter/color.h new file mode 100644 index 0000000..9f4f872 --- /dev/null +++ b/src/extension/internal/filter/color.h @@ -0,0 +1,1963 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ +/* Change the 'COLOR' above to be your file name */ + +/* + * Copyright (C) 2013-2015 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Color filters + * Brilliance + * Channel painting + * Color blindness + * Color shift + * Colorize + * Component transfer + * Duochrome + * Extract channel + * Fade to black or white + * Greyscale + * Invert + * Lighting + * Lightness-contrast + * Nudge RGB + * Nudge CMY + * Quadritone + * Simple blend + * Solarize + * Tritone + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Brilliance filter. + + Brilliance filter. + + Filter's parameters: + * Brilliance (1.->10., default 2.) -> colorMatrix (RVB entries) + * Over-saturation (0.->10., default 0.5) -> colorMatrix (6 other entries) + * Lightness (-10.->10., default 0.) -> colorMatrix (last column) + * Inverted (boolean, default false) -> colorMatrix + + Matrix: + St Vi Vi 0 Li + Vi St Vi 0 Li + Vi Vi St 0 Li + 0 0 0 1 0 +*/ +class Brilliance : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Brilliance ( ) : Filter() { }; + ~Brilliance ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Brilliance") "</name>\n" + "<id>org.inkscape.effect.filter.Brilliance</id>\n" + "<param name=\"brightness\" gui-text=\"" N_("Brightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"10.00\">2</param>\n" + "<param name=\"sat\" gui-text=\"" N_("Over-saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.0\" max=\"10.00\">0.5</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.00\" max=\"10.00\">0</param>\n" + "<param name=\"invert\" gui-text=\"" N_("Inverted") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Brightness filter") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Brilliance()); + // clang-format on + }; +}; + +gchar const * +Brilliance::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream brightness; + std::ostringstream sat; + std::ostringstream lightness; + + if (ext->get_param_bool("invert")) { + brightness << -ext->get_param_float("brightness"); + sat << 1 + ext->get_param_float("sat"); + lightness << -ext->get_param_float("lightness"); + } else { + brightness << ext->get_param_float("brightness"); + sat << -ext->get_param_float("sat"); + lightness << ext->get_param_float("lightness"); + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Brilliance\">\n" + "<feColorMatrix values=\"%s %s %s 0 %s %s %s %s 0 %s %s %s %s 0 %s 0 0 0 1 0 \" />\n" + "</filter>\n", brightness.str().c_str(), sat.str().c_str(), sat.str().c_str(), + lightness.str().c_str(), sat.str().c_str(), brightness.str().c_str(), + sat.str().c_str(), lightness.str().c_str(), sat.str().c_str(), + sat.str().c_str(), brightness.str().c_str(), lightness.str().c_str() ); + // clang-format on + + return _filter; +}; /* Brilliance filter */ + +/** + \brief Custom predefined Channel Painting filter. + + Channel Painting filter. + + Filter's parameters: + * Saturation (0.->1., default 1.) -> colormatrix1 (values) + * Red (-10.->10., default -1.) -> colormatrix2 (values) + * Green (-10.->10., default 0.5) -> colormatrix2 (values) + * Blue (-10.->10., default 0.5) -> colormatrix2 (values) + * Alpha (-10.->10., default 1.) -> colormatrix2 (values) + * Flood colors (guint, default 16777215) -> flood (flood-opacity, flood-color) + * Inverted (boolean, default false) -> composite1 (operator, true='in', false='out') + + Matrix: + 1 0 0 0 0 + 0 1 0 0 0 + 0 0 1 0 0 + R G B A 0 +*/ +class ChannelPaint : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ChannelPaint ( ) : Filter() { }; + ~ChannelPaint ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Channel Painting") "</name>\n" + "<id>org.inkscape.effect.filter.ChannelPaint</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"saturation\" gui-text=\"" N_("Saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1.\">1</param>\n" + "<param name=\"red\" gui-text=\"" N_("Red") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">-1</param>\n" + "<param name=\"green\" gui-text=\"" N_("Green") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0.5</param>\n" + "<param name=\"blue\" gui-text=\"" N_("Blue") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0.5</param>\n" + "<param name=\"alpha\" gui-text=\"" N_("Alpha") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">1</param>\n" + "<param name=\"invert\" gui-text=\"" N_("Inverted") "\" type=\"bool\">false</param>\n" + "</page>\n" + "<page name=\"colortab\" gui-text=\"Color\">\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">16777215</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Replace RGB by any color") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ChannelPaint()); + // clang-format on + }; +}; + +gchar const * +ChannelPaint::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream saturation; + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream alpha; + std::ostringstream invert; + std::ostringstream floodRed; + std::ostringstream floodGreen; + std::ostringstream floodBlue; + std::ostringstream floodAlpha; + + saturation << ext->get_param_float("saturation"); + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + alpha << ext->get_param_float("alpha"); + + guint32 color = ext->get_param_color("color"); + floodRed << ((color >> 24) & 0xff); + floodGreen << ((color >> 16) & 0xff); + floodBlue << ((color >> 8) & 0xff); + floodAlpha << (color & 0xff) / 255.0F; + + if (ext->get_param_bool("invert")) { + invert << "in"; + } else { + invert << "out"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Channel Painting\">\n" + "<feColorMatrix values=\"%s\" type=\"saturate\" result=\"colormatrix1\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 %s %s %s %s 0 \" in=\"SourceGraphic\" result=\"colormatrix2\" />\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" result=\"flood\" />\n" + "<feComposite in2=\"colormatrix2\" operator=\"%s\" result=\"composite1\" />\n" + "<feMerge result=\"merge\">\n" + "<feMergeNode in=\"colormatrix1\" />\n" + "<feMergeNode in=\"composite1\" />\n" + "</feMerge>\n" + "<feComposite in=\"merge\" in2=\"SourceGraphic\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", saturation.str().c_str(), red.str().c_str(), green.str().c_str(), + blue.str().c_str(), alpha.str().c_str(), floodRed.str().c_str(), + floodGreen.str().c_str(), floodBlue.str().c_str(), floodAlpha.str().c_str(), + invert.str().c_str() ); + // clang-format on + + return _filter; +}; /* Channel Painting filter */ + +/** + \brief Custom predefined Color Blindness filter. + + Color Blindness filter. + Based on https://openclipart.org/detail/22299/Color%20Blindness%20filters + + Filter's parameters: + * Blindness type (enum, default Achromatomaly) -> colormatrix +*/ +class ColorBlindness : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorBlindness ( ) : Filter() { }; + ~ColorBlindness ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Color Blindness") "</name>\n" + "<id>org.inkscape.effect.filter.ColorBlindness</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"type\" gui-text=\"" N_("Blindness type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"0.618 0.32 0.062 0 0 0.163 0.775 0.062 0 0 0.163 0.32 0.516 0 0 0 0 0 1 0 \">" N_("Rod monochromacy (atypical achromatopsia)") "</option>\n" + "<option value=\"0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0 \">" N_("Cone monochromacy (typical achromatopsia)") "</option>\n" + "<option value=\"0.8 0.2 0 0 0 0.2583 0.74167 0 0 0 0 0.14167 0.85833 0 0 0 0 0 1 0 \">" N_("Green weak (deuteranomaly)") "</option>\n" + "<option value=\"0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0 \">" N_("Green blind (deuteranopia)") "</option>\n" + "<option value=\"0.8166 0.1833 0 0 0 0.333 0.666 0 0 0 0 0.125 0.875 0 0 0 0 0 1 0 \">" N_("Red weak (protanomaly)") "</option>\n" + "<option value=\"0.566 0.43333 0 0 0 0.55833 0.4416 0 0 0 0 0.24167 0.75833 0 0 0 0 0 1 0 \">" N_("Red blind (protanopia)") "</option>\n" + "<option value=\"0.966 0.033 0 0 0 0 0.733 0.266 0 0 0 0.1833 0.816 0 0 0 0 0 1 0 \">" N_("Blue weak (tritanomaly)") "</option>\n" + "<option value=\"0.95 0.05 0 0 0 0.2583 0.4333 0.5667 0 0 0 0.475 0.525 0 0 0 0 0 1 0 \">" N_("Blue blind (tritanopia)") "</option>\n" + "</param>\n" + "</page>\n" + "<page name=\"helptab\" gui-text=\"Help\">\n" + "<label xml:space=\"preserve\">\n" +"Filters based on https://openclipart.org/detail/22299/Color%20Blindness%20filters\n" +"\n" +"These filters don't correctly reflect actual color blindness for two main reasons:\n" +" * Everyone is different, and is not affected exactly the same way.\n" +" * The filters are in the RGB color space, and ignore confusion lines.\n" + "</label>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Simulate color blindness") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ColorBlindness()); + // clang-format on + }; +}; + +gchar const * +ColorBlindness::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + type << ext->get_param_optiongroup("type"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" height=\"1\" width=\"1\" y=\"0\" x=\"0\" inkscape:label=\"Color Blindness\">\n" + "<feColorMatrix values=\"%s\" type=\"matrix\" result=\"colormatrix1\" />\n" + "</filter>\n", type.str().c_str()); + // clang-format on + + return _filter; +}; /* Color Blindness filter */ + +/** + \brief Custom predefined Color shift filter. + + Rotate and desaturate hue + + Filter's parameters: + * Shift (0->360, default 330) -> color1 (values) + * Saturation (0.->1., default 0.6) -> color2 (values) +*/ + +class ColorShift : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorShift ( ) : Filter() { }; + ~ColorShift ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Color Shift") "</name>\n" + "<id>org.inkscape.effect.filter.ColorShift</id>\n" + "<param name=\"shift\" gui-text=\"" N_("Shift (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">330</param>\n" + "<param name=\"sat\" gui-text=\"" N_("Saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1\">0.6</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Rotate and desaturate hue") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ColorShift()); + // clang-format on + }; + +}; + +gchar const * +ColorShift::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream shift; + std::ostringstream sat; + + shift << ext->get_param_int("shift"); + sat << ext->get_param_float("sat"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Color Shift\">\n" + "<feColorMatrix type=\"hueRotate\" values=\"%s\" result=\"color1\" />\n" + "<feColorMatrix type=\"saturate\" values=\"%s\" result=\"color2\" />\n" + "</filter>\n", shift.str().c_str(), sat.str().c_str() ); + // clang-format on + + return _filter; +}; /* ColorShift filter */ + +/** + \brief Custom predefined Colorize filter. + + Blend image or object with a flood color. + + Filter's parameters: + * Harsh light (0.->10., default 0) -> composite1 (k1) + * Normal light (0.->10., default 1) -> composite2 (k2) + * Duotone (boolean, default false) -> colormatrix1 (values="0") + * Filtered greys (boolean, default false) -> colormatrix2 (values="0") + * Blend mode 1 (enum, default Multiply) -> blend1 (mode) + * Blend mode 2 (enum, default Screen) -> blend2 (mode) +*/ + +class Colorize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Colorize ( ) : Filter() { }; + ~Colorize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Colorize") "</name>\n" + "<id>org.inkscape.effect.filter.Colorize</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"hlight\" gui-text=\"" N_("Harsh light") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"10\">0</param>\n" + "<param name=\"nlight\" gui-text=\"" N_("Normal light") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"10\">1</param>\n" + "<param name=\"duotone\" gui-text=\"" N_("Duotone") "\" type=\"bool\" >false</param>\n" + "<param name=\"blend1\" gui-text=\"" N_("Blend 1:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "<param name=\"blend2\" gui-text=\"" N_("Blend 2:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "</page>\n" + "<page name=\"colortab\" gui-text=\"Color\">\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">-1639776001</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blend image or object with a flood color") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Colorize()); + // clang-format on + }; + +}; + +gchar const * +Colorize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream hlight; + std::ostringstream nlight; + std::ostringstream duotone; + std::ostringstream blend1; + std::ostringstream blend2; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + hlight << ext->get_param_float("hlight"); + nlight << ext->get_param_float("nlight"); + blend1 << ext->get_param_optiongroup("blend1"); + blend2 << ext->get_param_optiongroup("blend2"); + if (ext->get_param_bool("duotone")) { + duotone << "0"; + } else { + duotone << "1"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Colorize\">\n" + "<feComposite in2=\"SourceGraphic\" operator=\"arithmetic\" k1=\"%s\" k2=\"%s\" result=\"composite1\" />\n" + "<feColorMatrix in=\"composite1\" values=\"%s\" type=\"saturate\" result=\"colormatrix1\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood1\" />\n" + "<feBlend in=\"flood1\" in2=\"colormatrix1\" mode=\"%s\" result=\"blend1\" />\n" + "<feBlend in2=\"blend1\" mode=\"%s\" result=\"blend2\" />\n" + "<feColorMatrix in=\"blend2\" values=\"1\" type=\"saturate\" result=\"colormatrix2\" />\n" + "<feComposite in=\"colormatrix2\" in2=\"SourceGraphic\" operator=\"in\" k2=\"1\" result=\"composite2\" />\n" + "</filter>\n", hlight.str().c_str(), nlight.str().c_str(), duotone.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + blend1.str().c_str(), blend2.str().c_str() ); + // clang-format on + + return _filter; +}; /* Colorize filter */ + +/** + \brief Custom predefined ComponentTransfer filter. + + Basic component transfer structure. + + Filter's parameters: + * Type (enum, default identity) -> component function + +*/ +class ComponentTransfer : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ComponentTransfer ( ) : Filter() { }; + ~ComponentTransfer ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Component Transfer") "</name>\n" + "<id>org.inkscape.effect.filter.ComponentTransfer</id>\n" + "<param name=\"type\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"identity\">" N_("Identity") "</option>\n" + "<option value=\"table\">" N_("Table") "</option>\n" + "<option value=\"discrete\">" N_("Discrete") "</option>\n" + "<option value=\"linear\">" N_("Linear") "</option>\n" + "<option value=\"gamma\">" N_("Gamma") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Basic component transfer structure") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ComponentTransfer()); + // clang-format on + }; +}; + +gchar const * +ComponentTransfer::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream CTfunction; + const gchar *type = ext->get_param_optiongroup("type"); + + if ((g_ascii_strcasecmp("identity", type) == 0)) { + CTfunction << "<feFuncR type=\"identity\" tableValues=\"1 0\" />\n" + << "<feFuncG type=\"identity\" tableValues=\"1 0\" />\n" + << "<feFuncB type=\"identity\" tableValues=\"1 0\" />\n" + << "<feFuncA type=\"identity\" tableValues=\"0 1\" />\n"; + } else if ((g_ascii_strcasecmp("table", type) == 0)) { + CTfunction << "<feFuncR type=\"table\" tableValues=\"0 1 0\" />\n" + << "<feFuncG type=\"table\" tableValues=\"0 1 0\" />\n" + << "<feFuncB type=\"table\" tableValues=\"0 1 0\" />\n"; + } else if ((g_ascii_strcasecmp("discrete", type) == 0)) { + CTfunction << "<feFuncR tableValues=\"0 0.2 0.4 0.6 0.8 1 1\" type=\"discrete\" />\n" + << "<feFuncG tableValues=\"0 0.2 0.4 0.6 0.8 1 1\" type=\"discrete\" />\n" + << "<feFuncB tableValues=\"0 0.2 0.4 0.6 0.8 1 1\" type=\"discrete\" />\n"; + } else if ((g_ascii_strcasecmp("linear", type) == 0)) { + CTfunction << "<feFuncR type=\"linear\" slope=\".5\" intercept=\".10\" />\n" + << "<feFuncG type=\"linear\" slope=\".5\" intercept=\".10\" />\n" + << "<feFuncB type=\"linear\" slope=\".5\" intercept=\".10\" />\n"; + } else { //Gamma + CTfunction << "<feFuncR type=\"gamma\" amplitude=\"3\" exponent=\"3\" offset=\"0.1\" />\n" + << "<feFuncG type=\"gamma\" amplitude=\"3\" exponent=\"3\" offset=\"0.1\" />\n" + << "<feFuncB type=\"gamma\" amplitude=\"3\" exponent=\"3\" offset=\"0.1\" />\n"; + } + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Component Transfer\">\n" + "<feComponentTransfer>\n" + "%s\n" + "</feComponentTransfer>\n" + "</filter>\n", CTfunction.str().c_str()); + // clang-format on + + return _filter; +}; /* ComponentTransfer filter */ + +/** + \brief Custom predefined Duochrome filter. + + Convert luminance values to a duochrome palette. + + Filter's parameters: + * Fluorescence level (0.->2., default 0) -> composite4 (k2) + * Swap (enum, default "No swap") -> composite1, composite2 (operator) + * Color 1 (guint, default 1364325887) -> flood1 (flood-opacity, flood-color) + * Color 2 (guint, default -65281) -> flood2 (flood-opacity, flood-color) +*/ + +class Duochrome : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Duochrome ( ) : Filter() { }; + ~Duochrome ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Duochrome") "</name>\n" + "<id>org.inkscape.effect.filter.Duochrome</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"fluo\" gui-text=\"" N_("Fluorescence level") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"2\">0</param>\n" + "<param name=\"swap\" gui-text=\"" N_("Swap:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"none\">" N_("No swap") "</option>\n" + "<option value=\"full\">" N_("Color and alpha") "</option>\n" + "<option value=\"color\">" N_("Color only") "</option>\n" + "<option value=\"alpha\">" N_("Alpha only") "</option>\n" + "</param>\n" + "</page>\n" + "<page name=\"co11tab\" gui-text=\"Color 1\">\n" + "<param name=\"color1\" gui-text=\"" N_("Color 1") "\" type=\"color\">1364325887</param>\n" + "</page>\n" + "<page name=\"co12tab\" gui-text=\"Color 2\">\n" + "<param name=\"color2\" gui-text=\"" N_("Color 2") "\" type=\"color\">-65281</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Convert luminance values to a duochrome palette") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Duochrome()); + // clang-format on + }; + +}; + +gchar const * +Duochrome::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a1; + std::ostringstream r1; + std::ostringstream g1; + std::ostringstream b1; + std::ostringstream a2; + std::ostringstream r2; + std::ostringstream g2; + std::ostringstream b2; + std::ostringstream fluo; + std::ostringstream swap1; + std::ostringstream swap2; + guint32 color1 = ext->get_param_color("color1"); + guint32 color2 = ext->get_param_color("color2"); + double fluorescence = ext->get_param_float("fluo"); + const gchar *swaptype = ext->get_param_optiongroup("swap"); + + r1 << ((color1 >> 24) & 0xff); + g1 << ((color1 >> 16) & 0xff); + b1 << ((color1 >> 8) & 0xff); + r2 << ((color2 >> 24) & 0xff); + g2 << ((color2 >> 16) & 0xff); + b2 << ((color2 >> 8) & 0xff); + fluo << fluorescence; + + if ((g_ascii_strcasecmp("full", swaptype) == 0)) { + swap1 << "in"; + swap2 << "out"; + a1 << (color1 & 0xff) / 255.0F; + a2 << (color2 & 0xff) / 255.0F; + } else if ((g_ascii_strcasecmp("color", swaptype) == 0)) { + swap1 << "in"; + swap2 << "out"; + a1 << (color2 & 0xff) / 255.0F; + a2 << (color1 & 0xff) / 255.0F; + } else if ((g_ascii_strcasecmp("alpha", swaptype) == 0)) { + swap1 << "out"; + swap2 << "in"; + a1 << (color2 & 0xff) / 255.0F; + a2 << (color1 & 0xff) / 255.0F; + } else { + swap1 << "out"; + swap2 << "in"; + a1 << (color1 & 0xff) / 255.0F; + a2 << (color2 & 0xff) / 255.0F; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Duochrome\">\n" + "<feColorMatrix type=\"luminanceToAlpha\" result=\"colormatrix1\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood1\" />\n" + "<feComposite in2=\"colormatrix1\" operator=\"%s\" result=\"composite1\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood2\" />\n" + "<feComposite in2=\"colormatrix1\" result=\"composite2\" operator=\"%s\" />\n" + "<feComposite in=\"composite2\" in2=\"composite1\" k2=\"1\" k3=\"1\" operator=\"arithmetic\" result=\"composite3\" />\n" + "<feColorMatrix in=\"composite3\" type=\"matrix\" values=\"2 -1 0 0 0 0 2 -1 0 0 -1 0 2 0 0 0 0 0 1 0 \" result=\"colormatrix2\" />\n" + "<feComposite in=\"colormatrix2\" in2=\"composite3\" operator=\"arithmetic\" k2=\"%s\" result=\"composite4\" />\n" + "<feBlend in=\"composite4\" in2=\"composite3\" mode=\"normal\" result=\"blend\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" />\n" + "</filter>\n", a1.str().c_str(), r1.str().c_str(), g1.str().c_str(), b1.str().c_str(), swap1.str().c_str(), + a2.str().c_str(), r2.str().c_str(), g2.str().c_str(), b2.str().c_str(), swap2.str().c_str(), + fluo.str().c_str() ); + // clang-format on + + return _filter; +}; /* Duochrome filter */ + +/** + \brief Custom predefined Extract Channel filter. + + Extract color channel as a transparent image. + + Filter's parameters: + * Channel (enum, all colors, default Red) -> colormatrix (values) + * Background blend (enum, Normal, Multiply, Screen, default Normal) -> blend (mode) + * Channel to alpha (boolean, default false) -> colormatrix (values) + +*/ +class ExtractChannel : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ExtractChannel ( ) : Filter() { }; + ~ExtractChannel ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Extract Channel") "</name>\n" + "<id>org.inkscape.effect.filter.ExtractChannel</id>\n" + "<param name=\"source\" gui-text=\"" N_("Channel:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"r\">" N_("Red") "</option>\n" + "<option value=\"g\">" N_("Green") "</option>\n" + "<option value=\"b\">" N_("Blue") "</option>\n" + "<option value=\"c\">" N_("Cyan") "</option>\n" + "<option value=\"m\">" N_("Magenta") "</option>\n" + "<option value=\"y\">" N_("Yellow") "</option>\n" + "</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Background blend mode:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "</param>\n" + "<param name=\"alpha\" gui-text=\"" N_("Channel to alpha") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Extract color channel as a transparent image") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ExtractChannel()); + // clang-format on + }; +}; + +gchar const * +ExtractChannel::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blend; + std::ostringstream colors; + + blend << ext->get_param_optiongroup("blend"); + + const gchar *channel = ext->get_param_optiongroup("source"); + if (ext->get_param_bool("alpha")) { + if ((g_ascii_strcasecmp("r", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0"; + } else if ((g_ascii_strcasecmp("g", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0"; + } else if ((g_ascii_strcasecmp("b", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0"; + } else if ((g_ascii_strcasecmp("c", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 1 0"; + } else if ((g_ascii_strcasecmp("m", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 1 0"; + } else { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 1 0"; + } + } else { + if ((g_ascii_strcasecmp("r", channel) == 0)) { + colors << "0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0"; + } else if ((g_ascii_strcasecmp("g", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0"; + } else if ((g_ascii_strcasecmp("b", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0"; + } else if ((g_ascii_strcasecmp("c", channel) == 0)) { + colors << "0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 -1 0 0 1 0"; + } else if ((g_ascii_strcasecmp("m", channel) == 0)) { + colors << "0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 -1 0 1 0"; + } else { + colors << "0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 -1 1 0"; + } + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Extract Channel\">\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"%s 0 \" result=\"colormatrix\" />\n" + "<feBlend in2=\"BackgroundImage\" mode=\"%s\" result=\"blend\" />\n" + "</filter>\n", colors.str().c_str(), blend.str().c_str() ); + // clang-format on + + return _filter; +}; /* ExtractChannel filter */ + +/** + \brief Custom predefined Fade to Black or White filter. + + Fade to black or white. + + Filter's parameters: + * Level (0.->1., default 1.) -> colorMatrix (RVB entries) + * Fade to (enum [black|white], default black) -> colorMatrix (RVB entries) + + Matrix + black white + Lv 0 0 0 0 Lv 0 0 1-lv 0 + 0 Lv 0 0 0 0 Lv 0 1-lv 0 + 0 0 Lv 0 0 0 0 Lv 1-lv 0 + 0 0 0 1 0 0 0 0 1 0 +*/ +class FadeToBW : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + FadeToBW ( ) : Filter() { }; + ~FadeToBW ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Fade to Black or White") "</name>\n" + "<id>org.inkscape.effect.filter.FadeToBW</id>\n" + "<param name=\"level\" gui-text=\"" N_("Level") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1\">1</param>\n" + "<param name=\"fadeto\" gui-text=\"" N_("Fade to:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"black\">" N_("Black") "</option>\n" + "<option value=\"white\">" N_("White") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Fade to black or white") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new FadeToBW()); + // clang-format on + }; +}; + +gchar const * +FadeToBW::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream level; + std::ostringstream wlevel; + + level << ext->get_param_float("level"); + + const gchar *fadeto = ext->get_param_optiongroup("fadeto"); + if ((g_ascii_strcasecmp("white", fadeto) == 0)) { + // White + wlevel << (1 - ext->get_param_float("level")); + } else { + // Black + wlevel << "0"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Fade to Black or White\">\n" + "<feColorMatrix values=\"%s 0 0 0 %s 0 %s 0 0 %s 0 0 %s 0 %s 0 0 0 1 0\" />\n" + "</filter>\n", level.str().c_str(), wlevel.str().c_str(), + level.str().c_str(), wlevel.str().c_str(), + level.str().c_str(), wlevel.str().c_str() ); + // clang-format on + + return _filter; +}; /* Fade to black or white filter */ + +/** + \brief Custom predefined Greyscale filter. + + Customize greyscale components. + + Filter's parameters: + * Red (-10.->10., default .21) -> colorMatrix (values) + * Green (-10.->10., default .72) -> colorMatrix (values) + * Blue (-10.->10., default .072) -> colorMatrix (values) + * Lightness (-10.->10., default 0.) -> colorMatrix (values) + * Transparent (boolean, default false) -> matrix structure + + Matrix: + normal transparency + R G B St 0 0 0 0 0 0 + R G B St 0 0 0 0 0 0 + R G B St 0 0 0 0 0 0 + 0 0 0 1 0 R G B 1-St 0 +*/ +class Greyscale : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Greyscale ( ) : Filter() { }; + ~Greyscale ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Greyscale") "</name>\n" + "<id>org.inkscape.effect.filter.Greyscale</id>\n" + "<param name=\"red\" gui-text=\"" N_("Red") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.00\" max=\"10.00\">0.21</param>\n" + "<param name=\"green\" gui-text=\"" N_("Green") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.00\" max=\"10.00\">0.72</param>\n" + "<param name=\"blue\" gui-text=\"" N_("Blue") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.00\" max=\"10.00\">0.072</param>\n" + "<param name=\"strength\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.00\" max=\"10.00\">0</param>\n" + "<param name=\"transparent\" gui-text=\"" N_("Transparent") "\" type=\"bool\" >false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Customize greyscale components") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Greyscale()); + // clang-format on + }; +}; + +gchar const * +Greyscale::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream strength; + std::ostringstream redt; + std::ostringstream greent; + std::ostringstream bluet; + std::ostringstream strengtht; + std::ostringstream transparency; + std::ostringstream line; + + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + strength << ext->get_param_float("strength"); + + redt << - ext->get_param_float("red"); + greent << - ext->get_param_float("green"); + bluet << - ext->get_param_float("blue"); + strengtht << 1 - ext->get_param_float("strength"); + + if (ext->get_param_bool("transparent")) { + line << "0 0 0 0"; + transparency << redt.str().c_str() << " " << greent.str().c_str() << " " << bluet.str().c_str() << " " << strengtht.str().c_str(); + } else { + line << red.str().c_str() << " " << green.str().c_str() << " " << blue.str().c_str() << " " << strength.str().c_str(); + transparency << "0 0 0 1"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Greyscale\">\n" + "<feColorMatrix values=\"%s 0 %s 0 %s 0 %s 0 \" />\n" + "</filter>\n", line.str().c_str(), line.str().c_str(), line.str().c_str(), transparency.str().c_str() ); + // clang-format on + + return _filter; +}; /* Greyscale filter */ + +/** + \brief Custom predefined Invert filter. + + Manage hue, lightness and transparency inversions + + Filter's parameters: + * Invert hue (boolean, default false) -> color1 (values, true: 180, false: 0) + * Invert lightness (boolean, default false) -> color1 (values, true: 180, false: 0; XOR with Invert hue), + color2 (values: from a00 to a22, if 1, set -1 and set 1 in ax4, if -1, set 1 and set 0 in ax4) + * Invert transparency (boolean, default false) -> color2 (values: negate a30, a31 and a32, substract 1 from a33) + * Invert channels (enum, default Red and blue) -> color2 (values -for R&B: swap ax0 and ax2 in the first 3 lines) + * Light transparency (0.->1., default 0.) -> color2 (values: a33=a33-x) +*/ + +class Invert : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Invert ( ) : Filter() { }; + ~Invert ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Invert") "</name>\n" + "<id>org.inkscape.effect.filter.Invert</id>\n" + "<param name=\"channels\" gui-text=\"" N_("Invert channels:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"0\">" N_("No inversion") "</option>\n" + "<option value=\"1\">" N_("Red and blue") "</option>\n" + "<option value=\"2\">" N_("Red and green") "</option>\n" + "<option value=\"3\">" N_("Green and blue") "</option>\n" + "</param>\n" + "<param name=\"opacify\" gui-text=\"" N_("Light transparency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1\">0</param>\n" + "<param name=\"hue\" gui-text=\"" N_("Invert hue") "\" type=\"bool\" >false</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Invert lightness") "\" type=\"bool\" >false</param>\n" + "<param name=\"transparency\" gui-text=\"" N_("Invert transparency") "\" type=\"bool\" >false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Manage hue, lightness and transparency inversions") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Invert()); + // clang-format on + }; + +}; + +gchar const * +Invert::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream line1; + std::ostringstream line2; + std::ostringstream line3; + + std::ostringstream col5; + std::ostringstream transparency; + std::ostringstream hue; + + if (ext->get_param_bool("hue") ^ ext->get_param_bool("lightness")) { + hue << "<feColorMatrix type=\"hueRotate\" values=\"180\" result=\"color1\" />\n"; + } else { + hue << ""; + } + + if (ext->get_param_bool("transparency")) { + transparency << "0.21 0.72 0.07 " << 1 - ext->get_param_float("opacify"); + } else { + transparency << "-0.21 -0.72 -0.07 " << 2 - ext->get_param_float("opacify"); + } + + if (ext->get_param_bool("lightness")) { + switch (atoi(ext->get_param_optiongroup("channels"))) { + case 1: + line1 << "0 0 -1"; + line2 << "0 -1 0"; + line3 << "-1 0 0"; + break; + case 2: + line1 << "0 -1 0"; + line2 << "-1 0 0"; + line3 << "0 0 -1"; + break; + case 3: + line1 << "-1 0 0"; + line2 << "0 0 -1"; + line3 << "0 -1 0"; + break; + default: + line1 << "-1 0 0"; + line2 << "0 -1 0"; + line3 << "0 0 -1"; + break; + } + col5 << "1"; + } else { + switch (atoi(ext->get_param_optiongroup("channels"))) { + case 1: + line1 << "0 0 1"; + line2 << "0 1 0"; + line3 << "1 0 0"; + break; + case 2: + line1 << "0 1 0"; + line2 << "1 0 0"; + line3 << "0 0 1"; + break; + case 3: + line1 << "1 0 0"; + line2 << "0 0 1"; + line3 << "0 1 0"; + break; + default: + line1 << "1 0 0"; + line2 << "0 1 0"; + line3 << "0 0 1"; + break; + } + col5 << "0"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Invert\">\n" + "%s" + "<feColorMatrix values=\"%s 0 %s %s 0 %s %s 0 %s %s 0 \" result=\"color2\" />\n" + "</filter>\n", hue.str().c_str(), + line1.str().c_str(), col5.str().c_str(), + line2.str().c_str(), col5.str().c_str(), + line3.str().c_str(), col5.str().c_str(), + transparency.str().c_str() ); + // clang-format on + + return _filter; +}; /* Invert filter */ + +/** + \brief Custom predefined Lighting filter. + + Modify lights and shadows separately. + + Filter's parameters: + * Lightness (0.->20., default 1.) -> component (amplitude) + * Shadow (0.->20., default 1.) -> component (exponent) + * Offset (-1.->1., default 0.) -> component (offset) +*/ +class Lighting : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Lighting ( ) : Filter() { }; + ~Lighting ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Lighting") "</name>\n" + "<id>org.inkscape.effect.filter.Lighting</id>\n" + "<param name=\"amplitude\" gui-text=\"" N_("Lights") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"20.00\">1</param>\n" + "<param name=\"exponent\" gui-text=\"" N_("Shadows") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"20.00\">1</param>\n" + "<param name=\"offset\" gui-text=\"" N_("Offset") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-1.00\" max=\"1.00\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Modify lights and shadows separately") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Lighting()); + // clang-format on + }; +}; + +gchar const * +Lighting::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream amplitude; + std::ostringstream exponent; + std::ostringstream offset; + + amplitude << ext->get_param_float("amplitude"); + exponent << ext->get_param_float("exponent"); + offset << ext->get_param_float("offset"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Lighting\">\n" + "<feComponentTransfer in=\"blur\" result=\"component\" >\n" + "<feFuncR type=\"gamma\" amplitude=\"%s\" exponent=\"%s\" offset=\"%s\" />\n" + "<feFuncG type=\"gamma\" amplitude=\"%s\" exponent=\"%s\" offset=\"%s\" />\n" + "<feFuncB type=\"gamma\" amplitude=\"%s\" exponent=\"%s\" offset=\"%s\" />\n" + "</feComponentTransfer>\n" + "</filter>\n", amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str(), + amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str(), + amplitude.str().c_str(), exponent.str().c_str(), offset.str().c_str() ); + // clang-format on + + return _filter; +}; /* Lighting filter */ + +/** + \brief Custom predefined Lightness-Contrast filter. + + Modify lightness and contrast separately. + + Filter's parameters: + * Lightness (0.->100., default 0.) -> colorMatrix + * Contrast (0.->100., default 0.) -> colorMatrix + + Matrix: + Co/10 0 0 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 Co/10 0 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 0 Co/10 1+(Co-1)*Li/2000 -(Co-1)/20 + 0 0 0 1 0 +*/ +class LightnessContrast : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + LightnessContrast ( ) : Filter() { }; + ~LightnessContrast ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Lightness-Contrast") "</name>\n" + "<id>org.inkscape.effect.filter.LightnessContrast</id>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-100\" max=\"100\">0</param>\n" + "<param name=\"contrast\" gui-text=\"" N_("Contrast") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-100\" max=\"100\">0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Modify lightness and contrast separately") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new LightnessContrast()); + // clang-format on + }; +}; + +gchar const * +LightnessContrast::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream lightness; + std::ostringstream contrast; + std::ostringstream contrast5; + + double c5; + if (ext->get_param_float("contrast") > 0) { + contrast << (1 + ext->get_param_float("contrast") / 10); + c5 = (- ext->get_param_float("contrast") / 20); + } else { + contrast << (1 + ext->get_param_float("contrast") / 100); + c5 =(- ext->get_param_float("contrast") / 200); + } + + contrast5 << c5; + lightness << ((1 - c5) * ext->get_param_float("lightness") / 100); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Lightness-Contrast\">\n" + "<feColorMatrix values=\"%s 0 0 %s %s 0 %s 0 %s %s 0 0 %s %s %s 0 0 0 1 0\" />\n" + "</filter>\n", contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str(), + contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str(), + contrast.str().c_str(), lightness.str().c_str(), contrast5.str().c_str() ); + // clang-format on + + return _filter; +}; /* Lightness-Contrast filter */ + +/** + \brief Custom predefined Nudge RGB filter. + + Nudge RGB channels separately and blend them to different types of backgrounds + + Filter's parameters: + Offsets + * Red + * x (-100.->100., default -6) -> offset1 (dx) + * y (-100.->100., default -6) -> offset1 (dy) + * Green + * x (-100.->100., default 6) -> offset2 (dx) + * y (-100.->100., default 7) -> offset2 (dy) + * Blue + * x (-100.->100., default 1) -> offset3 (dx) + * y (-100.->100., default -16) -> offset3 (dy) + Color + * Background color (guint, default 255)-> flood (flood-color, flood-opacity) + +*/ +class NudgeRGB : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NudgeRGB ( ) : Filter() { }; + ~NudgeRGB ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Nudge RGB") "</name>\n" + "<id>org.inkscape.effect.filter.NudgeRGB</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"offsettab\" gui-text=\"Offset\">\n" + "<label appearance=\"header\">" N_("Red offset") "</label>\n" + "<param name=\"rx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-6</param>\n" + "<param name=\"ry\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-6</param>\n" + "<label appearance=\"header\">" N_("Green offset") "</label>\n" + "<param name=\"gx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">6</param>\n" + "<param name=\"gy\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">7</param>\n" + "<label appearance=\"header\">" N_("Blue offset") "</label>\n" + "<param name=\"bx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">1</param>\n" + "<param name=\"by\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-16</param>\n" + "</page>\n" + "<page name=\"coltab\" gui-text=\"Color\">\n" + "<param name=\"color\" gui-text=\"" N_("Background color") "\" type=\"color\">255</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Nudge RGB channels separately and blend them to different types of backgrounds") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new NudgeRGB()); + // clang-format on + }; +}; + +gchar const * +NudgeRGB::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream rx; + std::ostringstream ry; + std::ostringstream gx; + std::ostringstream gy; + std::ostringstream bx; + std::ostringstream by; + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + + rx << ext->get_param_float("rx"); + ry << ext->get_param_float("ry"); + gx << ext->get_param_float("gx"); + gy << ext->get_param_float("gy"); + bx << ext->get_param_float("bx"); + by << ext->get_param_float("by"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Nudge RGB\">\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 \" result=\"colormatrix1\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset1\" />\n" + "<feBlend in2=\"flood\" mode=\"screen\" result=\"blend1\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 \" result=\"colormatrix2\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset2\" />\n" + "<feBlend in2=\"blend1\" mode=\"screen\" result=\"blend2\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset3\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 \" result=\"colormatrix3\" />\n" + "<feBlend in2=\"offset3\" mode=\"screen\" result=\"blend3\" />\n" + "</filter>\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + rx.str().c_str(), ry.str().c_str(), + gx.str().c_str(), gy.str().c_str(), + bx.str().c_str(), by.str().c_str() ); + // clang-format on + + return _filter; + +}; /* Nudge RGB filter */ + +/** + \brief Custom predefined Nudge CMY filter. + + Nudge CMY channels separately and blend them to different types of backgrounds + + Filter's parameters: + Offsets + * Cyan + * x (-100.->100., default -6) -> offset1 (dx) + * y (-100.->100., default -6) -> offset1 (dy) + * Magenta + * x (-100.->100., default 6) -> offset2 (dx) + * y (-100.->100., default 7) -> offset2 (dy) + * Yellow + * x (-100.->100., default 1) -> offset3 (dx) + * y (-100.->100., default -16) -> offset3 (dy) + Color + * Background color (guint, default -1)-> flood (flood-color, flood-opacity) +*/ +class NudgeCMY : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NudgeCMY ( ) : Filter() { }; + ~NudgeCMY ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Nudge CMY") "</name>\n" + "<id>org.inkscape.effect.filter.NudgeCMY</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"offsettab\" gui-text=\"Offset\">\n" + "<label appearance=\"header\">" N_("Cyan offset") "</label>\n" + "<param name=\"cx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-6</param>\n" + "<param name=\"cy\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-6</param>\n" + "<label appearance=\"header\">" N_("Magenta offset") "</label>\n" + "<param name=\"mx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">6</param>\n" + "<param name=\"my\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">7</param>\n" + "<label appearance=\"header\">" N_("Yellow offset") "</label>\n" + "<param name=\"yx\" gui-text=\"" N_("X") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">1</param>\n" + "<param name=\"yy\" gui-text=\"" N_("Y") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-100.\" max=\"100.\">-16</param>\n" + "</page>\n" + "<page name=\"coltab\" gui-text=\"Color\">\n" + "<param name=\"color\" gui-text=\"" N_("Background color") "\" type=\"color\">-1</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Nudge CMY channels separately and blend them to different types of backgrounds") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new NudgeCMY()); + // clang-format on + }; +}; + +gchar const * +NudgeCMY::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream cx; + std::ostringstream cy; + std::ostringstream mx; + std::ostringstream my; + std::ostringstream yx; + std::ostringstream yy; + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + + cx << ext->get_param_float("cx"); + cy << ext->get_param_float("cy"); + mx << ext->get_param_float("mx"); + my << ext->get_param_float("my"); + yx << ext->get_param_float("yx"); + yy << ext->get_param_float("yy"); + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Nudge CMY\">\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 -1 0 0 1 0 \" result=\"colormatrix1\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset1\" />\n" + "<feBlend in2=\"flood\" mode=\"multiply\" result=\"blend1\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 -1 0 1 0 \" result=\"colormatrix2\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset2\" />\n" + "<feBlend in2=\"blend1\" mode=\"multiply\" result=\"blend2\" />\n" + "<feOffset dy=\"%s\" dx=\"%s\" result=\"offset3\" />\n" + "<feColorMatrix in=\"SourceGraphic\" values=\"0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 -1 1 0 \" result=\"colormatrix3\" />\n" + "<feBlend in2=\"offset3\" mode=\"multiply\" result=\"blend3\" />\n" + "</filter>\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + cx.str().c_str(), cy.str().c_str(), + mx.str().c_str(), my.str().c_str(), + yx.str().c_str(), yy.str().c_str() ); + // clang-format on + + return _filter; + +}; /* Nudge CMY filter */ + +/** + \brief Custom predefined Quadritone filter. + + Replace hue by two colors. + + Filter's parameters: + * Hue distribution (0->360, default 280) -> colormatrix1 (values) + * Colors (0->360, default 100) -> colormatrix3 (values) + * Blend mode 1 (enum, default Normal) -> blend1 (mode) + * Over-saturation (0.->1., default 0) -> composite1 (k2) + * Blend mode 2 (enum, default Normal) -> blend2 (mode) +*/ + +class Quadritone : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Quadritone ( ) : Filter() { }; + ~Quadritone ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Quadritone Fantasy") "</name>\n" + "<id>org.inkscape.effect.filter.Quadritone</id>\n" + "<param name=\"dist\" gui-text=\"" N_("Hue distribution (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">280</param>\n" + "<param name=\"colors\" gui-text=\"" N_("Colors") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">100</param>\n" + "<param name=\"blend1\" gui-text=\"" N_("Blend 1:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "</param>\n" + "<param name=\"sat\" gui-text=\"" N_("Over-saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"1.00\">0</param>\n" + "<param name=\"blend2\" gui-text=\"" N_("Blend 2:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Replace hue by two colors") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Quadritone()); + // clang-format on + }; + +}; + +gchar const * +Quadritone::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream dist; + std::ostringstream colors; + std::ostringstream blend1; + std::ostringstream sat; + std::ostringstream blend2; + + dist << ext->get_param_int("dist"); + colors << ext->get_param_int("colors"); + blend1 << ext->get_param_optiongroup("blend1"); + sat << ext->get_param_float("sat"); + blend2 << ext->get_param_optiongroup("blend2"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Quadritone fantasy\">\n" + "<feColorMatrix in=\"SourceGraphic\" type=\"hueRotate\" values=\"%s\" result=\"colormatrix1\" />\n" + "<feColorMatrix type=\"matrix\" values=\"0.5 0 0.5 0 0 0 1 0 0 0 0.5 0 0.5 0 0 0 0 0 1 0 \" result=\"colormatrix2\" />\n" + "<feColorMatrix type=\"hueRotate\" values=\"%s\" result=\"colormatrix3\" />\n" + "<feBlend in2=\"colormatrix3\" mode=\"%s\" result=\"blend1\" />\n" + "<feColorMatrix type=\"matrix\" values=\"2.5 -0.75 -0.75 0 0 -0.75 2.5 -0.75 0 0 -0.75 -0.75 2.5 0 0 0 0 0 1 0 \" result=\"colormatrix4\" />\n" + "<feComposite in=\"colormatrix4\" in2=\"blend1\" operator=\"arithmetic\" k2=\"%s\" result=\"composite1\" />\n" + "<feBlend in2=\"blend1\" mode=\"%s\" result=\"blend2\" />\n" + "</filter>\n", dist.str().c_str(), colors.str().c_str(), blend1.str().c_str(), sat.str().c_str(), blend2.str().c_str() ); + // clang-format on + + return _filter; +}; /* Quadritone filter */ + + +/** + \brief Custom predefined Simple blend filter. + + Simple blend filter. + + Filter's parameters: + * Color (guint, default 16777215) -> flood1 (flood-opacity, flood-color) + * Blend mode (enum, default Hue) -> blend1 (mode) +*/ +class SimpleBlend : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + SimpleBlend ( ) : Filter() { }; + ~SimpleBlend ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Simple blend") "</name>\n" + "<id>org.inkscape.effect.filter.SimpleBlend</id>\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">16777215</param>\n" + "<param name=\"blendmode\" gui-text=\"" N_("Blend mode:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"saturation\">" N_("Saturation") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"difference\">" N_("Difference") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"luminosity\">" N_("Luminosity") "</option>\n" + "<option value=\"overlay\">" N_("Overlay") "</option>\n" + "<option value=\"color-dodge\">" N_("Color Dodge") "</option>\n" + "<option value=\"color-burn\">" N_("Color Burn") "</option>\n" + "<option value=\"color\">" N_("Color") "</option>\n" + "<option value=\"hard-light\">" N_("Hard Light") "</option>\n" + "<option value=\"hue\">" N_("Hue") "</option>\n" + "<option value=\"exclusion\">" N_("Exclusion") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Simple blend filter") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new SimpleBlend()); + // clang-format on + }; +}; + +gchar const * +SimpleBlend::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream blend; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + blend << ext->get_param_optiongroup("blendmode"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Simple blend\">\n" + "<feFlood result=\"flood1\" flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" />\n" + "<feBlend result=\"blend1\" in=\"flood1\" in2=\"SourceGraphic\" mode=\"%s\" />\n" + "<feComposite operator=\"in\" in=\"blend1\" in2=\"SourceGraphic\" />\n" + "</filter>\n", r.str().c_str(), g.str().c_str(), b.str().c_str(), + a.str().c_str(), blend.str().c_str()); + // clang-format on + + return _filter; +}; /* SimpleBlend filter */ + +/** + \brief Custom predefined Solarize filter. + + Classic photographic solarization effect. + + Filter's parameters: + * Type (enum, default "Solarize") -> + Solarize = blend1 (mode="darken"), blend2 (mode="screen") + Moonarize = blend1 (mode="lighten"), blend2 (mode="multiply") [No other access to the blend modes] + * Hue rotation (0->360, default 0) -> colormatrix1 (values) +*/ + + +class Solarize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Solarize ( ) : Filter() { }; + ~Solarize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Solarize") "</name>\n" + "<id>org.inkscape.effect.filter.Solarize</id>\n" + "<param name=\"rotate\" gui-text=\"" N_("Hue rotation (°)") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">0</param>\n" + "<param name=\"type\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"solarize\">" N_("Solarize") "</option>\n" + "<option value=\"moonarize\">" N_("Moonarize") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Classic photographic solarization effect") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Solarize()); + // clang-format on + }; + +}; + +gchar const * +Solarize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream rotate; + std::ostringstream blend1; + std::ostringstream blend2; + + rotate << ext->get_param_int("rotate"); + const gchar *type = ext->get_param_optiongroup("type"); + if ((g_ascii_strcasecmp("solarize", type) == 0)) { + // Solarize + blend1 << "darken"; + blend2 << "screen"; + } else { + // Moonarize + blend1 << "lighten"; + blend2 << "multiply"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Solarize\">\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 1 \" />\n" + "<feColorMatrix type=\"hueRotate\" values=\"%s\" result=\"colormatrix2\" />\n" + "<feColorMatrix in=\"colormatrix2\" values=\"-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 \" result=\"colormatrix3\" />\n" + "<feBlend in=\"colormatrix3\" in2=\"colormatrix2\" mode=\"%s\" result=\"blend1\" />\n" + "<feBlend in2=\"blend1\" mode=\"%s\" result=\"blend2\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" />\n" + "</filter>\n", rotate.str().c_str(), blend1.str().c_str(), blend2.str().c_str() ); + // clang-format on + + return _filter; +}; /* Solarize filter */ + +/** + \brief Custom predefined Tritone filter. + + Create a custom tritone palette with additional glow, blend modes and hue moving. + + Filter's parameters: + * Option (enum, default Normal) -> + Normal = composite1 (in2="flood"), composite2 (in="p", in2="blend6"), blend6 (in2="composite1") + Enhance hue = Normal + composite2 (in="SourceGraphic") + Phosphorescence = Normal + blend6 (in2="SourceGraphic") composite2 (in="blend6", in2="composite1") + PhosphorescenceB = Normal + blend6 (in2="flood") composite1 (in2="SourceGraphic") + Hue to background = Normal + composite1 (in2="BackgroundImage") [a template with an activated background is needed, or colors become black] + * Hue distribution (0->360, default 0) -> colormatrix1 (values) + * Colors (guint, default -73203457) -> flood (flood-opacity, flood-color) + * Global blend (enum, default Lighten) -> blend5 (mode) [Multiply, Screen, Darken, Lighten only!] + * Glow (0.01->10., default 0.01) -> blur (stdDeviation) + * Glow & blend (enum, default Normal) -> blend6 (mode) [Normal, Multiply and Darken only!] + * Local light (0.->10., default 0) -> composite2 (k1) + * Global light (0.->10., default 1) -> composite2 (k3) [k2 must be fixed to 1]. +*/ + +class Tritone : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Tritone ( ) : Filter() { }; + ~Tritone ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Tritone") "</name>\n" + "<id>org.inkscape.effect.filter.Tritone</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"type\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"enhue\">" N_("Enhance hue") "</option>\n" + "<option value=\"phospho\">" N_("Phosphorescence") "</option>\n" + "<option value=\"phosphoB\">" N_("Colored nights") "</option>\n" + "<option value=\"htb\">" N_("Hue to background") "</option>\n" + "</param>\n" + "<param name=\"globalblend\" gui-text=\"" N_("Global blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "<param name=\"glow\" gui-text=\"" N_("Glow") "\" type=\"float\" appearance=\"full\" min=\"0.01\" max=\"10\">0.01</param>\n" + "<param name=\"glowblend\" gui-text=\"" N_("Glow blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "<param name=\"llight\" gui-text=\"" N_("Local light") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"10\">0</param>\n" + "<param name=\"glight\" gui-text=\"" N_("Global light") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"10\">1</param>\n" + "</page>\n" + "<page name=\"co1tab\" gui-text=\"Color\">\n" + "<param name=\"dist\" gui-text=\"" N_("Hue distribution (°):") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"360\">0</param>\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">-73203457</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Color") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Create a custom tritone palette with additional glow, blend modes and hue moving") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Tritone()); + // clang-format on + }; + +}; + +gchar const * +Tritone::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream dist; + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream globalblend; + std::ostringstream glow; + std::ostringstream glowblend; + std::ostringstream llight; + std::ostringstream glight; + std::ostringstream c1in2; + std::ostringstream c2in; + std::ostringstream c2in2; + std::ostringstream b6in2; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + globalblend << ext->get_param_optiongroup("globalblend"); + dist << ext->get_param_int("dist"); + glow << ext->get_param_float("glow"); + glowblend << ext->get_param_optiongroup("glowblend"); + llight << ext->get_param_float("llight"); + glight << ext->get_param_float("glight"); + + const gchar *type = ext->get_param_optiongroup("type"); + if ((g_ascii_strcasecmp("enhue", type) == 0)) { + // Enhance hue + c1in2 << "flood"; + c2in << "SourceGraphic"; + c2in2 << "blend6"; + b6in2 << "composite1"; + } else if ((g_ascii_strcasecmp("phospho", type) == 0)) { + // Phosphorescence + c1in2 << "flood"; + c2in << "blend6"; + c2in2 << "composite1"; + b6in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("phosphoB", type) == 0)) { + // Phosphorescence B + c1in2 << "SourceGraphic"; + c2in << "blend6"; + c2in2 << "composite1"; + b6in2 << "flood"; + } else if ((g_ascii_strcasecmp("htb", type) == 0)) { + // Hue to background + c1in2 << "BackgroundImage"; + c2in << "blend2"; + c2in2 << "blend6"; + b6in2 << "composite1"; + } else { + // Normal + c1in2 << "flood"; + c2in << "blend2"; + c2in2 << "blend6"; + b6in2 << "composite"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Tritone\">\n" + "<feColorMatrix type=\"hueRotate\" values=\"%s\" result=\"colormatrix1\" />\n" + "<feColorMatrix in=\"colormatrix1\" type=\"matrix\" values=\"1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1 \" result=\"colormatrix2\" />\n" + "<feColorMatrix in=\"colormatrix1\" type=\"matrix\" values=\"0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 \" result=\"colormatrix3\" />\n" + "<feColorMatrix in=\"colormatrix1\" type=\"matrix\" values=\"0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 1 \" result=\"colormatrix4\" />\n" + "<feBlend in=\"colormatrix2\" in2=\"colormatrix3\" mode=\"darken\" result=\"blend1\" />\n" + "<feBlend in=\"blend1\" in2=\"colormatrix4\" mode=\"darken\" result=\"blend2\" />\n" + "<feBlend in=\"colormatrix2\" in2=\"colormatrix3\" mode=\"lighten\" result=\"blend3\" />\n" + "<feBlend in=\"blend3\" in2=\"colormatrix4\" mode=\"lighten\" result=\"blend4\" />\n" + "<feComponentTransfer in=\"blend4\" result=\"componentTransfer\">\n" + "<feFuncR type=\"linear\" slope=\"0\" />\n" + "</feComponentTransfer>\n" + "<feBlend in=\"blend2\" in2=\"componentTransfer\" mode=\"%s\" result=\"blend5\" />\n" + "<feColorMatrix in=\"blend5\" type=\"matrix\" values=\"-1 1 0 0 0 -1 1 0 0 0 -1 1 0 0 0 0 0 0 0 1 \" result=\"colormatrix5\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feComposite in=\"colormatrix5\" in2=\"%s\" operator=\"arithmetic\" k1=\"1\" result=\"composite1\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur\" />\n" + "<feBlend in2=\"%s\" mode=\"%s\" result=\"blend6\" />\n" + "<feComposite in=\"%s\" in2=\"%s\" operator=\"arithmetic\" k1=\"%s\" k2=\"1\" k3=\"%s\" k4=\"0\" result=\"composite2\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" result=\"composite3\" />\n" + "</filter>\n", dist.str().c_str(), globalblend.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + c1in2.str().c_str(), glow.str().c_str(), b6in2.str().c_str(), glowblend.str().c_str(), + c2in.str().c_str(), c2in2.str().c_str(), llight.str().c_str(), glight.str().c_str() ); + // clang-format on + + return _filter; +}; /* Tritone filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'COLOR' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_COLOR_H__ */ diff --git a/src/extension/internal/filter/distort.h b/src/extension/internal/filter/distort.h new file mode 100644 index 0000000..c27dba5 --- /dev/null +++ b/src/extension/internal/filter/distort.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ +/* Change the 'DISTORT' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Distort filters + * Felt Feather + * Roughen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined FeltFeather filter. + + Blur and displace edges of shapes and pictures + + Filter's parameters: + * Type (enum, default "In") -> + in = map (in="composite3") + out = map (in="blur") + * Horizontal blur (0.01->30., default 15) -> blur (stdDeviation) + * Vertical blur (0.01->30., default 15) -> blur (stdDeviation) + * Dilatation (n-1th value, 0.->100., default 1) -> colormatrix (matrix) + * Erosion (nth value, 0.->100., default 0) -> colormatrix (matrix) + * Stroke (enum, default "Normal") -> + Normal = composite4 (operator="atop") + Wide = composite4 (operator="over") + Narrow = composite4 (operator="in") + No fill = composite4 (operator="xor") + * Roughness (group) + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->1., default 0.05) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->1., default 0.05) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->100, default 0) -> turbulence (seed) + * Intensity (0.0->100., default 30) -> displacement (scale) +*/ + +class FeltFeather : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + FeltFeather ( ) : Filter() { }; + ~FeltFeather ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Felt Feather") "</name>\n" + "<id>org.inkscape.effect.filter.FeltFeather</id>\n" + "<param name=\"type\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"in\">" N_("In") "</option>\n" + "<option value=\"out\">" N_("Out") "</option>\n" + "</param>\n" + "<param name=\"hblur\" gui-text=\"" N_("Horizontal blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.00\">15</param>\n" + "<param name=\"vblur\" gui-text=\"" N_("Vertical blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.00\">15</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">1</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">0</param>\n" + "<param name=\"stroke\" gui-text=\"" N_("Stroke:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"atop\">" N_("Normal") "</option>\n" + "<option value=\"over\">" N_("Wide") "</option>\n" + "<option value=\"in\">" N_("Narrow") "</option>\n" + "<option value=\"xor\">" N_("No fill") "</option>\n" + "</param>\n" + "<param name=\"turbulence\" indent=\"1\" gui-text=\"" N_("Turbulence:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"fractalNoise\">" N_("Fractal noise") "</option>\n" + "<option value=\"turbulence\">" N_("Turbulence") "</option>\n" + "</param>\n" + "<param name=\"hfreq\" gui-text=\"" N_("Horizontal frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.\">5</param>\n" + "<param name=\"vfreq\" gui-text=\"" N_("Vertical frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.\">5</param>\n" + "<param name=\"complexity\" gui-text=\"" N_("Complexity") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">3</param>\n" + "<param name=\"variation\" gui-text=\"" N_("Variation") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"100\">0</param>\n" + "<param name=\"intensity\" gui-text=\"" N_("Intensity") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"100\">30</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Distort") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blur and displace edges of shapes and pictures") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new FeltFeather()); + // clang-format on + }; + +}; + +gchar const * +FeltFeather::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream dilat; + std::ostringstream erosion; + + std::ostringstream turbulence; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream intensity; + + std::ostringstream map; + std::ostringstream stroke; + + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + dilat << ext->get_param_float("dilat"); + erosion << -ext->get_param_float("erosion"); + + turbulence << ext->get_param_optiongroup("turbulence"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + intensity << ext->get_param_float("intensity"); + + stroke << ext->get_param_optiongroup("stroke"); + + const gchar *maptype = ext->get_param_optiongroup("type"); + if (g_ascii_strcasecmp("in", maptype) == 0) { + map << "composite3"; + } else { + map << "blur"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" x=\"-0.3\" width=\"1.6\" y=\"-0.3\" height=\"1.6\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Felt Feather\">\n" + "<feGaussianBlur stdDeviation=\"%s %s\" result=\"blur\" />\n" + "<feComposite in=\"SourceGraphic\" in2=\"blur\" operator=\"atop\" result=\"composite1\" />\n" + "<feComposite in2=\"composite1\" operator=\"in\" result=\"composite2\" />\n" + "<feComposite in2=\"composite2\" operator=\"in\" result=\"composite3\" />\n" + "<feTurbulence type=\"%s\" numOctaves=\"%s\" seed=\"%s\" baseFrequency=\"%s %s\" result=\"turbulence\" />\n" + "<feDisplacementMap in=\"%s\" in2=\"turbulence\" xChannelSelector=\"R\" scale=\"%s\" yChannelSelector=\"G\" result=\"map\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix\" />\n" + "<feComposite in=\"composite3\" in2=\"colormatrix\" operator=\"%s\" result=\"composite4\" />\n" + "</filter>\n", hblur.str().c_str(), vblur.str().c_str(), + turbulence.str().c_str(), complexity.str().c_str(), variation.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), + map.str().c_str(), intensity.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), stroke.str().c_str() ); + // clang-format on + + return _filter; +}; /* Felt feather filter */ + +/** + \brief Custom predefined Roughen filter. + + Small-scale roughening to edges and content + + Filter's parameters: + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->10., default 0.013) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->10., default 0.013) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 5) -> turbulence (numOctaves) + * Variation (1->360, default 1) -> turbulence (seed) + * Intensity (0.0->50., default 6.6) -> displacement (scale) +*/ + +class Roughen : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Roughen ( ) : Filter() { }; + ~Roughen ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Roughen") "</name>\n" + "<id>org.inkscape.effect.filter.Roughen</id>\n" + "<param name=\"type\" gui-text=\"" N_("Turbulence type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"fractalNoise\">" N_("Fractal noise") "</option>\n" + "<option value=\"turbulence\">" N_("Turbulence") "</option>\n" + "</param>\n" + "<param name=\"hfreq\" gui-text=\"" N_("Horizontal frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.1\" max=\"1000.00\">1.3</param>\n" + "<param name=\"vfreq\" gui-text=\"" N_("Vertical frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.1\" max=\"1000.00\">1.3</param>\n" + "<param name=\"complexity\" gui-text=\"" N_("Complexity") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">5</param>\n" + "<param name=\"variation\" gui-text=\"" N_("Variation") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"360\">0</param>\n" + "<param name=\"intensity\" gui-text=\"" N_("Intensity") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"50\">6.6</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Distort") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Small-scale roughening to edges and content") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Roughen()); + // clang-format on + }; + +}; + +gchar const * +Roughen::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream intensity; + + type << ext->get_param_optiongroup("type"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + intensity << ext->get_param_float("intensity"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Roughen\">\n" + "<feTurbulence type=\"%s\" numOctaves=\"%s\" seed=\"%s\" baseFrequency=\"%s %s\" result=\"turbulence\" />\n" + "<feDisplacementMap in=\"SourceGraphic\" in2=\"turbulence\" scale=\"%s\" yChannelSelector=\"G\" xChannelSelector=\"R\" />\n" + "</filter>\n", type.str().c_str(), complexity.str().c_str(), variation.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), intensity.str().c_str()); + // clang-format on + + return _filter; +}; /* Roughen filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'DISTORT' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_DISTORT_H__ */ diff --git a/src/extension/internal/filter/filter-all.cpp b/src/extension/internal/filter/filter-all.cpp new file mode 100644 index 0000000..5aa3900 --- /dev/null +++ b/src/extension/internal/filter/filter-all.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2008 Authors: + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter.h" + +/* Put your filter here */ +#include "bevels.h" +#include "blurs.h" +#include "bumps.h" +#include "color.h" +#include "distort.h" +#include "image.h" +#include "morphology.h" +#include "overlays.h" +#include "paint.h" +#include "protrusions.h" +#include "shadows.h" +#include "textures.h" +#include "transparency.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +void +Filter::filters_all ( ) +{ + // Here come the filters which are coded in C++ in order to present a parameters dialog + + /* Experimental custom predefined filters */ + + // Bevels + DiffuseLight::init(); + MatteJelly::init(); + SpecularLight::init(); + + // Blurs + Blur::init(); + CleanEdges::init(); + CrossBlur::init(); + Feather::init(); + ImageBlur::init(); + + // Bumps + Bump::init(); + WaxBump::init(); + + // Color + Brilliance::init(); + ChannelPaint::init(); + ColorBlindness::init(); + ColorShift::init(); + Colorize::init(); + ComponentTransfer::init(); + Duochrome::init(); + ExtractChannel::init(); + FadeToBW::init(); + Greyscale::init(); + Invert::init(); + Lighting::init(); + LightnessContrast::init(); + NudgeRGB::init(); + NudgeCMY::init(); + Quadritone::init(); + SimpleBlend::init(); + Solarize::init(); + Tritone::init(); + + // Distort + FeltFeather::init(); + Roughen::init(); + + // Image effect + EdgeDetect::init(); + + // Image paint and draw + Chromolitho::init(); + CrossEngraving::init(); + Drawing::init(); + Electrize::init(); + NeonDraw::init(); + PointEngraving::init(); + Posterize::init(); + PosterizeBasic::init(); + + // Morphology + Crosssmooth::init(); + Outline::init(); + + // Overlays + NoiseFill::init(); + + // Protrusions + Snow::init(); + + // Shadows and glows + ColorizableDropShadow::init(); + + // Textures + InkBlot::init(); + + // Fill and transparency + Blend::init(); + ChannelTransparency::init(); + LightEraser::init(); + Opacity::init(); + Silhouette::init(); + + // Here come the rest of the filters that are read from SVG files in share/filters and + // .config/Inkscape/filters + /* This should always be last, don't put stuff below this + * line. */ + Filter::filters_all_files(); + + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/internal/filter/filter-file.cpp b/src/extension/internal/filter/filter-file.cpp new file mode 100644 index 0000000..0776350 --- /dev/null +++ b/src/extension/internal/filter/filter-file.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2008 Authors: + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "filter.h" + +#include "io/sys.h" +#include "io/resource.h" +#include "io/stream/inkscapestream.h" + +/* Directory includes */ +#include "path-prefix.h" +#include "inkscape.h" + +/* Extension */ +#include "extension/extension.h" +#include "extension/system.h" + +/* System includes */ +#include <glibmm/i18n.h> +#include <glibmm/fileutils.h> + +using namespace Inkscape::IO::Resource; + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +void +filters_load_file (Glib::ustring filename, gchar * menuname) +{ + Inkscape::XML::Document *doc = sp_repr_read_file(filename.c_str(), INKSCAPE_EXTENSION_URI); + if (doc == nullptr) { + g_warning("File (%s) is not parseable as XML. Ignored.", filename.c_str()); + return; + } + + Inkscape::XML::Node * root = doc->root(); + if (strcmp(root->name(), "svg:svg")) { + Inkscape::GC::release(doc); + g_warning("File (%s) is not SVG. Ignored.", filename.c_str()); + return; + } + + for (Inkscape::XML::Node * child = root->firstChild(); + child != nullptr; child = child->next()) { + if (!strcmp(child->name(), "svg:defs")) { + for (Inkscape::XML::Node * defs = child->firstChild(); + defs != nullptr; defs = defs->next()) { + if (!strcmp(defs->name(), "svg:filter")) { + Filter::filters_load_node(defs, menuname); + } // oh! a filter + } //defs + } // is defs + } // children of root + + Inkscape::GC::release(doc); + return; +} + +void Filter::filters_all_files() +{ + for(auto &filename: get_filenames(USER, FILTERS, {".svg"})) { + filters_load_file(filename, _("Personal")); + } + for(auto &filename: get_filenames(SYSTEM, FILTERS, {".svg"})) { + filters_load_file(filename, _("Bundled")); + } +} + + +#include "extension/internal/clear-n_.h" + +class mywriter : public Inkscape::IO::BasicWriter { + Glib::ustring _str; +public: + void close() override; + void flush() override; + void put (char ch) override; + gchar const * c_str () { return _str.c_str(); } +}; + +void mywriter::close () { return; } +void mywriter::flush () { return; } +void mywriter::put (char ch) { _str += ch; } + + +void +Filter::filters_load_node (Inkscape::XML::Node *node, gchar * menuname) +{ + gchar const * label = node->attribute("inkscape:label"); + gchar const * menu = node->attribute("inkscape:menu"); + gchar const * menu_tooltip = node->attribute("inkscape:menu-tooltip"); + gchar const * id = node->attribute("id"); + + if (label == nullptr) { + label = id; + } + + // clang-format off + gchar * xml_str = g_strdup_printf( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>%s</name>\n" + "<id>org.inkscape.effect.filter.%s</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"%s\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>%s</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", label, id, menu? menu : menuname, menu_tooltip? menu_tooltip : label); + // clang-format on + + // FIXME: Bad hack: since we pull out a single filter node out of SVG file and + // serialize it, it loses the namespace declarations from the root, so we must provide + // one right here for our inkscape attributes + node->setAttribute("xmlns:inkscape", SP_INKSCAPE_NS_URI); + + mywriter writer; + sp_repr_write_stream(node, writer, 0, FALSE, g_quark_from_static_string("svg"), 0, 0); + + Inkscape::Extension::build_from_mem(xml_str, new Filter(g_strdup(writer.c_str()))); + g_free(xml_str); + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + diff --git a/src/extension/internal/filter/filter.cpp b/src/extension/internal/filter/filter.cpp new file mode 100644 index 0000000..c9779c7 --- /dev/null +++ b/src/extension/internal/filter/filter.cpp @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "selection.h" +#include "extension/extension.h" +#include "extension/effect.h" +#include "extension/system.h" +#include "xml/repr.h" +#include "xml/simple-node.h" +#include "xml/attribute-record.h" +#include "object/sp-defs.h" + +#include "filter.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +Filter::Filter() : + Inkscape::Extension::Implementation::Implementation(), + _filter(nullptr) { + return; +} + +Filter::Filter(gchar const * filter) : + Inkscape::Extension::Implementation::Implementation(), + _filter(filter) { + return; +} + +Filter::~Filter () { + if (_filter != nullptr) { + _filter = nullptr; + } + + return; +} + +bool Filter::load(Inkscape::Extension::Extension * /*module*/) +{ + return true; +} + +Inkscape::Extension::Implementation::ImplementationDocumentCache *Filter::newDocCache(Inkscape::Extension::Extension * /*ext*/, + Inkscape::UI::View::View * /*doc*/) +{ + return nullptr; +} + +gchar const *Filter::get_filter_text(Inkscape::Extension::Extension * /*ext*/) +{ + return _filter; +} + +Inkscape::XML::Document * +Filter::get_filter (Inkscape::Extension::Extension * ext) { + gchar const * filter = get_filter_text(ext); + return sp_repr_read_mem(filter, strlen(filter), nullptr); +} + +void +Filter::merge_filters( Inkscape::XML::Node * to, Inkscape::XML::Node * from, + Inkscape::XML::Document * doc, + gchar const * srcGraphic, gchar const * srcGraphicAlpha) +{ + if (from == nullptr) return; + + // copy attributes + for ( const auto & iter : from->attributeList()) { + gchar const * attr = g_quark_to_string(iter.key); + //printf("Attribute List: %s\n", attr); + if (!strcmp(attr, "id")) continue; // nope, don't copy that one! + to->setAttribute(attr, from->attribute(attr)); + + if (!strcmp(attr, "in") || !strcmp(attr, "in2") || !strcmp(attr, "in3")) { + if (srcGraphic != nullptr && !strcmp(from->attribute(attr), "SourceGraphic")) { + to->setAttribute(attr, srcGraphic); + } + + if (srcGraphicAlpha != nullptr && !strcmp(from->attribute(attr), "SourceAlpha")) { + to->setAttribute(attr, srcGraphicAlpha); + } + } + } + + // for each child call recursively + for (Inkscape::XML::Node * from_child = from->firstChild(); + from_child != nullptr ; from_child = from_child->next()) { + Glib::ustring name = "svg:"; + name += from_child->name(); + + Inkscape::XML::Node * to_child = doc->createElement(name.c_str()); + to->appendChild(to_child); + merge_filters(to_child, from_child, doc, srcGraphic, srcGraphicAlpha); + + if (from_child == from->firstChild() && !strcmp("filter", from->name()) && srcGraphic != nullptr && to_child->attribute("in") == nullptr) { + to_child->setAttribute("in", srcGraphic); + } + Inkscape::GC::release(to_child); + } +} + +#define FILTER_SRC_GRAPHIC "fbSourceGraphic" +#define FILTER_SRC_GRAPHIC_ALPHA "fbSourceGraphicAlpha" + +void Filter::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, + Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + Inkscape::XML::Document *filterdoc = get_filter(module); + if (filterdoc == nullptr) { + return; // could not parse the XML source of the filter; typically parser will stderr a warning + } + + //printf("Calling filter effect\n"); + Inkscape::Selection * selection = ((SPDesktop *)document)->selection; + + // TODO need to properly refcount the items, at least + std::vector<SPItem*> items(selection->items().begin(), selection->items().end()); + + Inkscape::XML::Document * xmldoc = document->doc()->getReprDoc(); + Inkscape::XML::Node * defsrepr = document->doc()->getDefs()->getRepr(); + + for(auto spitem : items) { + Inkscape::XML::Node *node = spitem->getRepr(); + + SPCSSAttr * css = sp_repr_css_attr(node, "style"); + gchar const * filter = sp_repr_css_property(css, "filter", nullptr); + + if (filter == nullptr) { + + Inkscape::XML::Node * newfilterroot = xmldoc->createElement("svg:filter"); + merge_filters(newfilterroot, filterdoc->root(), xmldoc); + defsrepr->appendChild(newfilterroot); + document->doc()->resources_changed_signals[g_quark_from_string("filter")].emit(); + + Glib::ustring url = "url(#"; url += newfilterroot->attribute("id"); url += ")"; + + + Inkscape::GC::release(newfilterroot); + + sp_repr_css_set_property(css, "filter", url.c_str()); + sp_repr_css_set(node, css, "style"); + } else { + if (strncmp(filter, "url(#", strlen("url(#")) || filter[strlen(filter) - 1] != ')') { + // This is not url(#id) -- we can't handle it + continue; + } + + gchar * lfilter = g_strndup(filter + 5, strlen(filter) - 6); + Inkscape::XML::Node * filternode = nullptr; + for (Inkscape::XML::Node * child = defsrepr->firstChild(); child != nullptr; child = child->next()) { + if (!strcmp(lfilter, child->attribute("id"))) { + filternode = child; + break; + } + } + g_free(lfilter); + + // no filter + if (filternode == nullptr) { + g_warning("no assigned filter found!"); + continue; + } + + if (filternode->lastChild() == nullptr) { + // empty filter, we insert + merge_filters(filternode, filterdoc->root(), xmldoc); + } else { + // existing filter, we merge + filternode->lastChild()->setAttribute("result", FILTER_SRC_GRAPHIC); + Inkscape::XML::Node * alpha = xmldoc->createElement("svg:feColorMatrix"); + alpha->setAttribute("result", FILTER_SRC_GRAPHIC_ALPHA); + alpha->setAttribute("in", FILTER_SRC_GRAPHIC); // not required, but we're being explicit + alpha->setAttribute("values", "0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"); + + filternode->appendChild(alpha); + + merge_filters(filternode, filterdoc->root(), xmldoc, FILTER_SRC_GRAPHIC, FILTER_SRC_GRAPHIC_ALPHA); + + Inkscape::GC::release(alpha); + } + } + } + + return; +} + +#include "extension/internal/clear-n_.h" + +void +Filter::filter_init (gchar const * id, gchar const * name, gchar const * submenu, gchar const * tip, gchar const * filter) +{ + // clang-format off + gchar * xml_str = g_strdup_printf( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>%s</name>\n" + "<id>org.inkscape.effect.filter.%s</id>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\" />\n" + "<submenu name=\"%s\"/>\n" + "</effects-menu>\n" + "<menu-tip>%s</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", name, id, submenu, tip); + // clang-format on + Inkscape::Extension::build_from_mem(xml_str, new Filter(filter)); + g_free(xml_str); + return; +} + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/filter/filter.h b/src/extension/internal/filter/filter.h new file mode 100644 index 0000000..cb3ed36 --- /dev/null +++ b/src/extension/internal/filter/filter.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H +#define INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H + +/* + * Copyright (C) 2008 Authors: + * Ted Gould <ted@gould.cx> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> + +#include "extension/implementation/implementation.h" + +namespace Inkscape { + +namespace XML { + struct Document; +} + +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { +namespace Filter { + +class Filter : public Inkscape::Extension::Implementation::Implementation { +protected: + gchar const * _filter; + virtual gchar const * get_filter_text (Inkscape::Extension::Extension * ext); + +private: + Inkscape::XML::Document * get_filter (Inkscape::Extension::Extension * ext); + void merge_filters (Inkscape::XML::Node * to, Inkscape::XML::Node * from, Inkscape::XML::Document * doc, gchar const * srcGraphic = nullptr, gchar const * srcGraphicAlpha = nullptr); + +public: + Filter(); + Filter(gchar const * filter); + ~Filter() override; + + bool load(Inkscape::Extension::Extension *module) override; + Inkscape::Extension::Implementation::ImplementationDocumentCache * newDocCache (Inkscape::Extension::Extension * ext, Inkscape::UI::View::View * doc) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void filter_init(gchar const * id, gchar const * name, gchar const * submenu, gchar const * tip, gchar const * filter); + static void filters_all(); + + /* File loader related */ + static void filters_all_files(); + static void filters_load_node(Inkscape::XML::Node *node, gchar * menuname); + +}; + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +#endif // INKSCAPE_EXTENSION_INTERNAL_FILTER_FILTER_H diff --git a/src/extension/internal/filter/image.h b/src/extension/internal/filter/image.h new file mode 100644 index 0000000..8820122 --- /dev/null +++ b/src/extension/internal/filter/image.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ +/* Change the 'IMAGE' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Image filters + * Edge detect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Edge detect filter. + + Detect color edges in object. + + Filter's parameters: + * Detection type (enum, default Full) -> convolve (kernelMatrix) + * Level (0.01->10., default 1.) -> convolve (divisor) + * Inverted (boolean, default false) -> convolve (bias) +*/ +class EdgeDetect : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + EdgeDetect ( ) : Filter() { }; + ~EdgeDetect ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Edge Detect") "</name>\n" + "<id>org.inkscape.effect.filter.EdgeDetect</id>\n" + "<param name=\"type\" gui-text=\"" N_("Detect:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value=\"all\">" N_("All") "</option>\n" + "<option value=\"vertical\">" N_("Vertical lines") "</option>\n" + "<option value=\"horizontal\">" N_("Horizontal lines") "</option>\n" + "</param>\n" + "<param name=\"level\" gui-text=\"" N_("Level") "\" type=\"float\" appearance=\"full\" min=\"0.1\" max=\"100.0\">1.0</param>\n" + "<param name=\"inverted\" gui-text=\"" N_("Invert colors") "\" type=\"bool\" >false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Effects") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Detect color edges in object") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new EdgeDetect()); + // clang-format on + }; + +}; + +gchar const * +EdgeDetect::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream matrix; + std::ostringstream inverted; + std::ostringstream level; + + const gchar *type = ext->get_param_optiongroup("type"); + + level << 1 / ext->get_param_float("level"); + + if ((g_ascii_strcasecmp("vertical", type) == 0)) { + matrix << "0 0 0 1 -2 1 0 0 0"; + } else if ((g_ascii_strcasecmp("horizontal", type) == 0)) { + matrix << "0 1 0 0 -2 0 0 1 0"; + } else { + matrix << "0 1 0 1 -4 1 0 1 0"; + } + + if (ext->get_param_bool("inverted")) { + inverted << "1"; + } else { + inverted << "0"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Edge Detect\">\n" + "<feConvolveMatrix in=\"SourceGraphic\" kernelMatrix=\"%s\" order=\"3 3\" bias=\"%s\" divisor=\"%s\" targetX=\"1\" targetY=\"1\" preserveAlpha=\"true\" result=\"convolve\" />\n" + "</filter>\n", matrix.str().c_str(), inverted.str().c_str(), level.str().c_str()); + // clang-format on + + return _filter; +}; + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'IMAGE' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_IMAGE_H__ */ diff --git a/src/extension/internal/filter/morphology.h b/src/extension/internal/filter/morphology.h new file mode 100644 index 0000000..e2bfa15 --- /dev/null +++ b/src/extension/internal/filter/morphology.h @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ +/* Change the 'MORPHOLOGY' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Morphology filters + * Cross-smooth + * Outline + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Cross-smooth filter. + + Smooth the outside of shapes and pictures. + + Filter's parameters: + * Type (enum, default "Smooth edges") -> + Inner = composite1 (operator="in") + Outer = composite1 (operator="over") + Open = composite1 (operator="XOR") + * Width (0.01->30., default 10.) -> blur (stdDeviation) + * Level (0.2->2., default 1.) -> composite2 (k2) + * Dilatation (1.->100., default 10.) -> colormatrix1 (last-1 value) + * Erosion (1.->100., default 1.) -> colormatrix1 (last value) + * Antialiasing (0.01->1., default 1) -> blur2 (stdDeviation) + * Blur content (boolean, default false) -> blend (true: in="colormatrix2", false: in="SourceGraphic") +*/ + +class Crosssmooth : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Crosssmooth ( ) : Filter() { }; + ~Crosssmooth ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Cross-smooth") "</name>\n" + "<id>org.inkscape.effect.filter.crosssmooth</id>\n" + "<param name=\"type\" gui-text=\"" N_("Type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"in\">" N_("Inner") "</option>\n" + "<option value=\"over\">" N_("Outer") "</option>\n" + "<option value=\"xor\">" N_("Open") "</option>\n" + "</param>\n" + "<param name=\"width\" gui-text=\"" N_("Width") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.\">10</param>\n" + "<param name=\"level\" gui-text=\"" N_("Level") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.2\" max=\"2\">1</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"100\">10</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"100\">1</param>\n" + "<param name=\"antialias\" gui-text=\"" N_("Antialiasing") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"1\">1</param>\n" + "<param name=\"content\" gui-text=\"" N_("Blur content") "\" type=\"bool\" >false</param>\n" + + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Morphology") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Smooth edges and angles of shapes") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Crosssmooth()); + // clang-format on + }; + +}; + +gchar const * +Crosssmooth::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream width; + std::ostringstream level; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream antialias; + std::ostringstream content; + + type << ext->get_param_optiongroup("type"); + width << ext->get_param_float("width"); + level << ext->get_param_float("level"); + dilat << ext->get_param_float("dilat"); + erosion << (1 - ext->get_param_float("erosion")); + antialias << ext->get_param_float("antialias"); + + if (ext->get_param_bool("content")) { + content << "colormatrix2"; + } else { + content << "SourceGraphic"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Cross-smooth\">\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feComposite in=\"blur1\" in2=\"blur1\" operator=\"%s\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"composite1\" k2=\"%s\" operator=\"arithmetic\" result=\"composite2\" />\n" + "<feColorMatrix in=\"composite2\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix1\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feColorMatrix in=\"blur2\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 5 -1 \" result=\"colormatrix2\" />\n" + "<feBlend in=\"%s\" in2=\"colormatrix2\" stdDeviation=\"17\" mode=\"normal\" result=\"blend\" />\n" + "<feComposite in=\"blend\" in2=\"colormatrix2\" operator=\"in\" result=\"composite3\" />\n" + "</filter>\n", width.str().c_str(), type.str().c_str(), level.str().c_str(), + dilat.str().c_str(), erosion.str().c_str(), antialias.str().c_str(), + content.str().c_str()); + // clang-format on + + return _filter; +}; /* Cross-smooth filter */ + +/** + \brief Custom predefined Outline filter. + + Adds a colorizable outline + + Filter's parameters: + * Fill image (boolean, default false) -> true: composite2 (in="SourceGraphic"), false: composite2 (in="blur2") + * Hide image (boolean, default false) -> true: composite4 (in="composite3"), false: composite4 (in="SourceGraphic") + * Stroke type (enum, default over) -> composite2 (operator) + * Stroke position (enum, default inside) + * inside -> composite1 (operator="out", in="SourceGraphic", in2="blur1") + * outside -> composite1 (operator="out", in="blur1", in2="SourceGraphic") + * overlayed -> composite1 (operator="xor", in="blur1", in2="SourceGraphic") + * Width 1 (0.01->20., default 4) -> blur1 (stdDeviation) + * Dilatation 1 (1.->100., default 100) -> colormatrix1 (n-1th value) + * Erosion 1 (0.->100., default 1) -> colormatrix1 (nth value 0->-100) + * Width 2 (0.01->20., default 0.5) -> blur2 (stdDeviation) + * Dilatation 2 (1.->100., default 50) -> colormatrix2 (n-1th value) + * Erosion 2 (0.->100., default 5) -> colormatrix2 (nth value 0->-100) + * Antialiasing (0.01->1., default 1) -> blur3 (stdDeviation) + * Color (guint, default 0,0,0,255) -> flood (flood-color, flood-opacity) + * Fill opacity (0.->1., default 1) -> composite5 (k2) + * Stroke opacity (0.->1., default 1) -> composite5 (k3) + +*/ + +class Outline : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Outline ( ) : Filter() { }; + ~Outline ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Outline") "</name>\n" + "<id>org.inkscape.effect.filter.Outline</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"fill\" gui-text=\"" N_("Fill image") "\" type=\"bool\" >false</param>\n" + "<param name=\"outline\" gui-text=\"" N_("Hide image") "\" type=\"bool\" >false</param>\n" + "<param name=\"type\" gui-text=\"" N_("Composite type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"over\">" N_("Over") "</option>\n" + "<option value=\"in\">" N_("In") "</option>\n" + "<option value=\"out\">" N_("Out") "</option>\n" + "<option value=\"atop\">" N_("Atop") "</option>\n" + "<option value=\"xor\">" N_("XOR") "</option>\n" + "</param>\n" + "<param name=\"position\" gui-text=\"" N_("Position:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"inside\">" N_("Inside") "</option>\n" + "<option value=\"outside\">" N_("Outside") "</option>\n" + "<option value=\"overlayed\">" N_("Overlayed") "</option>\n" + "</param>\n" + "<param name=\"width1\" gui-text=\"" N_("Width 1") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">4</param>\n" + "<param name=\"dilat1\" gui-text=\"" N_("Dilatation 1") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"100\">100</param>\n" + "<param name=\"erosion1\" gui-text=\"" N_("Erosion 1") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">1</param>\n" + "<param name=\"width2\" gui-text=\"" N_("Width 2") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">0.5</param>\n" + "<param name=\"dilat2\" gui-text=\"" N_("Dilatation 2") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"100\">50</param>\n" + "<param name=\"erosion2\" gui-text=\"" N_("Erosion 2") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100\">5</param>\n" + "<param name=\"antialias\" gui-text=\"" N_("Antialiasing") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"1\">1</param>\n" + "<param name=\"smooth\" gui-text=\"" N_("Smooth") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "<page name=\"co11tab\" gui-text=\"Color\">\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">255</param>\n" + "<param name=\"fopacity\" gui-text=\"" N_("Fill opacity:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1\">1</param>\n" + "<param name=\"sopacity\" gui-text=\"" N_("Stroke opacity:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1\">1</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Morphology") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Adds a colorizable outline") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Outline()); + // clang-format on + }; + +}; + +gchar const * +Outline::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream width1; + std::ostringstream dilat1; + std::ostringstream erosion1; + std::ostringstream width2; + std::ostringstream dilat2; + std::ostringstream erosion2; + std::ostringstream antialias; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream fopacity; + std::ostringstream sopacity; + std::ostringstream smooth; + + std::ostringstream c1in; + std::ostringstream c1in2; + std::ostringstream c1op; + std::ostringstream c2in; + std::ostringstream c2op; + std::ostringstream c4in; + + + width1 << ext->get_param_float("width1"); + dilat1 << ext->get_param_float("dilat1"); + erosion1 << (- ext->get_param_float("erosion1")); + width2 << ext->get_param_float("width2"); + dilat2 << ext->get_param_float("dilat2"); + erosion2 << (- ext->get_param_float("erosion2")); + antialias << ext->get_param_float("antialias"); + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + + fopacity << ext->get_param_float("fopacity"); + sopacity << ext->get_param_float("sopacity"); + + const gchar *position = ext->get_param_optiongroup("position"); + if((g_ascii_strcasecmp("inside", position) == 0)) { + // Inside + c1in << "SourceGraphic"; + c1in2 << "blur1"; + c1op << "out"; + } else if((g_ascii_strcasecmp("outside", position) == 0)) { + // Outside + c1in << "blur1"; + c1in2 << "SourceGraphic"; + c1op << "out"; + } else { + // Overlayed + c1in << "blur1"; + c1in2 << "SourceGraphic"; + c1op << "xor"; + } + + if (ext->get_param_bool("fill")) { + c2in << "SourceGraphic"; + } else { + c2in << "blur2"; + } + + c2op << ext->get_param_optiongroup("type"); + + if (ext->get_param_bool("outline")) { + c4in << "composite3"; + } else { + c4in << "SourceGraphic"; + } + + if (ext->get_param_bool("smooth")) { + smooth << "1 0"; + } else { + smooth << "5 -1"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" height=\"1.4\" width=\"1.4\" y=\"-0.2\" x=\"-0.2\" inkscape:label=\"Outline\">\n" + "<feGaussianBlur in=\"SourceAlpha\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feComposite in=\"%s\" in2=\"%s\" operator=\"%s\" result=\"composite1\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix1\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feComposite in=\"%s\" in2=\"blur2\" operator=\"%s\" result=\"composite2\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix2\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur3\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s \" result=\"colormatrix3\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feComposite in=\"flood\" in2=\"colormatrix3\" k2=\"1\" operator=\"in\" result=\"composite3\" />\n" + "<feComposite in=\"%s\" in2=\"colormatrix3\" operator=\"out\" result=\"composite4\" />\n" + "<feComposite in=\"composite4\" in2=\"composite3\" k2=\"%s\" k3=\"%s\" operator=\"arithmetic\" result=\"composite5\" />\n" + "</filter>\n", width1.str().c_str(), c1in.str().c_str(), c1in2.str().c_str(), c1op.str().c_str(), + dilat1.str().c_str(), erosion1.str().c_str(), + width2.str().c_str(), c2in.str().c_str(), c2op.str().c_str(), + dilat2.str().c_str(), erosion2.str().c_str(), antialias.str().c_str(), smooth.str().c_str(), + a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + c4in.str().c_str(), fopacity.str().c_str(), sopacity.str().c_str() ); + // clang-format on + + return _filter; +}; /* Outline filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'MORPHOLOGY' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_MORPHOLOGY_H__ */ diff --git a/src/extension/internal/filter/overlays.h b/src/extension/internal/filter/overlays.h new file mode 100644 index 0000000..b93c070 --- /dev/null +++ b/src/extension/internal/filter/overlays.h @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ +/* Change the 'OVERLAYS' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Overlays filters + * Noise fill + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Noise fill filter. + + Basic noise fill and transparency texture + + Filter's parameters: + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (*1000) (0.01->10000., default 20) -> turbulence (baseFrequency [/1000]) + * Vertical frequency (*1000) (0.01->10000., default 40) -> turbulence (baseFrequency [/1000]) + * Complexity (1->5, default 5) -> turbulence (numOctaves) + * Variation (1->360, default 1) -> turbulence (seed) + * Dilatation (1.->50., default 3) -> color (n-1th value) + * Erosion (0.->50., default 1) -> color (nth value 0->-50) + * Color (guint, default 148,115,39,255) -> flood (flood-color, flood-opacity) + * Inverted (boolean, default false) -> composite1 (operator, true="in", false="out") +*/ + +class NoiseFill : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NoiseFill ( ) : Filter() { }; + ~NoiseFill ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Noise Fill") "</name>\n" + "<id>org.inkscape.effect.filter.NoiseFill</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"" N_("Options") "\">\n" + "<param name=\"type\" gui-text=\"" N_("Turbulence type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"fractalNoise\">" N_("Fractal noise") "</option>\n" + "<option value=\"turbulence\">" N_("Turbulence") "</option>\n" + "</param>\n" + "<param name=\"hfreq\" gui-text=\"" N_("Horizontal frequency:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100.00\">20</param>\n" + "<param name=\"vfreq\" gui-text=\"" N_("Vertical frequency:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"100.00\">40</param>\n" + "<param name=\"complexity\" gui-text=\"" N_("Complexity:") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">5</param>\n" + "<param name=\"variation\" gui-text=\"" N_("Variation:") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"360\">0</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"50\">3</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"50\">1</param>\n" + "<param name=\"inverted\" gui-text=\"" N_("Inverted") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "<page name=\"co11tab\" gui-text=\"" N_("Noise color") "\">\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">354957823</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Overlays") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Basic noise fill and transparency texture") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new NoiseFill()); + // clang-format on + }; + +}; + +gchar const * +NoiseFill::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream inverted; + + type << ext->get_param_optiongroup("type"); + hfreq << (ext->get_param_float("hfreq")); + vfreq << (ext->get_param_float("vfreq")); + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + if (ext->get_param_bool("inverted")) + inverted << "out"; + else + inverted << "in"; + + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Noise Fill\">\n" + "<feTurbulence type=\"%s\" baseFrequency=\"%s %s\" numOctaves=\"%s\" seed=\"%s\" result=\"turbulence\"/>\n" + "<feComposite in=\"SourceGraphic\" in2=\"turbulence\" operator=\"%s\" result=\"composite1\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"color\" />\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feMerge result=\"merge\">\n" + "<feMergeNode in=\"flood\" />\n" + "<feMergeNode in=\"color\" />\n" + "</feMerge>\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), inverted.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str()); + + return _filter; +}; /* NoiseFill filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'OVERLAYS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_OVERLAYS_H__ */ diff --git a/src/extension/internal/filter/paint.h b/src/extension/internal/filter/paint.h new file mode 100644 index 0000000..920b275 --- /dev/null +++ b/src/extension/internal/filter/paint.h @@ -0,0 +1,1061 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ +/* Change the 'PAINT' above to be your file name */ + +/* + * Copyright (C) 2012 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Image paint and draw filters + * Chromolitho + * Cross engraving + * Drawing + * Electrize + * Neon draw + * Point engraving + * Posterize + * Posterize basic + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Chromolitho filter. + + Chromo effect with customizable edge drawing and graininess + + Filter's parameters: + * Drawing (boolean, default checked) -> Checked = blend1 (in="convolve1"), unchecked = blend1 (in="composite1") + * Transparent (boolean, default unchecked) -> Checked = colormatrix5 (in="colormatrix4"), Unchecked = colormatrix5 (in="component1") + * Invert (boolean, default false) -> component1 (tableValues) [adds a trailing 0] + * Dented (boolean, default false) -> component1 (tableValues) [adds intermediate 0s] + * Lightness (0.->10., default 0.) -> composite1 (k1) + * Saturation (0.->1., default 1.) -> colormatrix3 (values) + * Noise reduction (1->1000, default 20) -> convolve (kernelMatrix, central value -1001->-2000, default -1020) + * Drawing blend (enum, default Normal) -> blend1 (mode) + * Smoothness (0.01->10, default 1) -> blur1 (stdDeviation) + * Grain (boolean, default unchecked) -> Checked = blend2 (in="colormatrix2"), Unchecked = blend2 (in="blur1") + * Grain x frequency (0.->1000, default 1000) -> turbulence1 (baseFrequency, first value) + * Grain y frequency (0.->1000, default 1000) -> turbulence1 (baseFrequency, second value) + * Grain complexity (1->5, default 1) -> turbulence1 (numOctaves) + * Grain variation (0->1000, default 0) -> turbulence1 (seed) + * Grain expansion (1.->50., default 1.) -> colormatrix1 (n-1 value) + * Grain erosion (0.->40., default 0.) -> colormatrix1 (nth value) [inverted] + * Grain color (boolean, default true) -> colormatrix2 (values) + * Grain blend (enum, default Normal) -> blend2 (mode) +*/ +class Chromolitho : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Chromolitho ( ) : Filter() { }; + ~Chromolitho ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Chromolitho") "</name>\n" + "<id>org.inkscape.effect.filter.Chromolitho</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<param name=\"drawing\" gui-text=\"" N_("Drawing mode") "\" type=\"bool\" >true</param>\n" + "<param name=\"dblend\" gui-text=\"" N_("Drawing blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"darken\">Darken</option>\n" + "<option value=\"normal\">Normal</option>\n" + "<option value=\"multiply\">Multiply</option>\n" + "<option value=\"screen\">Screen</option>\n" + "<option value=\"lighten\">Lighten</option>\n" + "</param>\n" + "<param name=\"transparent\" gui-text=\"" N_("Transparent") "\" type=\"bool\" >false</param>\n" + "<param name=\"dented\" gui-text=\"" N_("Dented") "\" type=\"bool\" >false</param>\n" + "<param name=\"inverted\" gui-text=\"" N_("Inverted") "\" type=\"bool\" >false</param>\n" + "<param name=\"light\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"10\">0</param>\n" + "<param name=\"saturation\" gui-text=\"" N_("Saturation") "\" type=\"float\" precision=\"2\" appearance=\"full\" min=\"0\" max=\"1\">1</param>\n" + "<param name=\"noise\" gui-text=\"" N_("Noise reduction") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"1000\">10</param>\n" + "<param name=\"smooth\" gui-text=\"" N_("Smoothness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"10.00\">1</param>\n" + "</page>\n" + "<page name=\"graintab\" gui-text=\"" N_("Grain") "\">\n" + "<param name=\"grain\" gui-text=\"" N_("Grain mode") "\" type=\"bool\" >true</param>\n" + "<param name=\"grainxf\" gui-text=\"" N_("Horizontal frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1000\">1000</param>\n" + "<param name=\"grainyf\" gui-text=\"" N_("Vertical frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1000\">1000</param>\n" + "<param name=\"grainc\" gui-text=\"" N_("Complexity") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">1</param>\n" + "<param name=\"grainv\" gui-text=\"" N_("Variation") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"1000\">0</param>\n" + "<param name=\"grainexp\" gui-text=\"" N_("Expansion") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"1\" max=\"50\">1</param>\n" + "<param name=\"grainero\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"40\">0</param>\n" + "<param name=\"graincol\" gui-text=\"" N_("Color") "\" type=\"bool\" >true</param>\n" + "<param name=\"gblend\" gui-text=\"" N_("Grain blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">Normal</option>\n" + "<option value=\"multiply\">Multiply</option>\n" + "<option value=\"screen\">Screen</option>\n" + "<option value=\"lighten\">Lighten</option>\n" + "<option value=\"darken\">Darken</option>\n" + "</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Chromo effect with customizable edge drawing and graininess") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Chromolitho()); + // clang-format on + }; +}; + +gchar const * +Chromolitho::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream b1in; + std::ostringstream b2in; + std::ostringstream col3in; + std::ostringstream transf; + std::ostringstream light; + std::ostringstream saturation; + std::ostringstream noise; + std::ostringstream dblend; + std::ostringstream smooth; + std::ostringstream grainxf; + std::ostringstream grainyf; + std::ostringstream grainc; + std::ostringstream grainv; + std::ostringstream gblend; + std::ostringstream grainexp; + std::ostringstream grainero; + std::ostringstream graincol; + + if (ext->get_param_bool("drawing")) + b1in << "convolve1"; + else + b1in << "composite1"; + + if (ext->get_param_bool("transparent")) + col3in << "colormatrix4"; + else + col3in << "component1"; + light << ext->get_param_float("light"); + saturation << ext->get_param_float("saturation"); + noise << (-1000 - ext->get_param_int("noise")); + dblend << ext->get_param_optiongroup("dblend"); + smooth << ext->get_param_float("smooth"); + + if (ext->get_param_bool("dented")) { + transf << "0 1 0 1"; + } else { + transf << "0 1 1"; + } + if (ext->get_param_bool("inverted")) + transf << " 0"; + + if (ext->get_param_bool("grain")) + b2in << "colormatrix2"; + else + b2in << "blur1"; + grainxf << (ext->get_param_float("grainxf") / 1000); + grainyf << (ext->get_param_float("grainyf") / 1000); + grainc << ext->get_param_int("grainc"); + grainv << ext->get_param_int("grainv"); + gblend << ext->get_param_optiongroup("gblend"); + grainexp << ext->get_param_float("grainexp"); + grainero << (-ext->get_param_float("grainero")); + if (ext->get_param_bool("graincol")) + graincol << "1"; + else + graincol << "0"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Chromolitho\">\n" + "<feComposite in=\"SourceGraphic\" in2=\"SourceGraphic\" operator=\"arithmetic\" k1=\"%s\" k2=\"1\" result=\"composite1\" />\n" + "<feConvolveMatrix in=\"composite1\" kernelMatrix=\"0 250 0 250 %s 250 0 250 0 \" order=\"3 3\" result=\"convolve1\" />\n" + "<feBlend in=\"%s\" in2=\"composite1\" mode=\"%s\" result=\"blend1\" />\n" + "<feGaussianBlur in=\"blend1\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feTurbulence baseFrequency=\"%s %s\" numOctaves=\"%s\" seed=\"%s\" type=\"fractalNoise\" result=\"turbulence1\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"colormatrix1\" />\n" + "<feColorMatrix type=\"saturate\" values=\"%s\" result=\"colormatrix2\" />\n" + "<feBlend in=\"%s\" in2=\"blur1\" mode=\"%s\" result=\"blend2\" />\n" + "<feColorMatrix in=\"blend2\" type=\"saturate\" values=\"%s\" result=\"colormatrix3\" />\n" + "<feComponentTransfer in=\"colormatrix3\" result=\"component1\">\n" + "<feFuncR type=\"discrete\" tableValues=\"%s\" />\n" + "<feFuncG type=\"discrete\" tableValues=\"%s\" />\n" + "<feFuncB type=\"discrete\" tableValues=\"%s\" />\n" + "</feComponentTransfer>\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"colormatrix4\" />\n" + "<feColorMatrix in=\"%s\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 15 0 \" result=\"colormatrix5\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", light.str().c_str(), noise.str().c_str(), b1in.str().c_str(), dblend.str().c_str(), smooth.str().c_str(), grainxf.str().c_str(), grainyf.str().c_str(), grainc.str().c_str(), grainv.str().c_str(), grainexp.str().c_str(), grainero.str().c_str(), graincol.str().c_str(), b2in.str().c_str(), gblend.str().c_str(), saturation.str().c_str(), transf.str().c_str(), transf.str().c_str(), transf.str().c_str(), col3in.str().c_str()); + // clang-format on + + return _filter; +}; /* Chromolitho filter */ + +/** + \brief Custom predefined Cross engraving filter. + + Convert image to an engraving made of vertical and horizontal lines + + Filter's parameters: + * Clean-up (1->500, default 30) -> convolve1 (kernelMatrix, central value -1001->-1500, default -1030) + * Dilatation (1.->50., default 1) -> color2 (n-1th value) + * Erosion (0.->50., default 0) -> color2 (nth value 0->-50) + * Strength (0.->10., default 0.5) -> composite2 (k2) + * Length (0.5->20, default 4) -> blur1 (stdDeviation x), blur2 (stdDeviation y) + * Transparent (boolean, default false) -> composite 4 (in, true->composite3, false->blend) +*/ +class CrossEngraving : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + CrossEngraving ( ) : Filter() { }; + ~CrossEngraving ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Cross Engraving") "</name>\n" + "<id>org.inkscape.effect.filter.CrossEngraving</id>\n" + "<param name=\"clean\" gui-text=\"" N_("Clean-up") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"500\">30</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" appearance=\"full\" min=\"1\" max=\"50\">1</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"50\">0</param>\n" + "<param name=\"strength\" gui-text=\"" N_("Strength") "\" type=\"float\" appearance=\"full\" min=\"0.1\" max=\"10\">0.5</param>\n" + "<param name=\"length\" gui-text=\"" N_("Length") "\" type=\"float\" appearance=\"full\" min=\"0.5\" max=\"20\">4</param>\n" + "<param name=\"trans\" gui-text=\"" N_("Transparent") "\" type=\"bool\" >false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Convert image to an engraving made of vertical and horizontal lines") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new CrossEngraving()); + // clang-format on + }; +}; + +gchar const * +CrossEngraving::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream clean; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream strength; + std::ostringstream length; + std::ostringstream trans; + + clean << (-1000 - ext->get_param_int("clean")); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + strength << ext->get_param_float("strength"); + length << ext->get_param_float("length"); + if (ext->get_param_bool("trans")) + trans << "composite3"; + else + trans << "blend"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Cross Engraving\">\n" + "<feConvolveMatrix in=\"SourceGraphic\" targetY=\"1\" targetX=\"1\" kernelMatrix=\"0 250 0 250 %s 250 0 250 0 \" order=\"3 3\" result=\"convolve\" />\n" + "<feComposite in=\"convolve\" in2=\"convolve\" k1=\"1\" k2=\"1\" operator=\"arithmetic\" result=\"composite1\" />\n" + "<feColorMatrix in=\"composite1\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"color1\" />\n" + "<feColorMatrix in=\"color1\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"color2\" />\n" + "<feComposite in=\"color2\" in2=\"color2\" operator=\"arithmetic\" k2=\"%s\" result=\"composite2\" />\n" + "<feGaussianBlur in=\"composite2\" stdDeviation=\"%s 0.01\" result=\"blur1\" />\n" + "<feGaussianBlur in=\"composite2\" stdDeviation=\"0.01 %s\" result=\"blur2\" />\n" + "<feComposite in=\"blur2\" in2=\"blur1\" k3=\"1\" k2=\"1\" operator=\"arithmetic\" result=\"composite3\" />\n" + "<feFlood flood-color=\"rgb(255,255,255)\" flood-opacity=\"1\" result=\"flood\" />\n" + "<feBlend in=\"flood\" in2=\"composite3\" mode=\"multiply\" result=\"blend\" />\n" + "<feComposite in=\"%s\" in2=\"SourceGraphic\" operator=\"in\" result=\"composite4\" />\n" + "</filter>\n", clean.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), strength.str().c_str(), length.str().c_str(), length.str().c_str(), trans.str().c_str()); + // clang-format on + + return _filter; +}; /* CrossEngraving filter */ + +/** + \brief Custom predefined Drawing filter. + + Convert images to duochrome drawings. + + Filter's parameters: + * Simplification strength (0.01->20, default 0.6) -> blur1 (stdDeviation) + * Clean-up (1->500, default 10) -> convolve1 (kernelMatrix, central value -1001->-1500, default -1010) + * Erase (0.->6., default 0) -> composite1 (k4) + * Smoothness strength (0.01->20, default 0.6) -> blur2 (stdDeviation) + * Dilatation (1.->50., default 6) -> color2 (n-1th value) + * Erosion (0.->50., default 2) -> color2 (nth value 0->-50) + * translucent (boolean, default false) -> composite 8 (in, true->merge1, false->color5) + + * Blur strength (0.01->20., default 1.) -> blur3 (stdDeviation) + * Blur dilatation (1.->50., default 6) -> color4 (n-1th value) + * Blur erosion (0.->50., default 2) -> color4 (nth value 0->-50) + + * Stroke color (guint, default 64,64,64,255) -> flood2 (flood-color), composite3 (k2) + * Image on stroke (boolean, default false) -> composite2 (in="flood2" true-> in="SourceGraphic") + * Offset (-100->100, default 0) -> offset (val) + + * Fill color (guint, default 200,200,200,255) -> flood3 (flood-opacity), composite5 (k2) + * Image on fill (boolean, default false) -> composite4 (in="flood3" true-> in="SourceGraphic") + +*/ + +class Drawing : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Drawing ( ) : Filter() { }; + ~Drawing ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Drawing") "</name>\n" + "<id>org.inkscape.effect.filter.Drawing</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"Options\">\n" + "<label appearance=\"header\">" N_("Simplify") "</label>\n" + "<param name=\"simply\" gui-text=\"" N_("Strength") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">0.6</param>\n" + "<param name=\"clean\" gui-text=\"" N_("Clean-up") "\" type=\"int\" indent=\"1\" appearance=\"full\" min=\"1\" max=\"500\">10</param>\n" + "<param name=\"erase\" gui-text=\"" N_("Erase") "\" type=\"float\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"60\">0</param>\n" + "<param name=\"translucent\" gui-text=\"" N_("Translucent") "\" indent=\"1\" type=\"bool\" >false</param>\n" + "<label appearance=\"header\">" N_("Smoothness") "</label>\n" + "<param name=\"smooth\" gui-text=\"" N_("Strength") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">0.6</param>\n" + "<param name=\"dilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" indent=\"1\" appearance=\"full\" min=\"1\" max=\"50\">6</param>\n" + "<param name=\"erosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"50\">2</param>\n" + "<label appearance=\"header\">" N_("Melt") "</label>\n" + "<param name=\"blur\" gui-text=\"" N_("Level") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">1</param>\n" + "<param name=\"bdilat\" gui-text=\"" N_("Dilatation") "\" type=\"float\" indent=\"1\" appearance=\"full\" min=\"1\" max=\"50\">6</param>\n" + "<param name=\"berosion\" gui-text=\"" N_("Erosion") "\" type=\"float\" indent=\"1\" appearance=\"full\" min=\"0\" max=\"50\">2</param>\n" + "</page>\n" + "<page name=\"co11tab\" gui-text=\"Fill color\">\n" + "<param name=\"fcolor\" gui-text=\"" N_("Fill color") "\" type=\"color\">-1515870721</param>\n" + "<param name=\"iof\" gui-text=\"" N_("Image on fill") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "<page name=\"co12tab\" gui-text=\"Stroke color\">\n" + "<param name=\"scolor\" gui-text=\"" N_("Stroke color") "\" type=\"color\">589505535</param>\n" + "<param name=\"ios\" gui-text=\"" N_("Image on stroke") "\" type=\"bool\" >false</param>\n" + "<param name=\"offset\" gui-text=\"" N_("Offset") "\" type=\"int\" appearance=\"full\" min=\"-100\" max=\"100\">0</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Convert images to duochrome drawings") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Drawing()); + // clang-format on + }; +}; + +gchar const * +Drawing::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream simply; + std::ostringstream clean; + std::ostringstream erase; + std::ostringstream smooth; + std::ostringstream dilat; + std::ostringstream erosion; + std::ostringstream translucent; + std::ostringstream offset; + std::ostringstream blur; + std::ostringstream bdilat; + std::ostringstream berosion; + std::ostringstream strokea; + std::ostringstream stroker; + std::ostringstream strokeg; + std::ostringstream strokeb; + std::ostringstream ios; + std::ostringstream filla; + std::ostringstream fillr; + std::ostringstream fillg; + std::ostringstream fillb; + std::ostringstream iof; + + simply << ext->get_param_float("simply"); + clean << (-1000 - ext->get_param_int("clean")); + erase << (ext->get_param_float("erase") / 10); + smooth << ext->get_param_float("smooth"); + dilat << ext->get_param_float("dilat"); + erosion << (- ext->get_param_float("erosion")); + if (ext->get_param_bool("translucent")) + translucent << "merge1"; + else + translucent << "color5"; + offset << ext->get_param_int("offset"); + + blur << ext->get_param_float("blur"); + bdilat << ext->get_param_float("bdilat"); + berosion << (- ext->get_param_float("berosion")); + + guint32 fcolor = ext->get_param_color("fcolor"); + fillr << ((fcolor >> 24) & 0xff); + fillg << ((fcolor >> 16) & 0xff); + fillb << ((fcolor >> 8) & 0xff); + filla << (fcolor & 0xff) / 255.0F; + if (ext->get_param_bool("iof")) + iof << "SourceGraphic"; + else + iof << "flood3"; + + guint32 scolor = ext->get_param_color("scolor"); + stroker << ((scolor >> 24) & 0xff); + strokeg << ((scolor >> 16) & 0xff); + strokeb << ((scolor >> 8) & 0xff); + strokea << (scolor & 0xff) / 255.0F; + if (ext->get_param_bool("ios")) + ios << "SourceGraphic"; + else + ios << "flood2"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Drawing\">\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feConvolveMatrix in=\"blur1\" targetX=\"1\" targetY=\"1\" order=\"3 3\" kernelMatrix=\"0 250 0 250 %s 250 0 250 0 \" result=\"convolve1\" />\n" + "<feComposite in=\"convolve1\" in2=\"convolve1\" k1=\"1\" k2=\"1\" k4=\"%s\" operator=\"arithmetic\" result=\"composite1\" />\n" + "<feColorMatrix in=\"composite1\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"color1\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"color2\" />\n" + "<feFlood flood-color=\"rgb(255,255,255)\" result=\"flood1\" />\n" + "<feBlend in2=\"color2\" mode=\"multiply\" result=\"blend1\" />\n" + "<feComponentTransfer in=\"blend1\" result=\"component1\">\n" + "<feFuncR type=\"discrete\" tableValues=\"0 1 1 1\" />\n" + "<feFuncG type=\"discrete\" tableValues=\"0 1 1 1\" />\n" + "<feFuncB type=\"discrete\" tableValues=\"0 1 1 1\" />\n" + "</feComponentTransfer>\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur3\" />\n" + "<feColorMatrix in=\"blur3\" values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.2125 -0.7154 -0.0721 1 0 \" result=\"color3\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s %s \" result=\"color4\" />\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" result=\"flood2\" />\n" + "<feComposite in=\"%s\" in2=\"color4\" operator=\"in\" result=\"composite2\" />\n" + "<feComposite in=\"composite2\" in2=\"composite2\" operator=\"arithmetic\" k2=\"%s\" result=\"composite3\" />\n" + "<feOffset dx=\"%s\" dy=\"%s\" result=\"offset1\" />\n" + "<feFlood in=\"color4\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood3\" />\n" + "<feComposite in=\"%s\" in2=\"color4\" operator=\"out\" result=\"composite4\" />\n" + "<feComposite in=\"composite4\" in2=\"composite4\" operator=\"arithmetic\" k2=\"%s\" result=\"composite5\" />\n" + "<feMerge result=\"merge1\">\n" + "<feMergeNode in=\"composite5\" />\n" + "<feMergeNode in=\"offset1\" />\n" + "</feMerge>\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1.3 0 \" result=\"color5\" flood-opacity=\"0.56\" />\n" + "<feComposite in=\"%s\" in2=\"SourceGraphic\" operator=\"in\" result=\"composite8\" />\n" + "</filter>\n", simply.str().c_str(), clean.str().c_str(), erase.str().c_str(), smooth.str().c_str(), dilat.str().c_str(), erosion.str().c_str(), blur.str().c_str(), bdilat.str().c_str(), berosion.str().c_str(), stroker.str().c_str(), strokeg.str().c_str(), strokeb.str().c_str(), ios.str().c_str(), strokea.str().c_str(), offset.str().c_str(), offset.str().c_str(), fillr.str().c_str(), fillg.str().c_str(), fillb.str().c_str(), iof.str().c_str(), filla.str().c_str(), translucent.str().c_str()); + // clang-format on + + return _filter; +}; /* Drawing filter */ + + +/** + \brief Custom predefined Electrize filter. + + Electro solarization effects. + + Filter's parameters: + * Simplify (0.01->10., default 2.) -> blur (stdDeviation) + * Effect type (enum: table or discrete, default "table") -> component (type) + * Level (0->10, default 3) -> component (tableValues) + * Inverted (boolean, default false) -> component (tableValues) +*/ +class Electrize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Electrize ( ) : Filter() { }; + ~Electrize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Electrize") "</name>\n" + "<id>org.inkscape.effect.filter.Electrize</id>\n" + "<param name=\"blur\" gui-text=\"" N_("Simplify") "\" type=\"float\" appearance=\"full\" min=\"0.01\" max=\"10.0\">2.0</param>\n" + "<param name=\"type\" gui-text=\"" N_("Effect type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"table\">" N_("Table") "</option>\n" + "<option value=\"discrete\">" N_("Discrete") "</option>\n" + "</param>\n" + "<param name=\"levels\" gui-text=\"" N_("Levels") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"10\">3</param>\n" + "<param name=\"invert\" gui-text=\"" N_("Inverted") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Electro solarization effects") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Electrize()); + // clang-format on + }; +}; + +gchar const * +Electrize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream type; + std::ostringstream values; + + blur << ext->get_param_float("blur"); + type << ext->get_param_optiongroup("type"); + + // TransfertComponent table values are calculated based on the effect level and inverted parameters. + int val = 0; + int levels = ext->get_param_int("levels") + 1; + if (ext->get_param_bool("invert")) { + val = 1; + } + values << val; + for ( int step = 1 ; step <= levels ; step++ ) { + if (val == 1) { + val = 0; + } + else { + val = 1; + } + values << " " << val; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Electrize\">\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur\" />\n" + "<feComponentTransfer in=\"blur\" result=\"component\" >\n" + "<feFuncR type=\"%s\" tableValues=\"%s\" />\n" + "<feFuncG type=\"%s\" tableValues=\"%s\" />\n" + "<feFuncB type=\"%s\" tableValues=\"%s\" />\n" + "</feComponentTransfer>\n" + "</filter>\n", blur.str().c_str(), type.str().c_str(), values.str().c_str(), type.str().c_str(), values.str().c_str(), type.str().c_str(), values.str().c_str()); + // clang-format on + + return _filter; +}; /* Electrize filter */ + +/** + \brief Custom predefined Neon draw filter. + + Posterize and draw smooth lines around color shapes + + Filter's parameters: + * Lines type (enum, default smooth) -> + smooth = component2 (type="table"), composite1 (in2="blur2") + hard = component2 (type="discrete"), composite1 (in2="component1") + * Simplify (0.01->20., default 3) -> blur1 (stdDeviation) + * Line width (0.01->20., default 3) -> blur2 (stdDeviation) + * Lightness (0.->10., default 1) -> composite1 (k2) + * Blend (enum [normal, multiply, screen], default normal) -> blend (mode) + * Dark mode (boolean, default false) -> composite2 (true: in2="component2") +*/ +class NeonDraw : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + NeonDraw ( ) : Filter() { }; + ~NeonDraw ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Neon Draw") "</name>\n" + "<id>org.inkscape.effect.filter.NeonDraw</id>\n" + "<param name=\"type\" gui-text=\"" N_("Line type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"table\">" N_("Smoothed") "</option>\n" + "<option value=\"discrete\">" N_("Contrasted") "</option>\n" + "</param>\n" + "<param name=\"simply\" gui-text=\"" N_("Simplify") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">3</param>\n" + "<param name=\"width\" gui-text=\"" N_("Line width") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">3</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"10.00\">1</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend mode:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">Normal</option>\n" + "<option value=\"multiply\">Multiply</option>\n" + "<option value=\"screen\">Screen</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Posterize and draw smooth lines around color shapes") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new NeonDraw()); + // clang-format on + }; +}; + +gchar const * +NeonDraw::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blend; + std::ostringstream simply; + std::ostringstream width; + std::ostringstream lightness; + std::ostringstream type; + + type << ext->get_param_optiongroup("type"); + blend << ext->get_param_optiongroup("blend"); + simply << ext->get_param_float("simply"); + width << ext->get_param_float("width"); + lightness << ext->get_param_float("lightness"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Neon Draw\">\n" + "<feBlend mode=\"%s\" result=\"blend\" />\n" + "<feGaussianBlur in=\"blend\" stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 50 0\" result=\"color1\" />\n" + "<feComponentTransfer result=\"component1\">\n" + "<feFuncR type=\"discrete\" tableValues=\"0 0.3 0.3 0.3 0.3 0.6 0.6 0.6 0.6 1 1\" />\n" + "<feFuncG type=\"discrete\" tableValues=\"0 0.3 0.3 0.3 0.3 0.6 0.6 0.6 0.6 1 1\" />\n" + "<feFuncB type=\"discrete\" tableValues=\"0 0.3 0.3 0.3 0.3 0.6 0.6 0.6 0.6 1 1\" />\n" + "</feComponentTransfer>\n" + "<feGaussianBlur in=\"component1\" stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 50 0\" result=\"color2\" />\n" + "<feComponentTransfer in=\"color2\" result=\"component2\">\n" + "<feFuncR type=\"%s\" tableValues=\"0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1\" />\n" + "<feFuncG type=\"%s\" tableValues=\"0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1\" />\n" + "<feFuncB type=\"%s\" tableValues=\"0 1 1 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1\" />\n" + "</feComponentTransfer>\n" + "<feComposite in=\"component2\" in2=\"blur2\" k3=\"%s\" operator=\"arithmetic\" k2=\"1\" result=\"composite1\" />\n" + "<feComposite in=\"composite1\" in2=\"SourceGraphic\" operator=\"in\" result=\"composite2\" />\n" + "</filter>\n", blend.str().c_str(), simply.str().c_str(), width.str().c_str(), type.str().c_str(), type.str().c_str(), type.str().c_str(), lightness.str().c_str()); + // clang-format on + + return _filter; +}; /* NeonDraw filter */ + +/** + \brief Custom predefined Point engraving filter. + + Convert image to a transparent point engraving + + Filter's parameters: + + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Horizontal frequency (0.001->1., default 1) -> turbulence (baseFrequency [/100]) + * Vertical frequency (0.001->1., default 1) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->1000, default 0) -> turbulence (seed) + * Noise reduction (-1000->-1500, default -1045) -> convolve (kernelMatrix, central value) + * Noise blend (enum, all blend options, default normal) -> blend (mode) + * Lightness (0.->10., default 2.5) -> composite1 (k1) + * Grain lightness (0.->10., default 1.3) -> composite1 (k2) + * Erase (0.00->1., default 0) -> composite1 (k4) + * Blur (0.01->2., default 0.5) -> blur (stdDeviation) + + * Drawing color (guint32, default rgb(255,255,255)) -> flood1 (flood-color, flood-opacity) + + * Background color (guint32, default rgb(99,89,46)) -> flood2 (flood-color, flood-opacity) +*/ + +class PointEngraving : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + PointEngraving ( ) : Filter() { }; + ~PointEngraving ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Point Engraving") "</name>\n" + "<id>org.inkscape.effect.filter.PointEngraving</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"" N_("Options") "\">\n" + "<param name=\"type\" gui-text=\"" N_("Turbulence type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"fractalNoise\">" N_("Fractal noise") "</option>\n" + "<option value=\"turbulence\">" N_("Turbulence") "</option>\n" + "</param>\n" + "<param name=\"hfreq\" gui-text=\"" N_("Horizontal frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.1\" max=\"100.00\">100</param>\n" + "<param name=\"vfreq\" gui-text=\"" N_("Vertical frequency") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.1\" max=\"100.00\">100</param>\n" + "<param name=\"complexity\" gui-text=\"" N_("Complexity") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">1</param>\n" + "<param name=\"variation\" gui-text=\"" N_("Variation") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"100\">0</param>\n" + "<param name=\"reduction\" gui-text=\"" N_("Noise reduction") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"500\">45</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Noise blend:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "</param>\n" + "<param name=\"lightness\" gui-text=\"" N_("Lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"10\">2.5</param>\n" + "<param name=\"grain\" gui-text=\"" N_("Grain lightness") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"10\">1.3</param>\n" + "<param name=\"erase\" gui-text=\"" N_("Erase") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0\" max=\"1\">0</param>\n" + "<param name=\"blur\" gui-text=\"" N_("Blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"2\">0.5</param>\n" + "</page>\n" + "<page name=\"fcolortab\" gui-text=\"" N_("Fill color") "\">\n" + "<param name=\"fcolor\" gui-text=\"" N_("Color") "\" type=\"color\">-1</param>\n" + "<param name=\"iof\" gui-text=\"" N_("Image on fill") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "<page name=\"pcolortab\" gui-text=\"" N_("Points color") "\">\n" + "<param name=\"pcolor\" gui-text=\"" N_("Color") "\" type=\"color\">1666789119</param>\n" + "<param name=\"iop\" gui-text=\"" N_("Image on points") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Convert image to a transparent point engraving") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new PointEngraving()); + // clang-format on + }; + +}; + +gchar const * +PointEngraving::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream hfreq; + std::ostringstream vfreq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream reduction; + std::ostringstream blend; + std::ostringstream lightness; + std::ostringstream grain; + std::ostringstream erase; + std::ostringstream blur; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream a; + std::ostringstream br; + std::ostringstream bg; + std::ostringstream bb; + std::ostringstream ba; + std::ostringstream iof; + std::ostringstream iop; + + type << ext->get_param_optiongroup("type"); + hfreq << ext->get_param_float("hfreq") / 100; + vfreq << ext->get_param_float("vfreq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + reduction << (-1000 - ext->get_param_int("reduction")); + blend << ext->get_param_optiongroup("blend"); + lightness << ext->get_param_float("lightness"); + grain << ext->get_param_float("grain"); + erase << ext->get_param_float("erase"); + blur << ext->get_param_float("blur"); + + guint32 fcolor = ext->get_param_color("fcolor"); + r << ((fcolor >> 24) & 0xff); + g << ((fcolor >> 16) & 0xff); + b << ((fcolor >> 8) & 0xff); + a << (fcolor & 0xff) / 255.0F; + + guint32 pcolor = ext->get_param_color("pcolor"); + br << ((pcolor >> 24) & 0xff); + bg << ((pcolor >> 16) & 0xff); + bb << ((pcolor >> 8) & 0xff); + ba << (pcolor & 0xff) / 255.0F; + + if (ext->get_param_bool("iof")) + iof << "SourceGraphic"; + else + iof << "flood2"; + + if (ext->get_param_bool("iop")) + iop << "SourceGraphic"; + else + iop << "flood1"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" inkscape:label=\"Point Engraving\" style=\"color-interpolation-filters:sRGB;\">\n" + "<feConvolveMatrix in=\"SourceGraphic\" kernelMatrix=\"0 250 0 250 %s 250 0 250 0\" order=\"3 3\" result=\"convolve\" />\n" + "<feBlend in=\"convolve\" in2=\"SourceGraphic\" mode=\"%s\" result=\"blend\" />\n" + "<feTurbulence type=\"%s\" baseFrequency=\"%s %s\" numOctaves=\"%s\" seed=\"%s\" result=\"turbulence\" />\n" + "<feColorMatrix in=\"blend\" type=\"luminanceToAlpha\" result=\"colormatrix1\" />\n" + "<feComposite in=\"turbulence\" in2=\"colormatrix1\" k1=\"%s\" k2=\"%s\" k4=\"%s\" operator=\"arithmetic\" result=\"composite1\" />\n" + "<feColorMatrix in=\"composite1\" values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 10 -9 \" result=\"colormatrix2\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur\" />\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" result=\"flood1\" />\n" + "<feComposite in=\"%s\" in2=\"blur\" operator=\"out\" result=\"composite2\" />\n" + "<feFlood flood-color=\"rgb(%s,%s,%s)\" flood-opacity=\"%s\" result=\"flood2\" />\n" + "<feComposite in=\"%s\" in2=\"blur\" operator=\"in\" result=\"composite3\" />\n" + "<feComposite in=\"composite3\" in2=\"composite2\" k2=\"%s\" k3=\"%s\" operator=\"arithmetic\" result=\"composite4\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" result=\"composite5\" />\n" + "</filter>\n", reduction.str().c_str(), blend.str().c_str(), + type.str().c_str(), hfreq.str().c_str(), vfreq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), + lightness.str().c_str(), grain.str().c_str(), erase.str().c_str(), blur.str().c_str(), + br.str().c_str(), bg.str().c_str(), bb.str().c_str(), ba.str().c_str(), iop.str().c_str(), + r.str().c_str(), g.str().c_str(), b.str().c_str(), a.str().c_str(), iof.str().c_str(), + a.str().c_str(), ba.str().c_str() ); + // clang-format on + + return _filter; +}; /* Point engraving filter */ + +/** + \brief Custom predefined Poster paint filter. + + Poster and painting effects. + + Filter's parameters: + * Effect type (enum, default "Normal") -> + Normal = feComponentTransfer + Dented = Normal + intermediate values + * Transfer type (enum, default "discrete") -> component (type) + * Levels (0->15, default 5) -> component (tableValues) + * Blend mode (enum, default "Lighten") -> blend (mode) + * Primary simplify (0.01->100., default 4.) -> blur1 (stdDeviation) + * Secondary simplify (0.01->100., default 0.5) -> blur2 (stdDeviation) + * Pre-saturation (0.->1., default 1.) -> color1 (values) + * Post-saturation (0.->1., default 1.) -> color2 (values) + * Simulate antialiasing (boolean, default false) -> blur3 (true->stdDeviation=0.5, false->stdDeviation=0.01) +*/ +class Posterize : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Posterize ( ) : Filter() { }; + ~Posterize ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Poster Paint") "</name>\n" + "<id>org.inkscape.effect.filter.Posterize</id>\n" + "<param name=\"type\" gui-text=\"" N_("Effect type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"normal\">Normal</option>\n" + "<option value=\"dented\">Dented</option>\n" + "</param>\n" + "<param name=\"table\" gui-text=\"" N_("Transfer type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"discrete\">" N_("Poster") "</option>\n" + "<option value=\"table\">" N_("Painting") "</option>\n" + "</param>\n" + "<param name=\"levels\" gui-text=\"" N_("Levels") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"15\">5</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend mode:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"lighten\">Lighten</option>\n" + "<option value=\"normal\">Normal</option>\n" + "<option value=\"darken\">Darken</option>\n" + "<option value=\"multiply\">Multiply</option>\n" + "<option value=\"screen\">Screen</option>\n" + "</param>\n" + "<param name=\"blur1\" gui-text=\"" N_("Simplify (primary)") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">4.0</param>\n" + "<param name=\"blur2\" gui-text=\"" N_("Simplify (secondary)") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">0.5</param>\n" + "<param name=\"presaturation\" gui-text=\"" N_("Pre-saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"1.00\">1.00</param>\n" + "<param name=\"postsaturation\" gui-text=\"" N_("Post-saturation") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.00\" max=\"1.00\">1.00</param>\n" + "<param name=\"antialiasing\" gui-text=\"" N_("Simulate antialiasing") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Poster and painting effects") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Posterize()); + // clang-format on + }; +}; + +gchar const * +Posterize::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream table; + std::ostringstream blendmode; + std::ostringstream blur1; + std::ostringstream blur2; + std::ostringstream presat; + std::ostringstream postsat; + std::ostringstream transf; + std::ostringstream antialias; + + table << ext->get_param_optiongroup("table"); + blendmode << ext->get_param_optiongroup("blend"); + blur1 << ext->get_param_float("blur1"); + blur2 << ext->get_param_float("blur2"); + presat << ext->get_param_float("presaturation"); + postsat << ext->get_param_float("postsaturation"); + + // TransfertComponent table values are calculated based on the poster type. + transf << "0"; + int levels = ext->get_param_int("levels") + 1; + const gchar *effecttype = ext->get_param_optiongroup("type"); + if (levels == 1) { + if ((g_ascii_strcasecmp("dented", effecttype) == 0)) { + transf << " 1 0 1"; + } else { + transf << " 1"; + } + } else { + for ( int step = 1 ; step <= levels ; step++ ) { + float val = (float) step / levels; + transf << " " << val; + if ((g_ascii_strcasecmp("dented", effecttype) == 0)) { + transf << " " << (val - ((float) 1 / (3 * levels))) << " " << (val + ((float) 1 / (2 * levels))); + } + } + } + transf << " 1"; + + if (ext->get_param_bool("antialiasing")) + antialias << "0.5"; + else + antialias << "0.01"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Poster Paint\">\n" + "<feComposite operator=\"arithmetic\" k2=\"1\" result=\"composite1\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feGaussianBlur in=\"composite1\" stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feBlend in2=\"blur1\" mode=\"%s\" result=\"blend\"/>\n" + "<feColorMatrix type=\"saturate\" values=\"%s\" result=\"color1\" />\n" + "<feComponentTransfer result=\"component\">\n" + "<feFuncR type=\"%s\" tableValues=\"%s\" />\n" + "<feFuncG type=\"%s\" tableValues=\"%s\" />\n" + "<feFuncB type=\"%s\" tableValues=\"%s\" />\n" + "</feComponentTransfer>\n" + "<feColorMatrix type=\"saturate\" values=\"%s\" result=\"color2\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur3\" />\n" + "<feComposite in2=\"SourceGraphic\" operator=\"in\" result=\"composite3\" />\n" + "</filter>\n", blur1.str().c_str(), blur2.str().c_str(), blendmode.str().c_str(), presat.str().c_str(), table.str().c_str(), transf.str().c_str(), table.str().c_str(), transf.str().c_str(), table.str().c_str(), transf.str().c_str(), postsat.str().c_str(), antialias.str().c_str()); + // clang-format on + + return _filter; +}; /* Posterize filter */ + +/** + \brief Custom predefined Posterize basic filter. + + Simple posterizing effect + + Filter's parameters: + * Levels (0->20, default 5) -> component1 (tableValues) + * Blur (0.01->20., default 4.) -> blur1 (stdDeviation) +*/ +class PosterizeBasic : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + PosterizeBasic ( ) : Filter() { }; + ~PosterizeBasic ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Posterize Basic") "</name>\n" + "<id>org.inkscape.effect.filter.PosterizeBasic</id>\n" + "<param name=\"levels\" gui-text=\"" N_("Levels") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"20\">5</param>\n" + "<param name=\"blur\" gui-text=\"" N_("Simplify") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"20.00\">4.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Image Paint and Draw") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Simple posterizing effect") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new PosterizeBasic()); + // clang-format on + }; +}; + +gchar const * +PosterizeBasic::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream transf; + + blur << ext->get_param_float("blur"); + + transf << "0"; + int levels = ext->get_param_int("levels") + 1; + for ( int step = 1 ; step <= levels ; step++ ) { + const float val = (float) step / levels; + transf << " " << val; + } + transf << " 1"; + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Posterize Basic\">\n" + "<feGaussianBlur stdDeviation=\"%s\" result=\"blur1\" />\n" + "<feComponentTransfer in=\"blur1\" result=\"component1\">\n" + "<feFuncR type=\"discrete\" tableValues=\"%s\" />\n" + "<feFuncG type=\"discrete\" tableValues=\"%s\" />\n" + "<feFuncB type=\"discrete\" tableValues=\"%s\" />\n" + "</feComponentTransfer>\n" + "<feComposite in=\"component1\" in2=\"SourceGraphic\" operator=\"in\" />\n" + "</filter>\n", blur.str().c_str(), transf.str().c_str(), transf.str().c_str(), transf.str().c_str()); + // clang-format on + + return _filter; +}; /* PosterizeBasic filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'PAINT' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PAINT_H__ */ diff --git a/src/extension/internal/filter/protrusions.h b/src/extension/internal/filter/protrusions.h new file mode 100644 index 0000000..ccf54b4 --- /dev/null +++ b/src/extension/internal/filter/protrusions.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ +/* Change the 'PROTRUSIONS' above to be your file name */ + +/* + * Copyright (C) 2008 Authors: + * Ted Gould <ted@gould.cx> + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Protrusion filters + * Snow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +/** + \brief Custom predefined Snow filter. + + Snow has fallen on object. +*/ +class Snow : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Snow ( ) : Filter() { }; + ~Snow ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + +public: + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Snow Crest") "</name>\n" + "<id>org.inkscape.effect.filter.snow</id>\n" + "<param name=\"drift\" gui-text=\"" N_("Drift Size") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"20.0\">3.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"Protrusions\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Snow has fallen on object") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Snow()); + // clang-format on + }; + +}; + +gchar const * +Snow::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream drift; + drift << ext->get_param_float("drift"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Snow\">\n" + "<feConvolveMatrix order=\"3 3\" kernelMatrix=\"1 1 1 0 0 0 -1 -1 -1\" preserveAlpha=\"false\" divisor=\"3\"/>\n" + "<feMorphology operator=\"dilate\" radius=\"1 %s\"/>\n" + "<feGaussianBlur stdDeviation=\"1.6270889487870621\" result=\"result0\"/>\n" + "<feColorMatrix values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 10 0\" result=\"result1\"/>\n" + "<feOffset dx=\"0\" dy=\"1\" result=\"result5\"/>\n" + "<feDiffuseLighting in=\"result0\" diffuseConstant=\"2.2613065326633168\" surfaceScale=\"1\">\n" + "<feDistantLight azimuth=\"225\" elevation=\"32\"/>\n" + "</feDiffuseLighting>\n" + "<feComposite in2=\"result1\" operator=\"in\" result=\"result2\"/>\n" + "<feColorMatrix values=\"0.4 0 0 0 0.6 0 0.4 0 0 0.6 0 0 0 0 1 0 0 0 1 0\" result=\"result4\"/>\n" + "<feComposite in2=\"result5\" in=\"result4\"/>\n" + "<feComposite in2=\"SourceGraphic\"/>\n" + "</filter>\n", drift.str().c_str()); + // clang-format on + + return _filter; +}; /* Snow filter */ + + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'PROTRUSIONS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_PROTRUSIONS_H__ */ diff --git a/src/extension/internal/filter/shadows.h b/src/extension/internal/filter/shadows.h new file mode 100644 index 0000000..2616b25 --- /dev/null +++ b/src/extension/internal/filter/shadows.h @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ +/* Change the 'SHADOWS' above to be your file name */ + +/* + * Copyright (C) 2013 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Shadow filters + * Drop shadow + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Drop shadow filter. + + Colorizable Drop shadow. + + Filter's parameters: + * Blur radius (0.->200., default 3) -> blur (stdDeviation) + * Horizontal offset (-50.->50., default 6.0) -> offset (dx) + * Vertical offset (-50.->50., default 6.0) -> offset (dy) + * Blur type (enum, default outer) -> + outer = composite1 (operator="in"), composite2 (operator="over", in1="SourceGraphic", in2="offset") + inner = composite1 (operator="out"), composite2 (operator="atop", in1="offset", in2="SourceGraphic") + innercut = composite1 (operator="in"), composite2 (operator="out", in1="offset", in2="SourceGraphic") + outercut = composite1 (operator="out"), composite2 (operator="in", in1="SourceGraphic", in2="offset") + shadow = composite1 (operator="out"), composite2 (operator="atop", in1="offset", in2="offset") + * Color (guint, default 0,0,0,127) -> flood (flood-opacity, flood-color) + * Use object's color (boolean, default false) -> composite1 (in1, in2) +*/ +class ColorizableDropShadow : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ColorizableDropShadow ( ) : Filter() { }; + ~ColorizableDropShadow ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Drop Shadow") "</name>\n" + "<id>org.inkscape.effect.filter.ColorDropShadow</id>\n" + "<param name=\"tab\" type=\"notebook\">\n" + "<page name=\"optionstab\" gui-text=\"" N_("Options") "\">\n" + "<param name=\"blur\" gui-text=\"" N_("Blur radius (px)") "\" type=\"float\" appearance=\"full\" min=\"0.0\" max=\"200.0\">3.0</param>\n" + "<param name=\"xoffset\" gui-text=\"" N_("Horizontal offset (px)") "\" type=\"float\" appearance=\"full\" min=\"-50.0\" max=\"50.0\">6.0</param>\n" + "<param name=\"yoffset\" gui-text=\"" N_("Vertical offset (px)") "\" type=\"float\" appearance=\"full\" min=\"-50.0\" max=\"50.0\">6.0</param>\n" + "<param name=\"type\" gui-text=\"" N_("Shadow type:") "\" type=\"optiongroup\" appearance=\"combo\" >\n" + "<option value=\"outer\">" N_("Outer") "</option>\n" + "<option value=\"inner\">" N_("Inner") "</option>\n" + "<option value=\"outercut\">" N_("Outer cutout") "</option>\n" + "<option value=\"innercut\">" N_("Inner cutout") "</option>\n" + "<option value=\"shadow\">" N_("Shadow only") "</option>\n" + "</param>\n" + "</page>\n" + "<page name=\"coltab\" gui-text=\"" N_("Blur color") "\">\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">127</param>\n" + "<param name=\"objcolor\" gui-text=\"" N_("Use object's color") "\" type=\"bool\" >false</param>\n" + "</page>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Shadows and Glows") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Colorizable Drop shadow") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ColorizableDropShadow()); + // clang-format on + }; + +}; + +gchar const * +ColorizableDropShadow::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream blur; + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream x; + std::ostringstream y; + std::ostringstream comp1in1; + std::ostringstream comp1in2; + std::ostringstream comp1op; + std::ostringstream comp2in1; + std::ostringstream comp2in2; + std::ostringstream comp2op; + + const gchar *type = ext->get_param_optiongroup("type"); + guint32 color = ext->get_param_color("color"); + + blur << ext->get_param_float("blur"); + x << ext->get_param_float("xoffset"); + y << ext->get_param_float("yoffset"); + a << (color & 0xff) / 255.0F; + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + + // Select object or user-defined color + if ((g_ascii_strcasecmp("innercut", type) == 0)) { + if (ext->get_param_bool("objcolor")) { + comp2in1 << "SourceGraphic"; + comp2in2 << "offset"; + } else { + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } + } else { + if (ext->get_param_bool("objcolor")) { + comp1in1 << "SourceGraphic"; + comp1in2 << "flood"; + } else { + comp1in1 << "flood"; + comp1in2 << "SourceGraphic"; + } + } + + // Shadow mode + if ((g_ascii_strcasecmp("outer", type) == 0)) { + comp1op << "in"; + comp2op << "over"; + comp2in1 << "SourceGraphic"; + comp2in2 << "offset"; + } else if ((g_ascii_strcasecmp("inner", type) == 0)) { + comp1op << "out"; + comp2op << "atop"; + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("outercut", type) == 0)) { + comp1op << "in"; + comp2op << "out"; + comp2in1 << "offset"; + comp2in2 << "SourceGraphic"; + } else if ((g_ascii_strcasecmp("innercut", type) == 0)){ + comp1op << "out"; + comp1in1 << "flood"; + comp1in2 << "SourceGraphic"; + comp2op << "in"; + } else { //shadow + comp1op << "in"; + comp2op << "atop"; + comp2in1 << "offset"; + comp2in2 << "offset"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Drop Shadow\">\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feComposite in=\"%s\" in2=\"%s\" operator=\"%s\" result=\"composite1\" />\n" + "<feGaussianBlur in=\"composite1\" stdDeviation=\"%s\" result=\"blur\" />\n" + "<feOffset dx=\"%s\" dy=\"%s\" result=\"offset\" />\n" + "<feComposite in=\"%s\" in2=\"%s\" operator=\"%s\" result=\"composite2\" />\n" + "</filter>\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), + comp1in1.str().c_str(), comp1in2.str().c_str(), comp1op.str().c_str(), + blur.str().c_str(), x.str().c_str(), y.str().c_str(), + comp2in1.str().c_str(), comp2in2.str().c_str(), comp2op.str().c_str()); + // clang-format on + + return _filter; +}; /* Drop shadow filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'SHADOWS' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_SHADOWS_H__ */ diff --git a/src/extension/internal/filter/textures.h b/src/extension/internal/filter/textures.h new file mode 100644 index 0000000..66066ae --- /dev/null +++ b/src/extension/internal/filter/textures.h @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ +/* Change the 'TEXTURES' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Protrusion filters + * Ink blot + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + + +/** + \brief Custom predefined Ink Blot filter. + + Inkblot on tissue or rough paper. + + Filter's parameters: + + * Turbulence type (enum, default fractalNoise else turbulence) -> turbulence (type) + * Frequency (0.001->1., default 0.04) -> turbulence (baseFrequency [/100]) + * Complexity (1->5, default 3) -> turbulence (numOctaves) + * Variation (0->100, default 0) -> turbulence (seed) + * Horizontal inlay (0.01->30., default 10) -> blur1 (stdDeviation x) + * Vertical inlay (0.01->30., default 10) -> blur1 (stdDeviation y) + * Displacement (0.->100., default 50) -> map (scale) + * Blend (0.01->30., default 5) -> blur2 (stdDeviation) + * Stroke (enum, default over) -> composite (operator) + * Arithmetic stroke options + * k1 (-10.->10., default 1.5) + * k2 (-10.->10., default -0.25) + * k3 (-10.->10., default 0.5) +*/ +class InkBlot : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + InkBlot ( ) : Filter() { }; + ~InkBlot ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + +public: + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Ink Blot") "</name>\n" + "<id>org.inkscape.effect.filter.InkBlot</id>\n" + "<param name=\"type\" gui-text=\"" N_("Turbulence type:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"fractalNoise\">Fractal noise</option>\n" + "<option value=\"turbulence\">Turbulence</option>\n" + "</param>\n" + "<param name=\"freq\" gui-text=\"" N_("Frequency:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"100.00\">4</param>\n" + "<param name=\"complexity\" gui-text=\"" N_("Complexity:") "\" type=\"int\" appearance=\"full\" min=\"1\" max=\"5\">3</param>\n" + "<param name=\"variation\" gui-text=\"" N_("Variation:") "\" type=\"int\" appearance=\"full\" min=\"0\" max=\"100\">0</param>\n" + "<param name=\"hblur\" gui-text=\"" N_("Horizontal inlay:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.00\">10</param>\n" + "<param name=\"vblur\" gui-text=\"" N_("Vertical inlay:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.00\">10</param>\n" + "<param name=\"displacement\" gui-text=\"" N_("Displacement:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"100.00\">50</param>\n" + "<param name=\"blend\" gui-text=\"" N_("Blend:") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"30.00\">5</param>\n" + "<param name=\"stroke\" gui-text=\"" N_("Stroke:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"over\">" N_("Wide") "</option>\n" + "<option value=\"atop\">" N_("Normal") "</option>\n" + "<option value=\"in\">" N_("Narrow") "</option>\n" + "<option value=\"xor\">" N_("Overlapping") "</option>\n" + "<option value=\"out\">" N_("External") "</option>\n" + "<option value=\"arithmetic\">" N_("Custom") "</option>\n" + "</param>\n" + "<label appearance=\"header\">" N_("Custom stroke options") "</label>\n" + "<param name=\"k1\" gui-text=\"" N_("k1:") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">1.5</param>\n" + "<param name=\"k2\" gui-text=\"" N_("k2:") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">-0.25</param>\n" + "<param name=\"k3\" gui-text=\"" N_("k3:") "\" type=\"float\" indent=\"1\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0.5</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"Textures\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Inkblot on tissue or rough paper") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new InkBlot()); + // clang-format on + }; + +}; + +gchar const * +InkBlot::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream type; + std::ostringstream freq; + std::ostringstream complexity; + std::ostringstream variation; + std::ostringstream hblur; + std::ostringstream vblur; + std::ostringstream displacement; + std::ostringstream blend; + std::ostringstream stroke; + std::ostringstream custom; + + type << ext->get_param_optiongroup("type"); + freq << ext->get_param_float("freq") / 100; + complexity << ext->get_param_int("complexity"); + variation << ext->get_param_int("variation"); + hblur << ext->get_param_float("hblur"); + vblur << ext->get_param_float("vblur"); + displacement << ext->get_param_float("displacement"); + blend << ext->get_param_float("blend"); + + const gchar *ope = ext->get_param_optiongroup("stroke"); + if (g_ascii_strcasecmp("arithmetic", ope) == 0) { + custom << "k1=\"" << ext->get_param_float("k1") << "\" k2=\"" << ext->get_param_float("k2") << "\" k3=\"" << ext->get_param_float("k3") << "\""; + } else { + custom << ""; + } + + stroke << ext->get_param_optiongroup("stroke"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" x=\"-0.15\" width=\"1.3\" y=\"-0.15\" height=\"1.3\" inkscape:label=\"Ink Blot\" >\n" + "<feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"%s %s\" result=\"blur1\" />\n" + "<feTurbulence type=\"%s\" baseFrequency=\"%s\" numOctaves=\"%s\" seed=\"%s\" result=\"turbulence\" />\n" + "<feDisplacementMap in=\"blur1\" in2=\"turbulence\" xChannelSelector=\"R\" yChannelSelector=\"G\" scale=\"%s\" result=\"map\" />\n" + "<feGaussianBlur in=\"map\" stdDeviation=\"%s\" result=\"blur2\" />\n" + "<feComposite in=\"blur2\" in2=\"map\" %s operator=\"%s\" result=\"composite\" />\n" + "</filter>\n", hblur.str().c_str(), vblur.str().c_str(), type.str().c_str(), + freq.str().c_str(), complexity.str().c_str(), variation.str().c_str(), + displacement.str().c_str(), blend.str().c_str(), + custom.str().c_str(), stroke.str().c_str() ); + // clang-format on + + return _filter; + +}; /* Ink Blot filter */ + + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'TEXTURES' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TEXTURES_H__ */ diff --git a/src/extension/internal/filter/transparency.h b/src/extension/internal/filter/transparency.h new file mode 100644 index 0000000..39cd240 --- /dev/null +++ b/src/extension/internal/filter/transparency.h @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ +/* Change the 'TRANSPARENCY' above to be your file name */ + +/* + * Copyright (C) 2011 Authors: + * Ivan Louette (filters) + * Nicolas Dufour (UI) <nicoduf@yahoo.fr> + * + * Fill and transparency filters + * Blend + * Channel transparency + * Light eraser + * Opacity + * Silhouette + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* ^^^ Change the copyright to be you and your e-mail address ^^^ */ + +#include "filter.h" + +#include "extension/internal/clear-n_.h" +#include "extension/system.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { +namespace Filter { + +/** + \brief Custom predefined Blend filter. + + Blend objects with background images or with themselves + + Filter's parameters: + * Source (enum [SourceGraphic,BackgroundImage], default BackgroundImage) -> blend (in2) + * Mode (enum, all blend modes, default Multiply) -> blend (mode) +*/ + +class Blend : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Blend ( ) : Filter() { }; + ~Blend ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Blend") "</name>\n" + "<id>org.inkscape.effect.filter.Blend</id>\n" + "<param name=\"source\" gui-text=\"" N_("Source:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"BackgroundImage\">" N_("Background") "</option>\n" + "<option value=\"SourceGraphic\">" N_("Image") "</option>\n" + "</param>\n" + "<param name=\"mode\" gui-text=\"" N_("Mode:") "\" type=\"optiongroup\" appearance=\"combo\">\n" + "<option value=\"multiply\">" N_("Multiply") "</option>\n" + "<option value=\"normal\">" N_("Normal") "</option>\n" + "<option value=\"screen\">" N_("Screen") "</option>\n" + "<option value=\"darken\">" N_("Darken") "</option>\n" + "<option value=\"lighten\">" N_("Lighten") "</option>\n" + "</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Fill and Transparency") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Blend objects with background images or with themselves") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Blend()); + // clang-format on + }; + +}; + +gchar const * +Blend::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream source; + std::ostringstream mode; + + source << ext->get_param_optiongroup("source"); + mode << ext->get_param_optiongroup("mode"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Blend\">\n" + "<feBlend in2=\"%s\" mode=\"%s\" result=\"blend\" />\n" + "</filter>\n", source.str().c_str(), mode.str().c_str() ); + // clang-format on + + return _filter; +}; /* Blend filter */ + +/** + \brief Custom predefined Channel transparency filter. + + Channel transparency filter. + + Filter's parameters: + * Saturation (0.->1., default 1.) -> colormatrix1 (values) + * Red (-10.->10., default -1.) -> colormatrix2 (values) + * Green (-10.->10., default 0.5) -> colormatrix2 (values) + * Blue (-10.->10., default 0.5) -> colormatrix2 (values) + * Alpha (-10.->10., default 1.) -> colormatrix2 (values) + * Flood colors (guint, default 16777215) -> flood (flood-opacity, flood-color) + * Inverted (boolean, default false) -> composite1 (operator, true='in', false='out') + + Matrix: + 1 0 0 0 0 + 0 1 0 0 0 + 0 0 1 0 0 + R G B A 0 +*/ +class ChannelTransparency : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + ChannelTransparency ( ) : Filter() { }; + ~ChannelTransparency ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Channel Transparency") "</name>\n" + "<id>org.inkscape.effect.filter.ChannelTransparency</id>\n" + "<param name=\"red\" gui-text=\"" N_("Red") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">-1</param>\n" + "<param name=\"green\" gui-text=\"" N_("Green") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0.5</param>\n" + "<param name=\"blue\" gui-text=\"" N_("Blue") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">0.5</param>\n" + "<param name=\"alpha\" gui-text=\"" N_("Alpha") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"-10.\" max=\"10.\">1</param>\n" + "<param name=\"invert\" gui-text=\"" N_("Inverted") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Fill and Transparency") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Replace RGB with transparency") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new ChannelTransparency()); + // clang-format on + }; +}; + +gchar const * +ChannelTransparency::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream red; + std::ostringstream green; + std::ostringstream blue; + std::ostringstream alpha; + std::ostringstream invert; + + red << ext->get_param_float("red"); + green << ext->get_param_float("green"); + blue << ext->get_param_float("blue"); + alpha << ext->get_param_float("alpha"); + + if (!ext->get_param_bool("invert")) { + invert << "in"; + } else { + invert << "xor"; + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" inkscape:label=\"Channel Transparency\" style=\"color-interpolation-filters:sRGB;\" >\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 %s %s %s %s 0 \" in=\"SourceGraphic\" result=\"colormatrix\" />\n" + "<feComposite in=\"colormatrix\" in2=\"SourceGraphic\" operator=\"%s\" result=\"composite1\" />\n" + "</filter>\n", red.str().c_str(), green.str().c_str(), blue.str().c_str(), alpha.str().c_str(), + invert.str().c_str()); + // clang-format on + + return _filter; +}; /* Channel transparency filter */ + +/** + \brief Custom predefined LightEraser filter. + + Make the lightest parts of the object progressively transparent. + + Filter's parameters: + * Expansion (0.->1000., default 50) -> colormatrix (4th value, multiplicator) + * Erosion (1.->1000., default 100) -> colormatrix (first 3 values, multiplicator) + * Global opacity (0.->1., default 1.) -> composite (k2) + * Inverted (boolean, default false) -> colormatrix (values, true: first 3 values positive, 4th negative) + +*/ +class LightEraser : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + LightEraser ( ) : Filter() { }; + ~LightEraser ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Light Eraser") "</name>\n" + "<id>org.inkscape.effect.filter.LightEraser</id>\n" + "<param name=\"expand\" gui-text=\"" N_("Expansion") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"1000\">50</param>\n" + "<param name=\"erode\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" min=\"1\" max=\"1000\">100</param>\n" + "<param name=\"opacity\" gui-text=\"" N_("Global opacity") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1.\">1</param>\n" + "<param name=\"invert\" gui-text=\"" N_("Inverted") "\" type=\"bool\">false</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Fill and Transparency") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Make the lightest parts of the object progressively transparent") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new LightEraser()); + // clang-format on + }; +}; + +gchar const * +LightEraser::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream expand; + std::ostringstream erode; + std::ostringstream opacity; + + opacity << ext->get_param_float("opacity"); + + if (ext->get_param_bool("invert")) { + expand << (ext->get_param_float("erode") * 0.2125) << " " + << (ext->get_param_float("erode") * 0.7154) << " " + << (ext->get_param_float("erode") * 0.0721); + erode << (-ext->get_param_float("expand")); + } else { + expand << (-ext->get_param_float("erode") * 0.2125) << " " + << (-ext->get_param_float("erode") * 0.7154) << " " + << (-ext->get_param_float("erode") * 0.0721); + erode << ext->get_param_float("expand"); + } + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" inkscape:label=\"Light Eraser\" style=\"color-interpolation-filters:sRGB;\" >\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 %s %s 0 \" result=\"colormatrix\" />\n" + "<feComposite in2=\"colormatrix\" operator=\"arithmetic\" k2=\"%s\" result=\"composite\" />\n" + "</filter>\n", expand.str().c_str(), erode.str().c_str(), opacity.str().c_str()); + // clang-format on + + return _filter; +}; /* Light Eraser filter */ + + +/** + \brief Custom predefined Opacity filter. + + Set opacity and strength of opacity boundaries. + + Filter's parameters: + * Expansion (0.->1000., default 5) -> colormatrix (last-1th value) + * Erosion (0.->1000., default 1) -> colormatrix (last value) + * Global opacity (0.->1., default 1.) -> composite (k2) + +*/ +class Opacity : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Opacity ( ) : Filter() { }; + ~Opacity ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Opacity") "</name>\n" + "<id>org.inkscape.effect.filter.Opacity</id>\n" + "<param name=\"expand\" gui-text=\"" N_("Expansion") "\" type=\"float\" appearance=\"full\" min=\"1\" max=\"1000\">5</param>\n" + "<param name=\"erode\" gui-text=\"" N_("Erosion") "\" type=\"float\" appearance=\"full\" min=\"0\" max=\"1000\">1</param>\n" + "<param name=\"opacity\" gui-text=\"" N_("Global opacity") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.\" max=\"1.\">1</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Fill and Transparency") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Set opacity and strength of opacity boundaries") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Opacity()); + // clang-format on + }; +}; + +gchar const * +Opacity::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream matrix; + std::ostringstream opacity; + + opacity << ext->get_param_float("opacity"); + + matrix << (ext->get_param_float("expand")) << " " + << (-ext->get_param_float("erode")); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" inkscape:label=\"Opacity\" style=\"color-interpolation-filters:sRGB;\" >\n" + "<feColorMatrix values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 %s \" result=\"colormatrix\" />\n" + "<feComposite in2=\"colormatrix\" operator=\"arithmetic\" k2=\"%s\" result=\"composite\" />\n" + "</filter>\n", matrix.str().c_str(), opacity.str().c_str()); + // clang-format on + + return _filter; +}; /* Opacity filter */ + +/** + \brief Custom predefined Silhouette filter. + + Repaint anything visible monochrome + + Filter's parameters: + * Blur (0.01->50., default 0.01) -> blur (stdDeviation) + * Cutout (boolean, default False) -> composite (false=in, true=out) + * Color (guint, default 0,0,0,255) -> flood (flood-color, flood-opacity) +*/ + +class Silhouette : public Inkscape::Extension::Internal::Filter::Filter { +protected: + gchar const * get_filter_text (Inkscape::Extension::Extension * ext) override; + +public: + Silhouette ( ) : Filter() { }; + ~Silhouette ( ) override { if (_filter != nullptr) g_free((void *)_filter); return; } + + static void init () { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Silhouette") "</name>\n" + "<id>org.inkscape.effect.filter.Silhouette</id>\n" + "<param name=\"blur\" gui-text=\"" N_("Blur") "\" type=\"float\" appearance=\"full\" precision=\"2\" min=\"0.01\" max=\"50.00\">0.01</param>\n" + "<param name=\"cutout\" gui-text=\"" N_("Cutout") "\" type=\"bool\">false</param>\n" + "<param name=\"color\" gui-text=\"" N_("Color") "\" type=\"color\">255</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Filters") "\">\n" + "<submenu name=\"" N_("Fill and Transparency") "\"/>\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Repaint anything visible monochrome") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Silhouette()); + // clang-format on + }; + +}; + +gchar const * +Silhouette::get_filter_text (Inkscape::Extension::Extension * ext) +{ + if (_filter != nullptr) g_free((void *)_filter); + + std::ostringstream a; + std::ostringstream r; + std::ostringstream g; + std::ostringstream b; + std::ostringstream cutout; + std::ostringstream blur; + + guint32 color = ext->get_param_color("color"); + r << ((color >> 24) & 0xff); + g << ((color >> 16) & 0xff); + b << ((color >> 8) & 0xff); + a << (color & 0xff) / 255.0F; + if (ext->get_param_bool("cutout")) + cutout << "out"; + else + cutout << "in"; + blur << ext->get_param_float("blur"); + + // clang-format off + _filter = g_strdup_printf( + "<filter xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" style=\"color-interpolation-filters:sRGB;\" inkscape:label=\"Silhouette\">\n" + "<feFlood flood-opacity=\"%s\" flood-color=\"rgb(%s,%s,%s)\" result=\"flood\" />\n" + "<feComposite in=\"flood\" in2=\"SourceGraphic\" operator=\"%s\" result=\"composite\" />\n" + "<feGaussianBlur stdDeviation=\"%s\" />\n" + "</filter>\n", a.str().c_str(), r.str().c_str(), g.str().c_str(), b.str().c_str(), cutout.str().c_str(), blur.str().c_str()); + // clang-format on + + return _filter; +}; /* Silhouette filter */ + +}; /* namespace Filter */ +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* Change the 'TRANSPARENCY' below to be your file name */ +#endif /* SEEN_INKSCAPE_EXTENSION_INTERNAL_FILTER_TRANSPARENCY_H__ */ diff --git a/src/extension/internal/gdkpixbuf-input.cpp b/src/extension/internal/gdkpixbuf-input.cpp new file mode 100644 index 0000000..7441879 --- /dev/null +++ b/src/extension/internal/gdkpixbuf-input.cpp @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <set> + +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gdkmm/pixbuf.h> +#include <gdkmm/pixbufformat.h> +#include <glib/gprintf.h> +#include <glibmm/i18n.h> + +#include "document.h" +#include "document-undo.h" +#include "gdkpixbuf-input.h" +#include "image-resolution.h" +#include "preferences.h" +#include "selection-chemistry.h" + +#include "display/cairo-utils.h" + +#include "extension/input.h" +#include "extension/system.h" + +#include "io/dir-util.h" + +#include "object/sp-image.h" +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { + +namespace Extension { +namespace Internal { + +SPDocument * +GdkpixbufInput::open(Inkscape::Extension::Input *mod, char const *uri) +{ + // Determine whether the image should be embedded + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool ask = prefs->getBool( "/dialogs/import/ask"); + bool forcexdpi = prefs->getBool( "/dialogs/import/forcexdpi"); + Glib::ustring link = prefs->getString("/dialogs/import/link"); + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + + // If we asked about import preferences, get values and update preferences. + if (ask) { + ask = !mod->get_param_bool("do_not_ask"); + forcexdpi = (strcmp(mod->get_param_optiongroup("dpi"), "from_default") == 0); + link = mod->get_param_optiongroup("link"); + scale = mod->get_param_optiongroup("scale"); + + prefs->setBool( "/dialogs/import/ask", ask ); + prefs->setBool( "/dialogs/import/forcexdpi", forcexdpi); + prefs->setString("/dialogs/import/link", link ); + prefs->setString("/dialogs/import/scale", scale ); + } + + bool embed = (link == "embed"); + + SPDocument *doc = nullptr; + std::unique_ptr<Inkscape::Pixbuf> pb(Inkscape::Pixbuf::create_from_file(uri)); + + // TODO: the pixbuf is created again from the base64-encoded attribute in SPImage. + // Find a way to create the pixbuf only once. + + if (pb) { + doc = SPDocument::createNewDoc(nullptr, TRUE, TRUE); + bool saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // no need to undo in this temporary document + + double width = pb->width(); + double height = pb->height(); + double defaultxdpi = prefs->getDouble("/dialogs/import/defaultxdpi/value", Inkscape::Util::Quantity::convert(1, "in", "px")); + + ImageResolution *ir = nullptr; + double xscale = 1; + double yscale = 1; + + + if (!ir && !forcexdpi) { + ir = new ImageResolution(uri); + } + if (ir && ir->ok()) { + xscale = 960.0 / round(10.*ir->x()); // round-off to 0.1 dpi + yscale = 960.0 / round(10.*ir->y()); + // prevent crash on image with too small dpi (bug 1479193) + if (ir->x() <= .05) + xscale = 960.0; + if (ir->y() <= .05) + yscale = 960.0; + } else { + xscale = 96.0 / defaultxdpi; + yscale = 96.0 / defaultxdpi; + } + + width *= xscale; + height *= yscale; + doc->setWidthAndHeight(Util::Quantity(width, "px"), Util::Quantity(height, "px")); + + delete ir; // deleting NULL is safe + + // Create image node + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *image_node = xml_doc->createElement("svg:image"); + image_node->setAttributeSvgDouble("width", width); + image_node->setAttributeSvgDouble("height", height); + + // Set default value as we honor "preserveAspectRatio". + image_node->setAttribute("preserveAspectRatio", "none"); + + // This is actually 'image-rendering'. + if( scale.compare( "auto" ) != 0 ) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + sp_repr_css_set(image_node, css, "style"); + sp_repr_css_attr_unref( css ); + } + + if (embed) { + sp_embed_image(image_node, pb.get()); + } else { + // convert filename to uri + gchar* _uri = g_filename_to_uri(uri, nullptr, nullptr); + if(_uri) { + image_node->setAttribute("xlink:href", _uri); + g_free(_uri); + } else { + image_node->setAttribute("xlink:href", uri); + } + } + + // Add it to the current layer + Inkscape::XML::Node *layer_node = xml_doc->createElement("svg:g"); + layer_node->setAttribute("inkscape:groupmode", "layer"); + layer_node->setAttribute("inkscape:label", "Image"); + doc->getRoot()->appendChildRepr(layer_node); + layer_node->appendChild(image_node); + Inkscape::GC::release(image_node); + Inkscape::GC::release(layer_node); + fit_canvas_to_drawing(doc); + + // Set viewBox if it doesn't exist + if (!doc->getRoot()->viewBox_set) { + // std::cerr << "Viewbox not set, setting" << std::endl; + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + // restore undo, as now this document may be shown to the user if a bitmap was opened + DocumentUndo::setUndoSensitive(doc, saved); + } else { + printf("GdkPixbuf loader failed\n"); + } + + return doc; +} + +#include "clear-n_.h" + +void +GdkpixbufInput::init() +{ + static std::vector< Gdk::PixbufFormat > formatlist = Gdk::Pixbuf::get_formats(); + for (auto i: formatlist) { + GdkPixbufFormat *pixformat = i.gobj(); + + gchar *name = gdk_pixbuf_format_get_name(pixformat); + gchar *description = gdk_pixbuf_format_get_description(pixformat); + gchar **extensions = gdk_pixbuf_format_get_extensions(pixformat); + gchar **mimetypes = gdk_pixbuf_format_get_mime_types(pixformat); + + for (int i = 0; extensions[i] != nullptr; i++) { + for (int j = 0; mimetypes[j] != nullptr; j++) { + + /* thanks but no thanks, we'll handle SVG extensions... */ + if (strcmp(extensions[i], "svg") == 0) { + continue; + } + if (strcmp(extensions[i], "svgz") == 0) { + continue; + } + if (strcmp(extensions[i], "svg.gz") == 0) { + continue; + } + gchar *caption = g_strdup_printf(_("%s bitmap image import"), name); + + // clang-format off + gchar *xmlString = g_strdup_printf( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>%s</name>\n" + "<id>org.inkscape.input.gdkpixbuf.%s</id>\n" + + "<param name='link' type='optiongroup' gui-text='" N_("Image Import Type:") "' gui-description='" N_("Embed results in stand-alone, larger SVG files. Link references a file outside this SVG document and all files must be moved together.") "' >\n" + "<option value='embed' >" N_("Embed") "</option>\n" + "<option value='link' >" N_("Link") "</option>\n" + "</param>\n" + + "<param name='dpi' type='optiongroup' gui-text='" N_("Image DPI:") "' gui-description='" N_("Take information from file or use default bitmap import resolution as defined in the preferences.") "' >\n" + "<option value='from_file' >" N_("From file") "</option>\n" + "<option value='from_default' >" N_("Default import resolution") "</option>\n" + "</param>\n" + + "<param name='scale' type='optiongroup' gui-text='" N_("Image Rendering Mode:") "' gui-description='" N_("When an image is upscaled, apply smoothing or keep blocky (pixelated). (Will not work in all browsers.)") "' >\n" + "<option value='auto' >" N_("None (auto)") "</option>\n" + "<option value='optimizeQuality' >" N_("Smooth (optimizeQuality)") "</option>\n" + "<option value='optimizeSpeed' >" N_("Blocky (optimizeSpeed)") "</option>\n" + "</param>\n" + + "<param name=\"do_not_ask\" gui-description='" N_("Hide the dialog next time and always apply the same actions.") "' gui-text=\"" N_("Don't ask again") "\" type=\"bool\" >false</param>\n" + "<input>\n" + "<extension>.%s</extension>\n" + "<mimetype>%s</mimetype>\n" + "<filetypename>%s (*.%s)</filetypename>\n" + "<filetypetooltip>%s</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", + caption, + extensions[i], + extensions[i], + mimetypes[j], + name, + extensions[i], + description + ); + // clang-format off + + Inkscape::Extension::build_from_mem(xmlString, new GdkpixbufInput()); + g_free(xmlString); + g_free(caption); + }} + + g_free(name); + g_free(description); + g_strfreev(mimetypes); + g_strfreev(extensions); + } +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/gdkpixbuf-input.h b/src/extension/internal/gdkpixbuf-input.h new file mode 100644 index 0000000..1980700 --- /dev/null +++ b/src/extension/internal/gdkpixbuf-input.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H +#define INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class GdkpixbufInput : Inkscape::Extension::Implementation::Implementation { +public: + SPDocument *open(Inkscape::Extension::Input *mod, + char const *uri) override; + static void init(); +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* INKSCAPE_EXTENSION_INTERNAL_GDKPIXBUF_INPUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/gimpgrad.cpp b/src/extension/internal/gimpgrad.cpp new file mode 100644 index 0000000..c22d498 --- /dev/null +++ b/src/extension/internal/gimpgrad.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Inkscape::Extension::Internal::GimpGrad implementation + */ + +/* + * Authors: + * Ted Gould <ted@gould.cx> + * Abhishek Sharma + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <color-rgba.h> +#include "io/sys.h" +#include "extension/system.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include "gimpgrad.h" +#include "streq.h" +#include "strneq.h" +#include "document.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocate anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool GimpGrad::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm loading!\n" << std::endl; + return TRUE; +} + +/** + \brief A function to remove what was allocated + \param module Unused + \return None +*/ +void GimpGrad::unload (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Nooo! I'm being unloaded!" << std::endl; + return; +} + +static void append_css_num(Glib::ustring &str, double const num) +{ + CSSOStringStream stream; + stream << num; + str += stream.str(); +} + +/** + \brief A function to turn a color into a gradient stop + \param in_color The color for the stop + \param location Where the stop is placed in the gradient + \return The text that is the stop. Full SVG containing the element. + + This function encapsulates all of the translation of the ColorRGBA + and the location into the gradient. It is really pretty simple except + that the ColorRGBA is in floats that are 0 to 1 and the SVG wants + hex values from 0 to 255 for color. Otherwise mostly this is just + turning the values into strings and returning it. +*/ +static Glib::ustring stop_svg(ColorRGBA const in_color, double const location) +{ + Glib::ustring ret("<stop stop-color=\""); + + char stop_color_css[16]; + sp_svg_write_color(stop_color_css, sizeof(stop_color_css), in_color.getIntValue()); + ret += stop_color_css; + ret += '"'; + + if (in_color[3] != 1) { + ret += " stop-opacity=\""; + append_css_num(ret, in_color[3]); + ret += '"'; + } + ret += " offset=\""; + append_css_num(ret, location); + ret += "\"/>\n"; + return ret; +} + +/** + \brief Actually open the gradient and turn it into an SPDocument + \param module The input module being used + \param filename The filename of the gradient to be opened + \return A Document with the gradient in it. + + GIMP gradients are pretty simple (atleast the newer format, this + function does not handle the old one yet). They start out with + the like "GIMP Gradient", then name it, and tell how many entries + there are. This function currently ignores the name and the number + of entries just reading until it fails. + + The other small piece of trickery here is that GIMP gradients define + a left position, right position and middle position. SVG gradients + have no middle position in them. In order to handle this case the + left and right colors are averaged in a linear manner and the middle + position is used for that color. + + That is another point, the GIMP gradients support many different types + of gradients -- linear being the most simple. This plugin assumes + that they are all linear. Most GIMP gradients are done this way, + but it is possible to encounter more complex ones -- which won't be + handled correctly. + + The one optimization that this plugin makes that if the right side + of the previous segment is the same color as the left side of the + current segment, then the second one is dropped. This is often + done in GIMP gradients and they are not necissary in SVG. + + What this function does is build up an SVG document with a single + linear gradient in it with all the stops of the colors in the GIMP + gradient that is passed in. This document is then turned into a + document using the \c sp_document_from_mem. That is then returned + to Inkscape. +*/ +SPDocument * +GimpGrad::open (Inkscape::Extension::Input */*module*/, gchar const *filename) +{ + Inkscape::IO::dump_fopen_call(filename, "I"); + FILE *gradient = Inkscape::IO::fopen_utf8name(filename, "r"); + if (gradient == nullptr) { + return nullptr; + } + + { + char tempstr[1024]; + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the read failed" << std::endl; + goto error; + } + if (!streq(tempstr, "GIMP Gradient\n")) { + // std::cout << "This doesn't appear to be a GIMP gradient" << std::endl; + goto error; + } + + /* Name field. */ + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the second read failed" << std::endl; + goto error; + } + if (!strneq(tempstr, "Name: ", 6)) { + goto error; + } + /* Handle very long names. (And also handle nul bytes gracefully: don't use strlen.) */ + while (memchr(tempstr, '\n', sizeof(tempstr) - 1) == nullptr) { + if (fgets(tempstr, sizeof(tempstr), gradient) == nullptr) { + goto error; + } + } + + /* n. segments */ + if (fgets(tempstr, 1024, gradient) == nullptr) { + // std::cout << "Seems that the third read failed" << std::endl; + goto error; + } + char *endptr = nullptr; + long const n_segs = strtol(tempstr, &endptr, 10); + if ((*endptr != '\n') + || n_segs < 1) { + /* SVG gradients are allowed to have zero stops (treated as `none'), but gimp 2.2 + * requires at least one segment (i.e. at least two stops) (see gimp_gradient_load in + * gimpgradient-load.c). We try to use the same error handling as gimp, so that + * .ggr files that work in one program work in both programs. */ + goto error; + } + + ColorRGBA prev_color(-1.0, -1.0, -1.0, -1.0); + Glib::ustring outsvg("<svg><defs><linearGradient>\n"); + long n_segs_found = 0; + double prev_right = 0.0; + while (fgets(tempstr, 1024, gradient) != nullptr) { + double dbls[3 + 4 + 4]; + gchar *p = tempstr; + for (double & dbl : dbls) { + gchar *end = nullptr; + double const xi = g_ascii_strtod(p, &end); + if (!end || end == p || !g_ascii_isspace(*end)) { + goto error; + } + if (xi < 0 || 1 < xi) { + goto error; + } + dbl = xi; + p = end + 1; + } + + double const left = dbls[0]; + if (left != prev_right) { + goto error; + } + double const middle = dbls[1]; + if (!(left <= middle)) { + goto error; + } + double const right = dbls[2]; + if (!(middle <= right)) { + goto error; + } + + ColorRGBA const leftcolor(dbls[3], dbls[4], dbls[5], dbls[6]); + ColorRGBA const rightcolor(dbls[7], dbls[8], dbls[9], dbls[10]); + g_assert(11 == G_N_ELEMENTS(dbls)); + + /* Interpolation enums: curve shape and colour space. */ + { + /* TODO: Currently we silently ignore type & color, assuming linear interpolation in + * sRGB space (or whatever the default in SVG is). See gimp/app/core/gimpgradient.c + * for how gimp uses these. We could use gimp functions to sample at a few points, and + * add some intermediate stops to convert to the linear/sRGB interpolation */ + int type; /* enum: linear, curved, sine, sphere increasing, sphere decreasing. */ + int color_interpolation; /* enum: rgb, hsv anticlockwise, hsv clockwise. */ + if (sscanf(p, "%8d %8d", &type, &color_interpolation) != 2) { + continue; + } + } + + if (prev_color != leftcolor) { + outsvg += stop_svg(leftcolor, left); + } + if (fabs(middle - .5 * (left + right)) > 1e-4) { + outsvg += stop_svg(leftcolor.average(rightcolor), middle); + } + outsvg += stop_svg(rightcolor, right); + + prev_color = rightcolor; + prev_right = right; + ++n_segs_found; + } + if (prev_right != 1.0) { + goto error; + } + + if (n_segs_found != n_segs) { + goto error; + } + + outsvg += "</linearGradient></defs></svg>"; + + // std::cout << "SVG Output: " << outsvg << std::endl; + + fclose(gradient); + + return SPDocument::createNewDocFromMem(outsvg.c_str(), outsvg.length(), TRUE); + } + +error: + fclose(gradient); + return nullptr; +} + +#include "clear-n_.h" + +void GimpGrad::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("GIMP Gradients") "</name>\n" + "<id>org.inkscape.input.gimpgrad</id>\n" + "<input>\n" + "<extension>.ggr</extension>\n" + "<mimetype>application/x-gimp-gradient</mimetype>\n" + "<filetypename>" N_("GIMP Gradient (*.ggr)") "</filetypename>\n" + "<filetypetooltip>" N_("Gradients used in GIMP") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>\n", new GimpGrad()); + // clang-format on + return; +} + +} } } /* namespace Internal; Extension; Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/gimpgrad.h b/src/extension/internal/gimpgrad.h new file mode 100644 index 0000000..8daadef --- /dev/null +++ b/src/extension/internal/gimpgrad.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +// TODO add include guard +#include <glibmm/ustring.h> + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Extension; + +namespace Internal { + +/** + * Implementation class of the GIMP gradient plugin. + * This mostly just creates a namespace for the GIMP gradient plugin today. + */ +class GimpGrad : public Inkscape::Extension::Implementation::Implementation +{ +public: + bool load(Inkscape::Extension::Extension *module) override; + void unload(Inkscape::Extension::Extension *module) override; + SPDocument *open(Inkscape::Extension::Input *module, gchar const *filename) override; + + static void init(); +}; + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/grid.cpp b/src/extension/internal/grid.cpp new file mode 100644 index 0000000..8c1ae0d --- /dev/null +++ b/src/extension/internal/grid.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file grid.cpp + + A plug-in to add a grid creation effect into Inkscape. +*/ +/* + * Copyright (C) 2004-2005 Ted Gould <ted@gould.cx> + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/spinbutton.h> + +#include "desktop.h" + +#include "document.h" +#include "layer-manager.h" +#include "selection.h" +#include "2geom/geom.h" + +#include "svg/path-string.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "util/units.h" + +#include "grid.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +Grid::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Grid, I'm loading!" << std::endl; + return TRUE; +} + +namespace { + +Glib::ustring build_lines(Geom::Rect bounding_area, + Geom::Point const &offset, Geom::Point const &spacing) +{ + Geom::Point point_offset(0.0, 0.0); + + SVG::PathString path_data; + + for ( int axis = Geom::X ; axis <= Geom::Y ; ++axis ) { + point_offset[axis] = offset[axis]; + + for (Geom::Point start_point = bounding_area.min(); + start_point[axis] + offset[axis] <= (bounding_area.max())[axis]; + start_point[axis] += spacing[axis]) { + Geom::Point end_point = start_point; + end_point[1-axis] = (bounding_area.max())[1-axis]; + + path_data.moveTo(start_point + point_offset) + .lineTo(end_point + point_offset); + } + } + // std::cout << "Path data:" << path_data.c_str() << std::endl; + return path_data; +} + +} // namespace + +/** + \brief This actually draws the grid. + \param module The effect that was called (unused) + \param document What should be edited. +*/ +void +Grid::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *view, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + auto desktop = dynamic_cast<SPDesktop *>(view); + Inkscape::Selection *selection = desktop->selection; + SPDocument *doc = desktop->doc(); + + Geom::Rect bounding_area = Geom::Rect(Geom::Point(0,0), Geom::Point(100,100)); + if (selection->isEmpty()) { + /* get page size */ + if (auto bounds = doc->preferredBounds()) { + bounding_area = *bounds; + } + } else { + if (auto bounds = selection->visualBounds()) { + bounding_area = *bounds; + } + Geom::Rect temprec = bounding_area * desktop->doc2dt(); + bounding_area = temprec; + } + + double scale = doc->getDocumentScale().inverse()[Geom::X]; + + bounding_area *= Geom::Scale(scale); + Geom::Point spacings( scale * module->get_param_float("xspacing"), + scale * module->get_param_float("yspacing") ); + gdouble line_width = scale * module->get_param_float("lineWidth"); + Geom::Point offsets( scale * module->get_param_float("xoffset"), + scale * module->get_param_float("yoffset") ); + + Glib::ustring path_data(""); + + path_data = build_lines(bounding_area, offsets, spacings); + Inkscape::XML::Document * xml_doc = doc->getReprDoc(); + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node * current_layer = desktop->layerManager().currentLayer()->getRepr(); + Inkscape::XML::Node * path = xml_doc->createElement("svg:path"); + + path->setAttribute("d", path_data); + + std::ostringstream stringstream; + stringstream << "fill:none;stroke:#000000;stroke-width:" << line_width << "px"; + path->setAttribute("style", stringstream.str()); + + current_layer->appendChild(path); + Inkscape::GC::release(path); +} + +/** \brief A class to make an adjustment that uses Extension params */ +class PrefAdjustment : public Gtk::Adjustment { + /** Extension that this relates to */ + Inkscape::Extension::Extension * _ext; + /** The string which represents the parameter */ + char * _pref; +public: + /** \brief Make the adjustment using an extension and the string + describing the parameter. */ + PrefAdjustment(Inkscape::Extension::Extension * ext, char * pref) : + Gtk::Adjustment(0.0, 0.0, 10.0, 0.1), _ext(ext), _pref(pref) { + this->set_value(_ext->get_param_float(_pref)); + this->signal_value_changed().connect(sigc::mem_fun(this, &PrefAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class PrefAdjustment */ + +/** \brief A function to respond to the value_changed signal from the + adjustment. + + This function just grabs the value from the adjustment and writes + it to the parameter. Very simple, but yet beautiful. +*/ +void +PrefAdjustment::val_changed () +{ + // std::cout << "Value Changed to: " << this->get_value() << std::endl; + _ext->set_param_float(_pref, this->get_value()); + return; +} + +/** \brief A function to get the preferences for the grid + \param module Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +Grid::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = nullptr; + if (!selected.empty()) { + first_select = selected.front()->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + +#include "clear-n_.h" + +void +Grid::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Grid") "</name>\n" + "<id>org.inkscape.effect.grid</id>\n" + "<param name=\"lineWidth\" gui-text=\"" N_("Line Width:") "\" type=\"float\">1.0</param>\n" + "<param name=\"xspacing\" gui-text=\"" N_("Horizontal Spacing:") "\" type=\"float\" min=\"0.1\" max=\"1000\">10.0</param>\n" + "<param name=\"yspacing\" gui-text=\"" N_("Vertical Spacing:") "\" type=\"float\" min=\"0.1\" max=\"1000\">10.0</param>\n" + "<param name=\"xoffset\" gui-text=\"" N_("Horizontal Offset:") "\" type=\"float\" min=\"0.0\" max=\"1000\">0.0</param>\n" + "<param name=\"yoffset\" gui-text=\"" N_("Vertical Offset:") "\" type=\"float\" min=\"0.0\" max=\"1000\">0.0</param>\n" + "<effect>\n" + "<object-type>all</object-type>\n" + "<effects-menu>\n" + "<submenu name=\"" N_("Render") "\">\n" + "<submenu name=\"" N_("Grids") "\" />\n" + "</submenu>\n" + "</effects-menu>\n" + "<menu-tip>" N_("Draw a path which is a grid") "</menu-tip>\n" + "</effect>\n" + "</inkscape-extension>\n", new Grid()); + // clang-format on + return; +} + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/grid.h b/src/extension/internal/grid.h new file mode 100644 index 0000000..16f4cd7 --- /dev/null +++ b/src/extension/internal/grid.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class Grid : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + + static void init (); +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/image-resolution.cpp b/src/extension/internal/image-resolution.cpp new file mode 100644 index 0000000..3ca596c --- /dev/null +++ b/src/extension/internal/image-resolution.cpp @@ -0,0 +1,447 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Daniel Wagenaar <daw@caltech.edu> + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "util/units.h" +#include "image-resolution.h" + +#define IR_TRY_PNG 1 +#include <png.h> + +#ifdef HAVE_EXIF +#include <math.h> +#include <libexif/exif-data.h> +#endif + +#define IR_TRY_EXIV 0 + +#ifdef HAVE_JPEG +#define IR_TRY_JFIF 1 +#include <jpeglib.h> +#include <csetjmp> +#endif + +#ifdef WITH_MAGICK +#include <Magick++.h> +#endif + +#define noIMAGE_RESOLUTION_DEBUG + +#ifdef IMAGE_RESOLUTION_DEBUG +# define debug(f, a...) { g_print("%s(%d) %s:", \ + __FILE__,__LINE__,__FUNCTION__); \ + g_print(f, ## a); \ + g_print("\n"); \ + } +#else +# define debug(f, a...) /* */ +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + +ImageResolution::ImageResolution(char const *fn) { + ok_ = false; + + readpng(fn); + if (!ok_) { + readexiv(fn); + } + if (!ok_) { + readjfif(fn); + } + if (!ok_) { + readexif(fn); + } + if (!ok_) { + readmagick(fn); + } +} + +bool ImageResolution::ok() const { + return ok_; +} + +double ImageResolution::x() const { + return x_; +} + +double ImageResolution::y() const { + return y_; +} + + + +#if IR_TRY_PNG + +static bool haspngheader(FILE *fp) { + unsigned char header[8]; + if ( fread(header, 1, 8, fp) != 8 ) { + return false; + } + + fseek(fp, 0, SEEK_SET); + + if (png_sig_cmp(header, 0, 8)) { + return false; + } + + return true; +} + +// Implementation using libpng +void ImageResolution::readpng(char const *fn) { + FILE *fp = fopen(fn, "rb"); + if (!fp) + return; + + if (!haspngheader(fp)) { + fclose(fp); + return; + } + + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) + return; + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fp); + return; + } + + png_init_io(png_ptr, fp); + png_read_info(png_ptr, info_ptr); + + png_uint_32 res_x, res_y; +#ifdef PNG_INCH_CONVERSIONS_SUPPORTED + debug("PNG_INCH_CONVERSIONS_SUPPORTED"); + res_x = png_get_x_pixels_per_inch(png_ptr, info_ptr); + res_y = png_get_y_pixels_per_inch(png_ptr, info_ptr); + if (res_x != 0 && res_y != 0) { + ok_ = true; + x_ = res_x * 1.0; // FIXME: implicit conversion of png_uint_32 to double ok? + y_ = res_y * 1.0; // FIXME: implicit conversion of png_uint_32 to double ok? + } +#else + debug("PNG_RESOLUTION_METER"); + int unit_type; + // FIXME: png_get_pHYs() fails to return expected values + // with clang (based on LLVM 3.2svn) from Xcode 4.6.3 (OS X 10.7.5) + png_get_pHYs(png_ptr, info_ptr, &res_x, &res_y, &unit_type); + + if (unit_type == PNG_RESOLUTION_METER) { + ok_ = true; + x_ = res_x * 2.54 / 100; + y_ = res_y * 2.54 / 100; + } +#endif + + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + fclose(fp); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} +#else + +// Dummy implementation +void ImageResolution::readpng(char const *) { +} + +#endif + +#if IR_TRY_EXIF + +static double exifDouble(ExifEntry *entry, ExifByteOrder byte_order) { + switch (entry->format) { + case EXIF_FORMAT_BYTE: { + return double(entry->data[0]); + } + case EXIF_FORMAT_SHORT: { + return double(exif_get_short(entry->data, byte_order)); + } + case EXIF_FORMAT_LONG: { + return double(exif_get_long(entry->data, byte_order)); + } + case EXIF_FORMAT_RATIONAL: { + ExifRational r = exif_get_rational(entry->data, byte_order); + return double(r.numerator) / double(r.denominator); + } + case EXIF_FORMAT_SBYTE: { + return double(*(signed char *)entry->data); + } + case EXIF_FORMAT_SSHORT: { + return double(exif_get_sshort(entry->data, byte_order)); + } + case EXIF_FORMAT_SLONG: { + return double(exif_get_slong(entry->data, byte_order)); + } + case EXIF_FORMAT_SRATIONAL: { + ExifSRational r = exif_get_srational(entry->data, byte_order); + return double(r.numerator) / double(r.denominator); + } + case EXIF_FORMAT_FLOAT: { + return double((reinterpret_cast<float *>(entry->data))[0]); + } + case EXIF_FORMAT_DOUBLE: { + return (reinterpret_cast<double *>(entry->data))[0]; + } + default: { + return nan(0); + } + } +} + +// Implementation using libexif +void ImageResolution::readexif(char const *fn) { + ExifData *ed; + ed = exif_data_new_from_file(fn); + if (!ed) + return; + + ExifByteOrder byte_order = exif_data_get_byte_order(ed); + + ExifEntry *xres = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_X_RESOLUTION); + ExifEntry *yres = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_Y_RESOLUTION); + ExifEntry *unit = exif_content_get_entry(ed->ifd[EXIF_IFD_0], EXIF_TAG_RESOLUTION_UNIT); + + if ( xres && yres ) { + x_ = exifDouble(xres, byte_order); + y_ = exifDouble(yres, byte_order); + if (unit) { + double u = exifDouble(unit, byte_order); + if ( u == 3 ) { + x_ *= 2.54; + y_ *= 2.54; + } + } + ok_ = true; + } + exif_data_free(ed); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readexif(char const *) { +} + +#endif + +#if IR_TRY_EXIV + +void ImageResolution::readexiv(char const *fn) { + Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(fn); + if (!image.get()) + return; + + image->readMetadata(); + Exiv2::ExifData &exifData = image->exifData(); + if (exifData.empty()) + return; + + Exiv2::ExifData::const_iterator end = exifData.end(); + bool havex = false; + bool havey = false; + bool haveunit = false; + int unit; + for (Exiv2::ExifData::const_iterator i = exifData.begin(); i != end; ++i) { + if (ok_) + break; + if ( i->tag() == 0x011a ) { + // X Resolution + x_ = i->toFloat(); + havex = true; + } else if ( i->tag() == 0x011b ) { + // Y Resolution + y_ = i->toFloat(); + havey = true; + } else if ( i->tag() == 0x0128 ) { + unit = i->toLong(); + } + ok_ = havex && havey && haveunit; + } + if (haveunit) { + if ( unit == 3 ) { + x_ *= 2.54; + y_ *= 2.54; + } + } + ok_ = havex && havey; + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readexiv(char const *) { +} + +#endif + +#if IR_TRY_JFIF + +static void irjfif_error_exit(j_common_ptr cinfo) { + longjmp(*(jmp_buf*)cinfo->client_data, 1); +} + +static void irjfif_emit_message(j_common_ptr, int) { +} + +static void irjfif_output_message(j_common_ptr) { +} + +static void irjfif_format_message(j_common_ptr, char *) { +} + +static void irjfif_reset(j_common_ptr) { +} + +void ImageResolution::readjfif(char const *fn) { + FILE *ifd = fopen(fn, "rb"); + if (!ifd) { + return; + } + + struct jpeg_decompress_struct cinfo; + jmp_buf jbuf; + struct jpeg_error_mgr jerr; + + if (setjmp(jbuf)) { + fclose(ifd); + jpeg_destroy_decompress(&cinfo); + return; + } + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jerr.error_exit = &irjfif_error_exit; + jerr.emit_message = &irjfif_emit_message; + jerr.output_message = &irjfif_output_message; + jerr.format_message = &irjfif_format_message; + jerr.reset_error_mgr = &irjfif_reset; + cinfo.client_data = (void*)&jbuf; + + jpeg_stdio_src(&cinfo, ifd); + jpeg_read_header(&cinfo, TRUE); + + debug("cinfo.[XY]_density"); + if (cinfo.saw_JFIF_marker) { // JFIF APP0 marker was seen + if ( cinfo.density_unit == 1 ) { // dots/inch + x_ = cinfo.X_density; + y_ = cinfo.Y_density; + ok_ = true; + } else if ( cinfo.density_unit == 2 ) { // dots/cm + x_ = cinfo.X_density * 2.54; + y_ = cinfo.Y_density * 2.54; + ok_ = true; + } + /* According to http://www.jpeg.org/public/jfif.pdf (page 7): + * "Xdensity and Ydensity should always be non-zero". + * but in some cases, they are (see LP bug #1275443) */ + if (x_ == 0 or y_ == 0) { + ok_ = false; + } + } + jpeg_destroy_decompress(&cinfo); + fclose(ifd); + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readjfif(char const *) { +} + +#endif + +#ifdef WITH_MAGICK +void ImageResolution::readmagick(char const *fn) { + Magick::Image image; + debug("Trying image.read"); + try { + image.read(fn); + } catch (Magick::Error & err) { + debug("ImageMagick error: %s", err.what()); + return; + } catch (std::exception & err) { + debug("ImageResolution::readmagick: %s", err.what()); + return; + } + debug("image.[xy]Resolution"); + std::string const type = image.magick(); + x_ = image.xResolution(); + y_ = image.yResolution(); + +// TODO: find out why the hell the following conversion is necessary + if (type == "BMP") { + x_ = Inkscape::Util::Quantity::convert(x_, "in", "cm"); + y_ = Inkscape::Util::Quantity::convert(y_, "in", "cm"); + } + + if (x_ != 0 && y_ != 0) { + ok_ = true; + } + + if (ok_) { + debug("xdpi: %f", x_); + debug("ydpi: %f", y_); + } else { + debug("FAILED"); + debug("Using default Inkscape import resolution"); + } +} + +#else + +// Dummy implementation +void ImageResolution::readmagick(char const *) { +} + +#endif /* WITH_MAGICK */ + +} +} +} diff --git a/src/extension/internal/image-resolution.h b/src/extension/internal/image-resolution.h new file mode 100644 index 0000000..ad69df0 --- /dev/null +++ b/src/extension/internal/image-resolution.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Daniel Wagenaar <daw@caltech.edu> + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef IMAGE_RESOLUTION_H + +#define IMAGE_RESOLUTION_H + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class ImageResolution { +public: + ImageResolution(char const *fn); + bool ok() const; + double x() const; + double y() const; +private: + bool ok_; + double x_; + double y_; +private: + void readpng(char const *fn); + void readexif(char const *fn); + void readexiv(char const *fn); + void readjfif(char const *fn); + void readmagick(char const *fn); +}; + +} +} +} + +#endif diff --git a/src/extension/internal/latex-pstricks-out.cpp b/src/extension/internal/latex-pstricks-out.cpp new file mode 100644 index 0000000..824f382 --- /dev/null +++ b/src/extension/internal/latex-pstricks-out.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Michael Forbes <miforbes@mbhs.edu> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "latex-pstricks-out.h" +#include <print.h> +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "display/drawing.h" +#include "object/sp-root.h" + + +#include "document.h" + + +namespace Inkscape { +namespace Extension { +namespace Internal { + +LatexOutput::LatexOutput () // The null constructor +{ + return; +} + +LatexOutput::~LatexOutput () //The destructor +{ + return; +} + +bool LatexOutput::check(Inkscape::Extension::Extension * /*module*/) +{ + bool result = Inkscape::Extension::db.get("org.inkscape.print.latex") != nullptr; + return result; +} + + +void LatexOutput::save(Inkscape::Extension::Output * /*mod2*/, SPDocument *doc, gchar const *filename) +{ + SPPrintContext context; + doc->ensureUpToDate(); + + Inkscape::Extension::Print *mod = Inkscape::Extension::get_print(SP_MODULE_KEY_PRINT_LATEX); + const gchar * oldconst = mod->get_param_string("destination"); + gchar * oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + + // Start + context.module = mod; + // fixme: This has to go into module constructor somehow + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = (mod->base)->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + // Print document + mod->begin(doc); + (mod->base)->invoke_print(&context); + mod->finish(); + // Release things + (mod->base)->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // should have been deleted by invoke_hide + // end + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); +} + +#include "clear-n_.h" + +/** + \brief A function allocate a copy of this function. + + This is the definition of postscript out. This function just + calls the extension system with the memory allocated XML that + describes the data. +*/ +void +LatexOutput::init () +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("LaTeX Output") "</name>\n" + "<id>org.inkscape.output.latex</id>\n" + "<output>\n" + "<extension>.tex</extension>\n" + "<mimetype>text/x-tex</mimetype>\n" + "<filetypename>" N_("LaTeX With PSTricks macros (*.tex)") "</filetypename>\n" + "<filetypetooltip>" N_("LaTeX PSTricks File") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new LatexOutput()); + // clang-format on + + return; +} + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:cpp + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-pstricks-out.h b/src/extension/internal/latex-pstricks-out.h new file mode 100644 index 0000000..670904b --- /dev/null +++ b/src/extension/internal/latex-pstricks-out.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * Authors: + * Michael Forbes <miforbes-inkscape@mbhs.edu> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_LATEX_OUT_H +#define EXTENSION_INTERNAL_LATEX_OUT_H + +#include "extension/implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class LatexOutput : Inkscape::Extension::Implementation::Implementation { //This is a derived class + +public: + LatexOutput(); // Empty constructor + + ~LatexOutput() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + static void init();//Initialize the class +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* EXTENSION_INTERNAL_LATEX_OUT_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-pstricks.cpp b/src/extension/internal/latex-pstricks.cpp new file mode 100644 index 0000000..542a4bf --- /dev/null +++ b/src/extension/internal/latex-pstricks.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LaTeX Printing + * + * Author: + * Michael Forbes <miforbes@mbhs.edu> + * Abhishek Sharma + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/pathvector.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/curves.h> +#include <cerrno> +#include <csignal> +#include "util/units.h" +#include "helper/geom-curves.h" + +#include "extension/print.h" +#include "extension/system.h" +#include "inkscape-version.h" +#include "io/sys.h" +#include "latex-pstricks.h" +#include "style.h" +#include "document.h" +#include <cstring> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +PrintLatex::PrintLatex (): + _width(0), + _height(0), + _stream(nullptr) +{ +} + +PrintLatex::~PrintLatex () +{ + if (_stream) fclose(_stream); + + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + return; +} + +unsigned int PrintLatex::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + +unsigned int PrintLatex::begin (Inkscape::Extension::Print *mod, SPDocument *doc) +{ + Inkscape::SVGOStringStream os; + int res; + FILE *osf = nullptr; + const gchar * fn = nullptr; + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError* error = nullptr; + + os.setf(std::ios::fixed); + fn = mod->get_param_string("destination"); + gchar* local_fn = g_filename_from_utf8( fn, + -1, &bytesRead, &bytesWritten, &error); + fn = local_fn; + + /* TODO: Replace the below fprintf's with something that does the right thing whether in + * gui or batch mode (e.g. --print=blah). Consider throwing an exception: currently one of + * the callers (sp_print_document_to_file, "ret = mod->begin(doc)") wrongly ignores the + * return code. + */ + if (fn != nullptr) { + while (isspace(*fn)) fn += 1; + Inkscape::IO::dump_fopen_call(fn, "K"); + osf = Inkscape::IO::fopen_utf8name(fn, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", fn, strerror(errno)); + g_free(local_fn); + return 0; + } + _stream = osf; + } + + g_free(local_fn); + + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + + res = fprintf(_stream, "%%LaTeX with PSTricks extensions\n"); + /* flush this to test output stream as early as possible */ + if (fflush(_stream)) { + /*g_print("caught error in sp_module_print_plain_begin\n");*/ + if (ferror(_stream)) { + g_print("Error %d on output stream: %s\n", errno, + g_strerror(errno)); + } + g_print("Printing failed\n"); + /* fixme: should use pclose() for pipes */ + fclose(_stream); + _stream = nullptr; + fflush(stdout); + return 0; + } + + // width and height in pt + _width = doc->getWidth().value("pt"); + _height = doc->getHeight().value("pt"); + + if (res >= 0) { + + os << "%%Creator: Inkscape " << Inkscape::version_string << "\n"; + os << "%%Please note this file requires PSTricks extensions\n"; + + os << "\\psset{xunit=.5pt,yunit=.5pt,runit=.5pt}\n"; + // from now on we can output px, but they will be treated as pt + + os << "\\begin{pspicture}(" << doc->getWidth().value("px") << "," << doc->getHeight().value("px") << ")\n"; + } + + m_tr_stack.push( Geom::Scale(1, -1) * Geom::Translate(0, doc->getHeight().value("px"))); /// @fixme hardcoded doc2dt transform + + return fprintf(_stream, "%s", os.str().c_str()); +} + +unsigned int PrintLatex::finish(Inkscape::Extension::Print * /*mod*/) +{ + if (_stream) { + fprintf(_stream, "\\end{pspicture}\n"); + + // Flush stream to be sure. + fflush(_stream); + + fclose(_stream); + _stream = nullptr; + } + return 0; +} + +unsigned int PrintLatex::bind(Inkscape::Extension::Print * /*mod*/, Geom::Affine const &transform, float /*opacity*/) +{ + if (!m_tr_stack.empty()) { + Geom::Affine tr_top = m_tr_stack.top(); + m_tr_stack.push(transform * tr_top); + } else { + m_tr_stack.push(transform); + } + + return 1; +} + +unsigned int PrintLatex::release(Inkscape::Extension::Print * /*mod*/) +{ + m_tr_stack.pop(); + return 1; +} + +unsigned int PrintLatex::fill(Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const &transform, SPStyle const *style, + Geom::OptRect const & /*pbox*/, Geom::OptRect const & /*dbox*/, Geom::OptRect const & /*bbox*/) +{ + if (!_stream) { + return 0; // XXX: fixme, returning -1 as unsigned. + } + + if (style->fill.isColor()) { + Inkscape::SVGOStringStream os; + float rgb[3]; + float fill_opacity; + + os.setf(std::ios::fixed); + + fill_opacity=SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + style->fill.value.color.get_rgb_floatv(rgb); + os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; + os << "\\pscustom[linestyle=none,fillstyle=solid,fillcolor=curcolor"; + if (fill_opacity!=1.0) { + os << ",opacity="<<fill_opacity; + } + + os << "]\n{\n"; + + print_pathvector(os, pathv, transform); + + os << "}\n}\n"; + + fprintf(_stream, "%s", os.str().c_str()); + } + + return 0; +} + +unsigned int PrintLatex::stroke(Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const &transform, SPStyle const *style, + Geom::OptRect const & /*pbox*/, Geom::OptRect const & /*dbox*/, Geom::OptRect const & /*bbox*/) +{ + if (!_stream) { + return 0; // XXX: fixme, returning -1 as unsigned. + } + + if (style->stroke.isColor()) { + Inkscape::SVGOStringStream os; + float rgb[3]; + float stroke_opacity; + Geom::Affine tr_stack = m_tr_stack.top(); + double const scale = tr_stack.descrim(); + os.setf(std::ios::fixed); + + stroke_opacity=SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + style->stroke.value.color.get_rgb_floatv(rgb); + os << "{\n\\newrgbcolor{curcolor}{" << rgb[0] << " " << rgb[1] << " " << rgb[2] << "}\n"; + + os << "\\pscustom[linewidth=" << style->stroke_width.computed*scale<< ",linecolor=curcolor"; + + if (stroke_opacity!=1.0) { + os<<",strokeopacity="<<stroke_opacity; + } + + if (style->stroke_dasharray.set && !style->stroke_dasharray.values.empty()) { + os << ",linestyle=dashed,dash="; + for (unsigned i = 0; i < style->stroke_dasharray.values.size(); i++) { + if ((i)) { + os << " "; + } + os << style->stroke_dasharray.values[i].value; + } + } + + os <<"]\n{\n"; + + print_pathvector(os, pathv, transform); + + os << "}\n}\n"; + + fprintf(_stream, "%s", os.str().c_str()); + } + + return 0; +} + +// FIXME: why is 'transform' argument not used? +void +PrintLatex::print_pathvector(SVGOStringStream &os, Geom::PathVector const &pathv_in, const Geom::Affine & /*transform*/) +{ + if (pathv_in.empty()) + return; + +// Geom::Affine tf=transform; // why was this here? + Geom::Affine tf_stack=m_tr_stack.top(); // and why is transform argument not used? + Geom::PathVector pathv = pathv_in * tf_stack; // generates new path, which is a bit slow, but this doesn't have to be performance optimized + + os << "\\newpath\n"; + + for(const auto & it : pathv) { + + os << "\\moveto(" << it.initialPoint()[Geom::X] << "," << it.initialPoint()[Geom::Y] << ")\n"; + + for(Geom::Path::const_iterator cit = it.begin(); cit != it.end_open(); ++cit) { + print_2geomcurve(os, *cit); + } + + if (it.closed()) { + os << "\\closepath\n"; + } + + } +} + +void +PrintLatex::print_2geomcurve(SVGOStringStream &os, Geom::Curve const &c) +{ + using Geom::X; + using Geom::Y; + + if( is_straight_curve(c) ) + { + os << "\\lineto(" << c.finalPoint()[X] << "," << c.finalPoint()[Y] << ")\n"; + } + else if(Geom::CubicBezier const *cubic_bezier = dynamic_cast<Geom::CubicBezier const*>(&c)) { + std::vector<Geom::Point> points = cubic_bezier->controlPoints(); + os << "\\curveto(" << points[1][X] << "," << points[1][Y] << ")(" + << points[2][X] << "," << points[2][Y] << ")(" + << points[3][X] << "," << points[3][Y] << ")\n"; + } + else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + for(const auto & iter : sbasis_path) { + print_2geomcurve(os, iter); + } + } +} + +bool +PrintLatex::textToPath(Inkscape::Extension::Print * ext) +{ + return ext->get_param_bool("textToPath"); +} + +#include "clear-n_.h" + +void PrintLatex::init() +{ + /* SVG in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("LaTeX Print") "</name>\n" + "<id>" SP_MODULE_KEY_PRINT_LATEX "</id>\n" + "<param gui-hidden=\"true\" name=\"destination\" type=\"string\"></param>\n" + "<param gui-hidden=\"true\" name=\"textToPath\" type=\"bool\">true</param>\n" + "<print/>\n" + "</inkscape-extension>", new PrintLatex()); + // clang-format on +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : + diff --git a/src/extension/internal/latex-pstricks.h b/src/extension/internal/latex-pstricks.h new file mode 100644 index 0000000..edb1906 --- /dev/null +++ b/src/extension/internal/latex-pstricks.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX_H__ +#define __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX_H__ + +/* + * LaTeX Printing + * + * Author: + * Michael Forbes <miforbes@mbhs.edu> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <stack> + +#include "extension/implementation/implementation.h" +#include "extension/extension.h" + +#include "svg/stringstream.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintLatex : public Inkscape::Extension::Implementation::Implementation { + + float _width; + float _height; + FILE * _stream; + + std::stack<Geom::Affine> m_tr_stack; + + void print_pathvector(SVGOStringStream &os, Geom::PathVector const &pathv_in, const Geom::Affine & /*transform*/); + void print_2geomcurve(SVGOStringStream &os, Geom::Curve const & c ); + +public: + PrintLatex (); + ~PrintLatex () override; + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int bind(Inkscape::Extension::Print *module, Geom::Affine const &transform, float opacity) override; + unsigned int release(Inkscape::Extension::Print *module) override; + + unsigned int fill (Inkscape::Extension::Print *module, Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print *module, Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + bool textToPath (Inkscape::Extension::Print * ext) override; + + static void init (); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_LATEX */ + +/* + Local Variables: + mode:cpp + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/latex-text-renderer.cpp b/src/extension/internal/latex-text-renderer.cpp new file mode 100644 index 0000000..12fa66f --- /dev/null +++ b/src/extension/internal/latex-text-renderer.cpp @@ -0,0 +1,755 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering LaTeX file (pdf/eps/ps+latex output) + * + * The idea stems from GNUPlot's epslatex terminal output :-) + */ +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * Miklos Erdelyi <erdelyim@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "latex-text-renderer.h" + +#include <csignal> +#include <cerrno> + +#include <glibmm/i18n.h> +#include <glibmm/regex.h> + +#include "libnrtype/Layout-TNG.h" +#include <2geom/transforms.h> +#include <2geom/rect.h> + +#include "object/sp-item.h" +#include "object/sp-item-group.h" +#include "object/sp-root.h" +#include "object/sp-use.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/sp-rect.h" +#include "style.h" + +#include "text-editing.h" + +#include "util/units.h" + +#include "extension/output.h" +#include "extension/system.h" + +#include "inkscape-version.h" +#include "io/sys.h" +#include "document.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + * This method is called by the PDF, EPS and PS output extensions. + * @param filename This should be the filename without '_tex' extension to which the tex code should be written. Output goes to <filename>_tex, note the underscore instead of period. + */ +bool +latex_render_document_text_to_file( SPDocument *doc, gchar const *filename, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, double bleedmargin_px, + bool pdflatex) +{ + doc->ensureUpToDate(); + + SPRoot *root = doc->getRoot(); + SPItem *base = nullptr; + + bool pageBoundingBox = true; + if (exportId && strcmp(exportId, "")) { + // we want to export the given item only + base = dynamic_cast<SPItem *>(doc->getObjectById(exportId)); + if (!base) { + throw Inkscape::Extension::Output::export_id_not_found(exportId); + } + root->cropToObject(base); // TODO: This is inconsistent in CLI (should only happen for --export-id-only) + pageBoundingBox = exportCanvas; + } + else { + // we want to export the entire document from root + base = root; + pageBoundingBox = !exportDrawing; + } + + if (!base) + return false; + + /* Create renderer */ + LaTeXTextRenderer *renderer = new LaTeXTextRenderer(pdflatex); + + bool ret = renderer->setTargetFile(filename); + if (ret) { + /* Render document */ + bool ret = renderer->setupDocument(doc, pageBoundingBox, bleedmargin_px, base); + if (ret) { + renderer->renderItem(root); + } + } + + delete renderer; + + return ret; +} + +LaTeXTextRenderer::LaTeXTextRenderer(bool pdflatex) + : _stream(nullptr), + _filename(nullptr), + _pdflatex(pdflatex), + _omittext_state(EMPTY), + _omittext_page(1) +{ + push_transform(Geom::identity()); +} + +LaTeXTextRenderer::~LaTeXTextRenderer() +{ + if (_stream) { + writePostamble(); + + fclose(_stream); + } + + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + + if (_filename) { + g_free(_filename); + } + + return; +} + +/** This should create the output LaTeX file, and assign it to _stream. + * @return Returns true when successful + */ +bool +LaTeXTextRenderer::setTargetFile(gchar const *filename) { + if (filename != nullptr) { + while (isspace(*filename)) filename += 1; + + _filename = g_path_get_basename(filename); + + gchar *filename_ext = g_strdup_printf("%s_tex", filename); + Inkscape::IO::dump_fopen_call(filename_ext, "K"); + FILE *osf = Inkscape::IO::fopen_utf8name(filename_ext, "w+"); + if (!osf) { + fprintf(stderr, "inkscape: fopen(%s): %s\n", filename_ext, strerror(errno)); + g_free(filename_ext); + return false; + } + _stream = osf; + g_free(filename_ext); + } + + /* fixme: this is kinda icky */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_IGN); +#endif + + fprintf(_stream, "%%%% Creator: Inkscape %s, www.inkscape.org\n", Inkscape::version_string); + fprintf(_stream, "%%%% PDF/EPS/PS + LaTeX output extension by Johan Engelen, 2010\n"); + fprintf(_stream, "%%%% Accompanies image file '%s' (pdf, eps, ps)\n", _filename); + fprintf(_stream, "%%%%\n"); + /* flush this to test output stream as early as possible */ + if (fflush(_stream)) { + if (ferror(_stream)) { + g_print("Error %d on LaTeX file output stream: %s\n", errno, + g_strerror(errno)); + } + g_print("Output to LaTeX file failed\n"); + /* fixme: should use pclose() for pipes */ + fclose(_stream); + _stream = nullptr; + fflush(stdout); + return false; + } + + writePreamble(); + + return true; +} + +static char const preamble[] = +"%% To include the image in your LaTeX document, write\n" +"%% \\input{<filename>.pdf_tex}\n" +"%% instead of\n" +"%% \\includegraphics{<filename>.pdf}\n" +"%% To scale the image, write\n" +"%% \\def\\svgwidth{<desired width>}\n" +"%% \\input{<filename>.pdf_tex}\n" +"%% instead of\n" +"%% \\includegraphics[width=<desired width>]{<filename>.pdf}\n" +"%%\n" +"%% Images with a different path to the parent latex file can\n" +"%% be accessed with the `import' package (which may need to be\n" +"%% installed) using\n" +"%% \\usepackage{import}\n" +"%% in the preamble, and then including the image with\n" +"%% \\import{<path to file>}{<filename>.pdf_tex}\n" +"%% Alternatively, one can specify\n" +"%% \\graphicspath{{<path to file>/}}\n" +"%% \n" +"%% For more information, please see info/svg-inkscape on CTAN:\n" +"%% http://tug.ctan.org/tex-archive/info/svg-inkscape\n" +"%%\n" +"\\begingroup%\n" +" \\makeatletter%\n" +" \\providecommand\\color[2][]{%\n" +" \\errmessage{(Inkscape) Color is used for the text in Inkscape, but the package \'color.sty\' is not loaded}%\n" +" \\renewcommand\\color[2][]{}%\n" +" }%\n" +" \\providecommand\\transparent[1]{%\n" +" \\errmessage{(Inkscape) Transparency is used (non-zero) for the text in Inkscape, but the package \'transparent.sty\' is not loaded}%\n" +" \\renewcommand\\transparent[1]{}%\n" +" }%\n" +" \\providecommand\\rotatebox[2]{#2}%\n" +" \\newcommand*\\fsize{\\dimexpr\\f@size pt\\relax}%\n" +" \\newcommand*\\lineheight[1]{\\fontsize{\\fsize}{#1\\fsize}\\selectfont}%\n"; + +static char const postamble[] = +" \\end{picture}%\n" +"\\endgroup%\n"; + +void +LaTeXTextRenderer::writePreamble() +{ + fprintf(_stream, "%s", preamble); +} +void +LaTeXTextRenderer::writePostamble() +{ + fprintf(_stream, "%s", postamble); +} + +void LaTeXTextRenderer::sp_group_render(SPGroup *group) +{ + std::vector<SPObject*> l = (group->childList(false)); + for(auto x : l){ + SPItem *item = dynamic_cast<SPItem*>(x); + if (item) { + renderItem(item); + } + } +} + +void LaTeXTextRenderer::sp_use_render(SPUse *use) +{ + bool translated = false; + + if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { + Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); + push_transform(tp); + translated = true; + } + + SPItem *childItem = dynamic_cast<SPItem *>(use->child); + if (childItem) { + renderItem(childItem); + } + + if (translated) { + pop_transform(); + } +} + +void LaTeXTextRenderer::sp_text_render(SPText *textobj) +{ + // Nothing to do here... (so don't emit an empty box) + // Also avoids falling out of sync with the CairoRenderer (which won't render anything in this case either) + if (textobj->layout.getActualLength() == 0) + return; + + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + + SPStyle *style = textobj->style; + + // get position and alignment + // Align vertically on the baseline of the font (retrieved from the anchor point) + // Align horizontally on anchorpoint + gchar const *alignment = nullptr; + gchar const *aligntabular = nullptr; + switch (style->text_anchor.computed) { + case SP_CSS_TEXT_ANCHOR_START: + alignment = "[lt]"; + aligntabular = "{l}"; + break; + case SP_CSS_TEXT_ANCHOR_END: + alignment = "[rt]"; + aligntabular = "{r}"; + break; + case SP_CSS_TEXT_ANCHOR_MIDDLE: + default: + alignment = "[t]"; + aligntabular = "{c}"; + break; + } + + Geom::Point anchor; + const auto baseline_anchor_point = textobj->layout.baselineAnchorPoint(); + if (baseline_anchor_point) { + anchor = (*baseline_anchor_point) * transform(); + } else { + g_warning("LaTeXTextRenderer::sp_text_render: baselineAnchorPoint unset, text position will be wrong. Please report the issue."); + } + + // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) + bool has_color = false; // if the item has no color set, don't force black color + bool has_transparency = false; + // TODO: how to handle ICC colors? + // give priority to fill color + guint32 rgba = 0; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + if (style->fill.set && style->fill.isColor()) { + has_color = true; + rgba = style->fill.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + } else if (style->stroke.set && style->stroke.isColor()) { + has_color = true; + rgba = style->stroke.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + } + if (opacity < 1.0) { + has_transparency = true; + } + + // get rotation + Geom::Affine i2doc = textobj->i2doc_affine(); + Geom::Affine wotransl = i2doc.withoutTranslation(); + double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); + bool has_rotation = !Geom::are_near(degrees,0.); + + // get line-height + float line_height; + if (style->line_height.unit == SP_CSS_UNIT_NONE) { + // unitless 'line-height' (use as-is, computed value is relative value) + line_height = style->line_height.computed; + } else { + // 'line-height' with unit (make relative, computed value is absolute value) + line_height = style->line_height.computed / style->font_size.computed; + } + + // write to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // don't use scientific notation + + os << " \\put(" << anchor[Geom::X] << "," << anchor[Geom::Y] << "){"; + if (has_color) { + os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; + } + if (_pdflatex && has_transparency) { + os << "\\transparent{" << opacity << "}"; + } + if (has_rotation) { + os << "\\rotatebox{" << degrees << "}{"; + } + os << "\\makebox(0,0)" << alignment << "{"; + if (line_height != 1) { + os << "\\lineheight{" << line_height << "}"; + } + os << "\\smash{"; + os << "\\begin{tabular}[t]" << aligntabular; + + // Walk through all spans in the text object. + // Write span strings to LaTeX, associated with font weight and style. + Inkscape::Text::Layout const &layout = *(te_get_layout (textobj)); + for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); + li != le; li.nextStartOfSpan()) + { + Inkscape::Text::Layout::iterator ln = li; + ln.nextStartOfSpan(); + Glib::ustring uspanstr = sp_te_get_string_multiline (textobj, li, ln); + + // escape ampersands + uspanstr = Glib::Regex::create("&")->replace_literal(uspanstr, 0, "\\&", (Glib::RegexMatchFlags)0); + // escape percent + uspanstr = Glib::Regex::create("%")->replace_literal(uspanstr, 0, "\\%", (Glib::RegexMatchFlags)0); + + const gchar *spanstr = uspanstr.c_str(); + if (!spanstr) { + continue; + } + + bool is_bold = false, is_italic = false, is_oblique = false; + + // newline character only -> don't attempt to add style (will break compilation in LaTeX) + if (g_strcmp0(spanstr, "\n")) { + SPStyle const &spanstyle = *(sp_te_style_at_position (textobj, li)); + if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) + { + is_bold = true; + os << "\\textbf{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) + { + is_italic = true; + os << "\\textit{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) + { + is_oblique = true; + os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... + } + } + + // replace carriage return with double slash + gchar ** splitstr = g_strsplit(spanstr, "\n", 2); + os << splitstr[0]; + if (g_strv_length(splitstr) > 1) + { + os << "\\\\"; + } + g_strfreev(splitstr); + + if (is_oblique) { os << "}"; } // oblique end + if (is_italic) { os << "}"; } // italic end + if (is_bold) { os << "}"; } // bold end + } + + os << "\\end{tabular}"; // tabular end + os << "}"; // smash end + if (has_rotation) { os << "}"; } // rotatebox end + os << "}"; //makebox end + os << "}%\n"; // put end + + fprintf(_stream, "%s", os.str().c_str()); +} + +void LaTeXTextRenderer::sp_flowtext_render(SPFlowtext *flowtext) +{ +/* +Flowtext is possible by using a minipage! :) +Flowing in rectangle is possible, not in arb shape. +*/ + + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && _omittext_state == GRAPHIC_ON_TOP) + _omittext_state = NEW_PAGE_ON_GRAPHIC; + + SPStyle *style = flowtext->style; + + SPItem *frame_item = flowtext->get_frame(nullptr); + SPRect *frame = dynamic_cast<SPRect *>(frame_item); + if (!frame_item || !frame) { + g_warning("LaTeX export: non-rectangular flowed text shapes are not supported, skipping text."); + return; // don't know how to handle non-rect frames yet. is quite uncommon for latex users i think + } + + // We will transform the coordinates + Geom::Rect framebox = frame->getRect(); + + // get position and alignment + // Align on topleft corner. + gchar const *alignment = "[lt]"; + gchar const *justification = ""; + switch (flowtext->layout.paragraphAlignment(flowtext->layout.begin())) { + case Inkscape::Text::Layout::LEFT: + justification = "\\raggedright "; + break; + case Inkscape::Text::Layout::RIGHT: + justification = "\\raggedleft "; + break; + case Inkscape::Text::Layout::CENTER: + justification = "\\centering "; + case Inkscape::Text::Layout::FULL: + default: + // no need to add LaTeX code for standard justified output :) + break; + } + + // The topleft Corner was calculated after rotating the text which results in a wrong Coordinate. + // Now, the topleft Corner is rotated after calculating it + Geom::Point pos(framebox.corner(0) * transform()); //topleft corner + + // determine color and transparency (for now, use rgb color model as it is most native to Inkscape) + bool has_color = false; // if the item has no color set, don't force black color + bool has_transparency = false; + // TODO: how to handle ICC colors? + // give priority to fill color + guint32 rgba = 0; + float opacity = SP_SCALE24_TO_FLOAT(style->opacity.value); + if (style->fill.set && style->fill.isColor()) { + has_color = true; + rgba = style->fill.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + } else if (style->stroke.set && style->stroke.isColor()) { + has_color = true; + rgba = style->stroke.value.color.toRGBA32(1.); + opacity *= SP_SCALE24_TO_FLOAT(style->stroke_opacity.value); + } + if (opacity < 1.0) { + has_transparency = true; + } + + // get rotation + Geom::Affine i2doc = flowtext->i2doc_affine(); + Geom::Affine wotransl = i2doc.withoutTranslation(); + double degrees = -180/M_PI * Geom::atan2(wotransl.xAxis()); + bool has_rotation = !Geom::are_near(degrees,0.); + + // write to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // don't use scientific notation + + os << " \\put(" << pos[Geom::X] << "," << pos[Geom::Y] << "){"; + if (has_color) { + os << "\\color[rgb]{" << SP_RGBA32_R_F(rgba) << "," << SP_RGBA32_G_F(rgba) << "," << SP_RGBA32_B_F(rgba) << "}"; + } + if (_pdflatex && has_transparency) { + os << "\\transparent{" << opacity << "}"; + } + if (has_rotation) { + os << "\\rotatebox{" << degrees << "}{"; + } + os << "\\makebox(0,0)" << alignment << "{"; + + // Scale the x width correctly + os << "\\begin{minipage}{" << framebox.width() * transform().expansionX() << "\\unitlength}"; + os << justification; + + // Walk through all spans in the text object. + // Write span strings to LaTeX, associated with font weight and style. + Inkscape::Text::Layout const &layout = *(te_get_layout(flowtext)); + for (Inkscape::Text::Layout::iterator li = layout.begin(), le = layout.end(); + li != le; li.nextStartOfSpan()) + { + SPStyle const &spanstyle = *(sp_te_style_at_position(flowtext, li)); + bool is_bold = false, is_italic = false, is_oblique = false; + + if (spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_500 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_600 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_700 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_800 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_900 || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLD || + spanstyle.font_weight.computed == SP_CSS_FONT_WEIGHT_BOLDER) + { + is_bold = true; + os << "\\textbf{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_ITALIC) + { + is_italic = true; + os << "\\textit{"; + } + if (spanstyle.font_style.computed == SP_CSS_FONT_STYLE_OBLIQUE) + { + is_oblique = true; + os << "\\textsl{"; // this is an accurate choice if the LaTeX chosen font matches the font in Inkscape. Gives bad results when it is not so... + } + + Inkscape::Text::Layout::iterator ln = li; + ln.nextStartOfSpan(); + Glib::ustring uspanstr = sp_te_get_string_multiline(flowtext, li, ln); + const gchar *spanstr = uspanstr.c_str(); + if (!spanstr) { + continue; + } + // replace carriage return with double slash + gchar ** splitstr = g_strsplit(spanstr, "\n", -1); + gchar *spanstr_new = g_strjoinv("\\\\ ", splitstr); + os << spanstr_new; + g_strfreev(splitstr); + g_free(spanstr_new); + + if (is_oblique) { os << "}"; } // oblique end + if (is_italic) { os << "}"; } // italic end + if (is_bold) { os << "}"; } // bold end + } + + os << "\\end{minipage}"; + if (has_rotation) { + os << "}"; // rotatebox end + } + os << "}"; //makebox end + os << "}%\n"; // put end + + fprintf(_stream, "%s", os.str().c_str()); +} + +void LaTeXTextRenderer::sp_root_render(SPRoot *root) +{ + push_transform(root->c2p); + sp_group_render(root); + pop_transform(); +} + +void +LaTeXTextRenderer::sp_item_invoke_render(SPItem *item) +{ + // Check item's visibility + if (item->isHidden()) { + return; + } + + SPRoot *root = dynamic_cast<SPRoot *>(item); + if (root) { + return sp_root_render(root); + } + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + return sp_group_render(group); + } + SPUse *use = dynamic_cast<SPUse *>(item); + if (use) { + return sp_use_render(use); + } + SPText *text = dynamic_cast<SPText *>(item); + if (text) { + return sp_text_render(text); + } + SPFlowtext *flowtext = dynamic_cast<SPFlowtext *>(item); + if (flowtext) { + return sp_flowtext_render(flowtext); + } + // Only PDFLaTeX supports importing a single page of a graphics file, + // so only PDF backend gets interleaved text/graphics + if (_pdflatex && (_omittext_state == EMPTY || _omittext_state == NEW_PAGE_ON_GRAPHIC)) { + writeGraphicPage(); + } + _omittext_state = GRAPHIC_ON_TOP; +} + +void +LaTeXTextRenderer::renderItem(SPItem *item) +{ + push_transform(item->transform); + sp_item_invoke_render(item); + pop_transform(); +} + +void +LaTeXTextRenderer::writeGraphicPage() { + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // no scientific notation + + // strip pathname, as it is probably desired. Having a specific path in the TeX file is not convenient. + if (_pdflatex) + os << " \\put(0,0){\\includegraphics[width=\\unitlength,page=" << _omittext_page++ << "]{" << _filename << "}}%\n"; + else + os << " \\put(0,0){\\includegraphics[width=\\unitlength]{" << _filename << "}}%\n"; + + fprintf(_stream, "%s", os.str().c_str()); +} + +bool +LaTeXTextRenderer::setupDocument(SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base) +{ +// The boundingbox calculation here should be exactly the same as the one by CairoRenderer::setupDocument ! + + if (!base) { + base = doc->getRoot(); + } + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); + } else { + Geom::OptRect bbox = base->documentVisualBounds(); + if (!bbox) { + g_message("CairoRenderer: empty bounding box."); + return false; + } + d = *bbox; + } + d.expandBy(bleedmargin_px); + + // scale all coordinates, such that the width of the image is 1, this is convenient for scaling the image in LaTeX + double scale = 1/(d.width()); + double _width = d.width() * scale; + double _height = d.height() * scale; + push_transform(Geom::Translate(-d.corner(3)) * Geom::Scale(scale, -scale)); + + // write the info to LaTeX + Inkscape::SVGOStringStream os; + os.setf(std::ios::fixed); // no scientific notation + + // scaling of the image when including it in LaTeX + os << " \\ifx\\svgwidth\\undefined%\n"; + os << " \\setlength{\\unitlength}{" << Inkscape::Util::Quantity::convert(d.width(), "px", "pt") << "bp}%\n"; // note: 'bp' is the Postscript pt unit in LaTeX, see LP bug #792384 + os << " \\ifx\\svgscale\\undefined%\n"; + os << " \\relax%\n"; + os << " \\else%\n"; + os << " \\setlength{\\unitlength}{\\unitlength * \\real{\\svgscale}}%\n"; + os << " \\fi%\n"; + os << " \\else%\n"; + os << " \\setlength{\\unitlength}{\\svgwidth}%\n"; + os << " \\fi%\n"; + os << " \\global\\let\\svgwidth\\undefined%\n"; + os << " \\global\\let\\svgscale\\undefined%\n"; + os << " \\makeatother%\n"; + + os << " \\begin{picture}(" << _width << "," << _height << ")%\n"; + + // set \baselineskip equal to fontsize (the closest we can seem to get to CSS "line-height: 1;") + // and remove column spacing from tabular + os << " \\lineheight{1}%\n"; + os << " \\setlength\\tabcolsep{0pt}%\n"; + + fprintf(_stream, "%s", os.str().c_str()); + + if (!_pdflatex) + writeGraphicPage(); + + return true; +} + +Geom::Affine const & +LaTeXTextRenderer::transform() +{ + return _transform_stack.top(); +} + +void +LaTeXTextRenderer::push_transform(Geom::Affine const &tr) +{ + if(!_transform_stack.empty()){ + Geom::Affine tr_top = _transform_stack.top(); + _transform_stack.push(tr * tr_top); + } else { + _transform_stack.push(tr); + } +} + +void +LaTeXTextRenderer::pop_transform() +{ + _transform_stack.pop(); +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/latex-text-renderer.h b/src/extension/internal/latex-text-renderer.h new file mode 100644 index 0000000..0870f0c --- /dev/null +++ b/src/extension/internal/latex-text-renderer.h @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN +#define EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN + +/** \file + * Declaration of LaTeXTextRenderer, used for rendering the accompanying LaTeX file when exporting to PDF/EPS/PS + LaTeX + */ +/* + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2010 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "extension/extension.h" +#include <2geom/affine.h> +#include <stack> + +class SPItem; +class SPRoot; +class SPGroup; +class SPUse; +class SPText; +class SPFlowtext; + +namespace Inkscape { +namespace Extension { +namespace Internal { + +bool latex_render_document_text_to_file(SPDocument *doc, gchar const *filename, + const gchar * const exportId, bool exportDrawing, bool exportCanvas, double bleedmargin_px, + bool pdflatex); + +class LaTeXTextRenderer { +public: + LaTeXTextRenderer(bool pdflatex); + virtual ~LaTeXTextRenderer(); + + bool setTargetFile(gchar const *filename); + + /** Initializes the LaTeXTextRenderer according to the specified + SPDocument. Important to set the boundingbox to the pdf boundingbox */ + bool setupDocument(SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base); + + /** Traverses the object tree and invokes the render methods. */ + void renderItem(SPItem *item); + +protected: + enum LaTeXOmitTextPageState { + EMPTY, + GRAPHIC_ON_TOP, + NEW_PAGE_ON_GRAPHIC + }; + + FILE * _stream; + gchar * _filename; + + bool _pdflatex; /** true if outputting for pdfLaTeX*/ + + LaTeXOmitTextPageState _omittext_state; + gulong _omittext_page; + + void push_transform(Geom::Affine const &transform); + Geom::Affine const & transform(); + void pop_transform(); + std::stack<Geom::Affine> _transform_stack; + + void writePreamble(); + void writePostamble(); + + void writeGraphicPage(); + + void sp_item_invoke_render(SPItem *item); + void sp_root_render(SPRoot *item); + void sp_group_render(SPGroup *group); + void sp_use_render(SPUse *use); + void sp_text_render(SPText *text); + void sp_flowtext_render(SPFlowtext *flowtext); +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* !EXTENSION_INTERNAL_LATEX_TEXT_RENDERER_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-inout.cpp b/src/extension/internal/metafile-inout.cpp new file mode 100644 index 0000000..3c8d5dc --- /dev/null +++ b/src/extension/internal/metafile-inout.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile input - common routines + *//* + * Authors: + * David Mathog + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <fstream> +#include <glib.h> +#include <glibmm/miscutils.h> + +#include "display/curve.h" +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/print.h" +#include "path-prefix.h" +#include "document.h" +#include "util/units.h" +#include "ui/shape-editor.h" +#include "document-undo.h" +#include "inkscape.h" +#include "preferences.h" + +#include "object/sp-root.h" +#include "object/sp-namedview.h" +#include "svg/stringstream.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +Metafile::~Metafile() +{ + return; +} + +/** Construct a PNG in memory from an RGB from the EMF file + +from: +http://www.lemoda.net/c/write-png/ + +which was based on: +http://stackoverflow.com/questions/1821806/how-to-encode-png-to-buffer-using-libpng + +gcc -Wall -o testpng testpng.c -lpng + +Originally here, but moved up + +#include <png.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +*/ + + +/* Given "bitmap", this returns the pixel of bitmap at the point + ("x", "y"). */ + +pixel_t * Metafile::pixel_at (bitmap_t * bitmap, int x, int y) +{ + return bitmap->pixels + bitmap->width * y + x; +} + + +/* Write "bitmap" to a PNG file specified by "path"; returns 0 on + success, non-zero on error. */ + +void +Metafile::my_png_write_data(png_structp png_ptr, png_bytep data, png_size_t length) +{ + PMEMPNG p=(PMEMPNG)png_get_io_ptr(png_ptr); + + size_t nsize = p->size + length; + + /* allocate or grow buffer */ + if(p->buffer){ p->buffer = (char *) realloc(p->buffer, nsize); } + else{ p->buffer = (char *) malloc(nsize); } + + if(!p->buffer){ png_error(png_ptr, "Write Error"); } + + /* copy new bytes to end of buffer */ + memcpy(p->buffer + p->size, data, length); + p->size += length; +} + +void Metafile::toPNG(PMEMPNG accum, int width, int height, const char *px){ + bitmap_t bmStore; + bitmap_t *bitmap = &bmStore; + accum->buffer=nullptr; // PNG constructed in memory will end up here, caller must free(). + accum->size=0; + bitmap->pixels=(pixel_t *)px; + bitmap->width = width; + bitmap->height = height; + + png_structp png_ptr = nullptr; + png_infop info_ptr = nullptr; + size_t x, y; + png_byte ** row_pointers = nullptr; + /* The following number is set by trial and error only. I cannot + see where it it is documented in the libpng manual. + */ + int pixel_size = 3; + int depth = 8; + + png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (png_ptr == nullptr){ + accum->buffer=nullptr; + return; + } + + info_ptr = png_create_info_struct (png_ptr); + if (info_ptr == nullptr){ + png_destroy_write_struct (&png_ptr, &info_ptr); + accum->buffer=nullptr; + return; + } + + /* Set up error handling. */ + + if (setjmp (png_jmpbuf (png_ptr))) { + png_destroy_write_struct (&png_ptr, &info_ptr); + accum->buffer=nullptr; + return; + } + + /* Set image attributes. */ + + png_set_IHDR ( + png_ptr, + info_ptr, + bitmap->width, + bitmap->height, + depth, + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT + ); + + /* Initialize rows of PNG. */ + + row_pointers = (png_byte **) png_malloc (png_ptr, bitmap->height * sizeof (png_byte *)); + for (y = 0; y < bitmap->height; ++y) { + png_byte *row = + (png_byte *) png_malloc (png_ptr, sizeof (uint8_t) * bitmap->width * pixel_size); + row_pointers[bitmap->height - y - 1] = row; // Row order in EMF is reversed. + for (x = 0; x < bitmap->width; ++x) { + pixel_t * pixel = pixel_at (bitmap, x, y); + *row++ = pixel->red; // R & B channels were set correctly by DIB_to_RGB + *row++ = pixel->green; + *row++ = pixel->blue; + } + } + + /* Write the image data to memory */ + + png_set_rows (png_ptr, info_ptr, row_pointers); + + png_set_write_fn(png_ptr, accum, my_png_write_data, nullptr); + + png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr); + + for (y = 0; y < bitmap->height; y++) { + png_free (png_ptr, row_pointers[y]); + } + png_free (png_ptr, row_pointers); + png_destroy_write_struct(&png_ptr, &info_ptr); + +} + +/* If the viewBox is missing, set one +*/ +void Metafile::setViewBoxIfMissing(SPDocument *doc) { + + if (doc && !doc->getRoot()->viewBox_set) { + bool saved = Inkscape::DocumentUndo::getUndoSensitive(doc); + Inkscape::DocumentUndo::setUndoSensitive(doc, false); + + doc->ensureUpToDate(); + + // Set document unit + Inkscape::XML::Node *repr = doc->getNamedView()->getRepr(); + Inkscape::SVGOStringStream os; + Inkscape::Util::Unit const* doc_unit = doc->getWidth().unit; + os << doc_unit->abbr; + repr->setAttribute("inkscape:document-units", os.str()); + + // Set viewBox + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc_unit), doc->getHeight().value(doc_unit))); + doc->ensureUpToDate(); + + // Scale and translate objects + double scale = Inkscape::Util::Quantity::convert(1, "px", doc_unit); + Inkscape::UI::ShapeEditor::blockSetItem(true); + double dh; + if(SP_ACTIVE_DOCUMENT){ // for file menu open or import, or paste from clipboard + dh = SP_ACTIVE_DOCUMENT->getHeight().value("px"); + } + else { // for open via --file on command line + dh = doc->getHeight().value("px"); + } + + // These should not affect input, but they do, so set them to a neutral state + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs->getBool("/options/transform/stroke", true); + bool transform_rectcorners = prefs->getBool("/options/transform/rectcorners", true); + bool transform_pattern = prefs->getBool("/options/transform/pattern", true); + bool transform_gradient = prefs->getBool("/options/transform/gradient", true); + prefs->setBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/rectcorners", true); + prefs->setBool("/options/transform/pattern", true); + prefs->setBool("/options/transform/gradient", true); + + doc->getRoot()->scaleChildItemsRec(Geom::Scale(scale), Geom::Point(0, dh), true); + Inkscape::UI::ShapeEditor::blockSetItem(false); + + // restore options + prefs->setBool("/options/transform/stroke", transform_stroke); + prefs->setBool("/options/transform/rectcorners", transform_rectcorners); + prefs->setBool("/options/transform/pattern", transform_pattern); + prefs->setBool("/options/transform/gradient", transform_gradient); + + Inkscape::DocumentUndo::setUndoSensitive(doc, saved); + } +} + +/** + \fn Convert EMF/WMF region combining ops to livarot region combining ops + \return combination operators in livarot enumeration, or -1 on no match + \param ops (int) combination operator (Inkscape) +*/ +int Metafile::combine_ops_to_livarot(const int op) +{ + int ret = -1; + switch(op) { + case U_RGN_AND: + ret = bool_op_inters; + break; + case U_RGN_OR: + ret = bool_op_union; + break; + case U_RGN_XOR: + ret = bool_op_symdiff; + break; + case U_RGN_DIFF: + ret = bool_op_diff; + break; + } + return(ret); +} + + + +/* convert an EMF RGB(A) color to 0RGB +inverse of gethexcolor() in emf-print.cpp +*/ +uint32_t Metafile::sethexcolor(U_COLORREF color){ + + uint32_t out; + out = (U_RGBAGetR(color) << 16) + + (U_RGBAGetG(color) << 8 ) + + (U_RGBAGetB(color) ); + return(out); +} + +/* Return the base64 encoded png which is shown for all bad images. +Currently a random 3x4 blotch. +Caller must free. +*/ +gchar *Metafile::bad_image_png(){ + gchar *gstring = g_strdup("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAIAAAA7ljmRAAAAA3NCSVQICAjb4U/gAAAALElEQVQImQXBQQ2AMAAAsUJQMSWI2H8qME1yMshojwrvGB8XcHKvR1XtOTc/8HENumHCsOMAAAAASUVORK5CYII="); + return(gstring); +} + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-inout.h b/src/extension/internal/metafile-inout.h new file mode 100644 index 0000000..c742a64 --- /dev/null +++ b/src/extension/internal/metafile-inout.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile input - common functions + *//* + * Authors: + * David Mathog + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H + +#define PNG_SKIP_SETJMP_CHECK // else any further png.h include blows up in the compiler +#include <png.h> +#include <cstdio> +#include <cstdlib> +#include <cstdint> +#include <map> +#include <stack> +#include <glibmm/ustring.h> +#include <3rdparty/libuemf/uemf.h> +#include <2geom/affine.h> +#include <2geom/pathvector.h> + +#include "extension/implementation/implementation.h" + +class SPObject; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +/* A coloured pixel. */ +struct pixel_t { + uint8_t red; + uint8_t green; + uint8_t blue; + uint8_t opacity; +}; + +/* A picture. */ +struct bitmap_t { + pixel_t *pixels; + size_t width; + size_t height; +}; + +/* structure to store PNG image bytes */ +struct MEMPNG { + char *buffer; + size_t size; +}; +using PMEMPNG = MEMPNG *; + +class Metafile + : public Inkscape::Extension::Implementation::Implementation +{ +public: + Metafile() = default; + ~Metafile() override; + +protected: + static uint32_t sethexcolor(U_COLORREF color); + static pixel_t *pixel_at (bitmap_t * bitmap, int x, int y); + static void my_png_write_data(png_structp png_ptr, png_bytep data, png_size_t length); + static void toPNG(PMEMPNG accum, int width, int height, const char *px); + static gchar *bad_image_png(); + static void setViewBoxIfMissing(SPDocument *doc); + static int combine_ops_to_livarot(const int op); + + +private: +}; + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_INOUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ diff --git a/src/extension/internal/metafile-print.cpp b/src/extension/internal/metafile-print.cpp new file mode 100644 index 0000000..7cb5800 --- /dev/null +++ b/src/extension/internal/metafile-print.cpp @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile printing - common routines + *//* + * Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <fstream> +#include <glib.h> +#include <glibmm/miscutils.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include <2geom/svg-path-parser.h> + +#include "extension/internal/metafile-print.h" +#include "extension/print.h" +#include "path-prefix.h" +#include "object/sp-gradient.h" +#include "object/sp-image.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "style.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +PrintMetafile::~PrintMetafile() +{ +#ifndef G_OS_WIN32 + // restore default signal handling for SIGPIPE + (void) signal(SIGPIPE, SIG_DFL); +#endif + return; +} + +static std::map<Glib::ustring, FontfixParams> const &get_ppt_fixable_fonts() +{ + static std::map<Glib::ustring, FontfixParams> _ppt_fixable_fonts; + + if (_ppt_fixable_fonts.empty()) { + _ppt_fixable_fonts = { + // clang-format off + {{"Arial"}, { 0.05, -0.055, -0.065}}, + {{"Times New Roman"}, { 0.05, -0.055, -0.065}}, + {{"Lucida Sans"}, {-0.025, -0.055, -0.065}}, + {{"Sans"}, { 0.05, -0.055, -0.065}}, + {{"Microsoft Sans Serif"}, {-0.05, -0.055, -0.065}}, + {{"Serif"}, { 0.05, -0.055, -0.065}}, + {{"Garamond"}, { 0.05, -0.055, -0.065}}, + {{"Century Schoolbook"}, { 0.25, 0.025, 0.025}}, + {{"Verdana"}, { 0.025, 0.0, 0.0}}, + {{"Tahoma"}, { 0.045, 0.025, 0.025}}, + {{"Symbol"}, { 0.025, 0.0, 0.0}}, + {{"Wingdings"}, { 0.05, 0.0, 0.0}}, + {{"Zapf Dingbats"}, { 0.025, 0.0, 0.0}}, + {{"Convert To Symbol"}, { 0.025, 0.0, 0.0}}, + {{"Convert To Wingdings"}, { 0.05, 0.0, 0.0}}, + {{"Convert To Zapf Dingbats"}, { 0.025, 0.0, 0.0}}, + {{"Sylfaen"}, { 0.1, 0.0, 0.0}}, + {{"Palatino Linotype"}, { 0.175, 0.125, 0.125}}, + {{"Segoe UI"}, { 0.1, 0.0, 0.0}}, + // clang-format on + }; + } + return _ppt_fixable_fonts; +} + + +bool PrintMetafile::textToPath(Inkscape::Extension::Print *ext) +{ + return ext->get_param_bool("textToPath"); +} + +unsigned int PrintMetafile::bind(Inkscape::Extension::Print * /*mod*/, Geom::Affine const &transform, float /*opacity*/) +{ + if (!m_tr_stack.empty()) { + Geom::Affine tr_top = m_tr_stack.top(); + m_tr_stack.push(transform * tr_top); + } else { + m_tr_stack.push(transform); + } + + return 1; +} + +unsigned int PrintMetafile::release(Inkscape::Extension::Print * /*mod*/) +{ + m_tr_stack.pop(); + return 1; +} + +// Finds font fix parameters for the given fontname. +void PrintMetafile::_lookup_ppt_fontfix(Glib::ustring const &fontname, FontfixParams ¶ms) +{ + auto const &fixable_fonts = get_ppt_fixable_fonts(); + auto it = fixable_fonts.find(fontname); + if (it != fixable_fonts.end()) { + params = it->second; + } +} + +U_COLORREF PrintMetafile::_gethexcolor(uint32_t color) +{ + U_COLORREF out; + out = U_RGB( + (color >> 16) & 0xFF, + (color >> 8) & 0xFF, + (color >> 0) & 0xFF + ); + return out; +} + +// Translate Inkscape weights to EMF weights. +uint32_t PrintMetafile::_translate_weight(unsigned inkweight) +{ + switch (inkweight) { + // 400 is tested first, as it is the most common case + case SP_CSS_FONT_WEIGHT_400: return U_FW_NORMAL; + case SP_CSS_FONT_WEIGHT_100: return U_FW_THIN; + case SP_CSS_FONT_WEIGHT_200: return U_FW_EXTRALIGHT; + case SP_CSS_FONT_WEIGHT_300: return U_FW_LIGHT; + case SP_CSS_FONT_WEIGHT_500: return U_FW_MEDIUM; + case SP_CSS_FONT_WEIGHT_600: return U_FW_SEMIBOLD; + case SP_CSS_FONT_WEIGHT_700: return U_FW_BOLD; + case SP_CSS_FONT_WEIGHT_800: return U_FW_EXTRABOLD; + case SP_CSS_FONT_WEIGHT_900: return U_FW_HEAVY; + default: return U_FW_NORMAL; + } +} + +/* opacity weighting of two colors as float. v1 is the color, op is its opacity, v2 is the background color */ +inline float opweight(float v1, float v2, float op) +{ + return v1 * op + v2 * (1.0 - op); +} + +U_COLORREF PrintMetafile::avg_stop_color(SPGradient *gr) +{ + U_COLORREF cr; + int last = gr->vector.stops.size() - 1; + if (last >= 1) { + float rgbs[3]; + float rgbe[3]; + float ops, ope; + + ops = gr->vector.stops[0 ].opacity; + ope = gr->vector.stops[last].opacity; + gr->vector.stops[0 ].color.get_rgb_floatv(rgbs); + gr->vector.stops[last].color.get_rgb_floatv(rgbe); + + /* Replace opacity at start & stop with that fraction background color, then average those two for final color. */ + cr = U_RGB( + 255 * ((opweight(rgbs[0], gv.rgb[0], ops) + opweight(rgbe[0], gv.rgb[0], ope)) / 2.0), + 255 * ((opweight(rgbs[1], gv.rgb[1], ops) + opweight(rgbe[1], gv.rgb[1], ope)) / 2.0), + 255 * ((opweight(rgbs[2], gv.rgb[2], ops) + opweight(rgbe[2], gv.rgb[2], ope)) / 2.0) + ); + } else { + cr = U_RGB(0, 0, 0); // The default fill + } + return cr; +} + +U_COLORREF PrintMetafile::weight_opacity(U_COLORREF c1) +{ + float opa = c1.Reserved / 255.0; + U_COLORREF result = U_RGB( + 255 * opweight((float)c1.Red / 255.0, gv.rgb[0], opa), + 255 * opweight((float)c1.Green / 255.0, gv.rgb[1], opa), + 255 * opweight((float)c1.Blue / 255.0, gv.rgb[2], opa) + ); + return result; +} + +/* t between 0 and 1, values outside that range use the nearest limit */ +U_COLORREF PrintMetafile::weight_colors(U_COLORREF c1, U_COLORREF c2, double t) +{ +#define clrweight(a,b,t) ((1-t)*((double) a) + (t)*((double) b)) + U_COLORREF result; + t = ( t > 1.0 ? 1.0 : ( t < 0.0 ? 0.0 : t)); + // clang-format off + result.Red = clrweight(c1.Red, c2.Red, t); + result.Green = clrweight(c1.Green, c2.Green, t); + result.Blue = clrweight(c1.Blue, c2.Blue, t); + result.Reserved = clrweight(c1.Reserved, c2.Reserved, t); + // clang-format on + + // now handle the opacity, mix the RGB with background at the weighted opacity + + if (result.Reserved != 255) { + result = weight_opacity(result); + } + + return result; +} + +// Extract hatchType, hatchColor from a name like +// EMFhatch<hatchType>_<hatchColor> +// Where the first one is a number and the second a color in hex. +// hatchType and hatchColor have been set with defaults before this is called. +// +void PrintMetafile::hatch_classify(char *name, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor) +{ + int val; + uint32_t hcolor = 0; + uint32_t bcolor = 0; + + // name should be EMFhatch or WMFhatch but *MFhatch will be accepted + if (0 != strncmp(&name[1], "MFhatch", 7)) { + return; // not anything we can parse + } + name += 8; // EMFhatch already detected + val = 0; + while (*name && isdigit(*name)) { + val = 10 * val + *name - '0'; + name++; + } + *hatchType = val; + if (*name != '_' || val > U_HS_DITHEREDBKCLR) { // wrong syntax, cannot classify + *hatchType = -1; + } else { + name++; + if (2 != sscanf(name, "%X_%X", &hcolor, &bcolor)) { // not a pattern with background + if (1 != sscanf(name, "%X", &hcolor)) { + *hatchType = -1; // not a pattern, cannot classify + } + *hatchColor = _gethexcolor(hcolor); + } else { + *hatchColor = _gethexcolor(hcolor); + *bkColor = _gethexcolor(bcolor); + usebk = true; + } + } + /* Everything > U_HS_SOLIDCLR is solid, just specify the color in the brush rather than messing around with background or textcolor */ + if (*hatchType > U_HS_SOLIDCLR) { + *hatchType = U_HS_SOLIDCLR; + } +} + +// +// Recurse down from a brush pattern, try to figure out what it is. +// If an image is found set a pointer to the epixbuf, else set that to NULL +// If a pattern is found with a name like [EW]MFhatch3_3F7FFF return hatchType=3, hatchColor=3F7FFF (as a uint32_t), +// otherwise hatchType is set to -1 and hatchColor is not defined. +// + +void PrintMetafile::brush_classify(SPObject *parent, int depth, Inkscape::Pixbuf **epixbuf, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor) +{ + if (depth == 0) { + *epixbuf = nullptr; + *hatchType = -1; + *hatchColor = U_RGB(0, 0, 0); + *bkColor = U_RGB(255, 255, 255); + } + depth++; + // first look along the pattern chain, if there is one + if (SP_IS_PATTERN(parent)) { + for (SPPattern *pat_i = SP_PATTERN(parent); pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) { + if (SP_IS_IMAGE(pat_i)) { + *epixbuf = ((SPImage *)pat_i)->pixbuf; + return; + } + char temp[32]; // large enough + strncpy(temp, pat_i->getAttribute("id"), sizeof(temp)-1); // Some names may be longer than [EW]MFhatch#_###### + temp[sizeof(temp)-1] = '\0'; + hatch_classify(temp, hatchType, hatchColor, bkColor); + if (*hatchType != -1) { + return; + } + + // still looking? Look at this pattern's children, if there are any + for (auto& child: pat_i->children) { + if (*epixbuf || *hatchType != -1) { + break; + } + brush_classify(&child, depth, epixbuf, hatchType, hatchColor, bkColor); + } + } + } else if (SP_IS_IMAGE(parent)) { + *epixbuf = ((SPImage *)parent)->pixbuf; + return; + } else { // some inkscape rearrangements pass through nodes between pattern and image which are not classified as either. + for (auto& child: parent->children) { + if (*epixbuf || *hatchType != -1) { + break; + } + brush_classify(&child, depth, epixbuf, hatchType, hatchColor, bkColor); + } + } +} + +//swap R/B in 4 byte pixel +void PrintMetafile::swapRBinRGBA(char *px, int pixels) +{ + char tmp; + for (int i = 0; i < pixels * 4; px += 4, i += 4) { + tmp = px[2]; + px[2] = px[0]; + px[0] = tmp; + } +} + +int PrintMetafile::hold_gradient(void *gr, int mode) +{ + gv.mode = mode; + gv.grad = gr; + if (mode == DRAW_RADIAL_GRADIENT) { + SPRadialGradient *rg = (SPRadialGradient *) gr; + gv.r = rg->r.computed; // radius, but of what??? + gv.p1 = Geom::Point(rg->cx.computed, rg->cy.computed); // center + gv.p2 = Geom::Point(gv.r, 0) + gv.p1; // xhandle + gv.p3 = Geom::Point(0, -gv.r) + gv.p1; // yhandle + if (rg->gradientTransform_set) { + gv.p1 = gv.p1 * rg->gradientTransform; + gv.p2 = gv.p2 * rg->gradientTransform; + gv.p3 = gv.p3 * rg->gradientTransform; + } + } else if (mode == DRAW_LINEAR_GRADIENT) { + SPLinearGradient *lg = (SPLinearGradient *) gr; + gv.r = 0; // unused + gv.p1 = Geom::Point(lg->x1.computed, lg->y1.computed); // start + gv.p2 = Geom::Point(lg->x2.computed, lg->y2.computed); // end + gv.p3 = Geom::Point(0, 0); // unused + if (lg->gradientTransform_set) { + gv.p1 = gv.p1 * lg->gradientTransform; + gv.p2 = gv.p2 * lg->gradientTransform; + } + } else { + g_error("Fatal programming error, hold_gradient() in metafile-print.cpp called with invalid draw mode"); + } + return 1; +} + +/* convert from center ellipse to SVGEllipticalArc ellipse + + From: + http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter + A point (x,y) on the arc can be found by: + + {x,y} = {cx,cy} + {cosF,-sinF,sinF,cosF} x {rxcosT,rysinT} + + where + {cx,cy} is the center of the ellipse + F is the rotation angle of the X axis of the ellipse from the true X axis + T is the rotation angle around the ellipse + {,,,} is the rotation matrix + rx,ry are the radii of the ellipse's axes + + For SVG parameterization need two points. + Arbitrarily we can use T=0 and T=pi + Since the sweep is 180 the flags are always 0: + + F is in RADIANS, but the SVGEllipticalArc needs degrees! + +*/ +Geom::PathVector PrintMetafile::center_ellipse_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F) +{ + using Geom::X; + using Geom::Y; + double x1, y1, x2, y2; + Geom::Path SVGep; + + x1 = ctr[X] + cos(F) * rx * cos(0) + sin(-F) * ry * sin(0); + y1 = ctr[Y] + sin(F) * rx * cos(0) + cos(F) * ry * sin(0); + x2 = ctr[X] + cos(F) * rx * cos(M_PI) + sin(-F) * ry * sin(M_PI); + y2 = ctr[Y] + sin(F) * rx * cos(M_PI) + cos(F) * ry * sin(M_PI); + + char text[256]; + snprintf(text, 256, " M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z", + x1, y1, rx, ry, F * 360. / (2.*M_PI), x2, y2, rx, ry, F * 360. / (2.*M_PI), x1, y1); + Geom::PathVector outres = Geom::parse_svg_path(text); + return outres; +} + + +/* rx2,ry2 must be larger than rx1,ry1! + angle is in RADIANS +*/ +Geom::PathVector PrintMetafile::center_elliptical_ring_as_SVG_PathV(Geom::Point ctr, double rx1, double ry1, double rx2, double ry2, double F) +{ + using Geom::X; + using Geom::Y; + double x11, y11, x12, y12; + double x21, y21, x22, y22; + double degrot = F * 360. / (2.*M_PI); + + x11 = ctr[X] + cos(F) * rx1 * cos(0) + sin(-F) * ry1 * sin(0); + y11 = ctr[Y] + sin(F) * rx1 * cos(0) + cos(F) * ry1 * sin(0); + x12 = ctr[X] + cos(F) * rx1 * cos(M_PI) + sin(-F) * ry1 * sin(M_PI); + y12 = ctr[Y] + sin(F) * rx1 * cos(M_PI) + cos(F) * ry1 * sin(M_PI); + + x21 = ctr[X] + cos(F) * rx2 * cos(0) + sin(-F) * ry2 * sin(0); + y21 = ctr[Y] + sin(F) * rx2 * cos(0) + cos(F) * ry2 * sin(0); + x22 = ctr[X] + cos(F) * rx2 * cos(M_PI) + sin(-F) * ry2 * sin(M_PI); + y22 = ctr[Y] + sin(F) * rx2 * cos(M_PI) + cos(F) * ry2 * sin(M_PI); + + char text[512]; + snprintf(text, 512, " M %f,%f A %f %f %f 0 1 %f %f A %f %f %f 0 1 %f %f z M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z", + x11, y11, rx1, ry1, degrot, x12, y12, rx1, ry1, degrot, x11, y11, + x21, y21, rx2, ry2, degrot, x22, y22, rx2, ry2, degrot, x21, y21); + Geom::PathVector outres = Geom::parse_svg_path(text); + + return outres; +} + +/* Elliptical hole in a large square extending from -50k to +50k */ +Geom::PathVector PrintMetafile::center_elliptical_hole_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F) +{ + using Geom::X; + using Geom::Y; + double x1, y1, x2, y2; + Geom::Path SVGep; + + x1 = ctr[X] + cos(F) * rx * cos(0) + sin(-F) * ry * sin(0); + y1 = ctr[Y] + sin(F) * rx * cos(0) + cos(F) * ry * sin(0); + x2 = ctr[X] + cos(F) * rx * cos(M_PI) + sin(-F) * ry * sin(M_PI); + y2 = ctr[Y] + sin(F) * rx * cos(M_PI) + cos(F) * ry * sin(M_PI); + + char text[256]; + snprintf(text, 256, " M %f,%f A %f %f %f 0 0 %f %f A %f %f %f 0 0 %f %f z M 50000,50000 50000,-50000 -50000,-50000 -50000,50000 z", + x1, y1, rx, ry, F * 360. / (2.*M_PI), x2, y2, rx, ry, F * 360. / (2.*M_PI), x1, y1); + Geom::PathVector outres = Geom::parse_svg_path(text); + return outres; +} + +/* rectangular cutter. +ctr "center" of rectangle (might not actually be in the center with respect to leading/trailing edges +pos vector from center to leading edge +neg vector from center to trailing edge +width vector to side edge +*/ +Geom::PathVector PrintMetafile::rect_cutter(Geom::Point ctr, Geom::Point pos, Geom::Point neg, Geom::Point width) +{ + Geom::PathVector outres; + Geom::Path cutter; + cutter.start(ctr + pos - width); + cutter.appendNew<Geom::LineSegment>(ctr + pos + width); + cutter.appendNew<Geom::LineSegment>(ctr + neg + width); + cutter.appendNew<Geom::LineSegment>(ctr + neg - width); + cutter.close(); + outres.push_back(cutter); + return outres; +} + +/* Convert from SPWindRule to livarot's FillRule + This is similar to what sp_selected_path_boolop() does +*/ +FillRule PrintMetafile::SPWR_to_LVFR(SPWindRule wr) +{ + FillRule fr; + if (wr == SP_WIND_RULE_EVENODD) { + fr = fill_oddEven; + } else { + fr = fill_nonZero; + } + return fr; +} + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/metafile-print.h b/src/extension/internal/metafile-print.h new file mode 100644 index 0000000..d828f9f --- /dev/null +++ b/src/extension/internal/metafile-print.h @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Metafile printing - common functions + *//* + * Authors: + * Krzysztof KosiÅ„ski <tweenk.pl@gmail.com> + * + * Copyright (C) 2013 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_METAFILE_PRINT_H + +#include <map> +#include <stack> + +#include <glibmm/ustring.h> +#include <3rdparty/libuemf/uemf.h> +#include <2geom/affine.h> +#include <2geom/pathvector.h> + +#include "extension/implementation/implementation.h" + +#include "style-enums.h" // Fill rule +#include "livarot/LivarotDefs.h" // FillRule + +class SPGradient; +class SPObject; + +namespace Inkscape { +class Pixbuf; + +namespace Extension { +namespace Internal { + +enum MFDrawMode {DRAW_PAINT, DRAW_PATTERN, DRAW_IMAGE, DRAW_LINEAR_GRADIENT, DRAW_RADIAL_GRADIENT}; + +struct FontfixParams { + double f1; //Vertical (rotating) offset factor (* font height) + double f2; //Vertical (nonrotating) offset factor (* font height) + double f3; //Horizontal (nonrotating) offset factor (* font height) +}; + +class PrintMetafile + : public Inkscape::Extension::Implementation::Implementation +{ +public: + PrintMetafile() = default; + ~PrintMetafile() override; + + bool textToPath (Inkscape::Extension::Print * ext) override; + unsigned int bind(Inkscape::Extension::Print *module, Geom::Affine const &transform, float opacity) override; + unsigned int release(Inkscape::Extension::Print *module) override; + +protected: + struct GRADVALUES { + Geom::Point p1; // center or start + Geom::Point p2; // xhandle or end + Geom::Point p3; // yhandle or unused + double r; // radius or unused + void *grad; // to access the stops information + int mode; // DRAW_LINEAR_GRADIENT or DRAW_RADIAL_GRADIENT, if GRADVALUES is valid, else any value + U_COLORREF bgc; // document background color, this is as good a place as any to keep it + float rgb[3]; // also background color, but as 0-1 float. + }; + + double _width; + double _height; + double _doc_unit_scale; // to pixels, regardless of the document units + + U_RECTL rc; + + uint32_t htextalignment; + uint32_t hpolyfillmode; // used to minimize redundant records that set this + float htextcolor_rgb[3]; // used to minimize redundant records that set this + + std::stack<Geom::Affine> m_tr_stack; + Geom::PathVector fill_pathv; + Geom::Affine fill_transform; + bool use_stroke; + bool use_fill; + bool simple_shape; + bool usebk; + + GRADVALUES gv; + + static void _lookup_ppt_fontfix(Glib::ustring const &fontname, FontfixParams &); + static U_COLORREF _gethexcolor(uint32_t color); + static uint32_t _translate_weight(unsigned inkweight); + + U_COLORREF avg_stop_color(SPGradient *gr); + U_COLORREF weight_opacity(U_COLORREF c1); + U_COLORREF weight_colors(U_COLORREF c1, U_COLORREF c2, double t); + + void hatch_classify(char *name, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor); + void brush_classify(SPObject *parent, int depth, Inkscape::Pixbuf **epixbuf, int *hatchType, U_COLORREF *hatchColor, U_COLORREF *bkColor); + static void swapRBinRGBA(char *px, int pixels); + + int hold_gradient(void *gr, int mode); + static int snprintf_dots(char * s, size_t n, const char * format, ...); + static Geom::PathVector center_ellipse_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F); + static Geom::PathVector center_elliptical_ring_as_SVG_PathV(Geom::Point ctr, double rx1, double ry1, double rx2, double ry2, double F); + static Geom::PathVector center_elliptical_hole_as_SVG_PathV(Geom::Point ctr, double rx, double ry, double F); + static Geom::PathVector rect_cutter(Geom::Point ctr, Geom::Point pos, Geom::Point neg, Geom::Point width); + static FillRule SPWR_to_LVFR(SPWindRule wr); + + virtual int create_brush(SPStyle const *style, PU_COLORREF fcolor) = 0; + virtual void destroy_brush() = 0; + virtual int create_pen(SPStyle const *style, const Geom::Affine &transform) = 0; + virtual void destroy_pen() = 0; +}; + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/odf.cpp b/src/extension/internal/odf.cpp new file mode 100644 index 0000000..4b10711 --- /dev/null +++ b/src/extension/internal/odf.cpp @@ -0,0 +1,2126 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * OpenDocument (drawing) input and output + *//* + * Authors: + * Bob Jamison + * Abhishek Sharma + * Kris De Gussem + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ +/* + * This is an an entry in the extensions mechanism to begin to enable + * the inputting and outputting of OpenDocument Format (ODF) files from + * within Inkscape. Although the initial implementations will be very lossy + * due to the differences in the models of SVG and ODF, they will hopefully + * improve greatly with time. People should consider this to be a framework + * that can be continuously upgraded for ever improving fidelity. Potential + * developers should especially look in preprocess() and writeTree() to see how + * the SVG tree is scanned, read, translated, and then written to ODF. + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + * + */ + +#include "odf.h" + +//# System includes +#include <cstdio> +#include <ctime> +#include <vector> +#include <cmath> + +//# Inkscape includes +#include "clear-n_.h" +#include "inkscape.h" +#include "display/curve.h" +#include <2geom/pathvector.h> +#include <2geom/curves.h> +#include <2geom/transforms.h> +#include <helper/geom.h> +#include "helper/geom-curves.h" +#include "extension/system.h" + +#include "xml/repr.h" +#include "xml/attribute-record.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-stop.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" +#include "object/uri.h" +#include "style.h" + +#include "svg/svg.h" +#include "text-editing.h" +#include "util/units.h" + + +#include "inkscape-version.h" +#include "document.h" +#include "extension/extension.h" + +#include "io/stream/bufferstream.h" +#include "io/stream/stringstream.h" +#include "io/sys.h" +#include <util/ziptool.h> +#include <iomanip> +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ +//# Shorthand notation +typedef Inkscape::IO::BufferOutputStream BufferOutputStream; +typedef Inkscape::IO::OutputStreamWriter OutputStreamWriter; +typedef Inkscape::IO::StringOutputStream StringOutputStream; + + +//######################################################################## +//# C L A S S SingularValueDecomposition +//######################################################################## +#include <cmath> + +class SVDMatrix +{ +public: + + SVDMatrix() + { + init(); + } + + SVDMatrix(unsigned int rowSize, unsigned int colSize) + { + init(); + rows = rowSize; + cols = colSize; + size = rows * cols; + d = new double[size]; + for (unsigned int i=0 ; i<size ; i++) + d[i] = 0.0; + } + + SVDMatrix(double *vals, unsigned int rowSize, unsigned int colSize) + { + init(); + rows = rowSize; + cols = colSize; + size = rows * cols; + d = new double[size]; + for (unsigned int i=0 ; i<size ; i++) + d[i] = vals[i]; + } + + + SVDMatrix(const SVDMatrix &other) + { + init(); + assign(other); + } + + SVDMatrix &operator=(const SVDMatrix &other) + { + assign(other); + return *this; + } + + virtual ~SVDMatrix() + { + delete[] d; + } + + double& operator() (unsigned int row, unsigned int col) + { + if (row >= rows || col >= cols) + return badval; + return d[cols*row + col]; + } + + double operator() (unsigned int row, unsigned int col) const + { + if (row >= rows || col >= cols) + return badval; + return d[cols*row + col]; + } + + unsigned int getRows() + { + return rows; + } + + unsigned int getCols() + { + return cols; + } + + SVDMatrix multiply(const SVDMatrix &other) + { + if (cols != other.rows) + { + SVDMatrix dummy; + return dummy; + } + SVDMatrix result(rows, other.cols); + for (unsigned int i=0 ; i<rows ; i++) + { + for (unsigned int j=0 ; j<other.cols ; j++) + { + double sum = 0.0; + for (unsigned int k=0 ; k<cols ; k++) + { + //sum += a[i][k] * b[k][j]; + sum += d[i*cols +k] * other(k, j); + } + result(i, j) = sum; + } + + } + return result; + } + + SVDMatrix transpose() + { + SVDMatrix result(cols, rows); + for (unsigned int i=0 ; i<rows ; i++){ + for (unsigned int j=0 ; j<cols ; j++){ + result(j, i) = d[i*cols + j]; + } + } + return result; + } + +private: + + + virtual void init() + { + badval = 0.0; + d = nullptr; + rows = 0; + cols = 0; + size = 0; + } + + void assign(const SVDMatrix &other) + { + if (d) + { + delete[] d; + d = nullptr; + } + rows = other.rows; + cols = other.cols; + size = other.size; + badval = other.badval; + d = new double[size]; + for (unsigned int i=0 ; i<size ; i++){ + d[i] = other.d[i]; + } + } + + double badval; + + double *d; + unsigned int rows; + unsigned int cols; + unsigned int size; +}; + + + +/** + * + * ==================================================== + * + * NOTE: + * This class is ported almost verbatim from the public domain + * JAMA Matrix package. It is modified to handle only 3x3 matrices + * and our Geom::Affine affine transform class. We give full + * attribution to them, along with many thanks. JAMA can be found at: + * http://math.nist.gov/javanumerics/jama + * + * ==================================================== + * + * Singular Value Decomposition. + * <P> + * For an m-by-n matrix A with m >= n, the singular value decomposition is + * an m-by-n orthogonal matrix U, an n-by-n diagonal matrix S, and + * an n-by-n orthogonal matrix V so that A = U*S*V'. + * <P> + * The singular values, sigma[k] = S[k][k], are ordered so that + * sigma[0] >= sigma[1] >= ... >= sigma[n-1]. + * <P> + * The singular value decomposition always exists, so the constructor will + * never fail. The matrix condition number and the effective numerical + * rank can be computed from this decomposition. + */ +class SingularValueDecomposition +{ +public: + + /** Construct the singular value decomposition + @param A Rectangular matrix + @return Structure to access U, S and V. + */ + + SingularValueDecomposition (const SVDMatrix &mat) : + A (mat), + U (), + s (nullptr), + s_size (0), + V () + { + calculate(); + } + + virtual ~SingularValueDecomposition() + { + delete[] s; + } + + /** + * Return the left singular vectors + * @return U + */ + SVDMatrix &getU(); + + /** + * Return the right singular vectors + * @return V + */ + SVDMatrix &getV(); + + /** + * Return the s[index] value + */ double getS(unsigned int index); + + /** + * Two norm + * @return max(S) + */ + double norm2(); + + /** + * Two norm condition number + * @return max(S)/min(S) + */ + double cond(); + + /** + * Effective numerical matrix rank + * @return Number of nonnegligible singular values. + */ + int rank(); + +private: + + void calculate(); + + SVDMatrix A; + SVDMatrix U; + double *s; + unsigned int s_size; + SVDMatrix V; + +}; + + +static double svd_hypot(double a, double b) +{ + double r; + + if (fabs(a) > fabs(b)) + { + r = b/a; + r = fabs(a) * sqrt(1+r*r); + } + else if (b != 0) + { + r = a/b; + r = fabs(b) * sqrt(1+r*r); + } + else + { + r = 0.0; + } + return r; +} + + + +void SingularValueDecomposition::calculate() +{ + // Initialize. + int m = A.getRows(); + int n = A.getCols(); + + int nu = (m > n) ? m : n; + s_size = (m+1 < n) ? m+1 : n; + s = new double[s_size]; + U = SVDMatrix(m, nu); + V = SVDMatrix(n, n); + double *e = new double[n]; + double *work = new double[m]; + bool wantu = true; + bool wantv = true; + + // Reduce A to bidiagonal form, storing the diagonal elements + // in s and the super-diagonal elements in e. + + int nct = (m-1<n) ? m-1 : n; + int nrtx = (n-2<m) ? n-2 : m; + int nrt = (nrtx>0) ? nrtx : 0; + for (int k = 0; k < 2; k++) { + if (k < nct) { + + // Compute the transformation for the k-th column and + // place the k-th diagonal in s[k]. + // Compute 2-norm of k-th column without under/overflow. + s[k] = 0; + for (int i = k; i < m; i++) { + s[k] = svd_hypot(s[k],A(i, k)); + } + if (s[k] != 0.0) { + if (A(k, k) < 0.0) { + s[k] = -s[k]; + } + for (int i = k; i < m; i++) { + A(i, k) /= s[k]; + } + A(k, k) += 1.0; + } + s[k] = -s[k]; + } + for (int j = k+1; j < n; j++) { + if ((k < nct) & (s[k] != 0.0)) { + + // Apply the transformation. + + double t = 0; + for (int i = k; i < m; i++) { + t += A(i, k) * A(i, j); + } + t = -t/A(k, k); + for (int i = k; i < m; i++) { + A(i, j) += t*A(i, k); + } + } + + // Place the k-th row of A into e for the + // subsequent calculation of the row transformation. + + e[j] = A(k, j); + } + if (wantu & (k < nct)) { + + // Place the transformation in U for subsequent back + // multiplication. + + for (int i = k; i < m; i++) { + U(i, k) = A(i, k); + } + } + if (k < nrt) { + + // Compute the k-th row transformation and place the + // k-th super-diagonal in e[k]. + // Compute 2-norm without under/overflow. + e[k] = 0; + for (int i = k+1; i < n; i++) { + e[k] = svd_hypot(e[k],e[i]); + } + if (e[k] != 0.0) { + if (e[k+1] < 0.0) { + e[k] = -e[k]; + } + for (int i = k+1; i < n; i++) { + e[i] /= e[k]; + } + e[k+1] += 1.0; + } + e[k] = -e[k]; + if ((k+1 < m) & (e[k] != 0.0)) { + + // Apply the transformation. + + for (int i = k+1; i < m; i++) { + work[i] = 0.0; + } + for (int j = k+1; j < n; j++) { + for (int i = k+1; i < m; i++) { + work[i] += e[j]*A(i, j); + } + } + for (int j = k+1; j < n; j++) { + double t = -e[j]/e[k+1]; + for (int i = k+1; i < m; i++) { + A(i, j) += t*work[i]; + } + } + } + if (wantv) { + + // Place the transformation in V for subsequent + // back multiplication. + + for (int i = k+1; i < n; i++) { + V(i, k) = e[i]; + } + } + } + } + + // Set up the final bidiagonal matrix or order p. + + int p = (n < m+1) ? n : m+1; + if (nct < n) { + s[nct] = A(nct, nct); + } + if (m < p) { + s[p-1] = 0.0; + } + if (nrt+1 < p) { + e[nrt] = A(nrt, p-1); + } + e[p-1] = 0.0; + + // If required, generate U. + + if (wantu) { + for (int j = nct; j < nu; j++) { + for (int i = 0; i < m; i++) { + U(i, j) = 0.0; + } + U(j, j) = 1.0; + } + for (int k = nct-1; k >= 0; k--) { + if (s[k] != 0.0) { + for (int j = k+1; j < nu; j++) { + double t = 0; + for (int i = k; i < m; i++) { + t += U(i, k)*U(i, j); + } + t = -t/U(k, k); + for (int i = k; i < m; i++) { + U(i, j) += t*U(i, k); + } + } + for (int i = k; i < m; i++ ) { + U(i, k) = -U(i, k); + } + U(k, k) = 1.0 + U(k, k); + for (int i = 0; i < k-1; i++) { + U(i, k) = 0.0; + } + } else { + for (int i = 0; i < m; i++) { + U(i, k) = 0.0; + } + U(k, k) = 1.0; + } + } + } + + // If required, generate V. + + if (wantv) { + for (int k = n-1; k >= 0; k--) { + if ((k < nrt) & (e[k] != 0.0)) { + for (int j = k+1; j < nu; j++) { + double t = 0; + for (int i = k+1; i < n; i++) { + t += V(i, k)*V(i, j); + } + t = -t/V(k+1, k); + for (int i = k+1; i < n; i++) { + V(i, j) += t*V(i, k); + } + } + } + for (int i = 0; i < n; i++) { + V(i, k) = 0.0; + } + V(k, k) = 1.0; + } + } + + // Main iteration loop for the singular values. + + int pp = p-1; + //double eps = pow(2.0,-52.0); + //double tiny = pow(2.0,-966.0); + //let's just calculate these now + //a double can be e ± 308.25, so this is safe + double eps = 2.22e-16; + double tiny = 1.6e-291; + while (p > 0) { + int k,kase; + + // Here is where a test for too many iterations would go. + + // This section of the program inspects for + // negligible elements in the s and e arrays. On + // completion the variables kase and k are set as follows. + + // kase = 1 if s(p) and e[k-1] are negligible and k<p + // kase = 2 if s(k) is negligible and k<p + // kase = 3 if e[k-1] is negligible, k<p, and + // s(k), ..., s(p) are not negligible (qr step). + // kase = 4 if e(p-1) is negligible (convergence). + + for (k = p-2; k >= -1; k--) { + if (k == -1) { + break; + } + if (fabs(e[k]) <= + tiny + eps*(fabs(s[k]) + fabs(s[k+1]))) { + e[k] = 0.0; + break; + } + } + if (k == p-2) { + kase = 4; + } else { + int ks; + for (ks = p-1; ks >= k; ks--) { + if (ks == k) { + break; + } + double t = (ks != p ? fabs(e[ks]) : 0.) + + (ks != k+1 ? fabs(e[ks-1]) : 0.); + if (fabs(s[ks]) <= tiny + eps*t) { + s[ks] = 0.0; + break; + } + } + if (ks == k) { + kase = 3; + } else if (ks == p-1) { + kase = 1; + } else { + kase = 2; + k = ks; + } + } + k++; + + // Perform the task indicated by kase. + + switch (kase) { + + // Deflate negligible s(p). + + case 1: { + double f = e[p-2]; + e[p-2] = 0.0; + for (int j = p-2; j >= k; j--) { + double t = svd_hypot(s[j],f); + double cs = s[j]/t; + double sn = f/t; + s[j] = t; + if (j != k) { + f = -sn*e[j-1]; + e[j-1] = cs*e[j-1]; + } + if (wantv) { + for (int i = 0; i < n; i++) { + t = cs*V(i, j) + sn*V(i, p-1); + V(i, p-1) = -sn*V(i, j) + cs*V(i, p-1); + V(i, j) = t; + } + } + } + } + break; + + // Split at negligible s(k). + + case 2: { + double f = e[k-1]; + e[k-1] = 0.0; + for (int j = k; j < p; j++) { + double t = svd_hypot(s[j],f); + double cs = s[j]/t; + double sn = f/t; + s[j] = t; + f = -sn*e[j]; + e[j] = cs*e[j]; + if (wantu) { + for (int i = 0; i < m; i++) { + t = cs*U(i, j) + sn*U(i, k-1); + U(i, k-1) = -sn*U(i, j) + cs*U(i, k-1); + U(i, j) = t; + } + } + } + } + break; + + // Perform one qr step. + + case 3: { + + // Calculate the shift. + + double scale = fabs(s[p-1]); + double d = fabs(s[p-2]); + if (d>scale) scale=d; + d = fabs(e[p-2]); + if (d>scale) scale=d; + d = fabs(s[k]); + if (d>scale) scale=d; + d = fabs(e[k]); + if (d>scale) scale=d; + double sp = s[p-1]/scale; + double spm1 = s[p-2]/scale; + double epm1 = e[p-2]/scale; + double sk = s[k]/scale; + double ek = e[k]/scale; + double b = ((spm1 + sp)*(spm1 - sp) + epm1*epm1)/2.0; + double c = (sp*epm1)*(sp*epm1); + double shift = 0.0; + if ((b != 0.0) | (c != 0.0)) { + shift = sqrt(b*b + c); + if (b < 0.0) { + shift = -shift; + } + shift = c/(b + shift); + } + double f = (sk + sp)*(sk - sp) + shift; + double g = sk*ek; + + // Chase zeros. + + for (int j = k; j < p-1; j++) { + double t = svd_hypot(f,g); + double cs = f/t; + double sn = g/t; + if (j != k) { + e[j-1] = t; + } + f = cs*s[j] + sn*e[j]; + e[j] = cs*e[j] - sn*s[j]; + g = sn*s[j+1]; + s[j+1] = cs*s[j+1]; + if (wantv) { + for (int i = 0; i < n; i++) { + t = cs*V(i, j) + sn*V(i, j+1); + V(i, j+1) = -sn*V(i, j) + cs*V(i, j+1); + V(i, j) = t; + } + } + t = svd_hypot(f,g); + cs = f/t; + sn = g/t; + s[j] = t; + f = cs*e[j] + sn*s[j+1]; + s[j+1] = -sn*e[j] + cs*s[j+1]; + g = sn*e[j+1]; + e[j+1] = cs*e[j+1]; + if (wantu && (j < m-1)) { + for (int i = 0; i < m; i++) { + t = cs*U(i, j) + sn*U(i, j+1); + U(i, j+1) = -sn*U(i, j) + cs*U(i, j+1); + U(i, j) = t; + } + } + } + e[p-2] = f; + } + break; + + // Convergence. + + case 4: { + + // Make the singular values positive. + + if (s[k] <= 0.0) { + s[k] = (s[k] < 0.0 ? -s[k] : 0.0); + if (wantv) { + for (int i = 0; i <= pp; i++) { + V(i, k) = -V(i, k); + } + } + } + + // Order the singular values. + + while (k < pp) { + if (s[k] >= s[k+1]) { + break; + } + double t = s[k]; + s[k] = s[k+1]; + s[k+1] = t; + if (wantv && (k < n-1)) { + for (int i = 0; i < n; i++) { + t = V(i, k+1); V(i, k+1) = V(i, k); V(i, k) = t; + } + } + if (wantu && (k < m-1)) { + for (int i = 0; i < m; i++) { + t = U(i, k+1); U(i, k+1) = U(i, k); U(i, k) = t; + } + } + k++; + } + p--; + } + break; + } + } + + delete [] e; + delete [] work; + +} + + +/** + * Return the left singular vectors + * @return U + */ +SVDMatrix &SingularValueDecomposition::getU() +{ + return U; +} + +/** + * Return the right singular vectors + * @return V + */ + +SVDMatrix &SingularValueDecomposition::getV() +{ + return V; +} + +/** + * Return the s[0] value + */ +double SingularValueDecomposition::getS(unsigned int index) +{ + if (index >= s_size) + return 0.0; + return s[index]; +} + +/** + * Two norm + * @return max(S) + */ +double SingularValueDecomposition::norm2() +{ + return s[0]; +} + +/** + * Two norm condition number + * @return max(S)/min(S) + */ + +double SingularValueDecomposition::cond() +{ + return s[0]/s[2]; +} + +/** + * Effective numerical matrix rank + * @return Number of nonnegligible singular values. + */ +int SingularValueDecomposition::rank() +{ + double eps = pow(2.0,-52.0); + double tol = 3.0*s[0]*eps; + int r = 0; + for (int i = 0; i < 3; i++) + { + if (s[i] > tol) + r++; + } + return r; +} + +//######################################################################## +//# E N D C L A S S SingularValueDecomposition +//######################################################################## + + + + + +//#define pxToCm 0.0275 +#define pxToCm 0.03 + + +//######################################################################## +//# O U T P U T +//######################################################################## + +/** + * Get the value of a node/attribute pair + */ +static Glib::ustring getAttribute( Inkscape::XML::Node *node, char const *attrName) +{ + Glib::ustring val; + char const *valstr = node->attribute(attrName); + if (valstr) + val = valstr; + return val; +} + + +static Glib::ustring formatTransform(Geom::Affine &tf) +{ + Glib::ustring str; + if (!tf.isIdentity()) + { + StringOutputStream outs; + OutputStreamWriter out(outs); + out.printf("matrix(%.3f %.3f %.3f %.3f %.3f %.3f)", + tf[0], tf[1], tf[2], tf[3], tf[4], tf[5]); + str = outs.getString(); + } + return str; +} + + +/** + * Get the general transform from SVG pixels to + * ODF cm + */ +static Geom::Affine getODFTransform(const SPItem *item) +{ + //### Get SVG-to-ODF transform + Geom::Affine tf (item->i2doc_affine()); + tf = tf * Geom::Affine(Geom::Scale(pxToCm)); + return tf; +} + + +/** + * Get the bounding box of an item, as mapped onto + * an ODF document, in cm. + */ +static Geom::OptRect getODFBoundingBox(const SPItem *item) +{ + // TODO: geometric or visual? + Geom::OptRect bbox = item->documentVisualBounds(); + if (bbox) { + *bbox *= Geom::Affine(Geom::Scale(pxToCm)); + } + return bbox; +} + + +/** + * Get the transform for an item, including parents, but without + * root viewBox transformation. + */ +static Geom::Affine getODFItemTransform(const SPItem *item) +{ + Geom::Affine itemTransform (item->i2doc_affine() * + item->document->getRoot()->c2p.inverse()); + return itemTransform; +} + + +/** + * Get some fun facts from the transform + */ +static void analyzeTransform(Geom::Affine &tf, + double &rotate, double &/*xskew*/, double &/*yskew*/, + double &xscale, double &yscale) +{ + SVDMatrix mat(2, 2); + mat(0, 0) = tf[0]; + mat(0, 1) = tf[1]; + mat(1, 0) = tf[2]; + mat(1, 1) = tf[3]; + + SingularValueDecomposition svd(mat); + + SVDMatrix U = svd.getU(); + SVDMatrix V = svd.getV(); + SVDMatrix Vt = V.transpose(); + SVDMatrix UVt = U.multiply(Vt); + double s0 = svd.getS(0); + double s1 = svd.getS(1); + xscale = s0; + yscale = s1; + rotate = UVt(0,0); +} + +static void gatherText(Inkscape::XML::Node *node, Glib::ustring &buf) +{ + if (node->type() == Inkscape::XML::NodeType::TEXT_NODE) + { + char *s = (char *)node->content(); + if (s) + buf.append(s); + } + + for (Inkscape::XML::Node *child = node->firstChild() ; + child != nullptr; child = child->next()) + { + gatherText(child, buf); + } + +} + + +/** + * FIRST PASS. + * Method descends into the repr tree, converting image, style, and gradient info + * into forms compatible in ODF. + */ +void OdfOutput::preprocess(ZipFile &zf, SPDocument *doc, Inkscape::XML::Node *node) +{ + Glib::ustring nodeName = node->name(); + Glib::ustring id = getAttribute(node, "id"); + + //### First, check for metadata + if (nodeName == "metadata" || nodeName == "svg:metadata") + { + Inkscape::XML::Node *mchild = node->firstChild() ; + if (!mchild || strcmp(mchild->name(), "rdf:RDF")) + return; + Inkscape::XML::Node *rchild = mchild->firstChild() ; + if (!rchild || strcmp(rchild->name(), "cc:Work")) + return; + for (Inkscape::XML::Node *cchild = rchild->firstChild() ; + cchild ; cchild = cchild->next()) + { + Glib::ustring ccName = cchild->name(); + Glib::ustring ccVal; + gatherText(cchild, ccVal); + //g_message("ccName: %s ccVal:%s", ccName.c_str(), ccVal.c_str()); + metadata[ccName] = ccVal; + } + return; + } + + //Now consider items. + SPObject *reprobj = doc->getObjectByRepr(node); + if (!reprobj) + { + return; + } + if (!SP_IS_ITEM(reprobj)) + { + return; + } + + if (nodeName == "image" || nodeName == "svg:image") { + Glib::ustring href = getAttribute(node, "xlink:href"); + if (href.size() > 0 && imageTable.count(href) == 0) { + try { + auto uri = Inkscape::URI(href.c_str(), docBaseUri.c_str()); + auto mimetype = uri.getMimeType(); + + if (mimetype.substr(0, 6) != "image/") { + return; + } + + auto ext = mimetype.substr(6); + auto newName = Glib::ustring("Pictures/image") + std::to_string(imageTable.size()) + "." + ext; + + imageTable[href] = newName; + + auto ze = zf.newEntry(newName.raw(), ""); + ze->setUncompressedData(uri.getContents()); + ze->finish(); + } catch (...) { + g_warning("Could not handle URI '%.100s'", href.c_str()); + } + } + } + + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + preprocess(zf, doc, child); +} + + +/** + * Writes the manifest. Currently it only changes according to the + * file names of images packed into the zip file. + */ +bool OdfOutput::writeManifest(ZipFile &zf) +{ + BufferOutputStream bouts; + OutputStreamWriter outs(bouts); + + time_t tim; + time(&tim); + + outs.writeString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + outs.writeString("<!DOCTYPE manifest:manifest PUBLIC \"-//OpenOffice.org//DTD Manifest 1.0//EN\" \"Manifest.dtd\">\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" file: manifest.xml\n"); + outs.printf (" Generated by Inkscape: %s", ctime(&tim)); //ctime has its own <cr> + outs.writeString(" http://www.inkscape.org\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("\n"); + outs.writeString("<manifest:manifest xmlns:manifest=\"urn:oasis:names:tc:opendocument:xmlns:manifest:1.0\">\n"); + outs.writeString(" <manifest:file-entry manifest:media-type=\"application/vnd.oasis.opendocument.graphics\" manifest:full-path=\"/\"/>\n"); + outs.writeString(" <manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"content.xml\"/>\n"); + outs.writeString(" <manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"styles.xml\"/>\n"); + outs.writeString(" <manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"meta.xml\"/>\n"); + outs.writeString(" <!--List our images here-->\n"); + std::map<Glib::ustring, Glib::ustring>::iterator iter; + for (iter = imageTable.begin() ; iter!=imageTable.end() ; ++iter) + { + Glib::ustring newName = iter->second; + + // note: mime subtype was added as file extension in OdfOutput::preprocess + Glib::ustring mimesubtype = Inkscape::IO::get_file_extension(newName); + + outs.printf(" <manifest:file-entry manifest:media-type=\""); + outs.printf("image/"); + outs.printf("%s", mimesubtype.c_str()); + outs.printf("\" manifest:full-path=\""); + outs.writeString(newName.c_str()); + outs.printf("\"/>\n"); + } + outs.printf("</manifest:manifest>\n"); + + outs.close(); + + //Make our entry + ZipEntry *ze = zf.newEntry("META-INF/manifest.xml", "ODF file manifest"); + ze->setUncompressedData(bouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * This writes the document meta information to meta.xml + */ +bool OdfOutput::writeMeta(ZipFile &zf) +{ + BufferOutputStream bouts; + OutputStreamWriter outs(bouts); + + time_t tim; + time(&tim); + + std::map<Glib::ustring, Glib::ustring>::iterator iter; + Glib::ustring InkscapeVersion = Glib::ustring("Inkscape.org - ") + Inkscape::version_string; + Glib::ustring creator = InkscapeVersion; + iter = metadata.find("dc:creator"); + if (iter != metadata.end()) + { + creator = iter->second; + } + + Glib::ustring date; + Glib::ustring moddate; + char buf [80]; + time_t rawtime; + struct tm * timeinfo; + time (&rawtime); + timeinfo = localtime (&rawtime); + strftime (buf,80,"%Y-%m-%d %H:%M:%S",timeinfo); + moddate = Glib::ustring(buf); + + iter = metadata.find("dc:date"); + if (iter != metadata.end()) + { + date = iter->second; + } + else + { + date = moddate; + } + + outs.writeString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" file: meta.xml\n"); + outs.printf (" Generated by Inkscape: %s", ctime(&tim)); //ctime has its own <cr> + outs.writeString(" http://www.inkscape.org\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("<office:document-meta\n"); + outs.writeString("xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\"\n"); + outs.writeString("xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"); + outs.writeString("xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"); + outs.writeString("xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\"\n"); + outs.writeString("xmlns:presentation=\"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0\"\n"); + outs.writeString("xmlns:ooo=\"http://openoffice.org/2004/office\"\n"); + outs.writeString("xmlns:smil=\"urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0\"\n"); + outs.writeString("xmlns:anim=\"urn:oasis:names:tc:opendocument:xmlns:animation:1.0\"\n"); + outs.writeString("office:version=\"1.0\">\n"); + outs.writeString("<office:meta>\n"); + Glib::ustring tmp = Glib::ustring::compose(" <meta:generator>%1</meta:generator>\n", InkscapeVersion); + tmp += Glib::ustring::compose(" <meta:initial-creator>%1</meta:initial-creator>\n", creator); + tmp += Glib::ustring::compose(" <meta:creation-date>%1</meta:creation-date>\n", date); + tmp += Glib::ustring::compose(" <dc:date>%1</dc:date>\n", moddate); + outs.writeUString(tmp); + for (iter = metadata.begin() ; iter != metadata.end() ; ++iter) + { + Glib::ustring name = iter->first; + Glib::ustring value = iter->second; + if (!name.empty() && !value.empty()) + { + tmp = Glib::ustring::compose(" <%1>%2</%3>\n", name, value, name); + outs.writeUString(tmp); + } + } + // outs.writeString(" <meta:editing-cycles>2</meta:editing-cycles>\n"); + // outs.writeString(" <meta:editing-duration>PT56S</meta:editing-duration>\n"); + // outs.writeString(" <meta:user-defined meta:name=\"Info 1\"/>\n"); + // outs.writeString(" <meta:user-defined meta:name=\"Info 2\"/>\n"); + // outs.writeString(" <meta:user-defined meta:name=\"Info 3\"/>\n"); + // outs.writeString(" <meta:user-defined meta:name=\"Info 4\"/>\n"); + // outs.writeString(" <meta:document-statistic meta:object-count=\"2\"/>\n"); + outs.writeString("</office:meta>\n"); + outs.writeString("</office:document-meta>\n"); + outs.close(); + + //Make our entry + ZipEntry *ze = zf.newEntry("meta.xml", "ODF info file"); + ze->setUncompressedData(bouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * Writes an SVG path as an ODF <draw:path> and returns the number of points written + */ +static int +writePath(Writer &outs, Geom::PathVector const &pathv, + Geom::Affine const &tf, double xoff, double yoff) +{ + using Geom::X; + using Geom::Y; + + int nrPoints = 0; + + // convert the path to only lineto's and cubic curveto's: + Geom::PathVector pv = pathv_to_linear_and_cubic_beziers(pathv * tf * Geom::Translate(xoff, yoff) * Geom::Scale(1000.)); + + for (const auto & pit : pv) { + + double destx = pit.initialPoint()[X]; + double desty = pit.initialPoint()[Y]; + if (fabs(destx)<1.0) destx = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(desty)<1.0) desty = 0.0; + outs.printf("M %.3f %.3f ", destx, desty); + nrPoints++; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) { + + if( is_straight_curve(*cit) ) + { + double destx = cit->finalPoint()[X]; + double desty = cit->finalPoint()[Y]; + if (fabs(destx)<1.0) destx = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(desty)<1.0) desty = 0.0; + outs.printf("L %.3f %.3f ", destx, desty); + } + else if(Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const*>(&*cit)) { + std::vector<Geom::Point> points = cubic->controlPoints(); + for (unsigned i = 1; i <= 3; i++) { + if (fabs(points[i][X])<1.0) points[i][X] = 0.0; // Why is this needed? Shouldn't we just round all numbers then? + if (fabs(points[i][Y])<1.0) points[i][Y] = 0.0; + } + outs.printf("C %.3f %.3f %.3f %.3f %.3f %.3f ", points[1][X],points[1][Y], points[2][X],points[2][Y], points[3][X],points[3][Y]); + } + else { + g_error ("logical error, because pathv_to_linear_and_cubic_beziers was used"); + } + + nrPoints++; + } + + if (pit.closed()) { + outs.printf("Z"); + } + } + + return nrPoints; +} + +bool OdfOutput::processStyle(SPItem *item, const Glib::ustring &id, const Glib::ustring &gradientNameFill, const Glib::ustring &gradientNameStroke, Glib::ustring& output) +{ + output.clear(); + if (!item) + { + return false; + } + + SPStyle *style = item->style; + if (!style) + { + return false; + } + + StyleInfo si; + + // FILL + if (style->fill.isColor()) + { + guint32 fillCol = style->fill.value.color.toRGBA32( 0 ); + char buf[16]; + int r = (fillCol >> 24) & 0xff; + int g = (fillCol >> 16) & 0xff; + int b = (fillCol >> 8) & 0xff; + snprintf(buf, 15, "#%02x%02x%02x", r, g, b); + si.fillColor = buf; + si.fill = "solid"; + double opacityPercent = 100.0 * + (SP_SCALE24_TO_FLOAT(style->fill_opacity.value)); + snprintf(buf, 15, "%.3f%%", opacityPercent); + si.fillOpacity = buf; + } + else if (style->fill.isPaintserver()) + { + SPGradient *gradient = SP_GRADIENT(SP_STYLE_FILL_SERVER(style)); + if (gradient) + { + si.fill = "gradient"; + } + } + + // STROKE + if (style->stroke.isColor()) + { + guint32 strokeCol = style->stroke.value.color.toRGBA32( 0 ); + char buf[16]; + int r = (strokeCol >> 24) & 0xff; + int g = (strokeCol >> 16) & 0xff; + int b = (strokeCol >> 8) & 0xff; + snprintf(buf, 15, "#%02x%02x%02x", r, g, b); + si.strokeColor = buf; + snprintf(buf, 15, "%.3fpt", style->stroke_width.value); + si.strokeWidth = buf; + si.stroke = "solid"; + double opacityPercent = 100.0 * + (SP_SCALE24_TO_FLOAT(style->stroke_opacity.value)); + snprintf(buf, 15, "%.3f%%", opacityPercent); + si.strokeOpacity = buf; + } + else if (style->stroke.isPaintserver()) + { + SPGradient *gradient = SP_GRADIENT(SP_STYLE_STROKE_SERVER(style)); + if (gradient) + { + si.stroke = "gradient"; + } + } + + //Look for existing identical style; + bool styleMatch = false; + std::vector<StyleInfo>::iterator iter; + for (iter=styleTable.begin() ; iter!=styleTable.end() ; ++iter) + { + if (si.equals(*iter)) + { + //map to existing styleTable entry + Glib::ustring styleName = iter->name; + styleLookupTable[id] = styleName; + styleMatch = true; + break; + } + } + + // Don't need a new style + if (styleMatch) + { + return false; + } + + Glib::ustring styleName = Glib::ustring::compose("style%1", styleTable.size()); + si.name = styleName; + styleTable.push_back(si); + styleLookupTable[id] = styleName; + + output = Glib::ustring::compose ("<style:style style:name=\"%1\" style:family=\"graphic\" style:parent-style-name=\"standard\">\n", si.name); + output += "<style:graphic-properties"; + if (si.fill == "gradient") + { + output += Glib::ustring::compose (" draw:fill=\"gradient\" draw:fill-gradient-name=\"%1\"", gradientNameFill); + } + else + { + output += Glib::ustring(" draw:fill=\"") + si.fill + "\""; + if(si.fill != "none") + { + output += Glib::ustring::compose(" draw:fill-color=\"%1\"", si.fillColor); + } + } + if (si.stroke == "gradient") + { + //does not seem to be supported by Open Office.org + output += Glib::ustring::compose (" draw:stroke=\"gradient\" draw:stroke-gradient-name=\"%1\"", gradientNameStroke); + } + else + { + output += Glib::ustring(" draw:stroke=\"") + si.stroke + "\""; + if (si.stroke != "none") + { + output += Glib::ustring::compose (" svg:stroke-width=\"%1\" svg:stroke-color=\"%2\" ", si.strokeWidth, si.strokeColor); + } + } + output += "/>\n</style:style>\n"; + + return true; +} + +bool OdfOutput::processGradient(SPItem *item, + const Glib::ustring &id, Geom::Affine &/*tf*/, + Glib::ustring& gradientName, Glib::ustring& output, bool checkFillGradient) +{ + output.clear(); + if (!item) + { + return false; + } + + SPStyle *style = item->style; + if (!style) + { + return false; + } + + if ((checkFillGradient? (!style->fill.isPaintserver()) : (!style->stroke.isPaintserver()))) + { + return false; + } + + //## Gradient + SPGradient *gradient = SP_GRADIENT((checkFillGradient?(SP_STYLE_FILL_SERVER(style)) :(SP_STYLE_STROKE_SERVER(style)))); + + if (gradient == nullptr) + { + return false; + } + GradientInfo gi; + SPGradient *grvec = gradient->getVector(FALSE); + for (SPStop *stop = grvec->getFirstStop(); + stop ; stop = stop->getNextStop()) + { + unsigned long rgba = stop->get_rgba32(); + unsigned long rgb = (rgba >> 8) & 0xffffff; + double opacity = (static_cast<double>(rgba & 0xff)) / 256.0; + GradientStop gs(rgb, opacity); + gi.stops.push_back(gs); + } + + Glib::ustring gradientName2; + if (SP_IS_LINEARGRADIENT(gradient)) + { + gi.style = "linear"; + SPLinearGradient *linGrad = SP_LINEARGRADIENT(gradient); + gi.x1 = linGrad->x1.value; + gi.y1 = linGrad->y1.value; + gi.x2 = linGrad->x2.value; + gi.y2 = linGrad->y2.value; + gradientName2 = Glib::ustring::compose("ImportedLinearGradient%1", gradientTable.size()); + } + else if (SP_IS_RADIALGRADIENT(gradient)) + { + gi.style = "radial"; + SPRadialGradient *radGrad = SP_RADIALGRADIENT(gradient); + Geom::OptRect bbox = item->documentVisualBounds(); + gi.cx = (radGrad->cx.value-bbox->left())/bbox->width(); + gi.cy = (radGrad->cy.value-bbox->top())/bbox->height(); + gradientName2 = Glib::ustring::compose("ImportedRadialGradient%1", gradientTable.size()); + } + else + { + g_warning("not a supported gradient type"); + return false; + } + + //Look for existing identical style; + bool gradientMatch = false; + std::vector<GradientInfo>::iterator iter; + for (iter=gradientTable.begin() ; iter!=gradientTable.end() ; ++iter) + { + if (gi.equals(*iter)) + { + //map to existing gradientTable entry + gradientName = iter->name; + gradientLookupTable[id] = gradientName; + gradientMatch = true; + break; + } + } + + if (gradientMatch) + { + return true; + } + + // No match, let us write a new entry + gradientName = gradientName2; + gi.name = gradientName; + gradientTable.push_back(gi); + gradientLookupTable[id] = gradientName; + + // int gradientCount = gradientTable.size(); + char buf[128]; + if (gi.style == "linear") + { + /* + =================================================================== + LINEAR gradient. We need something that looks like this: + <draw:gradient draw:name="Gradient_20_7" + draw:display-name="Gradient 7" + draw:style="linear" + draw:start-color="#008080" draw:end-color="#993366" + draw:start-intensity="100%" draw:end-intensity="100%" + draw:angle="150" draw:border="0%"/> + =================================================================== + */ + if (gi.stops.size() < 2) + { + g_warning("Need at least 2 stops for a linear gradient"); + return false; + } + output += Glib::ustring::compose("<draw:gradient draw:name=\"%1\"", gi.name); + output += Glib::ustring::compose(" draw:display-name=\"%1\"", gi.name); + output += " draw:style=\"linear\""; + snprintf(buf, 127, " draw:start-color=\"#%06lx\" draw:end-color=\"#%06lx\"", gi.stops[0].rgb, gi.stops[1].rgb); + output += buf; + //TODO: apply maths, to define begin of gradient, taking gradient begin and end, as well as object boundary into account + double angle = (gi.y2-gi.y1); + angle = (angle != 0.) ? (atan((gi.x2-gi.x1)/(gi.y2-gi.y1))* 180. / M_PI) : 90; + angle = (angle < 0)?(180+angle):angle; + angle = angle * 10; //why do we need this: precision????????????? + output += Glib::ustring::compose(" draw:start-intensity=\"%1\" draw:end-intensity=\"%2\" draw:angle=\"%3\"/>\n", + gi.stops[0].opacity * 100.0, gi.stops[1].opacity * 100.0, angle);// draw:border=\"0%%\" + } + else if (gi.style == "radial") + { + /* + =================================================================== + RADIAL gradient. We need something that looks like this: + <!-- radial gradient, light gray to white, centered, 0% border --> + <draw:gradient draw:name="radial_20_borderless" + draw:display-name="radial borderless" + draw:style="radial" + draw:cx="50%" draw:cy="50%" + draw:start-color="#999999" draw:end-color="#ffffff" + draw:border="0%"/> + =================================================================== + */ + if (gi.stops.size() < 2) + { + g_warning("Need at least 2 stops for a radial gradient"); + return false; + } + output += Glib::ustring::compose("<draw:gradient draw:name=\"%1\" draw:display-name=\"%1\" ", gi.name); + snprintf(buf, 127, "draw:cx=\"%05.3f\" draw:cy=\"%05.3f\" ", gi.cx*100, gi.cy*100); + output += Glib::ustring("draw:style=\"radial\" ") + buf; + snprintf(buf, 127, "draw:start-color=\"#%06lx\" draw:end-color=\"#%06lx\" ", gi.stops[0].rgb, gi.stops[1].rgb); + output += buf; + snprintf(buf, 127, "draw:start-intensity=\"%f%%\" draw:end-intensity=\"%f%%\" ", gi.stops[0].opacity*100.0, gi.stops[1].opacity*100.0); + output += buf; + output += "/>\n";//draw:border=\"0%\" + } + else + { + g_warning("unsupported gradient style '%s'", gi.style.c_str()); + return false; + } + return true; +} + + +/** + * SECOND PASS. + * This is the main SPObject tree output to ODF. + */ +bool OdfOutput::writeTree(Writer &couts, Writer &souts, + SPDocument *doc, + Inkscape::XML::Node *node) +{ + //# Get the SPItem, if applicable + SPObject *reprobj = doc->getObjectByRepr(node); + if (!reprobj) + { + return true; + } + if (!SP_IS_ITEM(reprobj)) + { + return true; + } + SPItem *item = SP_ITEM(reprobj); + + Glib::ustring nodeName = node->name(); + Glib::ustring id = getAttribute(node, "id"); + Geom::Affine tf = getODFTransform(item);//Get SVG-to-ODF transform + Geom::OptRect bbox = getODFBoundingBox(item);//Get ODF bounding box params for item + if (!bbox) { + return true; + } + + double bbox_x = bbox->min()[Geom::X]; + double bbox_y = bbox->min()[Geom::Y]; + double bbox_width = (*bbox)[Geom::X].extent(); + double bbox_height = (*bbox)[Geom::Y].extent(); + + double rotate; + double xskew; + double yskew; + double xscale; + double yscale; + analyzeTransform(tf, rotate, xskew, yskew, xscale, yscale); + + //# Do our stuff + + if (nodeName == "svg" || nodeName == "svg:svg") + { + //# Iterate through the children + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + { + if (!writeTree(couts, souts, doc, child)) + { + return false; + } + } + return true; + } + else if (nodeName == "g" || nodeName == "svg:g") + { + if (!id.empty()) + { + couts.printf("<draw:g id=\"%s\">\n", id.c_str()); + } + else + { + couts.printf("<draw:g>\n"); + } + //# Iterate through the children + for (Inkscape::XML::Node *child = node->firstChild() ; + child ; child = child->next()) + { + if (!writeTree(couts, souts, doc, child)) + { + return false; + } + } + if (!id.empty()) + { + couts.printf("</draw:g> <!-- id=\"%s\" -->\n", id.c_str()); + } + else + { + couts.printf("</draw:g>\n"); + } + return true; + } + + //# GRADIENT + Glib::ustring gradientNameFill; + Glib::ustring gradientNameStroke; + Glib::ustring outputFill; + Glib::ustring outputStroke; + Glib::ustring outputStyle; + + processGradient(item, id, tf, gradientNameFill, outputFill, true); + processGradient(item, id, tf, gradientNameStroke, outputStroke, false); + souts.writeUString(outputFill); + souts.writeUString(outputStroke); + + //# STYLE + processStyle(item, id, gradientNameFill, gradientNameStroke, outputStyle); + souts.writeUString(outputStyle); + + //# ITEM DATA + if (nodeName == "image" || nodeName == "svg:image") + { + if (!SP_IS_IMAGE(item)) + { + g_warning("<image> is not an SPImage."); + return false; + } + + SPImage *img = SP_IMAGE(item); + double ix = img->x.value; + double iy = img->y.value; + double iwidth = img->width.value; + double iheight = img->height.value; + + Geom::Point ibbox_min = Geom::Point(ix, iy) * tf; + ix = ibbox_min.x(); + iy = ibbox_min.y(); + iwidth = xscale * iwidth; + iheight = yscale * iheight; + + Geom::Affine itemTransform = getODFItemTransform(item); + + Glib::ustring itemTransformString = formatTransform(itemTransform); + + Glib::ustring href = getAttribute(node, "xlink:href"); + std::map<Glib::ustring, Glib::ustring>::iterator iter = imageTable.find(href); + if (iter == imageTable.end()) + { + g_warning("image '%s' not in table", href.c_str()); + return false; + } + Glib::ustring newName = iter->second; + + couts.printf("<draw:frame "); + if (!id.empty()) + { + couts.printf("id=\"%s\" ", id.c_str()); + } + couts.printf("draw:style-name=\"gr1\" draw:text-style-name=\"P1\" draw:layer=\"layout\" "); + //no x or y. make them the translate transform, last one + couts.printf("svg:width=\"%.3fcm\" svg:height=\"%.3fcm\" ", + iwidth, iheight); + if (!itemTransformString.empty()) + { + couts.printf("draw:transform=\"%s translate(%.3fcm, %.3fcm)\" ", + itemTransformString.c_str(), ix, iy); + } + else + { + couts.printf("draw:transform=\"translate(%.3fcm, %.3fcm)\" ", ix, iy); + } + + couts.writeString(">\n"); + couts.printf(" <draw:image xlink:href=\"%s\" xlink:type=\"simple\"\n", + newName.c_str()); + couts.writeString(" xlink:show=\"embed\" xlink:actuate=\"onLoad\">\n"); + couts.writeString(" <text:p/>\n"); + couts.writeString(" </draw:image>\n"); + couts.writeString("</draw:frame>\n"); + return true; + } + + std::unique_ptr<SPCurve> curve; + + if (auto shape = dynamic_cast<SPShape const *>(item)) { + curve = SPCurve::copy(shape->curve()); + } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + curve = te_get_layout(item)->convertToCurves(); + } + + if (curve) + { + //### Default <path> output + couts.writeString("<draw:path "); + if (!id.empty()) + { + couts.printf("id=\"%s\" ", id.c_str()); + } + + std::map<Glib::ustring, Glib::ustring>::iterator siter; + siter = styleLookupTable.find(id); + if (siter != styleLookupTable.end()) + { + Glib::ustring styleName = siter->second; + couts.printf("draw:style-name=\"%s\" ", styleName.c_str()); + } + + couts.printf("draw:layer=\"layout\" svg:x=\"%.3fcm\" svg:y=\"%.3fcm\" ", + bbox_x, bbox_y); + couts.printf("svg:width=\"%.3fcm\" svg:height=\"%.3fcm\" ", + bbox_width, bbox_height); + couts.printf("svg:viewBox=\"0.0 0.0 %.3f %.3f\"", + bbox_width * 1000.0, bbox_height * 1000.0); + + couts.printf(" svg:d=\""); + int nrPoints = writePath(couts, curve->get_pathvector(), + tf, bbox_x, bbox_y); + couts.writeString("\""); + + couts.writeString(">\n"); + couts.printf(" <!-- %d nodes -->\n", nrPoints); + couts.writeString("</draw:path>\n\n"); + } + + return true; +} + + +/** + * Write the header for the content.xml file + */ +bool OdfOutput::writeStyleHeader(Writer &outs) +{ + time_t tim; + time(&tim); + + outs.writeString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" file: styles.xml\n"); + outs.printf (" Generated by Inkscape: %s", ctime(&tim)); //ctime has its own <cr> + outs.writeString(" http://www.inkscape.org\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("<office:document-styles\n"); + outs.writeString(" xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\"\n"); + outs.writeString(" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\"\n"); + outs.writeString(" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\"\n"); + outs.writeString(" xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\"\n"); + outs.writeString(" xmlns:draw=\"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\"\n"); + outs.writeString(" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\"\n"); + outs.writeString(" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"); + outs.writeString(" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"); + outs.writeString(" xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\"\n"); + outs.writeString(" xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\"\n"); + outs.writeString(" xmlns:presentation=\"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0\"\n"); + outs.writeString(" xmlns:svg=\"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0\"\n"); + outs.writeString(" xmlns:chart=\"urn:oasis:names:tc:opendocument:xmlns:chart:1.0\"\n"); + outs.writeString(" xmlns:dr3d=\"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0\"\n"); + outs.writeString(" xmlns:math=\"http://www.w3.org/1998/Math/MathML\"\n"); + outs.writeString(" xmlns:form=\"urn:oasis:names:tc:opendocument:xmlns:form:1.0\"\n"); + outs.writeString(" xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\"\n"); + outs.writeString(" xmlns:ooo=\"http://openoffice.org/2004/office\"\n"); + outs.writeString(" xmlns:ooow=\"http://openoffice.org/2004/writer\"\n"); + outs.writeString(" xmlns:oooc=\"http://openoffice.org/2004/calc\"\n"); + outs.writeString(" xmlns:dom=\"http://www.w3.org/2001/xml-events\"\n"); + outs.writeString(" xmlns:xforms=\"http://www.w3.org/2002/xforms\"\n"); + outs.writeString(" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n"); + outs.writeString(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"); + outs.writeString(" xmlns:smil=\"urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0\"\n"); + outs.writeString(" xmlns:anim=\"urn:oasis:names:tc:opendocument:xmlns:animation:1.0\"\n"); + outs.writeString(" office:version=\"1.0\">\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" S T Y L E S\n"); + outs.writeString(" Style entries have been pulled from the svg style and\n"); + outs.writeString(" representation attributes in the SVG tree. The tree elements\n"); + outs.writeString(" then refer to them by name, in the ODF manner\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("<office:styles>\n"); + outs.writeString("\n"); + + return true; +} + + +/** + * Write the footer for the style.xml file + */ +bool OdfOutput::writeStyleFooter(Writer &outs) +{ + outs.writeString("\n"); + outs.writeString("</office:styles>\n"); + outs.writeString("\n"); + outs.writeString("<office:automatic-styles>\n"); + outs.writeString("<!-- ####### 'Standard' styles ####### -->\n"); + outs.writeString("<style:style style:name=\"dp1\" style:family=\"drawing-page\"/>\n"); + outs.writeString("<style:style style:name=\"standard\" style:family=\"graphic\">\n"); + +///TODO: add default document style here + + outs.writeString("</style:style>\n"); + outs.writeString("<style:style style:name=\"gr1\" style:family=\"graphic\" style:parent-style-name=\"standard\">\n"); + outs.writeString(" <style:graphic-properties draw:stroke=\"none\" draw:fill=\"none\"\n"); + outs.writeString(" draw:textarea-horizontal-align=\"center\"\n"); + outs.writeString(" draw:textarea-vertical-align=\"middle\" draw:color-mode=\"standard\"\n"); + outs.writeString(" draw:luminance=\"0%\" draw:contrast=\"0%\" draw:gamma=\"100%\" draw:red=\"0%\"\n"); + outs.writeString(" draw:green=\"0%\" draw:blue=\"0%\" fo:clip=\"rect(0cm 0cm 0cm 0cm)\"\n"); + outs.writeString(" draw:image-opacity=\"100%\" style:mirror=\"none\"/>\n"); + outs.writeString("</style:style>\n"); + outs.writeString("<style:style style:name=\"P1\" style:family=\"paragraph\">\n"); + outs.writeString(" <style:paragraph-properties fo:text-align=\"center\"/>\n"); + outs.writeString("</style:style>\n"); + outs.writeString("</office:automatic-styles>\n"); + outs.writeString("\n"); + outs.writeString("<office:master-styles>\n"); + outs.writeString("<draw:layer-set>\n"); + outs.writeString(" <draw:layer draw:name=\"layout\"/>\n"); + outs.writeString(" <draw:layer draw:name=\"background\"/>\n"); + outs.writeString(" <draw:layer draw:name=\"backgroundobjects\"/>\n"); + outs.writeString(" <draw:layer draw:name=\"controls\"/>\n"); + outs.writeString(" <draw:layer draw:name=\"measurelines\"/>\n"); + outs.writeString("</draw:layer-set>\n"); + outs.writeString("\n"); + outs.writeString("<style:master-page style:name=\"Default\"\n"); + outs.writeString(" style:page-master-name=\"PM1\" draw:style-name=\"dp1\"/>\n"); + outs.writeString("</office:master-styles>\n"); + outs.writeString("\n"); + outs.writeString("</office:document-styles>\n"); + + return true; +} + + +/** + * Write the header for the content.xml file + */ +bool OdfOutput::writeContentHeader(Writer &outs) +{ + time_t tim; + time(&tim); + + outs.writeString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" file: content.xml\n"); + outs.printf (" Generated by Inkscape: %s", ctime(&tim)); //ctime has its own <cr> + outs.writeString(" http://www.inkscape.org\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("<office:document-content\n"); + outs.writeString(" xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\"\n"); + outs.writeString(" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\"\n"); + outs.writeString(" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\"\n"); + outs.writeString(" xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\"\n"); + outs.writeString(" xmlns:draw=\"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\"\n"); + outs.writeString(" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\"\n"); + outs.writeString(" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"); + outs.writeString(" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"); + outs.writeString(" xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\"\n"); + outs.writeString(" xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\"\n"); + outs.writeString(" xmlns:presentation=\"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0\"\n"); + outs.writeString(" xmlns:svg=\"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0\"\n"); + outs.writeString(" xmlns:chart=\"urn:oasis:names:tc:opendocument:xmlns:chart:1.0\"\n"); + outs.writeString(" xmlns:dr3d=\"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0\"\n"); + outs.writeString(" xmlns:math=\"http://www.w3.org/1998/Math/MathML\"\n"); + outs.writeString(" xmlns:form=\"urn:oasis:names:tc:opendocument:xmlns:form:1.0\"\n"); + outs.writeString(" xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\"\n"); + outs.writeString(" xmlns:ooo=\"http://openoffice.org/2004/office\"\n"); + outs.writeString(" xmlns:ooow=\"http://openoffice.org/2004/writer\"\n"); + outs.writeString(" xmlns:oooc=\"http://openoffice.org/2004/calc\"\n"); + outs.writeString(" xmlns:dom=\"http://www.w3.org/2001/xml-events\"\n"); + outs.writeString(" xmlns:xforms=\"http://www.w3.org/2002/xforms\"\n"); + outs.writeString(" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n"); + outs.writeString(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"); + outs.writeString(" xmlns:smil=\"urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0\"\n"); + outs.writeString(" xmlns:anim=\"urn:oasis:names:tc:opendocument:xmlns:animation:1.0\"\n"); + outs.writeString(" office:version=\"1.0\">\n"); + outs.writeString("<office:scripts/>\n"); + outs.writeString("\n"); + outs.writeString("<!--\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString(" D R A W I N G\n"); + outs.writeString(" This section is the heart of SVG-ODF conversion. We are\n"); + outs.writeString(" starting with simple conversions, and will slowly evolve\n"); + outs.writeString(" into a 'smarter' translation as time progresses. Any help\n"); + outs.writeString(" in improving .odg export is welcome.\n"); + outs.writeString("*************************************************************************\n"); + outs.writeString("-->\n"); + outs.writeString("\n"); + outs.writeString("<office:body>\n"); + outs.writeString("<office:drawing>\n"); + outs.writeString("<draw:page draw:name=\"page1\" draw:style-name=\"dp1\"\n"); + outs.writeString(" draw:master-page-name=\"Default\">\n"); + outs.writeString("\n"); + return true; +} + + +/** + * Write the footer for the content.xml file + */ +bool OdfOutput::writeContentFooter(Writer &outs) +{ + outs.writeString("\n"); + outs.writeString("</draw:page>\n"); + outs.writeString("</office:drawing>\n"); + outs.writeString("\n"); + outs.writeString("<!-- ######### CONVERSION FROM SVG ENDS ######## -->\n"); + outs.writeString("\n"); + outs.writeString("</office:body>\n"); + outs.writeString("</office:document-content>\n"); + return true; +} + + +/** + * Write the content.xml file. Writes the namespace headers, then + * calls writeTree(). + */ +bool OdfOutput::writeContent(ZipFile &zf, SPDocument *doc) +{ + //Content.xml stream + BufferOutputStream cbouts; + OutputStreamWriter couts(cbouts); + + if (!writeContentHeader(couts)) + { + return false; + } + + //Style.xml stream + BufferOutputStream sbouts; + OutputStreamWriter souts(sbouts); + + if (!writeStyleHeader(souts)) + { + return false; + } + + //# Descend into the tree, doing all of our conversions + //# to both files at the same time + char *oldlocale = g_strdup (setlocale (LC_NUMERIC, nullptr)); + setlocale (LC_NUMERIC, "C"); + if (!writeTree(couts, souts, doc, doc->getReprRoot())) + { + g_warning("Failed to convert SVG tree"); + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + return false; + } + setlocale (LC_NUMERIC, oldlocale); + g_free (oldlocale); + + //# Finish content file + if (!writeContentFooter(couts)) + { + return false; + } + + ZipEntry *ze = zf.newEntry("content.xml", "ODF master content file"); + ze->setUncompressedData(cbouts.getBuffer()); + ze->finish(); + + //# Finish style file + if (!writeStyleFooter(souts)) + { + return false; + } + + ze = zf.newEntry("styles.xml", "ODF style file"); + ze->setUncompressedData(sbouts.getBuffer()); + ze->finish(); + + return true; +} + + +/** + * Resets class to its pristine condition, ready to use again + */ +void OdfOutput::reset() +{ + metadata.clear(); + styleTable.clear(); + styleLookupTable.clear(); + gradientTable.clear(); + gradientLookupTable.clear(); + imageTable.clear(); +} + + +/** + * Descends into the SVG tree, mapping things to ODF when appropriate + */ +void OdfOutput::save(Inkscape::Extension::Output */*mod*/, SPDocument *doc, gchar const *filename) +{ + reset(); + + docBaseUri = Inkscape::URI::from_dirname(doc->getDocumentBase()).str(); + + ZipFile zf; + preprocess(zf, doc, doc->getReprRoot()); + + if (!writeManifest(zf)) + { + g_warning("Failed to write manifest"); + return; + } + + if (!writeContent(zf, doc)) + { + g_warning("Failed to write content"); + return; + } + + if (!writeMeta(zf)) + { + g_warning("Failed to write metafile"); + return; + } + + if (!zf.writeFile(filename)) + { + return; + } +} + + +/** + * This is the definition of PovRay output. This function just + * calls the extension system with the memory allocated XML that + * describes the data. +*/ +void OdfOutput::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("OpenDocument Drawing Output") "</name>\n" + "<id>org.inkscape.output.odf</id>\n" + "<output>\n" + "<extension>.odg</extension>\n" + "<mimetype>text/x-povray-script</mimetype>\n" + "<filetypename>" N_("OpenDocument drawing (*.odg)") "</filetypename>\n" + "<filetypetooltip>" N_("OpenDocument drawing file") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", + new OdfOutput()); + // clang-format on +} + +/** + * Make sure that we are in the database + */ +bool OdfOutput::check (Inkscape::Extension::Extension */*module*/) +{ + /* We don't need a Key + if (NULL == Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_POV)) + return FALSE; + */ + + return TRUE; +} + +} //namespace Internal +} //namespace Extension +} //namespace Inkscape + + +//######################################################################## +//# E N D O F F I L E +//######################################################################## + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/odf.h b/src/extension/internal/odf.h new file mode 100644 index 0000000..236687d --- /dev/null +++ b/src/extension/internal/odf.h @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/** @file + * OpenDocument (drawing) input and output + *//* + * Authors: + * Bob Jamison + * Abhishek Sharma + * + * Copyright (C) 2018 Authors + * Released under GNU LGPL v2.1+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_ODG_OUT_H +#define EXTENSION_INTERNAL_ODG_OUT_H + +#include <util/ziptool.h> + +#include "extension/implementation/implementation.h" + +#include <xml/repr.h> +#include <string> +#include <map> + +#include "object/uri.h" +class SPItem; + +#include <glibmm/ustring.h> + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + +typedef Inkscape::IO::Writer Writer; + +class StyleInfo +{ +public: + + StyleInfo() + { + init(); + } + + StyleInfo(const StyleInfo &other) + { + assign(other); + } + + StyleInfo &operator=(const StyleInfo &other) + { + assign(other); + return *this; + } + + void assign(const StyleInfo &other) + { + name = other.name; + stroke = other.stroke; + strokeColor = other.strokeColor; + strokeWidth = other.strokeWidth; + strokeOpacity = other.strokeOpacity; + fill = other.fill; + fillColor = other.fillColor; + fillOpacity = other.fillOpacity; + } + + void init() + { + name = "none"; + stroke = "none"; + strokeColor = "none"; + strokeWidth = "none"; + strokeOpacity = "none"; + fill = "none"; + fillColor = "none"; + fillOpacity = "none"; + } + + virtual ~StyleInfo() + = default; + + //used for eliminating duplicates in the styleTable + bool equals(const StyleInfo &other) + { + if ( + stroke != other.stroke || + strokeColor != other.strokeColor || + strokeWidth != other.strokeWidth || + strokeOpacity != other.strokeOpacity || + fill != other.fill || + fillColor != other.fillColor || + fillOpacity != other.fillOpacity + ) + return false; + return true; + } + + Glib::ustring name; + Glib::ustring stroke; + Glib::ustring strokeColor; + Glib::ustring strokeWidth; + Glib::ustring strokeOpacity; + Glib::ustring fill; + Glib::ustring fillColor; + Glib::ustring fillOpacity; + +}; + + + + +class GradientStop +{ +public: + GradientStop() : rgb(0), opacity(0) + {} + GradientStop(unsigned long rgbArg, double opacityArg) + { rgb = rgbArg; opacity = opacityArg; } + virtual ~GradientStop() + = default; + GradientStop(const GradientStop &other) + { assign(other); } + virtual GradientStop& operator=(const GradientStop &other) + { assign(other); return *this; } + void assign(const GradientStop &other) + { + rgb = other.rgb; + opacity = other.opacity; + } + unsigned long rgb; + double opacity; +}; + + + +class GradientInfo +{ +public: + + GradientInfo() + { + init(); + } + + GradientInfo(const GradientInfo &other) + { + assign(other); + } + + GradientInfo &operator=(const GradientInfo &other) + { + assign(other); + return *this; + } + + void assign(const GradientInfo &other) + { + name = other.name; + style = other.style; + cx = other.cx; + cy = other.cy; + fx = other.fx; + fy = other.fy; + r = other.r; + x1 = other.x1; + y1 = other.y1; + x2 = other.x2; + y2 = other.y2; + stops = other.stops; + } + + void init() + { + name = "none"; + style = "none"; + cx = 0.0; + cy = 0.0; + fx = 0.0; + fy = 0.0; + r = 0.0; + x1 = 0.0; + y1 = 0.0; + x2 = 0.0; + y2 = 0.0; + stops.clear(); + } + + virtual ~GradientInfo() + = default; + + //used for eliminating duplicates in the styleTable + bool equals(const GradientInfo &other) + { + if ( + name != other.name || + style != other.style || + cx != other.cx || + cy != other.cy || + fx != other.fx || + fy != other.fy || + r != other.r || + x1 != other.x1 || + y1 != other.y1 || + x2 != other.x2 || + y2 != other.y2 + ) + return false; + if (stops.size() != other.stops.size()) + return false; + for (unsigned int i=0 ; i<stops.size() ; i++) + { + GradientStop g1 = stops[i]; + GradientStop g2 = other.stops[i]; + if (g1.rgb != g2.rgb) + return false; + if (g1.opacity != g2.opacity) + return false; + } + return true; + } + + Glib::ustring name; + Glib::ustring style; + double cx; + double cy; + double fx; + double fy; + double r; + double x1; + double y1; + double x2; + double y2; + std::vector<GradientStop> stops; + +}; + + + +/** + * OpenDocument <drawing> input and output + * + * This is an an entry in the extensions mechanism to begin to enable + * the inputting and outputting of OpenDocument Format (ODF) files from + * within Inkscape. Although the initial implementations will be very lossy + * do to the differences in the models of SVG and ODF, they will hopefully + * improve greatly with time. + * + * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/idl-definitions.html + */ +class OdfOutput : public Inkscape::Extension::Implementation::Implementation +{ + +public: + + bool check (Inkscape::Extension::Extension * module) override; + + void save (Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename) override; + + static void init (); + +private: + + std::string docBaseUri; + + void reset(); + + //cc or dc metadata name/value pairs + std::map<Glib::ustring, Glib::ustring> metadata; + + /* Style table + Uses a two-stage lookup to avoid style duplication. + Use like: + StyleInfo si = styleTable[styleLookupTable[id]]; + but check for errors, of course + */ + //element id -> style entry name + std::map<Glib::ustring, Glib::ustring> styleLookupTable; + //style entry name -> style info + std::vector<StyleInfo> styleTable; + + //element id -> gradient entry name + std::map<Glib::ustring, Glib::ustring> gradientLookupTable; + //gradient entry name -> gradient info + std::vector<GradientInfo> gradientTable; + + //for renaming image file names + std::map<Glib::ustring, Glib::ustring> imageTable; + + void preprocess(ZipFile &zf, SPDocument *doc, Inkscape::XML::Node *node); + + bool writeManifest(ZipFile &zf); + + bool writeMeta(ZipFile &zf); + + bool writeStyle(ZipFile &zf); + + bool processStyle(SPItem *item, const Glib::ustring &id, const Glib::ustring &gradientNameFill, const Glib::ustring &gradientNameStroke, Glib::ustring& output); + + bool processGradient(SPItem *item, + const Glib::ustring &id, Geom::Affine &tf, Glib::ustring& gradientName, Glib::ustring& output, bool checkFillGradient = true); + + bool writeStyleHeader(Writer &outs); + + bool writeStyleFooter(Writer &outs); + + bool writeContentHeader(Writer &outs); + + bool writeContentFooter(Writer &outs); + + bool writeTree(Writer &couts, Writer &souts, SPDocument *doc, Inkscape::XML::Node *node); + + bool writeContent(ZipFile &zf, SPDocument *doc); + +}; + + +} //namespace Internal +} //namespace Extension +} //namespace Inkscape + + + +#endif /* EXTENSION_INTERNAL_ODG_OUT_H */ + diff --git a/src/extension/internal/pdfinput/pdf-input.cpp b/src/extension/internal/pdfinput/pdf-input.cpp new file mode 100644 index 0000000..cdf2237 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.cpp @@ -0,0 +1,1003 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Abhishek Sharma + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include "pdf-input.h" + +#ifdef HAVE_POPPLER +#include <poppler/goo/GooString.h> +#include <poppler/ErrorCodes.h> +#include <poppler/GlobalParams.h> +#include <poppler/PDFDoc.h> +#include <poppler/Page.h> +#include <poppler/Catalog.h> + +#ifdef HAVE_POPPLER_CAIRO +#include <poppler/glib/poppler.h> +#include <poppler/glib/poppler-document.h> +#include <poppler/glib/poppler-page.h> +#endif + +#include <gdkmm/general.h> +#include <glibmm/convert.h> +#include <glibmm/i18n.h> +#include <glibmm/miscutils.h> +#include <gtk/gtk.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/drawingarea.h> +#include <gtkmm/frame.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/scale.h> +#include <utility> + +#include "document-undo.h" +#include "extension/input.h" +#include "extension/system.h" +#include "inkscape.h" +#include "object/sp-root.h" +#include "pdf-parser.h" +#include "ui/dialog-events.h" +#include "ui/widget/frame.h" +#include "ui/widget/spinbutton.h" +#include "util/units.h" + + + +namespace { + +void sanitize_page_number(int &page_num, const int num_pages) { + if (page_num < 1 || page_num > num_pages) { + std::cerr << "Inkscape::Extension::Internal::PdfInput::open: Bad page number " + << page_num + << ". Import first page instead." + << std::endl; + page_num = 1; + } +} + +} + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + * \brief The PDF import dialog + * FIXME: Probably this should be placed into src/ui/dialog + */ + +static const gchar * crop_setting_choices[] = { + //TRANSLATORS: The following are document crop settings for PDF import + // more info: http://www.acrobatusers.com/tech_corners/javascript_corner/tips/2006/page_bounds/ + N_("media box"), + N_("crop box"), + N_("trim box"), + N_("bleed box"), + N_("art box") +}; + +PdfImportDialog::PdfImportDialog(std::shared_ptr<PDFDoc> doc, const gchar */*uri*/) + : _pdf_doc(std::move(doc)) +{ + assert(_pdf_doc); +#ifdef HAVE_POPPLER_CAIRO + _poppler_doc = NULL; +#endif // HAVE_POPPLER_CAIRO + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + + // All Pages + _pageAllPages = Gtk::manage(new class Gtk::CheckButton(_("All"))); + + // Page number + auto _pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _pdf_doc->getNumPages(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(_pageNumberSpin_adj, 1, 1)); + _pageNumberSpin->set_sensitive(false); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + hbox2 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + + // Disable the page selector when there's only one page + int num_pages = _pdf_doc->getCatalog()->getNumPages(); + if ( num_pages == 1 ) { + _pageAllPages->set_sensitive(false); + } else { + // Display total number of pages + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + } + + // Crop settings + _cropCheck = Gtk::manage(new class Gtk::CheckButton(_("Clip to:"))); + _cropTypeCombo = Gtk::manage(new class Gtk::ComboBoxText()); + int num_crop_choices = sizeof(crop_setting_choices) / sizeof(crop_setting_choices[0]); + for ( int i = 0 ; i < num_crop_choices ; i++ ) { + _cropTypeCombo->append(_(crop_setting_choices[i])); + } + _cropTypeCombo->set_active_text(_(crop_setting_choices[0])); + _cropTypeCombo->set_sensitive(false); + + hbox3 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + vbox2 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _pageSettingsFrame = Gtk::manage(new class Inkscape::UI::Widget::Frame(_("Page settings"))); + _labelPrecision = Gtk::manage(new class Gtk::Label(_("Precision of approximating gradient meshes:"))); + _labelPrecisionWarning = Gtk::manage(new class Gtk::Label(_("<b>Note</b>: setting the precision too high may result in a large SVG file and slow performance."))); + _labelPrecisionWarning->set_max_width_chars(50); + +#ifdef HAVE_POPPLER_CAIRO + Gtk::RadioButton::Group group; + _importViaPoppler = Gtk::manage(new class Gtk::RadioButton(group,_("Poppler/Cairo import"))); + _labelViaPoppler = Gtk::manage(new class Gtk::Label(_("Import via external library. Text consists of groups containing cloned glyphs where each glyph is a path. Images are stored internally. Meshes cause entire document to be rendered as a raster image."))); + _labelViaPoppler->set_max_width_chars(50); + _importViaInternal = Gtk::manage(new class Gtk::RadioButton(group,_("Internal import"))); + _labelViaInternal = Gtk::manage(new class Gtk::Label(_("Import via internal (Poppler derived) library. Text is stored as text but white space is missing. Meshes are converted to tiles, the number depends on the precision set below."))); + _labelViaInternal->set_max_width_chars(50); +#endif + + _fallbackPrecisionSlider_adj = Gtk::Adjustment::create(2, 1, 256, 1, 10, 10); + _fallbackPrecisionSlider = Gtk::manage(new class Gtk::Scale(_fallbackPrecisionSlider_adj)); + _fallbackPrecisionSlider->set_value(2.0); + _labelPrecisionComment = Gtk::manage(new class Gtk::Label(_("rough"))); + hbox6 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + + // Text options + // _labelText = Gtk::manage(new class Gtk::Label(_("Text handling:"))); + // _textHandlingCombo = Gtk::manage(new class Gtk::ComboBoxText()); + // _textHandlingCombo->append(_("Import text as text")); + // _textHandlingCombo->set_active_text(_("Import text as text")); + // hbox5 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + + // Font option + _localFontsCheck = Gtk::manage(new class Gtk::CheckButton(_("Replace PDF fonts by closest-named installed fonts"))); + + _embedImagesCheck = Gtk::manage(new class Gtk::CheckButton(_("Embed images"))); + vbox3 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _importSettingsFrame = Gtk::manage(new class Inkscape::UI::Widget::Frame(_("Import settings"))); + vbox1 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + _previewArea = Gtk::manage(new class Gtk::DrawingArea()); + hbox1 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + cancelbutton->set_can_focus(); + cancelbutton->set_can_default(); + cancelbutton->set_relief(Gtk::RELIEF_NORMAL); + okbutton->set_can_focus(); + okbutton->set_can_default(); + okbutton->set_relief(Gtk::RELIEF_NORMAL); + + _labelSelect->set_xalign(0.5); + _labelSelect->set_yalign(0.5); + _labelTotalPages->set_xalign(0.5); + _labelTotalPages->set_yalign(0.5); + _labelPrecision->set_xalign(0.0); + _labelPrecision->set_yalign(0.5); + _labelPrecisionWarning->set_xalign(0.0); + _labelPrecisionWarning->set_yalign(0.5); + _labelPrecisionComment->set_xalign(0.5); + _labelPrecisionComment->set_yalign(0.5); + + _labelSelect->set_margin_start(4); + _labelSelect->set_margin_end(4); + _labelTotalPages->set_margin_start(4); + _labelTotalPages->set_margin_end(4); + _labelPrecision->set_margin_start(4); + _labelPrecision->set_margin_end(4); + _labelPrecisionWarning->set_margin_start(4); + _labelPrecisionWarning->set_margin_end(4); + _labelPrecisionComment->set_margin_start(4); + _labelPrecisionComment->set_margin_end(4); + + _labelSelect->set_justify(Gtk::JUSTIFY_LEFT); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _pageAllPages->set_can_focus(); + _pageAllPages->set_relief(Gtk::RELIEF_NORMAL); + _pageAllPages->set_mode(true); + _pageAllPages->set_active(true); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_digits(0); + _pageNumberSpin->set_wrap(false); + _labelTotalPages->set_justify(Gtk::JUSTIFY_LEFT); + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + hbox2->pack_start(*_pageAllPages, Gtk::PACK_SHRINK, 4); + hbox2->pack_start(*_labelSelect, Gtk::PACK_SHRINK, 4); + hbox2->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK, 4); + hbox2->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK, 4); + _cropCheck->set_can_focus(); + _cropCheck->set_relief(Gtk::RELIEF_NORMAL); + _cropCheck->set_mode(true); + _cropCheck->set_active(false); + _cropTypeCombo->set_border_width(1); + hbox3->pack_start(*_cropCheck, Gtk::PACK_SHRINK, 4); + hbox3->pack_start(*_cropTypeCombo, Gtk::PACK_SHRINK, 0); + vbox2->pack_start(*hbox2); + vbox2->pack_start(*hbox3); + _pageSettingsFrame->add(*vbox2); + _pageSettingsFrame->set_border_width(4); + _labelPrecision->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecision->set_line_wrap(true); + _labelPrecision->set_use_markup(false); + _labelPrecision->set_selectable(false); + _labelPrecisionWarning->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecisionWarning->set_line_wrap(true); + _labelPrecisionWarning->set_use_markup(true); + _labelPrecisionWarning->set_selectable(false); + +#ifdef HAVE_POPPLER_CAIRO + _importViaPoppler->set_can_focus(); + _importViaPoppler->set_relief(Gtk::RELIEF_NORMAL); + _importViaPoppler->set_mode(true); + _importViaPoppler->set_active(false); + _importViaInternal->set_can_focus(); + _importViaInternal->set_relief(Gtk::RELIEF_NORMAL); + _importViaInternal->set_mode(true); + _importViaInternal->set_active(true); + _labelViaPoppler->set_line_wrap(true); + _labelViaInternal->set_line_wrap(true); + _labelViaPoppler->set_xalign(0); + _labelViaInternal->set_xalign(0); +#endif + + _fallbackPrecisionSlider->set_size_request(180,-1); + _fallbackPrecisionSlider->set_can_focus(); + _fallbackPrecisionSlider->set_inverted(false); + _fallbackPrecisionSlider->set_digits(1); + _fallbackPrecisionSlider->set_draw_value(true); + _fallbackPrecisionSlider->set_value_pos(Gtk::POS_TOP); + _labelPrecisionComment->set_size_request(90,-1); + _labelPrecisionComment->set_justify(Gtk::JUSTIFY_LEFT); + _labelPrecisionComment->set_line_wrap(false); + _labelPrecisionComment->set_use_markup(false); + _labelPrecisionComment->set_selectable(false); + hbox6->pack_start(*_fallbackPrecisionSlider, Gtk::PACK_SHRINK, 4); + hbox6->pack_start(*_labelPrecisionComment, Gtk::PACK_SHRINK, 0); + // _labelText->set_alignment(0.5,0.5); + // _labelText->set_padding(4,0); + // _labelText->set_justify(Gtk::JUSTIFY_LEFT); + // _labelText->set_line_wrap(false); + // _labelText->set_use_markup(false); + // _labelText->set_selectable(false); + // hbox5->pack_start(*_labelText, Gtk::PACK_SHRINK, 0); + // hbox5->pack_start(*_textHandlingCombo, Gtk::PACK_SHRINK, 0); + _localFontsCheck->set_can_focus(); + _localFontsCheck->set_relief(Gtk::RELIEF_NORMAL); + _localFontsCheck->set_mode(true); + _localFontsCheck->set_active(true); + _embedImagesCheck->set_can_focus(); + _embedImagesCheck->set_relief(Gtk::RELIEF_NORMAL); + _embedImagesCheck->set_mode(true); + _embedImagesCheck->set_active(true); +#ifdef HAVE_POPPLER_CAIRO + vbox3->pack_start(*_importViaPoppler, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelViaPoppler, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_importViaInternal, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelViaInternal, Gtk::PACK_SHRINK, 0); +#endif + vbox3->pack_start(*_localFontsCheck, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_embedImagesCheck, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelPrecision, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*hbox6, Gtk::PACK_SHRINK, 0); + vbox3->pack_start(*_labelPrecisionWarning, Gtk::PACK_SHRINK, 0); + // vbox3->pack_start(*hbox5, Gtk::PACK_SHRINK, 4); + _importSettingsFrame->add(*vbox3); + _importSettingsFrame->set_border_width(4); + vbox1->pack_start(*_pageSettingsFrame, Gtk::PACK_SHRINK, 0); + vbox1->pack_start(*_importSettingsFrame, Gtk::PACK_SHRINK, 0); + hbox1->pack_start(*vbox1); + hbox1->pack_start(*_previewArea, Gtk::PACK_SHRINK, 4); + + get_content_area()->set_homogeneous(false); + get_content_area()->set_spacing(0); + get_content_area()->pack_start(*hbox1); + + this->set_title(_("PDF Import Settings")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + this->add_action_widget(*cancelbutton, -6); + this->add_action_widget(*okbutton, -5); + + this->show_all(); + + // Connect signals + _previewArea->signal_draw().connect(sigc::mem_fun(*this, &PdfImportDialog::_onDraw)); + _pageAllPages->signal_toggled().connect(sigc::mem_fun(*this, &PdfImportDialog::_onToggleAllPages)); + _pageNumberSpin_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_onPageNumberChanged)); + _cropCheck->signal_toggled().connect(sigc::mem_fun(*this, &PdfImportDialog::_onToggleCropping)); + _fallbackPrecisionSlider_adj->signal_value_changed().connect(sigc::mem_fun(*this, &PdfImportDialog::_onPrecisionChanged)); +#ifdef HAVE_POPPLER_CAIRO + _importViaPoppler->signal_toggled().connect(sigc::mem_fun(*this, &PdfImportDialog::_onToggleImport)); +#endif + + _render_thumb = false; +#ifdef HAVE_POPPLER_CAIRO + _cairo_surface = NULL; + _render_thumb = true; + + // Create PopplerDocument + std::string filename = _pdf_doc->getFileName()->getCString(); + if (!Glib::path_is_absolute(filename)) { + filename = Glib::build_filename(Glib::get_current_dir(),filename); + } + Glib::ustring full_uri = Glib::filename_to_uri(filename); + + if (!full_uri.empty()) { + _poppler_doc = poppler_document_new_from_file(full_uri.c_str(), NULL, NULL); + } + + // Set sensitivity of some widgets based on selected import type. + _onToggleImport(); +#endif + + // Set default preview size + _preview_width = 200; + _preview_height = 300; + + // Init preview + _thumb_data = nullptr; + _pageNumberSpin_adj->set_value(1.0); + _current_page = -1; + _setPreviewPage(1); + + set_default (*okbutton); + set_focus (*okbutton); +} + +PdfImportDialog::~PdfImportDialog() { +#ifdef HAVE_POPPLER_CAIRO + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + if (_poppler_doc) { + g_object_unref(G_OBJECT(_poppler_doc)); + } +#endif + if (_thumb_data) { + gfree(_thumb_data); + } +} + +bool PdfImportDialog::showDialog() { + show(); + gint b = run(); + hide(); + if ( b == Gtk::RESPONSE_OK ) { + return TRUE; + } else { + return FALSE; + } +} + +int PdfImportDialog::getSelectedPage() { + return _current_page; +} + +bool PdfImportDialog::getImportMethod() { +#ifdef HAVE_POPPLER_CAIRO + return _importViaPoppler->get_active(); +#else + return false; +#endif +} + +/** + * \brief Retrieves the current settings into a repr which SvgBuilder will use + * for determining the behaviour desired by the user + */ +void PdfImportDialog::getImportSettings(Inkscape::XML::Node *prefs) { + prefs->setAttributeSvgDouble("selectedPage", (double)_current_page); + if (_cropCheck->get_active()) { + Glib::ustring current_choice = _cropTypeCombo->get_active_text(); + int num_crop_choices = sizeof(crop_setting_choices) / sizeof(crop_setting_choices[0]); + int i = 0; + for ( ; i < num_crop_choices ; i++ ) { + if ( current_choice == _(crop_setting_choices[i]) ) { + break; + } + } + prefs->setAttributeSvgDouble("cropTo", (double)i); + } else { + prefs->setAttributeSvgDouble("cropTo", -1.0); + } + prefs->setAttributeSvgDouble("approximationPrecision", _fallbackPrecisionSlider->get_value()); + if (_localFontsCheck->get_active()) { + prefs->setAttribute("localFonts", "1"); + } else { + prefs->setAttribute("localFonts", "0"); + } + if (_embedImagesCheck->get_active()) { + prefs->setAttribute("embedImages", "1"); + } else { + prefs->setAttribute("embedImages", "0"); + } +#ifdef HAVE_POPPLER_CAIRO + if (_importViaPoppler->get_active()) { + prefs->setAttribute("importviapoppler", "1"); + } else { + prefs->setAttribute("importviapoppler", "0"); + } +#endif +} + +/** + * \brief Redisplay the comment on the current approximation precision setting + * Evenly divides the interval of possible values between the available labels. + */ +void PdfImportDialog::_onPrecisionChanged() { + + static Glib::ustring precision_comments[] = { + Glib::ustring(C_("PDF input precision", "rough")), + Glib::ustring(C_("PDF input precision", "medium")), + Glib::ustring(C_("PDF input precision", "fine")), + Glib::ustring(C_("PDF input precision", "very fine")) + }; + + double min = _fallbackPrecisionSlider_adj->get_lower(); + double max = _fallbackPrecisionSlider_adj->get_upper(); + int num_intervals = sizeof(precision_comments) / sizeof(precision_comments[0]); + double interval_len = ( max - min ) / (double)num_intervals; + double value = _fallbackPrecisionSlider_adj->get_value(); + int comment_idx = (int)floor( ( value - min ) / interval_len ); + _labelPrecisionComment->set_label(precision_comments[comment_idx]); +} + +void PdfImportDialog::_onToggleAllPages() { + if (_pageAllPages->get_active()) { + _pageNumberSpin->set_sensitive(false); + _current_page = -1; + _setPreviewPage(1); + } else { + _pageNumberSpin->set_sensitive(true); + _onPageNumberChanged(); + } +} + +void PdfImportDialog::_onToggleCropping() { + _cropTypeCombo->set_sensitive(_cropCheck->get_active()); +} + +void PdfImportDialog::_onPageNumberChanged() { + int page = _pageNumberSpin->get_value_as_int(); + _current_page = CLAMP(page, 1, _pdf_doc->getCatalog()->getNumPages()); + _setPreviewPage(_current_page); +} + +#ifdef HAVE_POPPLER_CAIRO +void PdfImportDialog::_onToggleImport() { + if( _importViaPoppler->get_active() ) { + hbox3->set_sensitive(false); + _localFontsCheck->set_sensitive(false); + _embedImagesCheck->set_sensitive(false); + hbox6->set_sensitive(false); + _pageAllPages->set_sensitive(false); + _pageAllPages->set_active(false); + } else { + hbox3->set_sensitive(); + _localFontsCheck->set_sensitive(); + _embedImagesCheck->set_sensitive(); + hbox6->set_sensitive(); + _pageAllPages->set_sensitive(true); + _pageAllPages->set_active(true); + } +} +#endif + + +#ifdef HAVE_POPPLER_CAIRO +/** + * \brief Copies image data from a Cairo surface to a pixbuf + * + * Borrowed from libpoppler, from the file poppler-page.cc + * Copyright (C) 2005, Red Hat, Inc. + * + */ +static void copy_cairo_surface_to_pixbuf (cairo_surface_t *surface, + unsigned char *data, + GdkPixbuf *pixbuf) +{ + int cairo_width, cairo_height, cairo_rowstride; + unsigned char *pixbuf_data, *dst, *cairo_data; + int pixbuf_rowstride, pixbuf_n_channels; + unsigned int *src; + int x, y; + + cairo_width = cairo_image_surface_get_width (surface); + cairo_height = cairo_image_surface_get_height (surface); + cairo_rowstride = cairo_width * 4; + cairo_data = data; + + pixbuf_data = gdk_pixbuf_get_pixels (pixbuf); + pixbuf_rowstride = gdk_pixbuf_get_rowstride (pixbuf); + pixbuf_n_channels = gdk_pixbuf_get_n_channels (pixbuf); + + if (cairo_width > gdk_pixbuf_get_width (pixbuf)) + cairo_width = gdk_pixbuf_get_width (pixbuf); + if (cairo_height > gdk_pixbuf_get_height (pixbuf)) + cairo_height = gdk_pixbuf_get_height (pixbuf); + for (y = 0; y < cairo_height; y++) + { + src = reinterpret_cast<unsigned int *>(cairo_data + y * cairo_rowstride); + dst = pixbuf_data + y * pixbuf_rowstride; + for (x = 0; x < cairo_width; x++) + { + dst[0] = (*src >> 16) & 0xff; + dst[1] = (*src >> 8) & 0xff; + dst[2] = (*src >> 0) & 0xff; + if (pixbuf_n_channels == 4) + dst[3] = (*src >> 24) & 0xff; + dst += pixbuf_n_channels; + src++; + } + } +} + +#endif + +bool PdfImportDialog::_onDraw(const Cairo::RefPtr<Cairo::Context>& cr) { + // Check if we have a thumbnail at all + if (!_thumb_data) { + return true; + } + + // Create the pixbuf for the thumbnail + Glib::RefPtr<Gdk::Pixbuf> thumb; + + if (_render_thumb) { + thumb = Gdk::Pixbuf::create(Gdk::COLORSPACE_RGB, true, + 8, _thumb_width, _thumb_height); + } else { + thumb = Gdk::Pixbuf::create_from_data(_thumb_data, Gdk::COLORSPACE_RGB, + false, 8, _thumb_width, _thumb_height, _thumb_rowstride); + } + if (!thumb) { + return true; + } + + // Set background to white + if (_render_thumb) { + thumb->fill(0xffffffff); + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, 0); + cr->paint(); + } +#ifdef HAVE_POPPLER_CAIRO + // Copy the thumbnail image from the Cairo surface + if (_render_thumb) { + copy_cairo_surface_to_pixbuf(_cairo_surface, _thumb_data, thumb->gobj()); + } +#endif + + Gdk::Cairo::set_source_pixbuf(cr, thumb, 0, _render_thumb ? 0 : 20); + cr->paint(); + return true; +} + +/** + * \brief Renders the given page's thumbnail using Cairo + */ +void PdfImportDialog::_setPreviewPage(int page) { + + _previewed_page = _pdf_doc->getCatalog()->getPage(page); + g_return_if_fail(_previewed_page); + // Try to get a thumbnail from the PDF if possible + if (!_render_thumb) { + if (_thumb_data) { + gfree(_thumb_data); + _thumb_data = nullptr; + } + if (!_previewed_page->loadThumb(&_thumb_data, + &_thumb_width, &_thumb_height, &_thumb_rowstride)) { + return; + } + // Redraw preview area + _previewArea->set_size_request(_thumb_width, _thumb_height + 20); + _previewArea->queue_draw(); + return; + } +#ifdef HAVE_POPPLER_CAIRO + // Get page size by accounting for rotation + double width, height; + int rotate = _previewed_page->getRotate(); + if ( rotate == 90 || rotate == 270 ) { + height = _previewed_page->getCropWidth(); + width = _previewed_page->getCropHeight(); + } else { + width = _previewed_page->getCropWidth(); + height = _previewed_page->getCropHeight(); + } + // Calculate the needed scaling for the page + double scale_x = (double)_preview_width / width; + double scale_y = (double)_preview_height / height; + double scale_factor = ( scale_x > scale_y ) ? scale_y : scale_x; + // Create new Cairo surface + _thumb_width = (int)ceil( width * scale_factor ); + _thumb_height = (int)ceil( height * scale_factor ); + _thumb_rowstride = _thumb_width * 4; + if (_thumb_data) { + gfree(_thumb_data); + } + _thumb_data = reinterpret_cast<unsigned char *>(gmalloc(_thumb_rowstride * _thumb_height)); + if (_cairo_surface) { + cairo_surface_destroy(_cairo_surface); + } + _cairo_surface = cairo_image_surface_create_for_data(_thumb_data, + CAIRO_FORMAT_ARGB32, _thumb_width, _thumb_height, _thumb_rowstride); + cairo_t *cr = cairo_create(_cairo_surface); + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); // Set fill color to white + cairo_paint(cr); // Clear it + cairo_scale(cr, scale_factor, scale_factor); // Use Cairo for resizing the image + // Render page + if (_poppler_doc != NULL) { + PopplerPage *poppler_page = poppler_document_get_page(_poppler_doc, page-1); + poppler_page_render(poppler_page, cr); + g_object_unref(G_OBJECT(poppler_page)); + } + // Clean up + cairo_destroy(cr); + // Redraw preview area + _previewArea->set_size_request(_preview_width, _preview_height); + _previewArea->queue_draw(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// + +#ifdef HAVE_POPPLER_CAIRO +/// helper method +static cairo_status_t + _write_ustring_cb(void *closure, const unsigned char *data, unsigned int length) +{ + Glib::ustring* stream = static_cast<Glib::ustring*>(closure); + stream->append(reinterpret_cast<const char*>(data), length); + + return CAIRO_STATUS_SUCCESS; +} +#endif + +/** + * Parses the selected page of the given PDF document using PdfParser. + */ +SPDocument * +PdfInput::open(::Inkscape::Extension::Input * /*mod*/, const gchar * uri) { + + // Initialize the globalParams variable for poppler + if (!globalParams) { + globalParams = _POPPLER_NEW_GLOBAL_PARAMS(); + } + + + // Open the file using poppler + // PDFDoc is from poppler. PDFDoc is used for preview and for native import. + std::shared_ptr<PDFDoc> pdf_doc; + + // poppler does not use glib g_open. So on win32 we must use unicode call. code was copied from + // glib gstdio.c + pdf_doc = _POPPLER_MAKE_SHARED_PDFDOC(uri); // TODO: Could ask for password + + if (!pdf_doc->isOk()) { + int error = pdf_doc->getErrorCode(); + if (error == errEncrypted) { + g_message("Document is encrypted."); + } else if (error == errOpenFile) { + g_message("couldn't open the PDF file."); + } else if (error == errBadCatalog) { + g_message("couldn't read the page catalog."); + } else if (error == errDamaged) { + g_message("PDF file was damaged and couldn't be repaired."); + } else if (error == errHighlightFile) { + g_message("nonexistent or invalid highlight file."); + } else if (error == errBadPrinter) { + g_message("invalid printer."); + } else if (error == errPrinting) { + g_message("Error during printing."); + } else if (error == errPermission) { + g_message("PDF file does not allow that operation."); + } else if (error == errBadPageNum) { + g_message("invalid page number."); + } else if (error == errFileIO) { + g_message("file IO error."); + } else { + g_message("Failed to load document from data (error %d)", error); + } + + return nullptr; + } + + + std::unique_ptr<PdfImportDialog> dlg; + if (INKSCAPE.use_gui()) { + dlg = std::make_unique<PdfImportDialog>(pdf_doc, uri); + if (!dlg->showDialog()) { + throw Input::open_cancelled(); + } + } + + // Get options + int page_num = 1; + bool is_importvia_poppler = false; + if (dlg) { + page_num = dlg->getSelectedPage(); +#ifdef HAVE_POPPLER_CAIRO + is_importvia_poppler = dlg->getImportMethod(); + // printf("PDF import via %s.\n", is_importvia_poppler ? "poppler" : "native"); +#endif + } else { + page_num = INKSCAPE.get_pdf_page(); +#ifdef HAVE_POPPLER_CAIRO + is_importvia_poppler = INKSCAPE.get_pdf_poppler(); +#endif + } + + // Create Inkscape document from file + SPDocument *doc = nullptr; + bool saved = false; + if(!is_importvia_poppler) + { + // Create document + doc = SPDocument::createNewDoc(nullptr, true, true); + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document + + // Create builder + gchar *docname = g_path_get_basename(uri); + gchar *dot = g_strrstr(docname, "."); + if (dot) { + *dot = 0; + } + SvgBuilder *builder = new SvgBuilder(doc, docname, pdf_doc->getXRef()); + + // Get preferences + Inkscape::XML::Node *prefs = builder->getPreferences(); + if (dlg) + dlg->getImportSettings(prefs); + + if (page_num > 0) { + add_builder_page(pdf_doc, builder, doc, page_num); + } else { + // Multi page (open all pages) + Catalog *catalog = pdf_doc->getCatalog(); + for (int p = 1; p <= catalog->getNumPages(); p++) { + // Incriment the page building here. + builder->pushPage(); + // And then add each of the pages + add_builder_page(pdf_doc, builder, doc, p); + } + } + + delete builder; + g_free(docname); + } + else + { +#ifdef HAVE_POPPLER_CAIRO + // the poppler import + + std::string full_path = uri; + if (!Glib::path_is_absolute(uri)) { + full_path = Glib::build_filename(Glib::get_current_dir(),uri); + } + Glib::ustring full_uri = Glib::filename_to_uri(full_path); + + GError *error = NULL; + /// @todo handle password + /// @todo check if win32 unicode needs special attention + PopplerDocument* document = poppler_document_new_from_file(full_uri.c_str(), NULL, &error); + PopplerPage* page = nullptr; + + if(error != NULL) { + std::cerr << "PDFInput::open: error opening document: " << full_uri << std::endl; + g_error_free (error); + } + + if (document) { + int const num_pages = poppler_document_get_n_pages(document); + sanitize_page_number(page_num, num_pages); + page = poppler_document_get_page(document, page_num - 1); + } + + if (page) { + double width, height; + poppler_page_get_size(page, &width, &height); + + Glib::ustring output; + cairo_surface_t* surface = cairo_svg_surface_create_for_stream(Inkscape::Extension::Internal::_write_ustring_cb, + &output, width, height); + + // Reset back to PT for cairo 1.17.6 and above which sets to UNIT_USER + cairo_svg_surface_set_document_unit(surface, CAIRO_SVG_UNIT_PT); + + // This magical function results in more fine-grain fallbacks. In particular, a mesh + // gradient won't necessarily result in the whole PDF being rasterized. Of course, SVG + // 1.2 never made it as a standard, but hey, we'll take what we can get. This trick was + // found by examining the 'pdftocairo' code. + cairo_svg_surface_restrict_to_version( surface, CAIRO_SVG_VERSION_1_2 ); + + cairo_t* cr = cairo_create(surface); + + poppler_page_render_for_printing(page, cr); + cairo_show_page(cr); + + cairo_destroy(cr); + cairo_surface_destroy(surface); + + doc = SPDocument::createNewDocFromMem(output.c_str(), output.length(), TRUE); + } else if (document) { + std::cerr << "PDFInput::open: error opening page " << page_num << " of document: " << full_uri << std::endl; + } + + // Cleanup + if (document) { + g_object_unref(G_OBJECT(document)); + if (page) { + g_object_unref(G_OBJECT(page)); + } + } + + if (!doc) { + return nullptr; + } + + saved = DocumentUndo::getUndoSensitive(doc); + DocumentUndo::setUndoSensitive(doc, false); // No need to undo in this temporary document +#endif + } + + // Set viewBox if it doesn't exist + if (!doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + // Restore undo + DocumentUndo::setUndoSensitive(doc, saved); + + return doc; +} + +/** + * Parses the selected page object of the given PDF document using PdfParser. + */ +void +PdfInput::add_builder_page(std::shared_ptr<PDFDoc>pdf_doc, SvgBuilder *builder, SPDocument *doc, int page_num) +{ + // native importer + Catalog *catalog = pdf_doc->getCatalog(); + + int const num_pages = catalog->getNumPages(); + Inkscape::XML::Node *prefs = builder->getPreferences(); + + // Check page exists + sanitize_page_number(page_num, num_pages); + Page *page = catalog->getPage(page_num); + if (!page) { + std::cerr << "PDFInput::open: error opening page " << page_num << std::endl; + return; + } + + // Apply crop settings + _POPPLER_CONST PDFRectangle *clipToBox = nullptr; + double crop_setting = prefs->getAttributeDouble("cropTo", -1.0); + + if (crop_setting >= 0.0) { // Do page clipping + int crop_choice = (int)crop_setting; + switch (crop_choice) { + case 0: // Media box + clipToBox = page->getMediaBox(); + break; + case 1: // Crop box + clipToBox = page->getCropBox(); + break; + case 2: // Bleed box + clipToBox = page->getBleedBox(); + break; + case 3: // Trim box + clipToBox = page->getTrimBox(); + break; + case 4: // Art box + clipToBox = page->getArtBox(); + break; + default: + break; + } + } + + // Create parser (extension/internal/pdfinput/pdf-parser.h) + PdfParser *pdf_parser = new PdfParser(pdf_doc->getXRef(), builder, page_num-1, page->getRotate(), + page->getResourceDict(), page->getCropBox(), clipToBox); + + // Set up approximation precision for parser. Used for converting Mesh Gradients into tiles. + double color_delta = prefs->getAttributeDouble("approximationPrecision", 2.0); + if ( color_delta <= 0.0 ) { + color_delta = 1.0 / 2.0; + } else { + color_delta = 1.0 / color_delta; + } + for ( int i = 1 ; i <= pdfNumShadingTypes ; i++ ) { + pdf_parser->setApproximationPrecision(i, color_delta, 6); + } + + // Parse the document structure +#if defined(POPPLER_NEW_OBJECT_API) + Object obj = page->getContents(); +#else + Object obj; + page->getContents(&obj); +#endif + if (!obj.isNull()) { + pdf_parser->parse(&obj); + } + + // Cleanup +#if !defined(POPPLER_NEW_OBJECT_API) + obj.free(); +#endif + delete pdf_parser; +} + +#include "../clear-n_.h" + +void PdfInput::init() { + /* PDF in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("PDF Input") "</name>\n" + "<id>org.inkscape.input.pdf</id>\n" + "<input>\n" + "<extension>.pdf</extension>\n" + "<mimetype>application/pdf</mimetype>\n" + "<filetypename>" N_("Portable Document Format (*.pdf)") "</filetypename>\n" + "<filetypetooltip>" N_("Portable Document Format") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new PdfInput()); + // clang-format on + + /* AI in */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("AI Input") "</name>\n" + "<id>org.inkscape.input.ai</id>\n" + "<input>\n" + "<extension>.ai</extension>\n" + "<mimetype>image/x-adobe-illustrator</mimetype>\n" + "<filetypename>" N_("Adobe Illustrator 9.0 and above (*.ai)") "</filetypename>\n" + "<filetypetooltip>" N_("Open files saved in Adobe Illustrator 9.0 and newer versions") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new PdfInput()); + // clang-format on +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-input.h b/src/extension/internal/pdfinput/pdf-input.h new file mode 100644 index 0000000..fd97277 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-input.h @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +#include <gtkmm/dialog.h> + +#include "../../implementation/implementation.h" +#include "svg-builder.h" + +#ifdef HAVE_POPPLER_CAIRO +struct _PopplerDocument; +typedef struct _PopplerDocument PopplerDocument; +#endif + +struct _GdkEventExpose; +typedef _GdkEventExpose GdkEventExpose; + +class Page; +class PDFDoc; + +namespace Gtk { + class Button; + class CheckButton; + class ComboBoxText; + class DrawingArea; + class Frame; + class Scale; + class RadioButton; + class Box; + class Label; +} + +namespace Inkscape { + +namespace UI { +namespace Widget { + class SpinButton; + class Frame; +} +} + +namespace Extension { +namespace Internal { + +/** + * PDF import using libpoppler. + */ +class PdfImportDialog : public Gtk::Dialog +{ +public: + PdfImportDialog(std::shared_ptr<PDFDoc> doc, const gchar *uri); + ~PdfImportDialog() override; + + bool showDialog(); + int getSelectedPage(); + bool getImportMethod(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(int page); + + // Signal handlers + bool _onDraw(const Cairo::RefPtr<Cairo::Context>& cr); + void _onPageNumberChanged(); + void _onToggleAllPages(); + void _onToggleCropping(); + void _onPrecisionChanged(); +#ifdef HAVE_POPPLER_CAIRO + void _onToggleImport(); +#endif + + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + class Gtk::Label * _labelSelect; + class Gtk::CheckButton *_pageAllPages; + class Inkscape::UI::Widget::SpinButton * _pageNumberSpin; + class Gtk::Label * _labelTotalPages; + class Gtk::Box * hbox2; + class Gtk::CheckButton * _cropCheck; + class Gtk::ComboBoxText * _cropTypeCombo; + class Gtk::Box * hbox3; + class Gtk::Box * vbox2; + class Inkscape::UI::Widget::Frame * _pageSettingsFrame; + class Gtk::Label * _labelPrecision; + class Gtk::Label * _labelPrecisionWarning; +#ifdef HAVE_POPPLER_CAIRO + class Gtk::RadioButton * _importViaPoppler; // Use poppler_cairo importing + class Gtk::Label * _labelViaPoppler; + class Gtk::RadioButton * _importViaInternal; // Use native (poppler based) importing + class Gtk::Label * _labelViaInternal; +#endif + Gtk::Scale * _fallbackPrecisionSlider; + Glib::RefPtr<Gtk::Adjustment> _fallbackPrecisionSlider_adj; + class Gtk::Label * _labelPrecisionComment; + class Gtk::Box * hbox6; +#if 0 + class Gtk::Label * _labelText; + class Gtk::ComboBoxText * _textHandlingCombo; + class Gtk::Box * hbox5; +#endif + class Gtk::CheckButton * _localFontsCheck; + class Gtk::CheckButton * _embedImagesCheck; + class Gtk::Box * vbox3; + class Inkscape::UI::Widget::Frame * _importSettingsFrame; + class Gtk::Box * vbox1; + class Gtk::DrawingArea * _previewArea; + class Gtk::Box * hbox1; + + std::shared_ptr<PDFDoc> _pdf_doc; // Document to be imported + int _current_page; // Current selected page + Page *_previewed_page; // Currently previewed page + unsigned char *_thumb_data; // Thumbnail image data + int _thumb_width, _thumb_height; // Thumbnail size + int _thumb_rowstride; + int _preview_width, _preview_height; // Size of the preview area + bool _render_thumb; // Whether we can/shall render thumbnails +#ifdef HAVE_POPPLER_CAIRO + cairo_surface_t *_cairo_surface; + PopplerDocument *_poppler_doc; +#endif +}; + + +class PdfInput: public Inkscape::Extension::Implementation::Implementation { + PdfInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); +private: + void add_builder_page( + std::shared_ptr<PDFDoc> pdf_doc, + SvgBuilder *builder, SPDocument *doc, + int page_num); +}; + +} // namespace Implementation +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/pdf-parser.cpp b/src/extension/internal/pdfinput/pdf-parser.cpp new file mode 100644 index 0000000..d32a7fa --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.cpp @@ -0,0 +1,3368 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * Derived from poppler's Gfx.cc, which was derived from Xpdf by 1996-2003 Glyph & Cog, LLC + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER + +#ifdef USE_GCC_PRAGMAS +#pragma implementation +#endif + +#include <cstdlib> +#include <cstdio> +#include <cstddef> +#include <cstring> +#include <cmath> + +#include "svg-builder.h" +#include "Gfx.h" +#include "pdf-parser.h" +#include "util/units.h" +#include "poppler-transition-api.h" + +#include "glib/poppler-features.h" +#include "goo/gmem.h" +#include "goo/GooString.h" +#include "GlobalParams.h" +#include "CharTypes.h" +#include "Object.h" +#include "Array.h" +#include "Dict.h" +#include "Stream.h" +#include "Lexer.h" +#include "Parser.h" +#include "GfxFont.h" +#include "GfxState.h" +#include "OutputDev.h" +#include "Page.h" +#include "Annot.h" +#include "Error.h" + +// the MSVC math.h doesn't define this +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +// Default max delta allowed in any color component for a shading fill. +#define defaultShadingColorDelta (dblToCol( 1 / 2.0 )) + +// Default max recursive depth for a shading fill. +#define defaultShadingMaxDepth 6 + +// Max number of operators kept in the history list. +#define maxOperatorHistoryDepth 16 + +//------------------------------------------------------------------------ +// Operator table +//------------------------------------------------------------------------ + +PdfOperator PdfParser::opTab[] = { + {"\"", 3, {tchkNum, tchkNum, tchkString}, + &PdfParser::opMoveSetShowText}, + {"'", 1, {tchkString}, + &PdfParser::opMoveShowText}, + {"B", 0, {tchkNone}, + &PdfParser::opFillStroke}, + {"B*", 0, {tchkNone}, + &PdfParser::opEOFillStroke}, + {"BDC", 2, {tchkName, tchkProps}, + &PdfParser::opBeginMarkedContent}, + {"BI", 0, {tchkNone}, + &PdfParser::opBeginImage}, + {"BMC", 1, {tchkName}, + &PdfParser::opBeginMarkedContent}, + {"BT", 0, {tchkNone}, + &PdfParser::opBeginText}, + {"BX", 0, {tchkNone}, + &PdfParser::opBeginIgnoreUndef}, + {"CS", 1, {tchkName}, + &PdfParser::opSetStrokeColorSpace}, + {"DP", 2, {tchkName, tchkProps}, + &PdfParser::opMarkPoint}, + {"Do", 1, {tchkName}, + &PdfParser::opXObject}, + {"EI", 0, {tchkNone}, + &PdfParser::opEndImage}, + {"EMC", 0, {tchkNone}, + &PdfParser::opEndMarkedContent}, + {"ET", 0, {tchkNone}, + &PdfParser::opEndText}, + {"EX", 0, {tchkNone}, + &PdfParser::opEndIgnoreUndef}, + {"F", 0, {tchkNone}, + &PdfParser::opFill}, + {"G", 1, {tchkNum}, + &PdfParser::opSetStrokeGray}, + {"ID", 0, {tchkNone}, + &PdfParser::opImageData}, + {"J", 1, {tchkInt}, + &PdfParser::opSetLineCap}, + {"K", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeCMYKColor}, + {"M", 1, {tchkNum}, + &PdfParser::opSetMiterLimit}, + {"MP", 1, {tchkName}, + &PdfParser::opMarkPoint}, + {"Q", 0, {tchkNone}, + &PdfParser::opRestore}, + {"RG", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeRGBColor}, + {"S", 0, {tchkNone}, + &PdfParser::opStroke}, + {"SC", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetStrokeColor}, + {"SCN", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN,}, + &PdfParser::opSetStrokeColorN}, + {"T*", 0, {tchkNone}, + &PdfParser::opTextNextLine}, + {"TD", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMoveSet}, + {"TJ", 1, {tchkArray}, + &PdfParser::opShowSpaceText}, + {"TL", 1, {tchkNum}, + &PdfParser::opSetTextLeading}, + {"Tc", 1, {tchkNum}, + &PdfParser::opSetCharSpacing}, + {"Td", 2, {tchkNum, tchkNum}, + &PdfParser::opTextMove}, + {"Tf", 2, {tchkName, tchkNum}, + &PdfParser::opSetFont}, + {"Tj", 1, {tchkString}, + &PdfParser::opShowText}, + {"Tm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetTextMatrix}, + {"Tr", 1, {tchkInt}, + &PdfParser::opSetTextRender}, + {"Ts", 1, {tchkNum}, + &PdfParser::opSetTextRise}, + {"Tw", 1, {tchkNum}, + &PdfParser::opSetWordSpacing}, + {"Tz", 1, {tchkNum}, + &PdfParser::opSetHorizScaling}, + {"W", 0, {tchkNone}, + &PdfParser::opClip}, + {"W*", 0, {tchkNone}, + &PdfParser::opEOClip}, + {"b", 0, {tchkNone}, + &PdfParser::opCloseFillStroke}, + {"b*", 0, {tchkNone}, + &PdfParser::opCloseEOFillStroke}, + {"c", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opCurveTo}, + {"cm", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opConcat}, + {"cs", 1, {tchkName}, + &PdfParser::opSetFillColorSpace}, + {"d", 2, {tchkArray, tchkNum}, + &PdfParser::opSetDash}, + {"d0", 2, {tchkNum, tchkNum}, + &PdfParser::opSetCharWidth}, + {"d1", 6, {tchkNum, tchkNum, tchkNum, tchkNum, + tchkNum, tchkNum}, + &PdfParser::opSetCacheDevice}, + {"f", 0, {tchkNone}, + &PdfParser::opFill}, + {"f*", 0, {tchkNone}, + &PdfParser::opEOFill}, + {"g", 1, {tchkNum}, + &PdfParser::opSetFillGray}, + {"gs", 1, {tchkName}, + &PdfParser::opSetExtGState}, + {"h", 0, {tchkNone}, + &PdfParser::opClosePath}, + {"i", 1, {tchkNum}, + &PdfParser::opSetFlat}, + {"j", 1, {tchkInt}, + &PdfParser::opSetLineJoin}, + {"k", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillCMYKColor}, + {"l", 2, {tchkNum, tchkNum}, + &PdfParser::opLineTo}, + {"m", 2, {tchkNum, tchkNum}, + &PdfParser::opMoveTo}, + {"n", 0, {tchkNone}, + &PdfParser::opEndPath}, + {"q", 0, {tchkNone}, + &PdfParser::opSave}, + {"re", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opRectangle}, + {"rg", 3, {tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillRGBColor}, + {"ri", 1, {tchkName}, + &PdfParser::opSetRenderingIntent}, + {"s", 0, {tchkNone}, + &PdfParser::opCloseStroke}, + {"sc", -4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opSetFillColor}, + {"scn", -33, {tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN, tchkSCN, tchkSCN, tchkSCN, + tchkSCN,}, + &PdfParser::opSetFillColorN}, + {"sh", 1, {tchkName}, + &PdfParser::opShFill}, + {"v", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo1}, + {"w", 1, {tchkNum}, + &PdfParser::opSetLineWidth}, + {"y", 4, {tchkNum, tchkNum, tchkNum, tchkNum}, + &PdfParser::opCurveTo2} +}; + +#define numOps (sizeof(opTab) / sizeof(PdfOperator)) + +namespace { + +GfxPatch blankPatch() +{ + GfxPatch patch; + memset(&patch, 0, sizeof(patch)); // quick-n-dirty + return patch; +} + +} // namespace + +//------------------------------------------------------------------------ +// ClipHistoryEntry +//------------------------------------------------------------------------ + +class ClipHistoryEntry { +public: + + ClipHistoryEntry(GfxPath *clipPath = nullptr, GfxClipType clipType = clipNormal); + virtual ~ClipHistoryEntry(); + + // Manipulate clip path stack + ClipHistoryEntry *save(); + ClipHistoryEntry *restore(); + GBool hasSaves() { return saved != nullptr; } + void setClip(_POPPLER_CONST_83 GfxPath *newClipPath, GfxClipType newClipType = clipNormal); + GfxPath *getClipPath() { return clipPath; } + GfxClipType getClipType() { return clipType; } + +private: + + ClipHistoryEntry *saved; // next clip path on stack + + GfxPath *clipPath; // used as the path to be filled for an 'sh' operator + GfxClipType clipType; + + ClipHistoryEntry(ClipHistoryEntry *other); +}; + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +PdfParser::PdfParser(XRef *xrefA, + Inkscape::Extension::Internal::SvgBuilder *builderA, + int /*pageNum*/, + int rotate, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box, + _POPPLER_CONST PDFRectangle *cropBox) : + xref(xrefA), + builder(builderA), + subPage(gFalse), + printCommands(false), + res(new GfxResources(xref, resDict, nullptr)), // start the resource stack + state(new GfxState(72.0, 72.0, box, rotate, gTrue)), + fontChanged(gFalse), + clip(clipNone), + ignoreUndef(0), + baseMatrix(), + formDepth(0), + parser(nullptr), + colorDeltas(), + maxDepths(), + clipHistory(new ClipHistoryEntry()), + operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + builder->setDocumentSize(Inkscape::Util::Quantity::convert(state->getPageWidth(), "pt", "px"), + Inkscape::Util::Quantity::convert(state->getPageHeight(), "pt", "px")); + + const double *ctm = state->getCTM(); + double scaledCTM[6]; + for (int i = 0; i < 6; ++i) { + baseMatrix[i] = ctm[i]; + scaledCTM[i] = Inkscape::Util::Quantity::convert(ctm[i], "pt", "px"); + } + saveState(); + builder->setTransform((double*)&scaledCTM); + formDepth = 0; + + // set crop box + if (cropBox) { + if (printCommands) + printf("cropBox: %f %f %f %f\n", cropBox->x1, cropBox->y1, cropBox->x2, cropBox->y2); + // do not clip if it's not needed + if (cropBox->x1 != 0.0 || cropBox->y1 != 0.0 || + cropBox->x2 != state->getPageWidth() || cropBox->y2 != state->getPageHeight()) { + + state->moveTo(cropBox->x1, cropBox->y1); + state->lineTo(cropBox->x2, cropBox->y1); + state->lineTo(cropBox->x2, cropBox->y2); + state->lineTo(cropBox->x1, cropBox->y2); + state->closePath(); + state->clip(); + clipHistory->setClip(state->getPath(), clipNormal); + builder->setClipPath(state); + state->clearPath(); + } + } + pushOperator("startPage"); +} + +PdfParser::PdfParser(XRef *xrefA, + Inkscape::Extension::Internal::SvgBuilder *builderA, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box) : + xref(xrefA), + builder(builderA), + subPage(gTrue), + printCommands(false), + res(new GfxResources(xref, resDict, nullptr)), // start the resource stack + state(new GfxState(72, 72, box, 0, gFalse)), + fontChanged(gFalse), + clip(clipNone), + ignoreUndef(0), + baseMatrix(), + formDepth(0), + parser(nullptr), + colorDeltas(), + maxDepths(), + clipHistory(new ClipHistoryEntry()), + operatorHistory(nullptr) +{ + setDefaultApproximationPrecision(); + + for (int i = 0; i < 6; ++i) { + baseMatrix[i] = state->getCTM()[i]; + } + formDepth = 0; +} + +PdfParser::~PdfParser() { + while(operatorHistory) { + OpHistoryEntry *tmp = operatorHistory->next; + delete operatorHistory; + operatorHistory = tmp; + } + + while (state && state->hasSaves()) { + restoreState(); + } + + if (!subPage) { + //out->endPage(); + } + + while (res) { + popResources(); + } + + if (state) { + delete state; + state = nullptr; + } + + if (clipHistory) { + delete clipHistory; + clipHistory = nullptr; + } +} + +void PdfParser::parse(Object *obj, GBool topLevel) { + Object obj2; + + if (obj->isArray()) { + for (int i = 0; i < obj->arrayGetLength(); ++i) { + _POPPLER_CALL_ARGS(obj2, obj->arrayGet, i); + if (!obj2.isStream()) { + error(errInternal, -1, "Weird page contents"); + _POPPLER_FREE(obj2); + return; + } + _POPPLER_FREE(obj2); + } + } else if (!obj->isStream()) { + error(errInternal, -1, "Weird page contents"); + return; + } + parser = new _POPPLER_NEW_PARSER(xref, obj); + go(topLevel); + delete parser; + parser = nullptr; +} + +void PdfParser::go(GBool /*topLevel*/) +{ + Object obj; + Object args[maxArgs]; + + // scan a sequence of objects + int numArgs = 0; + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isEOF()) { + + // got a command - execute it + if (obj.isCmd()) { + if (printCommands) { + obj.print(stdout); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } + + // Run the operation + execOp(&obj, args, numArgs); + +#if !defined(POPPLER_NEW_OBJECT_API) + _POPPLER_FREE(obj); + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + numArgs = 0; + + // got an argument - save it + } else if (numArgs < maxArgs) { + args[numArgs++] = std::move(obj); + + // too many arguments - something is wrong + } else { + error(errSyntaxError, getPos(), "Too many args in content stream"); + if (printCommands) { + printf("throwing away arg: "); + obj.print(stdout); + printf("\n"); + fflush(stdout); + } + _POPPLER_FREE(obj); + } + + // grab the next object + _POPPLER_CALL(obj, parser->getObj); + } + _POPPLER_FREE(obj); + + // args at end with no command + if (numArgs > 0) { + error(errSyntaxError, getPos(), "Leftover args in content stream"); + if (printCommands) { + printf("%d leftovers:", numArgs); + for (int i = 0; i < numArgs; ++i) { + printf(" "); + args[i].print(stdout); + } + printf("\n"); + fflush(stdout); + } +#if !defined(POPPLER_NEW_OBJECT_API) + for (int i = 0; i < numArgs; ++i) + _POPPLER_FREE(args[i]); +#endif + } +} + +void PdfParser::pushOperator(const char *name) +{ + OpHistoryEntry *newEntry = new OpHistoryEntry; + newEntry->name = name; + newEntry->state = nullptr; + newEntry->depth = (operatorHistory != nullptr ? (operatorHistory->depth+1) : 0); + newEntry->next = operatorHistory; + operatorHistory = newEntry; + + // Truncate list if needed + if (operatorHistory->depth > maxOperatorHistoryDepth) { + OpHistoryEntry *curr = operatorHistory; + OpHistoryEntry *prev = nullptr; + while (curr && curr->next != nullptr) { + curr->depth--; + prev = curr; + curr = curr->next; + } + if (prev) { + if (curr->state != nullptr) + delete curr->state; + delete curr; + prev->next = nullptr; + } + } +} + +const char *PdfParser::getPreviousOperator(unsigned int look_back) { + OpHistoryEntry *prev = nullptr; + if (operatorHistory != nullptr && look_back > 0) { + prev = operatorHistory->next; + while (--look_back > 0 && prev != nullptr) { + prev = prev->next; + } + } + if (prev != nullptr) { + return prev->name; + } else { + return ""; + } +} + +void PdfParser::execOp(Object *cmd, Object args[], int numArgs) { + PdfOperator *op; + const char *name; + Object *argPtr; + int i; + + // find operator + name = cmd->getCmd(); + if (!(op = findOp(name))) { + if (ignoreUndef == 0) + error(errSyntaxError, getPos(), "Unknown operator '{0:s}'", name); + return; + } + + // type check args + argPtr = args; + if (op->numArgs >= 0) { + if (numArgs < op->numArgs) { + error(errSyntaxError, getPos(), "Too few ({0:d}) args to '{1:d}' operator", numArgs, name); + return; + } + if (numArgs > op->numArgs) { +#if 0 + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", numArgs, name); +#endif + argPtr += numArgs - op->numArgs; + numArgs = op->numArgs; + } + } else { + if (numArgs > -op->numArgs) { + error(errSyntaxError, getPos(), "Too many ({0:d}) args to '{1:s}' operator", + numArgs, name); + return; + } + } + for (i = 0; i < numArgs; ++i) { + if (!checkArg(&argPtr[i], op->tchk[i])) { + error(errSyntaxError, getPos(), "Arg #{0:d} to '{1:s}' operator is wrong type ({2:s})", + i, name, argPtr[i].getTypeName()); + return; + } + } + + // add to history + pushOperator((char*)&op->name); + + // do it + (this->*op->func)(argPtr, numArgs); +} + +PdfOperator* PdfParser::findOp(const char *name) { + int a = -1; + int b = numOps; + int cmp = -1; + // invariant: opTab[a] < name < opTab[b] + while (b - a > 1) { + const int m = (a + b) / 2; + cmp = strcmp(opTab[m].name, name); + if (cmp < 0) + a = m; + else if (cmp > 0) + b = m; + else + a = b = m; + } + if (cmp != 0) + return nullptr; + return &opTab[a]; +} + +GBool PdfParser::checkArg(Object *arg, TchkType type) { + switch (type) { + case tchkBool: return arg->isBool(); + case tchkInt: return arg->isInt(); + case tchkNum: return arg->isNum(); + case tchkString: return arg->isString(); + case tchkName: return arg->isName(); + case tchkArray: return arg->isArray(); + case tchkProps: return arg->isDict() || arg->isName(); + case tchkSCN: return arg->isNum() || arg->isName(); + case tchkNone: return gFalse; + } + return gFalse; +} + +int PdfParser::getPos() { + return parser ? parser->getPos() : -1; +} + +//------------------------------------------------------------------------ +// graphics state operators +//------------------------------------------------------------------------ + +void PdfParser::opSave(Object /*args*/[], int /*numArgs*/) +{ + saveState(); +} + +void PdfParser::opRestore(Object /*args*/[], int /*numArgs*/) +{ + restoreState(); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opConcat(Object args[], int /*numArgs*/) +{ + state->concatCTM(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + const char *prevOp = getPreviousOperator(); + double a0 = args[0].getNum(); + double a1 = args[1].getNum(); + double a2 = args[2].getNum(); + double a3 = args[3].getNum(); + double a4 = args[4].getNum(); + double a5 = args[5].getNum(); + if (!strcmp(prevOp, "q")) { + builder->setTransform(a0, a1, a2, a3, a4, a5); + } else if (!strcmp(prevOp, "cm") || !strcmp(prevOp, "startPage")) { + // multiply it with the previous transform + double otherMatrix[6]; + if (!builder->getTransform(otherMatrix)) { // invalid transform + // construct identity matrix + otherMatrix[0] = otherMatrix[3] = 1.0; + otherMatrix[1] = otherMatrix[2] = otherMatrix[4] = otherMatrix[5] = 0.0; + } + double c0 = a0*otherMatrix[0] + a1*otherMatrix[2]; + double c1 = a0*otherMatrix[1] + a1*otherMatrix[3]; + double c2 = a2*otherMatrix[0] + a3*otherMatrix[2]; + double c3 = a2*otherMatrix[1] + a3*otherMatrix[3]; + double c4 = a4*otherMatrix[0] + a5*otherMatrix[2] + otherMatrix[4]; + double c5 = a4*otherMatrix[1] + a5*otherMatrix[3] + otherMatrix[5]; + builder->setTransform(c0, c1, c2, c3, c4, c5); + } else { + builder->pushGroup(); + builder->setTransform(a0, a1, a2, a3, a4, a5); + } + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetDash(Object args[], int /*numArgs*/) +{ + double *dash = nullptr; + + Array *a = args[0].getArray(); + int length = a->getLength(); + if (length != 0) { + dash = (double *)gmallocn(length, sizeof(double)); + for (int i = 0; i < length; ++i) { + Object obj; + dash[i] = _POPPLER_CALL_ARGS_DEREF(obj, a->get, i).getNum(); + _POPPLER_FREE(obj); + } + } +#if POPPLER_CHECK_VERSION(22, 9, 0) + state->setLineDash(std::vector<double> (dash, dash + length), args[1].getNum()); +#else + state->setLineDash(dash, length, args[1].getNum()); +#endif + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFlat(Object args[], int /*numArgs*/) +{ + state->setFlatness((int)args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineJoin(Object args[], int /*numArgs*/) +{ + state->setLineJoin(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineCap(Object args[], int /*numArgs*/) +{ + state->setLineCap(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetMiterLimit(Object args[], int /*numArgs*/) +{ + state->setMiterLimit(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetLineWidth(Object args[], int /*numArgs*/) +{ + state->setLineWidth(args[0].getNum()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetExtGState(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, obj4, obj5; + Function *funcs[4] = {nullptr, nullptr, nullptr, nullptr}; + GfxColor backdropColor; + GBool haveBackdropColor = gFalse; + GBool alpha = gFalse; + + _POPPLER_CALL_ARGS(obj1, res->lookupGState, args[0].getName()); + if (obj1.isNull()) { + return; + } + if (!obj1.isDict()) { + error(errSyntaxError, getPos(), "ExtGState '{0:s}' is wrong type"), args[0].getName(); + _POPPLER_FREE(obj1); + return; + } + if (printCommands) { + printf(" gfx state dict: "); + obj1.print(); + printf("\n"); + } + + // transparency support: blend mode, fill/stroke opacity + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "BM").isNull()) { + GfxBlendMode mode = gfxBlendNormal; + if (state->parseBlendMode(&obj2, &mode)) { + state->setBlendMode(mode); + } else { + error(errSyntaxError, getPos(), "Invalid blend mode in ExtGState"); + } + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "ca").isNum()) { + state->setFillOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "CA").isNum()) { + state->setStrokeOpacity(obj2.getNum()); + } + _POPPLER_FREE(obj2); + + // fill/stroke overprint + GBool haveFillOP = gFalse; + if ((haveFillOP = _POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "op").isBool())) { + state->setFillOverprint(obj2.getBool()); + } + _POPPLER_FREE(obj2); + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "OP").isBool()) { + state->setStrokeOverprint(obj2.getBool()); + if (!haveFillOP) { + state->setFillOverprint(obj2.getBool()); + } + } + _POPPLER_FREE(obj2); + + // stroke adjust + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SA").isBool()) { + state->setStrokeAdjust(obj2.getBool()); + } + _POPPLER_FREE(obj2); + + // transfer function + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "TR2").isNull()) { + _POPPLER_FREE(obj2); + _POPPLER_CALL_ARGS(obj2, obj1.dictLookup, "TR"); + } + if (obj2.isName(const_cast<char*>("Default")) || + obj2.isName(const_cast<char*>("Identity"))) { + funcs[0] = funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } else if (obj2.isArray() && obj2.arrayGetLength() == 4) { + int pos = 4; + for (int i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj3, obj2.arrayGet, i); + funcs[i] = Function::parse(&obj3); + _POPPLER_FREE(obj3); + if (!funcs[i]) { + pos = i; + break; + } + } + if (pos == 4) { + state->setTransfer(funcs); + } + } else if (obj2.isName() || obj2.isDict() || obj2.isStream()) { + if ((funcs[0] = Function::parse(&obj2))) { + funcs[1] = funcs[2] = funcs[3] = nullptr; + state->setTransfer(funcs); + } + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid transfer function in ExtGState"); + } + _POPPLER_FREE(obj2); + + // soft mask + if (!_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "SMask").isNull()) { + if (obj2.isName(const_cast<char*>("None"))) { + builder->clearSoftMask(state); + } else if (obj2.isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "S").isName("Alpha")) { + alpha = gTrue; + } else { // "Luminosity" + alpha = gFalse; + } + _POPPLER_FREE(obj3); + funcs[0] = nullptr; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "TR").isNull()) { + funcs[0] = Function::parse(&obj3); + if (funcs[0]->getInputSize() != 1 || + funcs[0]->getOutputSize() != 1) { + error(errSyntaxError, getPos(), "Invalid transfer function in soft mask in ExtGState"); + delete funcs[0]; + funcs[0] = nullptr; + } + } + _POPPLER_FREE(obj3); + if ((haveBackdropColor = _POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "BC").isArray())) { + for (int & i : backdropColor.c) { + i = 0; + } + for (int i = 0; i < obj3.arrayGetLength() && i < gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj4, obj3.arrayGet, i); + if (obj4.isNum()) { + backdropColor.c[i] = dblToCol(obj4.getNum()); + } + _POPPLER_FREE(obj4); + } + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj2.dictLookup, "G").isStream()) { + if (_POPPLER_CALL_ARGS_DEREF(obj4, obj3.streamGetDict()->lookup, "Group").isDict()) { + GfxColorSpace *blendingColorSpace = nullptr; + GBool isolated = gFalse; + GBool knockout = gFalse; + if (!_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "CS").isNull()) { + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj5, nullptr, state); + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "I").isBool()) { + isolated = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (_POPPLER_CALL_ARGS_DEREF(obj5, obj4.dictLookup, "K").isBool()) { + knockout = obj5.getBool(); + } + _POPPLER_FREE(obj5); + if (!haveBackdropColor) { + if (blendingColorSpace) { + blendingColorSpace->getDefaultColor(&backdropColor); + } else { + //~ need to get the parent or default color space (?) + for (int & i : backdropColor.c) { + i = 0; + } + } + } + doSoftMask(&obj3, alpha, blendingColorSpace, + isolated, knockout, funcs[0], &backdropColor); + if (funcs[0]) { + delete funcs[0]; + } + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj4); + } else { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState - missing group"); + } + _POPPLER_FREE(obj3); + } else if (!obj2.isNull()) { + error(errSyntaxError, getPos(), "Invalid soft mask in ExtGState"); + } + } + _POPPLER_FREE(obj2); + + _POPPLER_FREE(obj1); +} + +void PdfParser::doSoftMask(Object *str, GBool alpha, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor) { + Dict *dict, *resDict; + double m[6], bbox[4]; + Object obj1, obj2; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BBox"); + if (!obj1.isArray()) { + _POPPLER_FREE(obj1); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + bbox[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // get matrix + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Matrix"); + if (obj1.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, i); + m[i] = obj2.getNum(); + _POPPLER_FREE(obj2); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(obj1); + + // get resources + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Resources"); + resDict = obj1.isDict() ? obj1.getDict() : (Dict *)nullptr; + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, gTrue, gTrue, + blendingColorSpace, isolated, knockout, + alpha, transferFunc, backdropColor); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(obj1); +} + +void PdfParser::opSetRenderingIntent(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// color operators +//------------------------------------------------------------------------ + +/** + * Get a newly allocated color space instance by CS operation argument. + * + * Maintains a cache for named color spaces to avoid expensive re-parsing. + */ +GfxColorSpace *PdfParser::lookupColorSpaceCopy(Object &arg) +{ + assert(!arg.isNull()); + + char const *name = arg.isName() ? arg.getName() : nullptr; + GfxColorSpace *colorSpace = nullptr; + + if (name && (colorSpace = colorSpacesCache[name].get())) { + return colorSpace->copy(); + } + + Object *argPtr = &arg; + Object obj; + + if (name) { + _POPPLER_CALL_ARGS(obj, res->lookupColorSpace, name); + if (!obj.isNull()) { + argPtr = &obj; + } + } + + colorSpace = GfxColorSpace::parse(res, argPtr, nullptr, state); + + _POPPLER_FREE(obj); + + if (name && colorSpace) { + colorSpacesCache[name].reset(colorSpace->copy()); + } + + return colorSpace; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeGray(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceGrayColorSpace()); + color.c[0] = dblToCol(args[0].getNum()); + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + int i; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceCMYKColorSpace()); + for (i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeCMYKColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceCMYKColorSpace()); + for (int i = 0; i < 4; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillRGBColor(Object args[], int /*numArgs*/) +{ + GfxColor color; + + state->setFillPattern(nullptr); + state->setFillColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeRGBColor(Object args[], int /*numArgs*/) { + GfxColor color; + + state->setStrokePattern(nullptr); + state->setStrokeColorSpace(new GfxDeviceRGBColorSpace()); + for (int i = 0; i < 3; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFillColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setFillPattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setFillColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setFillColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (fill)"); + } +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetStrokeColorSpace(Object args[], int numArgs) +{ + assert(numArgs >= 1); + GfxColorSpace *colorSpace = lookupColorSpaceCopy(args[0]); + + state->setStrokePattern(nullptr); + + if (colorSpace) { + GfxColor color; + state->setStrokeColorSpace(colorSpace); + colorSpace->getDefaultColor(&color); + state->setStrokeColor(&color); + builder->updateStyle(state); + } else { + error(errSyntaxError, getPos(), "Bad color space (stroke)"); + } +} + +void PdfParser::opSetFillColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'sc' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setFillColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetStrokeColor(Object args[], int numArgs) { + GfxColor color; + int i; + + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SC' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs; ++i) { + color.c[i] = dblToCol(args[i].getNum()); + } + state->setStrokeColor(&color); + builder->updateStyle(state); +} + +void PdfParser::opSetFillColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getFillColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getFillColorSpace())->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getFillColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } + GfxPattern *pattern; + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), nullptr, state))) { + state->setFillPattern(pattern); + builder->updateStyle(state); + } + + } else { + if (numArgs != state->getFillColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'scn' command"); + return; + } + state->setFillPattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setFillColor(&color); + builder->updateStyle(state); + } +} + +void PdfParser::opSetStrokeColorN(Object args[], int numArgs) { + GfxColor color; + int i; + + if (state->getStrokeColorSpace()->getMode() == csPattern) { + if (numArgs > 1) { + if (!((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder() || + numArgs - 1 != ((GfxPatternColorSpace *)state->getStrokeColorSpace()) + ->getUnder()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + for (i = 0; i < numArgs - 1 && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } + GfxPattern *pattern; + if (args[numArgs-1].isName() && + (pattern = res->lookupPattern(args[numArgs-1].getName(), nullptr, state))) { + state->setStrokePattern(pattern); + builder->updateStyle(state); + } + + } else { + if (numArgs != state->getStrokeColorSpace()->getNComps()) { + error(errSyntaxError, getPos(), "Incorrect number of arguments in 'SCN' command"); + return; + } + state->setStrokePattern(nullptr); + for (i = 0; i < numArgs && i < gfxColorMaxComps; ++i) { + if (args[i].isNum()) { + color.c[i] = dblToCol(args[i].getNum()); + } + } + state->setStrokeColor(&color); + builder->updateStyle(state); + } +} + +//------------------------------------------------------------------------ +// path segment operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveTo(Object args[], int /*numArgs*/) +{ + state->moveTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opLineTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in lineto"); + return; + } + state->lineTo(args[0].getNum(), args[1].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = args[4].getNum(); + double y3 = args[5].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo1(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto1"); + return; + } + double x1 = state->getCurX(); + double y1 = state->getCurY(); + double x2 = args[0].getNum(); + double y2 = args[1].getNum(); + double x3 = args[2].getNum(); + double y3 = args[3].getNum(); + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opCurveTo2(Object args[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in curveto2"); + return; + } + double x1 = args[0].getNum(); + double y1 = args[1].getNum(); + double x2 = args[2].getNum(); + double y2 = args[3].getNum(); + double x3 = x2; + double y3 = y2; + state->curveTo(x1, y1, x2, y2, x3, y3); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opRectangle(Object args[], int /*numArgs*/) +{ + double x = args[0].getNum(); + double y = args[1].getNum(); + double w = args[2].getNum(); + double h = args[3].getNum(); + state->moveTo(x, y); + state->lineTo(x + w, y); + state->lineTo(x + w, y + h); + state->lineTo(x, y + h); + state->closePath(); +} + +void PdfParser::opClosePath(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + error(errSyntaxError, getPos(), "No current point in closepath"); + return; + } + state->closePath(); +} + +//------------------------------------------------------------------------ +// path painting operators +//------------------------------------------------------------------------ + +void PdfParser::opEndPath(Object /*args*/[], int /*numArgs*/) +{ + doEndPath(); +} + +void PdfParser::opStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in stroke")); + return; + } + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opCloseStroke(Object * /*args[]*/, int /*numArgs*/) { + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/stroke")); + return; + } + state->closePath(); + if (state->isPath()) { + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + doPatternStrokeFallback(); + } else { + builder->addPath(state, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in fill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gFalse); + } else { + builder->addPath(state, true, false); + } + } + doEndPath(); +} + +void PdfParser::opEOFill(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in eofill")); + return; + } + if (state->isPath()) { + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + doPatternFillFallback(gTrue); + } else { + builder->addPath(state, true, false, true); + } + } + doEndPath(); +} + +void PdfParser::opFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in fill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gFalse); + } else { + builder->addPath(state, true, true); + } + doEndPath(); +} + +void PdfParser::opCloseFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/fill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gFalse); + } + doEndPath(); +} + +void PdfParser::opEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in eofill/stroke")); + return; + } + if (state->isPath()) { + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::opCloseEOFillStroke(Object /*args*/[], int /*numArgs*/) +{ + if (!state->isCurPt()) { + //error(getPos(), const_cast<char*>("No path in closepath/eofill/stroke")); + return; + } + if (state->isPath()) { + state->closePath(); + doFillAndStroke(gTrue); + } + doEndPath(); +} + +void PdfParser::doFillAndStroke(GBool eoFill) { + GBool fillOk = gTrue, strokeOk = gTrue; + if (state->getFillColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getFillPattern())) { + fillOk = gFalse; + } + if (state->getStrokeColorSpace()->getMode() == csPattern && + !builder->isPatternTypeSupported(state->getStrokePattern())) { + strokeOk = gFalse; + } + if (fillOk && strokeOk) { + builder->addPath(state, true, true, eoFill); + } else { + doPatternFillFallback(eoFill); + doPatternStrokeFallback(); + } +} + +void PdfParser::doPatternFillFallback(GBool eoFill) { + GfxPattern *pattern; + + if (!(pattern = state->getFillPattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast<GfxShadingPattern *>(pattern), gFalse, eoFill); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type (%d) in fill", + pattern->getType()); + break; + } +} + +void PdfParser::doPatternStrokeFallback() { + GfxPattern *pattern; + + if (!(pattern = state->getStrokePattern())) { + return; + } + switch (pattern->getType()) { + case 1: + break; + case 2: + doShadingPatternFillFallback(static_cast<GfxShadingPattern *>(pattern), gTrue, gFalse); + break; + default: + error(errUnimplemented, getPos(), "Unimplemented pattern type ({0:d}) in stroke", + pattern->getType()); + break; + } +} + +void PdfParser::doShadingPatternFillFallback(GfxShadingPattern *sPat, + GBool stroke, GBool eoFill) { + GfxShading *shading; + GfxPath *savedPath; + const double *ctm, *btm, *ptm; + double m[6], ictm[6], m1[6]; + double xMin, yMin, xMax, yMax; + double det; + + shading = sPat->getShading(); + + // save current graphics state + savedPath = state->getPath()->copy(); + saveState(); + + // clip to bbox + if (false ){//shading->getHasBBox()) { + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + state->closePath(); + state->clip(); + //builder->clip(state); + state->setPath(savedPath->copy()); + } + + // clip to current path + if (stroke) { + state->clipToStrokePath(); + //out->clipToStrokePath(state); + } else { + state->clip(); + if (eoFill) { + builder->setClipPath(state, true); + } else { + builder->setClipPath(state); + } + } + + // set the color space + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // background color fill + if (shading->getHasBackground()) { + state->setFillColor(shading->getBackground()); + builder->addPath(state, true, false); + } + state->clearPath(); + + // construct a (pattern space) -> (current space) transform matrix + ctm = state->getCTM(); + btm = baseMatrix; + ptm = sPat->getMatrix(); + // iCTM = invert CTM + det = 1 / (ctm[0] * ctm[3] - ctm[1] * ctm[2]); + ictm[0] = ctm[3] * det; + ictm[1] = -ctm[1] * det; + ictm[2] = -ctm[2] * det; + ictm[3] = ctm[0] * det; + ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) * det; + ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) * det; + // m1 = PTM * BTM = PTM * base transform matrix + m1[0] = ptm[0] * btm[0] + ptm[1] * btm[2]; + m1[1] = ptm[0] * btm[1] + ptm[1] * btm[3]; + m1[2] = ptm[2] * btm[0] + ptm[3] * btm[2]; + m1[3] = ptm[2] * btm[1] + ptm[3] * btm[3]; + m1[4] = ptm[4] * btm[0] + ptm[5] * btm[2] + btm[4]; + m1[5] = ptm[4] * btm[1] + ptm[5] * btm[3] + btm[5]; + // m = m1 * iCTM = (PTM * BTM) * (iCTM) + m[0] = m1[0] * ictm[0] + m1[1] * ictm[2]; + m[1] = m1[0] * ictm[1] + m1[1] * ictm[3]; + m[2] = m1[2] * ictm[0] + m1[3] * ictm[2]; + m[3] = m1[2] * ictm[1] + m1[3] * ictm[3]; + m[4] = m1[4] * ictm[0] + m1[5] * ictm[2] + ictm[4]; + m[5] = m1[4] * ictm[1] + m1[5] * ictm[3] + ictm[5]; + + // set the new matrix + state->concatCTM(m[0], m[1], m[2], m[3], m[4], m[5]); + builder->setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: + doFunctionShFill(static_cast<GfxFunctionShading *>(shading)); + break; + case 2: + case 3: + // no need to implement these + break; + case 4: + case 5: + doGouraudTriangleShFill(static_cast<GfxGouraudTriangleShading *>(shading)); + break; + case 6: + case 7: + doPatchMeshShFill(static_cast<GfxPatchMeshShading *>(shading)); + break; + } + + // restore graphics state + restoreState(); + state->setPath(savedPath); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShFill(Object args[], int /*numArgs*/) +{ + GfxShading *shading = nullptr; + GfxPath *savedPath = nullptr; + double xMin, yMin, xMax, yMax; + double xTemp, yTemp; + double gradientTransform[6]; + double *matrix = nullptr; + GBool savedState = gFalse; + + if (!(shading = res->lookupShading(args[0].getName(), nullptr, state))) { + return; + } + + // save current graphics state + if (shading->getType() != 2 && shading->getType() != 3) { + savedPath = state->getPath()->copy(); + saveState(); + savedState = gTrue; + } else { // get gradient transform if possible + // check proper operator sequence + // first there should be one W(*) and then one 'cm' somewhere before 'sh' + GBool seenClip, seenConcat; + seenClip = (clipHistory->getClipPath() != nullptr); + seenConcat = gFalse; + int i = 1; + while (i <= maxOperatorHistoryDepth) { + const char *opName = getPreviousOperator(i); + if (!strcmp(opName, "cm")) { + if (seenConcat) { // more than one 'cm' + break; + } else { + seenConcat = gTrue; + } + } + i++; + } + + if (seenConcat && seenClip) { + if (builder->getTransform(gradientTransform)) { + matrix = (double*)&gradientTransform; + builder->setTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0); // remove transform + } + } + } + + // clip to bbox + if (shading->getHasBBox()) { + shading->getBBox(&xMin, &yMin, &xMax, &yMax); + if (matrix != nullptr) { + xTemp = matrix[0]*xMin + matrix[2]*yMin + matrix[4]; + yTemp = matrix[1]*xMin + matrix[3]*yMin + matrix[5]; + state->moveTo(xTemp, yTemp); + xTemp = matrix[0]*xMax + matrix[2]*yMin + matrix[4]; + yTemp = matrix[1]*xMax + matrix[3]*yMin + matrix[5]; + state->lineTo(xTemp, yTemp); + xTemp = matrix[0]*xMax + matrix[2]*yMax + matrix[4]; + yTemp = matrix[1]*xMax + matrix[3]*yMax + matrix[5]; + state->lineTo(xTemp, yTemp); + xTemp = matrix[0]*xMin + matrix[2]*yMax + matrix[4]; + yTemp = matrix[1]*xMin + matrix[3]*yMax + matrix[5]; + state->lineTo(xTemp, yTemp); + } + else { + state->moveTo(xMin, yMin); + state->lineTo(xMax, yMin); + state->lineTo(xMax, yMax); + state->lineTo(xMin, yMax); + } + state->closePath(); + state->clip(); + if (savedState) + builder->setClipPath(state); + else + builder->clip(state); + state->clearPath(); + } + + // set the color space + if (savedState) + state->setFillColorSpace(shading->getColorSpace()->copy()); + + // do shading type-specific operations + switch (shading->getType()) { + case 1: + doFunctionShFill(static_cast<GfxFunctionShading *>(shading)); + break; + case 2: + case 3: + if (clipHistory->getClipPath()) { + builder->addShadedFill(shading, matrix, clipHistory->getClipPath(), + clipHistory->getClipType() == clipEO ? true : false); + } + break; + case 4: + case 5: + doGouraudTriangleShFill(static_cast<GfxGouraudTriangleShading *>(shading)); + break; + case 6: + case 7: + doPatchMeshShFill(static_cast<GfxPatchMeshShading *>(shading)); + break; + } + + // restore graphics state + if (savedState) { + restoreState(); + state->setPath(savedPath); + } + + delete shading; +} + +void PdfParser::doFunctionShFill(GfxFunctionShading *shading) { + double x0, y0, x1, y1; + GfxColor colors[4]; + + shading->getDomain(&x0, &y0, &x1, &y1); + shading->getColor(x0, y0, &colors[0]); + shading->getColor(x0, y1, &colors[1]); + shading->getColor(x1, y0, &colors[2]); + shading->getColor(x1, y1, &colors[3]); + doFunctionShFill1(shading, x0, y0, x1, y1, colors, 0); +} + +void PdfParser::doFunctionShFill1(GfxFunctionShading *shading, + double x0, double y0, + double x1, double y1, + GfxColor *colors, int depth) { + GfxColor fillColor; + GfxColor color0M, color1M, colorM0, colorM1, colorMM; + GfxColor colors2[4]; + double functionColorDelta = colorDeltas[pdfFunctionShading-1]; + const double *matrix; + double xM, yM; + int nComps, i, j; + + nComps = shading->getColorSpace()->getNComps(); + matrix = shading->getMatrix(); + + // compare the four corner colors + for (i = 0; i < 4; ++i) { + for (j = 0; j < nComps; ++j) { + if (abs(colors[i].c[j] - colors[(i+1)&3].c[j]) > functionColorDelta) { + break; + } + } + if (j < nComps) { + break; + } + } + + // center of the rectangle + xM = 0.5 * (x0 + x1); + yM = 0.5 * (y0 + y1); + + // the four corner colors are close (or we hit the recursive limit) + // -- fill the rectangle; but require at least one subdivision + // (depth==0) to avoid problems when the four outer corners of the + // shaded region are the same color + if ((i == 4 && depth > 0) || depth == maxDepths[pdfFunctionShading-1]) { + + // use the center color + shading->getColor(xM, yM, &fillColor); + state->setFillColor(&fillColor); + + // fill the rectangle + state->moveTo(x0 * matrix[0] + y0 * matrix[2] + matrix[4], + x0 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y0 * matrix[2] + matrix[4], + x1 * matrix[1] + y0 * matrix[3] + matrix[5]); + state->lineTo(x1 * matrix[0] + y1 * matrix[2] + matrix[4], + x1 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->lineTo(x0 * matrix[0] + y1 * matrix[2] + matrix[4], + x0 * matrix[1] + y1 * matrix[3] + matrix[5]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + + // the four corner colors are not close enough -- subdivide the + // rectangle + } else { + + // colors[0] colorM0 colors[2] + // (x0,y0) (xM,y0) (x1,y0) + // +----------+----------+ + // | | | + // | UL | UR | + // color0M | colorMM | color1M + // (x0,yM) +----------+----------+ (x1,yM) + // | (xM,yM) | + // | LL | LR | + // | | | + // +----------+----------+ + // colors[1] colorM1 colors[3] + // (x0,y1) (xM,y1) (x1,y1) + + shading->getColor(x0, yM, &color0M); + shading->getColor(x1, yM, &color1M); + shading->getColor(xM, y0, &colorM0); + shading->getColor(xM, y1, &colorM1); + shading->getColor(xM, yM, &colorMM); + + // upper-left sub-rectangle + colors2[0] = colors[0]; + colors2[1] = color0M; + colors2[2] = colorM0; + colors2[3] = colorMM; + doFunctionShFill1(shading, x0, y0, xM, yM, colors2, depth + 1); + + // lower-left sub-rectangle + colors2[0] = color0M; + colors2[1] = colors[1]; + colors2[2] = colorMM; + colors2[3] = colorM1; + doFunctionShFill1(shading, x0, yM, xM, y1, colors2, depth + 1); + + // upper-right sub-rectangle + colors2[0] = colorM0; + colors2[1] = colorMM; + colors2[2] = colors[2]; + colors2[3] = color1M; + doFunctionShFill1(shading, xM, y0, x1, yM, colors2, depth + 1); + + // lower-right sub-rectangle + colors2[0] = colorMM; + colors2[1] = colorM1; + colors2[2] = color1M; + colors2[3] = colors[3]; + doFunctionShFill1(shading, xM, yM, x1, y1, colors2, depth + 1); + } +} + +void PdfParser::doGouraudTriangleShFill(GfxGouraudTriangleShading *shading) { + double x0, y0, x1, y1, x2, y2; + GfxColor color0, color1, color2; + int i; + + for (i = 0; i < shading->getNTriangles(); ++i) { + shading->getTriangle(i, &x0, &y0, &color0, + &x1, &y1, &color1, + &x2, &y2, &color2); + gouraudFillTriangle(x0, y0, &color0, x1, y1, &color1, x2, y2, &color2, + shading->getColorSpace()->getNComps(), 0); + } +} + +void PdfParser::gouraudFillTriangle(double x0, double y0, GfxColor *color0, + double x1, double y1, GfxColor *color1, + double x2, double y2, GfxColor *color2, + int nComps, int depth) { + double x01, y01, x12, y12, x20, y20; + double gouraudColorDelta = colorDeltas[pdfGouraudTriangleShading-1]; + GfxColor color01, color12, color20; + int i; + + for (i = 0; i < nComps; ++i) { + if (abs(color0->c[i] - color1->c[i]) > gouraudColorDelta || + abs(color1->c[i] - color2->c[i]) > gouraudColorDelta) { + break; + } + } + if (i == nComps || depth == maxDepths[pdfGouraudTriangleShading-1]) { + state->setFillColor(color0); + state->moveTo(x0, y0); + state->lineTo(x1, y1); + state->lineTo(x2, y2); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + x01 = 0.5 * (x0 + x1); + y01 = 0.5 * (y0 + y1); + x12 = 0.5 * (x1 + x2); + y12 = 0.5 * (y1 + y2); + x20 = 0.5 * (x2 + x0); + y20 = 0.5 * (y2 + y0); + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + color01.c[i] = (color0->c[i] + color1->c[i]) / 2; + color12.c[i] = (color1->c[i] + color2->c[i]) / 2; + color20.c[i] = (color2->c[i] + color0->c[i]) / 2; + } + gouraudFillTriangle(x0, y0, color0, x01, y01, &color01, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x1, y1, color1, + x12, y12, &color12, nComps, depth + 1); + gouraudFillTriangle(x01, y01, &color01, x12, y12, &color12, + x20, y20, &color20, nComps, depth + 1); + gouraudFillTriangle(x20, y20, &color20, x12, y12, &color12, + x2, y2, color2, nComps, depth + 1); + } +} + +void PdfParser::doPatchMeshShFill(GfxPatchMeshShading *shading) { + int start, i; + + if (shading->getNPatches() > 128) { + start = 3; + } else if (shading->getNPatches() > 64) { + start = 2; + } else if (shading->getNPatches() > 16) { + start = 1; + } else { + start = 0; + } + for (i = 0; i < shading->getNPatches(); ++i) { + fillPatch(shading->getPatch(i), shading->getColorSpace()->getNComps(), + start); + } +} + +void PdfParser::fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth) { + GfxPatch patch00 = blankPatch(); + GfxPatch patch01 = blankPatch(); + GfxPatch patch10 = blankPatch(); + GfxPatch patch11 = blankPatch(); + GfxColor color = {{0}}; + double xx[4][8]; + double yy[4][8]; + double xxm; + double yym; + double patchColorDelta = colorDeltas[pdfPatchMeshShading - 1]; + + int i; + + for (i = 0; i < nComps; ++i) { + if (std::abs(patch->color[0][0].c[i] - patch->color[0][1].c[i]) + > patchColorDelta || + std::abs(patch->color[0][1].c[i] - patch->color[1][1].c[i]) + > patchColorDelta || + std::abs(patch->color[1][1].c[i] - patch->color[1][0].c[i]) + > patchColorDelta || + std::abs(patch->color[1][0].c[i] - patch->color[0][0].c[i]) + > patchColorDelta) { + break; + } + color.c[i] = GfxColorComp(patch->color[0][0].c[i]); + } + if (i == nComps || depth == maxDepths[pdfPatchMeshShading-1]) { + state->setFillColor(&color); + state->moveTo(patch->x[0][0], patch->y[0][0]); + state->curveTo(patch->x[0][1], patch->y[0][1], + patch->x[0][2], patch->y[0][2], + patch->x[0][3], patch->y[0][3]); + state->curveTo(patch->x[1][3], patch->y[1][3], + patch->x[2][3], patch->y[2][3], + patch->x[3][3], patch->y[3][3]); + state->curveTo(patch->x[3][2], patch->y[3][2], + patch->x[3][1], patch->y[3][1], + patch->x[3][0], patch->y[3][0]); + state->curveTo(patch->x[2][0], patch->y[2][0], + patch->x[1][0], patch->y[1][0], + patch->x[0][0], patch->y[0][0]); + state->closePath(); + builder->addPath(state, true, false); + state->clearPath(); + } else { + for (i = 0; i < 4; ++i) { + xx[i][0] = patch->x[i][0]; + yy[i][0] = patch->y[i][0]; + xx[i][1] = 0.5 * (patch->x[i][0] + patch->x[i][1]); + yy[i][1] = 0.5 * (patch->y[i][0] + patch->y[i][1]); + xxm = 0.5 * (patch->x[i][1] + patch->x[i][2]); + yym = 0.5 * (patch->y[i][1] + patch->y[i][2]); + xx[i][6] = 0.5 * (patch->x[i][2] + patch->x[i][3]); + yy[i][6] = 0.5 * (patch->y[i][2] + patch->y[i][3]); + xx[i][2] = 0.5 * (xx[i][1] + xxm); + yy[i][2] = 0.5 * (yy[i][1] + yym); + xx[i][5] = 0.5 * (xxm + xx[i][6]); + yy[i][5] = 0.5 * (yym + yy[i][6]); + xx[i][3] = xx[i][4] = 0.5 * (xx[i][2] + xx[i][5]); + yy[i][3] = yy[i][4] = 0.5 * (yy[i][2] + yy[i][5]); + xx[i][7] = patch->x[i][3]; + yy[i][7] = patch->y[i][3]; + } + for (i = 0; i < 4; ++i) { + patch00.x[0][i] = xx[0][i]; + patch00.y[0][i] = yy[0][i]; + patch00.x[1][i] = 0.5 * (xx[0][i] + xx[1][i]); + patch00.y[1][i] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch10.x[2][i] = 0.5 * (xx[2][i] + xx[3][i]); + patch10.y[2][i] = 0.5 * (yy[2][i] + yy[3][i]); + patch00.x[2][i] = 0.5 * (patch00.x[1][i] + xxm); + patch00.y[2][i] = 0.5 * (patch00.y[1][i] + yym); + patch10.x[1][i] = 0.5 * (xxm + patch10.x[2][i]); + patch10.y[1][i] = 0.5 * (yym + patch10.y[2][i]); + patch00.x[3][i] = 0.5 * (patch00.x[2][i] + patch10.x[1][i]); + patch00.y[3][i] = 0.5 * (patch00.y[2][i] + patch10.y[1][i]); + patch10.x[0][i] = patch00.x[3][i]; + patch10.y[0][i] = patch00.y[3][i]; + patch10.x[3][i] = xx[3][i]; + patch10.y[3][i] = yy[3][i]; + } + for (i = 4; i < 8; ++i) { + patch01.x[0][i-4] = xx[0][i]; + patch01.y[0][i-4] = yy[0][i]; + patch01.x[1][i-4] = 0.5 * (xx[0][i] + xx[1][i]); + patch01.y[1][i-4] = 0.5 * (yy[0][i] + yy[1][i]); + xxm = 0.5 * (xx[1][i] + xx[2][i]); + yym = 0.5 * (yy[1][i] + yy[2][i]); + patch11.x[2][i-4] = 0.5 * (xx[2][i] + xx[3][i]); + patch11.y[2][i-4] = 0.5 * (yy[2][i] + yy[3][i]); + patch01.x[2][i-4] = 0.5 * (patch01.x[1][i-4] + xxm); + patch01.y[2][i-4] = 0.5 * (patch01.y[1][i-4] + yym); + patch11.x[1][i-4] = 0.5 * (xxm + patch11.x[2][i-4]); + patch11.y[1][i-4] = 0.5 * (yym + patch11.y[2][i-4]); + patch01.x[3][i-4] = 0.5 * (patch01.x[2][i-4] + patch11.x[1][i-4]); + patch01.y[3][i-4] = 0.5 * (patch01.y[2][i-4] + patch11.y[1][i-4]); + patch11.x[0][i-4] = patch01.x[3][i-4]; + patch11.y[0][i-4] = patch01.y[3][i-4]; + patch11.x[3][i-4] = xx[3][i]; + patch11.y[3][i-4] = yy[3][i]; + } + //~ if the shading has a Function, this should interpolate on the + //~ function parameter, not on the color components + for (i = 0; i < nComps; ++i) { + patch00.color[0][0].c[i] = patch->color[0][0].c[i]; + patch00.color[0][1].c[i] = (patch->color[0][0].c[i] + + patch->color[0][1].c[i]) / 2; + patch01.color[0][0].c[i] = patch00.color[0][1].c[i]; + patch01.color[0][1].c[i] = patch->color[0][1].c[i]; + patch01.color[1][1].c[i] = (patch->color[0][1].c[i] + + patch->color[1][1].c[i]) / 2; + patch11.color[0][1].c[i] = patch01.color[1][1].c[i]; + patch11.color[1][1].c[i] = patch->color[1][1].c[i]; + patch11.color[1][0].c[i] = (patch->color[1][1].c[i] + + patch->color[1][0].c[i]) / 2; + patch10.color[1][1].c[i] = patch11.color[1][0].c[i]; + patch10.color[1][0].c[i] = patch->color[1][0].c[i]; + patch10.color[0][0].c[i] = (patch->color[1][0].c[i] + + patch->color[0][0].c[i]) / 2; + patch00.color[1][0].c[i] = patch10.color[0][0].c[i]; + patch00.color[1][1].c[i] = (patch00.color[1][0].c[i] + + patch01.color[1][1].c[i]) / 2; + patch01.color[1][0].c[i] = patch00.color[1][1].c[i]; + patch11.color[0][0].c[i] = patch00.color[1][1].c[i]; + patch10.color[0][1].c[i] = patch00.color[1][1].c[i]; + } + fillPatch(&patch00, nComps, depth + 1); + fillPatch(&patch10, nComps, depth + 1); + fillPatch(&patch01, nComps, depth + 1); + fillPatch(&patch11, nComps, depth + 1); + } +} + +void PdfParser::doEndPath() { + if (state->isCurPt() && clip != clipNone) { + state->clip(); + if (clip == clipNormal) { + clipHistory->setClip(state->getPath(), clipNormal); + builder->clip(state); + } else { + clipHistory->setClip(state->getPath(), clipEO); + builder->clip(state, true); + } + } + clip = clipNone; + state->clearPath(); +} + +//------------------------------------------------------------------------ +// path clipping operators +//------------------------------------------------------------------------ + +void PdfParser::opClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipNormal; +} + +void PdfParser::opEOClip(Object /*args*/[], int /*numArgs*/) +{ + clip = clipEO; +} + +//------------------------------------------------------------------------ +// text object operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginText(Object /*args*/[], int /*numArgs*/) +{ + state->setTextMat(1, 0, 0, 1, 0, 0); + state->textMoveTo(0, 0); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; + builder->beginTextObject(state); +} + +void PdfParser::opEndText(Object /*args*/[], int /*numArgs*/) +{ + builder->endTextObject(state); +} + +//------------------------------------------------------------------------ +// text state operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetCharSpacing(Object args[], int /*numArgs*/) +{ + state->setCharSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetFont(Object args[], int /*numArgs*/) +{ + auto font = res->lookupFont(args[0].getName()); + + if (!font) { + // unsetting the font (drawing no text) is better than using the + // previous one and drawing random glyphs from it + state->setFont(nullptr, args[1].getNum()); + fontChanged = gTrue; + return; + } + if (printCommands) { + printf(" font: tag=%s name='%s' %g\n", +#if POPPLER_CHECK_VERSION(21,11,0) + font->getTag().c_str(), +#else + font->getTag()->getCString(), +#endif + font->getName() ? font->getName()->getCString() : "???", + args[1].getNum()); + fflush(stdout); + } + +#if !POPPLER_CHECK_VERSION(22, 4, 0) + font->incRefCnt(); +#endif + state->setFont(font, args[1].getNum()); + fontChanged = gTrue; +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextLeading(Object args[], int /*numArgs*/) +{ + state->setLeading(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRender(Object args[], int /*numArgs*/) +{ + state->setRender(args[0].getInt()); + builder->updateStyle(state); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextRise(Object args[], int /*numArgs*/) +{ + state->setRise(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetWordSpacing(Object args[], int /*numArgs*/) +{ + state->setWordSpace(args[0].getNum()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetHorizScaling(Object args[], int /*numArgs*/) +{ + state->setHorizScaling(args[0].getNum()); + builder->updateTextMatrix(state); + fontChanged = gTrue; +} + +//------------------------------------------------------------------------ +// text positioning operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMove(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = state->getLineY() + args[1].getNum(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opTextMoveSet(Object args[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX() + args[0].getNum(); + ty = args[1].getNum(); + state->setLeading(-ty); + ty += state->getLineY(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opSetTextMatrix(Object args[], int /*numArgs*/) +{ + state->setTextMat(args[0].getNum(), args[1].getNum(), + args[2].getNum(), args[3].getNum(), + args[4].getNum(), args[5].getNum()); + state->textMoveTo(0, 0); + builder->updateTextMatrix(state); + builder->updateTextPosition(0.0, 0.0); + fontChanged = gTrue; +} + +void PdfParser::opTextNextLine(Object /*args*/[], int /*numArgs*/) +{ + double tx, ty; + + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); +} + +//------------------------------------------------------------------------ +// text string operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowText(Object args[], int /*numArgs*/) +{ + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[0].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opMoveSetShowText(Object args[], int /*numArgs*/) +{ + double tx = 0; + double ty = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in move/set/show"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + state->setWordSpace(args[0].getNum()); + state->setCharSpace(args[1].getNum()); + tx = state->getLineX(); + ty = state->getLineY() - state->getLeading(); + state->textMoveTo(tx, ty); + builder->updateTextPosition(tx, ty); + doShowText(args[2].getString()); +} + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opShowSpaceText(Object args[], int /*numArgs*/) +{ + Array *a = nullptr; + Object obj; + int wMode = 0; + + if (!state->getFont()) { + error(errSyntaxError, getPos(), "No font in show/space"); + return; + } + if (fontChanged) { + builder->updateFont(state); + fontChanged = gFalse; + } + wMode = state->getFont()->getWMode(); + a = args[0].getArray(); + for (int i = 0; i < a->getLength(); ++i) { + _POPPLER_CALL_ARGS(obj, a->get, i); + if (obj.isNum()) { + // this uses the absolute value of the font size to match + // Acrobat's behavior + if (wMode) { + state->textShift(0, -obj.getNum() * 0.001 * + fabs(state->getFontSize())); + } else { + state->textShift(-obj.getNum() * 0.001 * + fabs(state->getFontSize()), 0); + } + builder->updateTextShift(state, obj.getNum()); + } else if (obj.isString()) { + doShowText(obj.getString()); + } else { + error(errSyntaxError, getPos(), "Element of show/space array must be number or string"); + } + _POPPLER_FREE(obj); + } +} + +#if POPPLER_CHECK_VERSION(0,64,0) +void PdfParser::doShowText(const GooString *s) { +#else +void PdfParser::doShowText(GooString *s) { +#endif + int wMode; + double riseX, riseY; + CharCode code; + Unicode _POPPLER_CONST_82 *u = nullptr; + double x, y, dx, dy, tdx, tdy; + double originX, originY, tOriginX, tOriginY; + double oldCTM[6], newCTM[6]; + const double *mat; + Object charProc; + Dict *resDict; + Parser *oldParser; +#if POPPLER_CHECK_VERSION(0,64,0) + const char *p; +#else + char *p; +#endif + int len, n, uLen; + + auto font = state->getFont(); + wMode = font->getWMode(); + + builder->beginString(state); + + // handle a Type 3 char + if (font->getType() == fontType3 && false) {//out->interpretType3Chars()) { + mat = state->getCTM(); + for (int i = 0; i < 6; ++i) { + oldCTM[i] = mat[i]; + } + mat = state->getTextMat(); + newCTM[0] = mat[0] * oldCTM[0] + mat[1] * oldCTM[2]; + newCTM[1] = mat[0] * oldCTM[1] + mat[1] * oldCTM[3]; + newCTM[2] = mat[2] * oldCTM[0] + mat[3] * oldCTM[2]; + newCTM[3] = mat[2] * oldCTM[1] + mat[3] * oldCTM[3]; + mat = font->getFontMatrix(); + newCTM[0] = mat[0] * newCTM[0] + mat[1] * newCTM[2]; + newCTM[1] = mat[0] * newCTM[1] + mat[1] * newCTM[3]; + newCTM[2] = mat[2] * newCTM[0] + mat[3] * newCTM[2]; + newCTM[3] = mat[2] * newCTM[1] + mat[3] * newCTM[3]; + newCTM[0] *= state->getFontSize(); + newCTM[1] *= state->getFontSize(); + newCTM[2] *= state->getFontSize(); + newCTM[3] *= state->getFontSize(); + newCTM[0] *= state->getHorizScaling(); + newCTM[2] *= state->getHorizScaling(); + state->textTransformDelta(0, state->getRise(), &riseX, &riseY); + double curX = state->getCurX(); + double curY = state->getCurY(); + double lineX = state->getLineX(); + double lineY = state->getLineY(); + oldParser = parser; + p = s->getCString(); + len = s->getLength(); + while (len > 0) { + n = font->getNextChar(p, len, &code, + &u, &uLen, /* TODO: This looks like a memory leak for u. */ + &dx, &dy, &originX, &originY); + dx = dx * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dx += state->getWordSpace(); + } + dx *= state->getHorizScaling(); + dy *= state->getFontSize(); + state->textTransformDelta(dx, dy, &tdx, &tdy); + state->transform(curX + riseX, curY + riseY, &x, &y); + saveState(); + state->setCTM(newCTM[0], newCTM[1], newCTM[2], newCTM[3], x, y); + //~ the CTM concat values here are wrong (but never used) + //out->updateCTM(state, 1, 0, 0, 1, 0, 0); + if (false){ /*!out->beginType3Char(state, curX + riseX, curY + riseY, tdx, tdy, + code, u, uLen)) {*/ + _POPPLER_CALL_ARGS(charProc, _POPPLER_FONTPTR_TO_GFX8(font)->getCharProc, code); + if (resDict = _POPPLER_FONTPTR_TO_GFX8(font)->getResources()) { + pushResources(resDict); + } + if (charProc.isStream()) { + //parse(&charProc, gFalse); // TODO: parse into SVG font + } else { + error(errSyntaxError, getPos(), "Missing or bad Type3 CharProc entry"); + } + //out->endType3Char(state); + if (resDict) { + popResources(); + } + _POPPLER_FREE(charProc); + } + restoreState(); + // GfxState::restore() does *not* restore the current position, + // so we deal with it here using (curX, curY) and (lineX, lineY) + curX += tdx; + curY += tdy; + state->moveTo(curX, curY); + state->textSetPos(lineX, lineY); + p += n; + len -= n; + } + parser = oldParser; + + } else { + state->textTransformDelta(0, state->getRise(), &riseX, &riseY); + p = s->getCString(); + len = s->getLength(); + while (len > 0) { + n = font->getNextChar(p, len, &code, + &u, &uLen, /* TODO: This looks like a memory leak for u. */ + &dx, &dy, &originX, &originY); + + if (wMode) { + dx *= state->getFontSize(); + dy = dy * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dy += state->getWordSpace(); + } + } else { + dx = dx * state->getFontSize() + state->getCharSpace(); + if (n == 1 && *p == ' ') { + dx += state->getWordSpace(); + } + dx *= state->getHorizScaling(); + dy *= state->getFontSize(); + } + state->textTransformDelta(dx, dy, &tdx, &tdy); + originX *= state->getFontSize(); + originY *= state->getFontSize(); + state->textTransformDelta(originX, originY, &tOriginX, &tOriginY); + builder->addChar(state, state->getCurX() + riseX, state->getCurY() + riseY, + dx, dy, tOriginX, tOriginY, code, n, u, uLen); + state->shift(tdx, tdy); + p += n; + len -= n; + } + } + + builder->endString(state); +} + + +//------------------------------------------------------------------------ +// XObject operators +//------------------------------------------------------------------------ + +// TODO not good that numArgs is ignored but args[] is used: +void PdfParser::opXObject(Object args[], int /*numArgs*/) +{ + Object obj1, obj2, obj3, refObj; + +#if POPPLER_CHECK_VERSION(0,64,0) + const char *name = args[0].getName(); +#else + char *name = args[0].getName(); +#endif + _POPPLER_CALL_ARGS(obj1, res->lookupXObject, name); + if (obj1.isNull()) { + return; + } + if (!obj1.isStream()) { + error(errSyntaxError, getPos(), "XObject '{0:s}' is wrong type", name); + _POPPLER_FREE(obj1); + return; + } + _POPPLER_CALL_ARGS(obj2, obj1.streamGetDict()->lookup, "Subtype"); + if (obj2.isName(const_cast<char*>("Image"))) { + _POPPLER_CALL_ARGS(refObj, res->lookupXObjectNF, name); + doImage(&refObj, obj1.getStream(), gFalse); + _POPPLER_FREE(refObj); + } else if (obj2.isName(const_cast<char*>("Form"))) { + doForm(&obj1); + } else if (obj2.isName(const_cast<char*>("PS"))) { + _POPPLER_CALL_ARGS(obj3, obj1.streamGetDict()->lookup, "Level1"); +/* out->psXObject(obj1.getStream(), + obj3.isStream() ? obj3.getStream() : (Stream *)NULL);*/ + } else if (obj2.isName()) { + error(errSyntaxError, getPos(), "Unknown XObject subtype '{0:s}'", obj2.getName()); + } else { + error(errSyntaxError, getPos(), "XObject subtype is missing or wrong type"); + } + _POPPLER_FREE(obj2); + _POPPLER_FREE(obj1); +} + +void PdfParser::doImage(Object * /*ref*/, Stream *str, GBool inlineImg) +{ + Dict *dict; + int width, height; + int bits; + GBool interpolate; + StreamColorSpaceMode csMode; + GBool mask; + GBool invert; + Object maskObj, smaskObj; + GBool haveColorKeyMask, haveExplicitMask, haveSoftMask; + GBool maskInvert; + GBool maskInterpolate; + Object obj1, obj2; + + // get info from the stream + bits = 0; + csMode = streamCSNone; + str->getImageParams(&bits, &csMode); + + // get stream dict + dict = str->getDict(); + + // get size + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "W"); + } + if (obj1.isInt()){ + width = obj1.getInt(); + } + else if (obj1.isReal()) { + width = (int)obj1.getReal(); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "H"); + } + if (obj1.isInt()) { + height = obj1.getInt(); + } + else if (obj1.isReal()){ + height = static_cast<int>(obj1.getReal()); + } + else { + goto err2; + } + _POPPLER_FREE(obj1); + + // image interpolation + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "I"); + } + if (obj1.isBool()) + interpolate = obj1.getBool(); + else + interpolate = gFalse; + _POPPLER_FREE(obj1); + maskInterpolate = gFalse; + + // image or mask? + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "IM"); + } + mask = gFalse; + if (obj1.isBool()) { + mask = obj1.getBool(); + } + else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // bit depth + if (bits == 0) { + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "BPC"); + } + if (obj1.isInt()) { + bits = obj1.getInt(); + } else if (mask) { + bits = 1; + } else { + goto err2; + } + _POPPLER_FREE(obj1); + } + + // display a mask + if (mask) { + // check for inverted mask + if (bits != 1) { + goto err1; + } + invert = gFalse; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + invert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + + // draw it + builder->addImageMask(state, str, width, height, invert, interpolate); + + } else { + // get color space and color map + GfxColorSpace *colorSpace; + _POPPLER_CALL_ARGS(obj1, dict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "CS"); + } + if (!obj1.isNull()) { + colorSpace = lookupColorSpaceCopy(obj1); + } else if (csMode == streamCSDeviceGray) { + colorSpace = new GfxDeviceGrayColorSpace(); + } else if (csMode == streamCSDeviceRGB) { + colorSpace = new GfxDeviceRGBColorSpace(); + } else if (csMode == streamCSDeviceCMYK) { + colorSpace = new GfxDeviceCMYKColorSpace(); + } else { + colorSpace = nullptr; + } + _POPPLER_FREE(obj1); + if (!colorSpace) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, dict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, dict->lookup, "D"); + } + GfxImageColorMap *colorMap = new GfxImageColorMap(bits, &obj1, colorSpace); + _POPPLER_FREE(obj1); + if (!colorMap->isOk()) { + delete colorMap; + goto err1; + } + + // get the mask + int maskColors[2*gfxColorMaxComps]; + haveColorKeyMask = haveExplicitMask = haveSoftMask = gFalse; + Stream *maskStr = nullptr; + int maskWidth = 0; + int maskHeight = 0; + maskInvert = gFalse; + GfxImageColorMap *maskColorMap = nullptr; + _POPPLER_CALL_ARGS(maskObj, dict->lookup, "Mask"); + _POPPLER_CALL_ARGS(smaskObj, dict->lookup, "SMask"); + Dict* maskDict; + if (smaskObj.isStream()) { + // soft mask + if (inlineImg) { + goto err1; + } + maskStr = smaskObj.getStream(); + maskDict = smaskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BitsPerComponent"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "BPC"); + } + if (!obj1.isInt()) { + goto err2; + } + int maskBits = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ColorSpace"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "CS"); + } + GfxColorSpace *maskColorSpace = lookupColorSpaceCopy(obj1); + _POPPLER_FREE(obj1); + if (!maskColorSpace || maskColorSpace->getMode() != csDeviceGray) { + goto err1; + } + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + maskColorMap = new GfxImageColorMap(maskBits, &obj1, maskColorSpace); + _POPPLER_FREE(obj1); + if (!maskColorMap->isOk()) { + delete maskColorMap; + goto err1; + } + //~ handle the Matte entry + haveSoftMask = gTrue; + } else if (maskObj.isArray()) { + // color key mask + int i; + for (i = 0; i < maskObj.arrayGetLength() && i < 2*gfxColorMaxComps; ++i) { + _POPPLER_CALL_ARGS(obj1, maskObj.arrayGet, i); + maskColors[i] = obj1.getInt(); + _POPPLER_FREE(obj1); + } + haveColorKeyMask = gTrue; + } else if (maskObj.isStream()) { + // explicit mask + if (inlineImg) { + goto err1; + } + maskStr = maskObj.getStream(); + maskDict = maskObj.streamGetDict(); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Width"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "W"); + } + if (!obj1.isInt()) { + goto err2; + } + maskWidth = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Height"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "H"); + } + if (!obj1.isInt()) { + goto err2; + } + maskHeight = obj1.getInt(); + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "ImageMask"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "IM"); + } + if (!obj1.isBool() || !obj1.getBool()) { + goto err2; + } + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Interpolate"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "I"); + } + if (obj1.isBool()) + maskInterpolate = obj1.getBool(); + else + maskInterpolate = gFalse; + _POPPLER_FREE(obj1); + maskInvert = gFalse; + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "Decode"); + if (obj1.isNull()) { + _POPPLER_FREE(obj1); + _POPPLER_CALL_ARGS(obj1, maskDict->lookup, "D"); + } + if (obj1.isArray()) { + _POPPLER_CALL_ARGS(obj2, obj1.arrayGet, 0); + if (obj2.isInt() && obj2.getInt() == 1) { + maskInvert = gTrue; + } + _POPPLER_FREE(obj2); + } else if (!obj1.isNull()) { + goto err2; + } + _POPPLER_FREE(obj1); + haveExplicitMask = gTrue; + } + + // draw it + if (haveSoftMask) { + builder->addSoftMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskColorMap, maskInterpolate); + delete maskColorMap; + } else if (haveExplicitMask) { + builder->addMaskedImage(state, str, width, height, colorMap, interpolate, + maskStr, maskWidth, maskHeight, maskInvert, maskInterpolate); + } else { + builder->addImage(state, str, width, height, colorMap, interpolate, + haveColorKeyMask ? maskColors : static_cast<int *>(nullptr)); + } + delete colorMap; + + _POPPLER_FREE(maskObj); + _POPPLER_FREE(smaskObj); + } + + return; + + err2: + _POPPLER_FREE(obj1); + err1: + error(errSyntaxError, getPos(), "Bad image parameters"); +} + +void PdfParser::doForm(Object *str) { + Dict *dict; + GBool transpGroup, isolated, knockout; + GfxColorSpace *blendingColorSpace; + Object matrixObj, bboxObj; + double m[6], bbox[4]; + Object resObj; + Dict *resDict; + Object obj1, obj2, obj3; + int i; + + // check for excessive recursion + if (formDepth > 20) { + return; + } + + // get stream dict + dict = str->streamGetDict(); + + // check form type + _POPPLER_CALL_ARGS(obj1, dict->lookup, "FormType"); + if (!(obj1.isNull() || (obj1.isInt() && obj1.getInt() == 1))) { + error(errSyntaxError, getPos(), "Unknown form type"); + } + _POPPLER_FREE(obj1); + + // get bounding box + _POPPLER_CALL_ARGS(bboxObj, dict->lookup, "BBox"); + if (!bboxObj.isArray()) { + _POPPLER_FREE(bboxObj); + error(errSyntaxError, getPos(), "Bad form bounding box"); + return; + } + for (i = 0; i < 4; ++i) { + _POPPLER_CALL_ARGS(obj1, bboxObj.arrayGet, i); + bbox[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + _POPPLER_FREE(bboxObj); + + // get matrix + _POPPLER_CALL_ARGS(matrixObj, dict->lookup, "Matrix"); + if (matrixObj.isArray()) { + for (i = 0; i < 6; ++i) { + _POPPLER_CALL_ARGS(obj1, matrixObj.arrayGet, i); + m[i] = obj1.getNum(); + _POPPLER_FREE(obj1); + } + } else { + m[0] = 1; m[1] = 0; + m[2] = 0; m[3] = 1; + m[4] = 0; m[5] = 0; + } + _POPPLER_FREE(matrixObj); + + // get resources + _POPPLER_CALL_ARGS(resObj, dict->lookup, "Resources"); + resDict = resObj.isDict() ? resObj.getDict() : (Dict *)nullptr; + + // check for a transparency group + transpGroup = isolated = knockout = gFalse; + blendingColorSpace = nullptr; + if (_POPPLER_CALL_ARGS_DEREF(obj1, dict->lookup, "Group").isDict()) { + if (_POPPLER_CALL_ARGS_DEREF(obj2, obj1.dictLookup, "S").isName("Transparency")) { + transpGroup = gTrue; + if (!_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "CS").isNull()) { + blendingColorSpace = GfxColorSpace::parse(nullptr, &obj3, nullptr, state); + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "I").isBool()) { + isolated = obj3.getBool(); + } + _POPPLER_FREE(obj3); + if (_POPPLER_CALL_ARGS_DEREF(obj3, obj1.dictLookup, "K").isBool()) { + knockout = obj3.getBool(); + } + _POPPLER_FREE(obj3); + } + _POPPLER_FREE(obj2); + } + _POPPLER_FREE(obj1); + + // draw it + ++formDepth; + doForm1(str, resDict, m, bbox, + transpGroup, gFalse, blendingColorSpace, isolated, knockout); + --formDepth; + + if (blendingColorSpace) { + delete blendingColorSpace; + } + _POPPLER_FREE(resObj); +} + +void PdfParser::doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, + GBool transpGroup, GBool softMask, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + GBool alpha, Function *transferFunc, + GfxColor *backdropColor) { + Parser *oldParser; + double oldBaseMatrix[6]; + int i; + + // push new resources on stack + pushResources(resDict); + + // save current graphics state + saveState(); + + // kill any pre-existing path + state->clearPath(); + + if (softMask || transpGroup) { + builder->clearSoftMask(state); + builder->pushTransparencyGroup(state, bbox, blendingColorSpace, + isolated, knockout, softMask); + } + + // save current parser + oldParser = parser; + + // set form transformation matrix + state->concatCTM(matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5]); + builder->setTransform(matrix[0], matrix[1], matrix[2], + matrix[3], matrix[4], matrix[5]); + + // set form bounding box + state->moveTo(bbox[0], bbox[1]); + state->lineTo(bbox[2], bbox[1]); + state->lineTo(bbox[2], bbox[3]); + state->lineTo(bbox[0], bbox[3]); + state->closePath(); + state->clip(); + clipHistory->setClip(state->getPath()); + builder->clip(state); + state->clearPath(); + + if (softMask || transpGroup) { + if (state->getBlendMode() != gfxBlendNormal) { + state->setBlendMode(gfxBlendNormal); + } + if (state->getFillOpacity() != 1) { + builder->setGroupOpacity(state->getFillOpacity()); + state->setFillOpacity(1); + } + if (state->getStrokeOpacity() != 1) { + state->setStrokeOpacity(1); + } + } + + // set new base matrix + for (i = 0; i < 6; ++i) { + oldBaseMatrix[i] = baseMatrix[i]; + baseMatrix[i] = state->getCTM()[i]; + } + + // draw the form + parse(str, gFalse); + + // restore base matrix + for (i = 0; i < 6; ++i) { + baseMatrix[i] = oldBaseMatrix[i]; + } + + // restore parser + parser = oldParser; + + if (softMask || transpGroup) { + builder->popTransparencyGroup(state); + } + + // restore graphics state + restoreState(); + + // pop resource stack + popResources(); + + if (softMask) { + builder->setSoftMask(state, bbox, alpha, transferFunc, backdropColor); + } else if (transpGroup) { + builder->paintTransparencyGroup(state, bbox); + } + + return; +} + +//------------------------------------------------------------------------ +// in-line image operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginImage(Object /*args*/[], int /*numArgs*/) +{ + // build dict/stream + Stream *str = buildImageStream(); + + // display the image + if (str) { + doImage(nullptr, str, gTrue); + + // skip 'EI' tag + int c1 = str->getUndecodedStream()->getChar(); + int c2 = str->getUndecodedStream()->getChar(); + while (!(c1 == 'E' && c2 == 'I') && c2 != EOF) { + c1 = c2; + c2 = str->getUndecodedStream()->getChar(); + } + delete str; + } +} + +Stream *PdfParser::buildImageStream() { + Object dict; + Object obj; + Stream *str; + + // build dictionary +#if defined(POPPLER_NEW_OBJECT_API) + dict = Object(new Dict(xref)); +#else + dict.initDict(xref); +#endif + _POPPLER_CALL(obj, parser->getObj); + while (!obj.isCmd(const_cast<char*>("ID")) && !obj.isEOF()) { + if (!obj.isName()) { + error(errSyntaxError, getPos(), "Inline image dictionary key must be a name object"); + _POPPLER_FREE(obj); + } else { + Object obj2; + _POPPLER_CALL(obj2, parser->getObj); + if (obj2.isEOF() || obj2.isError()) { + _POPPLER_FREE(obj); + break; + } + _POPPLER_DICTADD(dict, obj.getName(), obj2); + _POPPLER_FREE(obj); + _POPPLER_FREE(obj2); + } + _POPPLER_CALL(obj, parser->getObj); + } + if (obj.isEOF()) { + error(errSyntaxError, getPos(), "End of file in inline image"); + _POPPLER_FREE(obj); + _POPPLER_FREE(dict); + return nullptr; + } + _POPPLER_FREE(obj); + + // make stream +#if defined(POPPLER_NEW_OBJECT_API) + str = new EmbedStream(parser->getStream(), dict.copy(), gFalse, 0); + str = str->addFilters(dict.getDict()); +#else + str = new EmbedStream(parser->getStream(), &dict, gFalse, 0); + str = str->addFilters(&dict); +#endif + + return str; +} + +void PdfParser::opImageData(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'ID' operator"); +} + +void PdfParser::opEndImage(Object /*args*/[], int /*numArgs*/) +{ + error(errInternal, getPos(), "Internal: got 'EI' operator"); +} + +//------------------------------------------------------------------------ +// type 3 font operators +//------------------------------------------------------------------------ + +void PdfParser::opSetCharWidth(Object /*args*/[], int /*numArgs*/) +{ +} + +void PdfParser::opSetCacheDevice(Object /*args*/[], int /*numArgs*/) +{ +} + +//------------------------------------------------------------------------ +// compatibility operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + ++ignoreUndef; +} + +void PdfParser::opEndIgnoreUndef(Object /*args*/[], int /*numArgs*/) +{ + if (ignoreUndef > 0) + --ignoreUndef; +} + +//------------------------------------------------------------------------ +// marked content operators +//------------------------------------------------------------------------ + +void PdfParser::opBeginMarkedContent(Object args[], int numArgs) { + if (printCommands) { + printf(" marked content: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + + if(numArgs == 2) { + //out->beginMarkedContent(args[0].getName(),args[1].getDict()); + } else { + //out->beginMarkedContent(args[0].getName()); + } +} + +void PdfParser::opEndMarkedContent(Object /*args*/[], int /*numArgs*/) +{ + //out->endMarkedContent(); +} + +void PdfParser::opMarkPoint(Object args[], int numArgs) { + if (printCommands) { + printf(" mark point: %s ", args[0].getName()); + if (numArgs == 2) + args[2].print(stdout); + printf("\n"); + fflush(stdout); + } + + if(numArgs == 2) { + //out->markPoint(args[0].getName(),args[1].getDict()); + } else { + //out->markPoint(args[0].getName()); + } + +} + +//------------------------------------------------------------------------ +// misc +//------------------------------------------------------------------------ + +void PdfParser::saveState() { + bool is_radial = false; + + GfxPattern *pattern = state->getFillPattern(); + if (pattern != nullptr) + if (pattern->getType() == 2 ) { + GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern); + GfxShading *shading = shading_pattern->getShading(); + if (shading->getType() == 3) + is_radial = true; + } + + builder->saveState(); + if (is_radial) + state->save(); // nasty hack to prevent GfxRadialShading from getting corrupted during copy operation + else + state = state->save(); // see LP Bug 919176 comment 8 + clipHistory = clipHistory->save(); +} + +void PdfParser::restoreState() { + clipHistory = clipHistory->restore(); + state = state->restore(); + builder->restoreState(); +} + +void PdfParser::pushResources(Dict *resDict) { + res = new GfxResources(xref, resDict, res); +} + +void PdfParser::popResources() { + GfxResources *resPtr; + + resPtr = res->getNext(); + delete res; + res = resPtr; +} + +void PdfParser::setDefaultApproximationPrecision() { + for (int i = 1; i <= pdfNumShadingTypes; ++i) { + setApproximationPrecision(i, defaultShadingColorDelta, defaultShadingMaxDepth); + } +} + +void PdfParser::setApproximationPrecision(int shadingType, double colorDelta, + int maxDepth) { + + if (shadingType > pdfNumShadingTypes || shadingType < 1) { + return; + } + colorDeltas[shadingType-1] = dblToCol(colorDelta); + maxDepths[shadingType-1] = maxDepth; +} + +//------------------------------------------------------------------------ +// ClipHistoryEntry +//------------------------------------------------------------------------ + +ClipHistoryEntry::ClipHistoryEntry(GfxPath *clipPathA, GfxClipType clipTypeA) : + saved(nullptr), + clipPath((clipPathA) ? clipPathA->copy() : nullptr), + clipType(clipTypeA) +{ +} + +ClipHistoryEntry::~ClipHistoryEntry() +{ + if (clipPath) { + delete clipPath; + clipPath = nullptr; + } +} + +void ClipHistoryEntry::setClip(_POPPLER_CONST_83 GfxPath *clipPathA, GfxClipType clipTypeA) { + // Free previous clip path + if (clipPath) { + delete clipPath; + } + if (clipPathA) { + clipPath = clipPathA->copy(); + clipType = clipTypeA; + } else { + clipPath = nullptr; + clipType = clipNormal; + } +} + +ClipHistoryEntry *ClipHistoryEntry::save() { + ClipHistoryEntry *newEntry = new ClipHistoryEntry(this); + newEntry->saved = this; + + return newEntry; +} + +ClipHistoryEntry *ClipHistoryEntry::restore() { + ClipHistoryEntry *oldEntry; + + if (saved) { + oldEntry = saved; + saved = nullptr; + delete this; // TODO really should avoid deleting from inside. + } else { + oldEntry = this; + } + + return oldEntry; +} + +ClipHistoryEntry::ClipHistoryEntry(ClipHistoryEntry *other) { + if (other->clipPath) { + this->clipPath = other->clipPath->copy(); + this->clipType = other->clipType; + } else { + this->clipPath = nullptr; + this->clipType = clipNormal; + } + saved = nullptr; +} + +#endif /* HAVE_POPPLER */ diff --git a/src/extension/internal/pdfinput/pdf-parser.h b/src/extension/internal/pdfinput/pdf-parser.h new file mode 100644 index 0000000..b92c415 --- /dev/null +++ b/src/extension/internal/pdfinput/pdf-parser.h @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PDF parsing using libpoppler. + *//* + * Authors: + * see git history + * + * Derived from Gfx.h from poppler (?) which derives from Xpdf, Copyright 1996-2003 Glyph & Cog, LLC, which is under GPL2+. + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef PDF_PARSER_H +#define PDF_PARSER_H + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +#ifdef USE_GCC_PRAGMAS +#pragma interface +#endif + +namespace Inkscape { + namespace Extension { + namespace Internal { + class SvgBuilder; + } + } +} + +// TODO clean up and remove using: +using Inkscape::Extension::Internal::SvgBuilder; + +#include "glib/poppler-features.h" +#include "Object.h" + +#include <map> +#include <memory> +#include <string> + +class GooString; +class XRef; +class Array; +class Stream; +class Parser; +class Dict; +class Function; +class OutputDev; +class GfxFontDict; +class GfxFont; +class GfxPattern; +class GfxTilingPattern; +class GfxShadingPattern; +class GfxShading; +class GfxFunctionShading; +class GfxAxialShading; +class GfxRadialShading; +class GfxGouraudTriangleShading; +class GfxPatchMeshShading; +struct GfxPatch; +class GfxState; +struct GfxColor; +class GfxColorSpace; +class Gfx; +class GfxResources; +class PDFRectangle; +class AnnotBorderStyle; + +class PdfParser; + +class ClipHistoryEntry; + +//------------------------------------------------------------------------ + +#ifndef GFX_H +enum GfxClipType { + clipNone, + clipNormal, + clipEO +}; + +enum TchkType { + tchkBool, // boolean + tchkInt, // integer + tchkNum, // number (integer or real) + tchkString, // string + tchkName, // name + tchkArray, // array + tchkProps, // properties (dictionary or name) + tchkSCN, // scn/SCN args (number of name) + tchkNone // used to avoid empty initializer lists +}; +#endif /* GFX_H */ + +#define maxOperatorArgs 33 + +struct PdfOperator { + char name[4]; + int numArgs; + TchkType tchk[maxOperatorArgs]; + void (PdfParser::*func)(Object args[], int numArgs); +}; + +#undef maxOperatorArgs + +struct OpHistoryEntry { + const char *name; // operator's name + GfxState *state; // saved state, NULL if none + GBool executed; // whether the operator has been executed + + OpHistoryEntry *next; // next entry on stack + unsigned depth; // total number of entries descending from this +}; + +//------------------------------------------------------------------------ +// PdfParser +//------------------------------------------------------------------------ + +//------------------------------------------------------------------------ +// constants +//------------------------------------------------------------------------ + +#define pdfFunctionShading 1 +#define pdfAxialShading 2 +#define pdfRadialShading 3 +#define pdfGouraudTriangleShading 4 +#define pdfPatchMeshShading 5 +#define pdfNumShadingTypes 5 + + + +/** + * PDF parsing module using libpoppler's facilities. + */ +class PdfParser { +public: + + // Constructor for regular output. + PdfParser(XRef *xrefA, SvgBuilder *builderA, int pageNum, int rotate, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box, + _POPPLER_CONST PDFRectangle *cropBox); + + // Constructor for a sub-page object. + PdfParser(XRef *xrefA, Inkscape::Extension::Internal::SvgBuilder *builderA, + Dict *resDict, + _POPPLER_CONST PDFRectangle *box); + + virtual ~PdfParser(); + + // Interpret a stream or array of streams. + void parse(Object *obj, GBool topLevel = gTrue); + + // Save graphics state. + void saveState(); + + // Restore graphics state. + void restoreState(); + + // Get the current graphics state object. + GfxState *getState() { return state; } + + // Set the precision of approximation for specific shading fills. + void setApproximationPrecision(int shadingType, double colorDelta, int maxDepth); + +private: + + XRef *xref; // the xref table for this PDF file + SvgBuilder *builder; // SVG generator + GBool subPage; // is this a sub-page object? + GBool printCommands; // print the drawing commands (for debugging) + GfxResources *res; // resource stack + + GfxState *state; // current graphics state + GBool fontChanged; // set if font or text matrix has changed + GfxClipType clip; // do a clip? + int ignoreUndef; // current BX/EX nesting level + double baseMatrix[6]; // default matrix for most recent + // page/form/pattern + int formDepth; + + Parser *parser; // parser for page content stream(s) + + static PdfOperator opTab[]; // table of operators + + int colorDeltas[pdfNumShadingTypes]; + // max deltas allowed in any color component + // for the approximation of shading fills + int maxDepths[pdfNumShadingTypes]; // max recursive depths + + ClipHistoryEntry *clipHistory; // clip path stack + OpHistoryEntry *operatorHistory; // list containing the last N operators + + //! Caches color spaces by name + std::map<std::string, std::unique_ptr<GfxColorSpace>> colorSpacesCache; + + GfxColorSpace *lookupColorSpaceCopy(Object &); + + void setDefaultApproximationPrecision(); // init color deltas + void pushOperator(const char *name); + OpHistoryEntry *popOperator(); + const char *getPreviousOperator(unsigned int look_back=1); // returns the nth previous operator's name + + void go(GBool topLevel); + void execOp(Object *cmd, Object args[], int numArgs); + PdfOperator *findOp(const char *name); + GBool checkArg(Object *arg, TchkType type); + int getPos(); + + // graphics state operators + void opSave(Object args[], int numArgs); + void opRestore(Object args[], int numArgs); + void opConcat(Object args[], int numArgs); + void opSetDash(Object args[], int numArgs); + void opSetFlat(Object args[], int numArgs); + void opSetLineJoin(Object args[], int numArgs); + void opSetLineCap(Object args[], int numArgs); + void opSetMiterLimit(Object args[], int numArgs); + void opSetLineWidth(Object args[], int numArgs); + void opSetExtGState(Object args[], int numArgs); + void doSoftMask(Object *str, GBool alpha, + GfxColorSpace *blendingColorSpace, + GBool isolated, GBool knockout, + Function *transferFunc, GfxColor *backdropColor); + void opSetRenderingIntent(Object args[], int numArgs); + + // color operators + void opSetFillGray(Object args[], int numArgs); + void opSetStrokeGray(Object args[], int numArgs); + void opSetFillCMYKColor(Object args[], int numArgs); + void opSetStrokeCMYKColor(Object args[], int numArgs); + void opSetFillRGBColor(Object args[], int numArgs); + void opSetStrokeRGBColor(Object args[], int numArgs); + void opSetFillColorSpace(Object args[], int numArgs); + void opSetStrokeColorSpace(Object args[], int numArgs); + void opSetFillColor(Object args[], int numArgs); + void opSetStrokeColor(Object args[], int numArgs); + void opSetFillColorN(Object args[], int numArgs); + void opSetStrokeColorN(Object args[], int numArgs); + + // path segment operators + void opMoveTo(Object args[], int numArgs); + void opLineTo(Object args[], int numArgs); + void opCurveTo(Object args[], int numArgs); + void opCurveTo1(Object args[], int numArgs); + void opCurveTo2(Object args[], int numArgs); + void opRectangle(Object args[], int numArgs); + void opClosePath(Object args[], int numArgs); + + // path painting operators + void opEndPath(Object args[], int numArgs); + void opStroke(Object args[], int numArgs); + void opCloseStroke(Object args[], int numArgs); + void opFill(Object args[], int numArgs); + void opEOFill(Object args[], int numArgs); + void opFillStroke(Object args[], int numArgs); + void opCloseFillStroke(Object args[], int numArgs); + void opEOFillStroke(Object args[], int numArgs); + void opCloseEOFillStroke(Object args[], int numArgs); + void doFillAndStroke(GBool eoFill); + void doPatternFillFallback(GBool eoFill); + void doPatternStrokeFallback(); + void doShadingPatternFillFallback(GfxShadingPattern *sPat, + GBool stroke, GBool eoFill); + void opShFill(Object args[], int numArgs); + void doFunctionShFill(GfxFunctionShading *shading); + void doFunctionShFill1(GfxFunctionShading *shading, + double x0, double y0, + double x1, double y1, + GfxColor *colors, int depth); + void doGouraudTriangleShFill(GfxGouraudTriangleShading *shading); + void gouraudFillTriangle(double x0, double y0, GfxColor *color0, + double x1, double y1, GfxColor *color1, + double x2, double y2, GfxColor *color2, + int nComps, int depth); + void doPatchMeshShFill(GfxPatchMeshShading *shading); + void fillPatch(_POPPLER_CONST GfxPatch *patch, int nComps, int depth); + void doEndPath(); + + // path clipping operators + void opClip(Object args[], int numArgs); + void opEOClip(Object args[], int numArgs); + + // text object operators + void opBeginText(Object args[], int numArgs); + void opEndText(Object args[], int numArgs); + + // text state operators + void opSetCharSpacing(Object args[], int numArgs); + void opSetFont(Object args[], int numArgs); + void opSetTextLeading(Object args[], int numArgs); + void opSetTextRender(Object args[], int numArgs); + void opSetTextRise(Object args[], int numArgs); + void opSetWordSpacing(Object args[], int numArgs); + void opSetHorizScaling(Object args[], int numArgs); + + // text positioning operators + void opTextMove(Object args[], int numArgs); + void opTextMoveSet(Object args[], int numArgs); + void opSetTextMatrix(Object args[], int numArgs); + void opTextNextLine(Object args[], int numArgs); + + // text string operators + void opShowText(Object args[], int numArgs); + void opMoveShowText(Object args[], int numArgs); + void opMoveSetShowText(Object args[], int numArgs); + void opShowSpaceText(Object args[], int numArgs); +#if POPPLER_CHECK_VERSION(0,64,0) + void doShowText(const GooString *s); +#else + void doShowText(GooString *s); +#endif + + + // XObject operators + void opXObject(Object args[], int numArgs); + void doImage(Object *ref, Stream *str, GBool inlineImg); + void doForm(Object *str); + void doForm1(Object *str, Dict *resDict, double *matrix, double *bbox, + GBool transpGroup = gFalse, GBool softMask = gFalse, + GfxColorSpace *blendingColorSpace = nullptr, + GBool isolated = gFalse, GBool knockout = gFalse, + GBool alpha = gFalse, Function *transferFunc = nullptr, + GfxColor *backdropColor = nullptr); + + // in-line image operators + void opBeginImage(Object args[], int numArgs); + Stream *buildImageStream(); + void opImageData(Object args[], int numArgs); + void opEndImage(Object args[], int numArgs); + + // type 3 font operators + void opSetCharWidth(Object args[], int numArgs); + void opSetCacheDevice(Object args[], int numArgs); + + // compatibility operators + void opBeginIgnoreUndef(Object args[], int numArgs); + void opEndIgnoreUndef(Object args[], int numArgs); + + // marked content operators + void opBeginMarkedContent(Object args[], int numArgs); + void opEndMarkedContent(Object args[], int numArgs); + void opMarkPoint(Object args[], int numArgs); + + void pushResources(Dict *resDict); + void popResources(); +}; + +#endif /* HAVE_POPPLER */ + +#endif /* PDF_PARSER_H */ diff --git a/src/extension/internal/pdfinput/poppler-transition-api.h b/src/extension/internal/pdfinput/poppler-transition-api.h new file mode 100644 index 0000000..dc9e47e --- /dev/null +++ b/src/extension/internal/pdfinput/poppler-transition-api.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO short description + *//* + * Authors: + * see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_POPPLER_TRANSITION_API_H +#define SEEN_POPPLER_TRANSITION_API_H + +#include <glib/poppler-features.h> + +#if POPPLER_CHECK_VERSION(22, 4, 0) +#define _POPPLER_FONTPTR_TO_GFX8(font_ptr) ((Gfx8BitFont *)font_ptr.get()) +#else +#define _POPPLER_FONTPTR_TO_GFX8(font_ptr) ((Gfx8BitFont *)font_ptr) +#endif + +#if POPPLER_CHECK_VERSION(22, 3, 0) +#define _POPPLER_MAKE_SHARED_PDFDOC(uri) std::make_shared<PDFDoc>(std::make_unique<GooString>(uri)) +#else +#define _POPPLER_MAKE_SHARED_PDFDOC(uri) std::make_shared<PDFDoc>(new GooString(uri), nullptr, nullptr, nullptr) +#endif + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_CONST_83 const +#else +#define _POPPLER_CONST_83 +#endif + +#if POPPLER_CHECK_VERSION(0, 82, 0) +#define _POPPLER_CONST_82 const +#else +#define _POPPLER_CONST_82 +#endif + +#if POPPLER_CHECK_VERSION(0, 76, 0) +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, obj, gFalse) +#else +#define _POPPLER_NEW_PARSER(xref, obj) Parser(xref, new Lexer(xref, obj), gFalse) +#endif + +#if POPPLER_CHECK_VERSION(0, 83, 0) +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) std::unique_ptr<GlobalParams>(new GlobalParams(args)) +#else +#define _POPPLER_NEW_GLOBAL_PARAMS(args...) new GlobalParams(args) +#endif + + +#if POPPLER_CHECK_VERSION(0, 72, 0) +#define getCString c_str +#endif + +#if POPPLER_CHECK_VERSION(0,71,0) +typedef bool GBool; +#define gTrue true +#define gFalse false +#endif + +#if POPPLER_CHECK_VERSION(0,70,0) +#define _POPPLER_CONST const +#else +#define _POPPLER_CONST +#endif + +#if POPPLER_CHECK_VERSION(0,69,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(key, std::move(obj)) +#elif POPPLER_CHECK_VERSION(0,58,0) +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), std::move(obj)) +#else +#define _POPPLER_DICTADD(dict, key, obj) (dict).dictAdd(copyString(key), &obj) +#endif + +#if POPPLER_CHECK_VERSION(0,58,0) +#define POPPLER_NEW_OBJECT_API +#define _POPPLER_FREE(obj) +#define _POPPLER_CALL(ret, func) (ret = func()) +#define _POPPLER_CALL_ARGS(ret, func, ...) (ret = func(__VA_ARGS__)) +#define _POPPLER_CALL_ARGS_DEREF _POPPLER_CALL_ARGS +#else +#define _POPPLER_FREE(obj) (obj).free() +#define _POPPLER_CALL(ret, func) (*func(&ret)) +#define _POPPLER_CALL_ARGS(ret, func, ...) (func(__VA_ARGS__, &ret)) +#define _POPPLER_CALL_ARGS_DEREF(...) (*_POPPLER_CALL_ARGS(__VA_ARGS__)) +#endif + +#if !POPPLER_CHECK_VERSION(0, 29, 0) +#error "Requires poppler >= 0.29" +#endif + +#endif diff --git a/src/extension/internal/pdfinput/svg-builder.cpp b/src/extension/internal/pdfinput/svg-builder.cpp new file mode 100644 index 0000000..9c49d37 --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.cpp @@ -0,0 +1,1983 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Native PDF import using libpoppler. + * + * Authors: + * miklos erdelyi + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <string> + +#ifdef HAVE_POPPLER + +#include "svg-builder.h" +#include "pdf-parser.h" + +#include "document.h" +#include "object/sp-namedview.h" +#include "png.h" + +#include "xml/document.h" +#include "xml/node.h" +#include "xml/repr.h" +#include "svg/svg.h" +#include "svg/path-string.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" +#include "color.h" +#include "util/units.h" +#include "display/nr-filter-utils.h" +#include "libnrtype/font-instance.h" +#include "object/sp-defs.h" + +#include "Function.h" +#include "GfxState.h" +#include "GfxFont.h" +#include "Stream.h" +#include "Page.h" +#include "UnicodeMap.h" +#include "GlobalParams.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +//#define IFTRACE(_code) _code +#define IFTRACE(_code) + +#define TRACE(_args) IFTRACE(g_print _args) + +/** + * \struct SvgTransparencyGroup + * \brief Holds information about a PDF transparency group + */ +struct SvgTransparencyGroup { + double bbox[6]; // TODO should this be 4? + Inkscape::XML::Node *container; + + bool isolated; + bool knockout; + bool for_softmask; + + SvgTransparencyGroup *next; +}; + +/** + * \class SvgBuilder + * + */ + +SvgBuilder::SvgBuilder(SPDocument *document, gchar *docname, XRef *xref) +{ + _is_top_level = true; + _doc = document; + _docname = docname; + _xref = xref; + _xml_doc = _doc->getReprDoc(); + _container = _root = _doc->getReprRoot(); + _init(); + + // Set default preference settings + _preferences = _xml_doc->createElement("svgbuilder:prefs"); + _preferences->setAttribute("embedImages", "1"); + _preferences->setAttribute("localFonts", "1"); +} + +SvgBuilder::SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root) { + _is_top_level = false; + _doc = parent->_doc; + _docname = parent->_docname; + _xref = parent->_xref; + _xml_doc = parent->_xml_doc; + _preferences = parent->_preferences; + _container = this->_root = root; + _init(); +} + +SvgBuilder::~SvgBuilder() = default; + +void SvgBuilder::_init() { + _font_style = nullptr; + _font_specification = nullptr; + _font_scaling = 1; + _need_font_update = true; + _in_text_object = false; + _invalidated_style = true; + _current_state = nullptr; + _width = 0; + _height = 0; + + // Fill _availableFontNames (Bug LP #179589) (code cfr. FontLister) + std::vector<PangoFontFamily *> families; + font_factory::Default()->GetUIFamilies(families); + for (auto & familie : families) { + _availableFontNames.emplace_back(pango_font_family_get_name(familie)); + } + + _transp_group_stack = nullptr; + SvgGraphicsState initial_state; + initial_state.softmask = nullptr; + initial_state.group_depth = 0; + _state_stack.push_back(initial_state); + _node_stack.push_back(_container); + + _ttm[0] = 1; _ttm[1] = 0; _ttm[2] = 0; _ttm[3] = 1; _ttm[4] = 0; _ttm[5] = 0; + _ttm_is_set = false; +} + +/** + * We're creating a multi-page document, push page number. + */ +void SvgBuilder::pushPage() { + // Move page over by the last page width + if (_page && this->_width) { + int gap = 20; + _page_left += this->_width + gap; + // TODO: A more interesting page layout could be implemented here. + } + _page_num += 1; + _page_offset = true; + + if (_page) { + Inkscape::GC::release(_page); + } + _page = _xml_doc->createElement("inkscape:page"); + _page->setAttributeSvgDouble("x", _page_left); + _page->setAttributeSvgDouble("y", _page_top); + auto _nv = _doc->getNamedView()->getRepr(); + _nv->appendChild(_page); +} + +void SvgBuilder::setDocumentSize(double width, double height) { + this->_width = width; + this->_height = height; + + if (_page_num < 2) { + _root->setAttributeSvgDouble("width", width); + _root->setAttributeSvgDouble("height", height); + } + if (_page) { + _page->setAttributeSvgDouble("width", width); + _page->setAttributeSvgDouble("height", height); + } +} + +/** + * \brief Sets groupmode of the current container to 'layer' and sets its label if given + */ +void SvgBuilder::setAsLayer(char *layer_name) { + _container->setAttribute("inkscape:groupmode", "layer"); + if (layer_name) { + _container->setAttribute("inkscape:label", layer_name); + } +} + +/** + * \brief Sets the current container's opacity + */ +void SvgBuilder::setGroupOpacity(double opacity) { + _container->setAttributeSvgDouble("opacity", CLAMP(opacity, 0.0, 1.0)); +} + +void SvgBuilder::saveState() { + SvgGraphicsState new_state; + new_state.group_depth = 0; + new_state.softmask = _state_stack.back().softmask; + _state_stack.push_back(new_state); + pushGroup(); +} + +void SvgBuilder::restoreState() { + while( _state_stack.back().group_depth > 0 ) { + popGroup(); + } + _state_stack.pop_back(); +} + +Inkscape::XML::Node *SvgBuilder::pushNode(const char *name) { + Inkscape::XML::Node *node = _xml_doc->createElement(name); + _node_stack.push_back(node); + _container = node; + return node; +} + +Inkscape::XML::Node *SvgBuilder::popNode() { + Inkscape::XML::Node *node = nullptr; + if ( _node_stack.size() > 1 ) { + node = _node_stack.back(); + _node_stack.pop_back(); + _container = _node_stack.back(); // Re-set container + } else { + TRACE(("popNode() called when stack is empty\n")); + node = _root; + } + return node; +} + +Inkscape::XML::Node *SvgBuilder::pushGroup() { + Inkscape::XML::Node *saved_container = _container; + Inkscape::XML::Node *node = pushNode("svg:g"); + saved_container->appendChild(node); + Inkscape::GC::release(node); + _state_stack.back().group_depth++; + // Set as a layer if this is a top-level group + if ( _container->parent() == _root && _is_top_level ) { + static int layer_count = 1; + if (_page_num) { + gchar *layer_name = g_strdup_printf("Page %d", _page_num); + setAsLayer(layer_name); + g_free(layer_name); + } else if ( layer_count > 1 ) { + gchar *layer_name = g_strdup_printf("%s%d", _docname, layer_count); + setAsLayer(layer_name); + g_free(layer_name); + layer_count++; + } else { + setAsLayer(_docname); + layer_count++; + } + } + if (_container->parent()->attribute("inkscape:groupmode") != nullptr) { + _ttm[0] = _ttm[3] = 1.0; // clear ttm if parent is a layer + _ttm[1] = _ttm[2] = _ttm[4] = _ttm[5] = 0.0; + _ttm_is_set = false; + } + return _container; +} + +Inkscape::XML::Node *SvgBuilder::popGroup() { + if (_container != _root) { // Pop if the current container isn't root + popNode(); + _state_stack.back().group_depth--; + } + + return _container; +} + +Inkscape::XML::Node *SvgBuilder::getContainer() { + return _container; +} + +static gchar *svgConvertRGBToText(double r, double g, double b) { + using Inkscape::Filters::clamp; + static gchar tmp[1023] = {0}; + snprintf(tmp, 1023, + "#%02x%02x%02x", + clamp(SP_COLOR_F_TO_U(r)), + clamp(SP_COLOR_F_TO_U(g)), + clamp(SP_COLOR_F_TO_U(b))); + return (gchar *)&tmp; +} + +static gchar *svgConvertGfxRGB(GfxRGB *color) { + double r = (double)color->r / 65535.0; + double g = (double)color->g / 65535.0; + double b = (double)color->b / 65535.0; + return svgConvertRGBToText(r, g, b); +} + +static void svgSetTransform(Inkscape::XML::Node *node, Geom::Affine matrix) { + node->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(matrix)); +} + +/** + * \brief Generates a SVG path string from poppler's data structure + */ +static gchar *svgInterpretPath(_POPPLER_CONST_83 GfxPath *path) { + Inkscape::SVG::PathString pathString; + for (int i = 0 ; i < path->getNumSubpaths() ; ++i ) { + _POPPLER_CONST_83 GfxSubpath *subpath = path->getSubpath(i); + if (subpath->getNumPoints() > 0) { + pathString.moveTo(subpath->getX(0), subpath->getY(0)); + int j = 1; + while (j < subpath->getNumPoints()) { + if (subpath->getCurve(j)) { + pathString.curveTo(subpath->getX(j), subpath->getY(j), + subpath->getX(j+1), subpath->getY(j+1), + subpath->getX(j+2), subpath->getY(j+2)); + + j += 3; + } else { + pathString.lineTo(subpath->getX(j), subpath->getY(j)); + ++j; + } + } + if (subpath->isClosed()) { + pathString.closePath(); + } + } + } + + return g_strdup(pathString.c_str()); +} + +/** + * \brief Sets stroke style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties + */ +void SvgBuilder::_setStrokeStyle(SPCSSAttr *css, GfxState *state) { + // Stroke color/pattern + if ( state->getStrokeColorSpace()->getMode() == csPattern ) { + gchar *urltext = _createPattern(state->getStrokePattern(), state, true); + sp_repr_css_set_property(css, "stroke", urltext); + if (urltext) { + g_free(urltext); + } + } else { + GfxRGB stroke_color; + state->getStrokeRGB(&stroke_color); + sp_repr_css_set_property(css, "stroke", svgConvertGfxRGB(&stroke_color)); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getStrokeOpacity(); + sp_repr_css_set_property(css, "stroke-opacity", os_opacity.str().c_str()); + + // Line width + Inkscape::CSSOStringStream os_width; + double lw = state->getLineWidth(); + if (lw > 0.0) { + os_width << lw; + } else { + // emit a stroke which is 1px in toplevel user units + double pxw = Inkscape::Util::Quantity::convert(1.0, "pt", "px"); + os_width << 1.0 / state->transformWidth(pxw); + } + sp_repr_css_set_property(css, "stroke-width", os_width.str().c_str()); + + // Line cap + switch (state->getLineCap()) { + case 0: + sp_repr_css_set_property(css, "stroke-linecap", "butt"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linecap", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linecap", "square"); + break; + } + + // Line join + switch (state->getLineJoin()) { + case 0: + sp_repr_css_set_property(css, "stroke-linejoin", "miter"); + break; + case 1: + sp_repr_css_set_property(css, "stroke-linejoin", "round"); + break; + case 2: + sp_repr_css_set_property(css, "stroke-linejoin", "bevel"); + break; + } + + // Miterlimit + Inkscape::CSSOStringStream os_ml; + os_ml << state->getMiterLimit(); + sp_repr_css_set_property(css, "stroke-miterlimit", os_ml.str().c_str()); + + // Line dash + int dash_length; + double dash_start; +#if POPPLER_CHECK_VERSION(22, 9, 0) + const double *dash_pattern; + const std::vector<double> &dash = state->getLineDash(&dash_start); + dash_pattern = dash.data(); + dash_length = dash.size(); +#else + double *dash_pattern; + state->getLineDash(&dash_pattern, &dash_length, &dash_start); +#endif + if ( dash_length > 0 ) { + Inkscape::CSSOStringStream os_array; + for ( int i = 0 ; i < dash_length ; i++ ) { + os_array << dash_pattern[i]; + if (i < (dash_length - 1)) { + os_array << ","; + } + } + sp_repr_css_set_property(css, "stroke-dasharray", os_array.str().c_str()); + + Inkscape::CSSOStringStream os_offset; + os_offset << dash_start; + sp_repr_css_set_property(css, "stroke-dashoffset", os_offset.str().c_str()); + } else { + sp_repr_css_set_property(css, "stroke-dasharray", "none"); + sp_repr_css_set_property(css, "stroke-dashoffset", nullptr); + } +} + +/** + * \brief Sets fill style from poppler's GfxState data structure + * Uses the given SPCSSAttr for storing the style properties. + */ +void SvgBuilder::_setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd) { + + // Fill color/pattern + if ( state->getFillColorSpace()->getMode() == csPattern ) { + gchar *urltext = _createPattern(state->getFillPattern(), state); + sp_repr_css_set_property(css, "fill", urltext); + if (urltext) { + g_free(urltext); + } + } else { + GfxRGB fill_color; + state->getFillRGB(&fill_color); + sp_repr_css_set_property(css, "fill", svgConvertGfxRGB(&fill_color)); + } + + // Opacity + Inkscape::CSSOStringStream os_opacity; + os_opacity << state->getFillOpacity(); + sp_repr_css_set_property(css, "fill-opacity", os_opacity.str().c_str()); + + // Fill rule + sp_repr_css_set_property(css, "fill-rule", even_odd ? "evenodd" : "nonzero"); +} + +/** + * \brief Sets blend style properties from poppler's GfxState data structure + * \update a SPCSSAttr with all mix-blend-mode set + */ +void SvgBuilder::_setBlendMode(Inkscape::XML::Node *node, GfxState *state) +{ + SPCSSAttr *css = sp_repr_css_attr(node, "style"); + GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(css, "mix-blend-mode", enum_blend_mode[blendmode].key); + } + Glib::ustring value; + sp_repr_css_write_string(css, value); + node->setAttributeOrRemoveIfEmpty("style", value); + sp_repr_css_attr_unref(css); +} +/** + * \brief Sets style properties from poppler's GfxState data structure + * \return SPCSSAttr with all the relevant properties set + */ +SPCSSAttr *SvgBuilder::_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd) { + SPCSSAttr *css = sp_repr_css_attr_new(); + if (fill) { + _setFillStyle(css, state, even_odd); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + + if (stroke) { + _setStrokeStyle(css, state); + } else { + sp_repr_css_set_property(css, "stroke", "none"); + } + + return css; +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * Can be used to do filling and stroking at once. + * + * \param fill whether the path should be filled + * \param stroke whether the path should be stroked + * \param even_odd whether the even-odd rule should be used when filling the path + */ +void SvgBuilder::addPath(GfxState *state, bool fill, bool stroke, bool even_odd) { + Inkscape::XML::Node *path = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(state->getPath()); + path->setAttribute("d", pathtext); + g_free(pathtext); + + // Set style + SPCSSAttr *css = _setStyle(state, fill, stroke, even_odd); + sp_repr_css_change(path, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(path, state); + _container->appendChild(path); + Inkscape::GC::release(path); +} + +/** + * \brief Emits the current path in poppler's GfxState data structure + * The path is set to be filled with the given shading. + */ +void SvgBuilder::addShadedFill(GfxShading *shading, double *matrix, GfxPath *path, + bool even_odd) { + + Inkscape::XML::Node *path_node = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(path); + path_node->setAttribute("d", pathtext); + g_free(pathtext); + + // Set style + SPCSSAttr *css = sp_repr_css_attr_new(); + gchar *id = _createGradient(shading, matrix, true); + if (id) { + gchar *urltext = g_strdup_printf ("url(#%s)", id); + sp_repr_css_set_property(css, "fill", urltext); + g_free(urltext); + g_free(id); + } else { + sp_repr_css_attr_unref(css); + Inkscape::GC::release(path_node); + return; + } + if (even_odd) { + sp_repr_css_set_property(css, "fill-rule", "evenodd"); + } + sp_repr_css_set_property(css, "stroke", "none"); + sp_repr_css_change(path_node, css, "style"); + sp_repr_css_attr_unref(css); + + _container->appendChild(path_node); + Inkscape::GC::release(path_node); + + // Remove the clipping path emitted before the 'sh' operator + int up_walk = 0; + Inkscape::XML::Node *node = _container->parent(); + while( node && node->childCount() == 1 && up_walk < 3 ) { + gchar const *clip_path_url = node->attribute("clip-path"); + if (clip_path_url) { + // Obtain clipping path's id from the URL + gchar clip_path_id[32]; + strncpy(clip_path_id, clip_path_url + 5, strlen(clip_path_url) - 6); + clip_path_id[sizeof (clip_path_id) - 1] = '\0'; + SPObject *clip_obj = _doc->getObjectById(clip_path_id); + if (clip_obj) { + clip_obj->deleteObject(); + node->removeAttribute("clip-path"); + TRACE(("removed clipping path: %s\n", clip_path_id)); + } + break; + } + node = node->parent(); + up_walk++; + } +} + +/** + * \brief Clips to the current path set in GfxState + * \param state poppler's data structure + * \param even_odd whether the even-odd rule should be applied + */ +void SvgBuilder::clip(GfxState *state, bool even_odd) { + pushGroup(); + setClipPath(state, even_odd); +} + +void SvgBuilder::setClipPath(GfxState *state, bool even_odd) { + // Create the clipPath repr + Inkscape::XML::Node *clip_path = _xml_doc->createElement("svg:clipPath"); + clip_path->setAttribute("clipPathUnits", "userSpaceOnUse"); + // Create the path + Inkscape::XML::Node *path = _xml_doc->createElement("svg:path"); + gchar *pathtext = svgInterpretPath(state->getPath()); + path->setAttribute("d", pathtext); + g_free(pathtext); + if (even_odd) { + path->setAttribute("clip-rule", "evenodd"); + } + clip_path->appendChild(path); + Inkscape::GC::release(path); + // Append clipPath to defs and get id + _doc->getDefs()->getRepr()->appendChild(clip_path); + gchar *urltext = g_strdup_printf ("url(#%s)", clip_path->attribute("id")); + Inkscape::GC::release(clip_path); + _container->setAttribute("clip-path", urltext); + g_free(urltext); +} + +/** + * \brief Fills the given array with the current container's transform, if set + * \param transform array of doubles to be filled + * \return true on success; false on invalid transformation + */ +bool SvgBuilder::getTransform(double *transform) { + Geom::Affine svd; + gchar const *tr = _container->attribute("transform"); + bool valid = sp_svg_transform_read(tr, &svd); + if (valid) { + for ( int i = 0 ; i < 6 ; i++ ) { + transform[i] = svd[i]; + } + return true; + } else { + return false; + } +} + +/** + * \brief Sets the transformation matrix of the current container + */ +void SvgBuilder::setTransform(double c0, double c1, double c2, double c3, + double c4, double c5) { + + auto matrix = Geom::Affine(c0, c1, c2, c3, c4, c5); + + // Add page transformation only once (see scaledCTM in pdf-parser.cpp) + if ( _container->parent() == _root && _is_top_level && _page_offset) { + matrix = matrix * Geom::Translate(_page_left, _page_top); + _page_offset = false; + } + + // do not remember the group which is a layer + if ((_container->attribute("inkscape:groupmode") == nullptr) && !_ttm_is_set) { + _ttm[0] = c0; + _ttm[1] = c1; + _ttm[2] = c2; + _ttm[3] = c3; + _ttm[4] = c4; + _ttm[5] = c5; + _ttm_is_set = true; + } + + // Avoid transforming a group with an already set clip-path + if ( _container->attribute("clip-path") != nullptr ) { + pushGroup(); + } + TRACE(("setTransform: %f %f %f %f %f %f\n", c0, c1, c2, c3, c4, c5)); + svgSetTransform(_container, matrix); +} + +void SvgBuilder::setTransform(double const *transform) { + setTransform(transform[0], transform[1], transform[2], transform[3], + transform[4], transform[5]); +} + +/** + * \brief Checks whether the given pattern type can be represented in SVG + * Used by PdfParser to decide when to do fallback operations. + */ +bool SvgBuilder::isPatternTypeSupported(GfxPattern *pattern) { + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // shading pattern + GfxShading *shading = (static_cast<GfxShadingPattern *>(pattern))->getShading(); + int shadingType = shading->getType(); + if ( shadingType == 2 || // axial shading + shadingType == 3 ) { // radial shading + return true; + } + return false; + } else if ( pattern->getType() == 1 ) { // tiling pattern + return true; + } + } + + return false; +} + +/** + * \brief Creates a pattern from poppler's data structure + * Handles linear and radial gradients. Creates a new PdfParser and uses it to + * build a tiling pattern. + * \return a url pointing to the created pattern + */ +gchar *SvgBuilder::_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke) { + gchar *id = nullptr; + if ( pattern != nullptr ) { + if ( pattern->getType() == 2 ) { // Shading pattern + GfxShadingPattern *shading_pattern = static_cast<GfxShadingPattern *>(pattern); + const double *ptm; + double m[6] = {1, 0, 0, 1, 0, 0}; + double det; + + // construct a (pattern space) -> (current space) transform matrix + + ptm = shading_pattern->getMatrix(); + det = _ttm[0] * _ttm[3] - _ttm[1] * _ttm[2]; + if (det) { + double ittm[6]; // invert ttm + ittm[0] = _ttm[3] / det; + ittm[1] = -_ttm[1] / det; + ittm[2] = -_ttm[2] / det; + ittm[3] = _ttm[0] / det; + ittm[4] = (_ttm[2] * _ttm[5] - _ttm[3] * _ttm[4]) / det; + ittm[5] = (_ttm[1] * _ttm[4] - _ttm[0] * _ttm[5]) / det; + m[0] = ptm[0] * ittm[0] + ptm[1] * ittm[2]; + m[1] = ptm[0] * ittm[1] + ptm[1] * ittm[3]; + m[2] = ptm[2] * ittm[0] + ptm[3] * ittm[2]; + m[3] = ptm[2] * ittm[1] + ptm[3] * ittm[3]; + m[4] = ptm[4] * ittm[0] + ptm[5] * ittm[2] + ittm[4]; + m[5] = ptm[4] * ittm[1] + ptm[5] * ittm[3] + ittm[5]; + } + id = _createGradient(shading_pattern->getShading(), + m, + !is_stroke); + } else if ( pattern->getType() == 1 ) { // Tiling pattern + id = _createTilingPattern(static_cast<GfxTilingPattern*>(pattern), state, is_stroke); + } + } else { + return nullptr; + } + gchar *urltext = g_strdup_printf ("url(#%s)", id); + g_free(id); + return urltext; +} + +/** + * \brief Creates a tiling pattern from poppler's data structure + * Creates a sub-page PdfParser and uses it to parse the pattern's content stream. + * \return id of the created pattern + */ +gchar *SvgBuilder::_createTilingPattern(GfxTilingPattern *tiling_pattern, + GfxState *state, bool is_stroke) { + + Inkscape::XML::Node *pattern_node = _xml_doc->createElement("svg:pattern"); + // Set pattern transform matrix + const double *p2u = tiling_pattern->getMatrix(); + double m[6] = {1, 0, 0, 1, 0, 0}; + double det; + det = _ttm[0] * _ttm[3] - _ttm[1] * _ttm[2]; // see LP Bug 1168908 + if (det) { + double ittm[6]; // invert ttm + ittm[0] = _ttm[3] / det; + ittm[1] = -_ttm[1] / det; + ittm[2] = -_ttm[2] / det; + ittm[3] = _ttm[0] / det; + ittm[4] = (_ttm[2] * _ttm[5] - _ttm[3] * _ttm[4]) / det; + ittm[5] = (_ttm[1] * _ttm[4] - _ttm[0] * _ttm[5]) / det; + m[0] = p2u[0] * ittm[0] + p2u[1] * ittm[2]; + m[1] = p2u[0] * ittm[1] + p2u[1] * ittm[3]; + m[2] = p2u[2] * ittm[0] + p2u[3] * ittm[2]; + m[3] = p2u[2] * ittm[1] + p2u[3] * ittm[3]; + m[4] = p2u[4] * ittm[0] + p2u[5] * ittm[2] + ittm[4]; + m[5] = p2u[4] * ittm[1] + p2u[5] * ittm[3] + ittm[5]; + } + Geom::Affine pat_matrix(m[0], m[1], m[2], m[3], m[4], m[5]); + pattern_node->setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(pat_matrix)); + pattern_node->setAttribute("patternUnits", "userSpaceOnUse"); + // Set pattern tiling + // FIXME: don't ignore XStep and YStep + const double *bbox = tiling_pattern->getBBox(); + pattern_node->setAttributeSvgDouble("x", 0.0); + pattern_node->setAttributeSvgDouble("y", 0.0); + pattern_node->setAttributeSvgDouble("width", bbox[2] - bbox[0]); + pattern_node->setAttributeSvgDouble("height", bbox[3] - bbox[1]); + + // Convert BBox for PdfParser + PDFRectangle box; + box.x1 = bbox[0]; + box.y1 = bbox[1]; + box.x2 = bbox[2]; + box.y2 = bbox[3]; + // Create new SvgBuilder and sub-page PdfParser + SvgBuilder *pattern_builder = new SvgBuilder(this, pattern_node); + PdfParser *pdf_parser = new PdfParser(_xref, pattern_builder, tiling_pattern->getResDict(), + &box); + // Get pattern color space + GfxPatternColorSpace *pat_cs = (GfxPatternColorSpace *)( is_stroke ? state->getStrokeColorSpace() + : state->getFillColorSpace() ); + // Set fill/stroke colors if this is an uncolored tiling pattern + GfxColorSpace *cs = nullptr; + if ( tiling_pattern->getPaintType() == 2 && ( cs = pat_cs->getUnder() ) ) { + GfxState *pattern_state = pdf_parser->getState(); + pattern_state->setFillColorSpace(cs->copy()); + pattern_state->setFillColor(state->getFillColor()); + pattern_state->setStrokeColorSpace(cs->copy()); + pattern_state->setStrokeColor(state->getFillColor()); + } + + // Generate the SVG pattern + pdf_parser->parse(tiling_pattern->getContentStream()); + + // Cleanup + delete pdf_parser; + delete pattern_builder; + + // Append the pattern to defs + _doc->getDefs()->getRepr()->appendChild(pattern_node); + gchar *id = g_strdup(pattern_node->attribute("id")); + Inkscape::GC::release(pattern_node); + + return id; +} + +/** + * \brief Creates a linear or radial gradient from poppler's data structure + * \param shading poppler's data structure for the shading + * \param matrix gradient transformation, can be null + * \param for_shading true if we're creating this for a shading operator; false otherwise + * \return id of the created object + */ +gchar *SvgBuilder::_createGradient(GfxShading *shading, double *matrix, bool for_shading) { + Inkscape::XML::Node *gradient; + _POPPLER_CONST Function *func; + int num_funcs; + bool extend0, extend1; + + if ( shading->getType() == 2 ) { // Axial shading + gradient = _xml_doc->createElement("svg:linearGradient"); + GfxAxialShading *axial_shading = static_cast<GfxAxialShading*>(shading); + double x1, y1, x2, y2; + axial_shading->getCoords(&x1, &y1, &x2, &y2); + gradient->setAttributeSvgDouble("x1", x1); + gradient->setAttributeSvgDouble("y1", y1); + gradient->setAttributeSvgDouble("x2", x2); + gradient->setAttributeSvgDouble("y2", y2); + extend0 = axial_shading->getExtend0(); + extend1 = axial_shading->getExtend1(); + num_funcs = axial_shading->getNFuncs(); + func = axial_shading->getFunc(0); + } else if (shading->getType() == 3) { // Radial shading + gradient = _xml_doc->createElement("svg:radialGradient"); + GfxRadialShading *radial_shading = static_cast<GfxRadialShading*>(shading); + double x1, y1, r1, x2, y2, r2; + radial_shading->getCoords(&x1, &y1, &r1, &x2, &y2, &r2); + // FIXME: the inner circle's radius is ignored here + gradient->setAttributeSvgDouble("fx", x1); + gradient->setAttributeSvgDouble("fy", y1); + gradient->setAttributeSvgDouble("cx", x2); + gradient->setAttributeSvgDouble("cy", y2); + gradient->setAttributeSvgDouble("r", r2); + extend0 = radial_shading->getExtend0(); + extend1 = radial_shading->getExtend1(); + num_funcs = radial_shading->getNFuncs(); + func = radial_shading->getFunc(0); + } else { // Unsupported shading type + return nullptr; + } + gradient->setAttribute("gradientUnits", "userSpaceOnUse"); + // If needed, flip the gradient transform around the y axis + if (matrix) { + Geom::Affine pat_matrix(matrix[0], matrix[1], matrix[2], matrix[3], + matrix[4], matrix[5]); + if ( !for_shading && _is_top_level ) { + Geom::Affine flip(1.0, 0.0, 0.0, -1.0, 0.0, Inkscape::Util::Quantity::convert(_height, "px", "pt")); + pat_matrix *= flip; + } + gradient->setAttributeOrRemoveIfEmpty("gradientTransform", sp_svg_transform_write(pat_matrix)); + } + + if ( extend0 && extend1 ) { + gradient->setAttribute("spreadMethod", "pad"); + } + + if ( num_funcs > 1 || !_addGradientStops(gradient, shading, func) ) { + Inkscape::GC::release(gradient); + return nullptr; + } + + Inkscape::XML::Node *defs = _doc->getDefs()->getRepr(); + defs->appendChild(gradient); + gchar *id = g_strdup(gradient->attribute("id")); + Inkscape::GC::release(gradient); + + return id; +} + +#define EPSILON 0.0001 +/** + * \brief Adds a stop with the given properties to the gradient's representation + */ +void SvgBuilder::_addStopToGradient(Inkscape::XML::Node *gradient, double offset, + GfxRGB *color, double opacity) { + Inkscape::XML::Node *stop = _xml_doc->createElement("svg:stop"); + SPCSSAttr *css = sp_repr_css_attr_new(); + Inkscape::CSSOStringStream os_opacity; + gchar *color_text = nullptr; + if ( _transp_group_stack != nullptr && _transp_group_stack->for_softmask ) { + double gray = (double)color->r / 65535.0; + gray = CLAMP(gray, 0.0, 1.0); + os_opacity << gray; + color_text = (char*) "#ffffff"; + } else { + os_opacity << opacity; + color_text = svgConvertGfxRGB(color); + } + sp_repr_css_set_property(css, "stop-opacity", os_opacity.str().c_str()); + sp_repr_css_set_property(css, "stop-color", color_text); + + sp_repr_css_change(stop, css, "style"); + sp_repr_css_attr_unref(css); + stop->setAttributeCssDouble("offset", offset); + + gradient->appendChild(stop); + Inkscape::GC::release(stop); +} + +static bool svgGetShadingColorRGB(GfxShading *shading, double offset, GfxRGB *result) { + GfxColorSpace *color_space = shading->getColorSpace(); + GfxColor temp; + if ( shading->getType() == 2 ) { // Axial shading + (static_cast<GfxAxialShading*>(shading))->getColor(offset, &temp); + } else if ( shading->getType() == 3 ) { // Radial shading + (static_cast<GfxRadialShading*>(shading))->getColor(offset, &temp); + } else { + return false; + } + // Convert it to RGB + color_space->getRGB(&temp, result); + + return true; +} + +#define INT_EPSILON 8 +bool SvgBuilder::_addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func) { + int type = func->getType(); + if ( type == 0 || type == 2 ) { // Sampled or exponential function + GfxRGB stop1, stop2; + if ( !svgGetShadingColorRGB(shading, 0.0, &stop1) || + !svgGetShadingColorRGB(shading, 1.0, &stop2) ) { + return false; + } else { + _addStopToGradient(gradient, 0.0, &stop1, 1.0); + _addStopToGradient(gradient, 1.0, &stop2, 1.0); + } + } else if ( type == 3 ) { // Stitching + auto stitchingFunc = static_cast<_POPPLER_CONST StitchingFunction*>(func); + const double *bounds = stitchingFunc->getBounds(); + const double *encode = stitchingFunc->getEncode(); + int num_funcs = stitchingFunc->getNumFuncs(); + + // Add stops from all the stitched functions + GfxRGB prev_color, color; + svgGetShadingColorRGB(shading, bounds[0], &prev_color); + _addStopToGradient(gradient, bounds[0], &prev_color, 1.0); + for ( int i = 0 ; i < num_funcs ; i++ ) { + svgGetShadingColorRGB(shading, bounds[i + 1], &color); + // Add stops + if (stitchingFunc->getFunc(i)->getType() == 2) { // process exponential fxn + double expE = (static_cast<_POPPLER_CONST ExponentialFunction*>(stitchingFunc->getFunc(i)))->getE(); + if (expE > 1.0) { + expE = (bounds[i + 1] - bounds[i])/expE; // approximate exponential as a single straight line at x=1 + if (encode[2*i] == 0) { // normal sequence + _addStopToGradient(gradient, bounds[i + 1] - expE, &prev_color, 1.0); + } else { // reflected sequence + _addStopToGradient(gradient, bounds[i] + expE, &color, 1.0); + } + } + } + _addStopToGradient(gradient, bounds[i + 1], &color, 1.0); + prev_color = color; + } + } else { // Unsupported function type + return false; + } + + return true; +} + +/** + * \brief Sets _invalidated_style to true to indicate that styles have to be updated + * Used for text output when glyphs are buffered till a font change + */ +void SvgBuilder::updateStyle(GfxState *state) { + if (_in_text_object) { + _invalidated_style = true; + _current_state = state; + } +} + +/* + MatchingChars + Count for how many characters s1 matches sp taking into account + that a space in sp may be removed or replaced by some other tokens + specified in the code. (Bug LP #179589) +*/ +static size_t MatchingChars(std::string s1, std::string sp) +{ + size_t is = 0; + size_t ip = 0; + + while(is < s1.length() && ip < sp.length()) { + if (s1[is] == sp[ip]) { + is++; ip++; + } else if (sp[ip] == ' ') { + ip++; + if (s1[is] == '_') { // Valid matches to spaces in sp. + is++; + } + } else { + break; + } + } + return ip; +} + +/* + SvgBuilder::_BestMatchingFont + Scan the available fonts to find the font name that best matches PDFname. + (Bug LP #179589) +*/ +std::string SvgBuilder::_BestMatchingFont(std::string PDFname) +{ + double bestMatch = 0; + std::string bestFontname = "Arial"; + + for (auto fontname : _availableFontNames) { + // At least the first word of the font name should match. + size_t minMatch = fontname.find(" "); + if (minMatch == std::string::npos) { + minMatch = fontname.length(); + } + + size_t Match = MatchingChars(PDFname, fontname); + if (Match >= minMatch) { + double relMatch = (float)Match / (fontname.length() + PDFname.length()); + if (relMatch > bestMatch) { + bestMatch = relMatch; + bestFontname = fontname; + } + } + } + + if (bestMatch == 0) + return PDFname; + else + return bestFontname; +} + +/** + * This array holds info about translating font weight names to more or less CSS equivalents + */ +static char *font_weight_translator[][2] = { + // clang-format off + {(char*) "bold", (char*) "bold"}, + {(char*) "light", (char*) "300"}, + {(char*) "black", (char*) "900"}, + {(char*) "heavy", (char*) "900"}, + {(char*) "ultrabold", (char*) "800"}, + {(char*) "extrabold", (char*) "800"}, + {(char*) "demibold", (char*) "600"}, + {(char*) "semibold", (char*) "600"}, + {(char*) "medium", (char*) "500"}, + {(char*) "book", (char*) "normal"}, + {(char*) "regular", (char*) "normal"}, + {(char*) "roman", (char*) "normal"}, + {(char*) "normal", (char*) "normal"}, + {(char*) "ultralight", (char*) "200"}, + {(char*) "extralight", (char*) "200"}, + {(char*) "thin", (char*) "100"} + // clang-format on +}; + +/** + * \brief Updates _font_style according to the font set in parameter state + */ +void SvgBuilder::updateFont(GfxState *state) { + + TRACE(("updateFont()\n")); + _need_font_update = false; + updateTextMatrix(state); // Ensure that we have a text matrix built + + _font_style = sp_repr_css_attr_new(); + auto font = state->getFont(); + // Store original name + if (font->getName()) { + _font_specification = font->getName()->getCString(); + } else { + _font_specification = "Arial"; + } + + // Prune the font name to get the correct font family name + // In a PDF font names can look like this: IONIPB+MetaPlusBold-Italic + char *font_family = nullptr; + char *font_style = nullptr; + char *font_style_lowercase = nullptr; + const char *plus_sign = strstr(_font_specification, "+"); + if (plus_sign) { + font_family = g_strdup(plus_sign + 1); + _font_specification = plus_sign + 1; + } else { + font_family = g_strdup(_font_specification); + } + char *style_delim = nullptr; + if ( ( style_delim = g_strrstr(font_family, "-") ) || + ( style_delim = g_strrstr(font_family, ",") ) ) { + font_style = style_delim + 1; + font_style_lowercase = g_ascii_strdown(font_style, -1); + style_delim[0] = 0; + } + + // Font family + if (font->getFamily()) { // if font family is explicitly given use it. + sp_repr_css_set_property(_font_style, "font-family", font->getFamily()->getCString()); + } else { + int attr_value = _preferences->getAttributeInt("localFonts", 1); + if (attr_value != 0) { + // Find the font that best matches the stripped down (orig)name (Bug LP #179589). + sp_repr_css_set_property(_font_style, "font-family", _BestMatchingFont(font_family).c_str()); + } else { + sp_repr_css_set_property(_font_style, "font-family", font_family); + } + } + + // Font style + if (font->isItalic()) { + sp_repr_css_set_property(_font_style, "font-style", "italic"); + } else if (font_style) { + if ( strstr(font_style_lowercase, "italic") || + strstr(font_style_lowercase, "slanted") ) { + sp_repr_css_set_property(_font_style, "font-style", "italic"); + } else if (strstr(font_style_lowercase, "oblique")) { + sp_repr_css_set_property(_font_style, "font-style", "oblique"); + } + } + + // Font variant -- default 'normal' value + sp_repr_css_set_property(_font_style, "font-variant", "normal"); + + // Font weight + GfxFont::Weight font_weight = font->getWeight(); + char *css_font_weight = nullptr; + if ( font_weight != GfxFont::WeightNotDefined ) { + if ( font_weight == GfxFont::W400 ) { + css_font_weight = (char*) "normal"; + } else if ( font_weight == GfxFont::W700 ) { + css_font_weight = (char*) "bold"; + } else { + gchar weight_num[4] = "100"; + weight_num[0] = (gchar)( '1' + (font_weight - GfxFont::W100) ); + sp_repr_css_set_property(_font_style, "font-weight", (gchar *)&weight_num); + } + } else if (font_style) { + // Apply the font weight translations + int num_translations = sizeof(font_weight_translator) / ( 2 * sizeof(char *) ); + for ( int i = 0 ; i < num_translations ; i++ ) { + if (strstr(font_style_lowercase, font_weight_translator[i][0])) { + css_font_weight = font_weight_translator[i][1]; + } + } + } else { + css_font_weight = (char*) "normal"; + } + if (css_font_weight) { + sp_repr_css_set_property(_font_style, "font-weight", css_font_weight); + } + g_free(font_family); + if (font_style_lowercase) { + g_free(font_style_lowercase); + } + + // Font stretch + GfxFont::Stretch font_stretch = font->getStretch(); + gchar *stretch_value = nullptr; + switch (font_stretch) { + case GfxFont::UltraCondensed: + stretch_value = (char*) "ultra-condensed"; + break; + case GfxFont::ExtraCondensed: + stretch_value = (char*) "extra-condensed"; + break; + case GfxFont::Condensed: + stretch_value = (char*) "condensed"; + break; + case GfxFont::SemiCondensed: + stretch_value = (char*) "semi-condensed"; + break; + case GfxFont::Normal: + stretch_value = (char*) "normal"; + break; + case GfxFont::SemiExpanded: + stretch_value = (char*) "semi-expanded"; + break; + case GfxFont::Expanded: + stretch_value = (char*) "expanded"; + break; + case GfxFont::ExtraExpanded: + stretch_value = (char*) "extra-expanded"; + break; + case GfxFont::UltraExpanded: + stretch_value = (char*) "ultra-expanded"; + break; + default: + break; + } + if ( stretch_value != nullptr ) { + sp_repr_css_set_property(_font_style, "font-stretch", stretch_value); + } + + // Font size + Inkscape::CSSOStringStream os_font_size; + double css_font_size = _font_scaling * state->getFontSize(); + if ( font->getType() == fontType3 ) { + const double *font_matrix = font->getFontMatrix(); + if ( font_matrix[0] != 0.0 ) { + css_font_size *= font_matrix[3] / font_matrix[0]; + } + } + os_font_size << css_font_size; + sp_repr_css_set_property(_font_style, "font-size", os_font_size.str().c_str()); + + // Writing mode + if ( font->getWMode() == 0 ) { + sp_repr_css_set_property(_font_style, "writing-mode", "lr"); + } else { + sp_repr_css_set_property(_font_style, "writing-mode", "tb"); + } + + _invalidated_style = true; +} + +/** + * \brief Shifts the current text position by the given amount (specified in text space) + */ +void SvgBuilder::updateTextShift(GfxState *state, double shift) { + double shift_value = -shift * 0.001 * fabs(state->getFontSize()); + if (state->getFont()->getWMode()) { + _text_position[1] += shift_value; + } else { + _text_position[0] += shift_value; + } +} + +/** + * \brief Updates current text position + */ +void SvgBuilder::updateTextPosition(double tx, double ty) { + Geom::Point new_position(tx, ty); + _text_position = new_position; +} + +/** + * \brief Flushes the buffered characters + */ +void SvgBuilder::updateTextMatrix(GfxState *state) { + _flushText(); + // Update text matrix + const double *text_matrix = state->getTextMat(); + double w_scale = sqrt( text_matrix[0] * text_matrix[0] + text_matrix[2] * text_matrix[2] ); + double h_scale = sqrt( text_matrix[1] * text_matrix[1] + text_matrix[3] * text_matrix[3] ); + double max_scale; + if ( w_scale > h_scale ) { + max_scale = w_scale; + } else { + max_scale = h_scale; + } + // Calculate new text matrix + Geom::Affine new_text_matrix(text_matrix[0] * state->getHorizScaling(), + text_matrix[1] * state->getHorizScaling(), + -text_matrix[2], -text_matrix[3], + 0.0, 0.0); + + if ( fabs( max_scale - 1.0 ) > EPSILON ) { + // Cancel out scaling by font size in text matrix + for ( int i = 0 ; i < 4 ; i++ ) { + new_text_matrix[i] /= max_scale; + } + } + _text_matrix = new_text_matrix; + _font_scaling = max_scale; +} + +/** + * \brief Writes the buffered characters to the SVG document + */ +void SvgBuilder::_flushText() { + // Ignore empty strings + if ( _glyphs.empty()) { + _glyphs.clear(); + return; + } + std::vector<SvgGlyph>::iterator i = _glyphs.begin(); + const SvgGlyph& first_glyph = (*i); + int render_mode = first_glyph.render_mode; + // Ignore invisible characters + if ( render_mode == 3 ) { + _glyphs.clear(); + return; + } + + Inkscape::XML::Node *text_node = _xml_doc->createElement("svg:text"); + + // we preserve spaces in the text objects we create, this applies to any descendant + text_node->setAttribute("xml:space", "preserve"); + + // Set text matrix + Geom::Affine text_transform(_text_matrix); + text_transform[4] = first_glyph.position[0]; + text_transform[5] = first_glyph.position[1]; + text_node->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(text_transform)); + + bool new_tspan = true; + bool same_coords[2] = {true, true}; + Geom::Point last_delta_pos; + unsigned int glyphs_in_a_row = 0; + Inkscape::XML::Node *tspan_node = nullptr; + Glib::ustring x_coords; + Glib::ustring y_coords; + Glib::ustring text_buffer; + + // Output all buffered glyphs + while (true) { + const SvgGlyph& glyph = (*i); + auto prev_iterator = (i == _glyphs.begin()) ? _glyphs.end() : (i-1); + // Check if we need to make a new tspan + if (glyph.style_changed) { + new_tspan = true; + } else if ( i != _glyphs.begin() ) { + const SvgGlyph& prev_glyph = (*prev_iterator); + if ( !( ( glyph.dy == 0.0 && prev_glyph.dy == 0.0 && + glyph.text_position[1] == prev_glyph.text_position[1] ) || + ( glyph.dx == 0.0 && prev_glyph.dx == 0.0 && + glyph.text_position[0] == prev_glyph.text_position[0] ) ) ) { + new_tspan = true; + } + } + + // Create tspan node if needed + if ( new_tspan || i == _glyphs.end() ) { + if (tspan_node) { + // Set the x and y coordinate arrays + if ( same_coords[0] ) { + tspan_node->setAttributeSvgDouble("x", last_delta_pos[0]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("x", x_coords); + } + if ( same_coords[1] ) { + tspan_node->setAttributeSvgDouble("y", last_delta_pos[1]); + } else { + tspan_node->setAttributeOrRemoveIfEmpty("y", y_coords); + } + TRACE(("tspan content: %s\n", text_buffer.c_str())); + if ( glyphs_in_a_row > 1 ) { + tspan_node->setAttribute("sodipodi:role", "line"); + } + // Add text content node to tspan + Inkscape::XML::Node *text_content = _xml_doc->createTextNode(text_buffer.c_str()); + tspan_node->appendChild(text_content); + Inkscape::GC::release(text_content); + text_node->appendChild(tspan_node); + // Clear temporary buffers + x_coords.clear(); + y_coords.clear(); + text_buffer.clear(); + Inkscape::GC::release(tspan_node); + glyphs_in_a_row = 0; + } + if ( i == _glyphs.end() ) { + sp_repr_css_attr_unref((*prev_iterator).style); + break; + } else { + tspan_node = _xml_doc->createElement("svg:tspan"); + + /////// + // Create a font specification string and save the attribute in the style + PangoFontDescription *descr = pango_font_description_from_string(glyph.font_specification); + Glib::ustring properFontSpec = font_factory::Default()->ConstructFontSpecification(descr); + pango_font_description_free(descr); + sp_repr_css_set_property(glyph.style, "-inkscape-font-specification", properFontSpec.c_str()); + + // Set style and unref SPCSSAttr if it won't be needed anymore + // assume all <tspan> nodes in a <text> node share the same style + sp_repr_css_change(text_node, glyph.style, "style"); + if ( glyph.style_changed && i != _glyphs.begin() ) { // Free previous style + sp_repr_css_attr_unref((*prev_iterator).style); + } + } + new_tspan = false; + } + if ( glyphs_in_a_row > 0 && i != _glyphs.begin() ) { + x_coords.append(" "); + y_coords.append(" "); + // Check if we have the same coordinates + const SvgGlyph& prev_glyph = (*prev_iterator); + for ( int p = 0 ; p < 2 ; p++ ) { + if ( glyph.text_position[p] != prev_glyph.text_position[p] ) { + same_coords[p] = false; + } + } + } + // Append the coordinates to their respective strings + Geom::Point delta_pos( glyph.text_position - first_glyph.text_position ); + delta_pos[1] += glyph.rise; + delta_pos[1] *= -1.0; // flip it + delta_pos *= _font_scaling; + Inkscape::CSSOStringStream os_x; + os_x << delta_pos[0]; + x_coords.append(os_x.str()); + Inkscape::CSSOStringStream os_y; + os_y << delta_pos[1]; + y_coords.append(os_y.str()); + last_delta_pos = delta_pos; + + // Append the character to the text buffer + if ( !glyph.code.empty() ) { + text_buffer.append(1, glyph.code[0]); + } + + glyphs_in_a_row++; + ++i; + } + _container->appendChild(text_node); + Inkscape::GC::release(text_node); + + _glyphs.clear(); +} + +void SvgBuilder::beginString(GfxState *state) { + if (_need_font_update) { + updateFont(state); + } + IFTRACE(double *m = state->getTextMat()); + TRACE(("tm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); + IFTRACE(m = state->getCTM()); + TRACE(("ctm: %f %f %f %f %f %f\n",m[0], m[1],m[2], m[3], m[4], m[5])); +} + +/** + * \brief Adds the specified character to the text buffer + * Takes care of converting it to UTF-8 and generates a new style repr if style + * has changed since the last call. + */ +void SvgBuilder::addChar(GfxState *state, double x, double y, + double dx, double dy, + double originX, double originY, + CharCode /*code*/, int /*nBytes*/, Unicode const *u, int uLen) { + + // Skip control characters, found in LaTeX generated PDFs + // https://gitlab.com/inkscape/inkscape/-/issues/1369 + if (uLen > 0 && u[0] < 0x80 && g_ascii_iscntrl(u[0]) && !g_ascii_isspace(u[0])) { + g_warning("Skipping ASCII control character %u", u[0]); + _text_position += Geom::Point(dx, dy); + return; + } + + bool is_space = ( uLen == 1 && u[0] == 32 ); + // Skip beginning space + if ( is_space && _glyphs.empty()) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + // Allow only one space in a row + if ( is_space && (_glyphs[_glyphs.size() - 1].code.size() == 1) && + (_glyphs[_glyphs.size() - 1].code[0] == 32) ) { + Geom::Point delta(dx, dy); + _text_position += delta; + return; + } + + SvgGlyph new_glyph; + new_glyph.is_space = is_space; + new_glyph.position = Geom::Point( x - originX, y - originY ); + new_glyph.text_position = _text_position; + new_glyph.dx = dx; + new_glyph.dy = dy; + Geom::Point delta(dx, dy); + _text_position += delta; + + // Convert the character to UTF-8 since that's our SVG document's encoding + { + gunichar2 uu[8] = {0}; + + for (int i = 0; i < uLen; i++) { + uu[i] = u[i]; + } + + gchar *tmp = g_utf16_to_utf8(uu, uLen, nullptr, nullptr, nullptr); + if ( tmp && *tmp ) { + new_glyph.code = tmp; + } else { + new_glyph.code.clear(); + } + g_free(tmp); + } + + // Copy current style if it has changed since the previous glyph + if (_invalidated_style || _glyphs.empty()) { + new_glyph.style_changed = true; + int render_mode = state->getRender(); + // Set style + bool has_fill = !( render_mode & 1 ); + bool has_stroke = ( render_mode & 3 ) == 1 || ( render_mode & 3 ) == 2; + new_glyph.style = _setStyle(state, has_fill, has_stroke); + // Find a way to handle blend modes on text + /* GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(new_glyph.style, "mix-blend-mode", enum_blend_mode[blendmode].key); + } */ + new_glyph.render_mode = render_mode; + sp_repr_css_merge(new_glyph.style, _font_style); // Merge with font style + _invalidated_style = false; + } else { + new_glyph.style_changed = false; + // Point to previous glyph's style information + const SvgGlyph& prev_glyph = _glyphs.back(); + new_glyph.style = prev_glyph.style; + /* GfxBlendMode blendmode = state->getBlendMode(); + if (blendmode) { + sp_repr_css_set_property(new_glyph.style, "mix-blend-mode", enum_blend_mode[blendmode].key); + } */ + new_glyph.render_mode = prev_glyph.render_mode; + } + new_glyph.font_specification = _font_specification; + new_glyph.rise = state->getRise(); + + _glyphs.push_back(new_glyph); +} + +void SvgBuilder::endString(GfxState * /*state*/) { +} + +void SvgBuilder::beginTextObject(GfxState *state) { + _in_text_object = true; + _invalidated_style = true; // Force copying of current state + _current_state = state; +} + +void SvgBuilder::endTextObject(GfxState * /*state*/) { + _flushText(); + // TODO: clip if render_mode >= 4 + _in_text_object = false; +} + +/** + * Helper functions for supporting direct PNG output into a base64 encoded stream + */ +void png_write_vector(png_structp png_ptr, png_bytep data, png_size_t length) +{ + auto *v_ptr = reinterpret_cast<std::vector<guchar> *>(png_get_io_ptr(png_ptr)); // Get pointer to stream + for ( unsigned i = 0 ; i < length ; i++ ) { + v_ptr->push_back(data[i]); + } +} + +/** + * \brief Creates an <image> element containing the given ImageStream as a PNG + * + */ +Inkscape::XML::Node *SvgBuilder::_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only, + bool invert_alpha) { + + // Create PNG write struct + png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if ( png_ptr == nullptr ) { + return nullptr; + } + // Create PNG info struct + png_infop info_ptr = png_create_info_struct(png_ptr); + if ( info_ptr == nullptr ) { + png_destroy_write_struct(&png_ptr, nullptr); + return nullptr; + } + // Set error handler + if (setjmp(png_jmpbuf(png_ptr))) { + png_destroy_write_struct(&png_ptr, &info_ptr); + return nullptr; + } + // Decide whether we should embed this image + int attr_value = _preferences->getAttributeInt("embedImages", 1); + bool embed_image = ( attr_value != 0 ); + // Set read/write functions + std::vector<guchar> png_buffer; + FILE *fp = nullptr; + gchar *file_name = nullptr; + if (embed_image) { + png_set_write_fn(png_ptr, &png_buffer, png_write_vector, nullptr); + } else { + static int counter = 0; + file_name = g_strdup_printf("%s_img%d.png", _docname, counter++); + fp = fopen(file_name, "wb"); + if ( fp == nullptr ) { + png_destroy_write_struct(&png_ptr, &info_ptr); + g_free(file_name); + return nullptr; + } + png_init_io(png_ptr, fp); + } + + // Set header data + if ( !invert_alpha && !alpha_only ) { + png_set_invert_alpha(png_ptr); + } + png_color_8 sig_bit; + if (alpha_only) { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_GRAY, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 0; + sig_bit.green = 0; + sig_bit.blue = 0; + sig_bit.gray = 8; + sig_bit.alpha = 0; + } else { + png_set_IHDR(png_ptr, info_ptr, + width, + height, + 8, /* bit_depth */ + PNG_COLOR_TYPE_RGB_ALPHA, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + sig_bit.alpha = 8; + } + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + png_set_bgr(png_ptr); + // Write the file header + png_write_info(png_ptr, info_ptr); + + // Convert pixels + ImageStream *image_stream; + if (alpha_only) { + if (color_map) { + image_stream = new ImageStream(str, width, color_map->getNumPixelComps(), + color_map->getBits()); + } else { + image_stream = new ImageStream(str, width, 1, 1); + } + image_stream->reset(); + + // Convert grayscale values + unsigned char *buffer = new unsigned char[width]; + int invert_bit = invert_alpha ? 1 : 0; + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + if (color_map) { + color_map->getGrayLine(row, buffer, width); + } else { + unsigned char *buf_ptr = buffer; + for ( int x = 0 ; x < width ; x++ ) { + if ( row[x] ^ invert_bit ) { + *buf_ptr++ = 0; + } else { + *buf_ptr++ = 255; + } + } + } + png_write_row(png_ptr, (png_bytep)buffer); + } + delete [] buffer; + } else if (color_map) { + image_stream = new ImageStream(str, width, + color_map->getNumPixelComps(), + color_map->getBits()); + image_stream->reset(); + + // Convert RGB values + unsigned int *buffer = new unsigned int[width]; + if (mask_colors) { + for ( int y = 0 ; y < height ; y++ ) { + unsigned char *row = image_stream->getLine(); + color_map->getRGBLine(row, buffer, width); + + unsigned int *dest = buffer; + for ( int x = 0 ; x < width ; x++ ) { + // Check each color component against the mask + for ( int i = 0; i < color_map->getNumPixelComps() ; i++) { + if ( row[i] < mask_colors[2*i] * 255 || + row[i] > mask_colors[2*i + 1] * 255 ) { + *dest = *dest | 0xff000000; + break; + } + } + // Advance to the next pixel + row += color_map->getNumPixelComps(); + dest++; + } + // Write it to the PNG + png_write_row(png_ptr, (png_bytep)buffer); + } + } else { + for ( int i = 0 ; i < height ; i++ ) { + unsigned char *row = image_stream->getLine(); + memset((void*)buffer, 0xff, sizeof(int) * width); + color_map->getRGBLine(row, buffer, width); + png_write_row(png_ptr, (png_bytep)buffer); + } + } + delete [] buffer; + + } else { // A colormap must be provided, so quit + png_destroy_write_struct(&png_ptr, &info_ptr); + if (!embed_image) { + fclose(fp); + g_free(file_name); + } + return nullptr; + } + delete image_stream; + str->close(); + // Close PNG + png_write_end(png_ptr, info_ptr); + png_destroy_write_struct(&png_ptr, &info_ptr); + + // Create repr + Inkscape::XML::Node *image_node = _xml_doc->createElement("svg:image"); + image_node->setAttributeSvgDouble("width", 1); + image_node->setAttributeSvgDouble("height", 1); + if( !interpolate ) { + SPCSSAttr *css = sp_repr_css_attr_new(); + // This should be changed after CSS4 Images widely supported. + sp_repr_css_set_property(css, "image-rendering", "optimizeSpeed"); + sp_repr_css_change(image_node, css, "style"); + sp_repr_css_attr_unref(css); + } + + // PS/PDF images are placed via a transformation matrix, no preserveAspectRatio used + image_node->setAttribute("preserveAspectRatio", "none"); + + // Set transformation + svgSetTransform(image_node, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + + // Create href + if (embed_image) { + // Append format specification to the URI + auto *base64String = g_base64_encode(png_buffer.data(), png_buffer.size()); + auto png_data = std::string("data:image/png;base64,") + base64String; + g_free(base64String); + image_node->setAttributeOrRemoveIfEmpty("xlink:href", png_data); + } else { + fclose(fp); + image_node->setAttribute("xlink:href", file_name); + g_free(file_name); + } + + return image_node; +} + +/** + * \brief Creates a <mask> with the specified width and height and adds to <defs> + * If we're not the top-level SvgBuilder, creates a <defs> too and adds the mask to it. + * \return the created XML node + */ +Inkscape::XML::Node *SvgBuilder::_createMask(double width, double height) { + Inkscape::XML::Node *mask_node = _xml_doc->createElement("svg:mask"); + mask_node->setAttribute("maskUnits", "userSpaceOnUse"); + mask_node->setAttributeSvgDouble("x", 0.0); + mask_node->setAttributeSvgDouble("y", 0.0); + mask_node->setAttributeSvgDouble("width", width); + mask_node->setAttributeSvgDouble("height", height); + // Append mask to defs + if (_is_top_level) { + _doc->getDefs()->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return _doc->getDefs()->getRepr()->lastChild(); + } else { // Work around for renderer bug when mask isn't defined in pattern + static int mask_count = 0; + Inkscape::XML::Node *defs = _root->firstChild(); + if ( !( defs && !strcmp(defs->name(), "svg:defs") ) ) { + // Create <defs> node + defs = _xml_doc->createElement("svg:defs"); + _root->addChild(defs, nullptr); + Inkscape::GC::release(defs); + defs = _root->firstChild(); + } + gchar *mask_id = g_strdup_printf("_mask%d", mask_count++); + mask_node->setAttribute("id", mask_id); + g_free(mask_id); + defs->appendChild(mask_node); + Inkscape::GC::release(mask_node); + return defs->lastChild(); + } +} + +void SvgBuilder::addImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, int *mask_colors) +{ + + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, mask_colors); + if (image_node) { + _setBlendMode(image_node, state); + _container->appendChild(image_node); + Inkscape::GC::release(image_node); + } +} + +void SvgBuilder::addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate) { + + // Create a rectangle + Inkscape::XML::Node *rect = _xml_doc->createElement("svg:rect"); + rect->setAttributeSvgDouble("x", 0.0); + rect->setAttributeSvgDouble("y", 0.0); + rect->setAttributeSvgDouble("width", 1.0); + rect->setAttributeSvgDouble("height", 1.0); + svgSetTransform(rect, Geom::Affine(1.0, 0.0, 0.0, -1.0, 0.0, 1.0)); + // Get current fill style and set it on the rectangle + SPCSSAttr *css = sp_repr_css_attr_new(); + _setFillStyle(css, state, false); + sp_repr_css_change(rect, css, "style"); + sp_repr_css_attr_unref(css); + _setBlendMode(rect, state); + + // Scaling 1x1 surfaces might not work so skip setting a mask with this size + if ( width > 1 || height > 1 ) { + Inkscape::XML::Node *mask_image_node = + _createImage(str, width, height, nullptr, interpolate, nullptr, true, invert); + if (mask_image_node) { + // Create the mask + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + Inkscape::GC::release(mask_image_node); + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + rect->setAttribute("mask", mask_url); + g_free(mask_url); + } + } + + // Add the rectangle to the container + _container->appendChild(rect); + Inkscape::GC::release(rect); +} + +void SvgBuilder::addMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, bool invert_mask, + bool mask_interpolate) +{ + + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + nullptr, mask_interpolate, nullptr, true, invert_mask); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Scale the mask to the size of the image + Geom::Affine mask_transform((double)width, 0.0, 0.0, (double)height, 0.0, 0.0); + mask_node->setAttributeOrRemoveIfEmpty("maskTransform", sp_svg_transform_write(mask_transform)); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _container->appendChild(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } + if (image_node) { + _setBlendMode(image_node, state); + Inkscape::GC::release(image_node); + } +} + +void SvgBuilder::addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, GfxImageColorMap *color_map, + bool interpolate, Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate) +{ + + Inkscape::XML::Node *mask_image_node = _createImage(mask_str, mask_width, mask_height, + mask_color_map, mask_interpolate, nullptr, true); + Inkscape::XML::Node *image_node = _createImage(str, width, height, color_map, interpolate, nullptr); + if ( mask_image_node && image_node ) { + // Create mask for the image + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Remove unnecessary transformation from the mask image + mask_image_node->removeAttribute("transform"); + mask_node->appendChild(mask_image_node); + // Set mask and add image + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + image_node->setAttribute("mask", mask_url); + g_free(mask_url); + _container->appendChild(image_node); + } + if (mask_image_node) { + Inkscape::GC::release(mask_image_node); + } + if (image_node) { + _setBlendMode(image_node, state); + Inkscape::GC::release(image_node); + } +} + +/** + * \brief Starts building a new transparency group + */ +void SvgBuilder::pushTransparencyGroup(GfxState * /*state*/, double *bbox, + GfxColorSpace * /*blending_color_space*/, + bool isolated, bool knockout, + bool for_softmask) { + + // Push node stack + pushNode("svg:g"); + + // Setup new transparency group + SvgTransparencyGroup *transpGroup = new SvgTransparencyGroup; + for (size_t i = 0; i < 4; i++) { + transpGroup->bbox[i] = bbox[i]; + } + transpGroup->isolated = isolated; + transpGroup->knockout = knockout; + transpGroup->for_softmask = for_softmask; + transpGroup->container = _container; + + // Push onto the stack + transpGroup->next = _transp_group_stack; + _transp_group_stack = transpGroup; +} + +void SvgBuilder::popTransparencyGroup(GfxState * /*state*/) { + // Restore node stack + popNode(); +} + +/** + * \brief Places the current transparency group into the current container + */ +void SvgBuilder::paintTransparencyGroup(GfxState * /*state*/, double * /*bbox*/) { + SvgTransparencyGroup *transpGroup = _transp_group_stack; + _container->appendChild(transpGroup->container); + Inkscape::GC::release(transpGroup->container); + // Pop the stack + _transp_group_stack = transpGroup->next; + delete transpGroup; +} + +/** + * \brief Creates a mask using the current transparency group as its content + */ +void SvgBuilder::setSoftMask(GfxState * /*state*/, double * /*bbox*/, bool /*alpha*/, + Function * /*transfer_func*/, GfxColor * /*backdrop_color*/) { + + // Create mask + Inkscape::XML::Node *mask_node = _createMask(1.0, 1.0); + // Add the softmask content to it + SvgTransparencyGroup *transpGroup = _transp_group_stack; + mask_node->appendChild(transpGroup->container); + Inkscape::GC::release(transpGroup->container); + // Apply the mask + _state_stack.back().softmask = mask_node; + pushGroup(); + gchar *mask_url = g_strdup_printf("url(#%s)", mask_node->attribute("id")); + _container->setAttribute("mask", mask_url); + g_free(mask_url); + // Pop the stack + _transp_group_stack = transpGroup->next; + delete transpGroup; +} + +void SvgBuilder::clearSoftMask(GfxState * /*state*/) { + if (_state_stack.back().softmask) { + _state_stack.back().softmask = nullptr; + popGroup(); + } +} + +} } } /* namespace Inkscape, Extension, Internal */ + +#endif /* HAVE_POPPLER */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/pdfinput/svg-builder.h b/src/extension/internal/pdfinput/svg-builder.h new file mode 100644 index 0000000..e91febd --- /dev/null +++ b/src/extension/internal/pdfinput/svg-builder.h @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H +#define SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + * Authors: + * miklos erdelyi + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef HAVE_POPPLER +#include "poppler-transition-api.h" + +class SPDocument; +namespace Inkscape { + namespace XML { + struct Document; + class Node; + } +} + +#include <2geom/point.h> +#include <2geom/affine.h> +#include <glibmm/ustring.h> + +#include "CharTypes.h" +class Function; +class GfxState; +struct GfxColor; +class GfxColorSpace; +struct GfxRGB; +class GfxPath; +class GfxPattern; +class GfxTilingPattern; +class GfxShading; +class GfxFont; +class GfxImageColorMap; +class Stream; +class XRef; + +class SPCSSAttr; + +#include <vector> +#include <glib.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +struct SvgTransparencyGroup; + +/** + * Holds information about the current softmask and group depth for use of libpoppler. + * Could be later used to store other graphics state parameters so that we could + * emit only the differences in style settings from the parent state. + */ +struct SvgGraphicsState { + Inkscape::XML::Node *softmask; // Points to current softmask node + int group_depth; // Depth of nesting groups at this level +}; + +/** + * Holds information about glyphs added by PdfParser which haven't been added + * to the document yet. + */ +struct SvgGlyph { + Geom::Point position; // Absolute glyph coords + Geom::Point text_position; // Absolute glyph coords in text space + double dx; // X advance value + double dy; // Y advance value + double rise; // Text rise parameter + Glib::ustring code; // UTF-8 coded character + bool is_space; + + bool style_changed; // Set to true if style has to be reset + SPCSSAttr *style; + int render_mode; // Text render mode + const char *font_specification; // Pointer to current font specification +}; + +/** + * Builds the inner SVG representation using libpoppler from the calls of PdfParser. + */ +class SvgBuilder { +public: + SvgBuilder(SPDocument *document, gchar *docname, XRef *xref); + SvgBuilder(SvgBuilder *parent, Inkscape::XML::Node *root); + virtual ~SvgBuilder(); + + // Property setting + void setDocumentSize(double width, double height); // Document size in px + void setAsLayer(char *layer_name=nullptr); + void setGroupOpacity(double opacity); + Inkscape::XML::Node *getPreferences() { + return _preferences; + } + void pushPage(); + + // Handling the node stack + Inkscape::XML::Node *pushGroup(); + Inkscape::XML::Node *popGroup(); + Inkscape::XML::Node *getContainer(); // Returns current group node + + // Path adding + void addPath(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void addShadedFill(GfxShading *shading, double *matrix, GfxPath *path, bool even_odd=false); + + // Image handling + void addImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, int *mask_colors); + void addImageMask(GfxState *state, Stream *str, int width, int height, + bool invert, bool interpolate); + void addMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + bool invert_mask, bool mask_interpolate); + void addSoftMaskedImage(GfxState *state, Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + Stream *mask_str, int mask_width, int mask_height, + GfxImageColorMap *mask_color_map, bool mask_interpolate); + + // Transparency group and soft mask handling + void pushTransparencyGroup(GfxState *state, double *bbox, + GfxColorSpace *blending_color_space, + bool isolated, bool knockout, + bool for_softmask); + void popTransparencyGroup(GfxState *state); + void paintTransparencyGroup(GfxState *state, double *bbox); + void setSoftMask(GfxState *state, double *bbox, bool alpha, + Function *transfer_func, GfxColor *backdrop_color); + void clearSoftMask(GfxState *state); + + // Text handling + void beginString(GfxState *state); + void endString(GfxState *state); + void addChar(GfxState *state, double x, double y, + double dx, double dy, + double originX, double originY, + CharCode code, int nBytes, Unicode const *u, int uLen); + void beginTextObject(GfxState *state); + void endTextObject(GfxState *state); + + bool isPatternTypeSupported(GfxPattern *pattern); + + // State manipulation + void saveState(); + void restoreState(); + void updateStyle(GfxState *state); + void updateFont(GfxState *state); + void updateTextPosition(double tx, double ty); + void updateTextShift(GfxState *state, double shift); + void updateTextMatrix(GfxState *state); + + // Clipping + void clip(GfxState *state, bool even_odd=false); + void setClipPath(GfxState *state, bool even_odd=false); + + // Transforming + void setTransform(double c0, double c1, double c2, double c3, double c4, + double c5); + void setTransform(double const *transform); + bool getTransform(double *transform); + +private: + void _init(); + + // Pattern creation + gchar *_createPattern(GfxPattern *pattern, GfxState *state, bool is_stroke=false); + gchar *_createGradient(GfxShading *shading, double *matrix, bool for_shading=false); + void _addStopToGradient(Inkscape::XML::Node *gradient, double offset, + GfxRGB *color, double opacity); + bool _addGradientStops(Inkscape::XML::Node *gradient, GfxShading *shading, + _POPPLER_CONST Function *func); + gchar *_createTilingPattern(GfxTilingPattern *tiling_pattern, GfxState *state, + bool is_stroke=false); + // Image/mask creation + Inkscape::XML::Node *_createImage(Stream *str, int width, int height, + GfxImageColorMap *color_map, bool interpolate, + int *mask_colors, bool alpha_only=false, + bool invert_alpha=false); + Inkscape::XML::Node *_createMask(double width, double height); + // Style setting + SPCSSAttr *_setStyle(GfxState *state, bool fill, bool stroke, bool even_odd=false); + void _setStrokeStyle(SPCSSAttr *css, GfxState *state); + void _setFillStyle(SPCSSAttr *css, GfxState *state, bool even_odd); + void _setBlendMode(Inkscape::XML::Node *node, GfxState *state); + void _flushText(); // Write buffered text into doc + + std::string _BestMatchingFont(std::string PDFname); + + // Handling of node stack + Inkscape::XML::Node *pushNode(const char* name); + Inkscape::XML::Node *popNode(); + std::vector<Inkscape::XML::Node *> _node_stack; + std::vector<int> _group_depth; // Depth of nesting groups + SvgTransparencyGroup *_transp_group_stack; // Transparency group stack + std::vector<SvgGraphicsState> _state_stack; + + SPCSSAttr *_font_style; // Current font style + const char *_font_specification; + double _font_scaling; + bool _need_font_update; + Geom::Affine _text_matrix; + Geom::Point _text_position; + std::vector<SvgGlyph> _glyphs; // Added characters + bool _in_text_object; // Whether we are inside a text object + bool _invalidated_style; + GfxState *_current_state; + std::vector<std::string> _availableFontNames; // Full names, used for matching font names (Bug LP #179589). + + bool _is_top_level; // Whether this SvgBuilder is the top-level one + SPDocument *_doc; + gchar *_docname; // Basename of the URI from which this document is created + XRef *_xref; // Cross-reference table from the PDF doc we're converting from + Inkscape::XML::Document *_xml_doc; + Inkscape::XML::Node *_root; // Root node from the point of view of this SvgBuilder + Inkscape::XML::Node *_container; // Current container (group/pattern/mask) + Inkscape::XML::Node *_preferences; // Preferences container node + double _width; // Document size in px + double _height; // Document size in px + double _ttm[6]; ///< temporary transform matrix + bool _ttm_is_set; + + Inkscape::XML::Node *_nv = nullptr; // XML NameView xml + Inkscape::XML::Node *_page = nullptr; // XML Page definition + int _page_num = 0; // Are we on a page + double _page_left = 0 ; // Move to the left for more pages + double _page_top = 0 ; // Move to the top (maybe) + bool _page_offset = false; +}; + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif // HAVE_POPPLER + +#endif // SEEN_EXTENSION_INTERNAL_PDFINPUT_SVGBUILDER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/png-output.cpp b/src/extension/internal/png-output.cpp new file mode 100644 index 0000000..68841ef --- /dev/null +++ b/src/extension/internal/png-output.cpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * An internal raster export which passes the generated PNG output + * to an external file. In the future this module could host more of + * the PNG generation code that isn't needed for other raster export options. + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "png-output.h" + +#include <cstdlib> +#include <iostream> +#include <string> +#include <glibmm.h> +#include <giomm.h> + +#include "clear-n_.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +void PngOutput::export_raster(Inkscape::Extension::Output * /*module*/, + const SPDocument * /*doc*/, std::string const &png_file, gchar const *filename) +{ + // We want to move the png file to the new location + Glib::RefPtr<Gio::File> input_fn = Gio::File::create_for_path(png_file); + Glib::RefPtr<Gio::File> output_fn = Gio::File::create_for_path(filename); + try { + // This file must be copied because the permissions must be created + // based on it's target location and not the temp directory. + input_fn->copy(output_fn, Gio::FILE_COPY_OVERWRITE | Gio::FILE_COPY_TARGET_DEFAULT_PERMS); + } + catch (const Gio::Error& e) { + std::cerr << "Moving resource " << png_file + << " to " << filename + << " failed: " << e.what().raw() << std::endl; + } +} + +void PngOutput::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("Portable Network Graphic") "</name>\n" + "<id>" SP_MODULE_KEY_RASTER_PNG "</id>\n" + "<param name='png_interlacing' type='bool' gui-text='" N_("Interlacing") "'>false</param>" + "<param name='png_bitdepth' type='optiongroup' appearance='combo' gui-text='" N_("Bit Depth") "'>" + "<option value='99'>" N_("RGBA 8") "</option>" // First because it's the default option + "<option value='100'>" N_("RGBA 16") "</option>" + "<option value='67'>" N_("GrayAlpha 8") "</option>" + "<option value='68'>" N_("GrayAlpha 16") "</option>" + "<option value='35'>" N_("RGB 8") "</option>" + "<option value='36'>" N_("RGB 16") "</option>" + "<option value='0'>" N_("Gray 1") "</option>" + "<option value='1'>" N_("Gray 2") "</option>" + "<option value='2'>" N_("Gray 4") "</option>" + "<option value='3'>" N_("Gray 8") "</option>" + "<option value='4'>" N_("Gray 16") "</option>" + "</param>" + "<param name='png_compression' type='optiongroup' appearance='combo' gui-text='" N_("Compression") "'>" + "<option value='0'>" N_("0 - No Compression") "</option>" + "<option value='1'>" N_("1 - Best Speed") "</option>" + "<option value='2'>2</option>" + "<option value='3'>3</option>" + "<option value='4'>4</option>" + "<option value='5'>5</option>" + "<option value='6'>" N_("6 - Default Compression") "</option>" // First because it's default (and broken) + "<option value='7'>7</option>" + "<option value='8'>8</option>" + "<option value='9'>" N_("9 - Best Compression") "</option>" + "</param>" + "<param name='png_phys' gui-text='" N_("pHYs DPI") "' type='float' min='0.0' max='100000.0'>0.0</param>" + "<param name='png_antialias' gui-text='" N_("Antialias") "' type='int' min='0' max='3'>2</param>" + "<output raster=\"true\">\n" + "<extension>.png</extension>\n" + "<mimetype>image/png</mimetype>\n" + "<filetypename>" N_("Portable Network Graphic (*.png)") "</filetypename>\n" + "<filetypetooltip>" N_("Default raster graphic export") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", + new PngOutput()); + // clang-format on +} + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/png-output.h b/src/extension/internal/png-output.h new file mode 100644 index 0000000..39e466d --- /dev/null +++ b/src/extension/internal/png-output.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * An internal raster export which passes the generated PNG output + * to an external file. In the future this module could host more of + * the PNG generation code that isn't needed for other raster export options. + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_PNG_OUTPUT_H +#define EXTENSION_INTERNAL_PNG_OUTPUT_H + +#include <glib.h> + +#include "extension/extension.h" +#include "extension/implementation/implementation.h" +#include "extension/output.h" +#include "extension/system.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PngOutput : public Inkscape::Extension::Implementation::Implementation +{ +public: + PngOutput(){}; + + bool check(Inkscape::Extension::Extension *module) override { return true; }; + + void export_raster(Inkscape::Extension::Output *module, + const SPDocument *doc, std::string const &png_file, gchar const *filename) override; + + static void init(); + +private: +}; + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + +#endif /* EXTENSION_INTERNAL_PNG_OUTPUT_H */ diff --git a/src/extension/internal/polyfill/README.md b/src/extension/internal/polyfill/README.md new file mode 100644 index 0000000..2677a50 --- /dev/null +++ b/src/extension/internal/polyfill/README.md @@ -0,0 +1,19 @@ +# JavaScript polyfills + +This directory contains JavaScript "Polyfills" to support rendering of SVG 2 +features that are not well supported by browsers, but appeared in the 2016 +[specification](https://www.w3.org/TR/2016/CR-SVG2-20160915/pservers.html#MeshGradients) + +The included files are: + - `mesh.js` mesh gradients supporting bicubic meshes and mesh on strokes. + - `mesh_compressed.include` mesh.js minified and wrapped as a C++11 raw string literal. + - `hatch.js` hatch paint server supporting linear and absolute paths hatches + (relative paths are not fully supported) + - `hatch_tests` folder with tests used for `hatch.js` rendering + +## Details +The coding standard used is [semistandard](https://github.com/Flet/semistandard), +a more permissive (allows endrow semicolons) over the famous, open-source +[standardjs](https://standardjs.com/). + +The minifier used for the compressed version is [JavaScript minifier](https://javascript-minifier.com/). diff --git a/src/extension/internal/polyfill/hatch.js b/src/extension/internal/polyfill/hatch.js new file mode 100644 index 0000000..c805425 --- /dev/null +++ b/src/extension/internal/polyfill/hatch.js @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: CC0 +/** @file + * Use patterns to render a hatch paint server via this polyfill + *//* + * Authors: + * - Valentin Ionita (2019) + * License: CC0 / Public Domain + */ + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const unitObjectBoundingBox = 'objectBoundingBox'; + const unitUserSpace = 'userSpaceOnUse'; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Copy attributes from the hatch with 'id' to the current element + const setReference = (el, id) => { + const attr = [ + 'x', 'y', 'pitch', 'rotate', + 'hatchUnits', 'hatchContentUnits', 'transform' + ]; + const template = document.getElementById(id.slice(1)); + + if (template && template.nodeName === 'hatch') { + attr.forEach(a => { + let t = template.getAttribute(a); + if (el.getAttribute(a) === null && t !== null) { + el.setAttribute(a, t); + } + }); + + if (el.children.length === 0) { + Array.from(template.children).forEach(c => { + el.appendChild(c.cloneNode(true)); + }); + } + } + }; + + // Order pain-order of hatchpaths relative to their pitch + const orderHatchPaths = (paths) => { + const nodeArray = []; + paths.forEach(p => nodeArray.push(p)); + + return nodeArray.sort((a, b) => + // (pitch - a.offset) - (pitch - b.offset) + Number(b.getAttribute('offset')) - Number(a.getAttribute('offset')) + ); + }; + + // Generate x-axis coordinates for the pattern paths + const generatePositions = (width, diagonal, initial, distance) => { + const offset = (diagonal - width) / 2; + const leftDistance = initial + offset; + const rightDistance = width + offset + distance; + const units = Math.round(leftDistance / distance) + 1; + let array = []; + + for (let i = initial - units * distance; i < rightDistance; i += distance) { + array.push(i); + } + + return array; + }; + + // Turn a path array into a tokenized version of it + const parsePath = (data) => { + let array = []; + let i = 0; + let len = data.length; + let last = 0; + + /* + * Last state (last) index map + * 0 => () + * 1 => (x y) + * 2 => (x) + * 3 => (y) + * 4 => (x1 y1 x2 y2 x y) + * 5 => (x2 y2 x y) + * 6 => (_ _ _ _ _ x y) + * 7 => (_) + */ + + while (i < len) { + switch (data[i].toUpperCase()) { + case 'Z': + array.push(data[i]); + i += 1; + last = 0; + break; + case 'M': + case 'L': + case 'T': + array.push(data[i], new Point(Number(data[i + 1]), Number(data[i + 2]))); + i += 3; + last = 1; + break; + case 'H': + array.push(data[i], new Point(Number(data[i + 1]), null)); + i += 2; + last = 2; + break; + case 'V': + array.push(data[i], new Point(null, Number(data[i + 1]))); + i += 2; + last = 3; + break; + case 'C': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])), + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + last = 4; + break; + case 'S': + case 'Q': + array.push( + data[i], new Point(Number(data[i + 1]), Number(data[i + 2])), + new Point(Number(data[i + 3]), Number(data[i + 4])) + ); + i += 5; + last = 5; + break; + case 'A': + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + data[i + 5], new Point(Number(data[i + 6]), Number(data[i + 7])) + ); + i += 8; + last = 6; + break; + case 'B': + array.push(data[i], data[i + 1]); + i += 2; + last = 7; + break; + default: + switch (last) { + case 1: + array.push(new Point(Number(data[i]), Number(data[i + 1]))); + i += 2; + break; + case 2: + array.push(new Point(Number(data[i]), null)); + i += 1; + break; + case 3: + array.push(new Point(null, Number(data[i]))); + i += 1; + break; + case 4: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])), + new Point(Number(data[i + 4]), Number(data[i + 5])) + ); + i += 6; + break; + case 5: + array.push( + new Point(Number(data[i]), Number(data[i + 1])), + new Point(Number(data[i + 2]), Number(data[i + 3])) + ); + i += 4; + break; + case 6: + array.push( + data[i], data[i + 1], data[i + 2], data[i + 3], data[i + 4], + new Point(Number(data[i + 5]), Number(data[i + 6])) + ); + i += 7; + break; + default: + array.push(data[i]); + i += 1; + } + } + } + + return array; + }; + + const getYDistance = (hatchpath) => { + const path = document.createElementNS(svgNS, 'path'); + let d = hatchpath.getAttribute('d'); + + if (d[0].toUpperCase() !== 'M') { + d = `M 0,0 ${d}`; + } + + path.setAttribute('d', d); + + return path.getPointAtLength(path.getTotalLength()).y - + path.getPointAtLength(0).y; + }; + + // Point class -------------------------------------- + class Point { + constructor (x, y) { + this.x = x; + this.y = y; + } + + toString () { + return `${this.x} ${this.y}`; + } + + isPoint () { + return true; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'hatch_shape_' + i; + shape.setAttribute('id', shapeId); + } + + const fill = shape.getAttribute('fill') || shape.style.fill; + const fillURL = fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const hatch = document.getElementById(fillURL[1]); + + if (hatch && hatch.nodeName === 'hatch') { + const href = hatch.getAttributeNS(xlinkNS, 'href'); + + if (href !== null && href !== '') { + setReference(hatch, href); + } + + // Degenerate hatch, with no hatchpath children + if (hatch.children.length === 0) { + return; + } + + const bbox = shape.getBBox(); + const hatchDiag = Math.ceil(Math.sqrt( + bbox.width * bbox.width + bbox.height * bbox.height + )); + + // Hatch variables + const units = hatch.getAttribute('hatchUnits') || unitObjectBoundingBox; + const contentUnits = hatch.getAttribute('hatchContentUnits') || unitUserSpace; + const rotate = Number(hatch.getAttribute('rotate')) || 0; + const transform = hatch.getAttribute('transform') || + hatch.getAttribute('hatchTransform') || ''; + const hatchpaths = orderHatchPaths(hatch.querySelectorAll('hatchpath,hatchPath')); + const x = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('x')) * bbox.width) || 0 + : Number(hatch.getAttribute('x')) || 0; + const y = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('y')) * bbox.width) || 0 + : Number(hatch.getAttribute('y')) || 0; + let pitch = units === unitObjectBoundingBox + ? (Number(hatch.getAttribute('pitch')) * bbox.width) || 0 + : Number(hatch.getAttribute('pitch')) || 0; + + if (contentUnits === unitObjectBoundingBox && bbox.height) { + pitch /= bbox.height; + } + + // A negative value is an error. + // A value of zero disables rendering of the element + if (pitch <= 0) { + console.error('Non-positive pitch'); + return; + } + + // Pattern variables + const pattern = document.createElementNS(svgNS, 'pattern'); + const patternId = `${fillURL[1]}_pattern`; + let patternWidth = bbox.width - bbox.width % pitch; + let patternHeight = 0; + + const xPositions = generatePositions(patternWidth, hatchDiag, x, pitch); + + hatchpaths.forEach(hatchpath => { + let offset = Number(hatchpath.getAttribute('offset')) || 0; + offset = offset > pitch ? (offset % pitch) : offset; + const currentXPositions = xPositions.map(p => p + offset); + + const path = document.createElementNS(svgNS, 'path'); + let d = ''; + + for (let j = 0; j < hatchpath.attributes.length; ++j) { + const attr = hatchpath.attributes.item(j); + if (attr.name !== 'd') { + path.setAttribute(attr.name, attr.value); + } + } + + if (hatchpath.getAttribute('d') === null) { + d += currentXPositions.reduce( + (acc, xPos) => `${acc}M ${xPos} ${y} V ${hatchDiag} `, '' + ); + patternHeight = hatchDiag; + } else { + const hatchData = hatchpath.getAttribute('d'); + const data = parsePath( + hatchData.match(/([+-]?(\d+(\.\d+)?))|[MmZzLlHhVvCcSsQqTtAaBb]/g) + ); + const len = data.length; + const startsWithM = data[0] === 'M'; + const relative = data[0].toLowerCase() === data[0]; + const point = new Point(0, 0); + let yOffset = getYDistance(hatchpath); + + if (data[len - 1].y !== undefined && yOffset < data[len - 1].y) { + yOffset = data[len - 1].y; + } + + // The offset must be positive + if (yOffset <= 0) { + console.error('y offset is non-positive'); + return; + } + patternHeight = bbox.height - bbox.height % yOffset; + + const currentYPositions = generatePositions( + patternHeight, hatchDiag, y, yOffset + ); + + currentXPositions.forEach(xPos => { + point.x = xPos; + + if (!startsWithM && !relative) { + d += `M ${xPos} 0`; + } + + currentYPositions.forEach(yPos => { + point.y = yPos; + + if (relative) { + // Path is relative, set the first point in each path render + d += `M ${xPos} ${yPos} ${hatchData}`; + } else { + // Path is absolute, translate every point + d += data.map(e => e.isPoint && e.isPoint() ? e.add(point) : e) + .map(e => e.isPoint && e.isPoint() ? e.toString() : e) + .reduce((acc, e) => `${acc} ${e}`, ''); + } + }); + }); + + // The hatchpaths are infinite, so they have no fill + path.style.fill = 'none'; + } + + path.setAttribute('d', d); + pattern.appendChild(path); + }); + + setAttributes(pattern, { + 'id': patternId, + 'patternUnits': unitUserSpace, + 'patternContentUnits': contentUnits, + 'width': patternWidth, + 'height': patternHeight, + 'x': bbox.x, + 'y': bbox.y, + 'patternTransform': `rotate(${rotate} ${0} ${0}) ${transform}` + }); + hatch.parentElement.insertBefore(pattern, hatch); + + shape.style.fill = `url(#${patternId})`; + shape.setAttribute('fill', `url(#${patternId})`); + } + } + }); +})(); diff --git a/src/extension/internal/polyfill/hatch_compressed.include b/src/extension/internal/polyfill/hatch_compressed.include new file mode 100644 index 0000000..cdd893f --- /dev/null +++ b/src/extension/internal/polyfill/hatch_compressed.include @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: CC0 +R"=====( +!function(){const t="http://www.w3.org/2000/svg",e=(t,e,r,n)=>{const u=(e-t)/2,i=r+u,s=t+u+n;let h=[];for(let t=r-(Math.round(i/n)+1)*n;t<s;t+=n)h.push(t);return h};class r{constructor(t,e){this.x=t,this.y=e}toString(){return`${this.x} ${this.y}`}isPoint(){return!0}clone(){return new r(this.x,this.y)}add(t){return new r(this.x+t.x,this.y+t.y)}distSquared(t){let e=this.x-t.x,r=this.y-t.y;return e*e+r*r}}document.querySelectorAll("rect,circle,ellipse,path,text").forEach((n,u)=>{let i=n.getAttribute("id");i||(i="hatch_shape_"+u,n.setAttribute("id",i));const s=(n.getAttribute("fill")||n.style.fill).match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(s&&s[1]){const u=document.getElementById(s[1]);if(u&&"hatch"===u.nodeName){const i=u.getAttributeNS("http://www.w3.org/1999/xlink","href");if(null!==i&&""!==i&&((t,e)=>{const r=["x","y","pitch","rotate","hatchUnits","hatchContentUnits","transform"],n=document.getElementById(e.slice(1));n&&"hatch"===n.nodeName&&(r.forEach(e=>{let r=n.getAttribute(e);null===t.getAttribute(e)&&null!==r&&t.setAttribute(e,r)}),0===t.children.length&&Array.from(n.children).forEach(e=>{t.appendChild(e.cloneNode(!0))}))})(u,i),0===u.children.length)return;const h=n.getBBox(),o=Math.ceil(Math.sqrt(h.width*h.width+h.height*h.height)),a=u.getAttribute("hatchUnits")||"objectBoundingBox",c=u.getAttribute("hatchContentUnits")||"userSpaceOnUse",b=Number(u.getAttribute("rotate"))||0,l=u.getAttribute("transform")||u.getAttribute("hatchTransform")||"",m=(t=>{const e=[];return t.forEach(t=>e.push(t)),e.sort((t,e)=>Number(e.getAttribute("offset"))-Number(t.getAttribute("offset")))})(u.querySelectorAll("hatchpath,hatchPath")),d="objectBoundingBox"===a?Number(u.getAttribute("x"))*h.width||0:Number(u.getAttribute("x"))||0,g="objectBoundingBox"===a?Number(u.getAttribute("y"))*h.width||0:Number(u.getAttribute("y"))||0;let p="objectBoundingBox"===a?Number(u.getAttribute("pitch"))*h.width||0:Number(u.getAttribute("pitch"))||0;if("objectBoundingBox"===c&&h.height&&(p/=h.height),p<=0)return void console.error("Non-positive pitch");const N=document.createElementNS(t,"pattern"),f=`${s[1]}_pattern`;let w=h.width-h.width%p,A=0;const y=e(w,o,d,p);m.forEach(n=>{let u=Number(n.getAttribute("offset"))||0;u=u>p?u%p:u;const i=y.map(t=>t+u),s=document.createElementNS(t,"path");let a="";for(let t=0;t<n.attributes.length;++t){const e=n.attributes.item(t);"d"!==e.name&&s.setAttribute(e.name,e.value)}if(null===n.getAttribute("d"))a+=i.reduce((t,e)=>`${t}M ${e} ${g} V ${o} `,""),A=o;else{const u=n.getAttribute("d"),c=(t=>{let e=[],n=0,u=t.length,i=0;for(;n<u;)switch(t[n].toUpperCase()){case"Z":e.push(t[n]),n+=1,i=0;break;case"M":case"L":case"T":e.push(t[n],new r(Number(t[n+1]),Number(t[n+2]))),n+=3,i=1;break;case"H":e.push(t[n],new r(Number(t[n+1]),null)),n+=2,i=2;break;case"V":e.push(t[n],new r(null,Number(t[n+1]))),n+=2,i=3;break;case"C":e.push(t[n],new r(Number(t[n+1]),Number(t[n+2])),new r(Number(t[n+3]),Number(t[n+4])),new r(Number(t[n+5]),Number(t[n+6]))),n+=7,i=4;break;case"S":case"Q":e.push(t[n],new r(Number(t[n+1]),Number(t[n+2])),new r(Number(t[n+3]),Number(t[n+4]))),n+=5,i=5;break;case"A":e.push(t[n],t[n+1],t[n+2],t[n+3],t[n+4],t[n+5],new r(Number(t[n+6]),Number(t[n+7]))),n+=8,i=6;break;case"B":e.push(t[n],t[n+1]),n+=2,i=7;break;default:switch(i){case 1:e.push(new r(Number(t[n]),Number(t[n+1]))),n+=2;break;case 2:e.push(new r(Number(t[n]),null)),n+=1;break;case 3:e.push(new r(null,Number(t[n]))),n+=1;break;case 4:e.push(new r(Number(t[n]),Number(t[n+1])),new r(Number(t[n+2]),Number(t[n+3])),new r(Number(t[n+4]),Number(t[n+5]))),n+=6;break;case 5:e.push(new r(Number(t[n]),Number(t[n+1])),new r(Number(t[n+2]),Number(t[n+3]))),n+=4;break;case 6:e.push(t[n],t[n+1],t[n+2],t[n+3],t[n+4],new r(Number(t[n+5]),Number(t[n+6]))),n+=7;break;default:e.push(t[n]),n+=1}}return e})(u.match(/([+-]?(\d+(\.\d+)?))|[MmZzLlHhVvCcSsQqTtAaBb]/g)),b=c.length,l="M"===c[0],m=c[0].toLowerCase()===c[0],d=new r(0,0);let p=(e=>{const r=document.createElementNS(t,"path");let n=e.getAttribute("d");return"M"!==n[0].toUpperCase()&&(n=`M 0,0 ${n}`),r.setAttribute("d",n),r.getPointAtLength(r.getTotalLength()).y-r.getPointAtLength(0).y})(n);if(void 0!==c[b-1].y&&p<c[b-1].y&&(p=c[b-1].y),p<=0)return void console.error("y offset is non-positive");A=h.height-h.height%p;const N=e(A,o,g,p);i.forEach(t=>{d.x=t,l||m||(a+=`M ${t} 0`),N.forEach(e=>{d.y=e,a+=m?`M ${t} ${e} ${u}`:c.map(t=>t.isPoint&&t.isPoint()?t.add(d):t).map(t=>t.isPoint&&t.isPoint()?t.toString():t).reduce((t,e)=>`${t} ${e}`,"")})}),s.style.fill="none"}s.setAttribute("d",a),N.appendChild(s)}),((t,e)=>{for(let r in e)t.setAttribute(r,e[r])})(N,{id:f,patternUnits:"userSpaceOnUse",patternContentUnits:c,width:w,height:A,x:h.x,y:h.y,patternTransform:`rotate(${b} 0 0) ${l}`}),u.parentElement.insertBefore(N,u),n.style.fill=`url(#${f})`,n.setAttribute("fill",`url(#${f})`)}}})}(); +)=====" diff --git a/src/extension/internal/polyfill/hatch_tests/hatch.svg b/src/extension/internal/polyfill/hatch_tests/hatch.svg new file mode 100644 index 0000000..7e2f8de --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch.svg @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="360"> + <defs> + <hatch + pitch="15" + hatchUnits="userSpaceOnUse" + id="simple6"> + <hatchPath + d="m 0,0 5,10 5,-5" + offset="5" + stroke="#a080ff" /> + </hatch> + <hatch + id="transform2" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" /> + </hatch> + <hatch + id="transform4" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="45"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" /> + </hatch> + <hatch + id="transform8" + hatchUnits="userSpaceOnUse" + pitch="15" + x="-5" + y="-10" + rotate="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" /> + </hatch> + <hatch + id="transform9" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="30" + x="-5" + y="-10" + hatchTransform="matrix(0.96592583,-0.25881905,0.25881905,0.96592583,-8.4757068,43.273395)"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" /> + </hatch> + </defs> + <rect fill="url(#simple6)" stroke="black" stroke-width="2" x="25" y="25" width="150" height="150"/> + <rect fill="url(#transform2) #ff0000;" stroke="black" stroke-width="1" width="115" height="115" x="25" y="200" /> + + <script type="text/javascript" xlink:href="../hatch.js"></script> +</svg> diff --git a/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg new file mode 100644 index 0000000..9c45296 --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch01_with_js.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:xlink="http://www.w3.org/1999/xlink" + inkscape:version="1.0alpha2 (5c5a8378bf, 2019-06-27, custom)" + sodipodi:docname="hatch01_with_js.svg" + id="svg24" + version="1.1" + height="400" + width="400"> + <metadata + id="metadata28"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + inkscape:current-layer="svg24" + inkscape:window-maximized="0" + inkscape:window-y="23" + inkscape:window-x="26" + inkscape:cy="200" + inkscape:cx="200" + inkscape:zoom="0.6025" + showgrid="false" + id="namedview26" + inkscape:window-height="480" + inkscape:window-width="686" + inkscape:pageshadow="2" + inkscape:pageopacity="0" + guidetolerance="10" + gridtolerance="10" + objecttolerance="10" + borderopacity="1" + bordercolor="#666666" + pagecolor="#ffffff" /> + <defs + id="defs14"> + <hatch + rotate="135" + pitch="5" + hatchContentUnits="userSpaceOnUse" + hatchUnits="userSpaceOnUse" + id="hatch1"> + <hatchpath + id="hatchpath2" + stroke-width="2" + stroke="#a080ff" /> + </hatch> + <hatch + rotate="135" + pitch="0.05" + hatchContentUnits="userSpaceOnUse" + hatchUnits="objectBoundingBox" + id="hatch2"> + <hatchpath + id="hatchpath5" + stroke-width="2" + stroke="#a080ff" /> + </hatch> + <hatch + rotate="135" + pitch="5" + hatchContentUnits="objectBoundingBox" + hatchUnits="userSpaceOnUse" + id="hatch3"> + <hatchpath + id="hatchpath8" + stroke-width="0.02" + stroke="#a080ff" /> + </hatch> + <hatch + rotate="135" + pitch="0.05" + hatchContentUnits="objectBoundingBox" + hatchUnits="objectBoundingBox" + id="hatch4"> + <hatchpath + id="hatchpath11" + stroke-width="0.02" + stroke="#a080ff" /> + </hatch> + </defs> + <rect + id="rect16" + height="100" + width="100" + y="50" + x="50" + stroke-width="2" + stroke="black" + fill="url(#hatch1)" /> + <rect + id="rect18" + height="100" + width="100" + y="50" + x="250" + stroke-width="2" + stroke="black" + fill="url(#hatch2)" /> + <rect + id="rect20" + height="100" + width="100" + y="250" + x="50" + stroke-width="2" + stroke="black" + fill="url(#hatch3)" /> + <rect + id="rect22" + height="100" + width="100" + y="250" + x="250" + stroke-width="2" + stroke="black" + fill="url(#hatch4)" /> + <script type="text/javascript" xlink:href="../hatch.js"></script> +</svg> diff --git a/src/extension/internal/polyfill/hatch_tests/hatch_test.svg b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg new file mode 100644 index 0000000..fd45a8d --- /dev/null +++ b/src/extension/internal/polyfill/hatch_tests/hatch_test.svg @@ -0,0 +1,11730 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="2600" + height="1700" + id="svg3780" + version="1.1" + inkscape:version="1.0alpha2 (2d4d49aaa0, 2019-06-18, custom)" + sodipodi:docname="hatch_test (copy).svg"> + <defs + id="defs3782"> + <hatch + id="simple1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + stroke-width="2" + id="hatchPath2" /> + </hatch> + <hatch + id="simple2" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="15" + id="hatchPath5" /> + </hatch> + <hatch + id="simple3" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="M 0,0 5,10" + id="hatchPath8" /> + </hatch> + <hatch + id="simple4" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="L 0,0 5,10" + id="hatchPath11" /> + </hatch> + <hatch + id="simple5" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="M 0,0 5,10 10,5" + id="hatchPath14" /> + </hatch> + <hatch + id="simple6" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="m 0,0 5,10 5,-5" + id="hatchPath17" /> + </hatch> + <hatch + id="simple7" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="M 0,0 5,10 M 5,20" + id="hatchPath20" /> + </hatch> + <hatch + id="transform1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="L 0,0 5,10 0,20" + id="hatchPath23" /> + </hatch> + <hatch + id="transform2" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" + id="hatchPath26" /> + </hatch> + <hatch + id="transform4" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="45"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" + id="hatchPath29" /> + </hatch> + <hatch + id="transform7" + hatchUnits="userSpaceOnUse" + pitch="15" + x="-5" + y="-10"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" + id="hatchPath32" /> + </hatch> + <hatch + id="transform8" + hatchUnits="userSpaceOnUse" + pitch="15" + x="-5" + y="-10" + rotate="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" + id="hatchPath35" /> + </hatch> + <hatch + id="transform9" + hatchUnits="userSpaceOnUse" + pitch="15" + rotate="30" + x="-5" + y="-10" + hatchTransform="matrix(0.96592583,-0.25881905,0.25881905,0.96592583,-8.4757068,43.273395)"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,20" + id="hatchPath38" /> + </hatch> + <hatch + id="multiple1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + id="hatchPath41" /> + <hatchPath + stroke="#32ff3f" + offset="10" + id="hatchPath43" /> + </hatch> + <hatch + id="multiple2" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + id="hatchPath46" /> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10" + id="hatchPath48" /> + </hatch> + <hatch + id="multiple3" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="L 0,0 5,17" + id="hatchPath51" /> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10" + id="hatchPath53" /> + </hatch> + <hatch + id="stroke1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + stroke-width="5" + stroke-dasharray="10 4 2 4" + id="hatchPath56" /> + </hatch> + <hatch + id="overflow1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + d="L 0,0 5,5 -5,15, 0,20" + id="hatchPath59" /> + </hatch> + <hatch + id="overflow2" + style="overflow:hidden" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="0" + d="L 0,0 5,5 -5,15, 0,20" + id="hatchPath62" /> + </hatch> + <hatch + id="overflow3" + style="overflow:visible" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="0" + d="L 0,0 5,5 -5,15, 0,20" + id="hatchPath65" /> + </hatch> + <hatch + id="overflow4" + style="overflow:visible" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#32ff3f" + offset="5" + id="hatchPath68" /> + <hatchPath + stroke="#ff0000" + offset="20" + id="hatchPath70" /> + </hatch> + <hatch + id="ref1" + hatchUnits="userSpaceOnUse" + pitch="15"> + <hatchPath + stroke="#a080ff" + offset="5" + id="hatchPath73" /> + </hatch> + <hatch + id="ref2" + xlink:href="#ref1" /> + <hatch + id="ref3" + xlink:href="#ref1" + pitch="45" /> + <hatch + id="degenerate1" + pitch="45" /> + <hatch + id="degenerate2" + xlink:href="#nonexisting" + pitch="45" /> + <hatch + id="degenerate3" + pitch="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 0,15" + id="hatchPath80" /> + </hatch> + <hatch + id="degenerate4" + pitch="30"> + <hatchPath + stroke="#a080ff" + offset="10" + d="L 0,0 5,10 -5,15" + id="hatchPath83" /> + </hatch> + <linearGradient + inkscape:collect="always" + id="linearGradient11910"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop11912" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop11914" /> + </linearGradient> + <linearGradient + id="linearGradient10590"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop10592" /> + <stop + style="stop-color:#c9c9c9;stop-opacity:0;" + offset="1" + id="stop10594" /> + </linearGradient> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4338"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4340" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4342"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4344" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4346"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4348" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4350"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4352" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4354"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4356" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4358"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4360" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4362"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4364" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath4366"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4368" + width="115" + height="115" + x="175" + y="42.362183" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7203"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7205" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7207"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7209" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7211"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7213" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7215"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7217" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7219"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7221" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7223"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7225" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7227"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7229" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7231"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7233" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7235"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7237" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7239"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7241" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7243"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7245" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7247"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7249" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7251"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7253" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7255"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7257" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7259"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7261" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7263"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7265" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7267"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7269" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7271"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7273" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7275"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7277" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7279"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7281" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7283"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7285" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7287"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7289" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7291"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7293" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7295"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7297" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7299"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7301" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7303"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7305" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7307"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7309" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7311"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7313" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7315"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7317" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7319"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7321" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7323"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7325" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7327"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7329" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7331"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7333" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7335"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7337" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7339"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7341" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7343"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7345" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7347"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7349" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7351"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7353" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7355"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7357" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7359"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7361" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7363"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7365" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7367"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7369" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7371"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7373" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7375"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7377" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7379"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7381" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7383"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7385" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7387"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7389" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7391"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7393" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7395"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7397" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7399"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7401" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7403"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7405" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7407"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7409" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7411"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7413" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7415"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7417" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7419"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7421" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7423"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7425" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7427"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7429" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7431"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7433" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7435"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7437" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7439"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7441" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7443"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7445" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7447"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7449" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7451"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7453" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7455"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7457" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7459"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7461" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7463"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7465" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7467"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7469" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7471"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7473" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7475"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7477" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7479"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7481" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7483"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7485" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7487"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7489" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7491"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7493" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7495"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7497" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7499"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7501" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7503"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7505" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7507"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7509" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7511"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7513" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7515"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7517" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7519"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7521" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7523"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7525" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7527"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7529" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7531"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7533" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7535"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7537" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7539"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7541" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7543"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7545" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7547"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7549" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7551"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7553" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7555"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7557" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7559"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7561" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7563"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7565" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7567"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7569" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7571"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7573" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7575"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7577" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7579"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7581" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7583"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7585" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7587"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7589" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7591"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7593" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7595"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7597" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7599"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7601" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7603"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7605" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7607"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7609" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7611"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7613" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7615"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7617" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7619"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7621" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7623"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7625" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7627"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7629" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7631"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7633" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7635"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7637" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7639"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7641" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7643"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7645" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7647"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7649" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7651"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7653" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7655"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7657" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7659"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7661" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7663"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7665" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7667"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7669" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7671"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7673" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7675"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7677" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7679"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7681" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7683"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7685" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7687"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7689" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7691"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7693" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7695"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7697" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7699"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7701" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7703"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7705" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7707"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7709" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7711"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7713" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7715"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7717" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7719"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7721" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7723"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7725" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7727"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7729" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7731"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7733" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7735"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7737" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7739"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7741" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7743"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7745" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7747"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7749" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7751"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7753" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7755"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7757" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7759"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7761" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7763"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7765" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7767"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7769" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7771"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7773" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7775"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7777" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7779"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7781" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7783"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7785" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7787"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7789" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7791"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7793" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7795"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7797" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7799"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7801" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7803"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7805" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7807"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7809" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7811"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7813" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7815"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7817" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7819"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7821" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7823"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7825" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7827"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7829" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7831"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7833" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7835"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7837" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7839"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7841" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7843"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7845" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7847"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7849" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7851"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7853" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7855"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7857" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7859"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7861" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7863"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7865" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7867"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7869" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7871"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7873" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7875"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7877" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7879"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7881" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7883"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7885" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7887"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7889" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7891"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7893" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7895"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7897" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7899"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7901" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7903"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7905" + width="115" + height="115" + x="170.5" + y="327.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7948"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7950" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7952"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7954" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7956"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7958" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7960"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7962" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7964"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7966" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7968"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7970" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7972"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7974" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath7976"> + <rect + y="452.86218" + x="170.5" + height="115" + width="115" + id="rect7978" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8370"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8372" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8374"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8376" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8378"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8380" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8382"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8384" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8386"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8388" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8390"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8392" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8394"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8396" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8398"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8400" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8402"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8404" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8406"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8408" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8410"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8412" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8414"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8416" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8418"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8420" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8422"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8424" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8426"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8428" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8430"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8432" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8434"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8436" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8438"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8440" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8442"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8444" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8446"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8448" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8450"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8452" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8454"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8456" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8458"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8460" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8462"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8464" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8466"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8468" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8470"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8472" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8474"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8476" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8478"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8480" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8482"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8484" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8486"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8488" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8490"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8492" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8494"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8496" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8498"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8500" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8502"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8504" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8506"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8508" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8510"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8512" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8514"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8516" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8518"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8520" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8522"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8524" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8526"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8528" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8530"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8532" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8534"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8536" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8538"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8540" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8542"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8544" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8546"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8548" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8550"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8552" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8554"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8556" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8558"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8560" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8562"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8564" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8566"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8568" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8570"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8572" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8574"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8576" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8578"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8580" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8582"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8584" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8586"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8588" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8590"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8592" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8594"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8596" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8598"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8600" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8602"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8604" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8606"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8608" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8610"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8612" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8614"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8616" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8618"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8620" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8622"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8624" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8626"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8628" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8630"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8632" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8634"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8636" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8638"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8640" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8642"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8644" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8646"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8648" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8650"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8652" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8654"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8656" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8658"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8660" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8662"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8664" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8666"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8668" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8670"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8672" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8674"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8676" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8678"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8680" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8682"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8684" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8686"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8688" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8690"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8692" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8694"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8696" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8698"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8700" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8702"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8704" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8706"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8708" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8710"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8712" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8714"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8716" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8718"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8720" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8722"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8724" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8726"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8728" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8730"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8732" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8734"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8736" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8738"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8740" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8742"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8744" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8746"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8748" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8750"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8752" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8754"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8756" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8758"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8760" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8762"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8764" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8766"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8768" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8770"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8772" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8774"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8776" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8778"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8780" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8782"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8784" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8786"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8788" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8790"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8792" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8794"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8796" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8798"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8800" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8802"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8804" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8806"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8808" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8810"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8812" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8814"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8816" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8818"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8820" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8822"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8824" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8826"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8828" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8830"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8832" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8834"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8836" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8838"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8840" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8842"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8844" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8846"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8848" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8850"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8852" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8854"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8856" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8858"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8860" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8862"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8864" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8866"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8868" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8870"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8872" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8874"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8876" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8878"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8880" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8882"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8884" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8886"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8888" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8890"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8892" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8894"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8896" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8898"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8900" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8902"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8904" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8906"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8908" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8910"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8912" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8914"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8916" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8918"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8920" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8922"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8924" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8926"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8928" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8930"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8932" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8934"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8936" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8938"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8940" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8942"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8944" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8946"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8948" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8950"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8952" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8954"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8956" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8958"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8960" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8962"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8964" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8966"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8968" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8970"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8972" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8974"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8976" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8978"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8980" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8982"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8984" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8986"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8988" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8990"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8992" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8994"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect8996" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath8998"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9000" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9002"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9004" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9006"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9008" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9010"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9012" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9014"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9016" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9018"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9020" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9022"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9024" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9026"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9028" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9030"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9032" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9034"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9036" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9038"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9040" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9042"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9044" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9046"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9048" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9050"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9052" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9054"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9056" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9058"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9060" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9062"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9064" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9066"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9068" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9070"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9072" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9074"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9076" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9078"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9080" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9082"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9084" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9086"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9088" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9090"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9092" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9094"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9096" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9098"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9100" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9102"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9104" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9106"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9108" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9110"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9112" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9114"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9116" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9118"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9120" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9122"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9124" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9126"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9128" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9130"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9132" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9134"> + <rect + style="fill:#000000;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9136" + width="115" + height="115" + x="170.5" + y="577.86218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9545"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9547" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9549"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9551" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9553"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9555" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9557"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9559" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9561"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9563" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9565"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9567" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9569"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9571" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath9573"> + <rect + y="62.862183" + x="355.5" + height="115" + width="115" + id="rect9575" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10402"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10404" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10406"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10408" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10410"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10412" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10414"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10416" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10418"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10420" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10422"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10424" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10426"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10428" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10430"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10432" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10434"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10436" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10438"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10440" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10442"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10444" + width="115" + height="115" + x="520" + y="267.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10476"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10478" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10480"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10482" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10484"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10486" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10488"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10490" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10492"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10494" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10496"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10498" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10500"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10502" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10504"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10506" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10508"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10510" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10512"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10514" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10516"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10518" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10520"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10522" + width="115" + height="115" + x="520" + y="462.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10554"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10556" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10558"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10560" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10562"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10564" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10566"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10568" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10570"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10572" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10574"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10576" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10578"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10580" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10582"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10584" + width="115" + height="115" + x="505" + y="577.36218" /> + </clipPath> + <pattern + patternUnits="userSpaceOnUse" + width="71" + height="71" + patternTransform="translate(-160.5,-138.125)" + id="pattern10608"> + <rect + y="0.48718262" + x="0.5" + height="70" + width="70" + id="rect10606" + style="opacity:0.94899998;fill:#000000;fill-opacity:1;stroke:#ffff00;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-opacity:0.94117647;stroke-dasharray:none;stroke-dashoffset:0" /> + </pattern> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10625"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10627" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10629"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10631" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10633"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10635" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10637"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10639" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10641"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10643" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10645"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10647" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10649"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10651" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10653"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10655" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10657"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10659" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10661"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10663" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10665"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10667" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10669"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10671" + width="115" + height="115" + x="530" + y="702.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10757"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10759" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10761"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10763" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10765"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10767" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10769"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10771" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10773"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10775" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10777"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10779" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10781"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10783" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10785"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10787" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10789"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10791" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10793"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10795" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10797"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10799" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10801"> + <rect + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10803" + width="115" + height="115" + x="535" + y="762.36218" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10877"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10879" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10881"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10883" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10885"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10887" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10889"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10891" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10893"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10895" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10897"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10899" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10901"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10903" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10905"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10907" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10909"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10911" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10913"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10915" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10917"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10919" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10921"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10923" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10925"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10927" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10929"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10931" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath10933"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect10935" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11035"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11037" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11039"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11041" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11043"> + <rect + y="212.36218" + x="915" + height="115" + width="115" + id="rect11045" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11047"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11049" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11051"> + <rect + y="212.36218" + x="930" + height="115" + width="115" + id="rect11053" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11055"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11057" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11059"> + <rect + y="212.36218" + x="945" + height="115" + width="115" + id="rect11061" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11063"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11065" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11067"> + <rect + y="212.36218" + x="960" + height="115" + width="115" + id="rect11069" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11071"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11073" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11075"> + <rect + y="212.36218" + x="975" + height="115" + width="115" + id="rect11077" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11079"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11081" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11083"> + <rect + y="212.36218" + x="990" + height="115" + width="115" + id="rect11085" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11087"> + <rect + y="342.36218" + x="1005" + height="115" + width="115" + id="rect11089" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11091"> + <rect + y="212.36218" + x="1005" + height="115" + width="115" + id="rect11093" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11155"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11157" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11159"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11161" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11163"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11165" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11167"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11169" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11171"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11173" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11175"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11177" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11179"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11181" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11183"> + <rect + y="217.36218" + x="1485" + height="115" + width="115" + id="rect11185" + style="fill:#000000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11488"> + <rect + y="168.9841" + x="-17.026188" + height="95.052338" + width="95.052338" + id="rect11490" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11492"> + <rect + y="177.75092" + x="-8.259387" + height="95.052338" + width="95.052338" + id="rect11494" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11496"> + <rect + y="186.51772" + x="0.50741416" + height="95.052338" + width="95.052338" + id="rect11498" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11500"> + <rect + y="195.28452" + x="9.2742147" + height="95.052338" + width="95.052338" + id="rect11502" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11504"> + <rect + y="204.05132" + x="18.041016" + height="95.052338" + width="95.052338" + id="rect11506" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11508"> + <rect + y="212.81812" + x="26.807816" + height="95.052338" + width="95.052338" + id="rect11510" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11512"> + <rect + y="221.58492" + x="35.574615" + height="95.052338" + width="95.052338" + id="rect11514" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11516"> + <rect + y="221.58492" + x="35.574615" + height="95.052338" + width="95.052338" + id="rect11518" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11520"> + <rect + y="221.58492" + x="35.574615" + height="95.052338" + width="95.052338" + id="rect11522" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11524"> + <rect + y="221.58492" + x="35.574615" + height="95.052338" + width="95.052338" + id="rect11526" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath11528"> + <rect + y="221.58492" + x="35.574615" + height="95.052338" + width="95.052338" + id="rect11530" + style="fill:#000000;stroke:#000000;stroke-width:1.06241;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + transform="matrix(0.85550008,-0.51780267,0.85550008,0.51780267,0,0)" /> + </clipPath> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient11910" + id="linearGradient11916" + x1="179.5" + y1="-217.63782" + x2="480.5" + y2="-217.63782" + gradientUnits="userSpaceOnUse" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="0.3245239" + inkscape:cx="1145.4883" + inkscape:cy="1129.6405" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:snap-bbox="true" + inkscape:window-width="1920" + inkscape:window-height="1014" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" + inkscape:object-paths="true" + inkscape:object-nodes="true"> + <inkscape:grid + type="xygrid" + id="grid3794" /> + </sodipodi:namedview> + <metadata + id="metadata3785"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="27.142857" + y="42.362183" + id="text3788"><tspan + sodipodi:role="line" + id="tspan3790" + x="27.142857" + y="42.362183" + style="font-size:24px;line-height:1.25;font-family:sans-serif">Simple hatches</tspan></text> + <g + id="g4372" + transform="translate(384.5,5.5000026)"> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path3798" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path4310" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path4314" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path4318" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path4322" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path4326" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path4330" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path4334" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="62.362183" + x="181" + height="115" + width="115" + id="rect4370" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <rect + style="fill:url(#simple1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4383" + width="115" + height="115" + x="435.5" + y="67.862183" /> + <g + transform="translate(384.5,135.5)" + id="g4385"> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 180,42.362183 V 192.36218" + id="path4387" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4366)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path4389" + d="M 195,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4362)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 210,42.362183 V 192.36218" + id="path4391" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4358)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path4393" + d="M 225,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4354)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path4395" + d="M 240,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4350)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 255,42.362183 V 192.36218" + id="path4397" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4346)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path4399" + d="M 270,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4342)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path4401" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4338)" + transform="translate(6,20)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect4403" + width="115" + height="115" + x="181" + y="62.362183" /> + </g> + <rect + y="197.86218" + x="435.5" + height="115" + width="115" + id="rect4405" + style="fill:url(#simple2) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <g + id="g4695" + transform="translate(384.5,135.5)"> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path4697" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path4699" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path4701" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path4703" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path4705" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path4707" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path4709" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path4711" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="62.362183" + x="181" + height="115" + width="115" + id="rect4713" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,327.36218 5,10" + id="path5443" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7903)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,337.36218 5,10" + id="path5445" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7899)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,347.36218 5,10" + id="path5447" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7895)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,357.36218 5,10" + id="path5449" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7891)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,367.36218 5,10" + id="path5451" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7887)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5453" + d="m 175,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7883)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5455" + d="m 175,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7879)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5457" + d="m 175,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7875)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5459" + d="m 175,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7871)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5461" + d="m 175,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7867)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,377.36218 5,10" + id="path5463" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7863)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5465" + d="m 175,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7859)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5467" + d="m 175,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7855)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5469" + d="m 175,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7851)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5471" + d="m 175,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7847)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5473" + d="m 175,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7843)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,387.36218 5,10" + id="path5475" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7839)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,397.36218 5,10" + id="path5477" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7835)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,407.36218 5,10" + id="path5479" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7831)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,417.36218 5,10" + id="path5481" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7827)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,427.36218 5,10" + id="path5483" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7823)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5485" + d="m 175,437.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7819)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5487" + d="m 190,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7815)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5489" + d="m 190,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7811)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5491" + d="m 190,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7807)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5493" + d="m 190,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7803)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5495" + d="m 190,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7799)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,327.36218 5,10" + id="path5497" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7795)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,337.36218 5,10" + id="path5499" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7791)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,347.36218 5,10" + id="path5501" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7787)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,357.36218 5,10" + id="path5503" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7783)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,367.36218 5,10" + id="path5505" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7779)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5507" + d="m 190,377.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7775)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,387.36218 5,10" + id="path5509" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7771)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,397.36218 5,10" + id="path5511" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7767)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,407.36218 5,10" + id="path5513" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7763)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,417.36218 5,10" + id="path5515" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7759)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,427.36218 5,10" + id="path5517" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7755)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5519" + d="m 190,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7751)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5521" + d="m 190,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7747)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5523" + d="m 190,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7743)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5525" + d="m 190,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7739)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5527" + d="m 190,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7735)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,437.36218 5,10" + id="path5529" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7731)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5531" + d="m 205,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7727)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5533" + d="m 205,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7723)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5535" + d="m 205,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7719)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5537" + d="m 205,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7715)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5539" + d="m 205,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7711)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,327.36218 5,10" + id="path5541" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7707)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,337.36218 5,10" + id="path5543" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7703)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,347.36218 5,10" + id="path5545" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7699)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,357.36218 5,10" + id="path5547" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7695)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,367.36218 5,10" + id="path5549" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7691)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5551" + d="m 205,377.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7687)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,387.36218 5,10" + id="path5553" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7683)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,397.36218 5,10" + id="path5555" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7679)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,407.36218 5,10" + id="path5557" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7675)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,417.36218 5,10" + id="path5559" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7671)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,427.36218 5,10" + id="path5561" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7667)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5563" + d="m 205,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7663)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5565" + d="m 205,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7659)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5567" + d="m 205,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7655)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5569" + d="m 205,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7651)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5571" + d="m 205,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7647)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,437.36218 5,10" + id="path5573" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7643)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,327.36218 5,10" + id="path5575" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7639)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,337.36218 5,10" + id="path5577" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7635)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,347.36218 5,10" + id="path5579" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7631)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,357.36218 5,10" + id="path5581" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7627)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,367.36218 5,10" + id="path5583" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7623)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5585" + d="m 220,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7619)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5587" + d="m 220,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7615)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5589" + d="m 220,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7611)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5591" + d="m 220,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7607)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5593" + d="m 220,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7603)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,377.36218 5,10" + id="path5595" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7599)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5597" + d="m 220,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7595)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5599" + d="m 220,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7591)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5601" + d="m 220,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7587)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5603" + d="m 220,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7583)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5605" + d="m 220,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7579)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,387.36218 5,10" + id="path5607" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7575)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,397.36218 5,10" + id="path5609" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7571)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,407.36218 5,10" + id="path5611" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7567)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,417.36218 5,10" + id="path5613" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7563)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,427.36218 5,10" + id="path5615" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7559)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5617" + d="m 220,437.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7555)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5619" + d="m 235,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7551)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5621" + d="m 235,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7547)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5623" + d="m 235,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7543)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5625" + d="m 235,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7539)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5627" + d="m 235,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7535)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,327.36218 5,10" + id="path5629" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7531)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,337.36218 5,10" + id="path5631" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7527)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,347.36218 5,10" + id="path5633" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7523)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,357.36218 5,10" + id="path5635" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7519)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,367.36218 5,10" + id="path5637" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7515)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5639" + d="m 235,377.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7511)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,387.36218 5,10" + id="path5641" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7507)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,397.36218 5,10" + id="path5643" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7503)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,407.36218 5,10" + id="path5645" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7499)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,417.36218 5,10" + id="path5647" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7495)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,427.36218 5,10" + id="path5649" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7491)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5651" + d="m 235,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7487)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5653" + d="m 235,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7483)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5655" + d="m 235,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7479)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5657" + d="m 235,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7475)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5659" + d="m 235,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7471)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,437.36218 5,10" + id="path5661" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7467)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,327.36218 5,10" + id="path5663" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7463)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,337.36218 5,10" + id="path5665" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7459)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,347.36218 5,10" + id="path5667" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7455)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,357.36218 5,10" + id="path5669" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7451)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,367.36218 5,10" + id="path5671" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7447)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5673" + d="m 250,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7443)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5675" + d="m 250,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7439)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5677" + d="m 250,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7435)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5679" + d="m 250,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7431)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5681" + d="m 250,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7427)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,377.36218 5,10" + id="path5683" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7423)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5685" + d="m 250,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7419)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5687" + d="m 250,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7415)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5689" + d="m 250,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7411)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5691" + d="m 250,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7407)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5693" + d="m 250,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7403)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,387.36218 5,10" + id="path5695" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7399)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,397.36218 5,10" + id="path5697" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7395)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,407.36218 5,10" + id="path5699" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7391)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,417.36218 5,10" + id="path5701" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7387)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,427.36218 5,10" + id="path5703" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7383)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5705" + d="m 250,437.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7379)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,327.36218 5,10" + id="path5707" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7375)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,337.36218 5,10" + id="path5709" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7371)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,347.36218 5,10" + id="path5711" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7367)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,357.36218 5,10" + id="path5713" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7363)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,367.36218 5,10" + id="path5715" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7359)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5717" + d="m 265,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7355)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5719" + d="m 265,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7351)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5721" + d="m 265,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7347)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5723" + d="m 265,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7343)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5725" + d="m 265,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7339)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,377.36218 5,10" + id="path5727" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7335)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5729" + d="m 265,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7331)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5731" + d="m 265,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7327)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5733" + d="m 265,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7323)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5735" + d="m 265,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7319)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5737" + d="m 265,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7315)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,387.36218 5,10" + id="path5739" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7311)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,397.36218 5,10" + id="path5741" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7307)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,407.36218 5,10" + id="path5743" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7303)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,417.36218 5,10" + id="path5745" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7299)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,427.36218 5,10" + id="path5747" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7295)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5749" + d="m 265,437.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7291)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5751" + d="m 280,327.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7287)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5753" + d="m 280,337.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7283)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5755" + d="m 280,347.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7279)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5757" + d="m 280,357.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7275)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5759" + d="m 280,367.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7271)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,327.36218 5,10" + id="path5761" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7267)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,337.36218 5,10" + id="path5763" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7263)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,347.36218 5,10" + id="path5765" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7259)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,357.36218 5,10" + id="path5767" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7255)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,367.36218 5,10" + id="path5769" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7251)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5771" + d="m 280,377.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7247)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,387.36218 5,10" + id="path5773" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7243)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,397.36218 5,10" + id="path5775" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7239)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,407.36218 5,10" + id="path5777" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7235)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,417.36218 5,10" + id="path5779" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7231)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,427.36218 5,10" + id="path5781" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7227)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5783" + d="m 280,387.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7223)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5785" + d="m 280,397.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7219)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5787" + d="m 280,407.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7215)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5789" + d="m 280,417.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7211)" + transform="translate(395,2.6171874e-6)" /> + <path + inkscape:connector-curvature="0" + id="path5791" + d="m 280,427.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7207)" + transform="translate(395,2.6171874e-6)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,437.36218 5,10" + id="path5793" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7203)" + transform="translate(395,2.6171874e-6)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7907" + width="115" + height="115" + x="565.5" + y="327.86218" /> + <rect + style="fill:url(#simple3) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect7909" + width="115" + height="115" + x="435.5" + y="327.86218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path7930" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7976)" + transform="translate(395,5.0000026)" /> + <path + inkscape:connector-curvature="0" + id="path7932" + d="m 190,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7972)" + transform="translate(395,5.0000026)" /> + <path + inkscape:connector-curvature="0" + id="path7934" + d="m 205,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7968)" + transform="translate(395,5.0000026)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path7936" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7964)" + transform="translate(395,5.0000026)" /> + <path + inkscape:connector-curvature="0" + id="path7938" + d="m 235,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7960)" + transform="translate(395,5.0000026)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path7940" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7956)" + transform="translate(395,5.0000026)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path7942" + inkscape:connector-curvature="0" + clip-path="url(#clipPath7952)" + transform="translate(395,5.0000026)" /> + <path + inkscape:connector-curvature="0" + id="path7944" + d="m 280,452.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath7948)" + transform="translate(395,5.0000026)" /> + <rect + y="457.86218" + x="565.5" + height="115" + width="115" + id="rect7980" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + y="457.86218" + x="435.5" + height="115" + width="115" + id="rect7982" + style="fill:url(#simple4) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <g + id="g9142" + transform="translate(395,5.0000026)"> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9134)" + inkscape:connector-curvature="0" + id="path7984" + d="m 175,582.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9130)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,587.36218 5,10 5,-5" + id="path7986" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9126)" + inkscape:connector-curvature="0" + id="path7988" + d="m 175,592.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9122)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,597.36218 5,10 5,-5" + id="path7990" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9118)" + inkscape:connector-curvature="0" + id="path7992" + d="m 175,602.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9114)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,607.36218 5,10 5,-5" + id="path7994" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9110)" + inkscape:connector-curvature="0" + id="path7996" + d="m 175,612.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9106)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,617.36218 5,10 5,-5" + id="path7998" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9102)" + inkscape:connector-curvature="0" + id="path8000" + d="m 175,622.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9098)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,627.36218 5,10 5,-5" + id="path8002" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9094)" + inkscape:connector-curvature="0" + id="path8004" + d="m 175,632.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9090)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,637.36218 5,10 5,-5" + id="path8006" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9086)" + inkscape:connector-curvature="0" + id="path8008" + d="m 175,642.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9082)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,647.36218 5,10 5,-5" + id="path8010" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9078)" + inkscape:connector-curvature="0" + id="path8012" + d="m 175,652.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9074)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,657.36218 5,10 5,-5" + id="path8014" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9070)" + inkscape:connector-curvature="0" + id="path8016" + d="m 175,662.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9066)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,667.36218 5,10 5,-5" + id="path8018" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9062)" + inkscape:connector-curvature="0" + id="path8020" + d="m 175,672.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9058)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,582.36218 5,10 5,-5" + id="path8022" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9054)" + inkscape:connector-curvature="0" + id="path8024" + d="m 190,587.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9050)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,592.36218 5,10 5,-5" + id="path8026" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9046)" + inkscape:connector-curvature="0" + id="path8028" + d="m 190,597.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9042)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,602.36218 5,10 5,-5" + id="path8030" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9038)" + inkscape:connector-curvature="0" + id="path8032" + d="m 190,607.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9034)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,612.36218 5,10 5,-5" + id="path8034" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9030)" + inkscape:connector-curvature="0" + id="path8036" + d="m 190,617.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9026)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,622.36218 5,10 5,-5" + id="path8038" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9022)" + inkscape:connector-curvature="0" + id="path8040" + d="m 190,627.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9018)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,632.36218 5,10 5,-5" + id="path8042" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9014)" + inkscape:connector-curvature="0" + id="path8044" + d="m 190,637.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9010)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,642.36218 5,10 5,-5" + id="path8046" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9006)" + inkscape:connector-curvature="0" + id="path8048" + d="m 190,647.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath9002)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,652.36218 5,10 5,-5" + id="path8050" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8998)" + inkscape:connector-curvature="0" + id="path8052" + d="m 190,657.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8994)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,662.36218 5,10 5,-5" + id="path8054" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8990)" + inkscape:connector-curvature="0" + id="path8056" + d="m 190,667.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8986)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,672.36218 5,10 5,-5" + id="path8058" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8982)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,582.36218 5,10 5,-5" + id="path8060" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8978)" + inkscape:connector-curvature="0" + id="path8062" + d="m 205,587.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8974)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,592.36218 5,10 5,-5" + id="path8064" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8970)" + inkscape:connector-curvature="0" + id="path8066" + d="m 205,597.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8966)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,602.36218 5,10 5,-5" + id="path8068" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8962)" + inkscape:connector-curvature="0" + id="path8070" + d="m 205,607.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8958)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,612.36218 5,10 5,-5" + id="path8072" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8954)" + inkscape:connector-curvature="0" + id="path8074" + d="m 205,617.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8950)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,622.36218 5,10 5,-5" + id="path8076" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8946)" + inkscape:connector-curvature="0" + id="path8078" + d="m 205,627.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8942)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,632.36218 5,10 5,-5" + id="path8080" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8938)" + inkscape:connector-curvature="0" + id="path8082" + d="m 205,637.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8934)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,642.36218 5,10 5,-5" + id="path8084" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8930)" + inkscape:connector-curvature="0" + id="path8086" + d="m 205,647.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8926)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,652.36218 5,10 5,-5" + id="path8088" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8922)" + inkscape:connector-curvature="0" + id="path8090" + d="m 205,657.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8918)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,662.36218 5,10 5,-5" + id="path8092" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8914)" + inkscape:connector-curvature="0" + id="path8094" + d="m 205,667.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8910)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,672.36218 5,10 5,-5" + id="path8096" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8906)" + inkscape:connector-curvature="0" + id="path8098" + d="m 220,582.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8902)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,587.36218 5,10 5,-5" + id="path8100" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8898)" + inkscape:connector-curvature="0" + id="path8102" + d="m 220,592.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8894)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,597.36218 5,10 5,-5" + id="path8104" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8890)" + inkscape:connector-curvature="0" + id="path8106" + d="m 220,602.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8886)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,607.36218 5,10 5,-5" + id="path8108" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8882)" + inkscape:connector-curvature="0" + id="path8110" + d="m 220,612.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8878)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,617.36218 5,10 5,-5" + id="path8112" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8874)" + inkscape:connector-curvature="0" + id="path8114" + d="m 220,622.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8870)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,627.36218 5,10 5,-5" + id="path8116" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8866)" + inkscape:connector-curvature="0" + id="path8118" + d="m 220,632.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8862)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,637.36218 5,10 5,-5" + id="path8120" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8858)" + inkscape:connector-curvature="0" + id="path8122" + d="m 220,642.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8854)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,647.36218 5,10 5,-5" + id="path8124" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8850)" + inkscape:connector-curvature="0" + id="path8126" + d="m 220,652.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8846)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,657.36218 5,10 5,-5" + id="path8128" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8842)" + inkscape:connector-curvature="0" + id="path8130" + d="m 220,662.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8838)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,667.36218 5,10 5,-5" + id="path8132" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8834)" + inkscape:connector-curvature="0" + id="path8134" + d="m 220,672.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8830)" + inkscape:connector-curvature="0" + id="path8136" + d="m 235,582.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8826)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,587.36218 5,10 5,-5" + id="path8138" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8822)" + inkscape:connector-curvature="0" + id="path8140" + d="m 235,592.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8818)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,597.36218 5,10 5,-5" + id="path8142" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8814)" + inkscape:connector-curvature="0" + id="path8144" + d="m 235,602.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8810)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,607.36218 5,10 5,-5" + id="path8146" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8806)" + inkscape:connector-curvature="0" + id="path8148" + d="m 235,612.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8802)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,617.36218 5,10 5,-5" + id="path8150" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8798)" + inkscape:connector-curvature="0" + id="path8152" + d="m 235,622.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8794)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,627.36218 5,10 5,-5" + id="path8154" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8790)" + inkscape:connector-curvature="0" + id="path8156" + d="m 235,632.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8786)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,637.36218 5,10 5,-5" + id="path8158" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8782)" + inkscape:connector-curvature="0" + id="path8160" + d="m 235,642.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8778)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,647.36218 5,10 5,-5" + id="path8162" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8774)" + inkscape:connector-curvature="0" + id="path8164" + d="m 235,652.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8770)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,657.36218 5,10 5,-5" + id="path8166" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8766)" + inkscape:connector-curvature="0" + id="path8168" + d="m 235,662.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8762)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,667.36218 5,10 5,-5" + id="path8170" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8758)" + inkscape:connector-curvature="0" + id="path8172" + d="m 235,672.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8754)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,582.36218 5,10 5,-5" + id="path8174" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8750)" + inkscape:connector-curvature="0" + id="path8176" + d="m 250,587.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8746)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,592.36218 5,10 5,-5" + id="path8178" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8742)" + inkscape:connector-curvature="0" + id="path8180" + d="m 250,597.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8738)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,602.36218 5,10 5,-5" + id="path8182" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8734)" + inkscape:connector-curvature="0" + id="path8184" + d="m 250,607.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8730)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,612.36218 5,10 5,-5" + id="path8186" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8726)" + inkscape:connector-curvature="0" + id="path8188" + d="m 250,617.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8722)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,622.36218 5,10 5,-5" + id="path8190" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8718)" + inkscape:connector-curvature="0" + id="path8192" + d="m 250,627.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8714)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,632.36218 5,10 5,-5" + id="path8194" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8710)" + inkscape:connector-curvature="0" + id="path8196" + d="m 250,637.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8706)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,642.36218 5,10 5,-5" + id="path8198" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8702)" + inkscape:connector-curvature="0" + id="path8200" + d="m 250,647.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8698)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,652.36218 5,10 5,-5" + id="path8202" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8694)" + inkscape:connector-curvature="0" + id="path8204" + d="m 250,657.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8690)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,662.36218 5,10 5,-5" + id="path8206" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8686)" + inkscape:connector-curvature="0" + id="path8208" + d="m 250,667.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8682)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,672.36218 5,10 5,-5" + id="path8210" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8678)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,582.36218 5,10 5,-5" + id="path8212" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8674)" + inkscape:connector-curvature="0" + id="path8214" + d="m 265,587.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8670)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,592.36218 5,10 5,-5" + id="path8216" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8666)" + inkscape:connector-curvature="0" + id="path8218" + d="m 265,597.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8662)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,602.36218 5,10 5,-5" + id="path8220" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8658)" + inkscape:connector-curvature="0" + id="path8222" + d="m 265,607.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8654)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,612.36218 5,10 5,-5" + id="path8224" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8650)" + inkscape:connector-curvature="0" + id="path8226" + d="m 265,617.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8646)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,622.36218 5,10 5,-5" + id="path8228" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8642)" + inkscape:connector-curvature="0" + id="path8230" + d="m 265,627.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8638)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,632.36218 5,10 5,-5" + id="path8232" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8634)" + inkscape:connector-curvature="0" + id="path8234" + d="m 265,637.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8630)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,642.36218 5,10 5,-5" + id="path8236" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8626)" + inkscape:connector-curvature="0" + id="path8238" + d="m 265,647.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8622)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,652.36218 5,10 5,-5" + id="path8240" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8618)" + inkscape:connector-curvature="0" + id="path8242" + d="m 265,657.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8614)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,662.36218 5,10 5,-5" + id="path8244" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8610)" + inkscape:connector-curvature="0" + id="path8246" + d="m 265,667.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8606)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,672.36218 5,10 5,-5" + id="path8248" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8602)" + inkscape:connector-curvature="0" + id="path8250" + d="m 280,582.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8598)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,587.36218 5,10 5,-5" + id="path8252" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8594)" + inkscape:connector-curvature="0" + id="path8254" + d="m 280,592.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8590)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,597.36218 5,10 5,-5" + id="path8256" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8586)" + inkscape:connector-curvature="0" + id="path8258" + d="m 280,602.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8582)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,607.36218 5,10 5,-5" + id="path8260" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8578)" + inkscape:connector-curvature="0" + id="path8262" + d="m 280,612.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8574)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,617.36218 5,10 5,-5" + id="path8264" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8570)" + inkscape:connector-curvature="0" + id="path8266" + d="m 280,622.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8566)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,627.36218 5,10 5,-5" + id="path8268" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8562)" + inkscape:connector-curvature="0" + id="path8270" + d="m 280,632.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8558)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,637.36218 5,10 5,-5" + id="path8272" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8554)" + inkscape:connector-curvature="0" + id="path8274" + d="m 280,642.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8550)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,647.36218 5,10 5,-5" + id="path8276" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8546)" + inkscape:connector-curvature="0" + id="path8278" + d="m 280,652.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8542)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,657.36218 5,10 5,-5" + id="path8280" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8538)" + inkscape:connector-curvature="0" + id="path8282" + d="m 280,662.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8534)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,667.36218 5,10 5,-5" + id="path8284" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8530)" + inkscape:connector-curvature="0" + id="path8286" + d="m 280,672.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8526)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,577.36218 5,10 5,-5" + id="path8288" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8522)" + inkscape:connector-curvature="0" + id="path8290" + d="m 190,577.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8518)" + inkscape:connector-curvature="0" + id="path8292" + d="m 205,577.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8514)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,577.36218 5,10 5,-5" + id="path8294" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8510)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,577.36218 5,10 5,-5" + id="path8296" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8506)" + inkscape:connector-curvature="0" + id="path8298" + d="m 250,577.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8502)" + inkscape:connector-curvature="0" + id="path8300" + d="m 265,577.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8498)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,577.36218 5,10 5,-5" + id="path8302" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8494)" + inkscape:connector-curvature="0" + id="path8304" + d="m 175,572.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8490)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,572.36218 5,10 5,-5" + id="path8306" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8486)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,572.36218 5,10 5,-5" + id="path8308" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8482)" + inkscape:connector-curvature="0" + id="path8310" + d="m 220,572.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8478)" + inkscape:connector-curvature="0" + id="path8312" + d="m 235,572.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8474)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,572.36218 5,10 5,-5" + id="path8314" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8470)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,572.36218 5,10 5,-5" + id="path8316" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8466)" + inkscape:connector-curvature="0" + id="path8318" + d="m 280,572.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8462)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,677.36218 5,10 5,-5" + id="path8322" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8458)" + inkscape:connector-curvature="0" + id="path8324" + d="m 190,677.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8454)" + inkscape:connector-curvature="0" + id="path8326" + d="m 205,677.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8450)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,677.36218 5,10 5,-5" + id="path8328" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8446)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,677.36218 5,10 5,-5" + id="path8330" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8442)" + inkscape:connector-curvature="0" + id="path8332" + d="m 250,677.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8438)" + inkscape:connector-curvature="0" + id="path8334" + d="m 265,677.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8434)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,677.36218 5,10 5,-5" + id="path8336" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8430)" + inkscape:connector-curvature="0" + id="path8338" + d="m 175,682.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8426)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 190,682.36218 5,10 5,-5" + id="path8340" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8422)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 205,682.36218 5,10 5,-5" + id="path8342" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8418)" + inkscape:connector-curvature="0" + id="path8344" + d="m 220,682.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8414)" + inkscape:connector-curvature="0" + id="path8346" + d="m 235,682.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8410)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 250,682.36218 5,10 5,-5" + id="path8348" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8406)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 265,682.36218 5,10 5,-5" + id="path8350" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8402)" + inkscape:connector-curvature="0" + id="path8352" + d="m 280,682.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8398)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 175,687.36218 5,10 5,-5" + id="path8354" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8394)" + inkscape:connector-curvature="0" + id="path8356" + d="m 190,687.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8390)" + inkscape:connector-curvature="0" + id="path8358" + d="m 205,687.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8386)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 220,687.36218 5,10 5,-5" + id="path8360" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8382)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 235,687.36218 5,10 5,-5" + id="path8362" + inkscape:connector-curvature="0" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8378)" + inkscape:connector-curvature="0" + id="path8364" + d="m 250,687.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8374)" + inkscape:connector-curvature="0" + id="path8366" + d="m 265,687.36218 5,10 5,-5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,5)" + clip-path="url(#clipPath8370)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 280,687.36218 5,10 5,-5" + id="path8368" + inkscape:connector-curvature="0" /> + <rect + y="582.86218" + x="170.5" + height="115" + width="115" + id="rect9138" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <rect + style="fill:url(#simple5) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9140" + width="115" + height="115" + x="435.5" + y="587.86218" /> + <use + x="0" + y="0" + xlink:href="#g9142" + id="use9337" + transform="translate(0,130)" + width="744.09448" + height="1052.3622" /> + <rect + y="717.86218" + x="435.5" + height="115" + width="115" + id="rect9339" + style="fill:url(#simple6) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 570,847.36218 5,10" + id="path9341" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9343" + d="m 570,867.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 570,887.36218 5,10" + id="path9345" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9347" + d="m 570,907.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 570,927.36218 5,10" + id="path9349" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9351" + d="m 570,947.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + inkscape:connector-curvature="0" + id="path9355" + d="m 585,847.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 585,867.36218 5,10" + id="path9357" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9359" + d="m 585,887.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 585,907.36218 5,10" + id="path9361" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9363" + d="m 585,927.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 585,947.36218 5,10" + id="path9365" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 600,847.36218 5,10" + id="path9369" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9371" + d="m 600,867.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 600,887.36218 5,10" + id="path9373" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9375" + d="m 600,907.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 600,927.36218 5,10" + id="path9377" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9379" + d="m 600,947.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + inkscape:connector-curvature="0" + id="path9383" + d="m 615,847.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 615,867.36218 5,10" + id="path9385" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9387" + d="m 615,887.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 615,907.36218 5,10" + id="path9389" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9391" + d="m 615,927.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 615,947.36218 5,10" + id="path9393" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 630,847.36218 5,10" + id="path9397" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9399" + d="m 630,867.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 630,887.36218 5,10" + id="path9401" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9403" + d="m 630,907.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 630,927.36218 5,10" + id="path9405" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9407" + d="m 630,947.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + inkscape:connector-curvature="0" + id="path9411" + d="m 645,847.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 645,867.36218 5,10" + id="path9413" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9415" + d="m 645,887.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 645,907.36218 5,10" + id="path9417" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9419" + d="m 645,927.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 645,947.36218 5,10" + id="path9421" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 660,847.36218 5,10" + id="path9425" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9427" + d="m 660,867.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 660,887.36218 5,10" + id="path9429" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9431" + d="m 660,907.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 660,927.36218 5,10" + id="path9433" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9435" + d="m 660,947.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + inkscape:connector-curvature="0" + id="path9439" + d="m 675,847.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 675,867.36218 5,10" + id="path9441" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9443" + d="m 675,887.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 675,907.36218 5,10" + id="path9445" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path9447" + d="m 675,927.36218 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 675,947.36218 5,10" + id="path9449" + inkscape:connector-curvature="0" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9453" + width="115" + height="115" + x="565.5" + y="847.86218" /> + <rect + style="fill:url(#simple7) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect9455" + width="115" + height="115" + x="435.5" + y="847.86218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 360,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + id="path9457" + inkscape:connector-curvature="0" + clip-path="url(#clipPath9573)" + transform="translate(1153.5,14.500003)" /> + <path + inkscape:connector-curvature="0" + id="path9459" + d="m 375,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath9569)" + transform="translate(1153.5,14.500003)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 390,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + id="path9461" + inkscape:connector-curvature="0" + clip-path="url(#clipPath9565)" + transform="translate(1153.5,14.500003)" /> + <path + inkscape:connector-curvature="0" + id="path9463" + d="m 405,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath9561)" + transform="translate(1153.5,14.500003)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 420,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + id="path9465" + inkscape:connector-curvature="0" + clip-path="url(#clipPath9557)" + transform="translate(1153.5,14.500003)" /> + <path + inkscape:connector-curvature="0" + id="path9467" + d="m 435,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath9553)" + transform="translate(1153.5,14.500003)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 450,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + id="path9469" + inkscape:connector-curvature="0" + clip-path="url(#clipPath9549)" + transform="translate(1153.5,14.500003)" /> + <path + inkscape:connector-curvature="0" + id="path9471" + d="m 465,62.362183 5,10 -5,10 5,10 -5,9.999997 5,10 -5,10 5,10 -5,10 5,10 -5,10 5,10 -5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath9545)" + transform="translate(1153.5,14.500003)" /> + <rect + y="77.362183" + x="1509" + height="115" + width="115" + id="rect9577" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + y="77.362183" + x="1378" + height="115" + width="115" + id="rect9579" + style="fill:url(#transform1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 464.08013,373.71824 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025" + id="path10332" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.91987" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10442)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-54.894536" + inkscape:transform-center-x="25.919871" + inkscape:connector-curvature="0" + id="path10352" + d="m 477.07051,381.21824 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10438)" + transform="translate(988,-59.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 490.06089,388.71824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + id="path10354" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.919872" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10434)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-54.894536" + inkscape:transform-center-x="25.919873" + inkscape:connector-curvature="0" + id="path10356" + d="m 503.05127,396.21824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10430)" + transform="translate(988,-59.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 516.04165,403.71824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + id="path10358" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.919874" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10426)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-54.894536" + inkscape:transform-center-x="25.919875" + inkscape:connector-curvature="0" + id="path10360" + d="m 529.03203,411.21824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10422)" + transform="translate(988,-59.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 542.02241,418.71824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + id="path10362" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.919876" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10418)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-54.894536" + inkscape:transform-center-x="25.919877" + inkscape:connector-curvature="0" + id="path10364" + d="m 555.01279,426.21824 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10414)" + transform="translate(988,-59.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 568.00318,433.71824 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025" + id="path10366" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.919868" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10410)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-54.894536" + inkscape:transform-center-x="25.919869" + inkscape:connector-curvature="0" + id="path10368" + d="m 580.99356,441.21824 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10406)" + transform="translate(988,-59.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 593.98394,448.71824 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025" + id="path10370" + inkscape:connector-curvature="0" + inkscape:transform-center-x="25.91987" + inkscape:transform-center-y="-54.894536" + clip-path="url(#clipPath10402)" + transform="translate(988,-59.999997)" /> + <path + inkscape:transform-center-y="-8.6602474" + inkscape:transform-center-x="5" + inkscape:connector-curvature="0" + id="path10376" + d="m 475,797.62497 9.33013,-6.16025 L 485,780.30447 494.33013,774.14421 495,762.98396 504.33013,756.8237 505,745.66345 514.33013,739.5032 515,728.34294 524.33013,722.18269 525,711.02243 534.33013,704.86218 535,693.70193 544.33013,687.54167 545,676.38142 554.33013,670.22116 555,659.06091 564.33013,652.90066 565,641.7404 574.33013,635.58015 575,624.41989" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10669)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 487.99038,805.12497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + id="path10378" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-7.99038" + inkscape:transform-center-y="-16.160247" + clip-path="url(#clipPath10665)" + transform="translate(978,-105)" /> + <path + inkscape:transform-center-y="-23.660247" + inkscape:transform-center-x="-20.98076" + inkscape:connector-curvature="0" + id="path10380" + d="m 500.98076,812.62497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10661)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 513.97114,820.12497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + id="path10382" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-33.97114" + inkscape:transform-center-y="-31.160247" + clip-path="url(#clipPath10657)" + transform="translate(978,-105)" /> + <path + inkscape:transform-center-y="-38.660247" + inkscape:transform-center-x="-46.96153" + inkscape:connector-curvature="0" + id="path10384" + d="m 526.96153,827.62497 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10653)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 539.95191,835.12497 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026" + id="path10386" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-59.95191" + inkscape:transform-center-y="-46.160247" + clip-path="url(#clipPath10649)" + transform="translate(978,-105)" /> + <path + inkscape:transform-center-y="-53.660247" + inkscape:transform-center-x="-72.94229" + inkscape:connector-curvature="0" + id="path10388" + d="m 552.94229,842.62497 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16026 0.66988,-11.16025 9.33012,-6.16025 0.66988,-11.16026 9.33012,-6.16025 0.66988,-11.16026" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10645)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 565.93267,850.12497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + id="path10390" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-85.93267" + inkscape:transform-center-y="-61.160247" + clip-path="url(#clipPath10641)" + transform="translate(978,-105)" /> + <path + inkscape:transform-center-y="-68.660247" + inkscape:transform-center-x="-98.92305" + inkscape:connector-curvature="0" + id="path10392" + d="m 578.92305,857.62497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10637)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 591.91343,865.12497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + id="path10394" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-111.91343" + inkscape:transform-center-y="-76.160247" + clip-path="url(#clipPath10633)" + transform="translate(978,-105)" /> + <path + inkscape:transform-center-y="-83.660247" + inkscape:transform-center-x="-124.90381" + inkscape:connector-curvature="0" + id="path10396" + d="m 604.90381,872.62497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10629)" + transform="translate(978,-105)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 617.89419,880.12497 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16026 0.66987,-11.16025 9.33013,-6.16025 0.66987,-11.16026 9.33013,-6.16025 0.66987,-11.16026" + id="path10398" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-137.89419" + inkscape:transform-center-y="-91.160247" + clip-path="url(#clipPath10625)" + transform="translate(978,-105)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10446" + width="115" + height="115" + x="1508" + y="207.36218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 438.68272,550.75053 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066" + id="path10452" + inkscape:connector-curvature="0" + inkscape:transform-center-x="10.6066" + inkscape:transform-center-y="-17.677667" + clip-path="url(#clipPath10520)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-28.284267" + inkscape:connector-curvature="0" + id="path10454" + d="m 449.28932,561.35713 10.60661,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.60661" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10516)" + transform="translate(988,-125)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 459.89593,571.96373 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066" + id="path10456" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-10.606605" + inkscape:transform-center-y="-38.890872" + clip-path="url(#clipPath10512)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-49.497472" + inkscape:transform-center-x="-21.213205" + inkscape:connector-curvature="0" + id="path10458" + d="m 470.50253,582.57033 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10508)" + transform="translate(988,-125)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 481.10913,593.17694 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066" + id="path10460" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-31.81981" + inkscape:transform-center-y="-60.104077" + clip-path="url(#clipPath10504)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-70.710677" + inkscape:transform-center-x="-42.42641" + inkscape:connector-curvature="0" + id="path10462" + d="m 491.71573,603.78354 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10500)" + transform="translate(988,-125)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 502.32233,614.39014 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066" + id="path10464" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-53.03301" + inkscape:transform-center-y="-81.317277" + clip-path="url(#clipPath10496)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-91.923877" + inkscape:transform-center-x="-63.63961" + inkscape:connector-curvature="0" + id="path10466" + d="m 512.92893,624.99674 10.60661,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.60661" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10492)" + transform="translate(988,-125)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 523.53554,635.60334 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066" + id="path10468" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-74.246215" + inkscape:transform-center-y="-102.53048" + clip-path="url(#clipPath10488)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-113.13708" + inkscape:transform-center-x="-84.852815" + inkscape:connector-curvature="0" + id="path10470" + d="m 534.14214,646.20994 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.60661,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.60661 10.6066,-3.53553 3.53553,-10.6066" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10484)" + transform="translate(988,-125)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 544.74874,656.81655 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.6066,-3.53553 3.53554,-10.6066 10.6066,-3.53554 3.53553,-10.6066 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066" + id="path10472" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-95.45942" + inkscape:transform-center-y="-123.74369" + clip-path="url(#clipPath10480)" + transform="translate(988,-125)" /> + <path + inkscape:transform-center-y="-134.35029" + inkscape:transform-center-x="-106.06602" + inkscape:connector-curvature="0" + id="path10474" + d="m 555.35534,667.42315 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53553 3.53553,-10.60661 10.60661,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066 10.6066,-3.53553 3.53553,-10.6066 10.6066,-3.53554 3.53554,-10.6066" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10476)" + transform="translate(988,-125)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10524" + width="115" + height="115" + x="1508" + y="337.36218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 505,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + id="path10528" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-7.815239" + inkscape:transform-center-y="-60.5" + clip-path="url(#clipPath10582)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + inkscape:transform-center-y="-60.5" + inkscape:transform-center-x="-7.8261091" + inkscape:connector-curvature="0" + id="path10530" + d="m 520,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10578)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 535,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + id="path10532" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-7.8261097" + inkscape:transform-center-y="-60.5" + clip-path="url(#clipPath10574)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + inkscape:transform-center-y="-60.5" + inkscape:transform-center-x="-7.8261103" + inkscape:connector-curvature="0" + id="path10534" + d="m 550,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10570)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 565,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + id="path10536" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-7.8261109" + inkscape:transform-center-y="-60.5" + clip-path="url(#clipPath10566)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + inkscape:transform-center-y="-60.5" + inkscape:transform-center-x="-7.8261115" + inkscape:connector-curvature="0" + id="path10538" + d="m 580,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10562)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 595,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + id="path10540" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-7.8261121" + inkscape:transform-center-y="-60.5" + clip-path="url(#clipPath10558)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <path + inkscape:transform-center-y="-60.5" + inkscape:transform-center-x="-7.8261127" + inkscape:connector-curvature="0" + id="path10542" + d="m 610,707.36218 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10 5,-10 -5,-10" + style="fill:none;stroke:#a080ff;stroke-width:0.978945px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10554)" + transform="matrix(1.0434783,0,0,1,981.04348,-110)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10586" + width="120.00001" + height="115" + x="1508" + y="467.36218" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1380" + y="892.36218" + id="text10615"><tspan + sodipodi:role="line" + id="tspan10617" + x="1380" + y="892.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif">Since hatchUnits="userSpaceOnUse" is used</tspan><tspan + sodipodi:role="line" + x="1380" + y="905.69556" + id="tspan10619" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif">the rendering will match when hatched shape</tspan><tspan + sodipodi:role="line" + x="1380" + y="919.02893" + id="tspan10621" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif">is moved to the point 0,0</tspan></text> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10673" + width="115" + height="115" + x="1508" + y="597.36218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 503.77134,889.56737 7.41782,-8.36516 -2.24143,-10.95335 7.41781,-8.36517 -2.24143,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335" + id="path10731" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-0.773965" + inkscape:transform-center-y="-20.612607" + clip-path="url(#clipPath10801)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-24.494892" + inkscape:transform-center-x="-15.262855" + inkscape:connector-curvature="0" + id="path10733" + d="m 518.26023,893.44966 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24143,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10797)" + transform="translate(973,-34.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 532.74912,897.33195 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24143,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335" + id="path10735" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-29.751745" + inkscape:transform-center-y="-28.377182" + clip-path="url(#clipPath10793)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-32.259467" + inkscape:transform-center-x="-44.24063" + inkscape:connector-curvature="0" + id="path10737" + d="m 547.23801,901.21423 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24143,-10.95335" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10789)" + transform="translate(973,-34.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 561.72689,905.09652 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24143,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336" + id="path10739" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-58.729515" + inkscape:transform-center-y="-36.141752" + clip-path="url(#clipPath10785)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-40.024037" + inkscape:transform-center-x="-73.218405" + inkscape:connector-curvature="0" + id="path10741" + d="m 576.21578,908.9788 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24143,-10.95335 7.41781,-8.36517 -2.24143,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10781)" + transform="translate(973,-34.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 590.70467,912.86109 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24143,-10.95336 7.41781,-8.36516 -2.24143,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336" + id="path10743" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-87.707295" + inkscape:transform-center-y="-43.906322" + clip-path="url(#clipPath10777)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-47.788607" + inkscape:transform-center-x="-102.19618" + inkscape:connector-curvature="0" + id="path10745" + d="m 605.19356,916.74337 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24143,-10.95335" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10773)" + transform="translate(973,-34.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 619.68244,920.62566 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24143,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336" + id="path10747" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-116.68507" + inkscape:transform-center-y="-51.670892" + clip-path="url(#clipPath10769)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-55.553182" + inkscape:transform-center-x="-131.17395" + inkscape:connector-curvature="0" + id="path10749" + d="m 634.17133,924.50795 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24143,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10765)" + transform="translate(973,-34.999997)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="m 648.66022,928.39023 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24143,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335" + id="path10751" + inkscape:connector-curvature="0" + inkscape:transform-center-x="-145.66284" + inkscape:transform-center-y="-59.435467" + clip-path="url(#clipPath10761)" + transform="translate(973,-34.999997)" /> + <path + inkscape:transform-center-y="-63.317752" + inkscape:transform-center-x="-160.15173" + inkscape:connector-curvature="0" + id="path10753" + d="m 663.14911,932.27252 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36517 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336 7.41782,-8.36516 -2.24144,-10.95335 7.41782,-8.36516 -2.24144,-10.95336" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + clip-path="url(#clipPath10757)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10805" + width="115" + height="115" + x="1508" + y="727.36218" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 2380.5,82.862185 V 197.86218" + id="path10807" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="M 2385.5,82.862185 V 197.86218" + id="path10809" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path10811" + d="M 2395.5,82.862185 V 197.86218" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path10813" + d="M 2400.5,82.862185 V 197.86218" + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 2410.5,82.862185 V 197.86218" + id="path10815" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="M 2415.5,82.862185 V 197.86218" + id="path10817" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path10819" + d="M 2425.5,82.862185 V 197.86218" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path10821" + d="M 2430.5,82.862185 V 197.86218" + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 2440.5,82.862185 V 197.86218" + id="path10823" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="M 2445.5,82.862185 V 197.86218" + id="path10825" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path10827" + d="M 2455.5,82.862185 V 197.86218" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path10829" + d="M 2460.5,82.862185 V 197.86218" + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="M 2470.5,82.862185 V 197.86218" + id="path10831" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" + d="M 2475.5,82.862185 V 197.86218" + id="path10833" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path10835" + d="M 2485.5,82.862185 V 197.86218" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path10837" + d="M 2490.5,82.862185 V 197.86218" + style="fill:none;stroke:#32ff3f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.941176" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10839" + width="115" + height="115" + x="2375.5" + y="82.862183" /> + <rect + style="fill:url(#multiple1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect10841" + width="115" + height="115" + x="2245" + y="82.362183" /> + <path + inkscape:connector-curvature="0" + id="path10843" + d="m 1010,212.36218 v 115" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10933)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path10845" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10929)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1025,212.36218 v 115" + id="path10847" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10925)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10849" + d="m 1030,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10921)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10851" + d="m 1040,212.36218 v 115" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10917)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1045,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path10853" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10913)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1055,212.36218 v 115" + id="path10855" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10909)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10857" + d="m 1060,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10905)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10859" + d="m 1070,212.36218 v 115" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10901)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1075,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path10861" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10897)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1085,212.36218 v 115" + id="path10863" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10893)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10865" + d="m 1090,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10889)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path10867" + d="m 1100,212.36218 v 115" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath10885)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1105,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path10869" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10881)" + transform="translate(1370.5,0.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1115,212.36218 v 115" + id="path10871" + inkscape:connector-curvature="0" + clip-path="url(#clipPath10877)" + transform="translate(1370.5,0.5)" /> + <rect + y="212.86218" + x="2375.5" + height="115" + width="115" + id="rect10937" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + clip-path="url(#clipPath11091)" + inkscape:connector-curvature="0" + id="path11001" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + transform="translate(1370.5,130.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1010,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + id="path11003" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11087)" + transform="translate(1370.5,0.5)" /> + <path + transform="translate(1385.5,130.5)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path11005" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11083)" /> + <path + inkscape:connector-curvature="0" + id="path11007" + d="m 1025,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11079)" + transform="translate(1370.5,0.5)" /> + <path + clip-path="url(#clipPath11075)" + inkscape:connector-curvature="0" + id="path11009" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + transform="translate(1400.5,130.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1040,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + id="path11011" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11071)" + transform="translate(1370.5,0.5)" /> + <path + transform="translate(1415.5,130.5)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path11013" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11067)" /> + <path + inkscape:connector-curvature="0" + id="path11015" + d="m 1055,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11063)" + transform="translate(1370.5,0.5)" /> + <path + clip-path="url(#clipPath11059)" + inkscape:connector-curvature="0" + id="path11017" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + transform="translate(1430.5,130.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1070,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + id="path11019" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11055)" + transform="translate(1370.5,0.5)" /> + <path + transform="translate(1445.5,130.5)" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + id="path11021" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11051)" /> + <path + inkscape:connector-curvature="0" + id="path11023" + d="m 1085,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11047)" + transform="translate(1370.5,0.5)" /> + <path + clip-path="url(#clipPath11043)" + inkscape:connector-curvature="0" + id="path11025" + d="m 1015,212.36218 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10 h -5 l 5,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + transform="translate(1460.5,130.5)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1100,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + id="path11027" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11039)" + transform="translate(1370.5,0.5)" /> + <path + inkscape:connector-curvature="0" + id="path11031" + d="m 1115,342.36218 5,17 h -5 l 5,17 h -6 l 6,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17 h -5 l 5,17" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11035)" + transform="translate(1370.5,0.5)" /> + <rect + y="342.86218" + x="2375.5" + height="115" + width="115" + id="rect11095" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" + d="m 570,1132.3622 v 115" + id="path11097" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11099" + d="m 585,1132.3622 v 115" + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" + d="m 600,1132.3622 v 115" + id="path11101" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11103" + d="m 615,1132.3622 v 115" + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" + d="m 630,1132.3622 v 115" + id="path11105" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11107" + d="m 645,1132.3622 v 115" + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" + d="m 660,1132.3622 v 115" + id="path11109" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11111" + d="m 675,1132.3622 v 115" + style="fill:none;stroke:#a080ff;stroke-width:5px;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:10, 4, 2, 4;stroke-opacity:1" /> + <rect + y="1132.3622" + x="565.00018" + height="115" + width="115" + id="rect11113" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1510.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11115" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11117" + d="m 1525.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1540.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11119" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11121" + d="m 1555.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1570.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11123" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11125" + d="m 1585.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1600.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11127" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11129" + d="m 1615.5,1132.8622 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11133" + width="115" + height="115" + x="1505.5002" + y="1132.8622" /> + <path + inkscape:connector-curvature="0" + id="path11135" + d="m 1485,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11183)" + transform="translate(19.5,1175)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1500,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11137" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11179)" + transform="translate(19.5,1175)" /> + <path + inkscape:connector-curvature="0" + id="path11139" + d="m 1515,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11175)" + transform="translate(19.5,1175)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1530,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11141" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11171)" + transform="translate(19.5,1175)" /> + <path + inkscape:connector-curvature="0" + id="path11143" + d="m 1545,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11167)" + transform="translate(19.5,1175)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1560,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11145" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11163)" + transform="translate(19.5,1175)" /> + <path + inkscape:connector-curvature="0" + id="path11147" + d="m 1575,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + clip-path="url(#clipPath11159)" + transform="translate(19.5,1175)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1590,217.36218 5,5 -10,10 5,5 5,5 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10 10,10 -10,10" + id="path11149" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11155)" + transform="translate(19.5,1175)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11187" + width="115" + height="115" + x="1504.5002" + y="1392.3622" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1505.5,1262.8622 5,5 -5,5" + id="path11189" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11191" + d="m 1505.5,1282.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1505.5,1302.8622 5,5 -5,5" + id="path11193" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11195" + d="m 1505.5,1322.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1505.5,1342.8622 5,5 -5,5" + id="path11197" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11199" + d="m 1505.5,1362.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path11203" + d="m 1520.5,1262.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1520.5,1282.8622 5,5 -5,5" + id="path11205" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11207" + d="m 1520.5,1302.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1520.5,1322.8622 5,5 -5,5" + id="path11209" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11211" + d="m 1520.5,1342.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1520.5,1362.8622 5,5 -5,5" + id="path11213" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1535.5,1262.8622 5,5 -5,5" + id="path11217" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11219" + d="m 1535.5,1282.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1535.5,1302.8622 5,5 -5,5" + id="path11221" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11223" + d="m 1535.5,1322.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1535.5,1342.8622 5,5 -5,5" + id="path11225" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11227" + d="m 1535.5,1362.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path11231" + d="m 1550.5,1262.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1550.5,1282.8622 5,5 -5,5" + id="path11233" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11235" + d="m 1550.5,1302.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1550.5,1322.8622 5,5 -5,5" + id="path11237" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11239" + d="m 1550.5,1342.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1550.5,1362.8622 5,5 -5,5" + id="path11241" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1565.5,1262.8622 5,5 -5,5" + id="path11245" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11247" + d="m 1565.5,1282.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1565.5,1302.8622 5,5 -5,5" + id="path11249" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11251" + d="m 1565.5,1322.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1565.5,1342.8622 5,5 -5,5" + id="path11253" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11255" + d="m 1565.5,1362.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path11259" + d="m 1580.5,1262.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1580.5,1282.8622 5,5 -5,5" + id="path11261" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11263" + d="m 1580.5,1302.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1580.5,1322.8622 5,5 -5,5" + id="path11265" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11267" + d="m 1580.5,1342.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1580.5,1362.8622 5,5 -5,5" + id="path11269" + inkscape:connector-curvature="0" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1595.5,1262.8622 5,5 -5,5" + id="path11273" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11275" + d="m 1595.5,1282.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1595.5,1302.8622 5,5 -5,5" + id="path11277" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11279" + d="m 1595.5,1322.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1595.5,1342.8622 5,5 -5,5" + id="path11281" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11283" + d="m 1595.5,1362.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + inkscape:connector-curvature="0" + id="path11287" + d="m 1610.5,1262.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1610.5,1282.8622 5,5 -5,5" + id="path11289" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11291" + d="m 1610.5,1302.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1610.5,1322.8622 5,5 -5,5" + id="path11293" + inkscape:connector-curvature="0" /> + <path + inkscape:connector-curvature="0" + id="path11295" + d="m 1610.5,1342.8622 5,5 -5,5" + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 1610.5,1362.8622 5,5 -5,5" + id="path11297" + inkscape:connector-curvature="0" /> + <rect + y="1262.8622" + x="1505.5002" + height="115" + width="115" + id="rect11315" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <g + id="g11321" + transform="translate(2194,540)"> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path11323" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path11325" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path11327" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path11329" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path11331" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path11333" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path11335" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path11337" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="62.362183" + x="181" + height="115" + width="115" + id="rect11339" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <g + transform="translate(2194,540)" + id="g11341"> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 180,42.362183 V 192.36218" + id="path11343" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4366)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11345" + d="M 195,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4362)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 210,42.362183 V 192.36218" + id="path11347" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4358)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11349" + d="M 225,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4354)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11351" + d="M 240,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4350)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 255,42.362183 V 192.36218" + id="path11353" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4346)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11355" + d="M 270,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4342)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path11357" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4338)" + transform="translate(6,20)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11359" + width="115" + height="115" + x="181" + y="62.362183" /> + </g> + <g + transform="translate(2194,670)" + id="g11361"> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 180,42.362183 V 192.36218" + id="path11363" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4366)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11365" + d="M 195,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4362)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 210,42.362183 V 192.36218" + id="path11367" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4358)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11369" + d="M 225,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4354)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11371" + d="M 240,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4350)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 255,42.362183 V 192.36218" + id="path11373" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4346)" + transform="translate(6,20)" /> + <path + inkscape:connector-curvature="0" + id="path11375" + d="M 270,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath4342)" + transform="translate(6,20)" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path11377" + inkscape:connector-curvature="0" + clip-path="url(#clipPath4338)" + transform="translate(6,20)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11379" + width="115" + height="115" + x="181" + y="62.362183" /> + </g> + <g + id="g11381" + transform="translate(2194,670)"> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path11383" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path11385" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path11387" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path11389" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path11391" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path11393" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path11395" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path11397" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="62.362183" + x="181" + height="115" + width="115" + id="rect11399" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <path + inkscape:connector-curvature="0" + id="path11409" + d="M 225,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath11528)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2332.4597,594.7755)" + inkscape:transform-center-x="28.034277" + inkscape:transform-center-y="-38.122284" /> + <path + inkscape:connector-curvature="0" + id="path11411" + d="M 240,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath11524)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2332.4597,594.7755)" + inkscape:transform-center-x="20.444614" + inkscape:transform-center-y="-45.711947" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 255,42.362183 V 192.36218" + id="path11413" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11520)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2332.4597,594.7755)" + inkscape:transform-center-x="9.8380123" + inkscape:transform-center-y="-51.015248" /> + <path + inkscape:connector-curvature="0" + id="path11415" + d="M 270,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + clip-path="url(#clipPath11516)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2332.4597,594.7755)" + inkscape:transform-center-x="-0.76858941" + inkscape:transform-center-y="-56.318549" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path11417" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11512)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2332.4597,594.7755)" + inkscape:transform-center-x="-11.375191" + inkscape:transform-center-y="-61.62185" /> + <path + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2343.0663,605.3821)" + clip-path="url(#clipPath11508)" + inkscape:connector-curvature="0" + id="path11450" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:transform-center-x="-21.981791" + inkscape:transform-center-y="-72.22845" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path11452" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11504)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2353.6729,615.9887)" + inkscape:transform-center-x="-32.588391" + inkscape:transform-center-y="-82.83505" /> + <path + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2364.2795,626.5953)" + clip-path="url(#clipPath11500)" + inkscape:connector-curvature="0" + id="path11454" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:transform-center-x="-43.194991" + inkscape:transform-center-y="-93.44165" /> + <path + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 285,42.362183 V 192.36218" + id="path11456" + inkscape:connector-curvature="0" + clip-path="url(#clipPath11496)" + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2374.8861,637.2019)" + inkscape:transform-center-x="-53.801591" + inkscape:transform-center-y="-104.04825" /> + <path + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2385.4927,647.8085)" + clip-path="url(#clipPath11492)" + inkscape:connector-curvature="0" + id="path11458" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:transform-center-x="-64.408191" + inkscape:transform-center-y="-114.65485" /> + <path + transform="matrix(0.70710678,0.70710678,-1.1682634,1.1682634,2396.0993,658.41511)" + clip-path="url(#clipPath11488)" + inkscape:connector-curvature="0" + id="path11462" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:0.777987;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:transform-center-x="-75.014791" + inkscape:transform-center-y="-125.26146" /> + <rect + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11532" + width="115" + height="115" + x="2375.5" + y="862.86218" /> + <rect + y="1132.3622" + x="2370" + height="115" + width="115" + id="rect11534" + style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11536" + width="115" + height="115" + x="2370" + y="1262.3622" /> + <rect + y="1392.3622" + x="2370" + height="115" + width="115" + id="rect11538" + style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11540" + width="115" + height="115" + x="2370" + y="1522.3622" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="112.36218" + id="text11542"><tspan + sodipodi:role="line" + id="tspan11544" + x="52.480652" + y="112.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="125.69556" + id="tspan11546" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" stroke-width="2"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="139.02893" + id="tspan11548" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="242.36218" + id="text11550"><tspan + sodipodi:role="line" + id="tspan11552" + x="52.480652" + y="242.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple2" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="255.69556" + id="tspan11554" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="15"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="269.02893" + id="tspan11556" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="372.36218" + id="text11558"><tspan + sodipodi:role="line" + id="tspan11560" + x="52.480652" + y="372.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple3" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="385.69556" + id="tspan11562" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="399.02893" + id="tspan11564" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="502.36218" + id="text11566"><tspan + sodipodi:role="line" + id="tspan11568" + x="52.480652" + y="502.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple4" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="515.69556" + id="tspan11570" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="529.02893" + id="tspan11572" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="632.36218" + id="text11574"><tspan + sodipodi:role="line" + id="tspan11576" + x="52.480652" + y="632.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple5" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="645.69556" + id="tspan11578" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 10,5"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="659.02893" + id="tspan11580" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="762.36218" + id="text11582"><tspan + sodipodi:role="line" + id="tspan11584" + x="52.480652" + y="762.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple6" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="775.69556" + id="tspan11586" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="m 0,0 5,10 5,-5"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="789.02893" + id="tspan11588" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="52.480652" + y="892.36218" + id="text11590"><tspan + sodipodi:role="line" + id="tspan11592" + x="52.480652" + y="892.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="simple7" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="905.69556" + id="tspan11594" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="M 0,0 5,10 M 5,20"/></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="919.02893" + id="tspan11596" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan><tspan + sodipodi:role="line" + x="52.480652" + y="932.3623" + id="tspan11598" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> </tspan></text> + <g + id="g11600" + transform="translate(1324.4998,1460.5)"> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path11602" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path11604" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path11606" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path11608" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path11610" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path11612" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path11614" + inkscape:connector-curvature="0" /> + <path + transform="translate(6,20)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path11616" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#a080ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="62.362183" + x="181" + height="115" + width="115" + id="rect11618" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + </g> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4366)" + inkscape:connector-curvature="0" + id="path11622" + d="M 180,42.362183 V 192.36218" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4362)" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 195,42.362183 V 192.36218" + id="path11624" + inkscape:connector-curvature="0" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4358)" + inkscape:connector-curvature="0" + id="path11626" + d="M 210,42.362183 V 192.36218" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4354)" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 225,42.362183 V 192.36218" + id="path11628" + inkscape:connector-curvature="0" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4350)" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 240,42.362183 V 192.36218" + id="path11630" + inkscape:connector-curvature="0" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4346)" + inkscape:connector-curvature="0" + id="path11632" + d="M 255,42.362183 V 192.36218" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4342)" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 270,42.362183 V 192.36218" + id="path11634" + inkscape:connector-curvature="0" /> + <path + transform="translate(1330.4998,1480.5)" + clip-path="url(#clipPath4338)" + inkscape:connector-curvature="0" + id="path11636" + d="M 285,42.362183 V 192.36218" + style="fill:none;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + <rect + y="1522.8622" + x="1505.5" + height="115" + width="115" + id="rect11638" + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#transform2) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11649" + width="115" + height="115" + x="1378" + y="207.36218" /> + <rect + y="337.36218" + x="1378" + height="115" + width="115" + id="rect11651" + style="fill:url(#transform4) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#transform7) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11653" + width="115" + height="115" + x="1378" + y="467.36218" /> + <rect + y="597.36218" + x="1378" + height="115" + width="115" + id="rect11655" + style="fill:url(#transform8) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#transform9) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11657" + width="115" + height="115" + x="1378" + y="727.36218" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="112.36218" + id="text11659"><tspan + sodipodi:role="line" + id="tspan11661" + x="820" + y="112.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="820" + y="125.69556" + id="tspan11663" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="139.02893" + id="tspan11665" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="240.36218" + id="text11667"><tspan + sodipodi:role="line" + id="tspan11669" + x="820" + y="240.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform2" hatchUnits="userSpaceOnUse" pitch="15" rotate="30"></tspan><tspan + sodipodi:role="line" + x="820" + y="253.69556" + id="tspan11671" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="267.02893" + id="tspan11673" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan><tspan + sodipodi:role="line" + x="820" + y="280.3623" + id="tspan11675" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> </tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="368.36218" + id="text11677"><tspan + sodipodi:role="line" + id="tspan11679" + x="820" + y="368.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform4" hatchUnits="userSpaceOnUse" pitch="15" rotate="45"></tspan><tspan + sodipodi:role="line" + x="820" + y="381.69556" + id="tspan11681" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="395.02893" + id="tspan11683" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="496.36218" + id="text11685"><tspan + sodipodi:role="line" + id="tspan11687" + x="820" + y="496.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform7" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10"></tspan><tspan + sodipodi:role="line" + x="820" + y="509.69556" + id="tspan11689" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="523.02893" + id="tspan11691" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="624.36218" + id="text11693"><tspan + sodipodi:role="line" + id="tspan11695" + x="820" + y="624.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform8" hatchUnits="userSpaceOnUse" pitch="15" x="-5" y="-10" rotate="30"></tspan><tspan + sodipodi:role="line" + x="820" + y="637.69556" + id="tspan11697" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="651.02893" + id="tspan11699" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="820" + y="752.36218" + id="text11701"><tspan + sodipodi:role="line" + x="820" + y="752.36218" + id="tspan11707" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="transform9" hatchUnits="userSpaceOnUse" pitch="15" rotate="30" x="-5" y="-10" </tspan><tspan + sodipodi:role="line" + x="820" + y="765.69556" + id="tspan11718" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif">hatchTransform="matrix(0.96592583,-0.25881905,0.25881905,0.96592583,-8.4757068,43.273395)"></tspan><tspan + sodipodi:role="line" + x="820" + y="779.02893" + id="tspan11714" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,20"/></tspan><tspan + sodipodi:role="line" + x="820" + y="792.3623" + id="tspan11716" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <rect + y="212.36218" + x="2245" + height="115" + width="115" + id="rect11720" + style="fill:url(#multiple2) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#multiple3) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11722" + width="115" + height="115" + x="2245" + y="342.36218" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1804" + y="120.46635" + id="text11724"><tspan + sodipodi:role="line" + id="tspan11726" + x="1804" + y="120.46635" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="multiple1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="1804" + y="133.79973" + id="tspan11728" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5"/></tspan><tspan + sodipodi:role="line" + x="1804" + y="147.1331" + id="tspan11730" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#32ff3f" offset="10"/></tspan><tspan + sodipodi:role="line" + x="1804" + y="160.46648" + id="tspan11732" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1804" + y="250.46635" + id="text11734"><tspan + sodipodi:role="line" + id="tspan11736" + x="1804" + y="250.46635" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="multiple2" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="1804" + y="263.79974" + id="tspan11738" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5"/></tspan><tspan + sodipodi:role="line" + x="1804" + y="277.13312" + id="tspan11740" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></tspan><tspan + sodipodi:role="line" + x="1804" + y="290.46649" + id="tspan11742" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1814" + y="380.46634" + id="text11744"><tspan + sodipodi:role="line" + id="tspan11746" + x="1814" + y="380.46634" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="multiple3" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="1814" + y="393.79971" + id="tspan11748" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,17" /></tspan><tspan + sodipodi:role="line" + x="1814" + y="407.13309" + id="tspan11750" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10"/></tspan><tspan + sodipodi:role="line" + x="1814" + y="420.46646" + id="tspan11752" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <rect + y="602.36218" + x="2245" + height="115" + width="115" + id="rect11754" + style="fill:url(#ref1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#ref2) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11756" + width="115" + height="115" + x="2245" + y="732.36218" /> + <rect + y="862.36218" + x="2245" + height="115" + width="115" + id="rect11758" + style="fill:url(#ref3) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1834" + y="640.46637" + id="text11760"><tspan + sodipodi:role="line" + id="tspan11762" + x="1834" + y="640.46637" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="ref1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="1834" + y="653.79974" + id="tspan11764" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5"/></tspan><tspan + sodipodi:role="line" + x="1834" + y="667.13312" + id="tspan11766" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1834" + y="752.36218" + id="text11768"><tspan + sodipodi:role="line" + id="tspan11770" + x="1834" + y="752.36218" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="ref2" xlink:href="#ref1"></tspan><tspan + sodipodi:role="line" + x="1834" + y="765.69556" + id="tspan11772" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1832.8698" + y="900.46637" + id="text11774"><tspan + sodipodi:role="line" + id="tspan11776" + x="1832.8698" + y="900.46637" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="ref3" xlink:href="#ref1" pitch="45"></tspan><tspan + sodipodi:role="line" + x="1832.8698" + y="913.79974" + id="tspan11778" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <rect + y="1132.3622" + x="435" + height="115" + width="115" + id="rect11780" + style="fill:url(#stroke1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="60" + y="1172.3622" + id="text11782"><tspan + sodipodi:role="line" + id="tspan11784" + x="60" + y="1172.3622" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="stroke1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="60" + y="1185.6956" + id="tspan11786" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" stroke-width="5" </tspan><tspan + sodipodi:role="line" + x="60" + y="1199.0289" + id="tspan11792" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> stroke-dasharray="10 4 2 4"/></tspan><tspan + sodipodi:role="line" + x="60" + y="1212.3623" + id="tspan11788" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan><tspan + sodipodi:role="line" + x="60" + y="1225.6957" + id="tspan11790" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> </tspan></text> + <rect + y="1132.3622" + x="1375" + height="115" + width="115" + id="rect11794" + style="fill:url(#overflow1) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#overflow2) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11796" + width="115" + height="115" + x="1375" + y="1262.3622" /> + <rect + y="1392.3622" + x="1375" + height="115" + width="115" + id="rect11798" + style="fill:url(#overflow3) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#overflow4) #ff0000;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11800" + width="115" + height="115" + x="1375" + y="1522.3622" /> + <rect + style="fill:url(#degenerate1) #00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11802" + width="115" + height="115" + x="2235" + y="1132.3622" /> + <rect + y="1262.3622" + x="2235" + height="115" + width="115" + id="rect11804" + style="fill:url(#degenerate2) #00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <rect + style="fill:url(#degenerate3) #00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11806" + width="115" + height="115" + x="2235" + y="1392.3622" /> + <rect + y="1522.3622" + x="2235" + height="115" + width="115" + id="rect11808" + style="fill:url(#degenerate4) #00ff00;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="870" + y="1160.4663" + id="text11810"><tspan + sodipodi:role="line" + id="tspan11812" + x="870" + y="1160.4663" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="overflow1" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + rotate="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" + sodipodi:role="line" + x="870" + y="1173.7997" + id="tspan11814" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="5" d="L 0,0 5,5 -5,15, 0,20"/></tspan><tspan + sodipodi:role="line" + x="870" + y="1187.1331" + id="tspan11816" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="860" + y="1300.4663" + id="text11818"><tspan + sodipodi:role="line" + id="tspan11820" + x="860" + y="1300.4663" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="overflow2" style="overflow:hidden" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="860" + y="1313.7997" + id="tspan11822" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></tspan><tspan + sodipodi:role="line" + x="860" + y="1327.1331" + id="tspan11824" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="860" + y="1422.3622" + id="text11826"><tspan + sodipodi:role="line" + id="tspan11828" + x="860" + y="1422.3622" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="overflow3" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="860" + y="1435.6956" + id="tspan11830" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="0" d="L 0,0 5,5 -5,15, 0,20"/></tspan><tspan + sodipodi:role="line" + x="860" + y="1449.0289" + id="tspan11832" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan><tspan + sodipodi:role="line" + x="860" + y="1462.3623" + id="tspan11834" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> </tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="860" + y="1561.3726" + id="text11836"><tspan + sodipodi:role="line" + id="tspan11838" + x="860" + y="1561.3726" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="overflow4" style="overflow:visible" hatchUnits="userSpaceOnUse" pitch="15"></tspan><tspan + sodipodi:role="line" + x="860" + y="1574.7059" + id="tspan11840" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#32ff3f" offset="5" ></tspan><tspan + sodipodi:role="line" + x="860" + y="1588.0393" + id="tspan11842" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#ff0000" offset="20" ></tspan><tspan + sodipodi:role="line" + x="860" + y="1601.3727" + id="tspan11844" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1849.0837" + y="1191.0269" + id="text11846"><tspan + sodipodi:role="line" + id="tspan11848" + x="1849.0837" + y="1191.0269" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="degenerate1" pitch="45"></tspan><tspan + sodipodi:role="line" + x="1849.0837" + y="1204.3602" + id="tspan11850" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1840" + y="1312.3622" + id="text11852"><tspan + sodipodi:role="line" + id="tspan11854" + x="1840" + y="1312.3622" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="degenerate2" xlink:href="#nonexisting" pitch="45"></tspan><tspan + sodipodi:role="line" + x="1840" + y="1325.6956" + id="tspan11856" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1840" + y="1440.4663" + id="text11858"><tspan + sodipodi:role="line" + id="tspan11860" + x="1840" + y="1440.4663" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="degenerate3" pitch="30"></tspan><tspan + sodipodi:role="line" + x="1840" + y="1453.7997" + id="tspan11862" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 0,15"/></tspan><tspan + sodipodi:role="line" + x="1840" + y="1467.1331" + id="tspan11864" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1840" + y="1572.3622" + id="text11866"><tspan + sodipodi:role="line" + id="tspan11868" + x="1840" + y="1572.3622" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"><hatch id="degenerate4" pitch="30"></tspan><tspan + sodipodi:role="line" + x="1840" + y="1585.6956" + id="tspan11870" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"> <hatchPath stroke="#a080ff" offset="10" d="L 0,0 5,10 -5,15"/></tspan><tspan + sodipodi:role="line" + x="1840" + y="1599.0289" + id="tspan11872" + style="font-size:10.6667px;line-height:1.25;font-family:sans-serif"></hatch></tspan></text> + <text + id="text11874" + y="30.596558" + x="818.41797" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + xml:space="preserve"><tspan + style="font-size:24px;line-height:1.25;font-family:sans-serif" + y="30.596558" + x="818.41797" + id="tspan11876" + sodipodi:role="line">Hatch transforms</tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1817.6445" + y="52.022339" + id="text11878"><tspan + sodipodi:role="line" + id="tspan11880" + x="1817.6445" + y="52.022339" + style="font-size:24px;line-height:1.25;font-family:sans-serif">Multiple hatch paths</tspan></text> + <text + id="text11882" + y="547.37" + x="1827.6445" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + xml:space="preserve"><tspan + style="font-size:24px;line-height:1.25;font-family:sans-serif" + y="547.37" + x="1827.6445" + id="tspan11884" + sodipodi:role="line">Hatch linking</tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="64.304688" + y="1090.5966" + id="text11886"><tspan + sodipodi:role="line" + id="tspan11888" + x="64.304688" + y="1090.5966" + style="font-size:24px;line-height:1.25;font-family:sans-serif">Stroke style</tspan></text> + <text + id="text11890" + y="1090.5966" + x="884.30469" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + xml:space="preserve"><tspan + style="font-size:24px;line-height:1.25;font-family:sans-serif" + y="1090.5966" + x="884.30469" + id="tspan11892" + sodipodi:role="line">Overflow property</tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;line-height:0%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none" + x="1844.3047" + y="1090.5966" + id="text11894"><tspan + sodipodi:role="line" + id="tspan11896" + x="1844.3047" + y="1090.5966" + style="font-size:24px;line-height:1.25;font-family:sans-serif">Degenerate cases</tspan></text> + <rect + style="fill:url(#linearGradient11916);fill-opacity:1;stroke:#32ff3f;stroke-width:1;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" + id="rect11898" + width="300" + height="100" + x="180" + y="-267.63782" /> + </g> + <script + xlink:href="../hatch.js" + type="text/javascript" + id="script1986" /> +</svg> diff --git a/src/extension/internal/polyfill/mesh.js b/src/extension/internal/polyfill/mesh.js new file mode 100644 index 0000000..bcce389 --- /dev/null +++ b/src/extension/internal/polyfill/mesh.js @@ -0,0 +1,1192 @@ +// SPDX-License-Identifier: CC0 +/** @file + * Use Canvas to render a mesh gradient, passing the rendering to an image via a data stream. + *//* + * Authors: + * - Tavmjong Bah 2018 + * - Valentin Ionita (2019) + * License: CC0 / Public Domain + */ + +(function () { + // Name spaces ----------------------------------- + const svgNS = 'http://www.w3.org/2000/svg'; + const xlinkNS = 'http://www.w3.org/1999/xlink'; + const xhtmlNS = 'http://www.w3.org/1999/xhtml'; + /* + * Maximum threshold for Bezier step size + * Larger values leave holes, smaller take longer to render. + */ + const maxBezierStep = 2.0; + + // Test if mesh gradients are supported. + if (document.createElementNS(svgNS, 'meshgradient').x) { + return; + } + + /* + * Utility functions ----------------------------- + */ + // Split Bezier using de Casteljau's method. + const splitBezier = (p0, p1, p2, p3) => { + let tmp = new Point((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5); + let p01 = new Point((p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5); + let p12 = new Point((p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5); + let p02 = new Point((tmp.x + p01.x) * 0.5, (tmp.y + p01.y) * 0.5); + let p11 = new Point((tmp.x + p12.x) * 0.5, (tmp.y + p12.y) * 0.5); + let p03 = new Point((p02.x + p11.x) * 0.5, (p02.y + p11.y) * 0.5); + + return ([ + [p0, p01, p02, p03], + [p03, p11, p12, p3] + ]); + }; + + // See Cairo: cairo-mesh-pattern-rasterizer.c + const bezierStepsSquared = (points) => { + let tmp0 = points[0].distSquared(points[1]); + let tmp1 = points[2].distSquared(points[3]); + let tmp2 = points[0].distSquared(points[2]) * 0.25; + let tmp3 = points[1].distSquared(points[3]) * 0.25; + + let max1 = tmp0 > tmp1 ? tmp0 : tmp1; + + let max2 = tmp2 > tmp3 ? tmp2 : tmp3; + + let max = max1 > max2 ? max1 : max2; + + return max * 18; + }; + + // Euclidean distance + const distance = (p0, p1) => Math.sqrt(p0.distSquared(p1)); + + // Weighted average to find Bezier points for linear sides. + const wAvg = (p0, p1) => p0.scale(2.0 / 3.0).add(p1.scale(1.0 / 3.0)); + + // Browsers return a string rather than a transform list for gradientTransform! + const parseTransform = (t) => { + let affine = new Affine(); + let trans, scale, radian, tan, skewx, skewy, rotate; + let transforms = t.match(/(\w+\(\s*[^)]+\))+/g); + + transforms.forEach((i) => { + let c = i.match(/[\w.-]+/g); + let type = c.shift(); + + switch (type) { + case 'translate': + if (c.length === 2) { + trans = new Affine(1, 0, 0, 1, c[0], c[1]); + } else { + console.error('mesh.js: translate does not have 2 arguments!'); + trans = new Affine(1, 0, 0, 1, 0, 0); + } + affine = affine.append(trans); + break; + + case 'scale': + if (c.length === 1) { + scale = new Affine(c[0], 0, 0, c[0], 0, 0); + } else if (c.length === 2) { + scale = new Affine(c[0], 0, 0, c[1], 0, 0); + } else { + console.error('mesh.js: scale does not have 1 or 2 arguments!'); + scale = new Affine(1, 0, 0, 1, 0, 0); + } + affine = affine.append(scale); + break; + + case 'rotate': + if (c.length === 3) { + trans = new Affine(1, 0, 0, 1, c[1], c[2]); + affine = affine.append(trans); + } + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + let cos = Math.cos(radian); + let sin = Math.sin(radian); + if (Math.abs(cos) < 1e-16) { // I hate rounding errors... + cos = 0; + } + if (Math.abs(sin) < 1e-16) { // I hate rounding errors... + sin = 0; + } + rotate = new Affine(cos, sin, -sin, cos, 0, 0); + affine = affine.append(rotate); + } else { + console.error('math.js: No argument to rotate transform!'); + } + if (c.length === 3) { + trans = new Affine(1, 0, 0, 1, -c[1], -c[2]); + affine = affine.append(trans); + } + break; + + case 'skewX': + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + tan = Math.tan(radian); + skewx = new Affine(1, 0, tan, 1, 0, 0); + affine = affine.append(skewx); + } else { + console.error('math.js: No argument to skewX transform!'); + } + break; + + case 'skewY': + if (c[0]) { + radian = c[0] * Math.PI / 180.0; + tan = Math.tan(radian); + skewy = new Affine(1, tan, 0, 1, 0, 0); + affine = affine.append(skewy); + } else { + console.error('math.js: No argument to skewY transform!'); + } + break; + + case 'matrix': + if (c.length === 6) { + affine = affine.append(new Affine(...c)); + } else { + console.error('math.js: Incorrect number of arguments for matrix!'); + } + break; + + default: + console.error('mesh.js: Unhandled transform type: ' + type); + break; + } + }); + + return affine; + }; + + const parsePoints = (s) => { + let points = []; + let values = s.split(/[ ,]+/); + for (let i = 0, imax = values.length - 1; i < imax; i += 2) { + points.push(new Point(parseFloat(values[i]), parseFloat(values[i + 1]))); + } + return points; + }; + + // Set multiple attributes to an element + const setAttributes = (el, attrs) => { + for (let key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; + + // Find the slope of point p_k by the values in p_k-1 and p_k+1 + const finiteDifferences = (c0, c1, c2, d01, d12) => { + let slope = [0, 0, 0, 0]; + let slow, shigh; + + for (let k = 0; k < 3; ++k) { + if ((c1[k] < c0[k] && c1[k] < c2[k]) || (c0[k] < c1[k] && c2[k] < c1[k])) { + slope[k] = 0; + } else { + slope[k] = 0.5 * ((c1[k] - c0[k]) / d01 + (c2[k] - c1[k]) / d12); + slow = Math.abs(3.0 * (c1[k] - c0[k]) / d01); + shigh = Math.abs(3.0 * (c2[k] - c1[k]) / d12); + + if (slope[k] > slow) { + slope[k] = slow; + } else if (slope[k] > shigh) { + slope[k] = shigh; + } + } + } + + return slope; + }; + + // Coefficient matrix used for solving linear system + const A = [ + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-3, 3, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, -2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2, -1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0], + [-3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0], + [9, -9, -9, 9, 6, 3, -6, -3, 6, -6, 3, -3, 4, 2, 2, 1], + [-6, 6, 6, -6, -3, -3, 3, 3, -4, 4, -2, 2, -2, -2, -1, -1], + [2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [-6, 6, 6, -6, -4, -2, 4, 2, -3, 3, -3, 3, -2, -1, -2, -1], + [4, -4, -4, 4, 2, 2, -2, -2, 2, -2, 2, -2, 1, 1, 1, 1] + ]; + + // Solve the linear system for bicubic interpolation + const solveLinearSystem = (v) => { + let alpha = []; + + for (let i = 0; i < 16; ++i) { + alpha[i] = 0; + for (let j = 0; j < 16; ++j) { + alpha[i] += A[i][j] * v[j]; + } + } + + return alpha; + }; + + // Evaluate the interpolation parameters at (y, x) + const evaluateSolution = (alpha, x, y) => { + const xx = x * x; + const yy = y * y; + const xxx = x * x * x; + const yyy = y * y * y; + + let result = + alpha[0] + + alpha[1] * x + + alpha[2] * xx + + alpha[3] * xxx + + alpha[4] * y + + alpha[5] * y * x + + alpha[6] * y * xx + + alpha[7] * y * xxx + + alpha[8] * yy + + alpha[9] * yy * x + + alpha[10] * yy * xx + + alpha[11] * yy * xxx + + alpha[12] * yyy + + alpha[13] * yyy * x + + alpha[14] * yyy * xx + + alpha[15] * yyy * xxx; + + return result; + }; + + // Split a patch into 8x8 smaller patches + const splitPatch = (patch) => { + let yPatches = []; + let xPatches = []; + let patches = []; + + // Horizontal splitting + for (let i = 0; i < 4; ++i) { + yPatches[i] = []; + yPatches[i][0] = splitBezier( + patch[0][i], patch[1][i], + patch[2][i], patch[3][i] + ); + + yPatches[i][1] = []; + yPatches[i][1].push(...splitBezier(...yPatches[i][0][0])); + yPatches[i][1].push(...splitBezier(...yPatches[i][0][1])); + + yPatches[i][2] = []; + yPatches[i][2].push(...splitBezier(...yPatches[i][1][0])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][1])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][2])); + yPatches[i][2].push(...splitBezier(...yPatches[i][1][3])); + } + + // Vertical splitting + for (let i = 0; i < 8; ++i) { + xPatches[i] = []; + + for (let j = 0; j < 4; ++j) { + xPatches[i][j] = []; + xPatches[i][j][0] = splitBezier( + yPatches[0][2][i][j], yPatches[1][2][i][j], + yPatches[2][2][i][j], yPatches[3][2][i][j] + ); + + xPatches[i][j][1] = []; + xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][0])); + xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][1])); + + xPatches[i][j][2] = []; + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][0])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][1])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][2])); + xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][3])); + } + } + + for (let i = 0; i < 8; ++i) { + patches[i] = []; + + for (let j = 0; j < 8; ++j) { + patches[i][j] = []; + + patches[i][j][0] = xPatches[i][0][2][j]; + patches[i][j][1] = xPatches[i][1][2][j]; + patches[i][j][2] = xPatches[i][2][2][j]; + patches[i][j][3] = xPatches[i][3][2][j]; + } + } + + return patches; + }; + + // Point class ----------------------------------- + class Point { + constructor (x, y) { + this.x = x || 0; + this.y = y || 0; + } + + toString () { + return `(x=${this.x}, y=${this.y})`; + } + + clone () { + return new Point(this.x, this.y); + } + + add (v) { + return new Point(this.x + v.x, this.y + v.y); + } + + scale (v) { + if (v.x === undefined) { + return new Point(this.x * v, this.y * v); + } + return new Point(this.x * v.x, this.y * v.y); + } + + distSquared (v) { + let x = this.x - v.x; + let y = this.y - v.y; + return (x * x + y * y); + } + + // Transform by affine + transform (affine) { + let x = this.x * affine.a + this.y * affine.c + affine.e; + let y = this.x * affine.b + this.y * affine.d + affine.f; + return new Point(x, y); + } + } + + /* + * Affine class ------------------------------------- + * + * As defined in the SVG spec + * | a c e | + * | b d f | + * | 0 0 1 | + * + */ + + class Affine { + constructor (a, b, c, d, e, f) { + if (a === undefined) { + this.a = 1; + this.b = 0; + this.c = 0; + this.d = 1; + this.e = 0; + this.f = 0; + } else { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + } + } + + toString () { + return `affine: ${this.a} ${this.c} ${this.e} \n\ + ${this.b} ${this.d} ${this.f}`; + } + + append (v) { + if (!(v instanceof Affine)) { + console.error(`mesh.js: argument to Affine.append is not affine!`); + } + let a = this.a * v.a + this.c * v.b; + let b = this.b * v.a + this.d * v.b; + let c = this.a * v.c + this.c * v.d; + let d = this.b * v.c + this.d * v.d; + let e = this.a * v.e + this.c * v.f + this.e; + let f = this.b * v.e + this.d * v.f + this.f; + return new Affine(a, b, c, d, e, f); + } + } + + // Curve class -------------------------------------- + class Curve { + constructor (nodes, colors) { + this.nodes = nodes; // 4 Bezier points + this.colors = colors; // 2 x 4 colors (two ends x R+G+B+A) + } + + /* + * Paint a Bezier curve + * w is canvas.width + * h is canvas.height + */ + paintCurve (v, w) { + // If inside, see if we need to split + if (bezierStepsSquared(this.nodes) > maxBezierStep) { + const beziers = splitBezier(...this.nodes); + // ([start][end]) + let colors0 = [[], []]; + let colors1 = [[], []]; + + /* + * Linear horizontal interpolation of the middle value for every + * patch exceeding thereshold + */ + for (let i = 0; i < 4; ++i) { + colors0[0][i] = this.colors[0][i]; + colors0[1][i] = (this.colors[0][i] + this.colors[1][i]) / 2; + colors1[0][i] = colors0[1][i]; + colors1[1][i] = this.colors[1][i]; + } + let curve0 = new Curve(beziers[0], colors0); + let curve1 = new Curve(beziers[1], colors1); + curve0.paintCurve(v, w); + curve1.paintCurve(v, w); + } else { + // Directly write data + let x = Math.round(this.nodes[0].x); + if (x >= 0 && x < w) { + let index = (~~this.nodes[0].y * w + x) * 4; + v[index] = Math.round(this.colors[0][0]); + v[index + 1] = Math.round(this.colors[0][1]); + v[index + 2] = Math.round(this.colors[0][2]); + v[index + 3] = Math.round(this.colors[0][3]); // Alpha + } + } + } + } + + // Patch class ------------------------------------- + class Patch { + constructor (nodes, colors) { + this.nodes = nodes; // 4x4 array of points + this.colors = colors; // 2x2x4 colors (four corners x R+G+B+A) + } + + // Split patch horizontally into two patches. + split () { + let nodes0 = [[], [], [], []]; + let nodes1 = [[], [], [], []]; + let colors0 = [ + [[], []], + [[], []] + ]; + let colors1 = [ + [[], []], + [[], []] + ]; + + for (let i = 0; i < 4; ++i) { + const beziers = splitBezier( + this.nodes[0][i], this.nodes[1][i], + this.nodes[2][i], this.nodes[3][i] + ); + + nodes0[0][i] = beziers[0][0]; + nodes0[1][i] = beziers[0][1]; + nodes0[2][i] = beziers[0][2]; + nodes0[3][i] = beziers[0][3]; + nodes1[0][i] = beziers[1][0]; + nodes1[1][i] = beziers[1][1]; + nodes1[2][i] = beziers[1][2]; + nodes1[3][i] = beziers[1][3]; + } + + /* + * Linear vertical interpolation of the middle value for every + * patch exceeding thereshold + */ + for (let i = 0; i < 4; ++i) { + colors0[0][0][i] = this.colors[0][0][i]; + colors0[0][1][i] = this.colors[0][1][i]; + colors0[1][0][i] = (this.colors[0][0][i] + this.colors[1][0][i]) / 2; + colors0[1][1][i] = (this.colors[0][1][i] + this.colors[1][1][i]) / 2; + colors1[0][0][i] = colors0[1][0][i]; + colors1[0][1][i] = colors0[1][1][i]; + colors1[1][0][i] = this.colors[1][0][i]; + colors1[1][1][i] = this.colors[1][1][i]; + } + + return ([new Patch(nodes0, colors0), new Patch(nodes1, colors1)]); + } + + paint (v, w) { + // Check if we need to split + let larger = false; + let step; + for (let i = 0; i < 4; ++i) { + step = bezierStepsSquared([ + this.nodes[0][i], this.nodes[1][i], + this.nodes[2][i], this.nodes[3][i] + ]); + + if (step > maxBezierStep) { + larger = true; + break; + } + } + + if (larger) { + let patches = this.split(); + patches[0].paint(v, w); + patches[1].paint(v, w); + } else { + /* + * Paint a Bezier curve using just the top of the patch. If + * the patch is thin enough this should work. We leave this + * function here in case we want to do something more fancy. + */ + let curve = new Curve([...this.nodes[0]], [...this.colors[0]]); + curve.paintCurve(v, w); + } + } + } + + // Mesh class --------------------------------------- + class Mesh { + constructor (mesh) { + this.readMesh(mesh); + this.type = mesh.getAttribute('type') || 'bilinear'; + } + + // Function to parse an SVG mesh and set the nodes (points) and colors + readMesh (mesh) { + let nodes = [[]]; + let colors = [[]]; + + let x = Number(mesh.getAttribute('x')); + let y = Number(mesh.getAttribute('y')); + nodes[0][0] = new Point(x, y); + + let rows = mesh.children; + for (let i = 0, imax = rows.length; i < imax; ++i) { + // Need to validate if meshrow... + nodes[3 * i + 1] = []; // Need three extra rows for each meshrow. + nodes[3 * i + 2] = []; + nodes[3 * i + 3] = []; + colors[i + 1] = []; // Need one more row than number of meshrows. + + let patches = rows[i].children; + for (let j = 0, jmax = patches.length; j < jmax; ++j) { + let stops = patches[j].children; + for (let k = 0, kmax = stops.length; k < kmax; ++k) { + let l = k; + if (i !== 0) { + ++l; // There is no top if row isn't first row. + } + let path = stops[k].getAttribute('path'); + let parts; + + let type = 'l'; // We need to still find mid-points even if no path. + if (path != null) { + parts = path.match(/\s*([lLcC])\s*(.*)/); + type = parts[1]; + } + let stopNodes = parsePoints(parts[2]); + + switch (type) { + case 'l': + if (l === 0) { // Top + nodes[3 * i][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]); + nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 3][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]); + } + nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]); + } else { // Left + nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]); + nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]); + } + break; + case 'L': + if (l === 0) { // Top + nodes[3 * i][3 * j + 3] = stopNodes[0]; + nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]); + nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 3][3 * j + 3] = stopNodes[0]; + nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[0]; + } + nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]); + } else { // Left + nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]); + nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]); + } + break; + case 'c': + if (l === 0) { // Top + nodes[3 * i][3 * j + 1] = stopNodes[0].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 2] = stopNodes[1].add(nodes[3 * i][3 * j]); + nodes[3 * i][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j]); + } else if (l === 1) { // Right + nodes[3 * i + 1][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 2][3 * j + 3] = stopNodes[1].add(nodes[3 * i][3 * j + 3]); + nodes[3 * i + 3][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j + 3]); + } else if (l === 2) { // Bottom + nodes[3 * i + 3][3 * j + 2] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]); + nodes[3 * i + 3][3 * j + 1] = stopNodes[1].add(nodes[3 * i + 3][3 * j + 3]); + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[2].add(nodes[3 * i + 3][3 * j + 3]); + } + } else { // Left + nodes[3 * i + 2][3 * j] = stopNodes[0].add(nodes[3 * i + 3][3 * j]); + nodes[3 * i + 1][3 * j] = stopNodes[1].add(nodes[3 * i + 3][3 * j]); + } + break; + case 'C': + if (l === 0) { // Top + nodes[3 * i][3 * j + 1] = stopNodes[0]; + nodes[3 * i][3 * j + 2] = stopNodes[1]; + nodes[3 * i][3 * j + 3] = stopNodes[2]; + } else if (l === 1) { // Right + nodes[3 * i + 1][3 * j + 3] = stopNodes[0]; + nodes[3 * i + 2][3 * j + 3] = stopNodes[1]; + nodes[3 * i + 3][3 * j + 3] = stopNodes[2]; + } else if (l === 2) { // Bottom + nodes[3 * i + 3][3 * j + 2] = stopNodes[0]; + nodes[3 * i + 3][3 * j + 1] = stopNodes[1]; + if (j === 0) { + nodes[3 * i + 3][3 * j + 0] = stopNodes[2]; + } + } else { // Left + nodes[3 * i + 2][3 * j] = stopNodes[0]; + nodes[3 * i + 1][3 * j] = stopNodes[1]; + } + break; + default: + console.error('mesh.js: ' + type + ' invalid path type.'); + } + + if ((i === 0 && j === 0) || k > 0) { + let colorRaw = window.getComputedStyle(stops[k]).stopColor + .match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); + let alphaRaw = window.getComputedStyle(stops[k]).stopOpacity; + let alpha = 255; + if (alphaRaw) { + alpha = Math.floor(alphaRaw * 255); + } + + if (colorRaw) { + if (l === 0) { // upper left corner + colors[i][j] = []; + colors[i][j][0] = Math.floor(colorRaw[1]); + colors[i][j][1] = Math.floor(colorRaw[2]); + colors[i][j][2] = Math.floor(colorRaw[3]); + colors[i][j][3] = alpha; // Alpha + } else if (l === 1) { // upper right corner + colors[i][j + 1] = []; + colors[i][j + 1][0] = Math.floor(colorRaw[1]); + colors[i][j + 1][1] = Math.floor(colorRaw[2]); + colors[i][j + 1][2] = Math.floor(colorRaw[3]); + colors[i][j + 1][3] = alpha; // Alpha + } else if (l === 2) { // lower right corner + colors[i + 1][j + 1] = []; + colors[i + 1][j + 1][0] = Math.floor(colorRaw[1]); + colors[i + 1][j + 1][1] = Math.floor(colorRaw[2]); + colors[i + 1][j + 1][2] = Math.floor(colorRaw[3]); + colors[i + 1][j + 1][3] = alpha; // Alpha + } else if (l === 3) { // lower left corner + colors[i + 1][j] = []; + colors[i + 1][j][0] = Math.floor(colorRaw[1]); + colors[i + 1][j][1] = Math.floor(colorRaw[2]); + colors[i + 1][j][2] = Math.floor(colorRaw[3]); + colors[i + 1][j][3] = alpha; // Alpha + } + } + } + } + + // SVG doesn't use tensor points but we need them for rendering. + nodes[3 * i + 1][3 * j + 1] = new Point(); + nodes[3 * i + 1][3 * j + 2] = new Point(); + nodes[3 * i + 2][3 * j + 1] = new Point(); + nodes[3 * i + 2][3 * j + 2] = new Point(); + + nodes[3 * i + 1][3 * j + 1].x = + (-4.0 * nodes[3 * i][3 * j].x + + 6.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 1][3 * j].x) + + -2.0 * (nodes[3 * i][3 * j + 3].x + nodes[3 * i + 3][3 * j].x) + + 3.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 1][3 * j + 3].x) + + -1.0 * nodes[3 * i + 3][3 * j + 3].x) / 9.0; + nodes[3 * i + 1][3 * j + 2].x = + (-4.0 * nodes[3 * i][3 * j + 3].x + + 6.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 1][3 * j + 3].x) + + -2.0 * (nodes[3 * i][3 * j].x + nodes[3 * i + 3][3 * j + 3].x) + + 3.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 1][3 * j].x) + + -1.0 * nodes[3 * i + 3][3 * j].x) / 9.0; + nodes[3 * i + 2][3 * j + 1].x = + (-4.0 * nodes[3 * i + 3][3 * j].x + + 6.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 2][3 * j].x) + + -2.0 * (nodes[3 * i + 3][3 * j + 3].x + nodes[3 * i][3 * j].x) + + 3.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 2][3 * j + 3].x) + + -1.0 * nodes[3 * i][3 * j + 3].x) / 9.0; + nodes[3 * i + 2][3 * j + 2].x = + (-4.0 * nodes[3 * i + 3][3 * j + 3].x + + 6.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 2][3 * j + 3].x) + + -2.0 * (nodes[3 * i + 3][3 * j].x + nodes[3 * i][3 * j + 3].x) + + 3.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 2][3 * j].x) + + -1.0 * nodes[3 * i][3 * j].x) / 9.0; + + nodes[3 * i + 1][3 * j + 1].y = + (-4.0 * nodes[3 * i][3 * j].y + + 6.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 1][3 * j].y) + + -2.0 * (nodes[3 * i][3 * j + 3].y + nodes[3 * i + 3][3 * j].y) + + 3.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 1][3 * j + 3].y) + + -1.0 * nodes[3 * i + 3][3 * j + 3].y) / 9.0; + nodes[3 * i + 1][3 * j + 2].y = + (-4.0 * nodes[3 * i][3 * j + 3].y + + 6.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 1][3 * j + 3].y) + + -2.0 * (nodes[3 * i][3 * j].y + nodes[3 * i + 3][3 * j + 3].y) + + 3.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 1][3 * j].y) + + -1.0 * nodes[3 * i + 3][3 * j].y) / 9.0; + nodes[3 * i + 2][3 * j + 1].y = + (-4.0 * nodes[3 * i + 3][3 * j].y + + 6.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 2][3 * j].y) + + -2.0 * (nodes[3 * i + 3][3 * j + 3].y + nodes[3 * i][3 * j].y) + + 3.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 2][3 * j + 3].y) + + -1.0 * nodes[3 * i][3 * j + 3].y) / 9.0; + nodes[3 * i + 2][3 * j + 2].y = + (-4.0 * nodes[3 * i + 3][3 * j + 3].y + + 6.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 2][3 * j + 3].y) + + -2.0 * (nodes[3 * i + 3][3 * j].y + nodes[3 * i][3 * j + 3].y) + + 3.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 2][3 * j].y) + + -1.0 * nodes[3 * i][3 * j].y) / 9.0; + } + } + + this.nodes = nodes; // (m*3+1) x (n*3+1) points + this.colors = colors; // (m+1) x (n+1) x 4 colors (R+G+B+A) + } + + // Extracts out each patch and then paints it + paintMesh (v, w) { + let imax = (this.nodes.length - 1) / 3; + let jmax = (this.nodes[0].length - 1) / 3; + + if (this.type === 'bilinear' || imax < 2 || jmax < 2) { + let patch; + + for (let i = 0; i < imax; ++i) { + for (let j = 0; j < jmax; ++j) { + let sliceNodes = []; + for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) { + sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4)); + } + + let sliceColors = []; + sliceColors.push(this.colors[i].slice(j, j + 2)); + sliceColors.push(this.colors[i + 1].slice(j, j + 2)); + + patch = new Patch(sliceNodes, sliceColors); + patch.paint(v, w); + } + } + } else { + // Reference: + // https://en.wikipedia.org/wiki/Bicubic_interpolation#Computation + let d01, d12, patch, sliceNodes, nodes, f, alpha; + const ilast = imax; + const jlast = jmax; + imax++; + jmax++; + + /* + * d = the interpolation data + * d[i][j] = a node record (Point, color_array, color_dx, color_dy) + * d[i][j][0] : Point + * d[i][j][1] : [RGBA] + * d[i][j][2] = dx [RGBA] + * d[i][j][3] = dy [RGBA] + * d[i][j][][k] : color channel k + */ + let d = new Array(imax); + + // Setting the node and the colors + for (let i = 0; i < imax; ++i) { + d[i] = new Array(jmax); + for (let j = 0; j < jmax; ++j) { + d[i][j] = []; + d[i][j][0] = this.nodes[3 * i][3 * j]; + d[i][j][1] = this.colors[i][j]; + } + } + + // Calculate the inner derivatives + for (let i = 0; i < imax; ++i) { + for (let j = 0; j < jmax; ++j) { + // dx + if (i !== 0 && i !== ilast) { + d01 = distance(d[i - 1][j][0], d[i][j][0]); + d12 = distance(d[i + 1][j][0], d[i][j][0]); + d[i][j][2] = finiteDifferences(d[i - 1][j][1], d[i][j][1], + d[i + 1][j][1], d01, d12); + } + + // dy + if (j !== 0 && j !== jlast) { + d01 = distance(d[i][j - 1][0], d[i][j][0]); + d12 = distance(d[i][j + 1][0], d[i][j][0]); + d[i][j][3] = finiteDifferences(d[i][j - 1][1], d[i][j][1], + d[i][j + 1][1], d01, d12); + } + + // dxy is, by standard, set to 0 + } + } + + /* + * Calculate the exterior derivatives + * We fit the exterior derivatives onto parabolas generated by + * the point and the interior derivatives. + */ + for (let j = 0; j < jmax; ++j) { + d[0][j][2] = []; + d[ilast][j][2] = []; + + for (let k = 0; k < 4; ++k) { + d01 = distance(d[1][j][0], d[0][j][0]); + d12 = distance(d[ilast][j][0], d[ilast - 1][j][0]); + + if (d01 > 0) { + d[0][j][2][k] = 2.0 * (d[1][j][1][k] - d[0][j][1][k]) / d01 - + d[1][j][2][k]; + } else { + console.log(`0 was 0! (j: ${j}, k: ${k})`); + d[0][j][2][k] = 0; + } + + if (d12 > 0) { + d[ilast][j][2][k] = 2.0 * (d[ilast][j][1][k] - d[ilast - 1][j][1][k]) / + d12 - d[ilast - 1][j][2][k]; + } else { + console.log(`last was 0! (j: ${j}, k: ${k})`); + d[ilast][j][2][k] = 0; + } + } + } + + for (let i = 0; i < imax; ++i) { + d[i][0][3] = []; + d[i][jlast][3] = []; + + for (let k = 0; k < 4; ++k) { + d01 = distance(d[i][1][0], d[i][0][0]); + d12 = distance(d[i][jlast][0], d[i][jlast - 1][0]); + + if (d01 > 0) { + d[i][0][3][k] = 2.0 * (d[i][1][1][k] - d[i][0][1][k]) / d01 - + d[i][1][3][k]; + } else { + console.log(`0 was 0! (i: ${i}, k: ${k})`); + d[i][0][3][k] = 0; + } + + if (d12 > 0) { + d[i][jlast][3][k] = 2.0 * (d[i][jlast][1][k] - d[i][jlast - 1][1][k]) / + d12 - d[i][jlast - 1][3][k]; + } else { + console.log(`last was 0! (i: ${i}, k: ${k})`); + d[i][jlast][3][k] = 0; + } + } + } + + // Fill patches + for (let i = 0; i < ilast; ++i) { + for (let j = 0; j < jlast; ++j) { + let dLeft = distance(d[i][j][0], d[i + 1][j][0]); + let dRight = distance(d[i][j + 1][0], d[i + 1][j + 1][0]); + let dTop = distance(d[i][j][0], d[i][j + 1][0]); + let dBottom = distance(d[i + 1][j][0], d[i + 1][j + 1][0]); + let r = [[], [], [], []]; + + for (let k = 0; k < 4; ++k) { + f = []; + + f[0] = d[i][j][1][k]; + f[1] = d[i + 1][j][1][k]; + f[2] = d[i][j + 1][1][k]; + f[3] = d[i + 1][j + 1][1][k]; + f[4] = d[i][j][2][k] * dLeft; + f[5] = d[i + 1][j][2][k] * dLeft; + f[6] = d[i][j + 1][2][k] * dRight; + f[7] = d[i + 1][j + 1][2][k] * dRight; + f[8] = d[i][j][3][k] * dTop; + f[9] = d[i + 1][j][3][k] * dBottom; + f[10] = d[i][j + 1][3][k] * dTop; + f[11] = d[i + 1][j + 1][3][k] * dBottom; + f[12] = 0; // dxy + f[13] = 0; // dxy + f[14] = 0; // dxy + f[15] = 0; // dxy + + // get alpha values + alpha = solveLinearSystem(f); + + for (let l = 0; l < 9; ++l) { + r[k][l] = []; + + for (let m = 0; m < 9; ++m) { + // evaluation + r[k][l][m] = evaluateSolution(alpha, l / 8, m / 8); + + if (r[k][l][m] > 255) { + r[k][l][m] = 255; + } else if (r[k][l][m] < 0.0) { + r[k][l][m] = 0.0; + } + } + } + } + + // split the bezier patch into 8x8 patches + sliceNodes = []; + for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) { + sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4)); + } + + nodes = splitPatch(sliceNodes); + + // Create patches and paint the bilinearliy + for (let l = 0; l < 8; ++l) { + for (let m = 0; m < 8; ++m) { + patch = new Patch( + nodes[l][m], + [[ + [r[0][l][m], r[1][l][m], r[2][l][m], r[3][l][m]], + [r[0][l][m + 1], r[1][l][m + 1], r[2][l][m + 1], r[3][l][m + 1]] + ], [ + [r[0][l + 1][m], r[1][l + 1][m], r[2][l + 1][m], r[3][l + 1][m]], + [r[0][l + 1][m + 1], r[1][l + 1][m + 1], r[2][l + 1][m + 1], r[3][l + 1][m + 1]] + ]] + ); + + patch.paint(v, w); + } + } + } + } + } + } + + // Transforms mesh into coordinate space of canvas (t is either Point or Affine). + transform (t) { + if (t instanceof Point) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].add(t); + } + } + } else if (t instanceof Affine) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].transform(t); + } + } + } + } + + // Scale mesh into coordinate space of canvas (t is a Point). + scale (t) { + for (let i = 0, imax = this.nodes.length; i < imax; ++i) { + for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) { + this.nodes[i][j] = this.nodes[i][j].scale(t); + } + } + } + } + + // Start of document processing --------------------- + const shapes = document.querySelectorAll('rect,circle,ellipse,path,text'); + + shapes.forEach((shape, i) => { + // Get id. If no id, create one. + let shapeId = shape.getAttribute('id'); + if (!shapeId) { + shapeId = 'patchjs_shape' + i; + shape.setAttribute('id', shapeId); + } + + const fillURL = shape.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + const strokeURL = shape.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/); + + if (fillURL && fillURL[1]) { + const mesh = document.getElementById(fillURL[1]); + + if (mesh && mesh.nodeName === 'meshgradient') { + const bbox = shape.getBBox(); + + // Create temporary canvas + let myCanvas = document.createElementNS(xhtmlNS, 'canvas'); + setAttributes(myCanvas, { + 'width': bbox.width, + 'height': bbox.height + }); + + const myContext = myCanvas.getContext('2d'); + let myCanvasImage = myContext.createImageData(bbox.width, bbox.height); + + // Draw a mesh + const myMesh = new Mesh(mesh); + + // Adjust for bounding box if necessary. + if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') { + myMesh.scale(new Point(bbox.width, bbox.height)); + } + + // Apply gradient transform. + const gradientTransform = mesh.getAttribute('gradientTransform'); + if (gradientTransform != null) { + myMesh.transform(parseTransform(gradientTransform)); + } + + // Position to Canvas coordinate. + if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') { + myMesh.transform(new Point(-bbox.x, -bbox.y)); + } + + // Paint + myMesh.paintMesh(myCanvasImage.data, myCanvas.width); + myContext.putImageData(myCanvasImage, 0, 0); + + // Create image element of correct size + const myImage = document.createElementNS(svgNS, 'image'); + setAttributes(myImage, { + 'width': bbox.width, + 'height': bbox.height, + 'x': bbox.x, + 'y': bbox.y + }); + + // Set image to data url + let myPNG = myCanvas.toDataURL(); + myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG); + + // Insert image into document + shape.parentNode.insertBefore(myImage, shape); + shape.style.fill = 'none'; + + // Create clip referencing shape and insert into document + const use = document.createElementNS(svgNS, 'use'); + use.setAttributeNS(xlinkNS, 'xlink:href', '#' + shapeId); + + const clipId = 'patchjs_clip' + i; + const clip = document.createElementNS(svgNS, 'clipPath'); + clip.setAttribute('id', clipId); + clip.appendChild(use); + shape.parentElement.insertBefore(clip, shape); + myImage.setAttribute('clip-path', 'url(#' + clipId + ')'); + + // Force the Garbage Collector to free the space + myCanvasImage = null; + myCanvas = null; + myPNG = null; + } + } + + if (strokeURL && strokeURL[1]) { + const mesh = document.getElementById(strokeURL[1]); + + if (mesh && mesh.nodeName === 'meshgradient') { + const strokeWidth = parseFloat(shape.style.strokeWidth.slice(0, -2)); + const strokeMiterlimit = parseFloat(shape.style.strokeMiterlimit) || + parseFloat(shape.getAttribute('stroke-miterlimit')) || 1; + const phase = strokeWidth * strokeMiterlimit; + + const bbox = shape.getBBox(); + const boxWidth = Math.trunc(bbox.width + phase); + const boxHeight = Math.trunc(bbox.height + phase); + const boxX = Math.trunc(bbox.x - phase / 2); + const boxY = Math.trunc(bbox.y - phase / 2); + + // Create temporary canvas + let myCanvas = document.createElementNS(xhtmlNS, 'canvas'); + setAttributes(myCanvas, { + 'width': boxWidth, + 'height': boxHeight + }); + + const myContext = myCanvas.getContext('2d'); + let myCanvasImage = myContext.createImageData(boxWidth, boxHeight); + + // Draw a mesh + const myMesh = new Mesh(mesh); + + // Adjust for bounding box if necessary. + if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') { + myMesh.scale(new Point(boxWidth, boxHeight)); + } + + // Apply gradient transform. + const gradientTransform = mesh.getAttribute('gradientTransform'); + if (gradientTransform != null) { + myMesh.transform(parseTransform(gradientTransform)); + } + + // Position to Canvas coordinate. + if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') { + myMesh.transform(new Point(-boxX, -boxY)); + } + + // Paint + myMesh.paintMesh(myCanvasImage.data, myCanvas.width); + myContext.putImageData(myCanvasImage, 0, 0); + + // Create image element of correct size + const myImage = document.createElementNS(svgNS, 'image'); + setAttributes(myImage, { + 'width': boxWidth, + 'height': boxHeight, + 'x': 0, + 'y': 0 + }); + + // Set image to data url + let myPNG = myCanvas.toDataURL(); + myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG); + + // Create pattern to hold the stroke image + const patternId = 'pattern_clip' + i; + const myPattern = document.createElementNS(svgNS, 'pattern'); + setAttributes(myPattern, { + 'id': patternId, + 'patternUnits': 'userSpaceOnUse', + 'width': boxWidth, + 'height': boxHeight, + 'x': boxX, + 'y': boxY + }); + myPattern.appendChild(myImage); + + // Insert image into document + mesh.parentNode.appendChild(myPattern); + shape.style.stroke = 'url(#' + patternId + ')'; + + // Force the Garbage Collector to free the space + myCanvasImage = null; + myCanvas = null; + myPNG = null; + } + } + }); +})(); diff --git a/src/extension/internal/polyfill/mesh_compressed.include b/src/extension/internal/polyfill/mesh_compressed.include new file mode 100644 index 0000000..275be96 --- /dev/null +++ b/src/extension/internal/polyfill/mesh_compressed.include @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: CC0 +R"=====( +!function(){const t="http://www.w3.org/2000/svg",e="http://www.w3.org/1999/xlink",s="http://www.w3.org/1999/xhtml",r=2;if(document.createElementNS(t,"meshgradient").x)return;const n=(t,e,s,r)=>{let n=new x(.5*(e.x+s.x),.5*(e.y+s.y)),o=new x(.5*(t.x+e.x),.5*(t.y+e.y)),i=new x(.5*(s.x+r.x),.5*(s.y+r.y)),a=new x(.5*(n.x+o.x),.5*(n.y+o.y)),h=new x(.5*(n.x+i.x),.5*(n.y+i.y)),l=new x(.5*(a.x+h.x),.5*(a.y+h.y));return[[t,o,a,l],[l,h,i,r]]},o=t=>{let e=t[0].distSquared(t[1]),s=t[2].distSquared(t[3]),r=.25*t[0].distSquared(t[2]),n=.25*t[1].distSquared(t[3]),o=e>s?e:s,i=r>n?r:n;return 18*(o>i?o:i)},i=(t,e)=>Math.sqrt(t.distSquared(e)),a=(t,e)=>t.scale(2/3).add(e.scale(1/3)),h=t=>{let e,s,r,n,o,i,a,h=new g;return t.match(/(\w+\(\s*[^)]+\))+/g).forEach(t=>{let l=t.match(/[\w.-]+/g),d=l.shift();switch(d){case"translate":2===l.length?e=new g(1,0,0,1,l[0],l[1]):(console.error("mesh.js: translate does not have 2 arguments!"),e=new g(1,0,0,1,0,0)),h=h.append(e);break;case"scale":1===l.length?s=new g(l[0],0,0,l[0],0,0):2===l.length?s=new g(l[0],0,0,l[1],0,0):(console.error("mesh.js: scale does not have 1 or 2 arguments!"),s=new g(1,0,0,1,0,0)),h=h.append(s);break;case"rotate":if(3===l.length&&(e=new g(1,0,0,1,l[1],l[2]),h=h.append(e)),l[0]){r=l[0]*Math.PI/180;let t=Math.cos(r),e=Math.sin(r);Math.abs(t)<1e-16&&(t=0),Math.abs(e)<1e-16&&(e=0),a=new g(t,e,-e,t,0,0),h=h.append(a)}else console.error("math.js: No argument to rotate transform!");3===l.length&&(e=new g(1,0,0,1,-l[1],-l[2]),h=h.append(e));break;case"skewX":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),o=new g(1,0,n,1,0,0),h=h.append(o)):console.error("math.js: No argument to skewX transform!");break;case"skewY":l[0]?(r=l[0]*Math.PI/180,n=Math.tan(r),i=new g(1,n,0,1,0,0),h=h.append(i)):console.error("math.js: No argument to skewY transform!");break;case"matrix":6===l.length?h=h.append(new g(...l)):console.error("math.js: Incorrect number of arguments for matrix!");break;default:console.error("mesh.js: Unhandled transform type: "+d)}}),h},l=t=>{let e=[],s=t.split(/[ ,]+/);for(let t=0,r=s.length-1;t<r;t+=2)e.push(new x(parseFloat(s[t]),parseFloat(s[t+1])));return e},d=(t,e)=>{for(let s in e)t.setAttribute(s,e[s])},c=(t,e,s,r,n)=>{let o,i,a=[0,0,0,0];for(let h=0;h<3;++h)e[h]<t[h]&&e[h]<s[h]||t[h]<e[h]&&s[h]<e[h]?a[h]=0:(a[h]=.5*((e[h]-t[h])/r+(s[h]-e[h])/n),o=Math.abs(3*(e[h]-t[h])/r),i=Math.abs(3*(s[h]-e[h])/n),a[h]>o?a[h]=o:a[h]>i&&(a[h]=i));return a},u=[[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],[-3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0],[2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0],[0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0],[0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0],[-3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0],[0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0],[9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1],[-6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1],[2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0],[0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0],[-6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1],[4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1]],f=t=>{let e=[];for(let s=0;s<16;++s){e[s]=0;for(let r=0;r<16;++r)e[s]+=u[s][r]*t[r]}return e},p=(t,e,s)=>{const r=e*e,n=s*s,o=e*e*e,i=s*s*s;return t[0]+t[1]*e+t[2]*r+t[3]*o+t[4]*s+t[5]*s*e+t[6]*s*r+t[7]*s*o+t[8]*n+t[9]*n*e+t[10]*n*r+t[11]*n*o+t[12]*i+t[13]*i*e+t[14]*i*r+t[15]*i*o},y=t=>{let e=[],s=[],r=[];for(let s=0;s<4;++s)e[s]=[],e[s][0]=n(t[0][s],t[1][s],t[2][s],t[3][s]),e[s][1]=[],e[s][1].push(...n(...e[s][0][0])),e[s][1].push(...n(...e[s][0][1])),e[s][2]=[],e[s][2].push(...n(...e[s][1][0])),e[s][2].push(...n(...e[s][1][1])),e[s][2].push(...n(...e[s][1][2])),e[s][2].push(...n(...e[s][1][3]));for(let t=0;t<8;++t){s[t]=[];for(let r=0;r<4;++r)s[t][r]=[],s[t][r][0]=n(e[0][2][t][r],e[1][2][t][r],e[2][2][t][r],e[3][2][t][r]),s[t][r][1]=[],s[t][r][1].push(...n(...s[t][r][0][0])),s[t][r][1].push(...n(...s[t][r][0][1])),s[t][r][2]=[],s[t][r][2].push(...n(...s[t][r][1][0])),s[t][r][2].push(...n(...s[t][r][1][1])),s[t][r][2].push(...n(...s[t][r][1][2])),s[t][r][2].push(...n(...s[t][r][1][3]))}for(let t=0;t<8;++t){r[t]=[];for(let e=0;e<8;++e)r[t][e]=[],r[t][e][0]=s[t][0][2][e],r[t][e][1]=s[t][1][2][e],r[t][e][2]=s[t][2][2][e],r[t][e][3]=s[t][3][2][e]}return r};class x{constructor(t,e){this.x=t||0,this.y=e||0}toString(){return`(x=${this.x}, y=${this.y})`}clone(){return new x(this.x,this.y)}add(t){return new x(this.x+t.x,this.y+t.y)}scale(t){return void 0===t.x?new x(this.x*t,this.y*t):new x(this.x*t.x,this.y*t.y)}distSquared(t){let e=this.x-t.x,s=this.y-t.y;return e*e+s*s}transform(t){let e=this.x*t.a+this.y*t.c+t.e,s=this.x*t.b+this.y*t.d+t.f;return new x(e,s)}}class g{constructor(t,e,s,r,n,o){void 0===t?(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0):(this.a=t,this.b=e,this.c=s,this.d=r,this.e=n,this.f=o)}toString(){return`affine: ${this.a} ${this.c} ${this.e} \n ${this.b} ${this.d} ${this.f}`}append(t){t instanceof g||console.error("mesh.js: argument to Affine.append is not affine!");let e=this.a*t.a+this.c*t.b,s=this.b*t.a+this.d*t.b,r=this.a*t.c+this.c*t.d,n=this.b*t.c+this.d*t.d,o=this.a*t.e+this.c*t.f+this.e,i=this.b*t.e+this.d*t.f+this.f;return new g(e,s,r,n,o,i)}}class w{constructor(t,e){this.nodes=t,this.colors=e}paintCurve(t,e){if(o(this.nodes)>r){const s=n(...this.nodes);let r=[[],[]],o=[[],[]];for(let t=0;t<4;++t)r[0][t]=this.colors[0][t],r[1][t]=(this.colors[0][t]+this.colors[1][t])/2,o[0][t]=r[1][t],o[1][t]=this.colors[1][t];let i=new w(s[0],r),a=new w(s[1],o);i.paintCurve(t,e),a.paintCurve(t,e)}else{let s=Math.round(this.nodes[0].x);if(s>=0&&s<e){let r=4*(~~this.nodes[0].y*e+s);t[r]=Math.round(this.colors[0][0]),t[r+1]=Math.round(this.colors[0][1]),t[r+2]=Math.round(this.colors[0][2]),t[r+3]=Math.round(this.colors[0][3])}}}}class m{constructor(t,e){this.nodes=t,this.colors=e}split(){let t=[[],[],[],[]],e=[[],[],[],[]],s=[[[],[]],[[],[]]],r=[[[],[]],[[],[]]];for(let s=0;s<4;++s){const r=n(this.nodes[0][s],this.nodes[1][s],this.nodes[2][s],this.nodes[3][s]);t[0][s]=r[0][0],t[1][s]=r[0][1],t[2][s]=r[0][2],t[3][s]=r[0][3],e[0][s]=r[1][0],e[1][s]=r[1][1],e[2][s]=r[1][2],e[3][s]=r[1][3]}for(let t=0;t<4;++t)s[0][0][t]=this.colors[0][0][t],s[0][1][t]=this.colors[0][1][t],s[1][0][t]=(this.colors[0][0][t]+this.colors[1][0][t])/2,s[1][1][t]=(this.colors[0][1][t]+this.colors[1][1][t])/2,r[0][0][t]=s[1][0][t],r[0][1][t]=s[1][1][t],r[1][0][t]=this.colors[1][0][t],r[1][1][t]=this.colors[1][1][t];return[new m(t,s),new m(e,r)]}paint(t,e){let s,n=!1;for(let t=0;t<4;++t)if((s=o([this.nodes[0][t],this.nodes[1][t],this.nodes[2][t],this.nodes[3][t]]))>r){n=!0;break}if(n){let s=this.split();s[0].paint(t,e),s[1].paint(t,e)}else{new w([...this.nodes[0]],[...this.colors[0]]).paintCurve(t,e)}}}class b{constructor(t){this.readMesh(t),this.type=t.getAttribute("type")||"bilinear"}readMesh(t){let e=[[]],s=[[]],r=Number(t.getAttribute("x")),n=Number(t.getAttribute("y"));e[0][0]=new x(r,n);let o=t.children;for(let t=0,r=o.length;t<r;++t){e[3*t+1]=[],e[3*t+2]=[],e[3*t+3]=[],s[t+1]=[];let r=o[t].children;for(let n=0,o=r.length;n<o;++n){let o=r[n].children;for(let r=0,i=o.length;r<i;++r){let i=r;0!==t&&++i;let h,d=o[r].getAttribute("path"),c="l";null!=d&&(c=(h=d.match(/\s*([lLcC])\s*(.*)/))[1]);let u=l(h[2]);switch(c){case"l":0===i?(e[3*t][3*n+3]=u[0].add(e[3*t][3*n]),e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&&(e[3*t+3][3*n+0]=u[0].add(e[3*t+3][3*n+3])),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case"L":0===i?(e[3*t][3*n+3]=u[0],e[3*t][3*n+1]=a(e[3*t][3*n],e[3*t][3*n+3]),e[3*t][3*n+2]=a(e[3*t][3*n+3],e[3*t][3*n])):1===i?(e[3*t+3][3*n+3]=u[0],e[3*t+1][3*n+3]=a(e[3*t][3*n+3],e[3*t+3][3*n+3]),e[3*t+2][3*n+3]=a(e[3*t+3][3*n+3],e[3*t][3*n+3])):2===i?(0===n&&(e[3*t+3][3*n+0]=u[0]),e[3*t+3][3*n+1]=a(e[3*t+3][3*n],e[3*t+3][3*n+3]),e[3*t+3][3*n+2]=a(e[3*t+3][3*n+3],e[3*t+3][3*n])):(e[3*t+1][3*n]=a(e[3*t][3*n],e[3*t+3][3*n]),e[3*t+2][3*n]=a(e[3*t+3][3*n],e[3*t][3*n]));break;case"c":0===i?(e[3*t][3*n+1]=u[0].add(e[3*t][3*n]),e[3*t][3*n+2]=u[1].add(e[3*t][3*n]),e[3*t][3*n+3]=u[2].add(e[3*t][3*n])):1===i?(e[3*t+1][3*n+3]=u[0].add(e[3*t][3*n+3]),e[3*t+2][3*n+3]=u[1].add(e[3*t][3*n+3]),e[3*t+3][3*n+3]=u[2].add(e[3*t][3*n+3])):2===i?(e[3*t+3][3*n+2]=u[0].add(e[3*t+3][3*n+3]),e[3*t+3][3*n+1]=u[1].add(e[3*t+3][3*n+3]),0===n&&(e[3*t+3][3*n+0]=u[2].add(e[3*t+3][3*n+3]))):(e[3*t+2][3*n]=u[0].add(e[3*t+3][3*n]),e[3*t+1][3*n]=u[1].add(e[3*t+3][3*n]));break;case"C":0===i?(e[3*t][3*n+1]=u[0],e[3*t][3*n+2]=u[1],e[3*t][3*n+3]=u[2]):1===i?(e[3*t+1][3*n+3]=u[0],e[3*t+2][3*n+3]=u[1],e[3*t+3][3*n+3]=u[2]):2===i?(e[3*t+3][3*n+2]=u[0],e[3*t+3][3*n+1]=u[1],0===n&&(e[3*t+3][3*n+0]=u[2])):(e[3*t+2][3*n]=u[0],e[3*t+1][3*n]=u[1]);break;default:console.error("mesh.js: "+c+" invalid path type.")}if(0===t&&0===n||r>0){let e=window.getComputedStyle(o[r]).stopColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i),a=window.getComputedStyle(o[r]).stopOpacity,h=255;a&&(h=Math.floor(255*a)),e&&(0===i?(s[t][n]=[],s[t][n][0]=Math.floor(e[1]),s[t][n][1]=Math.floor(e[2]),s[t][n][2]=Math.floor(e[3]),s[t][n][3]=h):1===i?(s[t][n+1]=[],s[t][n+1][0]=Math.floor(e[1]),s[t][n+1][1]=Math.floor(e[2]),s[t][n+1][2]=Math.floor(e[3]),s[t][n+1][3]=h):2===i?(s[t+1][n+1]=[],s[t+1][n+1][0]=Math.floor(e[1]),s[t+1][n+1][1]=Math.floor(e[2]),s[t+1][n+1][2]=Math.floor(e[3]),s[t+1][n+1][3]=h):3===i&&(s[t+1][n]=[],s[t+1][n][0]=Math.floor(e[1]),s[t+1][n][1]=Math.floor(e[2]),s[t+1][n][2]=Math.floor(e[3]),s[t+1][n][3]=h))}}e[3*t+1][3*n+1]=new x,e[3*t+1][3*n+2]=new x,e[3*t+2][3*n+1]=new x,e[3*t+2][3*n+2]=new x,e[3*t+1][3*n+1].x=(-4*e[3*t][3*n].x+6*(e[3*t][3*n+1].x+e[3*t+1][3*n].x)+-2*(e[3*t][3*n+3].x+e[3*t+3][3*n].x)+3*(e[3*t+3][3*n+1].x+e[3*t+1][3*n+3].x)+-1*e[3*t+3][3*n+3].x)/9,e[3*t+1][3*n+2].x=(-4*e[3*t][3*n+3].x+6*(e[3*t][3*n+2].x+e[3*t+1][3*n+3].x)+-2*(e[3*t][3*n].x+e[3*t+3][3*n+3].x)+3*(e[3*t+3][3*n+2].x+e[3*t+1][3*n].x)+-1*e[3*t+3][3*n].x)/9,e[3*t+2][3*n+1].x=(-4*e[3*t+3][3*n].x+6*(e[3*t+3][3*n+1].x+e[3*t+2][3*n].x)+-2*(e[3*t+3][3*n+3].x+e[3*t][3*n].x)+3*(e[3*t][3*n+1].x+e[3*t+2][3*n+3].x)+-1*e[3*t][3*n+3].x)/9,e[3*t+2][3*n+2].x=(-4*e[3*t+3][3*n+3].x+6*(e[3*t+3][3*n+2].x+e[3*t+2][3*n+3].x)+-2*(e[3*t+3][3*n].x+e[3*t][3*n+3].x)+3*(e[3*t][3*n+2].x+e[3*t+2][3*n].x)+-1*e[3*t][3*n].x)/9,e[3*t+1][3*n+1].y=(-4*e[3*t][3*n].y+6*(e[3*t][3*n+1].y+e[3*t+1][3*n].y)+-2*(e[3*t][3*n+3].y+e[3*t+3][3*n].y)+3*(e[3*t+3][3*n+1].y+e[3*t+1][3*n+3].y)+-1*e[3*t+3][3*n+3].y)/9,e[3*t+1][3*n+2].y=(-4*e[3*t][3*n+3].y+6*(e[3*t][3*n+2].y+e[3*t+1][3*n+3].y)+-2*(e[3*t][3*n].y+e[3*t+3][3*n+3].y)+3*(e[3*t+3][3*n+2].y+e[3*t+1][3*n].y)+-1*e[3*t+3][3*n].y)/9,e[3*t+2][3*n+1].y=(-4*e[3*t+3][3*n].y+6*(e[3*t+3][3*n+1].y+e[3*t+2][3*n].y)+-2*(e[3*t+3][3*n+3].y+e[3*t][3*n].y)+3*(e[3*t][3*n+1].y+e[3*t+2][3*n+3].y)+-1*e[3*t][3*n+3].y)/9,e[3*t+2][3*n+2].y=(-4*e[3*t+3][3*n+3].y+6*(e[3*t+3][3*n+2].y+e[3*t+2][3*n+3].y)+-2*(e[3*t+3][3*n].y+e[3*t][3*n+3].y)+3*(e[3*t][3*n+2].y+e[3*t+2][3*n].y)+-1*e[3*t][3*n].y)/9}}this.nodes=e,this.colors=s}paintMesh(t,e){let s=(this.nodes.length-1)/3,r=(this.nodes[0].length-1)/3;if("bilinear"===this.type||s<2||r<2){let n;for(let o=0;o<s;++o)for(let s=0;s<r;++s){let r=[];for(let t=3*o,e=3*o+4;t<e;++t)r.push(this.nodes[t].slice(3*s,3*s+4));let i=[];i.push(this.colors[o].slice(s,s+2)),i.push(this.colors[o+1].slice(s,s+2)),(n=new m(r,i)).paint(t,e)}}else{let n,o,a,h,l,d,u;const x=s,g=r;s++,r++;let w=new Array(s);for(let t=0;t<s;++t){w[t]=new Array(r);for(let e=0;e<r;++e)w[t][e]=[],w[t][e][0]=this.nodes[3*t][3*e],w[t][e][1]=this.colors[t][e]}for(let t=0;t<s;++t)for(let e=0;e<r;++e)0!==t&&t!==x&&(n=i(w[t-1][e][0],w[t][e][0]),o=i(w[t+1][e][0],w[t][e][0]),w[t][e][2]=c(w[t-1][e][1],w[t][e][1],w[t+1][e][1],n,o)),0!==e&&e!==g&&(n=i(w[t][e-1][0],w[t][e][0]),o=i(w[t][e+1][0],w[t][e][0]),w[t][e][3]=c(w[t][e-1][1],w[t][e][1],w[t][e+1][1],n,o));for(let t=0;t<r;++t){w[0][t][2]=[],w[x][t][2]=[];for(let e=0;e<4;++e)n=i(w[1][t][0],w[0][t][0]),o=i(w[x][t][0],w[x-1][t][0]),w[0][t][2][e]=n>0?2*(w[1][t][1][e]-w[0][t][1][e])/n-w[1][t][2][e]:0,w[x][t][2][e]=o>0?2*(w[x][t][1][e]-w[x-1][t][1][e])/o-w[x-1][t][2][e]:0}for(let t=0;t<s;++t){w[t][0][3]=[],w[t][g][3]=[];for(let e=0;e<4;++e)n=i(w[t][1][0],w[t][0][0]),o=i(w[t][g][0],w[t][g-1][0]),w[t][0][3][e]=n>0?2*(w[t][1][1][e]-w[t][0][1][e])/n-w[t][1][3][e]:0,w[t][g][3][e]=o>0?2*(w[t][g][1][e]-w[t][g-1][1][e])/o-w[t][g-1][3][e]:0}for(let s=0;s<x;++s)for(let r=0;r<g;++r){let n=i(w[s][r][0],w[s+1][r][0]),o=i(w[s][r+1][0],w[s+1][r+1][0]),c=i(w[s][r][0],w[s][r+1][0]),x=i(w[s+1][r][0],w[s+1][r+1][0]),g=[[],[],[],[]];for(let t=0;t<4;++t){(d=[])[0]=w[s][r][1][t],d[1]=w[s+1][r][1][t],d[2]=w[s][r+1][1][t],d[3]=w[s+1][r+1][1][t],d[4]=w[s][r][2][t]*n,d[5]=w[s+1][r][2][t]*n,d[6]=w[s][r+1][2][t]*o,d[7]=w[s+1][r+1][2][t]*o,d[8]=w[s][r][3][t]*c,d[9]=w[s+1][r][3][t]*x,d[10]=w[s][r+1][3][t]*c,d[11]=w[s+1][r+1][3][t]*x,d[12]=0,d[13]=0,d[14]=0,d[15]=0,u=f(d);for(let e=0;e<9;++e){g[t][e]=[];for(let s=0;s<9;++s)g[t][e][s]=p(u,e/8,s/8),g[t][e][s]>255?g[t][e][s]=255:g[t][e][s]<0&&(g[t][e][s]=0)}}h=[];for(let t=3*s,e=3*s+4;t<e;++t)h.push(this.nodes[t].slice(3*r,3*r+4));l=y(h);for(let s=0;s<8;++s)for(let r=0;r<8;++r)(a=new m(l[s][r],[[[g[0][s][r],g[1][s][r],g[2][s][r],g[3][s][r]],[g[0][s][r+1],g[1][s][r+1],g[2][s][r+1],g[3][s][r+1]]],[[g[0][s+1][r],g[1][s+1][r],g[2][s+1][r],g[3][s+1][r]],[g[0][s+1][r+1],g[1][s+1][r+1],g[2][s+1][r+1],g[3][s+1][r+1]]]])).paint(t,e)}}}transform(t){if(t instanceof x)for(let e=0,s=this.nodes.length;e<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].add(t);else if(t instanceof g)for(let e=0,s=this.nodes.length;e<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].transform(t)}scale(t){for(let e=0,s=this.nodes.length;e<s;++e)for(let s=0,r=this.nodes[0].length;s<r;++s)this.nodes[e][s]=this.nodes[e][s].scale(t)}}document.querySelectorAll("rect,circle,ellipse,path,text").forEach((r,n)=>{let o=r.getAttribute("id");o||(o="patchjs_shape"+n,r.setAttribute("id",o));const i=r.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/),a=r.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);if(i&&i[1]){const a=document.getElementById(i[1]);if(a&&"meshgradient"===a.nodeName){const i=r.getBBox();let l=document.createElementNS(s,"canvas");d(l,{width:i.width,height:i.height});const c=l.getContext("2d");let u=c.createImageData(i.width,i.height);const f=new b(a);"objectBoundingBox"===a.getAttribute("gradientUnits")&&f.scale(new x(i.width,i.height));const p=a.getAttribute("gradientTransform");null!=p&&f.transform(h(p)),"userSpaceOnUse"===a.getAttribute("gradientUnits")&&f.transform(new x(-i.x,-i.y)),f.paintMesh(u.data,l.width),c.putImageData(u,0,0);const y=document.createElementNS(t,"image");d(y,{width:i.width,height:i.height,x:i.x,y:i.y});let g=l.toDataURL();y.setAttributeNS(e,"xlink:href",g),r.parentNode.insertBefore(y,r),r.style.fill="none";const w=document.createElementNS(t,"use");w.setAttributeNS(e,"xlink:href","#"+o);const m="patchjs_clip"+n,M=document.createElementNS(t,"clipPath");M.setAttribute("id",m),M.appendChild(w),r.parentElement.insertBefore(M,r),y.setAttribute("clip-path","url(#"+m+")"),u=null,l=null,g=null}}if(a&&a[1]){const o=document.getElementById(a[1]);if(o&&"meshgradient"===o.nodeName){const i=parseFloat(r.style.strokeWidth.slice(0,-2))*(parseFloat(r.style.strokeMiterlimit)||parseFloat(r.getAttribute("stroke-miterlimit"))||1),a=r.getBBox(),l=Math.trunc(a.width+i),c=Math.trunc(a.height+i),u=Math.trunc(a.x-i/2),f=Math.trunc(a.y-i/2);let p=document.createElementNS(s,"canvas");d(p,{width:l,height:c});const y=p.getContext("2d");let g=y.createImageData(l,c);const w=new b(o);"objectBoundingBox"===o.getAttribute("gradientUnits")&&w.scale(new x(l,c));const m=o.getAttribute("gradientTransform");null!=m&&w.transform(h(m)),"userSpaceOnUse"===o.getAttribute("gradientUnits")&&w.transform(new x(-u,-f)),w.paintMesh(g.data,p.width),y.putImageData(g,0,0);const M=document.createElementNS(t,"image");d(M,{width:l,height:c,x:0,y:0});let S=p.toDataURL();M.setAttributeNS(e,"xlink:href",S);const k="pattern_clip"+n,A=document.createElementNS(t,"pattern");d(A,{id:k,patternUnits:"userSpaceOnUse",width:l,height:c,x:u,y:f}),A.appendChild(M),o.parentNode.appendChild(A),r.style.stroke="url(#"+k+")",g=null,p=null,S=null}}})}(); +)=====" diff --git a/src/extension/internal/pov-out.cpp b/src/extension/internal/pov-out.cpp new file mode 100644 index 0000000..96daafb --- /dev/null +++ b/src/extension/internal/pov-out.cpp @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple utility for exporting Inkscape svg Shapes as PovRay bezier + * prisms. Note that this is output-only, and would thus seem to be + * better placed as an 'export' rather than 'output'. However, Export + * handles all or partial documents, while this outputs ALL shapes in + * the current SVG document. + * + * For information on the PovRay file format, see: + * http://www.povray.org + * + * Authors: + * Bob Jamison <ishmal@inkscape.org> + * Abhishek Sharma + * + * Copyright (C) 2004-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "pov-out.h" +#include <inkscape.h> +#include <inkscape-version.h> +#include <display/curve.h> +#include <extension/system.h> +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include <io/sys.h> + +#include "object/sp-root.h" +#include "object/sp-path.h" +#include "style.h" + +#include <string> +#include <cstdio> +#include <cstdarg> +#include "document.h" +#include "extension/extension.h" + + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + + +//######################################################################## +//# M E S S A G E S +//######################################################################## + +static void err(const char *fmt, ...) +{ + va_list args; + g_log(nullptr, G_LOG_LEVEL_WARNING, "Pov-out err: "); + va_start(args, fmt); + g_logv(nullptr, G_LOG_LEVEL_WARNING, fmt, args); + va_end(args); + g_log(nullptr, G_LOG_LEVEL_WARNING, "\n"); +} + + + + +//######################################################################## +//# U T I L I T Y +//######################################################################## + + + +static double effective_opacity(SPItem const *item) +{ + // TODO investigate this. The early return seems that it would abort early. + // Plus is will emit a warning, which may not be proper here. + double ret = 1.0; + for (SPObject const *obj = item; obj; obj = obj->parent) { + g_return_val_if_fail(obj->style, ret); + ret *= SP_SCALE24_TO_FLOAT(obj->style->opacity.value); + } + return ret; +} + + + + + +//######################################################################## +//# OUTPUT FORMATTING +//######################################################################## + +PovOutput::PovOutput() : + outbuf (), + nrNodes (0), + nrSegments (0), + nrShapes (0), + idIndex (0), + minx (0), + miny (0), + maxx (0), + maxy (0) +{ +} + +/** + * We want to control floating output format + */ +static PovOutput::String dstr(double d) +{ + char dbuf[G_ASCII_DTOSTR_BUF_SIZE+1]; + g_ascii_formatd(dbuf, G_ASCII_DTOSTR_BUF_SIZE, + "%.8f", (gdouble)d); + PovOutput::String s = dbuf; + return s; +} + +#define DSTR(d) (dstr(d).c_str()) + + +/** + * Output data to the buffer, printf()-style + */ +void PovOutput::out(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + gchar *output = g_strdup_vprintf(fmt, args); + va_end(args); + outbuf.append(output); + g_free(output); +} + + + + + +/** + * Output a 2d vector + */ +void PovOutput::vec2(double a, double b) +{ + out("<%s, %s>", DSTR(a), DSTR(b)); +} + + + +/** + * Output a 3d vector + */ +void PovOutput::vec3(double a, double b, double c) +{ + out("<%s, %s, %s>", DSTR(a), DSTR(b), DSTR(c)); +} + + + +/** + * Output a v4d ector + */ +void PovOutput::vec4(double a, double b, double c, double d) +{ + out("<%s, %s, %s, %s>", DSTR(a), DSTR(b), DSTR(c), DSTR(d)); +} + + + +/** + * Output an rgbf color vector + */ +void PovOutput::rgbf(double r, double g, double b, double f) +{ + //"rgbf < %1.3f, %1.3f, %1.3f %1.3f>" + out("rgbf "); + vec4(r, g, b, f); +} + + + +/** + * Output one bezier's start, start-control, end-control, and end nodes + */ +void PovOutput::segment(int segNr, + double startX, double startY, + double startCtrlX, double startCtrlY, + double endCtrlX, double endCtrlY, + double endX, double endY) +{ + //" /*%4d*/ <%f, %f>, <%f, %f>, <%f,%f>, <%f,%f>" + out(" /*%4d*/ ", segNr); + vec2(startX, startY); + out(", "); + vec2(startCtrlX, startCtrlY); + out(", "); + vec2(endCtrlX, endCtrlY); + out(", "); + vec2(endX, endY); +} + + + + + +/** + * Output the file header + */ +bool PovOutput::doHeader() +{ + time_t tim = time(nullptr); + out("/*###################################################################\n"); + out("### This PovRay document was generated by Inkscape\n"); + out("### http://www.inkscape.org\n"); + out("### Created: %s", ctime(&tim)); + out("### Version: %s\n", Inkscape::version_string); + out("#####################################################################\n"); + out("### NOTES:\n"); + out("### ============\n"); + out("### POVRay information can be found at\n"); + out("### http://www.povray.org\n"); + out("###\n"); + out("### The 'AllShapes' objects at the bottom are provided as a\n"); + out("### preview of how the output would look in a trace. However,\n"); + out("### the main intent of this file is to provide the individual\n"); + out("### shapes for inclusion in a POV project.\n"); + out("###\n"); + out("### For an example of how to use this file, look at\n"); + out("### share/examples/istest.pov\n"); + out("###\n"); + out("### If you have any problems with this output, please see the\n"); + out("### Inkscape project at http://www.inkscape.org, or visit\n"); + out("### the #inkscape channel on irc.freenode.net . \n"); + out("###\n"); + out("###################################################################*/\n"); + out("\n\n"); + out("/*###################################################################\n"); + out("## Exports in this file\n"); + out("##==========================\n"); + out("## Shapes : %d\n", nrShapes); + out("## Segments : %d\n", nrSegments); + out("## Nodes : %d\n", nrNodes); + out("###################################################################*/\n"); + out("\n\n\n"); + return true; +} + + + +/** + * Output the file footer + */ +bool PovOutput::doTail() +{ + out("\n\n"); + out("/*###################################################################\n"); + out("### E N D F I L E\n"); + out("###################################################################*/\n"); + out("\n\n"); + return true; +} + + + +/** + * Output the curve data to buffer + */ +bool PovOutput::doCurve(SPItem *item, const String &id) +{ + using Geom::X; + using Geom::Y; + + //### Get the Shape + if (!SP_IS_SHAPE(item))//Bulia's suggestion. Allow all shapes + return true; + + SPShape *shape = SP_SHAPE(item); + if (shape->curve()->is_empty()) { + return true; + } + + nrShapes++; + + PovShapeInfo shapeInfo; + shapeInfo.id = id; + shapeInfo.color = ""; + + //Try to get the fill color of the shape + SPStyle *style = shape->style; + /* fixme: Handle other fill types, even if this means translating gradients to a single + flat colour. */ + if (style) + { + if (style->fill.isColor()) + { + // see color.h for how to parse SPColor + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + double const dopacity = ( SP_SCALE24_TO_FLOAT(style->fill_opacity.value) + * effective_opacity(shape) ); + //gchar *str = g_strdup_printf("rgbf < %1.3f, %1.3f, %1.3f %1.3f>", + // rgb[0], rgb[1], rgb[2], 1.0 - dopacity); + String rgbf = "rgbf <"; + rgbf.append(dstr(rgb[0])); rgbf.append(", "); + rgbf.append(dstr(rgb[1])); rgbf.append(", "); + rgbf.append(dstr(rgb[2])); rgbf.append(", "); + rgbf.append(dstr(1.0 - dopacity)); rgbf.append(">"); + shapeInfo.color += rgbf; + } + } + + povShapes.push_back(shapeInfo); //passed all tests. save the info + + // convert the path to only lineto's and cubic curveto's: + Geom::Affine tf = item->i2dt_affine(); + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(shape->curve()->get_pathvector() * tf); + + /* + * We need to know the number of segments (NR_CURVETOs/LINETOs, including + * closing line segment) before we write out segment data. Since we are + * going to skip degenerate (zero length) paths, we need to loop over all + * subpaths and segments first. + */ + int segmentCount = 0; + /** + * For all Subpaths in the <path> + */ + for (const auto & pit : pathv) + { + /** + * For all segments in the subpath, including extra closing segment defined by 2geom + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) + { + + // Skip zero length segments. + if( !cit->isDegenerate() ) ++segmentCount; + } + } + + out("/*###################################################\n"); + out("### PRISM: %s\n", id.c_str()); + out("###################################################*/\n"); + out("#declare %s = prism {\n", id.c_str()); + out(" linear_sweep\n"); + out(" bezier_spline\n"); + out(" 1.0, //top\n"); + out(" 0.0, //bottom\n"); + out(" %d //nr points\n", segmentCount * 4); + int segmentNr = 0; + + nrSegments += segmentCount; + + /** + * at moment of writing, 2geom lacks proper initialization of empty intervals in rect... + */ + Geom::Rect cminmax( pathv.front().initialPoint(), pathv.front().initialPoint() ); + + + /** + * For all Subpaths in the <path> + */ + for (const auto & pit : pathv) + { + + cminmax.expandTo(pit.initialPoint()); + + /** + * For all segments in the subpath, including extra closing segment defined by 2geom + */ + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_closed(); ++cit) + { + + // Skip zero length segments + if( cit->isDegenerate() ) + continue; + + if( is_straight_curve(*cit) ) + { + Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + segment(segmentNr++, + p0[X], p0[Y], p0[X], p0[Y], p1[X], p1[Y], p1[X], p1[Y] ); + nrNodes += 8; + } + else if(Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const*>(&*cit)) + { + std::vector<Geom::Point> points = cubic->controlPoints(); + Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + segment(segmentNr++, + p0[X],p0[Y], p1[X],p1[Y], p2[X],p2[Y], p3[X],p3[Y]); + nrNodes += 8; + } + else + { + err("logical error, because pathv_to_linear_and_cubic_beziers was used"); + return false; + } + + if (segmentNr < segmentCount) + out(",\n"); + else + out("\n"); + if (segmentNr > segmentCount) + { + err("Too many segments"); + return false; + } + + cminmax.expandTo(cit->finalPoint()); + + } + } + + out("}\n"); + + double cminx = cminmax.min()[X]; + double cmaxx = cminmax.max()[X]; + double cminy = cminmax.min()[Y]; + double cmaxy = cminmax.max()[Y]; + + out("#declare %s_MIN_X = %s;\n", id.c_str(), DSTR(cminx)); + out("#declare %s_CENTER_X = %s;\n", id.c_str(), DSTR((cmaxx+cminx)/2.0)); + out("#declare %s_MAX_X = %s;\n", id.c_str(), DSTR(cmaxx)); + out("#declare %s_WIDTH = %s;\n", id.c_str(), DSTR(cmaxx-cminx)); + out("#declare %s_MIN_Y = %s;\n", id.c_str(), DSTR(cminy)); + out("#declare %s_CENTER_Y = %s;\n", id.c_str(), DSTR((cmaxy+cminy)/2.0)); + out("#declare %s_MAX_Y = %s;\n", id.c_str(), DSTR(cmaxy)); + out("#declare %s_HEIGHT = %s;\n", id.c_str(), DSTR(cmaxy-cminy)); + if (shapeInfo.color.length()>0) + out("#declare %s_COLOR = %s;\n", + id.c_str(), shapeInfo.color.c_str()); + out("/*###################################################\n"); + out("### end %s\n", id.c_str()); + out("###################################################*/\n\n\n\n"); + + if (cminx < minx) + minx = cminx; + if (cmaxx > maxx) + maxx = cmaxx; + if (cminy < miny) + miny = cminy; + if (cmaxy > maxy) + maxy = cmaxy; + + return true; +} + +/** + * Descend the svg tree recursively, translating data + */ +bool PovOutput::doTreeRecursive(SPDocument *doc, SPObject *obj) +{ + + String id; + if (!obj->getId()) + { + char buf[16]; + sprintf(buf, "id%d", idIndex++); + id = buf; + } + else + { + id = obj->getId(); + } + + if (SP_IS_ITEM(obj)) + { + SPItem *item = SP_ITEM(obj); + if (!doCurve(item, id)) + return false; + } + + /** + * Descend into children + */ + for (auto &child: obj->children) + { + if (!doTreeRecursive(doc, &child)) + return false; + } + + return true; +} + +/** + * Output the curve data to buffer + */ +bool PovOutput::doTree(SPDocument *doc) +{ + double bignum = 1000000.0; + minx = bignum; + maxx = -bignum; + miny = bignum; + maxy = -bignum; + + if (!doTreeRecursive(doc, doc->getRoot())) + return false; + + //## Let's make a union of all of the Shapes + if (!povShapes.empty()) + { + String id = "AllShapes"; + char *pfx = (char *)id.c_str(); + out("/*###################################################\n"); + out("### UNION OF ALL SHAPES IN DOCUMENT\n"); + out("###################################################*/\n"); + out("\n\n"); + out("/**\n"); + out(" * Allow the user to redefine the finish{}\n"); + out(" * by declaring it before #including this file\n"); + out(" */\n"); + out("#ifndef (%s_Finish)\n", pfx); + out("#declare %s_Finish = finish {\n", pfx); + out(" phong 0.5\n"); + out(" reflection 0.3\n"); + out(" specular 0.5\n"); + out("}\n"); + out("#end\n"); + out("\n\n"); + out("#declare %s = union {\n", id.c_str()); + for (auto & povShape : povShapes) + { + out(" object { %s\n", povShape.id.c_str()); + out(" texture { \n"); + if (povShape.color.length()>0) + out(" pigment { %s }\n", povShape.color.c_str()); + else + out(" pigment { rgb <0,0,0> }\n"); + out(" finish { %s_Finish }\n", pfx); + out(" } \n"); + out(" } \n"); + } + out("}\n\n\n\n"); + + + double zinc = 0.2 / (double)povShapes.size(); + out("/*#### Same union, but with Z-diffs (actually Y in pov) ####*/\n"); + out("\n\n"); + out("/**\n"); + out(" * Allow the user to redefine the Z-Increment\n"); + out(" */\n"); + out("#ifndef (AllShapes_Z_Increment)\n"); + out("#declare AllShapes_Z_Increment = %s;\n", DSTR(zinc)); + out("#end\n"); + out("\n"); + out("#declare AllShapes_Z_Scale = 1.0;\n"); + out("\n\n"); + out("#declare %s_Z = union {\n", pfx); + + for (auto & povShape : povShapes) + { + out(" object { %s\n", povShape.id.c_str()); + out(" texture { \n"); + if (povShape.color.length()>0) + out(" pigment { %s }\n", povShape.color.c_str()); + else + out(" pigment { rgb <0,0,0> }\n"); + out(" finish { %s_Finish }\n", pfx); + out(" } \n"); + out(" scale <1, %s_Z_Scale, 1>\n", pfx); + out(" } \n"); + out("#declare %s_Z_Scale = %s_Z_Scale + %s_Z_Increment;\n\n", + pfx, pfx, pfx); + } + + out("}\n"); + + out("#declare %s_MIN_X = %s;\n", pfx, DSTR(minx)); + out("#declare %s_CENTER_X = %s;\n", pfx, DSTR((maxx+minx)/2.0)); + out("#declare %s_MAX_X = %s;\n", pfx, DSTR(maxx)); + out("#declare %s_WIDTH = %s;\n", pfx, DSTR(maxx-minx)); + out("#declare %s_MIN_Y = %s;\n", pfx, DSTR(miny)); + out("#declare %s_CENTER_Y = %s;\n", pfx, DSTR((maxy+miny)/2.0)); + out("#declare %s_MAX_Y = %s;\n", pfx, DSTR(maxy)); + out("#declare %s_HEIGHT = %s;\n", pfx, DSTR(maxy-miny)); + out("/*##############################################\n"); + out("### end %s\n", id.c_str()); + out("##############################################*/\n"); + out("\n\n"); + } + + return true; +} + + +//######################################################################## +//# M A I N O U T P U T +//######################################################################## + + + +/** + * Set values back to initial state + */ +void PovOutput::reset() +{ + nrNodes = 0; + nrSegments = 0; + nrShapes = 0; + idIndex = 0; + outbuf.clear(); + povShapes.clear(); +} + + + +/** + * Saves the Shapes of an Inkscape SVG file as PovRay spline definitions + */ +void PovOutput::saveDocument(SPDocument *doc, gchar const *filename_utf8) +{ + reset(); + + //###### SAVE IN POV FORMAT TO BUFFER + //# Lets do the curves first, to get the stats + if (!doTree(doc)) + { + err("Could not output curves for %s", filename_utf8); + return; + } + + String curveBuf = outbuf; + outbuf.clear(); + + if (!doHeader()) + { + err("Could not write header for %s", filename_utf8); + return; + } + + outbuf.append(curveBuf); + + if (!doTail()) + { + err("Could not write footer for %s", filename_utf8); + return; + } + + + + + //###### WRITE TO FILE + Inkscape::IO::dump_fopen_call(filename_utf8, "L"); + FILE *f = Inkscape::IO::fopen_utf8name(filename_utf8, "w"); + if (!f) + return; + + for (String::iterator iter = outbuf.begin() ; iter!=outbuf.end(); ++iter) + { + int ch = *iter; + fputc(ch, f); + } + + fclose(f); +} + + + + +//######################################################################## +//# EXTENSION API +//######################################################################## + + + +#include "clear-n_.h" + + + +/** + * API call to save document +*/ +void +PovOutput::save(Inkscape::Extension::Output */*mod*/, + SPDocument *doc, gchar const *filename_utf8) +{ + /* See comments in JavaFSOutput::save re the name `filename_utf8'. */ + saveDocument(doc, filename_utf8); +} + + + +/** + * Make sure that we are in the database + */ +bool PovOutput::check (Inkscape::Extension::Extension */*module*/) +{ + /* We don't need a Key + if (NULL == Inkscape::Extension::db.get(SP_MODULE_KEY_OUTPUT_POV)) + return FALSE; + */ + + return true; +} + + + +/** + * This is the definition of PovRay output. This function just + * calls the extension system with the memory allocated XML that + * describes the data. +*/ +void +PovOutput::init() +{ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("PovRay Output") "</name>\n" + "<id>org.inkscape.output.pov</id>\n" + "<output>\n" + "<extension>.pov</extension>\n" + "<mimetype>text/x-povray-script</mimetype>\n" + "<filetypename>" N_("PovRay (*.pov) (paths and shapes only)") "</filetypename>\n" + "<filetypetooltip>" N_("PovRay Raytracer File") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", + new PovOutput()); + // clang-format on +} + + + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/pov-out.h b/src/extension/internal/pov-out.h new file mode 100644 index 0000000..3dee88b --- /dev/null +++ b/src/extension/internal/pov-out.h @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * A simple utility for exporting Inkscape svg Shapes as PovRay bezier + * prisms. Note that this is output-only, and would thus seem to be + * better placed as an 'export' rather than 'output'. However, Export + * handles all or partial documents, while this outputs ALL shapes in + * the current SVG document. + * + * Authors: + * Bob Jamison <ishmal@inkscape.org> + * + * Copyright (C) 2004-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef EXTENSION_INTERNAL_POV_OUT_H +#define EXTENSION_INTERNAL_POV_OUT_H + +#include <glib.h> +#include "extension/implementation/implementation.h" + +class SPObject; +class SPItem; + +namespace Inkscape +{ +namespace Extension +{ +namespace Internal +{ + + + +/** + * Output bezier splines in POVRay format. + * + * For information, @see: + * http://www.povray.org + */ +class PovOutput : public Inkscape::Extension::Implementation::Implementation +{ + + +public: + + PovOutput(); + + /** + * Our internal String definition + */ + typedef Glib::ustring String; + + + /** + * Check whether we can actually output using this module + */ + bool check (Inkscape::Extension::Extension * module) override; + + /** + * API call to perform the output to a file + */ + void save(Inkscape::Extension::Output *mod, + SPDocument *doc, gchar const *filename) override; + + /** + * Inkscape runtime startup call. + */ + static void init(); + + /** + * Reset variables to initial state + */ + void reset(); + +private: + + /** + * Format text to our output buffer + */ + void out(const char *fmt, ...) G_GNUC_PRINTF(2,3); + + /** + * Output a 2d vector + */ + void vec2(double a, double b); + + /** + * Output a 3d vector + */ + void vec3(double a, double b, double c); + + /** + * Output a 4d vector + */ + void vec4(double a, double b, double c, double d); + + /** + * Output an rgbf color vector + */ + void rgbf(double r, double g, double b, double f); + + /** + * Output one bezier's start, start-control, + * end-control, and end nodes + */ + void segment(int segNr, double a0, double a1, + double b0, double b1, + double c0, double c1, + double d0, double d1); + + + /** + * Output the file header + */ + bool doHeader(); + + /** + * Output the file footer + */ + bool doTail(); + + /** + * Output the SVG document's curve data as POV curves + */ + bool doCurve(SPItem *item, const String &id); + bool doTreeRecursive(SPDocument *doc, SPObject *obj); + bool doTree(SPDocument *doc); + + /** + * Actual method to save document + */ + void saveDocument(SPDocument *doc, gchar const *filename); + + + /** + * used for saving information about shapes + */ + class PovShapeInfo + { + public: + PovShapeInfo() + = default; + PovShapeInfo(const PovShapeInfo &other) + { assign(other); } + PovShapeInfo& operator=(const PovShapeInfo &other) + { assign(other); return *this; } + virtual ~PovShapeInfo() + = default; + String id; + String color; + + private: + void assign(const PovShapeInfo &other) + { + id = other.id; + color = other.color; + } + }; + + //A list for saving information about the shapes + std::vector<PovShapeInfo> povShapes; + + //For formatted output + String outbuf; + + //For statistics + int nrNodes; + int nrSegments; + int nrShapes; + int idIndex; + + double minx; + double miny; + double maxx; + double maxy; + +}; + + + + +} // namespace Internal +} // namespace Extension +} // namespace Inkscape + + + +#endif /* EXTENSION_INTERNAL_POV_OUT_H */ + diff --git a/src/extension/internal/svg.cpp b/src/extension/internal/svg.cpp new file mode 100644 index 0000000..461d174 --- /dev/null +++ b/src/extension/internal/svg.cpp @@ -0,0 +1,1056 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the code that moves all of the SVG loading and saving into + * the module format. Really Inkscape is built to handle these formats + * internally, so this is just calling those internal functions. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2002-2003 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include <giomm/file.h> +#include <giomm/action.h> + +#include "document.h" +#include "inkscape.h" +#include "inkscape-application.h" +#include "preferences.h" +#include "extension/output.h" +#include "extension/input.h" +#include "extension/system.h" +#include "file.h" +#include "svg.h" +#include "file.h" +#include "display/cairo-utils.h" +#include "extension/system.h" +#include "extension/output.h" +#include "xml/attribute-record.h" +#include "xml/simple-document.h" + +#include "object/sp-image.h" +#include "object/sp-root.h" +#include "object/sp-text.h" + +#include "util/units.h" +#include "selection-chemistry.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#include "clear-n_.h" + +using Inkscape::XML::Node; + +/* + * Removes all sodipodi and inkscape elements and attributes from an xml tree. + * used to make plain svg output. + */ +static void pruneExtendedNamespaces( Inkscape::XML::Node *repr ) +{ + if (repr) { + if ( repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) { + std::vector<gchar const*> attrsRemoved; + for ( const auto & it : repr->attributeList()) { + const gchar* attrName = g_quark_to_string(it.key); + if ((strncmp("inkscape:", attrName, 9) == 0) || (strncmp("sodipodi:", attrName, 9) == 0)) { + attrsRemoved.push_back(attrName); + } + } + // Can't change the set we're iterating over while we are iterating. + for (auto & it : attrsRemoved) { + repr->removeAttribute(it); + } + } + + std::vector<Inkscape::XML::Node *> nodesRemoved; + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + if((strncmp("inkscape:", child->name(), 9) == 0) || strncmp("sodipodi:", child->name(), 9) == 0) { + nodesRemoved.push_back(child); + } else { + pruneExtendedNamespaces(child); + } + } + for (auto & it : nodesRemoved) { + repr->removeChild(it); + } + } +} + +/* + * Similar to the above sodipodi and inkscape prune, but used on all documents + * to remove problematic elements (for example Adobe's i:pgf tag) only removes + * known garbage tags. + */ +static void pruneProprietaryGarbage( Inkscape::XML::Node *repr ) +{ + if (repr) { + std::vector<Inkscape::XML::Node *> nodesRemoved; + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + if((strncmp("i:pgf", child->name(), 5) == 0)) { + nodesRemoved.push_back(child); + g_warning( "An Adobe proprietary tag was found which is known to cause issues. It was removed before saving."); + } else { + pruneProprietaryGarbage(child); + } + } + for (auto & it : nodesRemoved) { + repr->removeChild(it); + } + } +} + +/** + * \return None + * + * \brief Create new markers where necessary to simulate the SVG 2 marker attribute 'orient' + * value 'auto-start-reverse'. + * + * \param repr The current element to check. + * \param defs A pointer to the <defs> element. + * \param css The properties of the element to check. + * \param property Which property to check, either 'marker' or 'marker-start'. + * + */ +static void remove_marker_auto_start_reverse(Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs, + SPCSSAttr *css, + Glib::ustring const &property) +{ + Glib::ustring value = sp_repr_css_property (css, property.c_str(), ""); + + if (!value.empty()) { + + // Find reference <marker> + static Glib::RefPtr<Glib::Regex> regex = Glib::Regex::create("url\\(#([^\\)]*)\\)"); + Glib::MatchInfo matchInfo; + regex->match(value, matchInfo); + + if (matchInfo.matches()) { + + auto marker_name = matchInfo.fetch(1).raw(); + Inkscape::XML::Node *marker = sp_repr_lookup_child (defs, "id", marker_name.c_str()); + if (marker) { + + // Does marker use "auto-start-reverse"? + if (strncmp(marker->attribute("orient"), "auto-start-reverse", 17)==0) { + + // See if a reversed marker already exists. + auto marker_name_reversed = marker_name + "_reversed"; + Inkscape::XML::Node *marker_reversed = + sp_repr_lookup_child (defs, "id", marker_name_reversed.c_str()); + + if (!marker_reversed) { + + // No reversed marker, need to create! + marker_reversed = repr->document()->createElement("svg:marker"); + + // Copy attributes + for (const auto & iter : marker->attributeList()) { + marker_reversed->setAttribute(g_quark_to_string(iter.key), iter.value); + } + + // Override attributes + marker_reversed->setAttribute("id", marker_name_reversed); + marker_reversed->setAttribute("orient", "auto"); + + // Find transform + const char* refX = marker_reversed->attribute("refX"); + const char* refY = marker_reversed->attribute("refY"); + std::string transform = "rotate(180"; + if (refX) { + transform += ","; + transform += refX; + + if (refY) { + if (refX) { + transform += ","; + transform += refY; + } else { + transform += ",0,"; + transform += refY; + } + } + } + transform += ")"; + + // We can't set a transform on a marker... must create group first. + Inkscape::XML::Node *group = repr->document()->createElement("svg:g"); + group->setAttribute("transform", transform); + marker_reversed->addChild(group, nullptr); + + // Copy all marker content to group. + for (auto child = marker->firstChild() ; child != nullptr ; child = child->next() ) { + auto new_child = child->duplicate(repr->document()); + group->addChild(new_child, nullptr); + new_child->release(); + } + + // Add new marker to <defs>. + defs->addChild(marker_reversed, marker); + marker_reversed->release(); + } + + // Change url to reference reversed marker. + std::string marker_url("url(#" + marker_name_reversed + ")"); + sp_repr_css_set_property(css, "marker-start", marker_url.c_str()); + + // Also fix up if property is marker shorthand. + if (property == "marker") { + std::string marker_old_url("url(#" + marker_name + ")"); + sp_repr_css_unset_property(css, "marker"); + sp_repr_css_set_property(css, "marker-mid", marker_old_url.c_str()); + sp_repr_css_set_property(css, "marker-end", marker_old_url.c_str()); + } + + sp_repr_css_set(repr, css, "style"); + + } // Uses auto-start-reverse + } + } + } +} + +// Called by remove_marker_context_paint() for each property value ("marker", "marker-start", ...). +static void remove_marker_context_paint (Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs, + Glib::ustring property) +{ + // Value of 'marker', 'marker-start', ... property. + std::string value("url(#"); + value += repr->attribute("id"); + value += ")"; + + // Generate a list of elements that reference this marker. + std::vector<Inkscape::XML::Node *> to_fix_fill_stroke = + sp_repr_lookup_property_many(repr->root(), property, value); + + for (auto it: to_fix_fill_stroke) { + + // Figure out value of fill... could be inherited. + SPCSSAttr* css = sp_repr_css_attr_inherited (it, "style"); + Glib::ustring fill = sp_repr_css_property (css, "fill", ""); + Glib::ustring stroke = sp_repr_css_property (css, "stroke", ""); + + // Name of new marker. + Glib::ustring marker_fixed_id = repr->attribute("id"); + if (!fill.empty()) { + marker_fixed_id += "_F" + fill; + } + if (!stroke.empty()) { + marker_fixed_id += "_S" + stroke; + } + + { + // Replace characters from color value that are invalid in ids + gchar *normalized_id = g_strdup(marker_fixed_id.c_str()); + g_strdelimit(normalized_id, "#%", '-'); + g_strdelimit(normalized_id, "(), \n\t\r", '.'); + marker_fixed_id = normalized_id; + g_free(normalized_id); + } + + // See if a fixed marker already exists. + // Could be more robust, assumes markers are direct children of <defs>. + Inkscape::XML::Node* marker_fixed = sp_repr_lookup_child(defs, "id", marker_fixed_id.c_str()); + + if (!marker_fixed) { + + // Need to create new marker. + + marker_fixed = repr->duplicate(repr->document()); + marker_fixed->setAttribute("id", marker_fixed_id); + + // This needs to be turned into a function that fixes all descendents. + for (auto child = marker_fixed->firstChild() ; child != nullptr ; child = child->next()) { + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( child, "style" ); + + Glib::ustring fill2 = sp_repr_css_property (css, "fill", ""); + if (fill2 == "context-fill" ) { + sp_repr_css_set_property (css, "fill", fill.c_str()); + } + if (fill2 == "context-stroke" ) { + sp_repr_css_set_property (css, "fill", stroke.c_str()); + } + + Glib::ustring stroke2 = sp_repr_css_property (css, "stroke", ""); + if (stroke2 == "context-fill" ) { + sp_repr_css_set_property (css, "stroke", fill.c_str()); + } + if (stroke2 == "context-stroke" ) { + sp_repr_css_set_property (css, "stroke", stroke.c_str()); + } + + sp_repr_css_set(child, css, "style"); + sp_repr_css_attr_unref(css); + } + + defs->addChild(marker_fixed, repr); + marker_fixed->release(); + } + + Glib::ustring marker_value = "url(#" + marker_fixed_id + ")"; + sp_repr_css_set_property (css, property.c_str(), marker_value.c_str()); + sp_repr_css_set (it, css, "style"); + sp_repr_css_attr_unref(css); + } +} + +static void remove_marker_context_paint (Inkscape::XML::Node *repr, + Inkscape::XML::Node *defs) +{ + if (strncmp("svg:marker", repr->name(), 10) == 0) { + + if (!repr->attribute("id")) { + + std::cerr << "remove_marker_context_paint: <marker> without 'id'!" << std::endl; + + } else { + + // First see if we need to do anything. + bool need_to_fix = false; + + // This needs to be turned into a function that searches all descendents. + for (auto child = repr->firstChild() ; child != nullptr ; child = child->next()) { + + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( child, "style" ); + Glib::ustring fill = sp_repr_css_property (css, "fill", ""); + Glib::ustring stroke = sp_repr_css_property (css, "stroke", ""); + if (fill == "context-fill" || + fill == "context-stroke" || + stroke == "context-fill" || + stroke == "context-stroke" ) { + need_to_fix = true; + break; + } + sp_repr_css_attr_unref(css); + } + + if (need_to_fix) { + + // Now we need to search document for all elements that use this marker. + remove_marker_context_paint (repr, defs, "marker"); + remove_marker_context_paint (repr, defs, "marker-start"); + remove_marker_context_paint (repr, defs, "marker-mid"); + remove_marker_context_paint (repr, defs, "marker-end"); + } + } + } +} + +/* + * Recursively insert SVG 1.1 fallback for SVG 2 text (ignored by SVG 2 renderers including ours). + * Notes: + * Text must have been layed out. Access via old document. + */ +static void insert_text_fallback( Inkscape::XML::Node *repr, const SPDocument *original_doc, Inkscape::XML::Node *defs = nullptr ) +{ + if (repr) { + + if (strncmp("svg:text", repr->name(), 8) == 0) { + + auto id = repr->attribute("id"); + // std::cout << "insert_text_fallback: found text! id: " << (id?id:"null") << std::endl; + + // We need to get original SPText object to access layout. + SPText* text = static_cast<SPText *>(original_doc->getObjectById( id )); + if (text == nullptr) { + std::cerr << "insert_text_fallback: bad cast" << std::endl; + return; + } + + if (!text->has_inline_size() && + !text->has_shape_inside()) { + // No SVG 2 text, nothing to do. + return; + } + + // We will keep this text node but replace all children. + // Text object must be visible for the text calculatons to work + bool was_hidden = text->isHidden(); + text->setHidden(false); + + // For text in a shape, We need to unset 'text-anchor' or SVG 1.1 fallback won't work. + // Note 'text' here refers to original document while 'repr' refers to new document copy. + if (text->has_shape_inside()) { + SPCSSAttr *css = sp_repr_css_attr(repr, "style" ); + sp_repr_css_unset_property(css, "text-anchor"); + sp_repr_css_set(repr, css, "style"); + sp_repr_css_attr_unref(css); + } + + // We need to put trailing white space into it's own tspan for inline size so + // it is excluded during calculation of line position in SVG 1.1 renderers. + bool trim = text->has_inline_size() && + !(text->style->text_anchor.computed == SP_CSS_TEXT_ANCHOR_START); + + // Make a list of children to delete at end: + std::vector<Inkscape::XML::Node *> old_children; + for (auto child = repr->firstChild(); child; child = child->next()) { + old_children.push_back(child); + } + + // For round-tripping, xml:space (or 'white-space:pre') must be set. + repr->setAttribute("xml:space", "preserve"); + + double text_x = repr->getAttributeDouble("x", 0.0); + double text_y = repr->getAttributeDouble("y", 0.0); + // std::cout << "text_x: " << text_x << " text_y: " << text_y << std::endl; + + // Loop over all lines in layout. + for (auto it = text->layout.begin() ; it != text->layout.end() ; ) { + + // Create a <tspan> with 'x' and 'y' for each line. + Inkscape::XML::Node *line_tspan = repr->document()->createElement("svg:tspan"); + + // This could be useful if one wants to edit in an old version of Inkscape but we + // need to check if it breaks anything: + // line_tspan->setAttribute("sodipodi:role", "line"); + + // Hide overflow tspan (one line of text). + if (text->layout.isHidden(it)) { + line_tspan->setAttribute("style", "visibility:hidden"); + } + + Geom::Point line_anchor_point = text->layout.characterAnchorPoint(it); + double line_x = line_anchor_point[Geom::X]; + double line_y = line_anchor_point[Geom::Y]; + + // std::cout << " line_anchor_point: " << line_anchor_point << std::endl; + if (line_tspan->childCount() == 0) { + if (text->is_horizontal()) { + // std::cout << " horizontal: " << text_x << " " << line_anchor_point[Geom::Y] << std::endl; + if (text->has_inline_size()) { + // We use text_x as this is the reference for 'text-anchor' + // (line_x is the start of the line which gives wrong position when 'text-anchor' not start). + line_tspan->setAttributeSvgDouble("x", text_x); + } else { + // shape-inside (we don't have to worry about 'text-anchor'). + line_tspan->setAttributeSvgDouble("x", line_x); + } + line_tspan->setAttributeSvgDouble("y", line_y); // FIXME: this will pick up the wrong end of counter-directional runs + } else { + // std::cout << " vertical: " << line_anchor_point[Geom::X] << " " << text_y << std::endl; + line_tspan->setAttributeSvgDouble("x", line_x); // FIXME: this will pick up the wrong end of counter-directional runs + if (text->has_inline_size()) { + line_tspan->setAttributeSvgDouble("y", text_y); + } else { + line_tspan->setAttributeSvgDouble("y", line_y); + } + } + } + + // Inside line <tspan>, create <tspan>s for each change of style or shift. (No shifts in SVG 2 flowed text.) + // For simple lines, this creates an unneeded <tspan> but so be it. + Inkscape::Text::Layout::iterator it_line_end = it; + it_line_end.nextStartOfLine(); + + // Find last span in line so we can put trailing whitespace in its own tspan for SVG 1.1 fallback. + Inkscape::Text::Layout::iterator it_last_span = it; + it_last_span.nextStartOfLine(); + it_last_span.prevStartOfSpan(); + + Glib::ustring trailing_whitespace; + + // Loop over chunks in line + while (it != it_line_end) { + + Inkscape::XML::Node *span_tspan = repr->document()->createElement("svg:tspan"); + + // use kerning to simulate justification and whatnot + Inkscape::Text::Layout::iterator it_span_end = it; + it_span_end.nextStartOfSpan(); + Inkscape::Text::Layout::OptionalTextTagAttrs attrs; + text->layout.simulateLayoutUsingKerning(it, it_span_end, &attrs); + + // 'dx' and 'dy' attributes are used to simulated justified text. + if (!text->is_horizontal()) { + std::swap(attrs.dx, attrs.dy); + } + TextTagAttributes(attrs).writeTo(span_tspan); + SPObject *source_obj = nullptr; + Glib::ustring::iterator span_text_start_iter; + text->layout.getSourceOfCharacter(it, &source_obj, &span_text_start_iter); + + // Set tspan style + Glib::ustring style_text = (dynamic_cast<SPString *>(source_obj) ? source_obj->parent : source_obj) + ->style->writeIfDiff(text->style); + if (!style_text.empty()) { + span_tspan->setAttributeOrRemoveIfEmpty("style", style_text); + } + + // If this tspan has no attributes, discard it and add content directly to parent element. + if (span_tspan->attributeList().empty()) { + Inkscape::GC::release(span_tspan); + span_tspan = line_tspan; + } else { + line_tspan->appendChild(span_tspan); + Inkscape::GC::release(span_tspan); + } + + // Add text node + SPString *str = dynamic_cast<SPString *>(source_obj); + if (str) { + Glib::ustring *string = &(str->string); // TODO fixme: dangerous, unsafe premature-optimization + SPObject *span_end_obj = nullptr; + Glib::ustring::iterator span_text_end_iter; + text->layout.getSourceOfCharacter(it_span_end, &span_end_obj, &span_text_end_iter); + if (span_end_obj != source_obj) { + if (it_span_end == text->layout.end()) { + span_text_end_iter = span_text_start_iter; + for (int i = text->layout.iteratorToCharIndex(it_span_end) - text->layout.iteratorToCharIndex(it) ; i ; --i) + ++span_text_end_iter; + } else + span_text_end_iter = string->end(); // spans will never straddle a source boundary + } + + if (span_text_start_iter != span_text_end_iter) { + Glib::ustring new_string; + while (span_text_start_iter != span_text_end_iter) + new_string += *span_text_start_iter++; // grr. no substr() with iterators + + if (it == it_last_span && trim) { + // Found last span in line + const auto s = new_string.find_last_not_of(" \t"); // Any other white space characters needed? + trailing_whitespace = new_string.substr(s+1, new_string.length()); + new_string.erase(s+1); + } + + Inkscape::XML::Node *new_text = repr->document()->createTextNode(new_string.c_str()); + span_tspan->appendChild(new_text); + Inkscape::GC::release(new_text); + // std::cout << " new_string: |" << new_string << "|" << std::endl; + } + } + it = it_span_end; + } + + // Add line tspan to document + repr->appendChild(line_tspan); + Inkscape::GC::release(line_tspan); + + // For center and end justified text, we need to remove any spaces and put them + // into a separate tspan (alignment is done by "text chunk" and spaces at ends of + // line will mess this up). + if (trim && trailing_whitespace.length() != 0) { + + Inkscape::XML::Node *space_tspan = repr->document()->createElement("svg:tspan"); + // Set either 'x' or 'y' to force a new text chunk. To do: this really should + // be positioned at the end of the line (overhanging). + if (text->is_horizontal()) { + space_tspan->setAttributeSvgDouble("y", line_y); + } else { + space_tspan->setAttributeSvgDouble("x", line_x); + } + Inkscape::XML::Node *space = repr->document()->createTextNode(trailing_whitespace.c_str()); + space_tspan->appendChild(space); + Inkscape::GC::release(space); + line_tspan->appendChild(space_tspan); + Inkscape::GC::release(space_tspan); + } + + } + + for (auto i: old_children) { + repr->removeChild (i); + } + + text->setHidden(was_hidden); + return; // No need to look at children of <text> + } + + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + insert_text_fallback (child, original_doc, defs); + } + } +} + + +static void insert_mesh_polyfill( Inkscape::XML::Node *repr ) +{ + if (repr) { + + Inkscape::XML::Node *defs = sp_repr_lookup_name (repr, "svg:defs"); + + if (defs == nullptr) { + // We always put meshes in <defs>, no defs -> no mesh. + return; + } + + bool has_mesh = false; + for ( Node *child = defs->firstChild(); child; child = child->next() ) { + if (strncmp("svg:meshgradient", child->name(), 16) == 0) { + has_mesh = true; + break; + } + } + + Inkscape::XML::Node *script = sp_repr_lookup_child (repr, "id", "mesh_polyfill"); + + if (has_mesh && script == nullptr) { + + script = repr->document()->createElement("svg:script"); + script->setAttribute ("id", "mesh_polyfill"); + script->setAttribute ("type", "text/javascript"); + repr->root()->appendChild(script); // Must be last + + // Insert JavaScript via raw string literal. + Glib::ustring js = +#include "polyfill/mesh_compressed.include" +; + + Inkscape::XML::Node *script_text = repr->document()->createTextNode(js.c_str()); + script->appendChild(script_text); + } + } +} + + +static void insert_hatch_polyfill( Inkscape::XML::Node *repr ) +{ + if (repr) { + + Inkscape::XML::Node *defs = sp_repr_lookup_name (repr, "svg:defs"); + + if (defs == nullptr) { + // We always put meshes in <defs>, no defs -> no mesh. + return; + } + + bool has_hatch = false; + for ( Node *child = defs->firstChild(); child; child = child->next() ) { + if (strncmp("svg:hatch", child->name(), 16) == 0) { + has_hatch = true; + break; + } + } + + Inkscape::XML::Node *script = sp_repr_lookup_child (repr, "id", "hatch_polyfill"); + + if (has_hatch && script == nullptr) { + + script = repr->document()->createElement("svg:script"); + script->setAttribute ("id", "hatch_polyfill"); + script->setAttribute ("type", "text/javascript"); + repr->root()->appendChild(script); // Must be last + + // Insert JavaScript via raw string literal. + Glib::ustring js = +#include "polyfill/hatch_compressed.include" +; + + Inkscape::XML::Node *script_text = repr->document()->createTextNode(js.c_str()); + script->appendChild(script_text); + } + } +} + +/* + * Recursively transform SVG 2 to SVG 1.1, if possible. + */ +static void transform_2_to_1( Inkscape::XML::Node *repr, Inkscape::XML::Node *defs = nullptr ) +{ + if (repr) { + + // std::cout << "transform_2_to_1: " << repr->name() << std::endl; + + // Things we do once per node. ----------------------- + + // Find defs, if does not exist, create. + if (defs == nullptr) { + defs = sp_repr_lookup_name (repr, "svg:defs"); + } + if (defs == nullptr) { + defs = repr->document()->createElement("svg:defs"); + repr->root()->addChild(defs, nullptr); + } + + // Find style. + SPCSSAttr* css = sp_repr_css_attr ( repr, "style" ); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Individual items ---------------------------------- + + // SVG 2 marker attribute orient:auto-start-reverse: + if ( prefs->getBool("/options/svgexport/marker_autostartreverse", false) ) { + // Do "marker-start" first for efficiency reasons. + remove_marker_auto_start_reverse(repr, defs, css, "marker-start"); + remove_marker_auto_start_reverse(repr, defs, css, "marker"); + } + + // SVG 2 paint values 'context-fill', 'context-stroke': + if ( prefs->getBool("/options/svgexport/marker_contextpaint", false) ) { + remove_marker_context_paint(repr, defs); + } + + // *** To Do *** + // Context fill & stroke outside of markers + // Paint-Order + // Meshes + // Hatches + + for ( Node *child = repr->firstChild(); child; child = child->next() ) { + transform_2_to_1 (child, defs); + } + + sp_repr_css_attr_unref(css); + } +} + + + + +/** + \return None + \brief What would an SVG editor be without loading/saving SVG + files. This function sets that up. + + For each module there is a call to Inkscape::Extension::build_from_mem + with a rather large XML file passed in. This is a constant string + that describes the module. At the end of this call a module is + returned that is basically filled out. The one thing that it doesn't + have is the key function for the operation. And that is linked at + the end of each call. +*/ +void +Svg::init() +{ + // clang-format off + /* SVG in */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVG Input") "</name>\n" + "<id>" SP_MODULE_KEY_INPUT_SVG "</id>\n" + SVG_COMMON_INPUT_PARAMS + "<input>\n" + "<extension>.svg</extension>\n" + "<mimetype>image/svg+xml</mimetype>\n" + "<filetypename>" N_("Scalable Vector Graphic (*.svg)") "</filetypename>\n" + "<filetypetooltip>" N_("Inkscape native file format and W3C standard") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new Svg()); + + /* SVG out Inkscape */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVG Output Inkscape") "</name>\n" + "<id>" SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE "</id>\n" + "<output is_exported='true'>\n" + "<extension>.svg</extension>\n" + "<mimetype>image/x-inkscape-svg</mimetype>\n" + "<filetypename>" N_("Inkscape SVG (*.svg)") "</filetypename>\n" + "<filetypetooltip>" N_("SVG format with Inkscape extensions") "</filetypetooltip>\n" + "<dataloss>false</dataloss>\n" + "</output>\n" + "</inkscape-extension>", new Svg()); + + /* SVG out */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVG Output") "</name>\n" + "<id>" SP_MODULE_KEY_OUTPUT_SVG "</id>\n" + "<output is_exported='true'>\n" + "<extension>.svg</extension>\n" + "<mimetype>image/svg+xml</mimetype>\n" + "<filetypename>" N_("Plain SVG (*.svg)") "</filetypename>\n" + "<filetypetooltip>" N_("Scalable Vector Graphics format as defined by the W3C") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new Svg()); + // clang-format on + + return; +} + + +/** + \return A new document just for you! + \brief This function takes in a filename of a SVG document and + turns it into a SPDocument. + \param mod Module to use + \param uri The path or URI to the file (UTF-8) + + This function is really simple, it just calls sp_document_new... + That's BS, it does all kinds of things for importing documents + that probably should be in a separate function. + + Most of the import code was copied from gdkpixpuf-input.cpp. +*/ +SPDocument * +Svg::open (Inkscape::Extension::Input *mod, const gchar *uri) +{ + g_assert(mod != nullptr); + + // This is only used at the end... but it should go here once uri stuff is fixed. + auto file = Gio::File::create_for_commandline_arg(uri); + const auto path = file->get_path(); + + // Fixing this means fixing a whole string of things. + // if (path.empty()) { + // // We lied, the uri wasn't a uri, try as path. + // file = Gio::File::create_for_path(uri); + // } + + // std::cout << "Svg::open: uri in: " << uri << std::endl; + // std::cout << " : uri: " << file->get_uri() << std::endl; + // std::cout << " : scheme: " << file->get_uri_scheme() << std::endl; + // std::cout << " : path: " << file->get_path() << std::endl; + // std::cout << " : parse: " << file->get_parse_name() << std::endl; + // std::cout << " : base: " << file->get_basename() << std::endl; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + // Get import preferences. + bool ask_svg = prefs->getBool( "/dialogs/import/ask_svg"); + Glib::ustring import_mode_svg = prefs->getString("/dialogs/import/import_mode_svg"); + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + + // If we popped up a window asking about import preferences, get values from + // there and update preferences. + if(mod->get_gui() && ask_svg) { + ask_svg = !mod->get_param_bool("do_not_ask"); + import_mode_svg = mod->get_param_optiongroup("import_mode_svg"); + scale = mod->get_param_optiongroup("scale"); + + prefs->setBool( "/dialogs/import/ask_svg", ask_svg); + prefs->setString("/dialogs/import/import_mode_svg", import_mode_svg ); + prefs->setString("/dialogs/import/scale", scale ); + } + + bool import = prefs->getBool("/options/onimport", false); + bool import_pages = (import_mode_svg == "pages"); + // Do we open a new svg instead of import? + if (uri && import && import_mode_svg == "new") { + prefs->setBool("/options/onimport", false); // set back to true in file_import + static auto gapp = InkscapeApplication::instance()->gtk_app(); + auto action = gapp->lookup_action("file-open-window"); + auto file_dnd = Glib::Variant<Glib::ustring>::create(uri); + action->activate(file_dnd); + return SPDocument::createNewDoc (nullptr, true, true); + } + // Do we "import" as <image>? + if (import && import_mode_svg != "include" && !import_pages) { + // We import! + + // New wrapper document. + SPDocument * doc = SPDocument::createNewDoc (nullptr, true, true); + + // Imported document + // SPDocument * ret = SPDocument::createNewDoc(file->get_uri().c_str(), true); + SPDocument * ret = SPDocument::createNewDoc(uri, true); + + if (!ret) { + return nullptr; + } + + // What is display unit doing here? + Glib::ustring display_unit = doc->getDisplayUnit()->abbr; + double width = ret->getWidth().value(display_unit); + double height = ret->getHeight().value(display_unit); + if (width < 0 || height < 0) { + return nullptr; + } + + // Create image node + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *image_node = xml_doc->createElement("svg:image"); + + // Set default value as we honor "preserveAspectRatio". + image_node->setAttribute("preserveAspectRatio", "none"); + + double svgdpi = mod->get_param_float("svgdpi"); + image_node->setAttribute("inkscape:svg-dpi", Glib::ustring::format(svgdpi)); + + image_node->setAttribute("width", Glib::ustring::format(width)); + image_node->setAttribute("height", Glib::ustring::format(height)); + + // This is actually "image-rendering" + Glib::ustring scale = prefs->getString("/dialogs/import/scale"); + if( scale != "auto") { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "image-rendering", scale.c_str()); + sp_repr_css_set(image_node, css, "style"); + sp_repr_css_attr_unref( css ); + } + + // Do we embed or link? + if (import_mode_svg == "embed") { + std::unique_ptr<Inkscape::Pixbuf> pb(Inkscape::Pixbuf::create_from_file(uri, svgdpi)); + if(pb) { + sp_embed_svg(image_node, uri); + } + } else { + // Convert filename to uri (why do we need to do this, we claimed it was already a uri). + gchar* _uri = g_filename_to_uri(uri, nullptr, nullptr); + if(_uri) { + // if (strcmp(_uri, uri) != 0) { + // std::cout << "Svg::open: _uri != uri! " << _uri << ":" << uri << std::endl; + // } + image_node->setAttribute("xlink:href", _uri); + g_free(_uri); + } else { + image_node->setAttribute("xlink:href", uri); + } + } + + // Add the image to a layer. + Inkscape::XML::Node *layer_node = xml_doc->createElement("svg:g"); + layer_node->setAttribute("inkscape:groupmode", "layer"); + layer_node->setAttribute("inkscape:label", "Image"); + doc->getRoot()->appendChildRepr(layer_node); + layer_node->appendChild(image_node); + Inkscape::GC::release(image_node); + Inkscape::GC::release(layer_node); + fit_canvas_to_drawing(doc); + + // Set viewBox if it doesn't exist. What is display unit doing here? + if (!doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + return doc; + } + + // We are not importing as <image>. Open as new document. + + // Try to open non-local file (when does this occur?). + if (!file->get_uri_scheme().empty()) { + if (path.empty()) { + try { + char *contents; + gsize length; + file->load_contents(contents, length); + return SPDocument::createNewDocFromMem(contents, length, true); + } catch (Gio::Error &e) { + g_warning("Could not load contents of non-local URI %s\n", uri); + return nullptr; + } + } else { + // Do we ever get here and does this actually work? + uri = path.c_str(); + } + } + + SPDocument *doc = SPDocument::createNewDoc(uri, true); + + // Convert single page docs into multi page mode, and visa-versa if + // we are importing. We never change the mode for opening. + if (doc && import) { + doc->setPages(import_pages); + } + + return doc; +} + +/** + \return None + \brief This is the function that does all of the SVG saves in + Inkscape. It detects whether it should do a Inkscape + namespace save internally. + \param mod Extension to use. + \param doc Document to save. + \param uri The filename to save the file to. + + This function first checks its parameters, and makes sure that + we're getting good data. It also checks the module ID of the + incoming module to figure out whether this save should include + the Inkscape namespace stuff or not. The result of that comparison + is stored in the exportExtensions variable. + + If there is not to be Inkscape name spaces a new document is created + without. (I think, I'm not sure on this code) + + All of the internally referenced imageins are also set to relative + paths in the file. And the file is saved. + + This really needs to be fleshed out more, but I don't quite understand + all of this code. I just stole it. +*/ +void +Svg::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + g_return_if_fail(doc != nullptr); + g_return_if_fail(filename != nullptr); + Inkscape::XML::Document *rdoc = doc->getReprDoc(); + const SPDocument *original_doc = doc->getOriginalDocument(); + + bool const exportExtensions = ( !mod->get_id() + || !strcmp (mod->get_id(), SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE) + || !strcmp (mod->get_id(), SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE)); + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool const transform_2_to_1_flag = + prefs->getBool("/dialogs/save_as/enable_svgexport", false); + + bool const insert_text_fallback_flag = + prefs->getBool("/options/svgexport/text_insertfallback", true); + bool const insert_mesh_polyfill_flag = + prefs->getBool("/options/svgexport/mesh_insertpolyfill", true); + bool const insert_hatch_polyfill_flag = + prefs->getBool("/options/svgexport/hatch_insertpolyfill", true); + + // We used to copy the svg here if there was modification, but no need now + // We copy the svg document for ALL exports and saves and the extensions can + // decide what to do without fear. + + // We prune the in-use document and deliberately loose data, because there + // is no known use for this data at the present time. + pruneProprietaryGarbage(rdoc->root()); + + // Start off with a svg 2.0 document + rdoc->setAttribute("standalone", "no"); + rdoc->setAttribute("version", "2.0"); + + if (!exportExtensions) { + pruneExtendedNamespaces(rdoc->root()); + } + + if (transform_2_to_1_flag) { + transform_2_to_1 (rdoc->root()); + rdoc->setAttribute("version", "1.1"); + } + + if (insert_text_fallback_flag && original_doc) { + insert_text_fallback (rdoc->root(), original_doc); + } + + if (insert_mesh_polyfill_flag) { + insert_mesh_polyfill (rdoc->root()); + } + + if (insert_hatch_polyfill_flag) { + insert_hatch_polyfill (rdoc->root()); + } + + if (!sp_repr_save_rebased_file(rdoc, filename, SP_SVG_NS_URI, + doc->getDocumentBase(), // + m_detachbase ? nullptr : filename)) { + throw Inkscape::Extension::Output::save_failed(); + } +} + +} } } /* namespace inkscape, module, implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svg.h b/src/extension/internal/svg.h new file mode 100644 index 0000000..ec76bf0 --- /dev/null +++ b/src/extension/internal/svg.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is the code that moves all of the SVG loading and saving into + * the module format. Really Sodipodi is built to handle these formats + * internally, so this is just calling those internal functions. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2003 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __SVG_H__ +#define __SVG_H__ + +#include "../implementation/implementation.h" + +// clang-format off +#define SVG_COMMON_INPUT_PARAMS \ + "<param name='import_mode_svg' type='optiongroup' gui-text='" N_("SVG Image Import Type:") "' >\n" \ + "<option value='include' >" N_("Include SVG image as editable object(s) in the current file") "</option>\n" \ + "<option value='pages' >" N_("Add SVG as new page(s) in the current file") "</option>\n" \ + "<option value='embed' >" N_("Embed the SVG file in an image tag (not editable in this document)") "</option>\n" \ + "<option value='link' >" N_("Link the SVG file in an image tag (not editable in this document).") "</option>\n" \ + "<option value='new' >" N_("Open SVG image as separate document") "</option>\n" \ + "</param>\n" \ + "<param name='svgdpi' type='float' precision='2' min='1' max='999999' gui-text='" N_("DPI for rendered SVG") "'>96.00</param>\n" \ + "<param name='scale' appearance='combo' type='optiongroup' gui-text='" N_("Image Rendering Mode:") "' gui-description='" N_("When an image is upscaled, apply smoothing or keep blocky (pixelated). (Will not work in all browsers.)") "' >\n" \ + "<option value='auto' >" N_("None (auto)") "</option>\n" \ + "<option value='optimizeQuality' >" N_("Smooth (optimizeQuality)") "</option>\n" \ + "<option value='optimizeSpeed' >" N_("Blocky (optimizeSpeed)") "</option>\n" \ + "</param>\n" \ + "<param name=\"do_not_ask\" gui-description='" N_("Hide the dialog next time and always apply the same actions.") "' gui-text=\"" N_("Don't ask again") "\" type=\"bool\" >false</param>\n" +// clang-format on + + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class Svg : public Inkscape::Extension::Implementation::Implementation { + bool m_detachbase = false; + +public: + void setDetachBase(bool detach) override { m_detachbase = detach; } + + void save( Inkscape::Extension::Output *mod, + SPDocument *doc, + gchar const *filename ) override; + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* __SVG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svgz.cpp b/src/extension/internal/svgz.cpp new file mode 100644 index 0000000..7799c67 --- /dev/null +++ b/src/extension/internal/svgz.cpp @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code to handle compressed SVG loading and saving. Almost identical to svg + * routines, but separated for simpler extension maintenance. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "svgz.h" +#include "extension/extension.h" +#include "extension/system.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#include "clear-n_.h" + +/** + \return None + \brief What would an SVG editor be without loading/saving SVG + files. This function sets that up. + + For each module there is a call to Inkscape::Extension::build_from_mem + with a rather large XML file passed in. This is a constant string + that describes the module. At the end of this call a module is + returned that is basically filled out. The one thing that it doesn't + have is the key function for the operation. And that is linked at + the end of each call. +*/ +void +Svgz::init() +{ + // clang-format off + /* SVGZ in */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVGZ Input") "</name>\n" + "<id>" SP_MODULE_KEY_INPUT_SVGZ "</id>\n" + "<dependency type=\"extension\">" SP_MODULE_KEY_INPUT_SVG "</dependency>\n" + SVG_COMMON_INPUT_PARAMS + "<input>\n" + "<extension>.svgz</extension>\n" + "<mimetype>image/svg+xml-compressed</mimetype>\n" + "<filetypename>" N_("Compressed Inkscape SVG (*.svgz)") "</filetypename>\n" + "<filetypetooltip>" N_("SVG file format compressed with GZip") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new Svgz()); + + /* SVGZ out Inkscape */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVGZ Output") "</name>\n" + "<id>" SP_MODULE_KEY_OUTPUT_SVGZ_INKSCAPE "</id>\n" + "<output>\n" + "<extension>.svgz</extension>\n" + "<mimetype>image/x-inkscape-svg-compressed</mimetype>\n" + "<filetypename>" N_("Compressed Inkscape SVG (*.svgz)") "</filetypename>\n" + "<filetypetooltip>" N_("Inkscape's native file format compressed with GZip") "</filetypetooltip>\n" + "<dataloss>false</dataloss>\n" + "</output>\n" + "</inkscape-extension>", new Svgz()); + + /* SVGZ out */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("SVGZ Output") "</name>\n" + "<id>" SP_MODULE_KEY_OUTPUT_SVGZ "</id>\n" + "<output>\n" + "<extension>.svgz</extension>\n" + "<mimetype>image/svg+xml-compressed</mimetype>\n" + "<filetypename>" N_("Compressed plain SVG (*.svgz)") "</filetypename>\n" + "<filetypetooltip>" N_("Scalable Vector Graphics format compressed with GZip") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>\n", new Svgz()); + // clang-format on + + return; +} + + +} } } // namespace inkscape, module, implementation + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/svgz.h b/src/extension/internal/svgz.h new file mode 100644 index 0000000..e923c4c --- /dev/null +++ b/src/extension/internal/svgz.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Code to handle compressed SVG loading and saving. Almost identical to svg + * routines, but separated for simpler extension maintenance. + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2002-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SVGZ_H +#define SEEN_SVGZ_H + +#include "svg.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class Svgz : public Svg { +public: + static void init( ); +}; + +} } } // namespace Inkscape, Extension, Implementation +#endif // SEEN_SVGZ_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/text_reassemble.c b/src/extension/internal/text_reassemble.c new file mode 100644 index 0000000..543106f --- /dev/null +++ b/src/extension/internal/text_reassemble.c @@ -0,0 +1,2973 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * text_reassemble.c from libTERE + *//* + * Authors: see below + * + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2.0+, read the file 'COPYING' for more information. + */ + + +/** + @file text_reassemble.c + +\verbatim +Method: + 1. For all ordered text objects which are sequential and share the same esc. + 2. For the first only pull x,y,esc and save, these define origin and rotation. + 3. Save the text object. + 4. Phase I: For all saved text objects construct lines. + 5. Check for allowed overlaps on sequential saved text object bounding rectangles. + 6 If found merge second with first, check next one. + 7. If not found, start a new complex (line). + 8. Phase II; for all lines construct paragraphs. + 9. Check alignment and line spacing of preceding line with current line. + 10. if alignment is the same, and line spacing is compatible merge current line into + current paragraph. Reaverage line spacing over all lines in paragraph. Check next one. + 11. If alignment does not match start a new paragraph. + (Test program) + 12. Over all phase II paragraphs + 13. Over all phase I lines in each paragraph. + 14. Over all text objects in each line. + Emit SVG corresponding to this construct to a file dump.svg. + (Conversion to other file types would be modeled on this example.) + 15. Clean up. + (General program) + Like for the Test program, but final representation may not be SVG. + Text object and bounding rectangle memory would all be released. If another set of + text will be processed then hang onto both Freetype and Fontconfig structures. If no + other text will be processed here, then also release Freetype structures. If the caller uses + Fontconfig elsewhere then do not release it, otherwise, do so. + +NOTE ON COORDINATES: x is positive to the right, y is positive down. So (0,0) is the upper left corner, and the +lower left corner of a rectangle has a LARGER Y coordinate than the upper left. Ie, LL=(10,10) UR=(30,5) is typical. +\endverbatim +*/ + +/* + +Compilation of test program (with all debugging output, but not loop testing): +On Windows use: + + gcc -Wall -DWIN32 -DTEST -DDBG_TR_PARA -DDBG_TR_INPUT \ + -I. -I/c/progs/devlibs32/include -I/c/progs/devlibs32/include/freetype2\ + -o text_reassemble text_reassemble.c uemf_utf.c \ + -lfreetype6 -lfontconfig-1 -liconv -lm -L/c/progs/devlibs32/bin + +On Linux use: + + gcc -Wall -DTEST -DDBG_TR_PARA -DDBG_TR_INPUT -I. -I/usr/include/freetype2 -o text_reassemble text_reassemble.c uemf_utf.c -lfreetype -lfontconfig -lm + +Compilation of object file only (Windows): + + gcc -Wall -DWIN32 -c \ + -I. -I/c/progs/devlibs32/include -I/c/progs/devlibs32/include/freetype2\ + text_reassemble.c + +Compilation of object file only (Linux): + gcc -Wall -c -I. -I/usr/include/freetype2 text_reassemble.c + + +Optional compiler switches for development: + -DDBG_TR_PARA draw bounding rectangles for paragraphs in SVG output + -DDBG_TR_INPUT draw input text and their bounding rectangles in SVG output + -DTEST build the test program + -DDBG_LOOP force the test program to cycle 5 times. Useful for finding + memory leaks. Output file is overwritten each time. + + +File: text_reassemble.c +Version: 0.0.18 +Date: 11-MAR-2016 +Author: David Mathog, Biology Division, Caltech +email: mathog@caltech.edu +Copyright: 2016 David Mathog and California Institute of Technology (Caltech) +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "text_reassemble.h" +#include <3rdparty/libuemf/uemf_utf.h> /* For a couple of text functions. Exact copy from libUEMF. */ +#include <locale.h> +#include <float.h> + +/* Code generated by make_ucd_mn_table.c using: + cat mnlist.txt | ./make_ucd_mn_table >generated.c +*/ +#include <stdint.h> +int is_mn_unicode(int test){ +#define MN_TEST_LIMIT 921600 +#define N_SPAGES 225 +#define N_CPAGES 192 +#define N_CPDATA 344 +#define C_PER_S 16 + + // clang-format off + uint8_t superpages[N_SPAGES]={ + 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x06, + 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0B}; + + uint8_t cpages[N_CPAGES]={ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x19, 0x00, 0x00, + 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x1C, 0x1D, 0x1E, 0x1F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x21, 0x00, + 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x25, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x27, + 0x00, 0x28, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + uint32_t cpage_data[N_CPDATA]={ + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000FFFF, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000F8, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFE0000, 0xBFFFFFFF, 0x000000B6, 0x00000000, + 0x07FF0000, 0x00000000, 0xFFFFF800, 0x00010000, 0x00000000, 0x00000000, 0x9FC00000, 0x00003D9F, + 0x00020000, 0xFFFF0000, 0x000007FF, 0x00000000, 0x00000000, 0x0001FFC0, 0x00000000, 0x000FF800, + 0xFBC00000, 0x00003EEF, 0x0E000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x7FFFFFF0, + 0x00000007, 0x14000000, 0x00FE21FE, 0x0000000C, 0x00000002, 0x10000000, 0x0000201E, 0x0000000C, + 0x00000006, 0x10000000, 0x00023986, 0x00230000, 0x00000006, 0x10000000, 0x000021BE, 0x0000000C, + 0x00000002, 0x90000000, 0x0040201E, 0x0000000C, 0x00000004, 0x00000000, 0x00002001, 0x00000000, + 0x00000000, 0xC0000000, 0x00603DC1, 0x0000000C, 0x00000000, 0x90000000, 0x00003040, 0x0000000C, + 0x00000000, 0x00000000, 0x0000201E, 0x0000000C, 0x00000000, 0x00000000, 0x005C0400, 0x00000000, + 0x00000000, 0x07F20000, 0x00007F80, 0x00000000, 0x00000000, 0x1BF20000, 0x00003F00, 0x00000000, + 0x03000000, 0x02A00000, 0x00000000, 0x7FFE0000, 0xFEFFE0DF, 0x1FFFFFFF, 0x00000040, 0x00000000, + 0x00000000, 0x66FDE000, 0xC3000000, 0x001E0001, 0x20002064, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0xE0000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x001C0000, 0x001C0000, 0x000C0000, 0x000C0000, 0x00000000, 0x3FB00000, 0x200FFE40, 0x00000000, + 0x00003800, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000200, 0x00000000, 0x00000000, + 0x00000000, 0x0E040187, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x01800000, 0x00000000, 0x7F400000, 0x9FF81FE5, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x0000000F, 0x17D00000, 0x00000004, 0x000FF800, 0x00000003, 0x00000B3C, 0x00000000, 0x0003A340, + 0x00000000, 0x00CFF000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFF70000, 0x001021FD, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF, 0xF000007F, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x1FFF0000, 0x0001FFE2, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00038000, + 0x00000000, 0x00000000, 0x00000000, 0x80000000, 0x00000000, 0x00000000, 0x00000000, 0xFFFFFFFF, + 0x00000000, 0x00003C00, 0x00000000, 0x00000000, 0x06000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x3FF08000, 0x80000000, 0x00000000, 0x00000000, 0x00030000, + 0x00000844, 0x00000060, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000010, 0x0003FFFF, + 0x00000000, 0x00003FC0, 0x0003FF80, 0x00000000, 0x00000007, 0x13C80000, 0x00000000, 0x00000000, + 0x00000000, 0x00667E00, 0x00001008, 0x00000000, 0x00000000, 0xC19D0000, 0x00000002, 0x00403000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00002120, + 0x40000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x0000FFFF, 0x0000007F, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x20000000, + 0x0000F06E, 0x87000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000002, 0xFF000000, 0x0000007F, 0x00000000, 0x00000003, 0x06780000, 0x00000000, 0x00000000, + 0x00000007, 0x001FEF80, 0x00000000, 0x00000000, 0x00000003, 0x7FC00000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00BF2800, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00078000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0xF8000380, 0x00000FE7, 0x00003C00, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x0000001C, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x0000FFFF}; + // clang-format on + + int result=0; + + int spage_idx; + int cpage_row, cpage_column, cpage_idx; + int cd_row, cd_column, cd_idx, cd_bit; + + if(test<MN_TEST_LIMIT){ + spage_idx = test >> 12; + cpage_row = superpages[spage_idx]; + cpage_column = (test>>8) & 15; + cpage_idx = C_PER_S*cpage_row + cpage_column; + cd_row = cpages[cpage_idx]; + cd_column = test>>5 & 7; + cd_idx = 8*cd_row + cd_column; + cd_bit = test & 31; + result = cpage_data[cd_idx] & (1 << cd_bit); + } + return(result); +} + + + +/** + \brief Find a (sub)string in a caseinvariant manner, used for locating "Narrow" in font name + \return Returns -1 if no match, else returns the position (numbered from 0) of the first character of the match. + \param string Text to search + \param sub Text to find +*/ +int TR_findcasesub(const char *string, const char *sub){ + int i,j; + int match=0; + for(i=0; string[i]; i++){ + for(match=1,j=0; sub[j] && string[i+j]; j++){ + if(toupper(sub[j]) != toupper(string[i+j])){ + match=0; + break; + } + } + if(match && !sub[j])break; /* matched over the entire substring */ + } + return((match ? i : -1)); +} + +/** + \brief Constrouct a fontspec from a TCHUNK_SPECS and a fontname + \return Returns NULL on error, new fontspec on success + \param tsp pointer to TCHUNK_SPECS to use for information + \param fontname Fontname to use in the new fontspec +*/ + /* construct a font name */ +char *TR_construct_fontspec(const TCHUNK_SPECS *tsp, const char *fontname){ + int newlen = 128 + strlen(fontname); /* too big, but not by much */ + char *newfs = NULL; + newfs = (char *) malloc(newlen); + sprintf(newfs,"%s:slant=%d:weight=%d:size=%f:width=%d",fontname,tsp->italics,tsp->weight,tsp->fs,(tsp->co ? 75 : tsp->condensed)); + return(newfs); +} + + +/** + \brief Reconstrouct a fontspec by substituting a font name into an existing spec + \return Returns NULL on error, new fontspec on success + \param fontspec Original fontspec, only the name will be changed + \param fontname Fontname to substitute into the new fontspec +*/ + /* construct a font name */ +char *TR_reconstruct_fontspec(const char *fontspec, const char *fontname){ + int colon; + int newlen = strlen(fontspec) + strlen(fontname) + 1; /* too big, but not by much */ + char *newfs = NULL; + newfs = (char *) malloc(newlen); + colon = strcspn(fontspec,":"); + if(colon){ sprintf(newfs,"%s%s",fontname,&fontspec[colon]); } + return(newfs); +} + +/** + \brief Find a font in the list that has a glyph for this character, change alternate to match + \return Returns 0 if no match or an error, else returns the glyph index in the new alternate font + \param fti pointer to the FT_INFO structure, may be modified if alternate font is added + \param efsp Pointer to a Pointer to the original FNT_SPECS struct. On return contains the FNT_SPECS corresponding to the glyph_index.. + \param wc Current character (32 bit int) +*/ +int TR_find_alternate_font(FT_INFO *fti, FNT_SPECS **efsp, uint32_t wc){ + int glyph_index=0; /* this is the unknown character glyph */ + uint32_t i; + FcCharSet *cs; + FcResult result = FcResultMatch; + FcPattern *pattern, *fpat; + char *filename; + char *fontname; + char *newfontspec; + int fi_idx; + FNT_SPECS *fsp,*fsp2; + if(!fti || !efsp || !*efsp)return(0); + fsp = *efsp; + for(i=0;i<fsp->used;i++){ /* first check in alts */ + fsp2 = &fti->fonts[fsp->alts[i].fi_idx]; /* these are in order of descending previous usage */ + glyph_index = FT_Get_Char_Index( fsp2->face, wc); /* we have the face, might as well check that directly */ + if (glyph_index){ /* found a glyph for the character in this font */ + (void) fsp_alts_weight(fsp, i); + *efsp = fsp2; + return(glyph_index); + } + } + + /* it was not in alts, now go through fontset and see if it is in there */ + for(i=1; i< (unsigned int) fsp->fontset->nfont;i++){ /* already know the primary does not have this character */ + result = FcPatternGetCharSet(fsp->fontset->fonts[i], FC_CHARSET, 0, &cs); + if(result != FcResultMatch) return(0); /* some terrible problem, this should never happen */ + if (FcCharSetHasChar(cs, wc)){ /* found a glyph for the character in this font */ + glyph_index = i; + + /* Do a lot of work to find the filename corresponding to the fontset entry. + None of these should ever fail, but if one does, return 0 + */ + if( + !(pattern = FcNameParse((const FcChar8 *)&(fsp->fontspec))) || + !FcConfigSubstitute(NULL, pattern, FcMatchPattern) + )return(0); + FcDefaultSubstitute(pattern); + if( + !(fpat = FcFontRenderPrepare(NULL, pattern, fsp->fontset->fonts[i])) || + (FcPatternGetString( fpat, FC_FILE, 0, (FcChar8 **)&filename) != FcResultMatch) || + (FcPatternGetString( fsp->fontset->fonts[i], FC_FULLNAME, 0, (FcChar8 **)&fontname) != FcResultMatch) + )return(0); + + /* find the font (added from an unrelated fontset, for instance) or insert it as new */ + fi_idx = ftinfo_find_loaded_by_src(fti, (uint8_t *) filename); + if(fi_idx < 0){ + newfontspec = TR_reconstruct_fontspec((char *) fsp->fontspec, fontname); + fi_idx = ftinfo_load_fontname(fti, newfontspec); + free(newfontspec); + if(fi_idx < 0)return(0); /* This could happen if we run out of memory*/ + } + + /* add the new font index to the alts list on the (current) fsp. */ + (void) fsp_alts_insert(fsp, fi_idx); + + /* release FC's own memory related to this call that does not need to be kept around so that face will work */ + FcPatternDestroy(pattern); + + *efsp = &(fti->fonts[fi_idx]); + return(glyph_index); + } + } + + return(0); +} + +/** + \brief Get the advance for the 32 bit character + + \return Returns -1 on error, or advance in units of 1/64th of a Point. + \param fti pointer to the FT_INFO structure, may be modified if alternate font is required + \param fsp Pointer to FNT_SPECS struct. + \param wc Current character (32 bit int) + \param pc Previous character + \param load_flags Controls internal advance: + FT_LOAD_NO_SCALE, internal advance is in 1/64th of a point. (kerning values are still scaled) + FT_LOAD_TARGET_NORMAL internal advance is in 1/64th of a point. The scale + factor seems to be (Font Size in points)*(DPI)/(32.0 pnts)*(72 dpi). + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. + \param ymin If the pointer is defined, the value is adjusted if ymin of wc character is less than the current value. + \param ymax If the pointer is defined, the value is adjusted if ymin of wc character is more than the current value. +*/ +int TR_getadvance(FT_INFO *fti, FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int load_flags, int kern_mode, int *ymin, int *ymax){ + FT_Glyph glyph; + int glyph_index; + int advance=-1; + FT_BBox bbox; + + if(is_mn_unicode(wc))return(0); /* no advance on Unicode Mn characters */ + + glyph_index = FT_Get_Char_Index( fsp->face, wc); + if(!glyph_index){ /* not in primary font, check alternates */ + glyph_index = TR_find_alternate_font(fti, &fsp, wc); + } + if(glyph_index){ + if (!FT_Load_Glyph( fsp->face, glyph_index, load_flags )){ + if ( !FT_Get_Glyph( fsp->face->glyph, &glyph ) ) { + advance = fsp->face->glyph->advance.x; + FT_Glyph_Get_CBox( glyph, FT_GLYPH_BBOX_UNSCALED, &bbox ); + if(ymin && (bbox.yMin < *ymin))*ymin=bbox.yMin; + if(ymax && (bbox.yMax > *ymax))*ymax=bbox.yMax; + if(pc)advance += TR_getkern2(fsp, wc, pc, kern_mode); + FT_Done_Glyph(glyph); + } + } + } + /* If there was no way to determine the width, this returns the error value */ + return(advance); +} + +/** + \brief Get the kerning for a pair of 32 bit characters + \return Returns 0 on error, or kerning value (which may be 0) for the pair in units of 1/64th of a point. + \param fsp Pointer to FNT_SPECS struct. + \param wc Current character (32 bit int) + \param pc Previous character + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int TR_getkern2(FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int kern_mode){ + int this_glyph_index; + int prev_glyph_index; + int kern=0; + FT_Vector akerning; + + this_glyph_index = FT_Get_Char_Index( fsp->face, wc); + prev_glyph_index = FT_Get_Char_Index( fsp->face, pc); + if(!FT_Get_Kerning( fsp->face, + prev_glyph_index, + this_glyph_index, + kern_mode, + &akerning )){ + kern = akerning.x; /* Is sign correct? */ + } + return(kern); +} + +/** + \brief Get the kerning for a pair of 32 bit characters, where one is the last character in the previous text block, and the other is the first in the current text block. + \return Returns 0 on error, or kerning value (which may be 0) for the pair in units of 1/64th of a point. + \param fsp Pointer to FNT_SPECS struct. + \param tsp current text object + \param ptsp previous text object + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int TR_kern_gap(FNT_SPECS *fsp, TCHUNK_SPECS *tsp, TCHUNK_SPECS *ptsp, int kern_mode){ + int kern=0; + uint32_t *text32=NULL; + uint32_t *ptxt32=NULL; + size_t tlen,plen; + while(ptsp && tsp){ + text32 = U_Utf8ToUtf32le((char *) tsp->string, 0, &tlen); + if(!text32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + text32 = U_Latin1ToUtf32le((char *) tsp->string,0, &tlen); + if(!text32)break; + } + ptxt32 = U_Utf8ToUtf32le((char *) ptsp->string,0,&plen); + if(!ptxt32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + ptxt32 = U_Latin1ToUtf32le((char *) ptsp->string,0, &plen); + if(!ptxt32)break; + } + kern = TR_getkern2(fsp, *text32, ptxt32[plen-1], kern_mode); + break; + } + if(text32)free(text32); + if(ptxt32)free(ptxt32); + return(kern); +} + + + + +/** + \brief Find baseline on Y axis of a complex. + If the complex is a TR_TEXT or TR_LINE find its baseline. + If the complex is TR_PARA_[UCLR]J find the baseline of the last line. + If there are multiple text elements in a TR_LINE, the baseline is that of the + element that uses the largest font. This will definitely give the wrong + result if that line starts with a super or subscript that is full font size, but + they are usually smaller. + \return Returns 0 if it cannot determine a baseline, else returns the baseline Y coordinate. + \param tri pointer to the TR_INFO structure holding all TR data + \param src index of the current complex + \param ymax If the pointer is defined, the value is adjusted if ymax of current complex is more than the current value. + \param ymin If the pointer is defined, the value is adjusted if ymin of current complex is less than the current value. +*/ +double TR_baseline(TR_INFO *tri, int src, double *ymax, double *ymin){ + double baseline=0; + double tmp=0.0; + double yheight; + int last; + int i; + int trec; + CX_INFO *cxi=tri->cxi; + BR_INFO *bri=tri->bri; + TP_INFO *tpi=tri->tpi; + FT_INFO *fti=tri->fti; + FNT_SPECS *fsp; + last = cxi->cx[src].kids.used - 1; + switch (cxi->cx[src].type){ + case TR_TEXT: + trec = cxi->cx[src].kids.members[0]; /* for this complex type there is only ever one member */ + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + fsp = &(fti->fonts[tpi->chunks[trec].fi_idx]); + yheight = fsp->face->bbox.yMax - fsp->face->bbox.yMin; + if(ymax){ + tmp = tpi->chunks[trec].fs * ((double)fsp->face->bbox.yMax/yheight); + if(*ymax <= tmp)*ymax = tmp; + } + else if(ymin){ + tmp = tpi->chunks[trec].fs * ((double)-fsp->face->bbox.yMin/yheight); /* yMin in face is negative */ + if(*ymin <= tmp)*ymin = tmp; + } + break; + case TR_LINE: + for(i=last;i>=0;i--){ /* here last is the count of text objects in the complex */ + trec = cxi->cx[src].kids.members[i]; + fsp = &(fti->fonts[tpi->chunks[trec].fi_idx]); + yheight = fsp->face->bbox.yMax - fsp->face->bbox.yMin; + if(ymax){ + tmp = tpi->chunks[trec].fs * (((double)fsp->face->bbox.yMax)/yheight); + if(*ymax <= tmp){ + *ymax = tmp; + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + } + } + else if(ymin){ + tmp = tpi->chunks[trec].fs * (((double)-fsp->face->bbox.yMin)/yheight); /* yMin in face is negative */ + if(*ymin <= tmp){ + *ymin = tmp; + baseline = bri->rects[trec].yll - tpi->chunks[trec].boff; + } + } + } + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + case TR_PARA_CJ: + case TR_PARA_RJ: + trec = cxi->cx[src].kids.members[last]; + baseline = TR_baseline(tri, trec, ymax, ymin); + break; + } + return(baseline); +} + +/** + \brief Check or set vertical advance on the growing complex relative to the current complex. + Vadvance is a multiplicative factor like 1.25. + The distance between successive baselines is vadvance * max(font_size), where the maximum + is over all text elements in src. + The growing complex is always the last one in the CX_INFO section of the TR_INFO structure. + If an existing vadvance does not match the one which would be required to fit the next complex + to add to the growing one, it terminates a growing complex. (Ie, starts a new paragraph.) + Find baseline on Y axis of a complex. + If the complex is a TR_TEXT or TR_LINE find its baseline. + If the complex is TR_PARA+* find the baseline of the last line. + If there are multiple text elements in a TR_LINE, the baseline is that of the + element that uses the largest font. This will definitely give the wrong + result if that line starts with a super or subscript that is full font size, but + they are usually smaller. + \return Returns 0 on success, !0 on failure. + \param tri pointer to the TR_INFO structure holding all TR data + \param src index of the current complex, to be added to the growing complex. + This lets the value of "src - lines" determine the weight to give to each new vadvance value + as it is merged into the running weighted average. This improves the accuracy of the vertical advance, + since there can be some noise introduced when lines have different maximum font sizes. + \param lines index of the first text block that was added to the growing complex. +*/ +int TR_check_set_vadvance(TR_INFO *tri, int src, int lines){ + int status = 0; + CX_INFO *cxi = tri->cxi; + TP_INFO *tpi = tri->tpi; + double ymax = DBL_MIN; + double ymin = DBL_MIN; + double prevbase; + double thisbase; + double weight; + int trec; + double newV; + int dst; + + dst = cxi->used-1; /* complex being grown */ + + prevbase = TR_baseline(tri, dst, NULL, &ymin); + thisbase = TR_baseline(tri, src, &ymax, NULL); + newV = (thisbase - prevbase)/(ymax + ymin); + trec = cxi->cx[dst].kids.members[0]; /* complex whose first text record holds vadvance for this complex */ + trec = cxi->cx[trec].kids.members[0]; /* text record that halds vadvance for this complex */ + if(tpi->chunks[trec].vadvance){ + /* already set on the first text (only place it is stored.) + See if the line to be added is compatible. + All text fields in a complex have the same advance, so just set/check the first one. + vadvance must be within 1% or do not add a new line */ + if(fabs(1.0 - (tpi->chunks[trec].vadvance/newV)) > 0.01){ + status = 1; + } + else { /* recalculate the weighted vadvance */ + weight = (1.0 / (double) (src - lines)); + tpi->chunks[trec].vadvance = tpi->chunks[trec].vadvance*(1.0-weight) + newV*weight; + } + } + else { /* only happens when src = lines + 1*/ + tpi->chunks[trec].vadvance = newV; + } + return(status); +} + + +/** + \brief Initialize an FT_INFO structure. Sets up a freetype library to use in this context. + \returns a pointer to the FT_INFO structure created, or NULL on error. +*/ +FT_INFO *ftinfo_init(void){ + FT_INFO *fti = NULL; + if(FcInit()){ + fti = (FT_INFO *)calloc(1,sizeof(FT_INFO)); + if(fti){ + if(!FT_Init_FreeType( &(fti->library))){ + fti->space=0; + fti->used=0; + + if(ftinfo_make_insertable(fti)){ + FT_Done_FreeType(fti->library); + free(fti); + fti=NULL; + } + } + else { + free(fti); + fti=NULL; + } + } + if(!fti)FcFini(); + } + return(fti); +} + +/** + \brief Make an FT_INFO structure insertable. Adds storage as needed. + \param fti pointer to the FT_INFO structure + \returns 0 on success, !0 on error. +*/ +int ftinfo_make_insertable(FT_INFO *fti){ + int status=0; + FNT_SPECS *tmp; + if(!fti)return(2); + if(fti->used >= fti->space){ + fti->space += ALLOCINFO_CHUNK; + tmp = (FNT_SPECS *) realloc(fti->fonts, fti->space * sizeof(FNT_SPECS) ); + if(tmp){ + fti->fonts = tmp; + memset(&fti->fonts[fti->used],0,(fti->space - fti->used)*sizeof(FNT_SPECS)); + } + else { + status=1; + } + } + return(status); +} + + +/** + \brief Insert a copy of a FNT_SPECS structure into the FT_INFO structure. + \param fti pointer to the FT_INFO structure. + \param fsp pointer to the FNT_SPECS structure. + \returns 0 on success, !0 on error. +*/ +int ftinfo_insert(FT_INFO *fti, FNT_SPECS *fsp){ + int status=1; + if(!fti)return(2); + if(!fsp)return(3); + if(!(status = ftinfo_make_insertable(fti))){ + memcpy(&(fti->fonts[fti->used]),fsp,sizeof(FNT_SPECS)); + fti->used++; + } + return(status); +} + + + +/** + \brief Release an FT_INFO structure. Release all associated memory. + Use like: fi_ptr = ftinfo_release(fi_ptr) + \param fti pointer to the FT_INFO structure. + \returns NULL. +*/ +FT_INFO *ftinfo_release(FT_INFO *fti){ + (void) ftinfo_clear(fti); + FcFini(); /* shut down FontConfig, release memory, patterns must have already been released or boom! */ + return NULL; +} + + +/** + \brief Clear an FT_INFO structure. Release all Freetype memory but does not release Fontconfig. + This would be called in preference to ftinfo_release() if some other part of the program needed + to continue using Fontconfig. + Use like: fi_ptr = ftinfo_clear(fi_ptr) + \param fti pointer to the FT_INFO structure. + \returns NULL. +*/ +FT_INFO *ftinfo_clear(FT_INFO *fti){ + uint32_t i; + FNT_SPECS *fsp; + if(fti){ + for(i=0;i<fti->used;i++){ + fsp = &(fti->fonts[i]); + FT_Done_Face(fsp->face); /* release memory for face controlled by FreeType */ + free(fsp->file); /* release memory holding copies of paths */ + free(fsp->fontspec); /* release memory holding copies of font names */ + FcPatternDestroy(fsp->fpat); /* release memory for FontConfig fpats */ + FcFontSetDestroy(fsp->fontset); + if(fsp->alts){ free(fsp->alts); } + } + free(fti->fonts); + FT_Done_FreeType(fti->library); /* release all other FreeType memory */ + free(fti); + } + return NULL; +} + + +/** + \brief Find the loaded font matching fontspec + \returns index of font on success, -1 if not found + \param tri pointer to the TR_INFO structure. + \param fontspec UTF-8 description of the font, as constructed in trinfo_load_fontname +*/ + +int ftinfo_find_loaded_by_spec(const FT_INFO *fti, const uint8_t *fontspec){ + uint32_t i; + int status = -1; + /* If it is already loaded, do not load it again */ + for(i=0;i<fti->used;i++){ + if(0==strcmp((char *) fti->fonts[i].fontspec, (char *)fontspec)){ + status=i; + break; + } + } + return(status); +} + +/** + \brief Find the loaded font matching the source file + \returns index of font on success, -1 if not found + \param tri pointer to the TR_INFO structure. + \param filename UTF-8 file name for the font +*/ + +int ftinfo_find_loaded_by_src(const FT_INFO *fti, const uint8_t *filename){ + uint32_t i; + int status = -1; + /* If it is already loaded, do not load it again */ + for(i=0;i<fti->used;i++){ + if(0==strcmp((char *) fti->fonts[i].file, (char *) filename)){ + status=i; + break; + } + } + return(status); +} + + +/** + \brief Load a (new) font by name into a TR_INFO structure or find it if it is already loaded + \returns fi_idx of inserted (or found) font on success, <0 on error. + \param fti pointer to the FT_INFO structure. + \param fontname UTF-8 font name + \param fontspec UTF-8 font specification used for query string. +*/ + +int ftinfo_load_fontname(FT_INFO *fti, const char *fontspec){ + FcPattern *pattern = NULL; + FcPattern *fpat = NULL; + FcFontSet *fontset = NULL; + FcResult result = FcResultMatch; + char *filename; + double fd; + FNT_SPECS *fsp; + int status; + int fi_idx; + + if(!fti)return(-1); + + /* If it is already loaded, do not load it again */ + status = ftinfo_find_loaded_by_spec(fti, (uint8_t *) fontspec); + if(status >= 0){ return(status); } + status = 0; /* was -1, reset to 0 */ + + ftinfo_make_insertable(fti); + fi_idx = fti->used; + + pattern = FcNameParse((const FcChar8 *)fontspec); + while(1) { /* this is NOT a loop, it uses breaks to avoid gotos and deep nesting */ + if(!(pattern)){ status = -2; break; } + if(!FcConfigSubstitute(NULL, pattern, FcMatchPattern)){ status = -3; break; }; + FcDefaultSubstitute(pattern); + /* get a fontset, trimmed to only those with new glyphs as needed, so that missing glyph's may be handled */ + if(!(fontset = FcFontSort (NULL,pattern, FcTrue, NULL, &result)) || (result != FcResultMatch)){ status = -4; break; } + if(!(fpat = FcFontRenderPrepare(NULL, pattern, fontset->fonts[0]))){ status = -405; break; } + if(FcPatternGetString( fpat, FC_FILE, 0, (FcChar8 **)&filename) != FcResultMatch){ status = -5; break; } + if(FcPatternGetDouble( fpat, FC_SIZE, 0, &fd) != FcResultMatch){ status = -6; break; } + + /* copy these into memory for external use */ + fsp = &(fti->fonts[fti->used]); + fsp->fontset = fontset; + fsp->alts = NULL; /* Initially no links to alternate fonts */ + fsp->space = 0; + fsp->file = (uint8_t *) U_strdup((char *) filename); + fsp->fontspec = (uint8_t *) U_strdup((char *) fontspec); + fsp->fpat = fpat; + fsp->fsize = fd; + break; + } + /* release FC's own memory related to this call that does not need to be kept around so that face will work */ + if(pattern)FcPatternDestroy(pattern); /* done with this memory */ + if(status<0){ + if(fontset)FcFontSetDestroy(fontset); + if(fpat)FcPatternDestroy(fpat); + return(status); + } + + /* get the current face */ + if(FT_New_Face( fti->library, (const char *) fsp->file, 0, &(fsp->face) )){ return(-8); } + + if(FT_Set_Char_Size( + fsp->face, /* handle to face object */ + 0, /* char_width in 1/64th of points */ + fd*64, /* char_height in 1/64th of points */ + 72, /* horizontal device resolution, DPI */ + 72) /* vebrical device resolution, DPI */ + ){ return(-9); } + + /* The space advance is needed in various places. Get it now, and get it in the font units, + so that it can be scaled later with the text size */ + status = TR_getadvance(fti, fsp,' ',0,FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, FT_KERNING_UNSCALED, NULL, NULL); + if(status < 0)return(-7); + fsp->spcadv = ((double) status)/(64.0); + + fti->used++; + +/* + char *fs; + int fb; + if(FcPatternGetBool( fpat, FC_OUTLINE, 0, &fb)== FcResultMatch){ printf("outline: %d\n",fb);fflush(stdout); } + if(FcPatternGetBool( fpat, FC_SCALABLE, 0, &fb)== FcResultMatch){ printf("scalable: %d\n",fb);fflush(stdout); } + if(FcPatternGetDouble( fpat, FC_DPI, 0, &fd)== FcResultMatch){ printf("DPI: %f\n",fd);fflush(stdout); } + if(FcPatternGetInteger( fpat, FC_FONTVERSION, 0, &fb)== FcResultMatch){ printf("fontversion: %d\n",fb);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FULLNAME , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FULLNAME : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FAMILY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FAMILY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_STYLE , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("STYLE : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FOUNDRY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FOUNDRY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FAMILYLANG , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FAMILYLANG : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_STYLELANG , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("STYLELANG : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FULLNAMELANG, 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FULLNAMELANG: %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_CAPABILITY , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("CAPABILITY : %s\n",fs);fflush(stdout); } + if(FcPatternGetString( fpat, FC_FONTFORMAT , 0, (FcChar8 **)&fs)== FcResultMatch){ printf("FONTFORMAT : %s\n",fs);fflush(stdout); } +*/ + + return(fi_idx); +} + +/** + \brief Dump the contents of the TR_INFO structure to stdout. For debugging purposes,not used in production code. + \param tri pointer to the TR_INFO structure. +*/ +void ftinfo_dump(const FT_INFO *fti){ + uint32_t i,j; + FNT_SPECS *fsp; + printf("fti space: %d\n",fti->space); + printf("fti used: %d\n",fti->used); + for(i=0; i< fti->used; i++){ + fsp = &(fti->fonts[i]); + printf("fti font: %6d space: %6d used: %6d spcadv %8f fsize %8f \n",i,fsp->space,fsp->used,fsp->spcadv,fsp->fsize); + printf(" file: %s\n",fsp->file); + printf(" fspc: %s\n",fsp->fontspec); + for(j=0;j<fsp->used;j++){ + printf(" alts: %6d fi_idx: %6d wgt: %6d\n",j,fsp->alts[j].fi_idx,fsp->alts[j].weight); + } + } + +} + +/** + \brief Make the FNT_SPECS alts structure insertable. Adds storage as needed. + \param fti pointer to the FT_INFO structure + \returns 0 on success, !0 on error. +*/ +int fsp_alts_make_insertable(FNT_SPECS *fsp){ + int status=0; + ALT_SPECS *tmp; + if(!fsp)return(2); + if(fsp->used >= fsp->space){ + fsp->space += ALLOCINFO_CHUNK; + tmp = (ALT_SPECS *) realloc(fsp->alts, fsp->space * sizeof(ALT_SPECS) ); + if(tmp){ + fsp->alts = tmp; + memset(&fsp->alts[fsp->used],0,(fsp->space - fsp->used)*sizeof(ALT_SPECS)); + } + else { + status=1; + } + } + return(status); +} + + +/** + \brief Insert a new ALT_SPECS into the FNT_SPECS alts list. + \param fsp pointer to the FNT_SPECS structure. + \param fi_idx font index to add to the alts list + \returns 0 on success, !0 on error. +*/ +int fsp_alts_insert(FNT_SPECS *fsp, uint32_t fi_idx){ + int status=1; + ALT_SPECS alt; + if(!fsp)return(3); + alt.fi_idx = fi_idx; + alt.weight = 1; /* new ones start with this weight, it can only go up */ + if(!(status = fsp_alts_make_insertable(fsp))){ + fsp->alts[fsp->used] = alt; + fsp->used++; + } + return(status); +} + +/** + \brief Increment the weight of an alts entry by 1, readjust order if necessary + \param fsp pointer to the FNT_SPECS structure. + \param idx index of the alts entry to increment + \returns 0 on success, !0 on error. +*/ +int fsp_alts_weight(FNT_SPECS *fsp, uint32_t a_idx){ + uint32_t i; + ALT_SPECS alt; + if(!fsp)return(1); + if(!fsp->used)return(2); + if(a_idx >= fsp->used)return(3); + /* If a counter hits the limit divide all counts in half. */ + if(fsp->alts[a_idx].weight == UINT32_MAX){ + for(i=0; i<fsp->used; i++){ fsp->alts[i].weight /= 2; } + } + fsp->alts[a_idx].weight++; + for(i=a_idx; i>0; i--){ + if(fsp->alts[i-1].weight >= fsp->alts[a_idx].weight)break; + alt = fsp->alts[i-1]; + fsp->alts[i-1] = fsp->alts[a_idx]; + fsp->alts[a_idx] = alt; + } + return(0); +} + + + +/** + \brief Make a CHILD_SPECS structure insertable. Adds storage as needed. + \param csp pointer to the CHILD_SPECS structure + \returns 0 on success, !0 on error. +*/ +int csp_make_insertable(CHILD_SPECS *csp){ + int status=0; + int *tmp; + if(!csp)return(2); + if(csp->used >= csp->space){ + csp->space += ALLOCINFO_CHUNK; + tmp = (int *) realloc(csp->members, csp->space * sizeof(int) ); + if(tmp){ + csp->members = tmp; + memset(&csp->members[csp->used],0,(csp->space - csp->used)*sizeof(int)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Add a member to a CHILD_SPECS structure. (Member is an index for either a text object or a complex.) + \param dst pointer to the CHILD_SPECS structure. + \param src index of the member. + \returns 0 on success, !0 on error. +*/ +int csp_insert(CHILD_SPECS *dst, int src){ + int status=1; + if(!dst)return(2); + if(!(status=csp_make_insertable(dst))){ + dst->members[dst->used]=src; + dst->used++; + } + return(status); +} + +/** + \brief Append all the members of one CHILD_SPECS structure to another CHILD_SPECS structure. + Member is an index for either a text object or a complex. + The donor is not modified. + \param dst pointer to the recipient CHILD_SPECS structure. + \param src pointer to the donor CHILD_SPECS structure. + \returns 0 on success, !0 on error. +*/ +int csp_merge(CHILD_SPECS *dst, CHILD_SPECS *src){ + uint32_t i; + int status=1; + if(!dst)return(2); + if(!src)return(3); + for(i=0;i<src->used;i++){ + status = csp_insert(dst, src->members[i]); + if(status)break; + } + return(status); +} + +/** + \brief Release a CHILD_SPECS structure. Release all associated memory. + \param csp pointer to the CHILD_SPECS structure. + \returns NULL. +*/ +void csp_release(CHILD_SPECS *csp){ + if(csp){ + free(csp->members); + csp->space = 0; + csp->used = 0; + } +} + +/** + \brief Clear a CHILD_SPECS structure, making all allocated slots usable. Does not release associated memory. + \param csp pointer to the CHILD_SPECS structure. + \returns NULL. +*/ +void csp_clear(CHILD_SPECS *csp){ + csp->used = 0; +} + + +/** + \brief Initialize an CX_INFO structure. Holds complexes (multiple text objects in known positions and order.) + \returns a pointer to the CX_INFO structure created, or NULL on error. +*/ +CX_INFO *cxinfo_init(void){ + CX_INFO *cxi = NULL; + cxi = (CX_INFO *)calloc(1,sizeof(CX_INFO)); + if(cxi){ + if(cxinfo_make_insertable(cxi)){ + free(cxi); + cxi=NULL; + } + } + return(cxi); +} + +/** + \brief Make a CX_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param cxi pointer to the CX_INFO structure +*/ +int cxinfo_make_insertable(CX_INFO *cxi){ + int status=0; + CX_SPECS *tmp; + if(cxi->used >= cxi->space){ + cxi->space += ALLOCINFO_CHUNK; + tmp = (CX_SPECS *) realloc(cxi->cx, cxi->space * sizeof(CX_SPECS) ); + if(tmp){ + cxi->cx = tmp; + memset(&cxi->cx[cxi->used],0,(cxi->space - cxi->used)*sizeof(CX_SPECS)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Insert a complex into the CX_INFO structure. (Insert may be either TR_TEXT or TR_LINE.) + \returns 0 on success, !0 on error. + \param cxi pointer to the CX_INFO structure (complexes). + \param src index of the complex to insert. + \param src_rt_tidx index of the bounding rectangle + \param type TR_TEXT (index is for tpi->chunks[]) or TR_LINE (index is for cxi->kids[]) +*/ +int cxinfo_insert(CX_INFO *cxi, int src, int src_rt_tidx, enum tr_classes type){ + int status=1; + if(!cxi)return(2); + if(!(status=cxinfo_make_insertable(cxi))){ + cxi->cx[cxi->used].rt_cidx = src_rt_tidx; + cxi->cx[cxi->used].type = type; + status = csp_insert(&(cxi->cx[cxi->used].kids), src); + cxi->used++; + } + return(status); +} + +/** + \brief Append a complex to the CX_INFO structure and give it a type. + \param cxi pointer to the CX_INFO structure (complexes). + \param src index of the complex to append. + \param type TR_LINE (src is an index for tpi->chunks[]) or TR_PARA (src is an index for cxi->kids[]). + \returns 0 on success, !0 on error. +*/ +int cxinfo_append(CX_INFO *cxi, int src, enum tr_classes type){ + int status=1; + if(!cxi)return(2); + if(!(status=cxinfo_make_insertable(cxi))){ + cxi->cx[cxi->used-1].type = type; + status = csp_insert(&(cxi->cx[cxi->used-1].kids), src); + } + return(status); +} + + +/** + \brief Merge a complex dst with N members (N>=1) by adding a second complex src, and change the type. + \param cxi pointer to the CX_INFO structure (complexes). + \param dst index of the complex to expand. + \param src index of the donor complex (which is not modified). + \param type TR_LINE (src is an index for tpi->chunks[]) or TR_PARA (src is an index for cxi->kids[]). + \returns 0 on success, !0 on error. +*/ +int cxinfo_merge(CX_INFO *cxi, int dst, int src, enum tr_classes type){ + int status =1; + if(!cxi)return(2); + if(!cxi->used)return(3); + if(dst < 0 || dst >= (int) cxi->used)return(4); + if(src < 0)return(5); + cxi->cx[dst].type = type; + status = csp_merge(&(cxi->cx[dst].kids), &(cxi->cx[src].kids)); + return(status); +} + +/** + \brief Trim the last complex from thelist of complexes. + \param cxi pointer to the CX_INFO structure (complexes). + \returns 0 on success, !0 on error. +*/ +int cxinfo_trim(CX_INFO *cxi){ + int status = 0; + int last ; + if(!cxi)return(1); + if(!cxi->used)return(2); + last = cxi->used - 1; + csp_clear(&(cxi->cx[last].kids)); + cxi->used--; + return(status); +} + + +/** + \brief Dump the contents of the TR_INFO structure to stdout. For debugging purposes,not used in production code. + \param tri pointer to the TR_INFO structure. +*/ +void cxinfo_dump(const TR_INFO *tri){ + uint32_t i,j,k; + CX_INFO *cxi = tri->cxi; + BR_INFO *bri = tri->bri; + TP_INFO *tpi = tri->tpi; + BRECT_SPECS *bsp; + CX_SPECS *csp; + if(cxi){ + printf("cxi space: %d\n",cxi->space); + printf("cxi used: %d\n",cxi->used); + printf("cxi phase1: %d\n",cxi->phase1); + printf("cxi lines: %d\n",cxi->lines); + printf("cxi paras: %d\n",cxi->paras); + printf("cxi xy: %f , %f\n",tri->x,tri->y); + + for(i=0;i<cxi->used;i++){ + csp = &(cxi->cx[i]); + bsp = &(bri->rects[csp->rt_cidx]); + printf("cxi cx[%d] type:%d rt_tidx:%d kids_used:%d kids_space:%d\n",i, csp->type, csp->rt_cidx, csp->kids.used, csp->kids.space); + printf("cxi cx[%d] br (LL,UR) (%f,%f),(%f,%f)\n",i,bsp->xll,bsp->yll,bsp->xur,bsp->yur); + for(j=0;j<csp->kids.used;j++){ + k = csp->kids.members[j]; + bsp = &(bri->rects[k]); + if(csp->type == TR_TEXT || csp->type == TR_LINE){ + printf("cxi cx[%d] member:%3d tp_idx:%3d ldir:%d rt_tidx:%3d br (LL,UR) (%8.3f,%8.3f),(%8.3f,%8.3f) xy (%8.3f,%8.3f) kern (%8.3f,%8.3f) text:<%s> decor:%5.5x\n", + i, j, k, tpi->chunks[k].ldir, tpi->chunks[k].rt_tidx, + bsp->xll,bsp->yll,bsp->xur,bsp->yur, + tpi->chunks[k].x, tpi->chunks[k].y, + tpi->chunks[k].xkern, tpi->chunks[k].ykern, + tpi->chunks[k].string, tpi->chunks[k].decoration ); + } + else { /* TR_PARA_* */ + printf("cxi cx[%d] member:%d cx_idx:%d\n",i, j, k); + } + } + } + } + return; +} + +/** + \brief Release a CX_INFO structure. Release all associated memory. + use like: cxi = cxiinfo_release(cxi); + \param cxi pointer to the CX_INFO structure. + \returns NULL. +*/ +CX_INFO *cxinfo_release(CX_INFO *cxi){ + uint32_t i; + if(cxi){ + for(i=0;i<cxi->used;i++){ csp_release(&cxi->cx[i].kids); } + free(cxi->cx); + free(cxi); /* release the overall cxinfo structure */ + } + return NULL; +} + + +/** + \brief Initialize an TP_INFO structure. Holds text objects from which complexes are built. + \returns a pointer to the TP_INFO structure created, or NULL on error. +*/ +TP_INFO *tpinfo_init(void){ + TP_INFO *tpi = NULL; + tpi = (TP_INFO *)calloc(1,sizeof(TP_INFO)); + if(tpi){ + if(tpinfo_make_insertable(tpi)){ + free(tpi); + tpi=NULL; + } + } + return(tpi); +} + + +/** + \brief Make a TP_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param tpi pointer to the TP_INFO structure +*/ +int tpinfo_make_insertable(TP_INFO *tpi){ + int status=0; + TCHUNK_SPECS *tmp; + if(tpi->used >= tpi->space){ + tpi->space += ALLOCINFO_CHUNK; + tmp = (TCHUNK_SPECS *) realloc(tpi->chunks, tpi->space * sizeof(TCHUNK_SPECS) ); + if(tmp){ + tpi->chunks = tmp; + memset(&tpi->chunks[tpi->used],0,(tpi->space - tpi->used)*sizeof(TCHUNK_SPECS)); + } + else { + status=1; + } + } + return(status); +} + +/** + \brief Insert a copy of a TCHUNK_SPECS structure into a TP_INFO structure. (Insert a text object.) + \returns 0 on success, !0 on error. + \param tpi pointer to the TP_INFO structure + \param tsp pointer to the TCHUNK_SPECS structure +*/ +int tpinfo_insert(TP_INFO *tpi, const TCHUNK_SPECS *tsp){ + int status=1; + TCHUNK_SPECS *ltsp; + if(!tpi)return(2); + if(!tsp)return(3); + if(!(status = tpinfo_make_insertable(tpi))){ + ltsp = &(tpi->chunks[tpi->used]); + memcpy(ltsp,tsp,sizeof(TCHUNK_SPECS)); + if(tsp->co)ltsp->condensed = 75; /* Narrow was set in the font name */ + ltsp->xkern = ltsp->ykern = 0.0; /* kerning will be calculated from the derived layout */ + tpi->used++; + } + return(status); +} + +/** + \brief Release a TP_INFO structure. Release all associated memory. + use like: tpi = tpinfo_release(tpi); + \returns NULL. + \param tpi pointer to the TP_INFO structure. +*/ +TP_INFO *tpinfo_release(TP_INFO *tpi){ + uint32_t i; + if(tpi){ + for(i=0;i<tpi->used;i++){ + free(tpi->chunks[i].string); } + free(tpi->chunks); /* release the array */ + free(tpi); /* release the overall tpinfo structure */ + } + return NULL; +} + +/** + \brief Initialize an BR_INFO structure. Holds bounding rectangles, for both text objects and complexes. + \returns a pointer to the BR_INFO structure created, or NULL on error. +*/ +BR_INFO *brinfo_init(void){ + BR_INFO *bri = NULL; + bri = (BR_INFO *)calloc(1,sizeof(BR_INFO)); + if(bri){ + if(brinfo_make_insertable(bri)){ + free(bri); + bri=NULL; + } + } + return(bri); +} + +/** + \brief Make a BR_INFO structure insertable. Adds storage as needed. + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure +*/ +int brinfo_make_insertable(BR_INFO *bri){ + int status=0; + BRECT_SPECS *tmp; + if(!bri)return(2); + if(bri->used >= bri->space){ + bri->space += ALLOCINFO_CHUNK; + tmp = (BRECT_SPECS *) realloc(bri->rects, bri->space * sizeof(BRECT_SPECS) ); + if(tmp){ bri->rects = tmp; } + else { status = 1;} + } + return(status); +} + +/** + \brief Insert a copy of a BRECT_SPEC structure into a BR_INFO structure. (Insert a bounding rectangle.) + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure + \param element pointer to the BRECT_SPECS structure +*/ +int brinfo_insert(BR_INFO *bri, const BRECT_SPECS *element){ + int status=1; + if(!bri)return(2); + if(!(status=brinfo_make_insertable(bri))){ + memcpy(&(bri->rects[bri->used]),element,sizeof(BRECT_SPECS)); + bri->used++; + } + return(status); +} + +/** + \brief Merge BRECT_SPEC element src into/with BRECT_SPEC element dst. src is unchanged. (Merge two bounding rectangles.) + \returns 0 on success, !0 on error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. +*/ +int brinfo_merge(BR_INFO *bri, int dst, int src){ + if(!bri)return(1); + if(!bri->used)return(2); + if(dst<0 || dst >= (int) bri->used)return(3); + if(src<0 || src >= (int) bri->used)return(4); + bri->rects[dst].xll = TEREMIN(bri->rects[dst].xll, bri->rects[src].xll); + bri->rects[dst].yll = TEREMAX(bri->rects[dst].yll, bri->rects[src].yll); /* MAX because Y is positive DOWN */ + bri->rects[dst].xur = TEREMAX(bri->rects[dst].xur, bri->rects[src].xur); + bri->rects[dst].yur = TEREMIN(bri->rects[dst].yur, bri->rects[src].yur); /* MIN because Y is positive DOWN */ +/* +printf("bri_Merge into rect:%d (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n",dst, +(bri->rects[dst].xll), +(bri->rects[dst].yll), +(bri->rects[dst].xur), +(bri->rects[dst].yur), +(bri->rects[src].xll), +(bri->rects[src].yll), +(bri->rects[src].xur), +(bri->rects[src].yur)); +*/ + return(0); +} + +/** + \brief Check for an allowable overlap of two bounding rectangles. + Allowable overlap is any area overlap of src and dst bounding rectangles, after + they have been expanded (padded) by allowed edge expansions. (For instance, if + missing spaces must be accounted for.) + The method works backwards: look for all reasons they might not overlap, + if none are found, then the rectangles do overlap. + An overlap here does not count just a line or a point - area must be involved. + \returns 0 on success (overlap detected), 1 on no overlap, anything else is an error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param rp_dst Pointer to edge padding values for dst. + \param rp_src Pointer to edge padding values for src. +*/ +int brinfo_overlap(const BR_INFO *bri, int dst, int src, RT_PAD *rp_dst, RT_PAD *rp_src){ + int status; + BRECT_SPECS *br_dst; + BRECT_SPECS *br_src; + if(!bri || !rp_dst || !rp_src)return(2); + if(!bri->used)return(3); + if(dst<0 || dst>= (int) bri->used)return(4); + if(src<0 || src>= (int) bri->used)return(5); + br_dst=&bri->rects[dst]; + br_src=&bri->rects[src]; + if( /* Test all conditions that exclude overlap, if any are true, then no overlap */ + ((br_dst->xur + rp_dst->right) < (br_src->xll - rp_src->left) ) || /* dst fully to the left */ + ((br_dst->xll - rp_dst->left) > (br_src->xur + rp_src->right) ) || /* dst fully to the right */ + ((br_dst->yur - rp_dst->up) > (br_src->yll + rp_src->down) ) || /* dst fully below (Y is positive DOWN) */ + ((br_dst->yll + rp_dst->down) < (br_src->yur - rp_src->up) ) /* dst fully above (Y is positive DOWN) */ + ){ + status = 1; + } + else { + /* overlap not excluded, so it must occur. + Only accept overlaps that are mostly at one end or the other, not mostly top or bottom. + If the following condition is true then there is no more than a tiny bit of horizontal overlap of src + within dist, which suggests that the two pieces of text may be considered part of one line. + (For a vertical alphabet the same method could be used for up/down.) */ + if( + (br_src->xll >= br_dst->xur - rp_dst->right) || /* src overlaps just a little on the right (L->R language) */ + (br_src->xur <= br_dst->xll + rp_dst->left) /* src overlaps just a little on the left (R->L language) */ + ){ + status = 0; + } + else { /* Too much overlap, reject the overlap */ + status = 1; + } + } +/* +printf("Overlap status:%d\nOverlap trects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +status, +(br_dst->xll - rp_dst->left ), +(br_dst->yll - rp_dst->down ), +(br_dst->xur + rp_dst->right), +(br_dst->yur + rp_dst->up ), +(br_src->xll - rp_src->left ), +(br_src->yll - rp_src->down ), +(br_src->xur + rp_src->right), +(br_src->yur + rp_src->up )); +printf("Overlap brects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +(br_dst->xll), +(br_dst->yll), +(br_dst->xur), +(br_dst->yur), +(br_src->xll), +(br_src->yll), +(br_src->xur), +(br_src->yur)); +printf("Overlap rprect (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +(rp_dst->left), +(rp_dst->down), +(rp_dst->right), +(rp_dst->up), +(rp_src->left), +(rp_src->down), +(rp_src->right), +(rp_src->up)); +*/ + return(status); +} + +/** + \brief Check for various sorts of invalid text elements upstream (language dir changes, draw order backwards from language direction) + \returns 0 on success (not upstream), 1 if upstream, anything else is an error. + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param ddir direction of dst + \param sdir direction of src +*/ + +int brinfo_upstream(BR_INFO *bri, int dst, int src, int ddir, int sdir){ + int status=0; + BRECT_SPECS *br_dst; + BRECT_SPECS *br_src; + if(!bri)return(2); + if(!bri->used)return(3); + if(dst<0 || dst>= (int) bri->used)return(4); + if(src<0 || src>= (int) bri->used)return(5); + br_dst=&bri->rects[dst]; + br_src=&bri->rects[src]; + if( ddir == LDIR_RL && sdir == LDIR_LR){ + if(br_dst->xur <= (br_src->xll + br_src->xur)/2.0){ status = 1; } + } + else if( ddir == LDIR_LR && sdir == LDIR_RL){ + if((br_src->xll + br_src->xur)/2.0 <= br_dst->xll ){ status = 1; } + } + else if( ddir == LDIR_RL && sdir == LDIR_RL){ + if(br_dst->xur <= (br_src->xll + br_src->xur)/2.0){ status = 1; } + } + else if( ddir == LDIR_LR && sdir == LDIR_LR){ + if((br_src->xll + br_src->xur)/2.0 <= br_dst->xll ){ status = 1; } + } + return(status); +} + + +/** + \brief Try to deduce justification of a paragraph from the bounding rectangles for two successive lines. + \returns one of TR_PARA_ UJ (unknown justified), LJ, CJ, or RJ (left, center, or right justified). + \param bri pointer to the BR_INFO structure + \param dst index of the destination bounding rectangle. + \param src index of the source bounding rectangle. + \param slop allowed error in edge alignment. + \param type Preexisting justification for dst, if any. Justification of dst and src must match this or + TR_PARA_UJ is returned even if dst and src have some (other) alignment. +*/ +enum tr_classes brinfo_pp_alignment(const BR_INFO *bri, int dst, int src, double slop, enum tr_classes type){ + enum tr_classes newtype; + BRECT_SPECS *br_dst = & bri->rects[dst]; + BRECT_SPECS *br_src = & bri->rects[src]; + if((br_dst->yur >= br_src->yur) || (br_dst->yll >= br_src->yll)){ /* Y is positive DOWN */ + /* lines in the wrong vertical order, no paragraph possible (Y is positive down) */ + newtype = TR_PARA_UJ; + } + else if(fabs(br_dst->xll - br_src->xll) < slop){ + /* LJ (might also be CJ but LJ takes precedence) */ + newtype = TR_PARA_LJ; + } + else if(fabs(br_dst->xur - br_src->xur) < slop){ + /* RJ */ + newtype = TR_PARA_RJ; + } + else if(fabs( (br_dst->xur + br_dst->xll)/2.0 - (br_src->xur + br_src->xll)/2.0 ) < slop){ + /* CJ */ + newtype = TR_PARA_CJ; + } + else { + /* not aligned */ + newtype = TR_PARA_UJ; + } + /* within a paragraph type can change from unknown to known, but not from one known type to another*/ + if((type != TR_PARA_UJ) && (newtype != type)){ + newtype = TR_PARA_UJ; + } +/* +printf("pp_align newtype:%d brects (LL,UR) dst:(%f,%f),(%f,%f) src:(%f,%f),(%f,%f)\n", +newtype, +(br_dst->xll), +(br_dst->yll), +(br_dst->xur), +(br_dst->yur), +(br_src->xll), +(br_src->yll), +(br_src->xur), +(br_src->yur)); +*/ + return(newtype); +} + +/** + \brief Release a BR_INFO structure. Release all associated memory. + use like: bri = brinfo_release(bri); + \param bri pointer to the BR_INFO structure. + \returns NULL. +*/ +BR_INFO *brinfo_release(BR_INFO *bri){ + if(bri){ + free(bri->rects); + free(bri); /* release the overall brinfo structure */ + } + return NULL; +} + + + +/** + \brief Initialize an TR_INFO structure. Holds all data for text reassembly. + \returns a pointer to the TR_INFO structure created, or NULL on error. +*/ +TR_INFO *trinfo_init(TR_INFO *tri){ + if(tri)return(tri); /* tri is already set, double initialization is not allowed */ + if(!(tri = (TR_INFO *)calloc(1,sizeof(TR_INFO))) || + !(tri->fti = ftinfo_init()) || + !(tri->tpi = tpinfo_init()) || + !(tri->bri = brinfo_init()) || + !(tri->cxi = cxinfo_init()) + ){ tri = trinfo_release(tri); } + tri->out = NULL; /* This will allocate as needed, it might not ever be needed. */ + tri->qe = 0.0; + tri->esc = 0.0; + tri->x = DBL_MAX; + tri->y = DBL_MAX; + tri->dirty = 0; + tri->use_kern = 1; + tri->load_flags = FT_LOAD_NO_SCALE; + tri->kern_mode = FT_KERNING_UNSCALED; + tri->outspace = 0; + tri->outused = 0; + tri->usebk = BKCLR_NONE; + memset(&(tri->bkcolor),0,sizeof(TRCOLORREF)); + return(tri); +} + +/** + \brief Release a TR_INFO structure completely. + Release all associated memory, including FontConfig. + See also trinfo_clear() and trinfo_release_except_FC(). + use like: tri = trinfo_release(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_release(TR_INFO *tri){ + if(tri){ + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->fti)tri->fti=ftinfo_release(tri->fti); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ free(tri->out); tri->out=NULL; }; + free(tri); + } + return(NULL); +} + +/** + \brief Release a TR_INFO structure mostly. + Release all associated memory EXCEPT Fontconfig. + Fontconfig may still be needed elsewhere in a program and there is no way to figure that out here. + See also trinfo_clear() and trinfo_release(). + use like: tri = trinfo_release_except_FC(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_release_except_FC(TR_INFO *tri){ + if(tri){ + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->fti)tri->fti=ftinfo_clear(tri->fti); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ free(tri->out); tri->out=NULL; }; + free(tri); + } + return(NULL); +} + +/** + \brief Clear a TR_INFO structure. + Releases text and rectangle information, but retains font information, both + Freetype information and Fontconfig information. + See also trinfo_release() and trinfo_release_except_FC(). + Use like: tri = trinfo_clear(tri); + \param tri pointer to the TR_INFO structure. + \returns NULL. +*/ +TR_INFO *trinfo_clear(TR_INFO *tri){ + if(tri){ + + if(tri->bri)tri->bri=brinfo_release(tri->bri); + if(tri->tpi)tri->tpi=tpinfo_release(tri->tpi); + if(tri->cxi)tri->cxi=cxinfo_release(tri->cxi); + if(tri->out){ + free(tri->out); + tri->out = NULL; + tri->outused = 0; + tri->outspace = 0; + }; + /* Do NOT modify: qe, use_kern, usebk, load_flags, kern_mode, or bkcolor. Set the rest back to their defaults */ + tri->esc = 0.0; + tri->x = DBL_MAX; + tri->y = DBL_MAX; + tri->dirty = 0; + if(!(tri->tpi = tpinfo_init()) || /* re-init the pieces just released */ + !(tri->bri = brinfo_init()) || + !(tri->cxi = cxinfo_init()) + ){ + tri = trinfo_release(tri); /* something horrible happened, clean out tri and return NULL */ + } + } + return(tri); +} + + +/** + \brief Set the quantization error value for a TR_INFO structure. + If coordinates have passed through an integer form limits + in accuracy may have been imposed. For instance, if the X coordinate of a point in such a file + is 1000, and the conversion factor from those coordinates to points is .04, then eq is .04. This + just says that single coordinates are only good to within .04, and two coordinates may differ by as much + as .08, just due to quantization error. So if some calculation shows a difference of + .02 it may be interpreted as this sort of error and set to 0.0. + \returns 0 on success, !0 on error. + \param tri pointer to TR_INFO structure + \param qe quantization error. +*/ +int trinfo_load_qe(TR_INFO *tri, double qe){ + if(!tri)return(1); + if(qe<0.0)return(2); + tri->qe=qe; + return(0); +} + +/** + \brief Set the background color and whether or not to use it. + When background color is turned on each line of text is underwritten with a rectangle + of the specified color. The rectangle is the merged bounding rectangle for that line. + \returns 0 on success but nothing changed, >0 on error, <0 on success and a value changed. + \param tri pointer to TR_INFO structure + \param usebk 0 for no background, anything else uses background color + \param bkcolor background color to use +*/ +int trinfo_load_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor){ + int status=0; + if(!tri){ status = 1; } + else { + if((usebk < BKCLR_NONE) || (usebk > BKCLR_ALL)){ status = 2; } + else { + status = trinfo_check_bk(tri, usebk, bkcolor); + tri->usebk = usebk; + tri->bkcolor = bkcolor; + } + } + return(status); +} + +/** + \brief Are the proposed new background and background color a change? + \returns 0 if they are the same, -1 if either is different + \param tri pointer to TR_INFO structure + \param usebk 0 for no background, anything else uses background color + \param bkcolor background color to use +*/ +int trinfo_check_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor){ + int status = 0; + if( (tri->usebk != usebk) || memcmp(&tri->bkcolor,&bkcolor,sizeof(TRCOLORREF))){ status = -1; } + return(status); +} + +/** + \brief Set Freetype parameters and kerning mode (if any) in a TRI_INFO structure. + \returns 0 on success, !0 on error. + \param tri pointer to a TR_INFO structure + \param use_kern 0 if kerning is to be employed, !0 otherwise. + \param load_flags Controls internal advance: + FT_LOAD_NO_SCALE, internal advance is in 1/64th of a point. (kerning values are still scaled) + FT_LOAD_TARGET_NORMAL internal advance is in 1/64th of a point. The scale + factor seems to be (Font Size in points)*(DPI)/(32.0 pnts)*(72 dpi). + \param kern_mode FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED. Set to match calling application. +*/ +int trinfo_load_ft_opts(TR_INFO *tri, int use_kern, int load_flags, int kern_mode){ + if(!tri)return(1); + tri->use_kern = use_kern; + tri->load_flags = load_flags; + tri->kern_mode = kern_mode; + return(0); +} + +/** + \brief Append text to a TR_INFO struct's output buffer, expanding it if necessary. + \returns 0 on success, !0 on error. + \param tri pointer to a TR_INFO structure + \param src Pointer to a text string. +*/ +int trinfo_append_out(TR_INFO *tri, const char *src){ + size_t slen; + uint8_t *tmp; + if(!src)return(-1); + slen = strlen(src); + if(tri->outused + (int) slen + 1 >= tri->outspace){ + tri->outspace += TEREMAX(ALLOCOUT_CHUNK,slen+1); + tmp = realloc(tri->out, tri->outspace * sizeof(uint8_t) ); + if(tmp){ tri->out = tmp; } + else { return(-1); } + } + memcpy(tri->out + tri->outused, src, slen+1); /* copy the terminator */ + tri->outused += slen; /* do not count the terminator in the length */ + return(0); +} + + +/** + \brief Load a text object into a TR_INFO struct. + \returns 0 on success, !0 on error. -1 means that the escapement is different from the objects already loaded. + \param tri pointer to a TR_INFO structure + \param tsp pointer to a TCHUNK_SPECS structure (text object to load) + \param escapement angle in degrees of the text object. + \param flags special processing flags: + TR_EMFBOT calculate Y coordinates of ALIBOT object compatible with EMF files TA_BOTTOM alignment. +*/ +int trinfo_load_textrec(TR_INFO *tri, const TCHUNK_SPECS *tsp, double escapement, int flags){ + + int status; + double x,y,xe; + double asc,dsc; /* these are the ascender/descender for the actual text */ + int ymin,ymax; + double fasc,fdsc; /* these are the ascender/descender for the font as a whole (text independent) */ + TP_INFO *tpi; + FT_INFO *fti; + BR_INFO *bri; + int current,idx,taln; + uint32_t prev; + uint32_t *text32,*tptr; + FNT_SPECS *fsp; + BRECT_SPECS bsp; + + /* check incoming parameters */ + if(!tri)return(1); + if(!tsp)return(2); + if(!tsp->string)return(3); + fti = tri->fti; + tpi = tri->tpi; + bri = tri->bri; + idx = tsp->fi_idx; + taln = tsp->taln; + if(!fti->used)return(4); + if(idx <0 || idx >= (int) fti->used)return(5); + fsp = &(fti->fonts[idx]); + + if(!tri->dirty){ + tri->x = tsp->x; + tri->y = tsp->y; + tri->esc = escapement; + tri->dirty = 1; + } + else { + if(tri->esc != escapement)return(-1); + } + + + tpinfo_insert(tpi,tsp); + current=tpi->used-1; + ymin = 64000; + ymax = -64000; + + /* The geometry model has origin Y at the top of screen, positive Y is down, maximum positive + Y is at the bottom of the screen. That makes "top" (by positive Y) actually the bottom + (as viewed on the screen.) */ + + escapement *= 2.0 * M_PI / 360.0; /* degrees to radians */ + x = tpi->chunks[current].x - tri->x; /* convert to internal orientation */ + y = tpi->chunks[current].y - tri->y; + tpi->chunks[current].x = x * cos(escapement) - y * sin(escapement); /* coordinate transformation */ + tpi->chunks[current].y = x * sin(escapement) + y * cos(escapement); + +/* Careful! face bbox does NOT scale with FT_Set_Char_Size +printf("Face idx:%d bbox: xMax/Min:%ld,%ld yMax/Min:%ld,%ld UpEM:%d asc/des:%d,%d height:%d size:%f\n", + idx, + fsp->face->bbox.xMax,fsp->face->bbox.xMin, + fsp->face->bbox.yMax,fsp->face->bbox.yMin, + fsp->face->units_per_EM,fsp->face->ascender,fsp->face->descender,fsp->face->height,fsp->fsize); +*/ + + text32 = U_Utf8ToUtf32le((char *) tsp->string,0,NULL); + if(!text32){ // LATIN1 encoded >128 are generally not valid UTF, so the first will fail + text32 = U_Latin1ToUtf32le((char *) tsp->string,0,NULL); + if(!text32)return(5); + } + /* baseline advance is independent of character orientation */ + for(xe=0.0, prev=0, tptr=text32; *tptr; tptr++){ + status = TR_getadvance(fti, fsp, *tptr, (tri->use_kern ? prev: 0), tri->load_flags, tri->kern_mode, &ymin, &ymax); + if(status>=0){ + xe += ((double) status)/64.0; + } + else { return(6); } + prev=*tptr; + } + + /* Some glyphs in fonts have no vertical extent, for instance, Hebrew glyphs in Century Schoolbook L. + Use the 3/4 of the font size as a (very bad) approximation for the actual values. */ + if(ymin==0 && ymax==0){ + ymax = 0.75 * fsp->fsize * 64.0; + } + + asc = ((double) (ymax))/64.0; + dsc = ((double) (ymin))/64.0; /* This is negative */ +/* This did not work very well because the ascender/descender went well beyond the actual characters, causing + overlaps on lines that did not actually overlap (vertically). + asc = ((double) (fsp->face->ascender) )/64.0; + dsc = ((double) (fsp->face->descender))/64.0; +*/ + + free(text32); + + /* find the font ascender descender (general one, not specific for current text) */ + fasc = ((double) (fsp->face->ascender) )/64.0; + fdsc = ((double) (fsp->face->descender))/64.0; + + /* originally the denominator was just 32.0, but it broke when units_per_EM wasn't 2048 */ + double fixscale = tsp->fs/(((double) fsp->face->units_per_EM)/64.0); + if(tri->load_flags & FT_LOAD_NO_SCALE) xe *= fixscale; + + /* now place the rectangle using ALN information */ + if( taln & ALIHORI & ALILEFT ){ + bsp.xll = tpi->chunks[current].x; + bsp.xur = tpi->chunks[current].x + xe; + } + else if( taln & ALIHORI & ALICENTER){ + bsp.xll = tpi->chunks[current].x - xe/2.0; + bsp.xur = tpi->chunks[current].x + xe/2.0; + } + else{ /* taln & ALIHORI & ALIRIGHT */ + bsp.xll = tpi->chunks[current].x - xe; + bsp.xur = tpi->chunks[current].x; + } + tpi->chunks[current].ldir = tsp->ldir; + + if(tri->load_flags & FT_LOAD_NO_SCALE){ + asc *= fixscale; + dsc *= fixscale; + fasc *= fixscale; + fdsc *= fixscale; + } + + + /* From this point forward y is on the baseline, so need to correct it in chunks. The asc/dsc are the general + ones for the font, else the text content will muck around with the baseline in BAD ways. */ + if( taln & ALIVERT & ALITOP ){ tpi->chunks[current].y += fasc; } + else if( taln & ALIVERT & ALIBASE){ } /* no correction required */ + else{ /* taln & ALIVERT & ALIBOT */ + if(flags & TR_EMFBOT){ tpi->chunks[current].y -= 0.35 * tsp->fs; } /* compatible with EMF implementations */ + else { tpi->chunks[current].y += fdsc; } + } + tpi->chunks[current].boff = -dsc; + + /* since y is always on the baseline, the lower left and upper right are easy. These use asc/dsc for the particular text, + so that the bounding box will fit it tightly. */ + bsp.yll = tpi->chunks[current].y - dsc; + bsp.yur = tpi->chunks[current].y - asc; + brinfo_insert(bri,&bsp); + tpi->chunks[current].rt_tidx = bri->used - 1; /* index of rectangle that contains it */ + + return(0); +} + +/** + \brief Fontweight conversion. Fontconfig units to SVG units. + Anything not recognized becomes "normal" == 400. + There is no interpolation because a value that mapped to 775, for instance, most + likely would not display properly because it is intermediate between 700 and 800, and + only those need be supported in SVG viewers. + \returns SVG font weight + \param weight Fontconfig font weight. +*/ +int TR_weight_FC_to_SVG(int weight){ + int ret=400; + if( weight == 0){ ret = 100; } + else if(weight == 40){ ret = 200; } + else if(weight == 50){ ret = 300; } + else if(weight == 80){ ret = 400; } + else if(weight == 100){ ret = 500; } + else if(weight == 180){ ret = 600; } + else if(weight == 200){ ret = 700; } + else if(weight == 205){ ret = 800; } + else if(weight == 210){ ret = 900; } + else { ret = 400; } + return(ret); +} + +/** + \brief Set the padding that will be added to bounding rectangles before checking for overlaps in brinfo_overlap(). + \returns void + \param rt_pad pointer to an RT_PAD structure. + \param up padding for the top of a bounding rectangle. + \param down padding for the bottom of a bounding rectangle. + \param left padding for the left of a bounding rectangle. + \param right padding for the right of a bounding rectangle. +*/ +void TR_rt_pad_set(RT_PAD *rt_pad, double up, double down, double left, double right){ + rt_pad->up = up; + rt_pad->down = down; + rt_pad->left = left; + rt_pad->right = right; +} + +/** + \brief Convert from analyzed complexes to SVG format. + \returns void + \param tri pointer to a TR_INFO struct which will be analyzed. Result is stored in its "out" buffer. +*/ +void TR_layout_2_svg(TR_INFO *tri){ + double x = tri->x; + double y = tri->y; + double dx,dy; + double esc; + double recenter; /* horizontal offset to set things up correctly for CJ and RJ text, is 0 for LJ*/ + double lineheight=1.25; + int cutat; + FT_INFO *fti=tri->fti; /* Font info storage */ + TP_INFO *tpi=tri->tpi; /* Text Info/Position Info storage */ + BR_INFO *bri=tri->bri; /* bounding Rectangle Info storage */ + CX_INFO *cxi=tri->cxi; /* Complexes deduced for this text */ + TCHUNK_SPECS *tsp; /* current text object */ + CX_SPECS *csp; + CX_SPECS *cline_sp; + unsigned int i,j,k,jdx,kdx; + int ldir; + char obuf[1024]; /* big enough for style and so forth */ + char cbuf[16]; /* big enough for one hex color */ + + char stransform[128]; + double newx,newy,tmpx; + uint32_t utmp; + + /* copy the current numeric locale, make a copy because setlocale may stomp on + the memory it points to. Then change it because SVG needs decimal points, + not commas, in floats. Restore on exit from this routine. + */ + char *prev_locale = setlocale(LC_NUMERIC,NULL); + char *hold_locale = malloc(sizeof(char) * (strlen(prev_locale) + 1)); + strcpy(hold_locale,prev_locale); + (void) setlocale(LC_NUMERIC,"POSIX"); + +/* +#define DBG_TR_PARA 0 +#define DBG_TR_INPUT 1 +*/ + /* The debug section below is difficult to see if usebk is anything other than BKCLR_NONE */ +#if DBG_TR_PARA || DBG_TR_INPUT /* enable debugging code, writes extra information into SVG */ + /* put rectangles down for each text string - debugging!!! This will not work properly for any Narrow fonts */ + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + sprintf(stransform,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc), 1.25*x,1.25*y); + for(i=cxi->phase1; i<cxi->used;i++){ /* over all complex members from phase2 == TR_PARA_* complexes */ + csp = &(cxi->cx[i]); + for(j=0; j<csp->kids.used; j++){ /* over all members of these complexes, which are phase1 complexes */ + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + for(k=0; k<cxi->cx[jdx].kids.used; k++){ /* over all members of the phase1 complex */ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi */ + tsp = &tpi->chunks[kdx]; + ldir = tsp->ldir; + if(!j && !k){ +#if DBG_TR_PARA + TRPRINT(tri, "<rect\n"); + TRPRINT(tri, "style=\"color:#0000FF;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:none;stroke:#000000;stroke-width:0.8;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;clip-rule:nonzero\"\n"); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[csp->rt_cidx].xur - bri->rects[csp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].yll - bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].xll),1.25*(bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); +#endif /* DBG_TR_PARA */ + } +#if DBG_TR_INPUT /* debugging code, this section writes the original text objects */ + newx = 1.25*(ldir == LDIR_RL ? bri->rects[tsp->rt_tidx].xur : bri->rects[tsp->rt_tidx].xll); + newy = 1.25*(bri->rects[tsp->rt_tidx].yur); + TRPRINT(tri, "<rect\n"); + TRPRINT(tri, "style=\"color:#000000;color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:none;stroke:#00FF00;stroke-width:0.3;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;clip-rule:nonzero\"\n"); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[tsp->rt_tidx].xur - bri->rects[tsp->rt_tidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].yll - bri->rects[tsp->rt_tidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].xll),newy); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + + newy = 1.25*(bri->rects[tsp->rt_tidx].yll - tsp->boff); + sprintf(obuf,"<text x=\"%f\" y=\"%f\"\n",newx, newy ); + TRPRINT(tri, obuf); + sprintf(obuf,"xml:space=\"preserve\"\n"); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "style=\"fill:#FF0000;"); + sprintf(obuf,"font-size:%fpx;",tsp->fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + sprintf(obuf,"text-anchor:%s;",(tsp->ldir == LDIR_RL ? "end" : "start")); + TRPRINT(tri, obuf); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + sprintf(obuf,"\n\">%s</text>\n",&tsp->string[tsp->spaces]); + TRPRINT(tri, obuf); +#endif /* DBG_TR_INPUT debugging code, original text objects */ + } + } + } +#endif /* DBG_TR_PARA and/or DBG_TR_INPUT */ + + + if(tri->usebk){ + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + sprintf(stransform,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc), 1.25*x,1.25*y); + + for(i=cxi->phase1; i<cxi->used;i++){ /* over all complex members from phase2 == TR_PARA_* complexes */ + TRPRINT(tri, "<g>\n"); /* group backgrounds for each <text> object in the SVG */ + csp = &(cxi->cx[i]); + for(j=0; j<csp->kids.used; j++){ /* over all members of these complexes, which are phase1 complexes */ + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + cline_sp = &(cxi->cx[jdx]); + if(tri->usebk == BKCLR_LINE){ + TRPRINT(tri, "<rect\n"); + sprintf(obuf,"style=\"color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#%2.2X%2.2X%2.2X;;stroke:none;;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;clip-rule:nonzero\"\n",tri->bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[cline_sp->rt_cidx].xur - bri->rects[cline_sp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[cline_sp->rt_cidx].yll - bri->rects[cline_sp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[cline_sp->rt_cidx].xll),1.25*(bri->rects[cline_sp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + + for(k=0; k<cxi->cx[jdx].kids.used; k++){ /* over all members of the phase1 complex */ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi */ + tsp = &tpi->chunks[kdx]; + ldir = tsp->ldir; + if(!j && !k){ + if(tri->usebk == BKCLR_ALL){ + TRPRINT(tri, "<rect\n"); + sprintf(obuf,"style=\"color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#%2.2X%2.2X%2.2X;;stroke:none;;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;clip-rule:nonzero\"\n",tri->bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[csp->rt_cidx].xur - bri->rects[csp->rt_cidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].yll - bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",1.25*(bri->rects[csp->rt_cidx].xll),1.25*(bri->rects[csp->rt_cidx].yur)); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + } + if(tri->usebk == BKCLR_FRAG){ + newx = 1.25*(ldir == LDIR_RL ? bri->rects[tsp->rt_tidx].xur : bri->rects[tsp->rt_tidx].xll); + newy = 1.25*(bri->rects[tsp->rt_tidx].yur); + TRPRINT(tri, "<rect\n"); + sprintf(obuf,"style=\"color-interpolation:sRGB;color-interpolation-filters:linearRGB;fill:#%2.2X%2.2X%2.2X;;stroke:none;;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;clip-rule:nonzero\"\n",tri->bkcolor.Red,tri->bkcolor.Green,tri->bkcolor.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"width=\"%f\"\n", 1.25*(bri->rects[tsp->rt_tidx].xur - bri->rects[tsp->rt_tidx].xll)); + TRPRINT(tri, obuf); + sprintf(obuf,"height=\"%f\"\n",1.25*(bri->rects[tsp->rt_tidx].yll - bri->rects[tsp->rt_tidx].yur)); + TRPRINT(tri, obuf); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n",newx,newy); + TRPRINT(tri, obuf); + TRPRINT(tri, stransform); + TRPRINT(tri, "/>\n"); + } + } + } + TRPRINT(tri, "</g>\n"); /* end of grouping for backgrounds for each <text> object in the SVG */ + } + } + + + /* over all complex members from phase2. Paragraphs == TR_PARA_* */ + for(i=cxi->phase1; i<cxi->used;i++){ + csp = &(cxi->cx[i]); + esc = tri->esc; + esc *= 2.0 * M_PI / 360.0; /* degrees to radians and change direction of rotation */ + + /* over all members of the present Paragraph. Each of these is a line and a phase 1 complex. + It may be either TR_TEXT or TR_LINE */ + for(j=0; j<csp->kids.used; j++){ + if(j){ + sprintf(obuf,"</tspan>"); + TRPRINT(tri, obuf); + } + jdx = csp->kids.members[j]; /* index of phase1 complex (all are TR_TEXT or TR_LINE) */ + recenter = 0; /* mostly to quiet a compiler warning, should always be set below */ + + + /* over all members of the present Line. These are the original text objects which were reassembled. + There will be one for TR_TEXT, more than one for TR_LINE */ + for(k=0; k<cxi->cx[jdx].kids.used; k++){ + kdx = cxi->cx[jdx].kids.members[k]; /* index for text objects in tpi, for this k */ + tsp = &tpi->chunks[kdx]; /* text chunk for this k */ + ldir = tsp->ldir; /* language direction for this k */ + if(!k){ /* first iteration */ + switch(csp->type){ /* set up the alignment, if there is one */ + case TR_TEXT: + case TR_LINE: + /* these should never occur, this section quiets a compiler warning */ + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + if(ldir == LDIR_RL){ recenter = -(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll); } + else { recenter = 0.0; } + break; + case TR_PARA_CJ: + if(ldir == LDIR_RL){ recenter = -(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll)/2.0; } + else { recenter = +(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll)/2.0; } + break; + case TR_PARA_RJ: + if(ldir == LDIR_RL){ recenter = 0.0; } + else { recenter = +(bri->rects[cxi->cx[jdx].rt_cidx].xur - bri->rects[cxi->cx[jdx].rt_cidx].xll); } + break; + } + if(!j){ + TRPRINT(tri, "<text\n"); + TRPRINT(tri, "xml:space=\"preserve\"\n"); + TRPRINT(tri, "style=\""); + sprintf(obuf,"font-size:%fpx;",tsp->fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + if(tsp->vadvance){ lineheight = tsp->vadvance *100.0; } + else { lineheight = 125.0; } + sprintf(obuf,"line-height:%f%%;",lineheight); + TRPRINT(tri, obuf); + TRPRINT(tri, "letter-spacing:0px;"); + TRPRINT(tri, "word-spacing:0px;"); + TRPRINT(tri, "fill:#000000;"); + TRPRINT(tri, "fill-opacity:1;"); + TRPRINT(tri, "stroke:none;"); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + switch(csp->type){ /* set up the alignment, if there is one */ + case TR_TEXT: + case TR_LINE: + /* these should never occur, this section quiets a compiler warning */ + break; + case TR_PARA_UJ: + case TR_PARA_LJ: + sprintf(obuf,"text-align:start;text-anchor:start;"); + break; + case TR_PARA_CJ: + sprintf(obuf,"text-align:center;text-anchor:middle;"); + break; + case TR_PARA_RJ: + sprintf(obuf,"text-align:end;text-anchor:end;"); + break; + } + TRPRINT(tri, obuf); + TRPRINT(tri, "\"\n"); /* End of style specification */ + sprintf(obuf,"transform=\"matrix(%f,%f,%f,%f,%f,%f)\"\n",cos(esc),-sin(esc),sin(esc),cos(esc),1.25*x,1.25*y); + TRPRINT(tri, obuf); + tmpx = 1.25*((ldir == LDIR_RL ? bri->rects[kdx].xur : bri->rects[kdx].xll) + recenter); + sprintf(obuf,"x=\"%f\" y=\"%f\"\n>",tmpx,1.25*(bri->rects[kdx].yll - tsp->boff)); + TRPRINT(tri, obuf); + } + tmpx = 1.25*((ldir == LDIR_RL ? bri->rects[kdx].xur : bri->rects[kdx].xll) + recenter); + sprintf(obuf,"<tspan sodipodi:role=\"line\"\nx=\"%f\" y=\"%f\"\n>",tmpx,1.25*(bri->rects[kdx].yll - tsp->boff)); + TRPRINT(tri, obuf); + } + TRPRINT(tri, "<tspan\n"); + + /* Scale kerning and make any other necessary adjustments + + */ + dx = 1.25 * tsp->xkern; + dy = 1.25 * tsp->ykern; + + sprintf(obuf,"dx=\"%f\" dy=\"%f\" ",dx, dy); + TRPRINT(tri, obuf); + sprintf(obuf,"style=\"fill:#%2.2X%2.2X%2.2X;",tsp->color.Red,tsp->color.Green,tsp->color.Blue); + TRPRINT(tri, obuf); + sprintf(obuf,"font-size:%fpx;",tsp->fs*1.25); /*IMPORTANT, if the FS is given in pt it looks like crap in browsers. As if px != 1.25 pt, maybe 96 dpi not 90?*/ + TRPRINT(tri, obuf); + sprintf(obuf,"font-style:%s;",(tsp->italics ? "italic" : "normal")); + TRPRINT(tri, obuf); + if(tsp->decoration & TXTDECOR_TMASK){ + sprintf(obuf,"text-decoration:"); + /* multiple text decoration styles may be set */ + utmp = tsp->decoration & TXTDECOR_TMASK; + if(utmp & TXTDECOR_UNDER ){ strcat(obuf," underline"); } + if(utmp & TXTDECOR_OVER ){ strcat(obuf," overline"); } + if(utmp & TXTDECOR_BLINK ){ strcat(obuf," blink"); } + if(utmp & TXTDECOR_STRIKE){ strcat(obuf," line-through");} + if(*obuf){ + /* only a single text decoration line type may be set */ + switch(tsp->decoration & TXTDECOR_LMASK){ + case TXTDECOR_SOLID: break; // "solid" is the CSS 3 default, omitting it remains CSS 2 compatible + case TXTDECOR_DOUBLE: strcat(obuf," double"); break; // these are all CSS3 + case TXTDECOR_DOTTED: strcat(obuf," dotted"); break; + case TXTDECOR_DASHED: strcat(obuf," dashed"); break; + case TXTDECOR_WAVY: strcat(obuf," wavy" ); break; + default: break; + } + if((tsp->decoration & TXTDECOR_CLRSET) && memcmp(&(tsp->decColor),&(tsp->color),sizeof(TRCOLORREF))){ + /* CSS 3, CSS 2 implementations may choke on it. If the specified color matches text color omit, for better CSS 2 compatitiblity. */ + sprintf(cbuf," #%2.2X%2.2X%2.2X",tsp->decColor.Red,tsp->decColor.Green,tsp->decColor.Blue); + strcat(obuf,cbuf); + } + } + strcat(obuf,";"); + TRPRINT(tri,obuf); + } + TRPRINT(tri, "font-variant:normal;"); + sprintf(obuf,"font-weight:%d;",TR_weight_FC_to_SVG(tsp->weight)); + TRPRINT(tri, obuf); + sprintf(obuf,"font-stretch:%s;",(tsp->condensed==100 ? "Normal" : "Condensed")); + TRPRINT(tri, obuf); + cutat=strcspn((char *)fti->fonts[tsp->fi_idx].fontspec,":"); + sprintf(obuf,"font-family:%.*s;\"",cutat,fti->fonts[tsp->fi_idx].fontspec); + TRPRINT(tri, obuf); + TRPRINT(tri, "\n>"); + TRPRINT(tri, (char *) tsp->string); + TRPRINT(tri, "</tspan>"); + } /* end of k loop */ + } /* end of j loop */ + TRPRINT(tri,"</tspan></text>\n"); + } /* end of i loop */ + + /* restore locale and free memory. */ + (void) setlocale(LC_NUMERIC,hold_locale); + free(hold_locale); +} + +/** + \brief Attempt to figure out the original organization, in lines and paragraphs, of the text objects. + The method is: + 1. Generate complexes from the text objects (strings) by overlaps (optionally allowing up to two spaces to be + added) to produce larger rectangles. Complexes that are more or less sequential and have 2 or more text objects + are TR_LINEs, therwise they are TR_TEXT. + 2. Group sequential complexes (TR_LINE or TR_TEXT) into TR_PARA_UJ (paragraphs,by smooth progression in vertical + position down page). + 3. Analyze the paragraphs to classify them as Left/Center/Right justified (possibly with indentation.) If + they do not fall into any of these categories break that one back down into TR_LINE/TR_TEXT. + 4. Return the number of complex text objects. + \returns Number of complexes. (>=1, <= number of text objects.) <0 is an error. + \param tri pointer to the TR_INFO structure holding the data, which will also hold the results. +*/ +int TR_layout_analyze(TR_INFO *tri){ + unsigned int i,j,k; + int ok; + int cxidx; + int src_rt; + int dst_rt; + TP_INFO *tpi; + BR_INFO *bri; + CX_INFO *cxi; + FT_INFO *fti; + BRECT_SPECS bsp; + RT_PAD rt_pad_i; + RT_PAD rt_pad_j; + double ratio; + double qsp,dx,dy; + double spcadv; + enum tr_classes type; + TCHUNK_SPECS *tspi; + TCHUNK_SPECS *tspj; + TCHUNK_SPECS *tspRevEnd=NULL; + TCHUNK_SPECS *tspRevStart=NULL; + CX_SPECS *csp; + CHILD_SPECS *kidp; /* used with preceding complex (see below) */ + CHILD_SPECS *kidc; /* used with current complex (see below) */ + int lastldir,ldir,rev; + + if(!tri)return(-1); + if(!tri->cxi)return(-2); + if(!tri->tpi)return(-3); + if(!tri->bri)return(-4); + if(!tri->fti)return(-5); + tpi=tri->tpi; + cxi=tri->cxi; + bri=tri->bri; + fti=tri->fti; + cxi->lines = 0; + cxi->paras = 0; + cxi->phase1 = 0; + +/* When debugging + ftinfo_dump(fti); +*/ + /* Phase 1. Working sequentially, insert text. Initially as TR_TEXT and then try to extend to TR_LINE by checking + overlaps. When done the complexes will contain a mix of TR_LINE and TR_TEXT. */ + + for(i=0; i<tpi->used; i++){ + tspi = &(tpi->chunks[i]); + memcpy(&bsp,&(bri->rects[tspi->rt_tidx]),sizeof(BRECT_SPECS)); /* Must make a copy as next call may reallocate rects! */ + (void) brinfo_insert(bri,&bsp); + dst_rt = bri->used-1; + (void) cxinfo_insert(cxi, i, dst_rt, TR_TEXT); + cxidx = cxi->used-1; + + spcadv = fti->fonts[tspi->fi_idx].spcadv * tspi->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + /* for the leading text: pad with no leading and two trailing spaces, leading and trailing depend on direction */ + if(tspi->ldir == LDIR_RL){ TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, tri->qe + 2.0 * spcadv, 0.0); } + else { TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, 0.0, tri->qe + 2.0 * spcadv); } + + for(j=i+1; j<tpi->used; j++){ + tspj = &(tpi->chunks[j]); + /* Reject font size changes of greater than 50%, these are almost certainly not continuous text. These happen + in math formulas, for instance, where a sum or integral is much larger than the other symbols. */ + ratio = (double)(tspj->fs)/(double)(tspi->fs); + if(ratio >2.0 || ratio <0.5)break; + + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + /* for the trailing text: pad with one leading and trailing spaces (so it should work L->R and R->L) */ + TR_rt_pad_set(&rt_pad_j,tri->qe, tri->qe, spcadv, spcadv); + src_rt = tspj->rt_tidx; + + /* Reject direction changes like [1 <- Hebrew][2 -> English], that is where the direction changes AND the + next logical piece of text is "upstream" positionally of its logical predecessor. The meaning of such + a construct is at best ambiguous. The test is only applied with respect to the first text chunk. This sort + of construct may appear when a valid initial construct like [1->English][2<-Hebrew][3->English] is edited + and the leading chunk of text removed. + + Also reject reversed order text as in (English) <A><B><C> (draw order) arranged as <C><B><A>. This happens + if the language direction field is incorrect, perhaps due to a corrupt or malformed input file. + */ + if(brinfo_upstream(bri, + dst_rt, /* index into bri for dst */ + src_rt, /* index into bri for src */ + tspi->ldir,tspj->ldir))break; + + if(!brinfo_overlap(bri, + dst_rt, /* index into bri for dst */ + src_rt, /* index into bri for src */ + &rt_pad_i,&rt_pad_j)){ + (void) cxinfo_append(cxi,j,TR_LINE); + (void) brinfo_merge(bri,dst_rt,src_rt); + /* for the leading text: pad with two leading and trailing spaces (so it should work L->R and R->L */ + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; /* spcadv was always FT_LOAD_NO_SCALE */ + TR_rt_pad_set(&rt_pad_i, tri->qe, tri->qe, + tri->qe + 2.0 * spcadv, tri->qe + 2.0 * spcadv); + } + else { /* either alignment ge*/ + break; + } + } + + /* Bidirectional text will cause complexes to not assemble in one pass. + This happens whenever a change of direction occurs with 2 or more sequential elements in + the opposite direction, + + Let + = LR and - = RL. + + Reading left to right, this happens with +-- or -++. + For instance, the sequence ++-+ ---+ would break into the two complexes shown. + Not until the last element in the second complex is added will the bounding rectangles for the complexes overlap. + + Check for this effect now if there is a preceding complex and the first element of the current complex is + reversed from the last in the preceding. */ + if(cxidx >= 1){ + kidp = &(cxi->cx[cxidx-1].kids); + kidc = &(cxi->cx[cxidx ].kids); + tspi = &(tpi->chunks[ kidp->members[kidp->used - 1] ]); /* here, the last text element in preceding complex */ + tspj = &(tpi->chunks[ kidc->members[0 ] ]); /* here, tge first text element in current complex */ + if(tspi->ldir != tspj->ldir){ + spcadv = fti->fonts[tspi->fi_idx].spcadv * tspi->fs/32.0; + if(tspi->ldir == LDIR_RL){ TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, tri->qe + 2.0 * spcadv, 0.0); } + else { TR_rt_pad_set(&rt_pad_i,tri->qe, tri->qe, 0.0, tri->qe + 2.0 * spcadv); } + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; + TR_rt_pad_set(&rt_pad_j,tri->qe, tri->qe, spcadv, spcadv); + if(!brinfo_overlap(bri, + cxi->cx[cxidx-1].rt_cidx, /* index into rt for dst cx */ + cxi->cx[cxidx].rt_cidx, /* index into rt for src cx */ + &rt_pad_i,&rt_pad_j)){ + /* Merge the current complex into the preceding one*/ + (void) cxinfo_merge(cxi, cxidx-1, cxidx, TR_LINE); + (void) brinfo_merge(bri,cxi->cx[cxidx-1].rt_cidx,cxi->cx[cxidx].rt_cidx); /* merge the bounding boxes*/ + (void) cxinfo_trim(cxi); + cxi->lines--; /* else the normal line count value is one too high */ + /* remove the current complex */ + } + } + } + + if(cxi->cx[cxidx].type == TR_LINE)cxi->lines++; + i=j-1; /* start up after the last merged entry (there may not be any) */ + } + cxi->phase1 = cxi->used; /* total complexes defined in this phase, all TR_LINE or TR_TEXT */ + + /* phase 1.5, calculate kerning. This is as good a place to do it as any. At this point all kern values + are zero. Each of these pieces is strictly unidirectional, but each piece can have a different direction. + The direction of the line is set by the first text element. The ends of runs of elements which are + reversed with respect to the line direction are special, everything else is simple: + Let: + == L->R, - == R->L, $ == end of text, the rules for kerning on B are: + A B others xkern + [+|$] + + [+|$] Bll - Aur + [-|$] - - [-|$] All - Bur (chs) + + - + [-|$] Bll - Aur (chs) + - + - [+|$] All - Bur + + - -...[-=C] [+|$] All - Cur (chs) + - + +...[+=C] [-|$] Cll - Aur + + chs = change sign, because dx is an absolute direction, and direction of text on RTL is in -x. + + Kerning calculations currently seems unstable for R->L if the kerning extends to the end of the line. If + the first and last characters are back in sync there are no issues. When things go south R->L left justified + text is not justified when read in. + */ + + for(i=0; i < cxi->phase1; i++){ /* over all lines */ + csp = &(cxi->cx[i]); + if(csp->kids.used < 2)continue; /* no kerning possible */ + tspi = &tpi->chunks[csp->kids.members[0]]; /* used here as last tsp. no kerning is applied to the first element */ + lastldir = ldir = tspi->ldir; + rev = 0; /* the first ldir defines forward and reverse */ + for(j=1; j<csp->kids.used; j++){ + tspj = &tpi->chunks[csp->kids.members[j]]; + ldir = tspj->ldir; + if(ldir != lastldir){ /* direction change */ + rev = !rev; /* reverse direction tracker */ + if(!rev){ /* back in original orientation */ + if(ldir == LDIR_RL){ tspj->xkern = bri->rects[tspj->rt_tidx].xur - bri->rects[tspRevStart->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspj->rt_tidx].xll - bri->rects[tspRevStart->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspj->rt_tidx].yll - tspj->boff) - + (bri->rects[tspRevStart->rt_tidx].yll - tspRevStart->boff); + } + else { /* now in reversed orientation */ + tspRevStart = tspj; /* Save the beginning of this run (length >=1 ) */ + /* scan forward for the last text object in this orientation, include the first */ + for(k=j; k <csp->kids.used; k++){ + if(tpi->chunks[csp->kids.members[k]].ldir == ldir){ tspRevEnd = &tpi->chunks[csp->kids.members[k]]; } + else { break; } + } + if(lastldir == LDIR_RL){ tspj->xkern = bri->rects[tspRevEnd->rt_tidx].xur - bri->rects[tspi->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspRevEnd->rt_tidx].xll - bri->rects[tspi->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspRevEnd->rt_tidx].yll - tspRevEnd->boff) - + (bri->rects[ tspi->rt_tidx].yll - tspi->boff ); + } + } + else { + if(ldir == LDIR_RL){ tspj->xkern = bri->rects[tspj->rt_tidx].xur - bri->rects[tspi->rt_tidx].xll; } + else { tspj->xkern = bri->rects[tspj->rt_tidx].xll - bri->rects[tspi->rt_tidx].xur; } + tspj->ykern = (bri->rects[tspj->rt_tidx].yll - tspj->boff) - + (bri->rects[tspi->rt_tidx].yll - tspi->boff); + } + + + /* + Sometimes a font substitution was absolutely terrible, for instance, for Arial Narrow on (most) Linux systems, + The resulting advance (xkern) may be much too large so that it overruns the next text chunk. Since + overlapping text on the same line is almost never encountered, this may be used to detect the bad + substitution so that a more appropriate offset can be used. + Detect this situation as a negative dx < 1/2 a space character's width while |dy| < an entire space width. + The y constraints allow super and subscripts, which overlap in x but are shifted above/below in y. + */ + spcadv = fti->fonts[tspj->fi_idx].spcadv * tspj->fs/32.0; + qsp = 0.25 * spcadv; + dx = tspj->xkern; + dy = tspj->ykern; + if(dy <=qsp && dy >= -qsp){ + if(ldir==LDIR_RL){ + if(dx > 2*qsp)tspj->xkern = 0.0; + } + else { + if(dx < -2*qsp)tspj->xkern = 0.0; + } + } + + /* if x or y kern is less than twice the quantization error it is probably noise, set it to zero */ + if(fabs(tspj->xkern) <= 2.0*tri->qe)tspj->xkern = 0.0; + if(fabs(tspj->ykern) <= 2.0*tri->qe)tspj->ykern = 0.0; + + /* reintroduce spaces on the leading edge of text "j" if the kerning can be in part or in whole replaced + with 1 or 2 spaces */ + if(tspj->ykern == 0.0){ + double spaces = tspj->xkern/spcadv; /* negative on RL language, positive on LR */ + if((ldir == LDIR_RL && (spaces <= -0.9 && spaces >= -2.1)) || + (ldir == LDIR_LR && (spaces >= 0.9 && spaces <= 2.1)) ){ + int ispaces = lround(spaces); + tspj->xkern -= ((double)ispaces*spcadv); + if(ispaces<0)ispaces=-ispaces; + size_t slen = strlen((char *)tspj->string); + uint8_t *newstring = malloc(1 + ispaces + slen); + sprintf((char *)newstring," "); /* start with two spaces, possibly overwrite one in the next line */ + memcpy(newstring+ispaces,tspj->string,slen+1); /* copy existing string to proper position */ + free(tspj->string); + tspj->string = newstring; + tspj->spaces = ispaces; // only needed to fix optional debugging SVG output later + } + } + + tspi = tspj; + lastldir = ldir; + } + } + + + /* Phase 2, try to group sequential lines. There may be "lines" that are still TR_TEXT, as in: + + ... this is a sentence that wraps by one + word. + + And some paragrahs might be single word lines (+ = bullet in the following) + + +verbs + +nouns + +adjectives + + Everything starts out as TR_PARA_UJ and if the next one can be lined up, the type changes to + an aligned paragraph and complexes are appended to the existing one. + */ + + for(i=0; i < cxi->phase1; i++){ + type = TR_PARA_UJ; /* any paragraph alignment will be acceptable */ + /* Must make a copy as next call may reallocate rects, so if we just passed a pointer to something in the structure + it would vaporize part way through the call. */ + memcpy(&bsp,&(bri->rects[cxi->cx[i].rt_cidx]),sizeof(BRECT_SPECS)); + (void) brinfo_insert(bri,&bsp); + dst_rt = bri->used-1; + (void) cxinfo_insert(cxi, i, dst_rt, type); + + cxi->paras++; + ok = 1; + for(j=i+1; ok && (j < cxi->phase1); j++){ + type = brinfo_pp_alignment(bri, cxi->cx[i].rt_cidx, cxi->cx[j].rt_cidx, 3*tri->qe, type); + switch (type){ + case TR_PARA_UJ: /* paragraph type was set and j line does not fit, or no paragraph alignment matched */ + ok = 0; /* force exit from j loop */ + j--; /* this will increment at loop bottom */ + break; + case TR_PARA_LJ: + case TR_PARA_CJ: + case TR_PARA_RJ: + /* two successive lines have been identified (possible following others already in the paragraph */ + if(TR_check_set_vadvance(tri,j,i)){ /* check for compatibility with vadvance if set, set it if it isn't. */ + ok = 0; /* force exit from j loop */ + j--; /* this will increment at loop bottom */ + } + else { + src_rt = cxi->cx[j].rt_cidx; + (void) cxinfo_append(cxi, j, type); + (void) brinfo_merge(bri, dst_rt, src_rt); + } + break; + default: + return(-6); /* programming error */ + } + } + if(j>=cxi->phase1)break; + i=j-1; + } + + +/* When debugging + cxinfo_dump(tri); +*/ + + return(cxi->used); +} + + +/* no doxygen documentation below this point, these pieces are for the text program, not the library. */ + +#if TEST +#define MAXLINE 2048 /* big enough for testing */ +enum OP_TYPES {OPCOM,OPOOPS,OPFONT,OPESC,OPORI,OPXY,OPFS,OPTEXT,OPALN,OPLDIR,OPMUL,OPITA,OPWGT,OPDEC,OPCND,OPBKG,OPCLR,OPDCLR,OPBCLR,OPFLAGS,OPEMIT,OPDONE}; + +int parseit(char *buffer,char **data){ + int pre; + pre = strcspn(buffer,":"); + if(!pre)return(OPOOPS); + *data=&buffer[pre+1]; + buffer[pre]='\0'; + if(*buffer=='#' )return(OPCOM ); + if(0==strcmp("FONT",buffer))return(OPFONT); + if(0==strcmp("ESC" ,buffer))return(OPESC ); + if(0==strcmp("ORI", buffer))return(OPORI ); + if(0==strcmp("XY", buffer))return(OPXY ); + if(0==strcmp("FS", buffer))return(OPFS ); + if(0==strcmp("TEXT",buffer))return(OPTEXT); + if(0==strcmp("ALN", buffer))return(OPALN ); + if(0==strcmp("LDIR",buffer))return(OPLDIR); + if(0==strcmp("MUL", buffer))return(OPMUL ); + if(0==strcmp("ITA", buffer))return(OPITA ); + if(0==strcmp("WGT", buffer))return(OPWGT ); + if(0==strcmp("DEC", buffer))return(OPDEC ); + if(0==strcmp("CND", buffer))return(OPCND ); + if(0==strcmp("BKG", buffer))return(OPBKG ); + if(0==strcmp("CLR", buffer))return(OPCLR ); + if(0==strcmp("DCLR", buffer))return(OPDCLR ); + if(0==strcmp("BCLR",buffer))return(OPBCLR ); + if(0==strcmp("FLAG",buffer))return(OPFLAGS); + if(0==strcmp("EMIT",buffer))return(OPEMIT); + if(0==strcmp("DONE",buffer))return(OPDONE); + return(OPOOPS); +} + +void boom(char *string,int lineno){ + fprintf(stderr,"Fatal error at line %d %s\n",lineno,string); + exit(EXIT_FAILURE); +} + + +void init_as_svg(TR_INFO *tri){ + TRPRINT(tri,"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"); + TRPRINT(tri,"<!-- Created with Inkscape (http://www.inkscape.org/) -->\n"); + TRPRINT(tri,"\n"); + TRPRINT(tri,"<svg\n"); + TRPRINT(tri," xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n"); + TRPRINT(tri," xmlns:cc=\"http://creativecommons.org/ns#\"\n"); + TRPRINT(tri," xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"); + TRPRINT(tri," xmlns:svg=\"http://www.w3.org/2000/svg\"\n"); + TRPRINT(tri," xmlns=\"http://www.w3.org/2000/svg\"\n"); + TRPRINT(tri," xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\n"); + TRPRINT(tri," xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"\n"); + TRPRINT(tri," width=\"900\"\n"); + TRPRINT(tri," height=\"675\"\n"); + TRPRINT(tri," id=\"svg4122\"\n"); + TRPRINT(tri," version=\"1.1\"\n"); + TRPRINT(tri," inkscape:version=\"0.48+devel r11679 custom\"\n"); + TRPRINT(tri," sodipodi:docname=\"simplest_text.svg\">\n"); + TRPRINT(tri," <defs\n"); + TRPRINT(tri," id=\"defs4124\" />\n"); + TRPRINT(tri," <sodipodi:namedview\n"); + TRPRINT(tri," id=\"base\"\n"); + TRPRINT(tri," pagecolor=\"#ffffff\"\n"); + TRPRINT(tri," bordercolor=\"#666666\"\n"); + TRPRINT(tri," borderopacity=\"1.0\"\n"); + TRPRINT(tri," inkscape:pageopacity=\"0.0\"\n"); + TRPRINT(tri," inkscape:pageshadow=\"2\"\n"); + TRPRINT(tri," inkscape:zoom=\"0.98994949\"\n"); + TRPRINT(tri," inkscape:cx=\"309.88761\"\n"); + TRPRINT(tri," inkscape:cy=\"482.63995\"\n"); + TRPRINT(tri," inkscape:document-units=\"px\"\n"); + TRPRINT(tri," inkscape:current-layer=\"layer1\"\n"); + TRPRINT(tri," showgrid=\"false\"\n"); + TRPRINT(tri," width=\"0px\"\n"); + TRPRINT(tri," height=\"0px\"\n"); + TRPRINT(tri," fit-margin-top=\"0\"\n"); + TRPRINT(tri," fit-margin-left=\"0\"\n"); + TRPRINT(tri," fit-margin-right=\"0\"\n"); + TRPRINT(tri," fit-margin-bottom=\"0\"\n"); + TRPRINT(tri," units=\"in\"\n"); + TRPRINT(tri," inkscape:window-width=\"1200\"\n"); + TRPRINT(tri," inkscape:window-height=\"675\"\n"); + TRPRINT(tri," inkscape:window-x=\"26\"\n"); + TRPRINT(tri," inkscape:window-y=\"51\"\n"); + TRPRINT(tri," inkscape:window-maximized=\"0\" />\n"); + TRPRINT(tri," <metadata\n"); + TRPRINT(tri," id=\"metadata4127\">\n"); + TRPRINT(tri," <rdf:RDF>\n"); + TRPRINT(tri," <cc:Work\n"); + TRPRINT(tri," rdf:about=\"\">\n"); + TRPRINT(tri," <dc:format>image/svg+xml</dc:format>\n"); + TRPRINT(tri," <dc:type\n"); + TRPRINT(tri," rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />\n"); + TRPRINT(tri," <dc:title></dc:title>\n"); + TRPRINT(tri," </cc:Work>\n"); + TRPRINT(tri," </rdf:RDF>\n"); + TRPRINT(tri," </metadata>\n"); + TRPRINT(tri," <g\n"); + TRPRINT(tri," inkscape:label=\"Layer 1\"\n"); + TRPRINT(tri," inkscape:groupmode=\"layer\"\n"); + TRPRINT(tri," id=\"layer1\"\n"); + TRPRINT(tri," transform=\"translate(0,0)\">\n"); + TRPRINT(tri,"\n"); +} + + +void flush_as_svg(TR_INFO *tri, FILE *fp){ + fwrite(tri->out,tri->outused,1,fp); +} + +FILE *close_as_svg(TR_INFO *tri, FILE *fp){ + TRPRINT(tri, " </g>\n"); + TRPRINT(tri, "</svg>\n"); + flush_as_svg(tri,fp); + fclose(fp); + return(NULL); +} + + +int main(int argc, char *argv[]){ + char *data; + char inbuf[MAXLINE]; + FILE *fpi = NULL; + FILE *fpo = NULL; + int op; + double fact = 1.0; /* input units to points */ + double escapement = 0.0; /* degrees */ + int lineno = 0; + int ok = 1; + int status; + TCHUNK_SPECS tsp; + TR_INFO *tri=NULL; + int flags=0; + char *infile; + uint32_t utmp32; + TRCOLORREF bkcolor; + int bkmode; + char *fontspec; + + infile=malloc(strlen(argv[1])+1); + strcpy(infile,argv[1]); + + if(argc < 2 || !(fpi = fopen(infile,"r"))){ + printf("Usage: text_reassemble input_file\n"); + printf(" Test program reads an input file containing lines like:\n"); + printf(" FONT:(font for next text)\n"); + printf(" ESC:(escapement angle degrees of text line, up from X axis)\n"); + printf(" ORI:(angle degrees of character orientation, up from X axis)\n"); + printf(" FS:(font size, units)\n"); + printf(" XY:(x,y) X 0 is at left, N is at right, Y 0 is at top, N is at bottom, as page is viewed.\n"); + printf(" TEXT:(UTF8 text)\n"); + printf(" ALN:combination of {LCR}{BLT} = Text is placed on {X,Y} at Left/Center/Right of text, at Bottom,baseLine,Top of text.\n"); + printf(" LDIR:{LR|RL|TB) Left to Right, Right to Left, and Top to Bottom \n"); + printf(" MUL:(float, multiplicative factor to convert FS,XY units to points).\n"); + printf(" ITA:(Italics, 0=normal, 100=italics, 110=oblique).\n"); + printf(" WGT:(Weight, 0-215: 80=normal, 200=bold, 215=ultrablack, 0=thin)).\n"); + printf(" DEC:(this is a bit field. For color see DCLR\n"); + printf(" style: 000 none, 001 underline,002 overline, 004 blink, 008 strike-through\n"); + printf(" line: 000 solid, 010 double, 020 dotted, 040 dashed, 080 wavy)\n"); + printf(" CND:(Condensed 50-200: 100=normal, 50=ultracondensed, 75=condensed, 200=expanded).\n"); + printf(" BKG:(Background color: 0 none, 1 by input fragment, 2 by assembled line, 3 by entire assembly. Use BCLR, THEN BKG) \n"); + printf(" CLR:(Text RGB color, as 6 HEX digits, like: FF0000 (red) or 0000FF (blue)) \n"); + printf(" DCLR:(Decoration color, specify like CLR, except 1000000 or higher disables.)\n"); + printf(" BCLR:(Background RGB color, specify like CLR.) \n"); + printf(" FLAG: Special processing options. 1 EMF compatible text alignment.\n"); + printf(" EMIT:(Process everything up to this point, then start clean for remaining input).\n"); + printf(" DONE:(no more input, process it).\n"); + printf(" # comment\n"); + printf("\n"); + printf(" The output is a summary of how the pieces are to be assembled into complex text.\n"); + printf("\n"); + printf(" egrep pattern: '^LOAD:|^FONT:|^ESC:|^ORI:|^FS:|^XY:|^TEXT:|^ALN:|^LDIR:|^MUL:|^ITA:|^WGT:|^DEC:|^CND:|^BKG:|^CLR:|^BCLR:|^DCLR:|^FLAG:|^EMIT:^DONE:'\n"); + exit(EXIT_FAILURE); + } + + tri = trinfo_init(tri); /* If it loops the trinfo_clear at the end will reset tri to the proper state, do NOT call trinfo_init twice! */ + +#ifdef DBG_LOOP + int ldx; + for(ldx=0;ldx<5;ldx++){ + if(fpi)fclose(fpi); + fpi = fopen(infile,"r"); +#endif + tsp.string = NULL; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.color.Red = tsp.decColor.Red = 0; /* RGBA Black */ + tsp.color.Green = tsp.decColor.Green = 0; /* RGBA Black */ + tsp.color.Blue = tsp.decColor.Blue = 0; /* RGBA Black */ + tsp.color.Reserved = tsp.decColor.Reserved = 0; /* unused */ + tsp.italics = 0; + tsp.weight = 80; + tsp.condensed = 100; + tsp.decoration = 0; /* none */ + tsp.spaces = 0; /* none */ + tsp.fi_idx = -1; /* set to an invalid */ + tsp.rt_tidx = -1; /* set to an invalid */ + tsp.xkern = tsp.ykern = 0.0; + /* no need to set rt_tidx */ + + + + if(!tri){ + fprintf(stderr,"Fatal error, could not initialize data structures\n"); + exit(EXIT_FAILURE); + } + (void) trinfo_load_ft_opts(tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + fpo=fopen("dump.svg","wb"); + init_as_svg(tri); + + while(ok){ + lineno++; + if(!fgets(inbuf,MAXLINE,fpi))boom("Unexpected end of file - no DONE:",lineno); + inbuf[strlen(inbuf)-1]='\0'; /* step on the EOL character */ + op = parseit(inbuf,&data); + switch(op){ + case OPCOM: /* ignore comments*/ + break; + case OPFONT: + /* If the font name includes "Narrow" condensed may not have been set */ + if(0<= TR_findcasesub(data, "Narrow")){ + tsp.co=1; + } + else { + tsp.co=0; + } + fontspec = TR_construct_fontspec(&tsp, data); + if((tsp.fi_idx = ftinfo_load_fontname(tri->fti, fontspec)) < 0 )boom("Font load failed",lineno); + free(fontspec); + break; + case OPESC: + if(1 != sscanf(data,"%lf",&escapement))boom("Invalid ESC:",lineno); + break; + case OPORI: + if(1 != sscanf(data,"%lf",&tsp.ori))boom("Invalid ORI:",lineno); + break; + case OPFS: + if(1 != sscanf(data,"%lf",&tsp.fs) || tsp.fs <= 0.0)boom("Invalid FS:",lineno); + tsp.fs *= fact; + break; + case OPXY: + if(2 != sscanf(data,"%lf,%lf",&tsp.x,&tsp.y) )boom("Invalid XY:",lineno); + tsp.x *= fact; + tsp.y *= fact; + break; + case OPTEXT: + tsp.string = (uint8_t *) U_strdup(data); + /* FreeType parameters match inkscape*/ + status = trinfo_load_textrec(tri, &tsp, escapement,flags); + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + if(trinfo_load_textrec(tri, &tsp, escapement,flags)){ boom("Text load failed",lineno); } + } + else if(status){ boom("Text load failed",lineno); } + break; + case OPALN: + tsp.taln=0; + switch (*data++){ + case 'L': tsp.taln |= ALILEFT; break; + case 'C': tsp.taln |= ALICENTER; break; + case 'R': tsp.taln |= ALIRIGHT; break; + default: boom("Invalid ALN:",lineno); + } + switch (*data++){ + case 'T': tsp.taln |= ALITOP; break; + case 'L': tsp.taln |= ALIBASE; break; + case 'B': tsp.taln |= ALIBOT; break; + default: boom("Invalid ALN:",lineno); + } + break; + case OPLDIR: + tsp.ldir=0; + if(0==strcmp("LR",data)){ tsp.ldir=LDIR_LR; break;} + if(0==strcmp("RL",data)){ tsp.ldir=LDIR_RL; break;} + if(0==strcmp("TB",data)){ tsp.ldir=LDIR_TB; break;} + boom("Invalid LDIR:",lineno); + break; + case OPMUL: + if(1 != sscanf(data,"%lf",&fact) || fact <= 0.0)boom("Invalid MUL:",lineno); + (void) trinfo_load_qe(tri,fact); + break; + case OPITA: + if(1 != sscanf(data,"%d",&tsp.italics) || tsp.italics < 0 || tsp.italics>110)boom("Invalid ITA:",lineno); + break; + case OPWGT: + if(1 != sscanf(data,"%d",&tsp.weight) || tsp.weight < 0 || tsp.weight > 215)boom("Invalid WGT:",lineno); + break; + case OPDEC: + if(1 != sscanf(data,"%X",(unsigned int *) &tsp.decoration))boom("Invalid DEC:",lineno); + break; + case OPCND: + if(1 != sscanf(data,"%d",&tsp.condensed) || tsp.condensed < 50 || tsp.condensed > 200)boom("Invalid CND:",lineno); + break; + case OPBKG: + if(1 != sscanf(data,"%d",&bkmode) )boom("Invalid BKG:",lineno); + (void) trinfo_load_bk(tri,bkmode,bkcolor); + break; + case OPCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid CLR:",lineno); + tsp.color.Red = (utmp32 >> 16) & 0xFF; + tsp.color.Green = (utmp32 >> 8) & 0xFF; + tsp.color.Blue = (utmp32 >> 0) & 0xFF; + tsp.color.Reserved = 0; + break; + case OPDCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid DCLR:",lineno); + if(utmp32 >= 0x1000000){ + tsp.decColor.Red = tsp.decColor.Green = tsp.decColor.Blue = tsp.decColor.Reserved = 0; + tsp.decoration &= ~TXTDECOR_CLRSET; + } + else { + tsp.decColor.Red = (utmp32 >> 16) & 0xFF; + tsp.decColor.Green = (utmp32 >> 8) & 0xFF; + tsp.decColor.Blue = (utmp32 >> 0) & 0xFF; + tsp.decColor.Reserved = 0; + tsp.decoration |= TXTDECOR_CLRSET; + } + break; + case OPBCLR: + if(1 != sscanf(data,"%x",&utmp32) )boom("Invalid BCLR:",lineno); + bkcolor.Red = (utmp32 >> 16) & 0xFF; + bkcolor.Green = (utmp32 >> 8) & 0xFF; + bkcolor.Blue = (utmp32 >> 0) & 0xFF; + bkcolor.Reserved = 0; + break; + case OPFLAGS: + if(1 != sscanf(data,"%d",&flags) )boom("Invalid FLAG:",lineno); + break; + case OPEMIT: + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + break; + case OPDONE: + TR_layout_analyze(tri); + TR_layout_2_svg(tri); + flush_as_svg(tri, fpo); + tri = trinfo_clear(tri); + ok = 0; + break; + case OPOOPS: + default: + boom("Input line cannot be parsed",lineno); + break; + } + + } + + if(fpo){ + fpo=close_as_svg(tri, fpo); + } + + +#ifdef DBG_LOOP + tri = trinfo_clear(tri); + ok = 1; + } +#endif /* DBG_LOOP */ + + fclose(fpi); + tri = trinfo_release(tri); + free(infile); + + exit(EXIT_SUCCESS); +} +#endif /* TEST */ + +#ifdef __cplusplus +} +#endif diff --git a/src/extension/internal/text_reassemble.h b/src/extension/internal/text_reassemble.h new file mode 100644 index 0000000..25b556a --- /dev/null +++ b/src/extension/internal/text_reassemble.h @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * text_reassemble.h from libTERE + *//* + * Authors: see below + * + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2.0+, read the file 'COPYING' for more information. + */ +/** + @file text_reassemble.h libTERE headers. + +See text_reassemble.c for notes + +File: text_reassemble.h +Version: 0.0.13 +Date: 06-FEB-2014 +Author: David Mathog, Biology Division, Caltech +email: mathog@caltech.edu +Copyright: 2014 David Mathog and California Institute of Technology (Caltech) +*/ + +#ifndef _TEXT_REASSEMBLE_ +#define _TEXT_REASSEMBLE_ + +#ifdef __cplusplus +extern "C" { +#endif + + +#include <stdlib.h> //NOLINT +#include <stdio.h> //NOLINT +#include <math.h> //NOLINT +#include <stdint.h> //NOLINT +#include <ctype.h> //NOLINT +#include <fontconfig/fontconfig.h> +#include <ft2build.h> +#include <iconv.h> +#include FT_FREETYPE_H +#include FT_GLYPH_H + +/** \cond */ +#define TEREMIN(A,B) (A < B ? A : B) +#define TEREMAX(A,B) (A > B ? A : B) + +#ifndef M_PI +# define M_PI 3.14159265358979323846 /* pi */ +#endif +#define ALLOCINFO_CHUNK 32 +#define ALLOCOUT_CHUNK 8192 +#define TRPRINT trinfo_append_out +/** \endcond */ + +/** \defgroup color background options + Text is underwritten with the background color not at all, + by reassembled line, or by full assembly . + @{ +*/ +#define BKCLR_NONE 0x00 /**< text is not underwritten with background color (default) */ +#define BKCLR_FRAG 0x01 /**< each fragment of text is underwritten with background color */ +#define BKCLR_LINE 0x02 /**< each line of text is underwritten with background color */ +#define BKCLR_ALL 0x03 /**< entire assembly is underwritten with background color */ +/** @} */ + +/** \defgroup decoration options + One of these values may be present in the decoration field. + Unused bits may be used by end user code. + These values are SVG specific. Other applications could use the text + decoration field for a different set of bits, so long as it provided its own + output function. + @{ +*/ +#define TXTDECOR_NONE 0x000 /**< text is not decorated (default) */ +#define TXTDECOR_UNDER 0x001 /**< underlined */ +#define TXTDECOR_OVER 0x002 /**< overlined */ +#define TXTDECOR_BLINK 0x004 /**< blinking text */ +#define TXTDECOR_STRIKE 0x008 /**< strike through */ +#define TXTDECOR_TMASK 0x00F /**< Mask for selecting bits above */ + +#define TXTDECOR_SOLID 0x000 /**< draw as single solid line */ +#define TXTDECOR_DOUBLE 0x010 /**< draw as double solid line */ +#define TXTDECOR_DOTTED 0x020 /**< draw as single dotted line */ +#define TXTDECOR_DASHED 0x040 /**< draw as single dashed line */ +#define TXTDECOR_WAVY 0x080 /**< draw as single wavy line */ +#define TXTDECOR_LMASK 0x0F0 /**< Mask for selecting these bits */ + +#define TXTDECOR_CLRSET 0x100 /**< decoration has its own color */ + +/** @} */ + + + + + +/** \defgroup text alignment types + Location of text's {X,Y} coordinate on bounding rectangle. + Values are compatible with Fontconfig. + @{ +*/ +#define ALILEFT 0x01 /**< text object horizontal alignment = left */ +#define ALICENTER 0x02 /**< text object horizontal alignment = center */ +#define ALIRIGHT 0x04 /**< text object horizontal alignment = right */ +#define ALIHORI 0x07 /**< text object horizontal alignment mask */ +#define ALITOP 0x08 /**< text object vertical alignment = top */ +#define ALIBASE 0x10 /**< text object vertical alignment = baseline */ +#define ALIBOT 0x20 /**< text object vertical alignment = bottom */ +#define ALIVERT 0x38 /**< text object vertical alignment mask */ +/** @} */ + +/** \defgroup language direction types + @{ +*/ +#define LDIR_LR 0x00 /**< left to right */ +#define LDIR_RL 0x01 /**< right to left */ +#define LDIR_TB 0x02 /**< top to bottom */ +/** @} */ + +/** \defgroup special processing flags + @{ +*/ +#define TR_EMFBOT 0x01 /**< use an approximation compatible with EMF file's "BOTTOM" text orientation, which is not the "bottom" for Freetype fonts */ +/** @} */ + +/** \enum tr_classes +classification of complexes + @{ +*/ +enum tr_classes { + TR_TEXT, /**< simple text object */ + TR_LINE, /**< linear assembly of TR_TEXTs */ + TR_PARA_UJ, /**< sequential assembly of TR_LINEs and TR_TEXTs into a paragraph - + unknown justification properties */ + TR_PARA_LJ, /**< ditto, left justified */ + TR_PARA_CJ, /**< ditto, center justified */ + TR_PARA_RJ /**< ditto, right justified */ + }; +/** @} */ + +/** + \brief alt font entries. +*/ +typedef struct { + uint32_t fi_idx; /**< index into FT_INFO fonts, for fonts added for missing glyphs */ + uint32_t weight; /**< integer weight for alt fonts, kept sorted into descending order */ +} ALT_SPECS; + +/** + \brief Information for a font instance. +*/ +typedef struct { + FcFontSet *fontset; /**< all matching fonts (for fallback on missing glyphs) */ + ALT_SPECS *alts; /**< index into FT_INFO fonts, for fonts added for missing glyphs */ + uint32_t space; /**< alts storage slots allocated */ + uint32_t used; /**< alts storage slots in use */ + FT_Face face; /**< font face structures (FT_FACE is a pointer!) */ + uint8_t *file; /**< pointer to font paths to files */ + uint8_t *fontspec; /**< pointer to a font specification (name:italics, etc.) */ + FcPattern *fpat; /**< current font, must hang onto this or faces operations break */ + double spcadv; /**< advance equal to a space, in points at font's face size */ + double fsize; /**< font's face size in points */ +} FNT_SPECS; + +/** + \brief Information for all font instances. +*/ +typedef struct { + FT_Library library; /**< Fontconfig handle */ + FNT_SPECS *fonts; /**< Array of fontinfo structures */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} FT_INFO; + +typedef struct { + uint8_t Red; //!< Red color (0-255) + uint8_t Green; //!< Green color (0-255) + uint8_t Blue; //!< Blue color (0-255) + uint8_t Reserved; //!< Not used +} TRCOLORREF; + +/** + \brief Information for a single text object +*/ +typedef struct { + uint8_t *string; /**< UTF-8 text */ + double ori; /**< Orientation, angle of characters with respect to baseline in degrees */ + double fs; /**< font size of text */ + double x; /**< x coordinate, relative to TR_INFO x,y, in points */ + double y; /**< y coordinate, relative to TR_INFO x,y, in points */ + double xkern; /**< x kern relative to preceding text chunk in complex (if any) */ + double ykern; /**< y kern relative to preceding text chunk in complex (if any) */ + double boff; /**< Y LL corner - boff finds baseline */ + double vadvance; /**< Line spacing typically 1.25 or 1.2, only set on the first text + element in a complex */ + TRCOLORREF color; /**< RGB */ + int taln; /**< text alignment with respect to x,y */ + int ldir; /**< language direction LDIR_* */ + int italics; /**< italics, as in FontConfig */ + int weight; /**< weight, as in FontConfig */ + int condensed; /**< condensed, as in FontConfig */ + int decoration; /**< text decorations, ignored during assembly, used during output */ + int spaces; /**< count of spaces converted from wide kerning (1 or 2) */ + TRCOLORREF decColor; /**< text decoration color, ignored during assembly, used during output */ + int co; /**< condensed override, if set Font name included narrow */ + int rt_tidx; /**< index of rectangle that contains it */ + int fi_idx; /**< index of the font it uses */ +} TCHUNK_SPECS; + +/** + \brief Information for all text objects. + Coordinates here are INTERNAL, after offset/rotate using values in TR_INFO. +*/ +typedef struct { + TCHUNK_SPECS *chunks; /**< text chunks */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} TP_INFO; + +/** + \brief Information for a single bounding rectangle. + Coordinates here are INTERNAL, after offset/rotate using values in TR_INFO. +*/ +typedef struct { + double xll; /**< x rectangle lower left corner */ + double yll; /**< y " */ + double xur; /**< x upper right corner */ + double yur; /**< y " */ + double xbearing; /**< x bearing of the leftmost character */ +} BRECT_SPECS; + +/** + \brief Information for all bounding rectangles. +*/ +typedef struct { + BRECT_SPECS *rects; /**< bounding rectangles */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} BR_INFO; + +/** + \brief List of all members of a single complex. +*/ +typedef struct { + int *members; /**< array of immediate children (for TR_PARA_* these are indices + for TR_TEXT or TR_LINE complexes also in cxi. For TR_TEXT + and TR_LINE these are indices to the actual text in tpi.) */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ +} CHILD_SPECS; + +/** + \brief Information for a single complex. +*/ +typedef struct { + int rt_cidx; /**< index of rectangle that contains all members */ + enum tr_classes type; /**< classification of the complex */ + CHILD_SPECS kids; /**< immediate child nodes of this complex, for type TR_TEXT the + idx refers to the tpi data. otherwise, cxi data */ +} CX_SPECS; + +/** + \brief Information for all complexes. +*/ +typedef struct { + CX_SPECS *cx; /**< complexes */ + uint32_t space; /**< storage slots allocated */ + uint32_t used; /**< storage slots in use */ + uint32_t phase1; /**< Number of complexes (lines + text fragments) entered in phase 1 */ + uint32_t lines; /**< Number of lines in phase 1 */ + uint32_t paras; /**< Number of complexes (paras) entered in phase 2 */ +} CX_INFO; + +/** + \brief Information for the entire text reassembly system. +*/ +typedef struct { + FT_INFO *fti; /**< Font info storage */ + TP_INFO *tpi; /**< Text Info/Position Info storage */ + BR_INFO *bri; /**< Bounding Rectangle Info storage */ + CX_INFO *cxi; /**< Complex Info storage */ + uint8_t *out; /**< buffer to hold formatted output */ + double qe; /**< quantization error in points. */ + double esc; /**< escapement angle in DEGREES */ + double x; /**< x coordinate of first text object, in points */ + double y; /**< y coordinate of first text object, in points */ + int dirty; /**< 1 if text records are loaded */ + int use_kern; /**< 1 if kerning is used, 0 if not */ + int load_flags; /**< FT_LOAD_NO_SCALE or FT_LOAD_TARGET_NORMAL */ + int kern_mode; /**< FT_KERNING_DEFAULT, FT_KERNING_UNFITTED, or FT_KERNING_UNSCALED */ + uint32_t outspace; /**< storage in output buffer allocated */ + uint32_t outused; /**< storage in output buffer in use */ + int usebk; /**< On output write the background color under the text */ + TRCOLORREF bkcolor; /**< RGB background color */ +} TR_INFO; + +/* padding added to rectangles before overlap test */ +/** + \brief Information for one padding record. (Padding is added to bounding rectangles before overlap tests.) +*/ +typedef struct { + double up; /**< to top */ + double down; /**< to bottom */ + double left; /**< to left */ + double right; /**< to right */ +} RT_PAD; + +/** \cond */ +/* + iconv() has a funny cast on some older systems, on most recent ones + it is just char **. This tries to work around the issue. If you build this + on another funky system this code may need to be modified, or define ICONV_CAST + on the compile line(but it may be tricky). +*/ +#ifdef SOL8 +#define ICONV_CAST (const char **) +#endif //SOL8 +#if !defined(ICONV_CAST) +#define ICONV_CAST (char **) +#endif //ICONV_CAST +/** \endcond */ + +/* Prototypes */ +int TR_findcasesub(const char *string, const char *sub); +char *TR_construct_fontspec(const TCHUNK_SPECS *tsp, const char *fontname); +char *TR_reconstruct_fontspec(const char *fontspec, const char *fontname); +int TR_find_alternate_font(FT_INFO *fti, FNT_SPECS **efsp, uint32_t wc); +int TR_getadvance(FT_INFO *fti, FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int load_flags, int kern_mode, int *ymin, int *ymax); +int TR_getkern2(FNT_SPECS *fsp, uint32_t wc, uint32_t pc, int kern_mode); +int TR_kern_gap(FNT_SPECS *fsp, TCHUNK_SPECS *tsp, TCHUNK_SPECS *ptsp, int kern_mode); +void TR_rt_pad_set(RT_PAD *rt_pad, double up, double down, double left, double right); +double TR_baseline(TR_INFO *tri, int src, double *AscMax, double *DscMax); +int TR_check_set_vadvance(TR_INFO *tri, int src, int lines); +int TR_layout_analyze(TR_INFO *tri); +void TR_layout_2_svg(TR_INFO *tri); +int TR_weight_FC_to_SVG(int weight); + +FT_INFO *ftinfo_init(void); +int ftinfo_make_insertable(FT_INFO *fti); +int ftinfo_insert(FT_INFO *fti, FNT_SPECS *fsp); +FT_INFO *ftinfo_release(FT_INFO *fti); +FT_INFO *ftinfo_clear(FT_INFO *fti); +int ftinfo_find_loaded_by_spec(const FT_INFO *fti, const uint8_t *fname); +int ftinfo_find_loaded_by_src(const FT_INFO *fti, const uint8_t *filename); +int ftinfo_load_fontname(FT_INFO *fti, const char *fontspec); +void ftinfo_dump(const FT_INFO *fti); + +int fsp_alts_make_insertable(FNT_SPECS *fsp); +int fsp_alts_insert(FNT_SPECS *fsp, uint32_t fi_idx); +int fsp_alts_weight(FNT_SPECS *fsp, uint32_t a_idx); + +int csp_make_insertable(CHILD_SPECS *csp); +int csp_insert(CHILD_SPECS *csp, int src); +int csp_merge(CHILD_SPECS *dst, CHILD_SPECS *src); +void csp_release(CHILD_SPECS *csp); +void csp_clear(CHILD_SPECS *csp); + +CX_INFO *cxinfo_init(void); +int cxinfo_make_insertable(CX_INFO *cxi); +int cxinfo_insert(CX_INFO *cxi, int src, int src_rt_idx, enum tr_classes type); +int cxinfo_append(CX_INFO *cxi, int src, enum tr_classes type); +int cxinfo_merge(CX_INFO *cxi, int dst, int src, enum tr_classes type); +int cxinfo_trim(CX_INFO *cxi); +CX_INFO *cxinfo_release(CX_INFO *cxi); +void cxinfo_dump(const TR_INFO *tri); + +TP_INFO *tpinfo_init(void); +int tpinfo_make_insertable(TP_INFO *tpi); +int tpinfo_insert(TP_INFO *tpi, const TCHUNK_SPECS *tsp); +TP_INFO *tpinfo_release(TP_INFO *tpi); + +BR_INFO *brinfo_init(void); +int brinfo_make_insertable(BR_INFO *bri); +int brinfo_insert(BR_INFO *bri, const BRECT_SPECS *element); +int brinfo_merge(BR_INFO *bri, int dst, int src); +enum tr_classes + brinfo_pp_alignment(const BR_INFO *bri, int dst, int src, double slop, enum tr_classes type); +int brinfo_overlap(const BR_INFO *bri, int dst, int src, RT_PAD *rp_dst, RT_PAD *rp_src); +BR_INFO *brinfo_release(BR_INFO *bri); + +TR_INFO *trinfo_init(TR_INFO *tri); +TR_INFO *trinfo_release(TR_INFO *tri); +TR_INFO *trinfo_release_except_FC(TR_INFO *tri); +TR_INFO *trinfo_clear(TR_INFO *tri); +int trinfo_load_qe(TR_INFO *tri, double qe); +int trinfo_load_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor); +int trinfo_load_ft_opts(TR_INFO *tri, int use_kern, int load_flags, int kern_mode); +int trinfo_load_textrec(TR_INFO *tri, const TCHUNK_SPECS *tsp, double escapement, int flags); +int trinfo_check_bk(TR_INFO *tri, int usebk, TRCOLORREF bkcolor); +int trinfo_append_out(TR_INFO *tri, const char *src); + +int is_mn_unicode(int test); + + +#ifdef __cplusplus +} +#endif +#endif /* _TEXT_REASSEMBLE_ */ diff --git a/src/extension/internal/vsd-input.cpp b/src/extension/internal/vsd-input.cpp new file mode 100644 index 0000000..57763d5 --- /dev/null +++ b/src/extension/internal/vsd-input.cpp @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstdio> + +#include "vsd-input.h" + +#ifdef WITH_LIBVISIO + +#include <string> +#include <cstring> + +#include <libvisio/libvisio.h> + +#include <librevenge-stream/librevenge-stream.h> + +using librevenge::RVNGString; +using librevenge::RVNGFileStream; +using librevenge::RVNGStringVector; + +#include <gtkmm/spinbutton.h> + +#include "extension/system.h" +#include "extension/input.h" + +#include "document.h" +#include "inkscape.h" + +#include "ui/dialog-events.h" +#include <glibmm/i18n.h> + +#include "ui/view/svg-view-widget.h" + +#include "object/sp-root.h" + +#include "util/units.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +class VsdImportDialog : public Gtk::Dialog { +public: + VsdImportDialog(const std::vector<RVNGString> &vec); + ~VsdImportDialog() override; + + bool showDialog(); + unsigned getSelectedPage(); + void getImportSettings(Inkscape::XML::Node *prefs); + +private: + void _setPreviewPage(); + + // Signal handlers + void _onPageNumberChanged(); + void _onSpinButtonPress(GdkEventButton* button_event); + void _onSpinButtonRelease(GdkEventButton* button_event); + + class Gtk::Box * vbox1; + class Inkscape::UI::View::SVGViewWidget * _previewArea; + class Gtk::Button * cancelbutton; + class Gtk::Button * okbutton; + + class Gtk::Box * _page_selector_box; + class Gtk::Label * _labelSelect; + class Gtk::Label * _labelTotalPages; + class Gtk::SpinButton * _pageNumberSpin; + + const std::vector<RVNGString> &_vec; // Document to be imported + unsigned _current_page; // Current selected page + bool _spinning; // whether SpinButton is pressed (i.e. we're "spinning") +}; + +VsdImportDialog::VsdImportDialog(const std::vector<RVNGString> &vec) + : _previewArea(nullptr) + , _vec(vec) + , _current_page(1) + , _spinning(false) +{ + int num_pages = _vec.size(); + if ( num_pages <= 1 ) + return; + + + // Dialog settings + this->set_title(_("Page Selector")); + this->set_modal(true); + sp_transientize(GTK_WIDGET(this->gobj())); //Make transient + this->property_window_position().set_value(Gtk::WIN_POS_NONE); + this->set_resizable(true); + this->property_destroy_with_parent().set_value(false); + + // Preview area + vbox1 = Gtk::manage(new class Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + this->get_content_area()->pack_start(*vbox1); + + // CONTROLS + _page_selector_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + // Labels + _labelSelect = Gtk::manage(new class Gtk::Label(_("Select page:"))); + _labelTotalPages = Gtk::manage(new class Gtk::Label()); + _labelSelect->set_line_wrap(false); + _labelSelect->set_use_markup(false); + _labelSelect->set_selectable(false); + _page_selector_box->pack_start(*_labelSelect, Gtk::PACK_SHRINK); + + // Adjustment + spinner + auto _pageNumberSpin_adj = Gtk::Adjustment::create(1, 1, _vec.size(), 1, 10, 0); + _pageNumberSpin = Gtk::manage(new Gtk::SpinButton(_pageNumberSpin_adj, 1, 0)); + _pageNumberSpin->set_can_focus(); + _pageNumberSpin->set_update_policy(Gtk::UPDATE_ALWAYS); + _pageNumberSpin->set_numeric(true); + _pageNumberSpin->set_wrap(false); + _page_selector_box->pack_start(*_pageNumberSpin, Gtk::PACK_SHRINK); + + _labelTotalPages->set_line_wrap(false); + _labelTotalPages->set_use_markup(false); + _labelTotalPages->set_selectable(false); + gchar *label_text = g_strdup_printf(_("out of %i"), num_pages); + _labelTotalPages->set_label(label_text); + g_free(label_text); + _page_selector_box->pack_start(*_labelTotalPages, Gtk::PACK_SHRINK); + + vbox1->pack_end(*_page_selector_box, Gtk::PACK_SHRINK); + + // Buttons + cancelbutton = Gtk::manage(new Gtk::Button(_("_Cancel"), true)); + okbutton = Gtk::manage(new Gtk::Button(_("_OK"), true)); + this->add_action_widget(*cancelbutton, Gtk::RESPONSE_CANCEL); + this->add_action_widget(*okbutton, Gtk::RESPONSE_OK); + + // Show all widgets in dialog + this->show_all(); + + // Connect signals + _pageNumberSpin->signal_value_changed().connect(sigc::mem_fun(*this, &VsdImportDialog::_onPageNumberChanged)); + _pageNumberSpin->signal_button_press_event().connect_notify(sigc::mem_fun(*this, &VsdImportDialog::_onSpinButtonPress)); + _pageNumberSpin->signal_button_release_event().connect_notify(sigc::mem_fun(*this, &VsdImportDialog::_onSpinButtonRelease)); + + _setPreviewPage(); +} + +VsdImportDialog::~VsdImportDialog() = default; + +bool VsdImportDialog::showDialog() +{ + show(); + gint b = run(); + hide(); + if (b == Gtk::RESPONSE_OK || b == Gtk::RESPONSE_ACCEPT) { + return TRUE; + } else { + return FALSE; + } +} + +unsigned VsdImportDialog::getSelectedPage() +{ + return _current_page; +} + +void VsdImportDialog::_onPageNumberChanged() +{ + unsigned page = static_cast<unsigned>(_pageNumberSpin->get_value_as_int()); + _current_page = CLAMP(page, 1U, _vec.size()); + _setPreviewPage(); +} + +void VsdImportDialog::_onSpinButtonPress(GdkEventButton* /*button_event*/) +{ + _spinning = true; +} + +void VsdImportDialog::_onSpinButtonRelease(GdkEventButton* /*button_event*/) +{ + _spinning = false; + _setPreviewPage(); +} + +/** + * \brief Renders the given page's thumbnail + */ +void VsdImportDialog::_setPreviewPage() +{ + if (_spinning) { + return; + } + + SPDocument *doc = SPDocument::createNewDocFromMem(_vec[_current_page-1].cstr(), strlen(_vec[_current_page-1].cstr()), false); + if(!doc) { + g_warning("VSD import: Could not create preview for page %d", _current_page); + gchar const *no_preview_template = R"A( + <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'> + <path d='M 82,10 18,74 m 0,-64 64,64' style='fill:none;stroke:#ff0000;stroke-width:2px;'/> + <rect x='18' y='10' width='64' height='64' style='fill:none;stroke:#000000;stroke-width:1.5px;'/> + <text x='50' y='92' style='font-size:10px;text-anchor:middle;font-family:sans-serif;'>%s</text> + </svg> + )A"; + gchar * no_preview = g_strdup_printf(no_preview_template, _("No preview")); + doc = SPDocument::createNewDocFromMem(no_preview, strlen(no_preview), false); + g_free(no_preview); + } + + if (!doc) { + std::cerr << "VsdImportDialog::_setPreviewPage: No document!" << std::endl; + return; + } + + if (_previewArea) { + _previewArea->setDocument(doc); + } else { + _previewArea = Gtk::manage(new Inkscape::UI::View::SVGViewWidget(doc)); + vbox1->pack_start(*_previewArea, Gtk::PACK_EXPAND_WIDGET, 0); + } + + _previewArea->setResize(400, 400); + _previewArea->show_all(); +} + +SPDocument *VsdInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGFileStream input(converted_uri); + g_free(converted_uri); + #else + RVNGFileStream input(uri); + #endif + + if (!libvisio::VisioDocument::isSupported(&input)) { + return nullptr; + } + + RVNGStringVector output; + librevenge::RVNGSVGDrawingGenerator generator(output, "svg"); + + if (!libvisio::VisioDocument::parse(&input, &generator)) { + return nullptr; + } + + if (output.empty()) { + return nullptr; + } + + std::vector<RVNGString> tmpSVGOutput; + for (unsigned i=0; i<output.size(); ++i) { + RVNGString tmpString("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"); + tmpString.append(output[i]); + tmpSVGOutput.push_back(tmpString); + } + + unsigned page_num = 1; + + // If only one page is present, import that one without bothering user + if (tmpSVGOutput.size() > 1) { + VsdImportDialog *dlg = nullptr; + if (INKSCAPE.use_gui()) { + dlg = new VsdImportDialog(tmpSVGOutput); + if (!dlg->showDialog()) { + delete dlg; + throw Input::open_cancelled(); + } + } + + // Get needed page + if (dlg) { + page_num = dlg->getSelectedPage(); + if (page_num < 1) + page_num = 1; + if (page_num > tmpSVGOutput.size()) + page_num = tmpSVGOutput.size(); + } + } + + SPDocument * doc = SPDocument::createNewDocFromMem(tmpSVGOutput[page_num-1].cstr(), strlen(tmpSVGOutput[page_num-1].cstr()), TRUE); + + // Set viewBox if it doesn't exist + if (doc && !doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + return doc; +} + +#include "clear-n_.h" + +void VsdInput::init() +{ + // clang-format off + /* VSD */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("VSD Input") "</name>\n" + "<id>org.inkscape.input.vsd</id>\n" + "<input>\n" + "<extension>.vsd</extension>\n" + "<mimetype>application/vnd.visio</mimetype>\n" + "<filetypename>" N_("Microsoft Visio Diagram (*.vsd)") "</filetypename>\n" + "<filetypetooltip>" N_("File format used by Microsoft Visio 6 and later") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new VsdInput()); + + /* VDX */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("VDX Input") "</name>\n" + "<id>org.inkscape.input.vdx</id>\n" + "<input>\n" + "<extension>.vdx</extension>\n" + "<mimetype>application/vnd.visio</mimetype>\n" + "<filetypename>" N_("Microsoft Visio XML Diagram (*.vdx)") "</filetypename>\n" + "<filetypetooltip>" N_("File format used by Microsoft Visio 2010 and later") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new VsdInput()); + + /* VSDM */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("VSDM Input") "</name>\n" + "<id>org.inkscape.input.vsdm</id>\n" + "<input>\n" + "<extension>.vsdm</extension>\n" + "<mimetype>application/vnd.visio</mimetype>\n" + "<filetypename>" N_("Microsoft Visio 2013 drawing (*.vsdm)") "</filetypename>\n" + "<filetypetooltip>" N_("File format used by Microsoft Visio 2013 and later") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new VsdInput()); + + /* VSDX */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("VSDX Input") "</name>\n" + "<id>org.inkscape.input.vsdx</id>\n" + "<input>\n" + "<extension>.vsdx</extension>\n" + "<mimetype>application/vnd.visio</mimetype>\n" + "<filetypename>" N_("Microsoft Visio 2013 drawing (*.vsdx)") "</filetypename>\n" + "<filetypetooltip>" N_("File format used by Microsoft Visio 2013 and later") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new VsdInput()); + // clang-format on + + return; + +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBVISIO */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/vsd-input.h b/src/extension/internal/vsd-input.h new file mode 100644 index 0000000..f30c905 --- /dev/null +++ b/src/extension/internal/vsd-input.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Fridrich Strba (fridrich.strba@bluewin.ch) + * + * Copyright (C) 2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_VSDOUTPUT_H__ +#define __EXTENSION_INTERNAL_VSDOUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBVISIO + +#include <gtkmm/dialog.h> + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class VsdInput : public Inkscape::Extension::Implementation::Implementation { + VsdInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBVISIO */ +#endif /* __EXTENSION_INTERNAL_VSDOUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/wmf-inout.cpp b/src/extension/internal/wmf-inout.cpp new file mode 100644 index 0000000..048295d --- /dev/null +++ b/src/extension/internal/wmf-inout.cpp @@ -0,0 +1,3262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows-only Enhanced Metafile input and output. + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * David Mathog + * Abhishek Sharma + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +//#include <png.h> //This must precede text_reassemble.h or it blows up in pngconf.h when compiling +#include <cstdio> +#include <cstdlib> +#include <cstdint> +#include <3rdparty/libuemf/symbol_convert.h> + +#include "document.h" +#include "object/sp-root.h" // even though it is included indirectly by wmf-inout.h +#include "object/sp-path.h" +#include "print.h" +#include "extension/system.h" +#include "extension/print.h" +#include "extension/db.h" +#include "extension/input.h" +#include "extension/output.h" +#include "display/drawing.h" +#include "display/drawing-item.h" +#include "clear-n_.h" +#include "path/path-boolop.h" +#include "svg/svg.h" +#include "util/units.h" // even though it is included indirectly by wmf-inout.h +#include "inkscape.h" // even though it is included indirectly by wmf-inout.h + + +#include "wmf-inout.h" +#include "wmf-print.h" + +#define PRINT_WMF "org.inkscape.print.wmf" + +#ifndef U_PS_JOIN_MASK +#define U_PS_JOIN_MASK (U_PS_JOIN_BEVEL|U_PS_JOIN_MITER|U_PS_JOIN_ROUND) +#endif + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +static bool clipset = false; +static uint32_t BLTmode=0; + +Wmf::Wmf () // The null constructor +{ + return; +} + + +Wmf::~Wmf () //The destructor +{ + return; +} + + +bool +Wmf::check (Inkscape::Extension::Extension * /*module*/) +{ + if (nullptr == Inkscape::Extension::db.get(PRINT_WMF)) + return FALSE; + return TRUE; +} + + +void +Wmf::print_document_to_file(SPDocument *doc, const gchar *filename) +{ + Inkscape::Extension::Print *mod; + SPPrintContext context; + const gchar *oldconst; + gchar *oldoutput; + + doc->ensureUpToDate(); + + mod = Inkscape::Extension::get_print(PRINT_WMF); + oldconst = mod->get_param_string("destination"); + oldoutput = g_strdup(oldconst); + mod->set_param_string("destination", filename); + +/* Start */ + context.module = mod; + /* fixme: This has to go into module constructor somehow */ + /* Create new arena */ + mod->base = doc->getRoot(); + Inkscape::Drawing drawing; + mod->dkey = SPItem::display_key_new(1); + mod->root = mod->base->invoke_show(drawing, mod->dkey, SP_ITEM_SHOW_DISPLAY); + drawing.setRoot(mod->root); + /* Print document */ + if (mod->begin(doc)) { + g_free(oldoutput); + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; + throw Inkscape::Extension::Output::save_failed(); + } + mod->base->invoke_print(&context); + mod->finish(); + /* Release arena */ + mod->base->invoke_hide(mod->dkey); + mod->base = nullptr; + mod->root = nullptr; // deleted by invoke_hide +/* end */ + + mod->set_param_string("destination", oldoutput); + g_free(oldoutput); + + return; +} + + +void +Wmf::save(Inkscape::Extension::Output *mod, SPDocument *doc, gchar const *filename) +{ + Inkscape::Extension::Extension * ext; + + ext = Inkscape::Extension::db.get(PRINT_WMF); + if (ext == nullptr) + return; + + bool new_val = mod->get_param_bool("textToPath"); + bool new_FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); // character position bug + // reserve FixPPT2 for opacity bug. Currently WMF does not export opacity values + bool new_FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); // dashed line bug + bool new_FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); // gradient bug + bool new_FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); // force all patterns as standard WMF hatch + + TableGen( //possibly regenerate the unicode-convert tables + mod->get_param_bool("TnrToSymbol"), + mod->get_param_bool("TnrToWingdings"), + mod->get_param_bool("TnrToZapfDingbats"), + mod->get_param_bool("UsePUA") + ); + + ext->set_param_bool("FixPPTCharPos",new_FixPPTCharPos); // Remember to add any new ones to PrintWmf::init or a mysterious failure will result! + ext->set_param_bool("FixPPTDashLine",new_FixPPTDashLine); + ext->set_param_bool("FixPPTGrad2Polys",new_FixPPTGrad2Polys); + ext->set_param_bool("FixPPTPatternAsHatch",new_FixPPTPatternAsHatch); + ext->set_param_bool("textToPath", new_val); + + // ensure usage of dot as decimal separator in scanf/printf functions (independently of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + print_document_to_file(doc, filename); + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return; +} + + +/* WMF has no worldTransform, so this always returns 1.0. Retain it to keep WMF and WMF in sync as much as possible.*/ +double Wmf::current_scale(PWMF_CALLBACK_DATA /*d*/){ + return 1.0; +} + +/* WMF has no worldTransform, so this always returns an Identity rotation matrix, but the offsets may have values.*/ +std::string Wmf::current_matrix(PWMF_CALLBACK_DATA d, double x, double y, int useoffset){ + SVGOStringStream cxform; + double scale = current_scale(d); + cxform << "\"matrix("; + cxform << 1.0/scale; cxform << ","; + cxform << 0.0; cxform << ","; + cxform << 0.0; cxform << ","; + cxform << 1.0/scale; cxform << ","; + if(useoffset){ cxform << x; cxform << ","; cxform << y; } + else { cxform << "0,0"; } + cxform << ")\""; + return(cxform.str()); +} + +/* WMF has no worldTransform, so this always returns 0. Retain it to keep WMF and WMF in sync as much as possible.*/ +double Wmf::current_rotation(PWMF_CALLBACK_DATA /*d*/){ + return 0.0; +} + +/* Add another 100 blank slots to the hatches array. +*/ +void Wmf::enlarge_hatches(PWMF_CALLBACK_DATA d){ + d->hatches.size += 100; + d->hatches.strings = (char **) realloc(d->hatches.strings,d->hatches.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_hatches(PWMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; i<d->hatches.count; i++){ + if(strcmp(test,d->hatches.strings[i])==0)return(i+1); + } + return(0); +} + +class TagEmitter +{ +public: + TagEmitter(Glib::ustring & p_defs, char * p_tmpcolor, char * p_hpathname): + defs(p_defs), tmpcolor(p_tmpcolor), hpathname(p_hpathname) + { + }; + void append(const char *prefix, const char * inner) + { + defs += " "; + defs += prefix; + defs += hpathname; + defs += inner; + defs += tmpcolor; + defs += "\" />\n"; + } + +protected: + Glib::ustring & defs; + char * tmpcolor, * hpathname; +}; + +/* (Conditionally) add a hatch. If a matching hatch already exists nothing happens. If one + does not exist it is added to the hatches list and also entered into <defs>. + This is also used to add the path part of the hatches, which they reference with a xlink:href +*/ +uint32_t Wmf::add_hatch(PWMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor){ + char hatchname[64]; // big enough + char hpathname[64]; // big enough + char hbkname[64]; // big enough + char tmpcolor[8]; + char bkcolor[8]; + uint32_t idx; + + switch(hatchType){ + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].textColor)); + break; + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + sprintf(tmpcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + break; + default: + sprintf(tmpcolor,"%6.6X",sethexcolor(hatchColor)); + break; + } + auto & defs = d->defs; + TagEmitter a(defs, tmpcolor, hpathname); + + /* For both bkMode types set the PATH + FOREGROUND COLOR for the indicated standard hatch. + This will be used late to compose, or recompose the transparent or opaque final hatch.*/ + + std::string refpath; // used to reference later the path pieces which are about to be created + sprintf(hpathname,"WMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hpathname); + if(!idx){ // add path/color if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hpathname); + + defs += "\n"; + switch(hatchType){ + case U_HS_HORIZONTAL: + a.append("<path id=\"", + "\" d=\"M 0 0 6 0\" style=\"fill:none;stroke:#"); + break; + case U_HS_VERTICAL: + a.append("<path id=\"", + "\" d=\"M 0 0 0 6\" style=\"fill:none;stroke:#"); + break; + case U_HS_FDIAGONAL: + a.append("<line id=\"sub", + "\" x1=\"-1\" y1=\"-1\" x2=\"7\" y2=\"7\" stroke=\"#"); + break; + case U_HS_BDIAGONAL: + a.append("<line id=\"sub", + "\" x1=\"-1\" y1=\"7\" x2=\"7\" y2=\"-1\" stroke=\"#"); + break; + case U_HS_CROSS: + a.append("<path id=\"", + "\" d=\"M 0 0 6 0 M 0 0 0 6\" style=\"fill:none;stroke:#"); + break; + case U_HS_DIAGCROSS: + a.append("<line id=\"subfd", + "\" x1=\"-1\" y1=\"-1\" x2=\"7\" y2=\"7\" stroke=\"#"); + a.append("<line id=\"subbd", + "\" x1=\"-1\" y1=\"7\" x2=\"7\" y2=\"-1\" stroke=\"#"); + break; + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + a.append("<path id=\"", + "\" d=\"M 0 0 6 0 6 6 0 6 z\" style=\"stroke:none;fill:#"); + break; + } + } + + // References to paths possibly just created above. These will be used in the actual patterns. + switch(hatchType){ + case U_HS_HORIZONTAL: + case U_HS_VERTICAL: + case U_HS_CROSS: + case U_HS_SOLIDCLR: + case U_HS_DITHEREDCLR: + case U_HS_SOLIDTEXTCLR: + case U_HS_DITHEREDTEXTCLR: + case U_HS_SOLIDBKCLR: + case U_HS_DITHEREDBKCLR: + default: + refpath += " <use xlink:href=\"#"; + refpath += hpathname; + refpath += "\" />\n"; + break; + case U_HS_FDIAGONAL: + case U_HS_BDIAGONAL: + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\" />\n"; + refpath += " <use xlink:href=\"#sub"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\" />\n"; + break; + case U_HS_DIAGCROSS: + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subfd"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" />\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" transform=\"translate(6,0)\"/>\n"; + refpath += " <use xlink:href=\"#subbd"; + refpath += hpathname; + refpath += "\" transform=\"translate(-6,0)\"/>\n"; + break; + } + + if(d->dc[d->level].bkMode == U_TRANSPARENT || hatchType >= U_HS_SOLIDCLR){ + sprintf(hatchname,"WMFhatch%d_%s",hatchType,tmpcolor); + sprintf(hpathname,"WMFhpath%d_%s",hatchType,tmpcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " <pattern id=\""; + defs += hatchname; + defs += "\" xlink:href=\"#WMFhbasepattern\">\n"; + defs += refpath; + defs += " </pattern>\n"; + idx = d->hatches.count; + } + } + else { // bkMode==U_OPAQUE + /* Set up an object in the defs for this background, if there is not one already there */ + sprintf(bkcolor,"%6.6X",sethexcolor(d->dc[d->level].bkColor)); + sprintf(hbkname,"WMFhbkclr_%s",bkcolor); + idx = in_hatches(d,hbkname); + if(!idx){ // add path/color if not already present. Hatchtype is not needed in the name. + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hbkname); + + defs += "\n"; + defs += " <rect id=\""; + defs += hbkname; + defs += "\" x=\"0\" y=\"0\" width=\"6\" height=\"6\" fill=\"#"; + defs += bkcolor; + defs += "\" />\n"; + } + + // this is the pattern, its name will show up in Inkscape's pattern selector + sprintf(hatchname,"WMFhatch%d_%s_%s",hatchType,tmpcolor,bkcolor); + idx = in_hatches(d,hatchname); + if(!idx){ // add it if not already present + if(d->hatches.count == d->hatches.size){ enlarge_hatches(d); } + d->hatches.strings[d->hatches.count++]=strdup(hatchname); + defs += "\n"; + defs += " <pattern id=\""; + defs += hatchname; + defs += "\" xlink:href=\"#WMFhbasepattern\">\n"; + defs += " <use xlink:href=\"#"; + defs += hbkname; + defs += "\" />\n"; + defs += refpath; + defs += " </pattern>\n"; + idx = d->hatches.count; + } + } + return(idx-1); +} + +/* Add another 100 blank slots to the images array. +*/ +void Wmf::enlarge_images(PWMF_CALLBACK_DATA d){ + d->images.size += 100; + d->images.strings = (char **) realloc(d->images.strings,d->images.size * sizeof(char *)); +} + +/* See if the image string is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_images(PWMF_CALLBACK_DATA d, char *test){ + int i; + for(i=0; i<d->images.count; i++){ + if(strcmp(test,d->images.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add an image from a DIB. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into <defs>. + +*/ +uint32_t Wmf::add_dib_image(PWMF_CALLBACK_DATA d, const char *dib, uint32_t iUsage){ + + uint32_t idx; + char imagename[64]; // big enough + char xywh[64]; // big enough + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // DIB color table + uint32_t numCt; + int32_t width, height, colortype, invert; // if needed these values will be set by wget_DIB_params + if(iUsage == U_DIB_RGB_COLORS){ + // next call returns pointers and values, but allocates no memory + dibparams = wget_DIB_params(dib, &px, &ct, &numCt, &width, &height, &colortype, &invert); + if(dibparams == U_BI_RGB){ + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array in record + height, // Height of pixel array in record + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG || dibparams==U_BI_PNG){ // image was binary png or jpg in source file + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=strdup(base64String); + + sprintf(imagename,"WMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " <image id=\""; + defs += imagename; + defs += "\"\n "; + defs += xywh; + defs += "\n"; + if(dibparams == U_BI_JPEG){ defs += " xlink:href=\"data:image/jpeg;base64,"; } + else { defs += " xlink:href=\"data:image/png;base64,"; } + defs += base64String; + defs += "\"\n"; + defs += " preserveAspectRatio=\"none\"\n"; + defs += " />\n"; + + + defs += "\n"; + defs += " <pattern id=\""; + defs += imagename; + defs += "_ref\"\n "; + defs += xywh; + defs += "\n patternUnits=\"userSpaceOnUse\""; + defs += " >\n"; + defs += " <use id=\""; + defs += imagename; + defs += "_ign\" "; + defs += " xlink:href=\"#"; + defs += imagename; + defs += "\" />\n"; + defs += " "; + defs += " </pattern>\n"; + } + g_free(base64String); //wait until this point to free because it might be a duplicate image + return(idx-1); +} + +/* (Conditionally) add an image from a Bitmap16. If a matching image already exists nothing happens. If one + does not exist it is added to the images list and also entered into <defs>. + +*/ +uint32_t Wmf::add_bm16_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px){ + + uint32_t idx; + char imagename[64]; // big enough + char xywh[64]; // big enough + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + const U_RGBQUAD *ct = nullptr; // color table, always NULL here + int32_t width, height, colortype, numCt, invert; + numCt = 0; + width = Bm16.Width; // bitmap width in pixels. + height = Bm16.Height; // bitmap height in scan lines. + colortype = Bm16.BitsPixel; // seems to be BitCount Enumeration + invert = 0; + if(colortype < 16)return(U_WMR_INVALID); // these would need a colortable if they were a dib, no idea what bm16 is supposed to do instead. + + if(!DIB_to_RGBA(// This is not really a dib, but close enough so that it still works. + px, // DIB pixel array + ct, // DIB color table (always NULL here) + numCt, // DIB color table number of entries (always 0) + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + toPNG( // Get the image from the RGBA px into mempng + &mempng, + width, height, // of the SRC bitmap + rgba_px + ); + free(rgba_px); + } + + gchar *base64String=nullptr; + if(mempng.buffer){ // image was Bm16 in source file, converted to png in this routine + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // failed conversion, insert the common bad image picture + width = 3; + height = 4; + base64String = bad_image_png(); + } + + idx = in_images(d, (char *) base64String); + auto & defs = d->defs; + if(!idx){ // add it if not already present - we looked at the actual data for comparison + if(d->images.count == d->images.size){ enlarge_images(d); } + idx = d->images.count; + d->images.strings[d->images.count++]=g_strdup(base64String); + + sprintf(imagename,"WMFimage%d",idx++); + sprintf(xywh," x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" ",width,height); // reuse this buffer + + defs += "\n"; + defs += " <image id=\""; + defs += imagename; + defs += "\"\n "; + defs += xywh; + defs += "\n"; + defs += " xlink:href=\"data:image/png;base64,"; + defs += base64String; + defs += "\"\n"; + defs += " preserveAspectRatio=\"none\"\n"; + defs += " />\n"; + + + defs += "\n"; + defs += " <pattern id=\""; + defs += imagename; + defs += "_ref\"\n "; + defs += xywh; + defs += "\n patternUnits=\"userSpaceOnUse\""; + defs += " >\n"; + defs += " <use id=\""; + defs += imagename; + defs += "_ign\" "; + defs += " xlink:href=\"#"; + defs += imagename; + defs += "\" />\n"; + defs += " </pattern>\n"; + } + g_free(base64String); //wait until this point to free because it might be a duplicate image + return(idx-1); +} + +/* Add another 100 blank slots to the clips array. +*/ +void Wmf::enlarge_clips(PWMF_CALLBACK_DATA d){ + d->clips.size += 100; + d->clips.strings = (char **) realloc(d->clips.strings,d->clips.size * sizeof(char *)); +} + +/* See if the pattern name is already in the list. If it is return its position (1->n, not 1-n-1) +*/ +int Wmf::in_clips(PWMF_CALLBACK_DATA d, const char *test){ + int i; + for(i=0; i<d->clips.count; i++){ + if(strcmp(test,d->clips.strings[i])==0)return(i+1); + } + return(0); +} + +/* (Conditionally) add a clip. + If a matching clip already exists nothing happens + If one does exist it is added to the clips list, entered into <defs>. +*/ +void Wmf::add_clips(PWMF_CALLBACK_DATA d, const char *clippath, unsigned int logic){ + int op = combine_ops_to_livarot(logic); + Geom::PathVector combined_vect; + std::string combined; + if (op >= 0 && d->dc[d->level].clip_id) { + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector old_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + Geom::PathVector new_vect = sp_svg_read_pathv(clippath); + combined_vect = sp_pathvector_boolop(new_vect, old_vect, (bool_op) op , (FillRule) fill_oddEven, (FillRule) fill_oddEven); + combined = sp_svg_write_path(combined_vect); + } + else { + combined = clippath; // COPY operation, erases everything and starts a new one + } + + uint32_t idx = in_clips(d, combined.c_str()); + if(!idx){ // add clip if not already present + if(d->clips.count == d->clips.size){ enlarge_clips(d); } + d->clips.strings[d->clips.count++] = strdup(combined.c_str()); + d->dc[d->level].clip_id = d->clips.count; // one more than the slot where it is actually stored + SVGOStringStream tmp_clippath; + tmp_clippath << "\n<clipPath"; + tmp_clippath << "\n\tclipPathUnits=\"userSpaceOnUse\" "; + tmp_clippath << "\n\tid=\"clipWmfPath" << d->dc[d->level].clip_id << "\""; + tmp_clippath << " >"; + tmp_clippath << "\n\t<path d=\""; + tmp_clippath << combined; + tmp_clippath << "\""; + tmp_clippath << "\n\t/>"; + tmp_clippath << "\n</clipPath>"; + d->outdef += tmp_clippath.str().c_str(); + } + else { + d->dc[d->level].clip_id = idx; + } +} + + + +void +Wmf::output_style(PWMF_CALLBACK_DATA d) +{ +// SVGOStringStream tmp_id; + SVGOStringStream tmp_style; + char tmp[1024] = {0}; + + float fill_rgb[3]; + d->dc[d->level].style.fill.value.color.get_rgb_floatv(fill_rgb); + float stroke_rgb[3]; + d->dc[d->level].style.stroke.value.color.get_rgb_floatv(stroke_rgb); + + // for U_WMR_BITBLT with no image, try to approximate some of these operations/ + // Assume src color is "white" + if(d->dwRop3){ + switch(d->dwRop3){ + case U_PATINVERT: // treat all of these as black + case U_SRCINVERT: + case U_DSTINVERT: + case U_BLACKNESS: + case U_SRCERASE: + case U_NOTSRCCOPY: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=0.0; + break; + case U_SRCCOPY: // treat all of these as white + case U_NOTSRCERASE: + case U_PATCOPY: + case U_WHITENESS: + fill_rgb[0]=fill_rgb[1]=fill_rgb[2]=1.0; + break; + case U_SRCPAINT: // use the existing color + case U_SRCAND: + case U_MERGECOPY: + case U_MERGEPAINT: + case U_PATPAINT: + default: + break; + } + d->dwRop3 = 0; // might as well reset it here, it must be set for each BITBLT + } + + // Implement some of these, the ones where the original screen color does not matter. + // The options that merge screen and pen colors cannot be done correctly because we + // have no way of knowing what color is already on the screen. For those just pass the + // pen color through. + switch(d->dwRop2){ + case U_R2_BLACK: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 0.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 0.0; + break; + case U_R2_NOTMERGEPEN: + case U_R2_MASKNOTPEN: + break; + case U_R2_NOTCOPYPEN: + fill_rgb[0] = 1.0 - fill_rgb[0]; + fill_rgb[1] = 1.0 - fill_rgb[1]; + fill_rgb[2] = 1.0 - fill_rgb[2]; + stroke_rgb[0] = 1.0 - stroke_rgb[0]; + stroke_rgb[1] = 1.0 - stroke_rgb[1]; + stroke_rgb[2] = 1.0 - stroke_rgb[2]; + break; + case U_R2_MASKPENNOT: + case U_R2_NOT: + case U_R2_XORPEN: + case U_R2_NOTMASKPEN: + case U_R2_NOTXORPEN: + case U_R2_NOP: + case U_R2_MERGENOTPEN: + case U_R2_COPYPEN: + case U_R2_MASKPEN: + case U_R2_MERGEPENNOT: + case U_R2_MERGEPEN: + break; + case U_R2_WHITE: + fill_rgb[0] = fill_rgb[1] = fill_rgb[2] = 1.0; + stroke_rgb[0]= stroke_rgb[1]= stroke_rgb[2] = 1.0; + break; + default: + break; + } + + +// tmp_id << "\n\tid=\"" << (d->id++) << "\""; +// d->outsvg += tmp_id.str().c_str(); + d->outsvg += "\n\tstyle=\""; + if (!d->dc[d->level].fill_set || ( d->mask & U_DRAW_NOFILL)) { // nofill are lines and arcs + tmp_style << "fill:none;"; + } else { + switch(d->dc[d->level].fill_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "fill:url(#%s); ",d->hatches.strings[d->dc[d->level].fill_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "fill:url(#WMFimage%d_ref); ",d->dc[d->level].fill_idx); + tmp_style << tmp; + break; + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "fill:#%02x%02x%02x;", + SP_COLOR_F_TO_U(fill_rgb[0]), + SP_COLOR_F_TO_U(fill_rgb[1]), + SP_COLOR_F_TO_U(fill_rgb[2]) + ); + tmp_style << tmp; + break; + } + snprintf( + tmp, 1023, + "fill-rule:%s;", + (d->dc[d->level].style.fill_rule.value == SP_WIND_RULE_NONZERO ? "evenodd" : "nonzero") + ); + tmp_style << tmp; + tmp_style << "fill-opacity:1;"; + + // if the stroke is the same as the fill, and the right size not to change the end size of the object, do not do it separately + if( + (d->dc[d->level].fill_set ) && + (d->dc[d->level].stroke_set ) && + (d->dc[d->level].style.stroke_width.value == 1 ) && + (d->dc[d->level].fill_mode == d->dc[d->level].stroke_mode) && + ( + (d->dc[d->level].fill_mode != DRAW_PAINT) || + ( + (fill_rgb[0]==stroke_rgb[0]) && + (fill_rgb[1]==stroke_rgb[1]) && + (fill_rgb[2]==stroke_rgb[2]) + ) + ) + ){ + d->dc[d->level].stroke_set = false; + } + } + + if (!d->dc[d->level].stroke_set) { + tmp_style << "stroke:none;"; + } else { + switch(d->dc[d->level].stroke_mode){ + // both of these use the url(#) method + case DRAW_PATTERN: + snprintf(tmp, 1023, "stroke:url(#%s); ",d->hatches.strings[d->dc[d->level].stroke_idx]); + tmp_style << tmp; + break; + case DRAW_IMAGE: + snprintf(tmp, 1023, "stroke:url(#WMFimage%d_ref); ",d->dc[d->level].stroke_idx); + tmp_style << tmp; + break; + case DRAW_PAINT: + default: // <-- this should never happen, but just in case... + snprintf( + tmp, 1023, + "stroke:#%02x%02x%02x;", + SP_COLOR_F_TO_U(stroke_rgb[0]), + SP_COLOR_F_TO_U(stroke_rgb[1]), + SP_COLOR_F_TO_U(stroke_rgb[2]) + ); + tmp_style << tmp; + break; + } + if(d->dc[d->level].style.stroke_width.value){ + tmp_style << "stroke-width:" << + MAX( 0.001, d->dc[d->level].style.stroke_width.value ) << "px;"; + } + else { // In a WMF a 0 width pixel means "1 pixel" + tmp_style << "stroke-width:" << pix_to_abs_size( d, 1 ) << "px;"; + } + + tmp_style << "stroke-linecap:" << + ( + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_BUTT ? "butt" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_ROUND ? "round" : + d->dc[d->level].style.stroke_linecap.computed == SP_STROKE_LINECAP_SQUARE ? "square" : + "unknown" + ) << ";"; + + tmp_style << "stroke-linejoin:" << + ( + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_MITER ? "miter" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_ROUND ? "round" : + d->dc[d->level].style.stroke_linejoin.computed == SP_STROKE_LINEJOIN_BEVEL ? "bevel" : + "unknown" + ) << ";"; + + // Set miter limit if known, even if it is not needed immediately (not miter) + tmp_style << "stroke-miterlimit:" << + MAX( 2.0, d->dc[d->level].style.stroke_miterlimit.value ) << ";"; + + if (d->dc[d->level].style.stroke_dasharray.set && + !d->dc[d->level].style.stroke_dasharray.values.empty()) + { + tmp_style << "stroke-dasharray:"; + for (unsigned i=0; i<d->dc[d->level].style.stroke_dasharray.values.size(); i++) { + if (i) + tmp_style << ","; + tmp_style << d->dc[d->level].style.stroke_dasharray.values[i].value; + } + tmp_style << ";"; + tmp_style << "stroke-dashoffset:0;"; + } else { + tmp_style << "stroke-dasharray:none;"; + } + tmp_style << "stroke-opacity:1;"; + } + tmp_style << "\" "; + if (d->dc[d->level].clip_id) + tmp_style << "\n\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\" "; + + d->outsvg += tmp_style.str().c_str(); +} + + +double +Wmf::_pix_x_to_point(PWMF_CALLBACK_DATA d, double px) +{ + double scale = (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0); + double tmp; + tmp = ((((double) (px - d->dc[d->level].winorg.x))*scale) + d->dc[d->level].vieworg.x) * d->D2PscaleX; + tmp -= d->ulCornerOutX; //The WMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + +double +Wmf::_pix_y_to_point(PWMF_CALLBACK_DATA d, double py) +{ + double scale = (d->dc[d->level].ScaleInY ? d->dc[d->level].ScaleInY : 1.0); + double tmp; + tmp = ((((double) (py - d->dc[d->level].winorg.y))*scale) * d->E2IdirY + d->dc[d->level].vieworg.y) * d->D2PscaleY; + tmp -= d->ulCornerOutY; //The WMF boundary rectangle can be anywhere, place its upper left corner in the Inkscape upper left corner + return(tmp); +} + + +double +Wmf::pix_to_x_point(PWMF_CALLBACK_DATA d, double px, double /*py*/) +{ + double x = _pix_x_to_point(d, px); + return x; +} + +double +Wmf::pix_to_y_point(PWMF_CALLBACK_DATA d, double /*px*/, double py) +{ + double y = _pix_y_to_point(d, py); + return y; + +} + +double +Wmf::pix_to_abs_size(PWMF_CALLBACK_DATA d, double px) +{ + double ppx = fabs(px * (d->dc[d->level].ScaleInX ? d->dc[d->level].ScaleInX : 1.0) * d->D2PscaleX * current_scale(d)); + return ppx; +} + +/* returns "x,y" (without the quotes) in inkscape coordinates for a pair of WMF x,y coordinates +*/ +std::string Wmf::pix_to_xy(PWMF_CALLBACK_DATA d, double x, double y){ + SVGOStringStream cxform; + cxform << pix_to_x_point(d,x,y); + cxform << ","; + cxform << pix_to_y_point(d,x,y); + return(cxform.str()); +} + + +void +Wmf::select_pen(PWMF_CALLBACK_DATA d, int index) +{ + int width; + char *record = nullptr; + U_PEN up; + + if (index < 0 && index >= d->n_obj){ return; } + record = d->wmf_obj[index].record; + if(!record){ return; } + d->dc[d->level].active_pen = index; + + (void) U_WMRCREATEPENINDIRECT_get(record, &up); + width = up.Widthw[0]; // width is stored in the first 16 bits of the 32. + + switch (up.Style & U_PS_STYLE_MASK) { + case U_PS_DASH: + case U_PS_DOT: + case U_PS_DASHDOT: + case U_PS_DASHDOTDOT: + { + int penstyle = (up.Style & U_PS_STYLE_MASK); + SPILength spilength(1.f); + if (!d->dc[d->level].style.stroke_dasharray.values.empty() && + (d->level == 0 || (d->level > 0 && d->dc[d->level].style.stroke_dasharray != + d->dc[d->level - 1].style.stroke_dasharray))) + d->dc[d->level].style.stroke_dasharray.values.clear(); + if (penstyle==U_PS_DASH || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + spilength.setDouble(3); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + spilength.setDouble(1); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DOT || penstyle==U_PS_DASHDOT || penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + if (penstyle==U_PS_DASHDOTDOT) { + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + d->dc[d->level].style.stroke_dasharray.values.push_back(spilength); + } + + d->dc[d->level].style.stroke_dasharray.set = true; + break; + } + + case U_PS_SOLID: + default: + { + d->dc[d->level].style.stroke_dasharray.set = false; + break; + } + } + + switch (up.Style & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + + switch (up.Style & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + + + double pen_width; + if (up.Style == U_PS_NULL) { + d->dc[d->level].stroke_set = false; + pen_width =0.0; + } else if (width) { + d->dc[d->level].stroke_set = true; + int cur_level = d->level; + d->level = d->wmf_obj[index].level; // this object may have been defined in some other DC. + pen_width = pix_to_abs_size( d, width ); + d->level = cur_level; + } else { // this stroke should always be rendered as 1 pixel wide, independent of zoom level (can that be done in SVG?) + d->dc[d->level].stroke_set = true; + int cur_level = d->level; + d->level = d->wmf_obj[index].level; // this object may have been defined in some other DC. + pen_width = pix_to_abs_size( d, 1 ); + d->level = cur_level; + } + d->dc[d->level].style.stroke_width.value = pen_width; + + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(up.Color) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(up.Color) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(up.Color) ); + d->dc[d->level].style.stroke.value.color.set( r, g, b ); +} + + +void +Wmf::select_brush(PWMF_CALLBACK_DATA d, int index) +{ + uint8_t iType; + char *record; + const char *membrush; + + if (index < 0 || index >= d->n_obj)return; + record = d->wmf_obj[index].record; + if(!record)return; + d->dc[d->level].active_brush = index; + + iType = *(uint8_t *)(record + offsetof(U_METARECORD, iType ) ); + if(iType == U_WMR_CREATEBRUSHINDIRECT){ + U_WLOGBRUSH lb; + (void) U_WMRCREATEBRUSHINDIRECT_get(record, &membrush); + memcpy(&lb, membrush, U_SIZE_WLOGBRUSH); + if(lb.Style == U_BS_SOLID){ + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(lb.Color) ); + g = SP_COLOR_U_TO_F( U_RGBAGetG(lb.Color) ); + b = SP_COLOR_U_TO_F( U_RGBAGetB(lb.Color) ); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + d->dc[d->level].fill_set = true; + } + else if(lb.Style == U_BS_HATCHED){ + d->dc[d->level].fill_idx = add_hatch(d, lb.Hatch, lb.Color); + d->dc[d->level].fill_recidx = index; // used if the hatch needs to be redone due to bkMode, textmode, etc. changes + d->dc[d->level].fill_mode = DRAW_PATTERN; + d->dc[d->level].fill_set = true; + } + else if(lb.Style == U_BS_NULL){ + d->dc[d->level].fill_mode = DRAW_PAINT; // set it to something + d->dc[d->level].fill_set = false; + } + } + else if(iType == U_WMR_DIBCREATEPATTERNBRUSH){ + uint32_t tidx; + uint16_t Style; + uint16_t cUsage; + const char *Bm16h = nullptr; // Pointer to Bitmap16 header (px follows) + const char *dib = nullptr; // Pointer to DIB + (void) U_WMRDIBCREATEPATTERNBRUSH_get(record, &Style, &cUsage, &Bm16h, &dib); + if(dib || Bm16h){ + if(dib){ tidx = add_dib_image(d, dib, cUsage); } + else { + U_BITMAP16 Bm16; + const char *px; + memcpy(&Bm16, Bm16h, U_SIZE_BITMAP16); + px = Bm16h + U_SIZE_BITMAP16; + tidx = add_bm16_image(d, Bm16, px); + } + if(tidx == U_WMR_INVALID){ // Problem with the image, for instance, an unsupported bitmap16 type + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + else { + g_message("Please send WMF file to developers - select_brush U_WMR_DIBCREATEPATTERNBRUSH not bm16 or dib, not handled"); + } + } + else if(iType == U_WMR_CREATEPATTERNBRUSH){ + uint32_t tidx; + int cbPx; + U_BITMAP16 Bm16h; + const char *px; + if(U_WMRCREATEPATTERNBRUSH_get(record, &Bm16h, &cbPx, &px)){ + tidx = add_bm16_image(d, Bm16h, px); + if(tidx == 0xFFFFFFFF){ // Problem with the image, for instance, an unsupported bitmap16 type + double r, g, b; + r = SP_COLOR_U_TO_F( U_RGBAGetR(d->dc[d->level].textColor)); + g = SP_COLOR_U_TO_F( U_RGBAGetG(d->dc[d->level].textColor)); + b = SP_COLOR_U_TO_F( U_RGBAGetB(d->dc[d->level].textColor)); + d->dc[d->level].style.fill.value.color.set( r, g, b ); + d->dc[d->level].fill_mode = DRAW_PAINT; + } + else { + d->dc[d->level].fill_idx = tidx; + d->dc[d->level].fill_mode = DRAW_IMAGE; + } + d->dc[d->level].fill_set = true; + } + } +} + + +void +Wmf::select_font(PWMF_CALLBACK_DATA d, int index) +{ + char *record = nullptr; + const char *memfont; + const char *facename; + U_FONT font; + + if (index < 0 || index >= d->n_obj)return; + record = d->wmf_obj[index].record; + if (!record)return; + d->dc[d->level].active_font = index; + + + (void) U_WMRCREATEFONTINDIRECT_get(record, &memfont); + memcpy(&font,memfont,U_SIZE_FONT_CORE); //make sure it is in a properly aligned structure before touching it + facename = memfont + U_SIZE_FONT_CORE; + + /* The logfont information always starts with a U_LOGFONT structure but the U_WMRCREATEFONTINDIRECT + is defined as U_LOGFONT_PANOSE so it can handle one of those if that is actually present. Currently only logfont + is supported, and the remainder, it it really is a U_LOGFONT_PANOSE record, is ignored + */ + int cur_level = d->level; + d->level = d->wmf_obj[index].level; + double font_size = pix_to_abs_size( d, font.Height ); + /* snap the font_size to the nearest 1/32nd of a point. + (The size is converted from Pixels to points, snapped, and converted back.) + See the notes where d->D2Pscale[XY] are set for the reason why. + Typically this will set the font to the desired exact size. If some peculiar size + was intended this will, at worst, make it .03125 off, which is unlikely to be a problem. */ + font_size = round(20.0 * 0.8 * font_size)/(20.0 * 0.8); + d->level = cur_level; + d->dc[d->level].style.font_size.computed = font_size; + d->dc[d->level].style.font_weight.value = + font.Weight == U_FW_THIN ? SP_CSS_FONT_WEIGHT_100 : + font.Weight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_200 : + font.Weight == U_FW_LIGHT ? SP_CSS_FONT_WEIGHT_300 : + font.Weight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_400 : + font.Weight == U_FW_MEDIUM ? SP_CSS_FONT_WEIGHT_500 : + font.Weight == U_FW_SEMIBOLD ? SP_CSS_FONT_WEIGHT_600 : + font.Weight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_700 : + font.Weight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_800 : + font.Weight == U_FW_HEAVY ? SP_CSS_FONT_WEIGHT_900 : + font.Weight == U_FW_NORMAL ? SP_CSS_FONT_WEIGHT_NORMAL : + font.Weight == U_FW_BOLD ? SP_CSS_FONT_WEIGHT_BOLD : + font.Weight == U_FW_EXTRALIGHT ? SP_CSS_FONT_WEIGHT_LIGHTER : + font.Weight == U_FW_EXTRABOLD ? SP_CSS_FONT_WEIGHT_BOLDER : + SP_CSS_FONT_WEIGHT_NORMAL; + d->dc[d->level].style.font_style.value = (font.Italic ? SP_CSS_FONT_STYLE_ITALIC : SP_CSS_FONT_STYLE_NORMAL); + d->dc[d->level].style.text_decoration_line.underline = font.Underline; + d->dc[d->level].style.text_decoration_line.line_through = font.StrikeOut; + d->dc[d->level].style.text_decoration_line.set = true; + d->dc[d->level].style.text_decoration_line.inherit = false; + + // malformed WMF with empty filename may exist, ignore font change if encountered + if(d->dc[d->level].font_name)free(d->dc[d->level].font_name); + if(*facename){ + d->dc[d->level].font_name = strdup(facename); + } + else { // Malformed WMF might specify an empty font name + d->dc[d->level].font_name = strdup("Arial"); // Default font, WMF spec says device can pick whatever it wants + } + d->dc[d->level].style.baseline_shift.value = round((double)((font.Escapement + 3600) % 3600) / 10.0); // use baseline_shift instead of text_transform to avoid overflow +} + +/* Find the first free hole where an object may be stored. + If there are not any return -1. This is a big error, possibly from a corrupt WMF file. +*/ +int Wmf::insertable_object(PWMF_CALLBACK_DATA d) +{ + int index = d->low_water; // Start looking from here, it may already have been filled + while(index < d->n_obj && d->wmf_obj[index].record != nullptr){ index++; } + if(index >= d->n_obj)return(-1); // this is a big problem, percolate it back up so the program can get out of this gracefully + d->low_water = index; // Could probably be index+1 + return(index); +} + +void +Wmf::delete_object(PWMF_CALLBACK_DATA d, int index) +{ + if (index >= 0 && index < d->n_obj) { + // If the active object is deleted set default draw values + if(index == d->dc[d->level].active_pen){ // Use default pen: solid, black, 1 pixel wide + d->dc[d->level].active_pen = -1; + d->dc[d->level].style.stroke_dasharray.set = false; + d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; // U_PS_ENDCAP_SQUARE + d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; + d->dc[d->level].stroke_set = true; + d->dc[d->level].style.stroke_width.value = 1.0; + d->dc[d->level].style.stroke.value.color.set( 0, 0, 0 ); + } + else if(index == d->dc[d->level].active_brush){ + d->dc[d->level].active_brush = -1; + d->dc[d->level].fill_set = false; + } + else if(index == d->dc[d->level].active_font){ + d->dc[d->level].active_font = -1; + if(d->dc[d->level].font_name){ free(d->dc[d->level].font_name);} + d->dc[d->level].font_name = strdup("Arial"); // Default font, WMF spec says device can pick whatever it wants + d->dc[d->level].style.font_size.computed = 16.0; + d->dc[d->level].style.font_weight.value = SP_CSS_FONT_WEIGHT_400; + d->dc[d->level].style.font_style.value = SP_CSS_FONT_STYLE_NORMAL; + d->dc[d->level].style.text_decoration_line.underline = false; + d->dc[d->level].style.text_decoration_line.line_through = false; + d->dc[d->level].style.baseline_shift.value = 0; + } + + + d->wmf_obj[index].type = 0; +// We are keeping a copy of the WMR rather than just a structure. Currently that is not necessary as the entire +// WMF is read in at once and is stored in a big malloc. However, in past versions it was handled +// record by record, and we might need to do that again at some point in the future if we start running into WMF +// files too big to fit into memory. + if (d->wmf_obj[index].record) + free(d->wmf_obj[index].record); + d->wmf_obj[index].record = nullptr; + if(index < d->low_water)d->low_water = index; + } +} + + +// returns the new index, or -1 on error. +int Wmf::insert_object(PWMF_CALLBACK_DATA d, int type, const char *record) +{ + int index = insertable_object(d); + if(index>=0){ + d->wmf_obj[index].type = type; + d->wmf_obj[index].level = d->level; + d->wmf_obj[index].record = wmr_dup(record); + } + return(index); +} + + +/** + \fn create a UTF-32LE buffer and fill it with UNICODE unknown character + \param count number of copies of the Unicode unknown character to fill with +*/ +uint32_t *Wmf::unknown_chars(size_t count){ + uint32_t *res = (uint32_t *) malloc(sizeof(uint32_t) * (count + 1)); + if(!res)throw "Inkscape fatal memory allocation error - cannot continue"; + for(uint32_t i=0; i<count; i++){ res[i] = 0xFFFD; } + res[count]=0; + return res; +} + +/** + \brief store SVG for an image given the pixmap and various coordinate information + \param d + \param dib packed DIB in memory + \param dx (double) destination x in inkscape pixels + \param dy (double) destination y in inkscape pixels + \param dw (double) destination width in inkscape pixels + \param dh (double) destination height in inkscape pixels + \param sx (int) source x in src image pixels + \param sy (int) source y in src image pixels + \param iUsage +*/ +void Wmf::common_dib_to_image(PWMF_CALLBACK_DATA d, const char *dib, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, uint32_t iUsage){ + + SVGOStringStream tmp_image; + int dibparams = U_BI_UNKNOWN; // type of image not yet determined + + tmp_image << "\n\t <image\n"; + if (d->dc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const char *px = nullptr; // DIB pixels + const U_RGBQUAD *ct = nullptr; // color table + uint32_t numCt; + int32_t width, height, colortype, invert; // if needed these values will be set in wget_DIB_params + if(iUsage == U_DIB_RGB_COLORS){ + // next call returns pointers and values, but allocates no memory + dibparams = wget_DIB_params(dib, &px, &ct, &numCt, &width, &height, &colortype, &invert); + if(dibparams == U_BI_RGB){ + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + if(!DIB_to_RGBA( + px, // DIB pixel array + ct, // DIB color table + numCt, // DIB color table number of entries + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + } + } + + gchar *base64String=nullptr; + if(dibparams == U_BI_JPEG){ // image was binary jpg in source file + tmp_image << " xlink:href=\"data:image/jpeg;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(dibparams==U_BI_PNG){ // image was binary png in source file + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) px, numCt ); + } + else if(mempng.buffer){ // image was DIB in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + tmp_image << " transform=" << current_matrix(d, 0.0, 0.0, 0); // returns an identity matrix, no offsets. + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \brief store SVG for an image given the pixmap and various coordinate information + \param d + \param Bm16 core Bitmap16 header + \param px pointer to Bitmap16 image data + \param dx (double) destination x in inkscape pixels + \param dy (double) destination y in inkscape pixels + \param dw (double) destination width in inkscape pixels + \param dh (double) destination height in inkscape pixels + \param sx (int) source x in src image pixels + \param sy (int) source y in src image pixels + \param iUsage +*/ +void Wmf::common_bm16_to_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh){ + + SVGOStringStream tmp_image; + + tmp_image << "\n\t <image\n"; + if (d->dc[d->level].clip_id){ + tmp_image << "\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n"; + } + tmp_image << " y=\"" << dy << "\"\n x=\"" << dx <<"\"\n "; + + MEMPNG mempng; // PNG in memory comes back in this + mempng.buffer = nullptr; + + char *rgba_px = nullptr; // RGBA pixels + char *sub_px = nullptr; // RGBA pixels, subarray + const U_RGBQUAD *ct = nullptr; // color table + int32_t width, height, colortype, numCt, invert; + + numCt = 0; + width = Bm16.Width; // bitmap width in pixels. + height = Bm16.Height; // bitmap height in scan lines. + colortype = Bm16.BitsPixel; // seems to be BitCount Enumeration + invert = 0; + + if(sw == 0 || sh == 0){ + sw = width; + sh = height; + } + + if(colortype < 16)return; // these would need a colortable if they were a dib, no idea what bm16 is supposed to do instead. + + if(!DIB_to_RGBA(// This is not really a dib, but close enough so that it still works. + px, // DIB pixel array + ct, // DIB color table (always NULL here) + numCt, // DIB color table number of entries (always 0) + &rgba_px, // U_RGBA pixel array (32 bits), created by this routine, caller must free. + width, // Width of pixel array + height, // Height of pixel array + colortype, // DIB BitCount Enumeration + numCt, // Color table used if not 0 + invert // If DIB rows are in opposite order from RGBA rows + )){ + sub_px = RGBA_to_RGBA( // returns either a subset (side effect: frees rgba_px) or NULL (for subset == entire image) + rgba_px, // full pixel array from DIB + width, // Width of pixel array + height, // Height of pixel array + sx,sy, // starting point in pixel array + &sw,&sh // columns/rows to extract from the pixel array (output array size) + ); + + if(!sub_px)sub_px=rgba_px; + toPNG( // Get the image from the RGBA px into mempng + &mempng, + sw, sh, // size of the extracted pixel array + sub_px + ); + free(sub_px); + } + + gchar *base64String=nullptr; + if(mempng.buffer){ // image was Bm16 in source file, converted to png in this routine + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = g_base64_encode((guchar*) mempng.buffer, mempng.size ); + free(mempng.buffer); + } + else { // unknown or unsupported image type or failed conversion, insert the common bad image picture + tmp_image << " xlink:href=\"data:image/png;base64,"; + base64String = bad_image_png(); + } + tmp_image << base64String; + g_free(base64String); + + tmp_image << "\"\n height=\"" << dh << "\"\n width=\"" << dw << "\"\n"; + tmp_image << " transform=" << current_matrix(d, 0.0, 0.0, 0); // returns an identity matrix, no offsets. + tmp_image << " preserveAspectRatio=\"none\"\n"; + tmp_image << "/> \n"; + + d->outsvg += tmp_image.str().c_str(); + d->path = ""; +} + +/** + \fn myMetaFileProc(char *contents, unsigned int length, PWMF_CALLBACK_DATA lpData) + \returns 1 on success, 0 on error + \param contents binary contents of an WMF file + \param length length in bytes of contents + \param d Inkscape data structures returned by this call +*/ +//THis was a callback, just build it into a normal function +int Wmf::myMetaFileProc(const char *contents, unsigned int length, PWMF_CALLBACK_DATA d) +{ + uint32_t off=0; + uint32_t wmr_mask; + int OK =1; + int file_status=1; + TCHUNK_SPECS tsp; + uint8_t iType; + int nSize; // size of the current record, in bytes, or an error value if <=0 + const char *blimit = contents + length; // 1 byte past the end of the last record + + /* variables used to retrieve data from WMF records */ + uint16_t utmp16; + U_POINT16 pt16; // any point + U_RECT16 rc; // any rectangle, usually a bounding rectangle + U_POINT16 Dst; // Destination coordinates + U_POINT16 cDst; // Destination w,h, if different from Src + U_POINT16 Src; // Source coordinates + U_POINT16 cSrc; // Source w,h, if different from Dst + U_POINT16 cwh; // w,h, if Src and Dst use the same values + uint16_t cUsage; // colorusage enumeration + uint32_t dwRop3; // raster operations, these are only barely supported here + const char *dib; // DIB style image structure + U_BITMAP16 Bm16; // Bitmap16 style image structure + const char *px; // Image for Bm16 + uint16_t cPts; // number of points in the next variable + const char *points; // any list of U_POINT16, may not be aligned + int16_t tlen; // length of returned text, in bytes + const char *text; // returned text, Latin1 encoded + uint16_t Opts; + const int16_t *dx; // character spacing for one text mode, inkscape ignores this + double left, right, top, bottom; // values used, because a bounding rect can have values reversed L<->R, T<->B + + uint16_t tbkMode = U_TRANSPARENT; // holds proposed change to bkMode, if text is involved saving these to the DC must wait until the text is written + U_COLORREF tbkColor = U_RGB(255, 255, 255); // holds proposed change to bkColor + + // code for end user debugging + int wDbgRecord=0; + int wDbgComment=0; + int wDbgFinal=0; + char const* wDbgString = getenv( "INKSCAPE_DBG_WMF" ); + if ( wDbgString != nullptr ) { + if(strstr(wDbgString,"RECORD")){ wDbgRecord = 1; } + if(strstr(wDbgString,"COMMENT")){ wDbgComment = 1; } + if(strstr(wDbgString,"FINAL")){ wDbgFinal = 1; } + } + + /* initialize the tsp for text reassembly */ + tsp.string = nullptr; + tsp.ori = 0.0; /* degrees */ + tsp.fs = 12.0; /* font size */ + tsp.x = 0.0; + tsp.y = 0.0; + tsp.boff = 0.0; /* offset to baseline from LL corner of bounding rectangle, changes with fs and taln*/ + tsp.vadvance = 0.0; /* meaningful only when a complex contains two or more lines */ + tsp.taln = ALILEFT + ALIBASE; + tsp.ldir = LDIR_LR; + tsp.spaces = 0; // this field is only used for debugging + tsp.color.Red = 0; /* RGB Black */ + tsp.color.Green = 0; /* RGB Black */ + tsp.color.Blue = 0; /* RGB Black */ + tsp.color.Reserved = 0; /* not used */ + tsp.italics = 0; + tsp.weight = 80; + tsp.decoration = TXTDECOR_NONE; + tsp.condensed = 100; + tsp.co = 0; + tsp.fi_idx = -1; /* set to an invalid */ + tsp.rt_tidx = -1; /* set to an invalid */ + + SVGOStringStream dbg_str; + + /* There is very little information in WMF headers, get what is there. In many cases pretty much everything will have to + default. If there is no placeable header we know pretty much nothing about the size of the page, in which case + assume that it is 1440 WMF pixels/inch and make the page A4 landscape. That is almost certainly the wrong page size + but it has to be set to something, and nothing horrible happens if the drawing goes off the page. */ + { + + U_WMRPLACEABLE Placeable; + U_WMRHEADER Header; + off = 0; + nSize = wmfheader_get(contents, blimit, &Placeable, &Header); + if (!nSize) { + return(0); + } + if(!Header.nObjects){ Header.nObjects = 256; }// there _may_ be WMF files with no objects, more likely it is corrupt. Try to use it anyway. + d->n_obj = Header.nObjects; + d->wmf_obj = new WMF_OBJECT[d->n_obj]; + d->low_water = 0; // completely empty at this point, so start searches at 0 + + // Init the new wmf_obj list elements to null, provided the + // dynamic allocation succeeded. + if ( d->wmf_obj != nullptr ) + { + for( int i=0; i < d->n_obj; ++i ) + d->wmf_obj[i].record = nullptr; + } //if + + if(!Placeable.Inch){ Placeable.Inch= 1440; } + if(!Placeable.Dst.right && !Placeable.Dst.left){ // no page size has been supplied + // This is gross, scan forward looking for a SETWINDOWEXT record, use the first one found to + // define the page size + int hold_nSize = off = nSize; + Placeable.Dst.left = 0; + Placeable.Dst.top = 0; + while(OK){ + nSize = U_WMRRECSAFE_get(contents + off, blimit); + if(nSize){ + iType = *(uint8_t *)(contents + off + offsetof(U_METARECORD, iType ) ); + if(iType == U_WMR_SETWINDOWEXT){ + OK=0; + (void) U_WMRSETWINDOWEXT_get(contents + off, &Dst); + Placeable.Dst.right = Dst.x; + Placeable.Dst.bottom = Dst.y; + } + else if(iType == U_WMR_EOF){ + OK=0; + // Really messed up WMF, have to set the page to something, make it A4 horizontal + Placeable.Dst.right = round(((double) Placeable.Inch) * 297.0/25.4); + Placeable.Dst.bottom = round(((double) Placeable.Inch) * 210.0/25.4); + } + else { + off += nSize; + } + } + else { + return(0); + } + } + off=0; + nSize = hold_nSize; + OK=1; + } + + // drawing size in WMF pixels + d->PixelsInX = Placeable.Dst.right - Placeable.Dst.left + 1; + d->PixelsInY = Placeable.Dst.bottom - Placeable.Dst.top + 1; + + /* + Set values for Window and ViewPort extents to 0 - not defined yet. + */ + d->dc[d->level].sizeView.x = d->dc[d->level].sizeWnd.x = 0; + d->dc[d->level].sizeView.y = d->dc[d->level].sizeWnd.y = 0; + + /* Upper left corner in device units, usually both 0, but not always. + If a placeable header is used, and later a windoworg/windowext are found, then + the placeable information will be ignored. + */ + d->ulCornerInX = Placeable.Dst.left; + d->ulCornerInY = Placeable.Dst.top; + + d->E2IdirY = 1.0; // assume MM_ANISOTROPIC, if not, this will be changed later + d->D2PscaleX = d->D2PscaleY = Inkscape::Util::Quantity::convert(1, "in", "px")/(double) Placeable.Inch; + trinfo_load_qe(d->tri, d->D2PscaleX); /* quantization error that will affect text positions */ + + // drawing size in Inkscape pixels + d->PixelsOutX = d->PixelsInX * d->D2PscaleX; + d->PixelsOutY = d->PixelsInY * d->D2PscaleY; + + // Upper left corner in Inkscape units + d->ulCornerOutX = d->ulCornerInX * d->D2PscaleX; + d->ulCornerOutY = d->ulCornerInY * d->E2IdirY * d->D2PscaleY; + + d->dc[0].style.stroke_width.value = pix_to_abs_size( d, 1 ); // This could not be set until the size of the WMF was known + dbg_str << "<!-- U_WMR_HEADER -->\n"; + + d->outdef += "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"; + + SVGOStringStream tmp_outdef; + tmp_outdef << "<svg\n"; + tmp_outdef << " xmlns:svg=\"http://www.w3.org/2000/svg\"\n"; + tmp_outdef << " xmlns=\"http://www.w3.org/2000/svg\"\n"; + tmp_outdef << " xmlns:xlink=\"http://www.w3.org/1999/xlink\"\n"; + tmp_outdef << " xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"\n"; // needed for sodipodi:role + tmp_outdef << " version=\"1.0\"\n"; + + tmp_outdef << + " width=\"" << Inkscape::Util::Quantity::convert(d->PixelsOutX, "px", "mm") << "mm\"\n" << + " height=\"" << Inkscape::Util::Quantity::convert(d->PixelsOutY, "px", "mm") << "mm\">\n"; + d->outdef += tmp_outdef.str().c_str(); + d->outdef += "<defs>"; // temporary end of header + + // d->defs holds any defines which are read in. + + + } + + + + while(OK){ + if (off>=length) { + return(0); //normally should exit from while after WMREOF sets OK to false. + } + contents += nSize; // pointer to the start of the next record + off += nSize; // offset from beginning of buffer to the start of the next record + + /* Currently this is a weaker check than for EMF, it only checks the size of the constant part + of the record */ + nSize = U_WMRRECSAFE_get(contents, blimit); + if(!nSize) { + file_status = 0; + break; + } + + iType = *(uint8_t *)(contents + offsetof(U_METARECORD, iType ) ); + + wmr_mask = U_wmr_properties(iType); + if (wmr_mask == U_WMR_INVALID) { + file_status = 0; + break; + } +// At run time define environment variable INKSCAPE_DBG_WMF to include string RECORD. +// Users may employ this to track down toxic records + if(wDbgRecord){ + std::cout << "record type: " << iType << " name " << U_wmr_names(iType) << " length: " << nSize << " offset: " << off <<std::endl; + } + + SVGOStringStream tmp_path; + SVGOStringStream tmp_str; + +/* Uncomment the following to track down text problems */ +//std::cout << "tri->dirty:"<< d->tri->dirty << " wmr_mask: " << std::hex << wmr_mask << std::dec << std::endl; + + // incompatible change to text drawing detected (color or background change) forces out existing text + // OR + // next record is valid type and forces pending text to be drawn immediately + if ((d->dc[d->level].dirty & DIRTY_TEXT) || ((wmr_mask != U_WMR_INVALID) && (wmr_mask & U_DRAW_TEXT) && d->tri->dirty)){ + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\n<g\n\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + SVGOStringStream ts; + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + if (d->dc[d->level].clip_id){ + d->outsvg += "\n</g>\n"; + } + } + if(d->dc[d->level].dirty){ //Apply the delayed background changes, clear the flag + d->dc[d->level].bkMode = tbkMode; + memcpy(&(d->dc[d->level].bkColor),&tbkColor, sizeof(U_COLORREF)); + + if(d->dc[d->level].dirty & DIRTY_TEXT){ + // U_COLORREF and TRCOLORREF are exactly the same in memory, but the compiler needs some convincing... + if(tbkMode == U_TRANSPARENT){ (void) trinfo_load_bk(d->tri, BKCLR_NONE, *(TRCOLORREF *) &tbkColor); } + else { (void) trinfo_load_bk(d->tri, BKCLR_LINE, *(TRCOLORREF *) &tbkColor); } // Opaque + } + + /* It is possible to have a series of EMF records that would result in + the following creating hash patterns which are never used. For instance, if + there were a series of records that changed the background color but did nothing + else. + */ + if((d->dc[d->level].fill_mode == DRAW_PATTERN) && (d->dc[d->level].dirty & DIRTY_FILL)){ + select_brush(d, d->dc[d->level].fill_recidx); + } + + d->dc[d->level].dirty = 0; + } + +//std::cout << "BEFORE DRAW logic d->mask: " << std::hex << d->mask << " wmr_mask: " << wmr_mask << std::dec << std::endl; +/* +std::cout << "BEFORE DRAW" + << " test0 " << ( d->mask & U_DRAW_VISIBLE) + << " test1 " << ( d->mask & U_DRAW_FORCE) + << " test2 " << (wmr_mask & U_DRAW_ALTERS) + << " test2.5 " << ((d->mask & U_DRAW_NOFILL) != (wmr_mask & U_DRAW_NOFILL) ) + << " test3 " << (wmr_mask & U_DRAW_VISIBLE) + << " test4 " << !(d->mask & U_DRAW_ONLYTO) + << " test5 " << ((d->mask & U_DRAW_ONLYTO) && !(wmr_mask & U_DRAW_ONLYTO) ) + << std::endl; +*/ + /* spurious moveto records should not affect the drawing. However, they set the NOFILL + bit and that messes up the logic about when to emit a path. So prune out any + stray moveto records. That is those which were never followed by a lineto. + */ + if((d->mask & U_DRAW_NOFILL) && !(d->mask & U_DRAW_VISIBLE) && + !(wmr_mask & U_DRAW_ONLYTO) && (wmr_mask & U_DRAW_VISIBLE)){ + d->mask ^= U_DRAW_NOFILL; + } + + if( + (wmr_mask != U_WMR_INVALID) && // next record is valid type + (d->mask & U_DRAW_VISIBLE) && // This record is drawable + ( + (d->mask & U_DRAW_FORCE) || // This draw is forced by STROKE/FILL/STROKEANDFILL PATH + (wmr_mask & U_DRAW_ALTERS) || // Next record would alter the drawing environment in some way + ((d->mask & U_DRAW_NOFILL) != (wmr_mask & U_DRAW_NOFILL)) || // Fill<->!Fill requires a draw between + ( (wmr_mask & U_DRAW_VISIBLE) && // Next record is visible... + ( + ( !(d->mask & U_DRAW_ONLYTO) ) || // Non *TO records cannot be followed by any Visible + ((d->mask & U_DRAW_ONLYTO) && !(wmr_mask & U_DRAW_ONLYTO) )// *TO records can only be followed by other *TO records + ) + ) + ) + ){ +// std::cout << "PATH DRAW at TOP <<+++++++++++++++++++++++++++++++++++++" << std::endl; + if(!(d->path.empty())){ + d->outsvg += " <path "; // this is the ONLY place <path should be used!!!! + output_style(d); + d->outsvg += "\n\t"; + d->outsvg += "\n\td=\""; // this is the ONLY place d=" should be used!!!! + d->outsvg += d->path; + d->outsvg += " \" /> \n"; + d->path = ""; //reset the path + } + // reset the flags + d->mask = 0; + d->drawtype = 0; + } +// std::cout << "AFTER DRAW logic d->mask: " << std::hex << d->mask << " wmr_mask: " << wmr_mask << std::dec << std::endl; + switch (iType) + { + case U_WMR_EOF: + { + dbg_str << "<!-- U_WMR_EOF -->\n"; + + d->outsvg = d->outdef + d->defs + "\n</defs>\n\n" + d->outsvg + "</svg>\n"; + OK=0; + break; + } + case U_WMR_SETBKCOLOR: + { + dbg_str << "<!-- U_WMR_SETBKCOLOR -->\n"; + nSize = U_WMRSETBKCOLOR_get(contents, &tbkColor); + if(memcmp(&tbkColor, &(d->dc[d->level].bkColor), sizeof(U_COLORREF))){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + tbkMode = d->dc[d->level].bkMode; + } + break; + } + case U_WMR_SETBKMODE:{ + dbg_str << "<!-- U_WMR_SETBKMODE -->\n"; + nSize = U_WMRSETBKMODE_get(contents, &tbkMode); + if(tbkMode != d->dc[d->level].bkMode){ + d->dc[d->level].dirty |= DIRTY_TEXT; + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + } + memcpy(&tbkColor,&(d->dc[d->level].bkColor),sizeof(U_COLORREF)); + } + break; + } + case U_WMR_SETMAPMODE: + { + dbg_str << "<!-- U_WMR_SETMAPMODE -->\n"; + nSize = U_WMRSETMAPMODE_get(contents, &utmp16); + switch (utmp16){ + case U_MM_TEXT: + default: + // Use all values from the header. + break; + /* For all of the following the indicated scale this will be encoded in WindowExtEx/ViewportExtex + and show up in ScaleIn[XY] + */ + case U_MM_LOMETRIC: // 1 LU = 0.1 mm, + case U_MM_HIMETRIC: // 1 LU = 0.01 mm + case U_MM_LOENGLISH: // 1 LU = 0.1 in + case U_MM_HIENGLISH: // 1 LU = 0.01 in + case U_MM_TWIPS: // 1 LU = 1/1440 in + d->E2IdirY = -1.0; + // Use d->D2Pscale[XY] values from the header. + break; + case U_MM_ISOTROPIC: // ScaleIn[XY] should be set elsewhere by SETVIEWPORTEXTEX and SETWINDOWEXTEX + case U_MM_ANISOTROPIC: + break; + } + break; + } + case U_WMR_SETROP2: + { + dbg_str << "<!-- U_WMR_SETROP2 -->\n"; + nSize = U_WMRSETROP2_get(contents, &utmp16); + d->dwRop2 = utmp16; + break; + } + case U_WMR_SETRELABS: dbg_str << "<!-- U_WMR_SETRELABS -->\n"; break; + case U_WMR_SETPOLYFILLMODE: + { + dbg_str << "<!-- U_WMR_SETPOLYFILLMODE -->\n"; + nSize = U_WMRSETPOLYFILLMODE_get(contents, &utmp16); + d->dc[d->level].style.fill_rule.value = + (utmp16 == U_ALTERNATE ? SP_WIND_RULE_NONZERO + : utmp16 == U_WINDING ? SP_WIND_RULE_INTERSECT : SP_WIND_RULE_NONZERO); + break; + } + case U_WMR_SETSTRETCHBLTMODE: + { + dbg_str << "<!-- U_WMR_SETSTRETCHBLTMODE -->\n"; + nSize = U_WMRSETSTRETCHBLTMODE_get(contents, &utmp16); + BLTmode = utmp16; + break; + } + case U_WMR_SETTEXTCHAREXTRA: dbg_str << "<!-- U_WMR_SETTEXTCHAREXTRA -->\n"; break; + case U_WMR_SETTEXTCOLOR: + { + dbg_str << "<!-- U_WMR_SETTEXTCOLOR -->\n"; + nSize = U_WMRSETTEXTCOLOR_get(contents, &(d->dc[d->level].textColor)); + if(tbkMode != d->dc[d->level].bkMode){ + if(d->dc[d->level].fill_mode == DRAW_PATTERN){ d->dc[d->level].dirty |= DIRTY_FILL; } + } + // not text_dirty, because multicolored complex text is supported in libTERE + break; + } + case U_WMR_SETTEXTJUSTIFICATION: dbg_str << "<!-- U_WMR_SETTEXTJUSTIFICATION -->\n"; break; + case U_WMR_SETWINDOWORG: + { + dbg_str << "<!-- U_WMR_SETWINDOWORG -->\n"; + nSize = U_WMRSETWINDOWORG_get(contents, &d->dc[d->level].winorg); + d->ulCornerOutX = 0.0; // In the examples seen to date if this record is used with a placeable header, that header is ignored + d->ulCornerOutY = 0.0; + break; + } + case U_WMR_SETWINDOWEXT: + { + dbg_str << "<!-- U_WMR_SETWINDOWEXT -->\n"; + + nSize = U_WMRSETWINDOWEXT_get(contents, &d->dc[d->level].sizeWnd); + + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd.x = d->PixelsOutX; + d->dc[d->level].sizeWnd.y = d->PixelsOutY; + } + } + else { + /* There are a lot WMF files in circulation with the x,y values in the setwindowext reversed. If this is detected, swap them. + There is a remote possibility that the strange scaling this implies was intended, and those will be rendered incorrectly */ + double Ox = d->PixelsOutX; + double Oy = d->PixelsOutY; + double Wx = d->dc[d->level].sizeWnd.x; + double Wy = d->dc[d->level].sizeWnd.y; + if(Wx != Wy && Geom::are_near(Ox/Wy, Oy/Wx, 1.01/MIN(Wx,Wy)) ){ + int tmp; + tmp = d->dc[d->level].sizeWnd.x; + d->dc[d->level].sizeWnd.x = d->dc[d->level].sizeWnd.y; + d->dc[d->level].sizeWnd.y = tmp; + } + } + + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + /* Previously it used sizeWnd, but that always resulted in scale = 1 if no viewport ever appeared, and in most files, it did not */ + d->dc[d->level].sizeView.x = d->PixelsInX - 1; + d->dc[d->level].sizeView.y = d->PixelsInY - 1; + } + + /* scales logical to WMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.x && d->dc[d->level].sizeWnd.y) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.x / (double) d->dc[d->level].sizeWnd.x; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.y / (double) d->dc[d->level].sizeWnd.y; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_WMR_SETVIEWPORTORG: + { + dbg_str << "<!-- U_WMR_SETWINDOWORG -->\n"; + nSize = U_WMRSETVIEWPORTORG_get(contents, &d->dc[d->level].vieworg); + break; + } + case U_WMR_SETVIEWPORTEXT: + { + dbg_str << "<!-- U_WMR_SETVIEWPORTEXTEX -->\n"; + + nSize = U_WMRSETVIEWPORTEXT_get(contents, &d->dc[d->level].sizeView); + + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + d->dc[d->level].sizeView = d->dc[d->level].sizeWnd; + if (!d->dc[d->level].sizeView.x || !d->dc[d->level].sizeView.y) { + d->dc[d->level].sizeView.x = d->PixelsOutX; + d->dc[d->level].sizeView.y = d->PixelsOutY; + } + } + + if (!d->dc[d->level].sizeWnd.x || !d->dc[d->level].sizeWnd.y) { + d->dc[d->level].sizeWnd = d->dc[d->level].sizeView; + } + + /* scales logical to WMF pixels, transfer a negative sign on Y, if any */ + if (d->dc[d->level].sizeWnd.x && d->dc[d->level].sizeWnd.y) { + d->dc[d->level].ScaleInX = (double) d->dc[d->level].sizeView.x / (double) d->dc[d->level].sizeWnd.x; + d->dc[d->level].ScaleInY = (double) d->dc[d->level].sizeView.y / (double) d->dc[d->level].sizeWnd.y; + if(d->dc[d->level].ScaleInY < 0){ + d->dc[d->level].ScaleInY *= -1.0; + d->E2IdirY = -1.0; + } + } + else { + d->dc[d->level].ScaleInX = 1; + d->dc[d->level].ScaleInY = 1; + } + break; + } + case U_WMR_OFFSETWINDOWORG: dbg_str << "<!-- U_WMR_OFFSETWINDOWORG -->\n"; break; + case U_WMR_SCALEWINDOWEXT: dbg_str << "<!-- U_WMR_SCALEWINDOWEXT -->\n"; break; + case U_WMR_OFFSETVIEWPORTORG: dbg_str << "<!-- U_WMR_OFFSETVIEWPORTORG -->\n"; break; + case U_WMR_SCALEVIEWPORTEXT: dbg_str << "<!-- U_WMR_SCALEVIEWPORTEXT -->\n"; break; + case U_WMR_LINETO: + { + dbg_str << "<!-- U_WMR_LINETO -->\n"; + + nSize = U_WMRLINETO_get(contents, &pt16); + + d->mask |= wmr_mask; + + tmp_path << "\n\tL " << pix_to_xy( d, pt16.x, pt16.y) << " "; + break; + } + case U_WMR_MOVETO: + { + dbg_str << "<!-- U_WMR_MOVETO -->\n"; + + nSize = U_WMRLINETO_get(contents, &pt16); + + d->mask |= wmr_mask; + + d->dc[d->level].cur = pt16; + + tmp_path << + "\n\tM " << pix_to_xy( d, pt16.x, pt16.y ) << " "; + break; + } + case U_WMR_EXCLUDECLIPRECT: + { + dbg_str << "<!-- U_WMR_EXCLUDECLIPRECT -->\n"; + + U_RECT16 rc; + nSize = U_WMREXCLUDECLIPRECT_get(contents, &rc); + + SVGOStringStream tmp_path; + float faraway = 10000000; // hopefully well outside any real drawing! + //outer rect, clockwise + tmp_path << "M " << faraway << "," << faraway << " "; + tmp_path << "L " << faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << -faraway << " "; + tmp_path << "L " << -faraway << "," << faraway << " "; + tmp_path << "z "; + //inner rect, counterclockwise (sign of Y is reversed) + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_WMR_INTERSECTCLIPRECT: + { + dbg_str << "<!-- U_WMR_INTERSECTCLIPRECT -->\n"; + + nSize = U_WMRINTERSECTCLIPRECT_get(contents, &rc); + + SVGOStringStream tmp_path; + tmp_path << "M " << pix_to_xy( d, rc.left , rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.top ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.right, rc.bottom ) << " "; + tmp_path << "L " << pix_to_xy( d, rc.left, rc.bottom ) << " "; + tmp_path << "z"; + + add_clips(d, tmp_path.str().c_str(), U_RGN_AND); + + d->path = ""; + d->drawtype = 0; + break; + } + case U_WMR_ARC: + { + dbg_str << "<!-- U_WMR_ARC -->\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRARC_get(contents, &ArcStart, &ArcEnd, &rc); + + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + int stat = wmr_arc_points(rc, ArcStart, ArcEnd,&f1, f2, ¢er, &start, &end, &size); + if(!stat){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + d->mask |= wmr_mask; + } + else { + dbg_str << "<!-- ARC record is invalid -->\n"; + } + break; + } + case U_WMR_ELLIPSE: + { + dbg_str << "<!-- U_WMR_ELLIPSE -->\n"; + + nSize = U_WMRELLIPSE_get(contents, &rc); + + double cx = pix_to_x_point( d, (rc.left + rc.right)/2.0, (rc.bottom + rc.top)/2.0 ); + double cy = pix_to_y_point( d, (rc.left + rc.right)/2.0, (rc.bottom + rc.top)/2.0 ); + double rx = pix_to_abs_size( d, std::abs(rc.right - rc.left )/2.0 ); + double ry = pix_to_abs_size( d, std::abs(rc.top - rc.bottom)/2.0 ); + + SVGOStringStream tmp_ellipse; + tmp_ellipse << "cx=\"" << cx << "\" "; + tmp_ellipse << "cy=\"" << cy << "\" "; + tmp_ellipse << "rx=\"" << rx << "\" "; + tmp_ellipse << "ry=\"" << ry << "\" "; + + d->mask |= wmr_mask; + + d->outsvg += " <ellipse "; + output_style(d); + d->outsvg += "\n\t"; + d->outsvg += tmp_ellipse.str().c_str(); + d->outsvg += "/> \n"; + d->path = ""; + break; + } + case U_WMR_FLOODFILL: dbg_str << "<!-- U_WMR_EXTFLOODFILL -->\n"; break; + case U_WMR_PIE: + { + dbg_str << "<!-- U_WMR_PIE -->\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRPIE_get(contents, &ArcStart, &ArcEnd, &rc); + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!wmr_arc_points(rc, ArcStart, ArcEnd, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, center.x, center.y); + tmp_path << "\n\tL " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= wmr_mask; + } + else { + dbg_str << "<!-- PIE record is invalid -->\n"; + } + break; + } + case U_WMR_RECTANGLE: + { + dbg_str << "<!-- U_WMR_RECTANGLE -->\n"; + + nSize = U_WMRRECTANGLE_get(contents, &rc); + U_sanerect16(rc, &left, &top, &right, &bottom); + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, left , top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, right, top ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, right, bottom ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, left, bottom ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_ROUNDRECT: + { + dbg_str << "<!-- U_WMR_ROUNDRECT -->\n"; + + int16_t Height,Width; + nSize = U_WMRROUNDRECT_get(contents, &Width, &Height, &rc); + U_sanerect16(rc, &left, &top, &right, &bottom); + double f = 4.*(sqrt(2) - 1)/3; + double f1 = 1.0 - f; + double cnx = Width/2; + double cny = Height/2; + + + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n" + << " M " + << pix_to_xy(d, left , top + cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, left , top + cny*f1 ) + << " " + << pix_to_xy(d, left + cnx*f1 , top ) + << " " + << pix_to_xy(d, left + cnx , top ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, right - cnx , top ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, right - cnx*f1 , top ) + << " " + << pix_to_xy(d, right , top + cny*f1 ) + << " " + << pix_to_xy(d, right , top + cny ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, right , bottom - cny ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, right , bottom - cny*f1 ) + << " " + << pix_to_xy(d, right - cnx*f1 , bottom ) + << " " + << pix_to_xy(d, right - cnx , bottom ) + << "\n"; + tmp_rectangle << " L " + << pix_to_xy(d, left + cnx , bottom ) + << "\n"; + tmp_rectangle << " C " + << pix_to_xy(d, left + cnx*f1 , bottom ) + << " " + << pix_to_xy(d, left , bottom - cny*f1 ) + << " " + << pix_to_xy(d, left , bottom - cny ) + << "\n"; + tmp_rectangle << " z\n"; + + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_PATBLT: + { + dbg_str << "<!-- U_WMR_PATBLT -->\n"; + // Treat this like any other rectangle, ie, ignore the dwRop3 + nSize = U_WMRPATBLT_get(contents, &Dst, &cwh, &dwRop3); + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, Dst.x , Dst.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x + cwh.x, Dst.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x + cwh.x, Dst.y + cwh.y ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, Dst.x, Dst.y + cwh.y ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + + tmp_path << tmp_rectangle.str().c_str(); + break; + } + case U_WMR_SAVEDC: + { + dbg_str << "<!-- U_WMR_SAVEDC -->\n"; + + if (d->level < WMF_MAX_DC) { + d->dc[d->level + 1] = d->dc[d->level]; + if(d->dc[d->level].font_name){ + d->dc[d->level + 1].font_name = strdup(d->dc[d->level].font_name); // or memory access problems because font name pointer duplicated + } + d->level = d->level + 1; + } + break; + } + case U_WMR_SETPIXEL: dbg_str << "<!-- U_WMR_SETPIXEL -->\n"; break; + case U_WMR_OFFSETCLIPRGN: + { + dbg_str << "<!-- U_EMR_OFFSETCLIPRGN -->\n"; + U_POINT16 off; + nSize = U_WMROFFSETCLIPRGN_get(contents,&off); + if (d->dc[d->level].clip_id) { // can only offset an existing clipping path + unsigned int real_idx = d->dc[d->level].clip_id - 1; + Geom::PathVector tmp_vect = sp_svg_read_pathv(d->clips.strings[real_idx]); + double ox = pix_to_x_point(d, off.x, off.y) - pix_to_x_point(d, 0, 0); // take into account all active transforms + double oy = pix_to_y_point(d, off.x, off.y) - pix_to_y_point(d, 0, 0); + Geom::Affine tf = Geom::Translate(ox,oy); + tmp_vect *= tf; + add_clips(d, sp_svg_write_path(tmp_vect).c_str(), U_RGN_COPY); + } + break; + } + // U_WMR_TEXTOUT should be here, but has been moved down to merge with U_WMR_EXTTEXTOUT + case U_WMR_BITBLT: + { + dbg_str << "<!-- U_WMR_BITBLT -->\n"; + nSize = U_WMRBITBLT_get(contents,&Dst,&cwh,&Src,&dwRop3,&Bm16,&px); + if(!px){ + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cwh.x; + int32_t dh = cwh.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { /* Not done yet, Bm16 image present */ } + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cwh.x); + double dh = pix_to_abs_size( d, cwh.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_bm16_to_image(d,Bm16,px,dx,dy,dw,dh,sx,sy,sw,sh); + break; + } + case U_WMR_STRETCHBLT: + { + dbg_str << "<!-- U_WMR_STRETCHBLT -->\n"; + nSize = U_WMRSTRETCHBLT_get(contents,&Dst,&cDst,&Src,&cSrc,&dwRop3,&Bm16,&px); + if(!px){ + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cDst.x; + int32_t dh = cDst.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { /* Not done yet, Bm16 image present */ } + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + if(sx<0)sx=0; + if(sy<0)sy=0; + common_bm16_to_image(d,Bm16,px,dx,dy,dw,dh,sx,sy,sw,sh); + break; + } + case U_WMR_POLYGON: + case U_WMR_POLYLINE: + { + dbg_str << "<!-- U_WMR_POLYGON/POLYLINE -->\n"; + nSize = U_WMRPOLYGON_get(contents, &cPts, &points); + uint32_t i; + + if (cPts < 2)break; + + d->mask |= wmr_mask; + memcpy(&pt16,points,U_SIZE_POINT16); points += U_SIZE_POINT16; + + tmp_str << "\n\tM " << pix_to_xy( d, pt16.x, pt16.y) << " "; + + for (i=1; i<cPts; i++) { + memcpy(&pt16,points,U_SIZE_POINT16); points+=U_SIZE_POINT16; + tmp_str << "\n\tL " << pix_to_xy( d, pt16.x, pt16.y) << " "; + } + + tmp_path << tmp_str.str().c_str(); + if(iType==U_WMR_POLYGON){ tmp_path << " z"; } + + break; + } + case U_WMR_ESCAPE: // only 3 types of escape are implemented + { + dbg_str << "<!-- U_WMR_ESCAPE -->\n"; + uint16_t Escape, elen; + nSize = U_WMRESCAPE_get(contents, &Escape, &elen, &text); + if(elen>=4){ + uint32_t utmp4; + memcpy(&utmp4, text ,4); + if(Escape == U_MFE_SETLINECAP){ + switch (utmp4 & U_PS_ENDCAP_MASK) { + case U_PS_ENDCAP_ROUND: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_ROUND; break; } + case U_PS_ENDCAP_SQUARE: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; break; } + case U_PS_ENDCAP_FLAT: + default: { d->dc[d->level].style.stroke_linecap.computed = SP_STROKE_LINECAP_BUTT; break; } + } + } + else if(Escape == U_MFE_SETLINEJOIN){ + switch (utmp4 & U_PS_JOIN_MASK) { + case U_PS_JOIN_BEVEL: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_BEVEL; break; } + case U_PS_JOIN_MITER: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; break; } + case U_PS_JOIN_ROUND: + default: { d->dc[d->level].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_ROUND; break; } + } + } + else if(Escape == U_MFE_SETMITERLIMIT){ + //The function takes a float but uses a 32 bit int in the record. + float miterlimit = utmp4; + d->dc[d->level].style.stroke_miterlimit.value = miterlimit; //ratio, not a pt size + if (d->dc[d->level].style.stroke_miterlimit.value < 2) + d->dc[d->level].style.stroke_miterlimit.value = 2.0; + } + } + break; + } + case U_WMR_RESTOREDC: + { + dbg_str << "<!-- U_WMR_RESTOREDC -->\n"; + + int16_t DC; + nSize = U_WMRRESTOREDC_get(contents, &DC); + int old_level = d->level; + if (DC >= 0) { + if (DC < d->level) + d->level = DC; + } + else { + if (d->level + DC >= 0) + d->level = d->level + DC; + } + while (old_level > d->level) { + if (!d->dc[old_level].style.stroke_dasharray.values.empty() && + (old_level == 0 || (old_level > 0 && d->dc[old_level].style.stroke_dasharray != + d->dc[old_level - 1].style.stroke_dasharray))) { + d->dc[old_level].style.stroke_dasharray.values.clear(); + } + if(d->dc[old_level].font_name){ + free(d->dc[old_level].font_name); // else memory leak + d->dc[old_level].font_name = nullptr; + } + old_level--; + } + break; + } + case U_WMR_FILLREGION: dbg_str << "<!-- U_WMR_FILLREGION -->\n"; break; + case U_WMR_FRAMEREGION: dbg_str << "<!-- U_WMR_FRAMEREGION -->\n"; break; + case U_WMR_INVERTREGION: dbg_str << "<!-- U_WMR_INVERTREGION -->\n"; break; + case U_WMR_PAINTREGION: dbg_str << "<!-- U_WMR_PAINTREGION -->\n"; break; + case U_WMR_SELECTCLIPREGION: + { + dbg_str << "<!-- U_WMR_EXTSELECTCLIPRGN -->\n"; + nSize = U_WMRSELECTCLIPREGION_get(contents, &utmp16); + if (utmp16 == U_RGN_COPY) + clipset = false; + break; + } + case U_WMR_SELECTOBJECT: + { + dbg_str << "<!-- U_WMR_SELECTOBJECT -->\n"; + + nSize = U_WMRSELECTOBJECT_get(contents, &utmp16); + unsigned int index = utmp16; + + // WMF has no stock objects + if ( /*index >= 0 &&*/ index < (unsigned int) d->n_obj) { + switch (d->wmf_obj[index].type) + { + case U_WMR_CREATEPENINDIRECT: + select_pen(d, index); + break; + case U_WMR_CREATEBRUSHINDIRECT: + case U_WMR_DIBCREATEPATTERNBRUSH: + case U_WMR_CREATEPATTERNBRUSH: // <- this one did not display properly on XP, DIBCREATEPATTERNBRUSH works + select_brush(d, index); + break; + case U_WMR_CREATEFONTINDIRECT: + select_font(d, index); + break; + case U_WMR_CREATEPALETTE: + case U_WMR_CREATEBITMAPINDIRECT: + case U_WMR_CREATEBITMAP: + case U_WMR_CREATEREGION: + /* these do not do anything, but their objects must be kept in the count */ + break; + } + } + break; + } + case U_WMR_SETTEXTALIGN: + { + dbg_str << "<!-- U_WMR_SETTEXTALIGN -->\n"; + nSize = U_WMRSETTEXTALIGN_get(contents, &(d->dc[d->level].textAlign)); + break; + } + case U_WMR_DRAWTEXT: dbg_str << "<!-- U_WMR_DRAWTEXT -->\n"; break; + case U_WMR_CHORD: + { + dbg_str << "<!-- U_WMR_CHORD -->\n"; + U_POINT16 ArcStart, ArcEnd; + nSize = U_WMRCHORD_get(contents, &ArcStart, &ArcEnd, &rc); + U_PAIRF center,start,end,size; + int f1; + int f2 = (d->arcdir == U_AD_COUNTERCLOCKWISE ? 0 : 1); + if(!wmr_arc_points(rc, ArcStart, ArcEnd, &f1, f2, ¢er, &start, &end, &size)){ + tmp_path << "\n\tM " << pix_to_xy(d, start.x, start.y); + tmp_path << " A " << pix_to_abs_size(d, size.x)/2.0 << "," << pix_to_abs_size(d, size.y)/2.0 ; + tmp_path << " "; + tmp_path << 180.0 * current_rotation(d)/M_PI; + tmp_path << " "; + tmp_path << " " << f1 << "," << f2 << " "; + tmp_path << pix_to_xy(d, end.x, end.y) << " \n"; + tmp_path << " z "; + d->mask |= wmr_mask; + } + else { + dbg_str << "<!-- CHORD record is invalid -->\n"; + } + break; + } + case U_WMR_SETMAPPERFLAGS: dbg_str << "<!-- U_WMR_SETMAPPERFLAGS -->\n"; break; + case U_WMR_TEXTOUT: + case U_WMR_EXTTEXTOUT: + { + if(iType == U_WMR_TEXTOUT){ + dbg_str << "<!-- U_WMR_TEXTOUT -->\n"; + nSize = U_WMRTEXTOUT_get(contents, &Dst, &tlen, &text); + Opts=0; + } + else { + dbg_str << "<!-- U_WMR_EXTTEXTOUT -->\n"; + nSize = U_WMREXTTEXTOUT_get(contents, &Dst, &tlen, &Opts, &text, &dx, &rc ); + } + uint32_t fOptions = Opts; + + double x1,y1; + int cChars; + x1 = Dst.x; + y1 = Dst.y; + cChars = tlen; + + if (d->dc[d->level].textAlign & U_TA_UPDATECP) { + x1 = d->dc[d->level].cur.x; + y1 = d->dc[d->level].cur.y; + } + + double x = pix_to_x_point(d, x1, y1); + double y = pix_to_y_point(d, x1, y1); + + /* Rotation issues are handled entirely in libTERE now */ + + uint32_t *dup_wt = nullptr; + + dup_wt = U_Latin1ToUtf32le(text, cChars, nullptr); + if(!dup_wt)dup_wt = unknown_chars(cChars); + + msdepua(dup_wt); //convert everything in Microsoft's private use area. For Symbol, Wingdings, Dingbats + + if(NonToUnicode(dup_wt, d->dc[d->level].font_name)){ + free(d->dc[d->level].font_name); + d->dc[d->level].font_name = strdup("Times New Roman"); + } + + char *ansi_text; + ansi_text = (char *) U_Utf32leToUtf8((uint32_t *)dup_wt, 0, nullptr); + free(dup_wt); + // Empty text or starts with an invalid escape/control sequence, which is bogus text. Throw it out before g_markup_escape_text can make things worse + if(*((uint8_t *)ansi_text) <= 0x1F){ + free(ansi_text); + ansi_text=nullptr; + } + + if (ansi_text) { + + SVGOStringStream ts; + + gchar *escaped_text = g_markup_escape_text(ansi_text, -1); + + tsp.x = x*0.8; // TERE expects sizes in points + tsp.y = y*0.8; + tsp.color.Red = d->dc[d->level].textColor.Red; + tsp.color.Green = d->dc[d->level].textColor.Green; + tsp.color.Blue = d->dc[d->level].textColor.Blue; + tsp.color.Reserved = 0; + switch(d->dc[d->level].style.font_style.value){ + case SP_CSS_FONT_STYLE_OBLIQUE: + tsp.italics = FC_SLANT_OBLIQUE; break; + case SP_CSS_FONT_STYLE_ITALIC: + tsp.italics = FC_SLANT_ITALIC; break; + default: + case SP_CSS_FONT_STYLE_NORMAL: + tsp.italics = FC_SLANT_ROMAN; break; + } + switch(d->dc[d->level].style.font_weight.value){ + case SP_CSS_FONT_WEIGHT_100: tsp.weight = FC_WEIGHT_THIN ; break; + case SP_CSS_FONT_WEIGHT_200: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_300: tsp.weight = FC_WEIGHT_LIGHT ; break; + case SP_CSS_FONT_WEIGHT_400: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_500: tsp.weight = FC_WEIGHT_MEDIUM ; break; + case SP_CSS_FONT_WEIGHT_600: tsp.weight = FC_WEIGHT_SEMIBOLD ; break; + case SP_CSS_FONT_WEIGHT_700: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_800: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + case SP_CSS_FONT_WEIGHT_900: tsp.weight = FC_WEIGHT_HEAVY ; break; + case SP_CSS_FONT_WEIGHT_NORMAL: tsp.weight = FC_WEIGHT_NORMAL ; break; + case SP_CSS_FONT_WEIGHT_BOLD: tsp.weight = FC_WEIGHT_BOLD ; break; + case SP_CSS_FONT_WEIGHT_LIGHTER: tsp.weight = FC_WEIGHT_EXTRALIGHT ; break; + case SP_CSS_FONT_WEIGHT_BOLDER: tsp.weight = FC_WEIGHT_EXTRABOLD ; break; + default: tsp.weight = FC_WEIGHT_NORMAL ; break; + } + // WMF only supports two types of text decoration + tsp.decoration = TXTDECOR_NONE; + if(d->dc[d->level].style.text_decoration_line.underline){ tsp.decoration |= TXTDECOR_UNDER; } + if(d->dc[d->level].style.text_decoration_line.line_through){ tsp.decoration |= TXTDECOR_STRIKE;} + + // WMF textalignment is a bit strange: 0x6 is center, 0x2 is right, 0x0 is left, the value 0x4 is also drawn left + tsp.taln = ((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_CENTER) ? ALICENTER : + (((d->dc[d->level].textAlign & U_TA_CENTER) == U_TA_LEFT) ? ALILEFT : + ALIRIGHT); + tsp.taln |= ((d->dc[d->level].textAlign & U_TA_BASEBIT) ? ALIBASE : + ((d->dc[d->level].textAlign & U_TA_BOTTOM) ? ALIBOT : + ALITOP)); + + // language direction can be encoded two ways, U_TA_RTLREADING is preferred + if( (fOptions & U_ETO_RTLREADING) || (d->dc[d->level].textAlign & U_TA_RTLREADING) ){ tsp.ldir = LDIR_RL; } + else{ tsp.ldir = LDIR_LR; } + + tsp.condensed = FC_WIDTH_NORMAL; // Not implemented well in libTERE (yet) + tsp.ori = d->dc[d->level].style.baseline_shift.value; // For now orientation is always the same as escapement + // There is no world transform, so ori need not be further rotated + tsp.string = (uint8_t *) U_strdup(escaped_text); // this will be free'd much later at a trinfo_clear(). + tsp.fs = d->dc[d->level].style.font_size.computed * 0.8; // Font size in points + char *fontspec = TR_construct_fontspec(&tsp, d->dc[d->level].font_name); + tsp.fi_idx = ftinfo_load_fontname(d->tri->fti,fontspec); + free(fontspec); + // when font name includes narrow it may not be set to "condensed". Narrow fonts do not work well anyway though + // as the metrics from fontconfig may not match, or the font may not be present. + if(0<= TR_findcasesub(d->dc[d->level].font_name, (char *) "Narrow")){ tsp.co=1; } + else { tsp.co=0; } + + int status; + + status = trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ori is actually escapement + if(status==-1){ // change of escapement, emit what we have and reset + TR_layout_analyze(d->tri); + if (d->dc[d->level].clip_id){ + SVGOStringStream tmp_clip; + tmp_clip << "\n<g\n\tclip-path=\"url(#clipWmfPath" << d->dc[d->level].clip_id << ")\"\n>"; + d->outsvg += tmp_clip.str().c_str(); + } + TR_layout_2_svg(d->tri); + ts << d->tri->out; + d->outsvg += ts.str().c_str(); + d->tri = trinfo_clear(d->tri); + (void) trinfo_load_textrec(d->tri, &tsp, tsp.ori,TR_EMFBOT); // ignore return status, it must work + if (d->dc[d->level].clip_id){ + d->outsvg += "\n</g>\n"; + } + } + + g_free(escaped_text); + free(ansi_text); + } + + break; + } + case U_WMR_SETDIBTODEV: dbg_str << "<!-- U_WMR_EXTTEXTOUT -->\n"; break; + case U_WMR_SELECTPALETTE: dbg_str << "<!-- U_WMR_SELECTPALETTE -->\n"; break; + case U_WMR_REALIZEPALETTE: dbg_str << "<!-- U_WMR_REALIZEPALETTE -->\n"; break; + case U_WMR_ANIMATEPALETTE: dbg_str << "<!-- U_WMR_ANIMATEPALETTE -->\n"; break; + case U_WMR_SETPALENTRIES: dbg_str << "<!-- U_WMR_SETPALENTRIES -->\n"; break; + case U_WMR_POLYPOLYGON: + { + dbg_str << "<!-- U_WMR_POLYPOLYGON16 -->\n"; + uint16_t nPolys; + const uint16_t *aPolyCounts; + const char *Points; + int cpts; /* total number of points in Points*/ + nSize = U_WMRPOLYPOLYGON_get(contents, &nPolys, &aPolyCounts, &Points); + int n, i, j; + + d->mask |= wmr_mask; + + U_POINT16 apt; + for (n=cpts=0; n < nPolys; n++) { cpts += aPolyCounts[n]; } + i = 0; // offset in BYTES + cpts *= U_SIZE_POINT16; // limit for offset i, in BYTES + + for (n=0; n < nPolys && i<cpts; n++) { + SVGOStringStream poly_path; + + memcpy(&apt, Points + i, U_SIZE_POINT16); // points may not be aligned, copy them this way + + poly_path << "\n\tM " << pix_to_xy( d, apt.x, apt.y) << " "; + i += U_SIZE_POINT16; + + for (j=1; j < aPolyCounts[n] && i < cpts; j++) { + memcpy(&apt, Points + i, U_SIZE_POINT16); // points may not be aligned, copy them this way + poly_path << "\n\tL " << pix_to_xy( d, apt.x, apt.y) << " "; + i += U_SIZE_POINT16; + } + + tmp_str << poly_path.str().c_str(); + tmp_str << " z"; + tmp_str << " \n"; + } + + tmp_path << tmp_str.str().c_str(); + + break; + } + case U_WMR_RESIZEPALETTE: dbg_str << "<!-- U_WMR_RESIZEPALETTE -->\n"; break; + case U_WMR_3A: + case U_WMR_3B: + case U_WMR_3C: + case U_WMR_3D: + case U_WMR_3E: + case U_WMR_3F: + { + dbg_str << "<!-- U_WMR_3A..3F -->\n"; + break; + } + case U_WMR_DIBBITBLT: + { + dbg_str << "<!-- U_WMR_DIBBITBLT -->\n"; + nSize = U_WMRDIBBITBLT_get(contents, &Dst, &cwh, &Src, &dwRop3, &dib); + + // Treat all nonImage bitblts as a rectangular write. Definitely not correct, but at + // least it leaves objects where the operations should have been. + if (!dib) { + // should be an application of a DIBPATTERNBRUSHPT, use a solid color instead + + if(dwRop3 == U_NOOP)break; /* GDI applications apparently often end with this as a sort of flush(), nothing should be drawn */ + int32_t dx = Dst.x; + int32_t dy = Dst.y; + int32_t dw = cwh.x; + int32_t dh = cwh.y; + SVGOStringStream tmp_rectangle; + tmp_rectangle << "\n\tM " << pix_to_xy( d, dx, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx + dw, dy + dh ) << " "; + tmp_rectangle << "\n\tL " << pix_to_xy( d, dx, dy + dh ) << " "; + tmp_rectangle << "\n\tz"; + + d->mask |= wmr_mask; + d->dwRop3 = dwRop3; // we will try to approximate SOME of these + d->mask |= U_DRAW_CLOSED; // Bitblit is not really open or closed, but we need it to fill, and this is the flag for that + + tmp_path << tmp_rectangle.str().c_str(); + } + else { + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = 0; // extract all of the image + int sh = 0; + if(sx<0)sx=0; + if(sy<0)sy=0; + // usageSrc not defined, implicitly it must be U_DIB_RGB_COLORS + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh,U_DIB_RGB_COLORS); + } + break; + } + case U_WMR_DIBSTRETCHBLT: + { + dbg_str << "<!-- U_WMR_DIBSTRETCHBLT -->\n"; + nSize = U_WMRDIBSTRETCHBLT_get(contents, &Dst, &cDst, &Src, &cSrc, &dwRop3, &dib); + // Always grab image, ignore modes. + if (dib) { + double dx = pix_to_x_point( d, Dst.x, Dst.y); + double dy = pix_to_y_point( d, Dst.x, Dst.y); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + //source position within the bitmap, in pixels + int sx = Src.x; + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + // usageSrc not defined, implicitly it must be U_DIB_RGB_COLORS + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh, U_DIB_RGB_COLORS); + } + break; + } + case U_WMR_DIBCREATEPATTERNBRUSH: + { + dbg_str << "<!-- U_WMR_DIBCREATEPATTERNBRUSH -->\n"; + insert_object(d, U_WMR_DIBCREATEPATTERNBRUSH, contents); + break; + } + case U_WMR_STRETCHDIB: + { + dbg_str << "<!-- U_WMR_STRETCHDIB -->\n"; + nSize = U_WMRSTRETCHDIB_get(contents, &Dst, &cDst, &Src, &cSrc, &cUsage, &dwRop3, &dib); + double dx = pix_to_x_point( d, Dst.x, Dst.y ); + double dy = pix_to_y_point( d, Dst.x, Dst.y ); + double dw = pix_to_abs_size( d, cDst.x); + double dh = pix_to_abs_size( d, cDst.y); + int sx = Src.x; //source position within the bitmap, in pixels + int sy = Src.y; + int sw = cSrc.x; // extract the specified amount of the image + int sh = cSrc.y; + uint32_t iUsageSrc; + iUsageSrc = cUsage; + common_dib_to_image(d,dib,dx,dy,dw,dh,sx,sy,sw,sh,iUsageSrc); + + break; + } + case U_WMR_44: + case U_WMR_45: + case U_WMR_46: + case U_WMR_47: + { + dbg_str << "<!-- U_WMR_44..47 -->\n"; + break; + } + case U_WMR_EXTFLOODFILL: dbg_str << "<!-- U_WMR_EXTFLOODFILL -->\n"; break; + case U_WMR_49: + case U_WMR_4A: + case U_WMR_4B: + case U_WMR_4C: + case U_WMR_4D: + case U_WMR_4E: + case U_WMR_4F: + case U_WMR_50: + case U_WMR_51: + case U_WMR_52: + case U_WMR_53: + case U_WMR_54: + case U_WMR_55: + case U_WMR_56: + case U_WMR_57: + case U_WMR_58: + case U_WMR_59: + case U_WMR_5A: + case U_WMR_5B: + case U_WMR_5C: + case U_WMR_5D: + case U_WMR_5E: + case U_WMR_5F: + case U_WMR_60: + case U_WMR_61: + case U_WMR_62: + case U_WMR_63: + case U_WMR_64: + case U_WMR_65: + case U_WMR_66: + case U_WMR_67: + case U_WMR_68: + case U_WMR_69: + case U_WMR_6A: + case U_WMR_6B: + case U_WMR_6C: + case U_WMR_6D: + case U_WMR_6E: + case U_WMR_6F: + case U_WMR_70: + case U_WMR_71: + case U_WMR_72: + case U_WMR_73: + case U_WMR_74: + case U_WMR_75: + case U_WMR_76: + case U_WMR_77: + case U_WMR_78: + case U_WMR_79: + case U_WMR_7A: + case U_WMR_7B: + case U_WMR_7C: + case U_WMR_7D: + case U_WMR_7E: + case U_WMR_7F: + case U_WMR_80: + case U_WMR_81: + case U_WMR_82: + case U_WMR_83: + case U_WMR_84: + case U_WMR_85: + case U_WMR_86: + case U_WMR_87: + case U_WMR_88: + case U_WMR_89: + case U_WMR_8A: + case U_WMR_8B: + case U_WMR_8C: + case U_WMR_8D: + case U_WMR_8E: + case U_WMR_8F: + case U_WMR_90: + case U_WMR_91: + case U_WMR_92: + case U_WMR_93: + case U_WMR_94: + case U_WMR_95: + case U_WMR_96: + case U_WMR_97: + case U_WMR_98: + case U_WMR_99: + case U_WMR_9A: + case U_WMR_9B: + case U_WMR_9C: + case U_WMR_9D: + case U_WMR_9E: + case U_WMR_9F: + case U_WMR_A0: + case U_WMR_A1: + case U_WMR_A2: + case U_WMR_A3: + case U_WMR_A4: + case U_WMR_A5: + case U_WMR_A6: + case U_WMR_A7: + case U_WMR_A8: + case U_WMR_A9: + case U_WMR_AA: + case U_WMR_AB: + case U_WMR_AC: + case U_WMR_AD: + case U_WMR_AE: + case U_WMR_AF: + case U_WMR_B0: + case U_WMR_B1: + case U_WMR_B2: + case U_WMR_B3: + case U_WMR_B4: + case U_WMR_B5: + case U_WMR_B6: + case U_WMR_B7: + case U_WMR_B8: + case U_WMR_B9: + case U_WMR_BA: + case U_WMR_BB: + case U_WMR_BC: + case U_WMR_BD: + case U_WMR_BE: + case U_WMR_BF: + case U_WMR_C0: + case U_WMR_C1: + case U_WMR_C2: + case U_WMR_C3: + case U_WMR_C4: + case U_WMR_C5: + case U_WMR_C6: + case U_WMR_C7: + case U_WMR_C8: + case U_WMR_C9: + case U_WMR_CA: + case U_WMR_CB: + case U_WMR_CC: + case U_WMR_CD: + case U_WMR_CE: + case U_WMR_CF: + case U_WMR_D0: + case U_WMR_D1: + case U_WMR_D2: + case U_WMR_D3: + case U_WMR_D4: + case U_WMR_D5: + case U_WMR_D6: + case U_WMR_D7: + case U_WMR_D8: + case U_WMR_D9: + case U_WMR_DA: + case U_WMR_DB: + case U_WMR_DC: + case U_WMR_DD: + case U_WMR_DE: + case U_WMR_DF: + case U_WMR_E0: + case U_WMR_E1: + case U_WMR_E2: + case U_WMR_E3: + case U_WMR_E4: + case U_WMR_E5: + case U_WMR_E6: + case U_WMR_E7: + case U_WMR_E8: + case U_WMR_E9: + case U_WMR_EA: + case U_WMR_EB: + case U_WMR_EC: + case U_WMR_ED: + case U_WMR_EE: + case U_WMR_EF: + { + dbg_str << "<!-- U_WMR_EXTFLOODFILL..EF -->\n"; + break; + } + case U_WMR_DELETEOBJECT: + { + dbg_str << "<!-- U_WMR_DELETEOBJECT -->\n"; + nSize = U_WMRDELETEOBJECT_get(contents, &utmp16); + delete_object(d, utmp16); + break; + } + case U_WMR_F1: + case U_WMR_F2: + case U_WMR_F3: + case U_WMR_F4: + case U_WMR_F5: + case U_WMR_F6: + { + dbg_str << "<!-- F1..F6 -->\n"; + break; + } + case U_WMR_CREATEPALETTE: + { + dbg_str << "<!-- U_WMR_CREATEPALETTE -->\n"; + insert_object(d, U_WMR_CREATEPALETTE, contents); + break; + } + case U_WMR_F8: dbg_str << "<!-- F8 -->\n"; break; + case U_WMR_CREATEPATTERNBRUSH: + { + dbg_str << "<!-- U_WMR_CREATEPATTERNBRUSH -->\n"; + insert_object(d, U_WMR_CREATEPATTERNBRUSH, contents); + break; + } + case U_WMR_CREATEPENINDIRECT: + { + dbg_str << "<!-- U_WMR_EXTCREATEPEN -->\n"; + insert_object(d, U_WMR_CREATEPENINDIRECT, contents); + break; + } + case U_WMR_CREATEFONTINDIRECT: + { + dbg_str << "<!-- U_WMR_CREATEFONTINDIRECT -->\n"; + insert_object(d, U_WMR_CREATEFONTINDIRECT, contents); + break; + } + case U_WMR_CREATEBRUSHINDIRECT: + { + dbg_str << "<!-- U_WMR_CREATEBRUSHINDIRECT -->\n"; + insert_object(d, U_WMR_CREATEBRUSHINDIRECT, contents); + break; + } + case U_WMR_CREATEBITMAPINDIRECT: + { + dbg_str << "<!-- U_WMR_CREATEBITMAPINDIRECT -->\n"; + insert_object(d, U_WMR_CREATEBITMAPINDIRECT, contents); + break; + } + case U_WMR_CREATEBITMAP: + { + dbg_str << "<!-- U_WMR_CREATEBITMAP -->\n"; + insert_object(d, U_WMR_CREATEBITMAP, contents); + break; + } + case U_WMR_CREATEREGION: + { + dbg_str << "<!-- U_WMR_CREATEREGION -->\n"; + insert_object(d, U_WMR_CREATEREGION, contents); + break; + } + default: + dbg_str << "<!-- U_WMR_??? -->\n"; + break; + } //end of switch +// At run time define environment variable INKSCAPE_DBG_WMF to include string COMMENT. +// Users may employ this to to place a comment for each processed WMR record in the SVG + if(wDbgComment){ + d->outsvg += dbg_str.str().c_str(); + } + d->path += tmp_path.str().c_str(); + if(!nSize){ // There was some problem with the processing of this record, it is not safe to continue + file_status = 0; + break; + } + + } //end of while on OK +// At run time define environment variable INKSCAPE_DBG_WMF to include string FINAL +// Users may employ this to to show the final SVG derived from the WMF + if(wDbgFinal){ + std::cout << d->outsvg << std::endl; + } + (void) U_wmr_properties(U_WMR_INVALID); // force the release of the lookup table memory, returned value is irrelevant + + return(file_status); +} + +void Wmf::free_wmf_strings(WMF_STRINGS name){ + if(name.count){ + for(int i=0; i< name.count; i++){ free(name.strings[i]); } + free(name.strings); + } + name.count = 0; + name.size = 0; +} + +SPDocument * +Wmf::open( Inkscape::Extension::Input * /*mod*/, const gchar *uri ) +{ + + if (uri == nullptr) { + return nullptr; + } + + // ensure usage of dot as decimal separator in scanf/printf functions (indepentendly of current locale) + char *oldlocale = g_strdup(setlocale(LC_NUMERIC, nullptr)); + setlocale(LC_NUMERIC, "C"); + + WMF_CALLBACK_DATA d; + + d.n_obj = 0; //these might not be set otherwise if the input file is corrupt + d.wmf_obj=nullptr; + + // Default font, WMF spec says device can pick whatever it wants. + // WMF files that do not specify a font are unlikely to look very good! + d.dc[0].style.font_size.computed = 16.0; + d.dc[0].style.font_weight.value = SP_CSS_FONT_WEIGHT_400; + d.dc[0].style.font_style.value = SP_CSS_FONT_STYLE_NORMAL; + d.dc[0].style.text_decoration_line.underline = false; + d.dc[0].style.text_decoration_line.line_through = false; + d.dc[0].style.baseline_shift.value = 0; + + // Default pen, WMF files that do not specify a pen are unlikely to look very good! + d.dc[0].style.stroke_dasharray.set = false; + d.dc[0].style.stroke_linecap.computed = SP_STROKE_LINECAP_SQUARE; // U_PS_ENDCAP_SQUARE; + d.dc[0].style.stroke_linejoin.computed = SP_STROKE_LINEJOIN_MITER; // U_PS_JOIN_MITER; + d.dc[0].style.stroke_width.value = 1.0; // will be reset to something reasonable once WMF drawing size is known + d.dc[0].style.stroke.value.color.set( 0, 0, 0 ); + d.dc[0].stroke_set = true; + + // Default brush is none - no fill. WMF files that do not specify a brush are unlikely to look very good! + d.dc[0].fill_set = false; + + d.dc[0].font_name = strdup("Arial"); // Default font, set only on lowest level, it copies up from there WMF spec says device can pick whatever it wants + + // set up the size default for patterns in defs. This might not be referenced if there are no patterns defined in the drawing. + + d.defs += "\n"; + d.defs += " <pattern id=\"WMFhbasepattern\" \n"; + d.defs += " patternUnits=\"userSpaceOnUse\"\n"; + d.defs += " width=\"6\" \n"; + d.defs += " height=\"6\" \n"; + d.defs += " x=\"0\" \n"; + d.defs += " y=\"0\"> \n"; + d.defs += " </pattern> \n"; + + + size_t length; + char *contents; + if(wmf_readdata(uri, &contents, &length))return(nullptr); + + // set up the text reassembly system + if(!(d.tri = trinfo_init(nullptr)))return(nullptr); + (void) trinfo_load_ft_opts(d.tri, 1, + FT_LOAD_NO_SCALE | FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP, + FT_KERNING_UNSCALED); + + int good = myMetaFileProc(contents,length, &d); + free(contents); + +// std::cout << "SVG Output: " << std::endl << d.outsvg << std::endl; + + SPDocument *doc = nullptr; + if (good) { + doc = SPDocument::createNewDocFromMem(d.outsvg.c_str(), strlen(d.outsvg.c_str()), TRUE); + } + + free_wmf_strings(d.hatches); + free_wmf_strings(d.images); + free_wmf_strings(d.clips); + + if (d.wmf_obj) { + int i; + for (i=0; i<d.n_obj; i++) + delete_object(&d, i); + delete[] d.wmf_obj; + } + + d.dc[0].style.stroke_dasharray.values.clear(); + + for(int i=0; i<=WMF_MAX_DC; i++){ + if(d.dc[i].font_name)free(d.dc[i].font_name); + } + + d.tri = trinfo_release_except_FC(d.tri); + + // in earlier versions no viewbox was generated and a call to setViewBoxIfMissing() was needed here. + + // restore decimal separator used in scanf/printf functions to initial value + setlocale(LC_NUMERIC, oldlocale); + g_free(oldlocale); + + return doc; +} + + +void +Wmf::init () +{ + // clang-format off + /* WMF in */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("WMF Input") "</name>\n" + "<id>org.inkscape.input.wmf</id>\n" + "<input>\n" + "<extension>.wmf</extension>\n" + "<mimetype>image/x-wmf</mimetype>\n" + "<filetypename>" N_("Windows Metafiles (*.wmf)") "</filetypename>\n" + "<filetypetooltip>" N_("Windows Metafiles") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new Wmf()); + + /* WMF out */ + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("WMF Output") "</name>\n" + "<id>org.inkscape.output.wmf</id>\n" + "<param name=\"textToPath\" gui-text=\"" N_("Convert texts to paths") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToSymbol\" gui-text=\"" N_("Map Unicode to Symbol font") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToWingdings\" gui-text=\"" N_("Map Unicode to Wingdings") "\" type=\"bool\">true</param>\n" + "<param name=\"TnrToZapfDingbats\" gui-text=\"" N_("Map Unicode to Zapf Dingbats") "\" type=\"bool\">true</param>\n" + "<param name=\"UsePUA\" gui-text=\"" N_("Use MS Unicode PUA (0xF020-0xF0FF) for converted characters") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTCharPos\" gui-text=\"" N_("Compensate for PPT font bug") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTDashLine\" gui-text=\"" N_("Convert dashed/dotted lines to single lines") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTGrad2Polys\" gui-text=\"" N_("Convert gradients to colored polygon series") "\" type=\"bool\">false</param>\n" + "<param name=\"FixPPTPatternAsHatch\" gui-text=\"" N_("Map all fill patterns to standard WMF hatches") "\" type=\"bool\">false</param>\n" + "<output>\n" + "<extension>.wmf</extension>\n" + "<mimetype>image/x-wmf</mimetype>\n" + "<filetypename>" N_("Windows Metafile (*.wmf)") "</filetypename>\n" + "<filetypetooltip>" N_("Windows Metafile") "</filetypetooltip>\n" + "</output>\n" + "</inkscape-extension>", new Wmf()); + // clang-format on + + return; +} + + +} } } /* namespace Inkscape, Extension, Implementation */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-inout.h b/src/extension/internal/wmf-inout.h new file mode 100644 index 0000000..129ab18 --- /dev/null +++ b/src/extension/internal/wmf-inout.h @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile Input/Output + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_EXTENSION_INTERNAL_WMF_H +#define SEEN_EXTENSION_INTERNAL_WMF_H + +#include <3rdparty/libuemf/uwmf.h> +#include "extension/internal/metafile-inout.h" // picks up PNG +#include "extension/implementation/implementation.h" +#include "style.h" +#include "text_reassemble.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define DIRTY_NONE 0x00 +#define DIRTY_TEXT 0x01 +#define DIRTY_FILL 0x02 +#define DIRTY_STROKE 0x04 // not used currently + +struct WMF_OBJECT { + int type = 0; + int level = 0; + char *record = nullptr; +}; +using PWMF_OBJECT = WMF_OBJECT *; + +struct WMF_STRINGS { + int size = 0; // number of slots allocated in strings + int count = 0; // number of slots used in strings + char **strings = nullptr; // place to store strings +}; +using PWMF_STRINGS = WMF_STRINGS *; + +struct WMF_DEVICE_CONTEXT { + WMF_DEVICE_CONTEXT() : + // SPStyle: class with constructor + font_name(nullptr), + clip_id(0), + stroke_set(false), stroke_mode(0), stroke_idx(0), stroke_recidx(0), + fill_set(false), fill_mode(0), fill_idx(0), fill_recidx(0), + dirty(0), + active_pen(-1), active_brush(-1), active_font(-1), // -1 when the default is used + // sizeWnd, sizeView, winorg, vieworg, + ScaleInX(0), ScaleInY(0), + ScaleOutX(0), ScaleOutY(0), + bkMode(U_TRANSPARENT), + // bkColor, textColor + textAlign(0) + // worldTransform, cur + { + sizeWnd = point16_set( 0.0, 0.0 ); + sizeView = point16_set( 0.0, 0.0 ); + winorg = point16_set( 0.0, 0.0 ); + vieworg = point16_set( 0.0, 0.0 ); + bkColor = U_RGB(255, 255, 255); // default foreground color (white) + textColor = U_RGB(0, 0, 0); // default foreground color (black) + cur = point16_set( 0.0, 0.0 ); + }; + SPStyle style; + char *font_name; + int clip_id; // 0 if none, else 1 + index into clips + bool stroke_set; + int stroke_mode; // enumeration from drawmode, not used if fill_set is not True + int stroke_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int stroke_recidx;// record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + bool fill_set; + int fill_mode; // enumeration from drawmode, not used if fill_set is not True + int fill_idx; // used with DRAW_PATTERN and DRAW_IMAGE to return the appropriate fill + int fill_recidx; // record used to regenerate hatch when it needs to be redone due to bkmode, textmode, etc. change + int dirty; // holds the dirty bits for text, stroke, fill + int active_pen; // used when the active object is deleted to set the default values, -1 is none active + int active_brush; // ditto + int active_font; // ditto. also used to hold object number in case font needs to be remade due to textcolor change. + U_POINT16 sizeWnd; + U_POINT16 sizeView; + U_POINT16 winorg; + U_POINT16 vieworg; + double ScaleInX, ScaleInY; + double ScaleOutX, ScaleOutY; + uint16_t bkMode; + U_COLORREF bkColor; + U_COLORREF textColor; + uint16_t textAlign; + U_POINT16 cur; +}; +using PWMF_DEVICE_CONTEXT = WMF_DEVICE_CONTEXT *; + +#define WMF_MAX_DC 128 + + +// like this causes a mysterious crash on the return from Wmf::open +//typedef struct emf_callback_data { +// this fixes it, so some confusion between this struct and the one in emf-inout??? +//typedef struct wmf_callback_data { +// as does this +struct WMF_CALLBACK_DATA { + + WMF_CALLBACK_DATA() : + // dc: array, structure w/ constructor + level(0), + E2IdirY(1.0), + D2PscaleX(1.0), D2PscaleY(1.0), + PixelsInX(0), PixelsInY(0), + PixelsOutX(0), PixelsOutY(0), + ulCornerInX(0), ulCornerInY(0), + ulCornerOutX(0), ulCornerOutY(0), + mask(0), + arcdir(U_AD_COUNTERCLOCKWISE), + dwRop2(U_R2_COPYPEN), dwRop3(0), + id(0), drawtype(0), + // hatches, images, gradients, struct w/ constructor + tri(nullptr), + n_obj(0), + low_water(0) + //wmf_obj + {}; + + Glib::ustring outsvg; + Glib::ustring path; + Glib::ustring outdef; + Glib::ustring defs; + + WMF_DEVICE_CONTEXT dc[WMF_MAX_DC+1]; // FIXME: This should be dynamic.. + int level; + + double E2IdirY; // WMF Y direction relative to Inkscape Y direction. Will be negative for MM_LOMETRIC etc. + double D2PscaleX,D2PscaleY; // WMF device to Inkscape Page scale. + float PixelsInX, PixelsInY; // size of the drawing, in WMF device pixels + float PixelsOutX, PixelsOutY; // size of the drawing, in Inkscape pixels + double ulCornerInX,ulCornerInY; // Upper left corner, from header rclBounds, in logical units + double ulCornerOutX,ulCornerOutY; // Upper left corner, in Inkscape pixels + uint32_t mask; // Draw properties + int arcdir; // U_AD_COUNTERCLOCKWISE 1 or U_AD_CLOCKWISE 2 + + uint32_t dwRop2; // Binary raster operation, 0 if none (use brush/pen unmolested) + uint32_t dwRop3; // Ternary raster operation, 0 if none (use brush/pen unmolested) + + unsigned int id; + unsigned int drawtype; // one of 0 or U_WMR_FILLPATH, U_WMR_STROKEPATH, U_WMR_STROKEANDFILLPATH + // both of these end up in <defs> under the names shown here. These structures allow duplicates to be avoided. + WMF_STRINGS hatches; // hold pattern names, all like WMFhatch#_$$$$$$ where # is the WMF hatch code and $$$$$$ is the color + WMF_STRINGS images; // hold images, all like Image#, where # is the slot the image lives. + WMF_STRINGS clips; // hold clipping paths, referred to be the slot where the clipping path lives + TR_INFO *tri; // Text Reassembly data structure + + + int n_obj; + int low_water; // first object slot which _might_ be unoccupied. Everything below is filled. + PWMF_OBJECT wmf_obj; +}; +using PWMF_CALLBACK_DATA = WMF_CALLBACK_DATA *; + +class Wmf : public Metafile +{ + +public: + Wmf(); // Empty constructor + + ~Wmf() override;//Destructor + + bool check(Inkscape::Extension::Extension *module) override; //Can this module load (always yes for now) + + void save(Inkscape::Extension::Output *mod, // Save the given document to the given filename + SPDocument *doc, + gchar const *filename) override; + + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + + static void init();//Initialize the class + +private: +protected: + static void print_document_to_file(SPDocument *doc, const gchar *filename); + static double current_scale(PWMF_CALLBACK_DATA d); + static std::string current_matrix(PWMF_CALLBACK_DATA d, double x, double y, int useoffset); + static double current_rotation(PWMF_CALLBACK_DATA d); + static void enlarge_hatches(PWMF_CALLBACK_DATA d); + static int in_hatches(PWMF_CALLBACK_DATA d, char *test); + static uint32_t add_hatch(PWMF_CALLBACK_DATA d, uint32_t hatchType, U_COLORREF hatchColor); + static void enlarge_images(PWMF_CALLBACK_DATA d); + static int in_images(PWMF_CALLBACK_DATA d, char *test); + static uint32_t add_dib_image(PWMF_CALLBACK_DATA d, const char *dib, uint32_t iUsage); + static uint32_t add_bm16_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px); + + static void enlarge_clips(PWMF_CALLBACK_DATA d); + static int in_clips(PWMF_CALLBACK_DATA d, const char *test); + static void add_clips(PWMF_CALLBACK_DATA d, const char *clippath, unsigned int logic); + + static void output_style(PWMF_CALLBACK_DATA d); + static double _pix_x_to_point(PWMF_CALLBACK_DATA d, double px); + static double _pix_y_to_point(PWMF_CALLBACK_DATA d, double py); + static double pix_to_x_point(PWMF_CALLBACK_DATA d, double px, double py); + static double pix_to_y_point(PWMF_CALLBACK_DATA d, double px, double py); + static double pix_to_abs_size(PWMF_CALLBACK_DATA d, double px); + static std::string pix_to_xy(PWMF_CALLBACK_DATA d, double x, double y); + static void select_brush(PWMF_CALLBACK_DATA d, int index); + static void select_font(PWMF_CALLBACK_DATA d, int index); + static void select_pen(PWMF_CALLBACK_DATA d, int index); + static int insertable_object(PWMF_CALLBACK_DATA d); + static void delete_object(PWMF_CALLBACK_DATA d, int index); + static int insert_object(PWMF_CALLBACK_DATA d, int type, const char *record); + static uint32_t *unknown_chars(size_t count); + static void common_dib_to_image(PWMF_CALLBACK_DATA d, const char *dib, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh, uint32_t iUsage); + static void common_bm16_to_image(PWMF_CALLBACK_DATA d, U_BITMAP16 Bm16, const char *px, + double dx, double dy, double dw, double dh, int sx, int sy, int sw, int sh); + static int myMetaFileProc(const char *contents, unsigned int length, PWMF_CALLBACK_DATA d); + static void free_wmf_strings(WMF_STRINGS name); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + + +#endif /* EXTENSION_INTERNAL_WMF_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-print.cpp b/src/extension/internal/wmf-print.cpp new file mode 100644 index 0000000..cab66d3 --- /dev/null +++ b/src/extension/internal/wmf-print.cpp @@ -0,0 +1,1602 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile printing + */ +/* Authors: + * Ulf Erikson <ulferikson@users.sf.net> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * David Mathog + * + * Copyright (C) 2006-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +/* + * References: + * - How to Create & Play Enhanced Metafiles in Win32 + * http://support.microsoft.com/kb/q145999/ + * - INFO: Windows Metafile Functions & Aldus Placeable Metafiles + * http://support.microsoft.com/kb/q66949/ + * - Metafile Functions + * http://msdn.microsoft.com/library/en-us/gdi/metafile_0whf.asp + * - Metafile Structures + * http://msdn.microsoft.com/library/en-us/gdi/metafile_5hkj.asp + */ + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/elliptical-arc.h> + +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/rect.h> +#include <2geom/curves.h> +#include "helper/geom.h" +#include "helper/geom-curves.h" + +#include "inkscape-version.h" + +#include "util/units.h" + +#include "extension/system.h" +#include "extension/print.h" +#include "document.h" +#include "path-prefix.h" + +#include "object/sp-pattern.h" +#include "object/sp-image.h" +#include "object/sp-gradient.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-root.h" +#include "object/sp-item.h" + +#include "path/path-boolop.h" + +#include <2geom/svg-path-parser.h> // to get from SVG text to Geom::Path + +#include "display/cairo-utils.h" // for Inkscape::Pixbuf::PF_CAIRO + +#include "wmf-print.h" + +#include <cstring> +#include <3rdparty/libuemf/symbol_convert.h> + +namespace Inkscape { +namespace Extension { +namespace Internal { + +#define PXPERMETER 2835 +#define MAXDISP 2.0 // This should be set in the output dialog. This is ok for experimenting, no more than 2 pixel deviation. Not actually used at present + + +/* globals */ +static double PX2WORLD; // value set in begin() +static bool FixPPTCharPos, FixPPTDashLine, FixPPTGrad2Polys, FixPPTPatternAsHatch; +static WMFTRACK *wt = nullptr; +static WMFHANDLES *wht = nullptr; + +void PrintWmf::smuggle_adxky_out(const char *string, int16_t **adx, double *ky, int *rtl, int *ndx, float scale) +{ + float fdx; + int i; + int16_t *ladx; + const char *cptr = &string[strlen(string) + 1]; // this works because of the first fake terminator + + *adx = nullptr; + *ky = 0.0; // set a default value + sscanf(cptr, "%7d", ndx); + if (!*ndx) { + return; // this could happen with an empty string + } + cptr += 7; + ladx = (int16_t *) malloc(*ndx * sizeof(int16_t)); + if (!ladx) { + g_error("Out of memory"); + } + *adx = ladx; + for (i = 0; i < *ndx; i++, cptr += 7, ladx++) { + sscanf(cptr, "%7f", &fdx); + *ladx = (int16_t) round(fdx * scale); + } + cptr++; // skip 2nd fake terminator + sscanf(cptr, "%7f", &fdx); + *ky = fdx; + cptr += 7; // advance over ky and its space + sscanf(cptr, "%07d", rtl); +} + +PrintWmf::PrintWmf() +{ + // all of the class variables are initialized elsewhere, many in PrintWmf::Begin, +} + + +unsigned int PrintWmf::setup(Inkscape::Extension::Print * /*mod*/) +{ + return TRUE; +} + + +unsigned int PrintWmf::begin(Inkscape::Extension::Print *mod, SPDocument *doc) +{ + char *rec; + gchar const *utf8_fn = mod->get_param_string("destination"); + + // Typically PX2WORLD is 1200/90, using inkscape's default dpi + PX2WORLD = 1200.0 / Inkscape::Util::Quantity::convert(1.0, "in", "px"); + FixPPTCharPos = mod->get_param_bool("FixPPTCharPos"); + FixPPTDashLine = mod->get_param_bool("FixPPTDashLine"); + FixPPTGrad2Polys = mod->get_param_bool("FixPPTGrad2Polys"); + FixPPTPatternAsHatch = mod->get_param_bool("FixPPTPatternAsHatch"); + + (void) wmf_start(utf8_fn, 1000000, 250000, &wt); // Initialize the wt structure + (void) wmf_htable_create(128, 128, &wht); // Initialize the wht structure + + // WMF header the only things that can be set are the page size in inches (w,h) and the dpi + // width and height in px + _width = doc->getWidth().value("px"); + _height = doc->getHeight().value("px"); + + // initialize a few global variables + hbrush = hpen = 0; + htextalignment = U_TA_BASELINE | U_TA_LEFT; + use_stroke = use_fill = simple_shape = usebk = false; + + Inkscape::XML::Node *nv = doc->getReprNamedView(); + if (nv) { + const char *p1 = nv->attribute("pagecolor"); + char *p2; + uint32_t lc = strtoul(&p1[1], &p2, 16); // it looks like "#ABC123" + if (*p2) { + lc = 0; + } + gv.bgc = _gethexcolor(lc); + gv.rgb[0] = (float) U_RGBAGetR(gv.bgc) / 255.0; + gv.rgb[1] = (float) U_RGBAGetG(gv.bgc) / 255.0; + gv.rgb[2] = (float) U_RGBAGetB(gv.bgc) / 255.0; + } + + bool pageBoundingBox; + pageBoundingBox = mod->get_param_bool("pageBoundingBox"); + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(0, 0, _width, _height); + } else { + SPItem *doc_item = doc->getRoot(); + Geom::OptRect bbox = doc_item->desktopVisualBounds(); + if (bbox) { + d = *bbox; + } + } + + d *= Geom::Scale(Inkscape::Util::Quantity::convert(1, "px", "in")); // 90 dpi inside inkscape, wmf file will be 1200 dpi + + /* -1/1200 in next two lines so that WMF read in will write out again at exactly the same size */ + float dwInchesX = d.width() - 1.0 / 1200.0; + float dwInchesY = d.height() - 1.0 / 1200.0; + int dwPxX = round(dwInchesX * 1200.0); + int dwPxY = round(dwInchesY * 1200.0); +#if 0 + float dwInchesX = d.width(); + float dwInchesY = d.height(); + int dwPxX = round(d.width() * 1200.0); + int dwPxY = round(d.height() * 1200.0); +#endif + + U_PAIRF *ps = U_PAIRF_set(dwInchesX, dwInchesY); + rec = U_WMRHEADER_set(ps, 1200); // Example: drawing is A4 horizontal, 1200 dpi + free(ps); + if (!rec) { + g_warning("Failed in PrintWmf::begin at WMRHEADER"); + return -1; + } + (void) wmf_header_append((U_METARECORD *)rec, wt, 1); + + rec = U_WMRSETWINDOWEXT_set(point16_set(dwPxX, dwPxY)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETWINDOWEXT"); + return -1; + } + + rec = U_WMRSETWINDOWORG_set(point16_set(0, 0)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETWINDOWORG"); + return -1; + } + + rec = U_WMRSETMAPMODE_set(U_MM_ANISOTROPIC); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at WMRSETMAPMODE"); + return -1; + } + + /* set some parameters, else the program that reads the WMF may default to other values */ + + rec = U_WMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETBKMODE"); + return -1; + } + + hpolyfillmode = U_WINDING; + rec = U_WMRSETPOLYFILLMODE_set(U_WINDING); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETPOLYFILLMODE"); + return -1; + } + + // Text alignment: (only changed if RTL text is encountered ) + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the WMF text alignment must always be TA_BASELINE|TA_LEFT. + rec = U_WMRSETTEXTALIGN_set(U_TA_BASELINE | U_TA_LEFT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETTEXTALIGN_set"); + return -1; + } + + htextcolor_rgb[0] = htextcolor_rgb[1] = htextcolor_rgb[2] = 0.0; + rec = U_WMRSETTEXTCOLOR_set(U_RGB(0, 0, 0)); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETTEXTCOLOR_set"); + return -1; + } + + rec = U_WMRSETROP2_set(U_R2_COPYPEN); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at U_WMRSETROP2"); + return -1; + } + + hmiterlimit = 5; + rec = wmiterlimit_set(5); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wmiterlimit_set"); + return -1; + } + + + // create a pen as object 0. We never use it (except by mistake). Its purpose it to make all of the other object indices >=1 + U_PEN up = U_PEN_set(U_PS_SOLID, 1, colorref_set(0, 0, 0)); + uint32_t Pen; + rec = wcreatepenindirect_set(&Pen, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatepenindirect_set"); + return -1; + } + + // create a null pen. If no specific pen is set, this is used + up = U_PEN_set(U_PS_NULL, 1, colorref_set(0, 0, 0)); + rec = wcreatepenindirect_set(&hpen_null, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatepenindirect_set"); + return -1; + } + destroy_pen(); // make this pen active + + // create a null brush. If no specific brush is set, this is used + U_WLOGBRUSH lb = U_WLOGBRUSH_set(U_BS_NULL, U_RGB(0, 0, 0), U_HS_HORIZONTAL); + rec = wcreatebrushindirect_set(&hbrush_null, wht, lb); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_warning("Failed in PrintWmf::begin at wcreatebrushindirect_set"); + return -1; + } + destroy_brush(); // make this brush active + + return 0; +} + + +unsigned int PrintWmf::finish(Inkscape::Extension::Print * /*mod*/) +{ + char *rec; + if (!wt) { + return 0; + } + + // get rid of null brush + rec = wdeleteobject_set(&hbrush_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set null brush"); + } + + // get rid of null pen + rec = wdeleteobject_set(&hpen_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set null pen"); + } + + // get rid of object 0, which was a pen that was used to shift the other object indices to >=1. + hpen = 0; + rec = wdeleteobject_set(&hpen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish at wdeleteobject_set filler object"); + } + + rec = U_WMREOF_set(); // generate the EOF record + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::finish"); + } + (void) wmf_finish(wt); // Finalize and write out the WMF + uwmf_free(&wt); // clean up + wmf_htable_free(&wht); // clean up + + return 0; +} + +// fcolor is defined when gradients are being expanded, it is the color of one stripe or ring. +int PrintWmf::create_brush(SPStyle const *style, U_COLORREF *fcolor) +{ + float rgb[3]; + char *rec; + U_WLOGBRUSH lb; + uint32_t brush, fmode; + MFDrawMode fill_mode; + Inkscape::Pixbuf *pixbuf; + uint32_t brushStyle; + int hatchType; + U_COLORREF hatchColor; + U_COLORREF bkColor; + uint32_t width = 0; // quiets a harmless compiler warning, initialization not otherwise required. + uint32_t height = 0; + + if (!wt) { + return 0; + } + + // set a default fill in case we can't figure out a better way to do it + fmode = U_ALTERNATE; + fill_mode = DRAW_PAINT; + brushStyle = U_BS_SOLID; + hatchType = U_HS_SOLIDCLR; + bkColor = U_RGB(0, 0, 0); + if (fcolor) { + hatchColor = *fcolor; + } else { + hatchColor = U_RGB(0, 0, 0); + } + + if (!fcolor && style) { + if (style->fill.isColor()) { + fill_mode = DRAW_PAINT; + /* Dead assignment: Value stored to 'opacity' is never read + float opacity = SP_SCALE24_TO_FLOAT(style->fill_opacity.value); + if (opacity <= 0.0) { + opacity = 0.0; // basically the same as no fill + } + */ + style->fill.value.color.get_rgb_floatv(rgb); + hatchColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + fmode = style->fill_rule.computed == 0 ? U_WINDING : (style->fill_rule.computed == 2 ? U_ALTERNATE : U_ALTERNATE); + } else if (SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style))) { // must be paint-server + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPPattern *pat = SP_PATTERN(paintserver); + double dwidth = pat->width(); + double dheight = pat->height(); + width = dwidth; + height = dheight; + brush_classify(pat, 0, &pixbuf, &hatchType, &hatchColor, &bkColor); + if (pixbuf) { + fill_mode = DRAW_IMAGE; + } else { // pattern + fill_mode = DRAW_PATTERN; + if (hatchType == -1) { // Not a standard hatch, so force it to something + hatchType = U_HS_CROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + if (FixPPTPatternAsHatch) { + if (hatchType == -1) { // image or unclassified + fill_mode = DRAW_PATTERN; + hatchType = U_HS_DIAGCROSS; + hatchColor = U_RGB(0xFF, 0xC3, 0xC3); + } + } + brushStyle = U_BS_HATCHED; + } else if (SP_IS_GRADIENT(SP_STYLE_FILL_SERVER(style))) { // must be a gradient + // currently we do not do anything with gradients, the code below just sets the color to the average of the stops + SPPaintServer *paintserver = style->fill.value.href->getObject(); + SPLinearGradient *lg = nullptr; + SPRadialGradient *rg = nullptr; + + if (SP_IS_LINEARGRADIENT(paintserver)) { + lg = SP_LINEARGRADIENT(paintserver); + lg->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_LINEAR_GRADIENT; + } else if (SP_IS_RADIALGRADIENT(paintserver)) { + rg = SP_RADIALGRADIENT(paintserver); + rg->ensureVector(); // when exporting from commandline, vector is not built + fill_mode = DRAW_RADIAL_GRADIENT; + } else { + // default fill + } + + if (rg) { + if (FixPPTGrad2Polys) { + return hold_gradient(rg, fill_mode); + } else { + hatchColor = avg_stop_color(rg); + } + } else if (lg) { + if (FixPPTGrad2Polys) { + return hold_gradient(lg, fill_mode); + } else { + hatchColor = avg_stop_color(lg); + } + } + } + } else { // if (!style) + // default fill + } + + switch (fill_mode) { + case DRAW_LINEAR_GRADIENT: // fill with average color unless gradients are converted to slices + case DRAW_RADIAL_GRADIENT: // ditto + case DRAW_PAINT: + case DRAW_PATTERN: + // SVG text has no background attribute, so OPAQUE mode ALWAYS cancels after the next draw, otherwise it would mess up future text output. + if (usebk) { + rec = U_WMRSETBKCOLOR_set(bkColor); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETBKCOLOR_set"); + } + rec = U_WMRSETBKMODE_set(U_OPAQUE); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETBKMODE_set"); + } + } + lb = U_WLOGBRUSH_set(brushStyle, hatchColor, hatchType); + rec = wcreatebrushindirect_set(&brush, wht, lb); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at createbrushindirect_set"); + } + break; + case DRAW_IMAGE: + char *px; + char *rgba_px; + uint32_t cbPx; + uint32_t colortype; + U_RGBQUAD *ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + U_BITMAPINFO *Bmi; + rgba_px = (char *) pixbuf->pixels(); // Do NOT free this!!! + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, rgba_px, width, height, width * 4, colortype, 0, 1); + // pixbuf can be either PF_CAIRO or PF_GDK, and these have R and B bytes swapped + if (pixbuf->pixelFormat() == Inkscape::Pixbuf::PF_CAIRO) { swapRBinRGBA(px, width * height); } + Bmih = bitmapinfoheader_set(width, height, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + rec = wcreatedibpatternbrush_srcdib_set(&brush, wht, U_DIB_RGB_COLORS, Bmi, cbPx, px); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at createdibpatternbrushpt_set"); + } + free(px); + free(Bmi); // ct will be NULL because of colortype + break; + } + + hbrush = brush; // need this later for destroy_brush + rec = wselectobject_set(brush, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at wselectobject_set"); + } + + if (fmode != hpolyfillmode) { + hpolyfillmode = fmode; + rec = U_WMRSETPOLYFILLMODE_set(fmode); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_brush at U_WMRSETPOLYFILLMODE_set"); + } + } + + return 0; +} + + +void PrintWmf::destroy_brush() +{ + char *rec; + // WMF lets any object be deleted whenever, and the chips fall where they may... + if (hbrush) { + rec = wdeleteobject_set(&hbrush, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_brush"); + } + hbrush = 0; + } + + // (re)select the null brush + + rec = wselectobject_set(hbrush_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_brush"); + } +} + + +int PrintWmf::create_pen(SPStyle const *style, const Geom::Affine &transform) +{ + char *rec = nullptr; + uint32_t pen; + uint32_t penstyle; + U_COLORREF penColor; + U_PEN up; + int modstyle; + + if (!wt) { + return 0; + } + + // set a default stroke in case we can't figure out a better way to do it + penstyle = U_PS_SOLID; + modstyle = 0; + penColor = U_RGB(0, 0, 0); + uint32_t linewidth = 1; + + if (style) { // override some or all of the preceding + float rgb[3]; + + // WMF does not support hatched, bitmap, or gradient pens, just set the color. + style->stroke.value.color.get_rgb_floatv(rgb); + penColor = U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2]); + + using Geom::X; + using Geom::Y; + + Geom::Point zero(0, 0); + Geom::Point one(1, 1); + Geom::Point p0(zero * transform); + Geom::Point p1(one * transform); + Geom::Point p(p1 - p0); + + double scale = sqrt((p[X] * p[X]) + (p[Y] * p[Y])) / sqrt(2); + + if (!style->stroke_width.computed) { + return 0; //if width is 0 do not (reset) the pen, it should already be NULL_PEN + } + linewidth = MAX(1, (uint32_t) round(scale * style->stroke_width.computed * PX2WORLD)); + + // most WMF readers will ignore linecap and linejoin, but set them anyway. Inkscape itself can read them back in. + + if (style->stroke_linecap.computed == 0) { + modstyle |= U_PS_ENDCAP_FLAT; + } else if (style->stroke_linecap.computed == 1) { + modstyle |= U_PS_ENDCAP_ROUND; + } else { + modstyle |= U_PS_ENDCAP_SQUARE; + } + + if (style->stroke_linejoin.computed == 0) { + float miterlimit = style->stroke_miterlimit.value; // This is a ratio. + if (miterlimit < 1) { + miterlimit = 1; + } + + // most WMF readers will ignore miterlimit, but set it anyway. Inkscape itself can read it back in + if ((uint32_t)miterlimit != hmiterlimit) { + hmiterlimit = (uint32_t)miterlimit; + rec = wmiterlimit_set((uint32_t) miterlimit); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wmiterlimit_set"); + } + } + modstyle |= U_PS_JOIN_MITER; + } else if (style->stroke_linejoin.computed == 1) { + modstyle |= U_PS_JOIN_ROUND; + } else { + modstyle |= U_PS_JOIN_BEVEL; + } + + if (!style->stroke_dasharray.values.empty()) { + if (!FixPPTDashLine) { // if this is set code elsewhere will break dots/dashes into many smaller lines. + int n_dash = style->stroke_dasharray.values.size(); + /* options are dash, dot, dashdot and dashdotdot. Try to pick the closest one. */ + int mark_short=INT_MAX; + int mark_long =0; + int i; + for (i=0;i<n_dash;i++) { + int mark = style->stroke_dasharray.values[i].value; + if (mark > mark_long) { + mark_long = mark; + } + if (mark < mark_short) { + mark_short = mark; + } + } + if(mark_long == mark_short){ // only one mark size + penstyle = U_PS_DOT; + } + else if (n_dash==2) { + penstyle = U_PS_DASH; + } + else if (n_dash==4) { + penstyle = U_PS_DASHDOT; + } + else { + penstyle = U_PS_DASHDOTDOT; + } + } + } + + } + + up = U_PEN_set(penstyle | modstyle, linewidth, penColor); + rec = wcreatepenindirect_set(&pen, wht, up); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wcreatepenindirect_set"); + } + + rec = wselectobject_set(pen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::create_pen at wselectobject_set"); + } + hpen = pen; // need this later for destroy_pen + + return 0; +} + +// delete the defined pen object +void PrintWmf::destroy_pen() +{ + char *rec = nullptr; + // WMF lets any object be deleted whenever, and the chips fall where they may... + if (hpen) { + rec = wdeleteobject_set(&hpen, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_pen"); + } + hpen = 0; + } + + // (re)select the null pen + + rec = wselectobject_set(hpen_null, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::destroy_pen"); + } +} + + +unsigned int PrintWmf::fill( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, Geom::Affine const & /*transform*/, SPStyle const *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + using Geom::X; + using Geom::Y; + + Geom::Affine tf = m_tr_stack.top(); + + use_fill = true; + use_stroke = false; + + fill_transform = tf; + + if (create_brush(style, nullptr)) { + /* + Handle gradients. Uses modified livarot as 2geom boolops is currently broken. + Can handle gradients with multiple stops. + + The overlap is needed to avoid antialiasing artifacts when edges are not strictly aligned on pixel boundaries. + There is an inevitable loss of accuracy saving through an WMF file because of the integer coordinate system. + Keep the overlap quite large so that loss of accuracy does not remove an overlap. + */ + destroy_pen(); //this sets the NULL_PEN, otherwise gradient slices may display with boundaries, see longer explanation below + Geom::Path cutter; + float rgb[3]; + U_COLORREF wc, c1, c2; + FillRule frb = SPWR_to_LVFR((SPWindRule) style->fill_rule.computed); + double doff, doff_base, doff_range; + double divisions = 128.0; + int nstops; + int istop = 1; + float opa; // opacity at stop + + SPRadialGradient *tg = (SPRadialGradient *)(gv.grad); // linear/radial are the same here + nstops = tg->vector.stops.size(); + tg->vector.stops[0].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[0].opacity; + c1 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + tg->vector.stops[nstops - 1].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[nstops - 1].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + doff = 0.0; + doff_base = 0.0; + doff_range = tg->vector.stops[1].offset; // next or last stop + + if (gv.mode == DRAW_RADIAL_GRADIENT) { + Geom::Point xv = gv.p2 - gv.p1; // X' vector + Geom::Point yv = gv.p3 - gv.p1; // Y' vector + Geom::Point xuv = Geom::unit_vector(xv); // X' unit vector + double rx = hypot(xv[X], xv[Y]); + double ry = hypot(yv[X], yv[Y]); + double range = fmax(rx, ry); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* radial gradient might stop part way through the shape, fill with outer color from there to "infinity". + Do this first so that outer colored ring will overlay it. + */ + pathvc = center_elliptical_hole_as_SVG_PathV(gv.p1, rx * (1.0 - overlap / range), ry * (1.0 - overlap / range), asin(xuv[Y])); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_oddEven, frb); + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + + pathvc = center_elliptical_ring_as_SVG_PathV(gv.p1, rx * start / range, ry * start / range, rx * stop / range, ry * stop / range, asin(xuv[Y])); + + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range - doff_base) { + istop++; + if (istop >= nstops) { + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else if (gv.mode == DRAW_LINEAR_GRADIENT) { + Geom::Point uv = Geom::unit_vector(gv.p2 - gv.p1); // unit vector + Geom::Point puv = uv.cw(); // perp. to unit vector + double range = Geom::distance(gv.p1, gv.p2); // length along the gradient + double step = range / divisions; // adequate approximation for gradient + double overlap = step / 4.0; // overlap slices slightly + double start; + double stop; + Geom::PathVector pathvc, pathvr; + + /* before lower end of gradient, overlap first slice position */ + wc = weight_opacity(c1); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p1, uv * (overlap), uv * (-50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + /* after high end of gradient, overlap last slice position */ + wc = weight_opacity(c2); + (void) create_brush(style, &wc); + pathvc = rect_cutter(gv.p2, uv * (-overlap), uv * (50000.0), puv * 50000.0); + pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); + + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + + for (start = 0.0; start < range; start += step, doff += 1. / divisions) { + stop = start + step + overlap; + if (stop > range) { + stop = range; + } + pathvc = rect_cutter(gv.p1, uv * start, uv * stop, puv * 50000.0); + + wc = weight_colors(c1, c2, (doff - doff_base) / (doff_range - doff_base)); + (void) create_brush(style, &wc); + Geom::PathVector pathvr = sp_pathvector_boolop(pathvc, pathv, bool_op_inters, (FillRule) fill_nonZero, frb); + print_pathv(pathvr, fill_transform); // show the intersection + + if (doff >= doff_range - doff_base) { + istop++; + if (istop >= nstops) { + continue; // could happen on a rounding error + } + doff_base = doff_range; + doff_range = tg->vector.stops[istop].offset; // next or last stop + c1 = c2; + tg->vector.stops[istop].color.get_rgb_floatv(rgb); + opa = tg->vector.stops[istop].opacity; + c2 = U_RGBA(255 * rgb[0], 255 * rgb[1], 255 * rgb[2], 255 * opa); + } + } + } else { + g_error("Fatal programming error in PrintWmf::fill, invalid gradient type detected"); + } + use_fill = false; // gradients handled, be sure stroke does not use stroke and fill + } else { + /* + Inkscape was not calling create_pen for objects with no border. + This was because it never called stroke() (next method). + PPT, and presumably others, pick whatever they want for the border if it is not specified, so no border can + become a visible border. + To avoid this force the pen to NULL_PEN if we can determine that no pen will be needed after the fill. + */ + if (style->stroke.noneSet || style->stroke_width.computed == 0.0) { + destroy_pen(); //this sets the NULL_PEN + } + + /* postpone fill in case stroke also required AND all stroke paths closed + Dashes converted to line segments will "open" a closed path. + */ + bool all_closed = true; + for (const auto & pit : pathv) { + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (pit.end_default() != pit.end_closed()) { + all_closed = false; + } + } + } + if ( + (style->stroke.isNone() || style->stroke.noneSet || style->stroke_width.computed == 0.0) || + (!style->stroke_dasharray.values.empty() && FixPPTDashLine) || + !all_closed + ) { + print_pathv(pathv, fill_transform); // do any fills. side effect: clears fill_pathv + use_fill = false; + } + } + + return 0; +} + + +unsigned int PrintWmf::stroke( + Inkscape::Extension::Print * /*mod*/, + Geom::PathVector const &pathv, const Geom::Affine &/*transform*/, const SPStyle *style, + Geom::OptRect const &/*pbox*/, Geom::OptRect const &/*dbox*/, Geom::OptRect const &/*bbox*/) +{ + + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + use_stroke = true; + // use_fill was set in ::fill, if it is needed, if not, the null brush is used, it should be already set + + if (create_pen(style, tf)) { + return 0; + } + + if (!style->stroke_dasharray.values.empty() && FixPPTDashLine) { + // convert the path, gets its complete length, and then make a new path with parameter length instead of t + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw; // pathv-> sbasis + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw2; // sbasis using arc length parameter + Geom::Piecewise<Geom::D2<Geom::SBasis> > tmp_pathpw3; // new (discontinuous) path, composed of dots/dashes + Geom::Piecewise<Geom::D2<Geom::SBasis> > first_frag; // first fragment, will be appended at end + int n_dash = style->stroke_dasharray.values.size(); + int i = 0; //dash index + double tlength; // length of tmp_pathpw + double slength = 0.0; // start of gragment + double elength; // end of gragment + for (const auto & i : pathv) { + tmp_pathpw.concat(i.toPwSb()); + } + tlength = length(tmp_pathpw, 0.1); + tmp_pathpw2 = arc_length_parametrization(tmp_pathpw); + + // go around the dash array repeatedly until the entire path is consumed (but not beyond). + while (slength < tlength) { + elength = slength + style->stroke_dasharray.values[i++].value; + if (elength > tlength) { + elength = tlength; + } + Geom::Piecewise<Geom::D2<Geom::SBasis> > fragment(portion(tmp_pathpw2, slength, elength)); + if (slength) { + tmp_pathpw3.concat(fragment); + } else { + first_frag = fragment; + } + slength = elength; + slength += style->stroke_dasharray.values[i++].value; // the gap + if (i >= n_dash) { + i = 0; + } + } + tmp_pathpw3.concat(first_frag); // may merge line around start point + Geom::PathVector out_pathv = Geom::path_from_piecewise(tmp_pathpw3, 0.01); + print_pathv(out_pathv, tf); + } else { + print_pathv(pathv, tf); + } + + use_stroke = false; + use_fill = false; + + if (usebk) { // OPAQUE was set, revert to TRANSPARENT + usebk = false; + rec = U_WMRSETBKMODE_set(U_TRANSPARENT); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::stroke at U_WMRSETBKMODE_set"); + } + } + + return 0; +} + + +// Draws simple_shapes, those with closed WMR_* primitives, like polygons, rectangles and ellipses. +// These use whatever the current pen/brush are and need not be followed by a FILLPATH or STROKEPATH. +// For other paths it sets a few flags and returns. +bool PrintWmf::print_simple_shape(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + + Geom::PathVector pv = pathv_to_linear(pathv * transform, MAXDISP); + + int nodes = 0; + int moves = 0; + int lines = 0; + int curves = 0; + char *rec = nullptr; + + for (const auto & pit : pv) { + moves++; + nodes++; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + nodes++; + + if (is_straight_curve(*cit)) { + lines++; + } else if (dynamic_cast<Geom::CubicBezier const *>(&*cit)) { + curves++; + } + } + } + + if (!nodes) { + return false; + } + + U_POINT16 *lpPoints = new U_POINT16[moves + lines + curves * 3]; + int i = 0; + + /** For all Subpaths in the <path> */ + + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + Geom::Point p0 = pit.initialPoint(); + + p0[X] = (p0[X] * PX2WORLD); + p0[Y] = (p0[Y] * PX2WORLD); + + int32_t const x0 = (int32_t) round(p0[X]); + int32_t const y0 = (int32_t) round(p0[Y]); + + lpPoints[i].x = x0; + lpPoints[i].y = y0; + i = i + 1; + + /** For all segments in the subpath */ + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + //Geom::Point p0 = cit->initialPoint(); + Geom::Point p1 = cit->finalPoint(); + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + i = i + 1; + } else if (Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*cit)) { + std::vector<Geom::Point> points = cubic->controlPoints(); + //Geom::Point p0 = points[0]; + Geom::Point p1 = points[1]; + Geom::Point p2 = points[2]; + Geom::Point p3 = points[3]; + + //p0[X] = (p0[X] * PX2WORLD); + p1[X] = (p1[X] * PX2WORLD); + p2[X] = (p2[X] * PX2WORLD); + p3[X] = (p3[X] * PX2WORLD); + //p0[Y] = (p0[Y] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + p2[Y] = (p2[Y] * PX2WORLD); + p3[Y] = (p3[Y] * PX2WORLD); + + //int32_t const x0 = (int32_t) round(p0[X]); + //int32_t const y0 = (int32_t) round(p0[Y]); + int32_t const x1 = (int32_t) round(p1[X]); + int32_t const y1 = (int32_t) round(p1[Y]); + int32_t const x2 = (int32_t) round(p2[X]); + int32_t const y2 = (int32_t) round(p2[Y]); + int32_t const x3 = (int32_t) round(p3[X]); + int32_t const y3 = (int32_t) round(p3[Y]); + + lpPoints[i].x = x1; + lpPoints[i].y = y1; + lpPoints[i + 1].x = x2; + lpPoints[i + 1].y = y2; + lpPoints[i + 2].x = x3; + lpPoints[i + 2].y = y3; + i = i + 3; + } + } + } + + bool done = false; + bool closed = (lpPoints[0].x == lpPoints[i - 1].x) && (lpPoints[0].y == lpPoints[i - 1].y); + bool polygon = false; + bool rectangle = false; + bool ellipse = false; + + if (moves == 1 && moves + lines == nodes && closed) { + polygon = true; + // if (nodes==5) { // disable due to LP Bug 407394 + // if (lpPoints[0].x == lpPoints[3].x && lpPoints[1].x == lpPoints[2].x && + // lpPoints[0].y == lpPoints[1].y && lpPoints[2].y == lpPoints[3].y) + // { + // rectangle = true; + // } + // } + } else if (moves == 1 && nodes == 5 && moves + curves == nodes && closed) { + // if (lpPoints[0].x == lpPoints[1].x && lpPoints[1].x == lpPoints[11].x && + // lpPoints[5].x == lpPoints[6].x && lpPoints[6].x == lpPoints[7].x && + // lpPoints[2].x == lpPoints[10].x && lpPoints[3].x == lpPoints[9].x && lpPoints[4].x == lpPoints[8].x && + // lpPoints[2].y == lpPoints[3].y && lpPoints[3].y == lpPoints[4].y && + // lpPoints[8].y == lpPoints[9].y && lpPoints[9].y == lpPoints[10].y && + // lpPoints[5].y == lpPoints[1].y && lpPoints[6].y == lpPoints[0].y && lpPoints[7].y == lpPoints[11].y) + // { // disable due to LP Bug 407394 + // ellipse = true; + // } + } + + if (polygon || ellipse) { + // pens and brushes already set by caller, do not touch them + + if (polygon) { + if (rectangle) { + U_RECT16 rcl = U_RECT16_set((U_POINT16) { + lpPoints[0].x, lpPoints[0].y + }, (U_POINT16) { + lpPoints[2].x, lpPoints[2].y + }); + rec = U_WMRRECTANGLE_set(rcl); + } else { + rec = U_WMRPOLYGON_set(nodes, lpPoints); + } + } else if (ellipse) { + U_RECT16 rcl = U_RECT16_set((U_POINT16) { + lpPoints[6].x, lpPoints[3].y + }, (U_POINT16) { + lpPoints[0].x, lpPoints[9].y + }); + rec = U_WMRELLIPSE_set(rcl); + } + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_simple_shape at retangle/ellipse/polygon"); + } + + done = true; + + } + + delete[] lpPoints; + + return done; +} + +/** Some parts based on win32.cpp by Lauris Kaplinski <lauris@kaplinski.com>. Was a part of Inkscape + in the past (or will be in the future?) Not in current trunk. (4/19/2012) + + Limitations of this code: + 1. Images lose their rotation, one corner stays in the same place. + 2. Transparency is lost on export. (A limitation of the WMF format.) + 3. Probably messes up if row stride != w*4 + 4. There is still a small memory leak somewhere, possibly in a pixbuf created in a routine + that calls this one and passes px, but never removes the rest of the pixbuf. The first time + this is called it leaked 5M (in one test) and each subsequent call leaked around 200K more. + If this routine is reduced to + if(1)return(0); + and called for a single 1280 x 1024 image then the program leaks 11M per call, or roughly the + size of two bitmaps. +*/ + +unsigned int PrintWmf::image( + Inkscape::Extension::Print * /* module */, /** not used */ + unsigned char *rgba_px, /** array of pixel values, Gdk::Pixbuf bitmap format */ + unsigned int w, /** width of bitmap */ + unsigned int h, /** height of bitmap */ + unsigned int rs, /** row stride (normally w*4) */ + Geom::Affine const &tf_rect, /** affine transform only used for defining location and size of rect, for all other transforms, use the one from m_tr_stack */ + SPStyle const * /*style*/) /** provides indirect link to image object */ +{ + double x1, y1, dw, dh; + char *rec = nullptr; + Geom::Affine tf = m_tr_stack.top(); + + rec = U_WMRSETSTRETCHBLTMODE_set(U_COLORONCOLOR); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::image at EMRHEADER"); + } + + x1 = tf_rect[4]; + y1 = tf_rect[5]; + dw = ((double) w) * tf_rect[0]; + dh = ((double) h) * tf_rect[3]; + Geom::Point pLL(x1, y1); + Geom::Point pLL2 = pLL * tf; //location of LL corner in Inkscape coordinates + + /* adjust scale of w and h. This works properly when there is no rotation. The values are + a bit strange when there is rotation, but since WMF cannot handle rotation in any case, all + answers are equally wrong. + */ + Geom::Point pWH(dw, dh); + Geom::Point pWH2 = pWH * tf.withoutTranslation(); + + char *px; + uint32_t cbPx; + uint32_t colortype; + U_RGBQUAD *ct; + int numCt; + U_BITMAPINFOHEADER Bmih; + U_BITMAPINFO *Bmi; + colortype = U_BCBM_COLOR32; + (void) RGBA_to_DIB(&px, &cbPx, &ct, &numCt, (char *) rgba_px, w, h, w * 4, colortype, 0, 1); + Bmih = bitmapinfoheader_set(w, h, 1, colortype, U_BI_RGB, 0, PXPERMETER, PXPERMETER, numCt, 0); + Bmi = bitmapinfo_set(Bmih, ct); + + U_POINT16 Dest = point16_set(round(pLL2[Geom::X] * PX2WORLD), round(pLL2[Geom::Y] * PX2WORLD)); + U_POINT16 cDest = point16_set(round(pWH2[Geom::X] * PX2WORLD), round(pWH2[Geom::Y] * PX2WORLD)); + U_POINT16 Src = point16_set(0, 0); + U_POINT16 cSrc = point16_set(w, h); + rec = U_WMRSTRETCHDIB_set( + Dest, //! Destination UL corner in logical units + cDest, //! Destination W & H in logical units + Src, //! Source UL corner in logical units + cSrc, //! Source W & H in logical units + U_DIB_RGB_COLORS, //! DIBColors Enumeration + U_SRCCOPY, //! RasterOPeration Enumeration + Bmi, //! (Optional) bitmapbuffer (U_BITMAPINFO section) + h * rs, //! size in bytes of px + px //! (Optional) bitmapbuffer (U_BITMAPINFO section) + ); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::image at U_WMRSTRETCHDIB_set"); + } + free(px); + free(Bmi); + if (numCt) { + free(ct); + } + return 0; +} + +// may also be called with a simple_shape or an empty path, whereupon it just returns without doing anything +unsigned int PrintWmf::print_pathv(Geom::PathVector const &pathv, const Geom::Affine &transform) +{ + char *rec = nullptr; + U_POINT16 *pt16hold, *pt16ptr; + uint16_t *n16hold; + uint16_t *n16ptr; + + simple_shape = print_simple_shape(pathv, transform); + if (!simple_shape && !pathv.empty()) { + // WMF does not have beziers, need to convert to ONLY linears with something like this: + Geom::PathVector pv = pathv_to_linear(pathv * transform, MAXDISP); + + /** For all Subpaths in the <path> */ + + /* If the path consists entirely of closed subpaths use one polypolygon. + Otherwise use a mix of polygon or polyline separately on each path. + If the polyline turns out to be single line segments, use a series of MOVETO/LINETO instead, + because WMF has no POLYPOLYLINE. + The former allows path delimited donuts and the like, which + cannot be represented in WMF with polygon or polyline because there is no external way to combine paths + as there is in EMF or SVG. + For polygons specify the last point the same as the first. The WMF/EMF manuals say that the + reading program SHOULD close the path, which allows a conforming program not to, potentially rendering + a closed path as an open one. */ + int nPolys = 0; + int totPoints = 0; + for (const auto & pit : pv) { + totPoints += 1 + pit.size_default(); // big array, will hold all points, for all polygons. Size_default ignores first point in each path. + if (pit.end_default() == pit.end_closed()) { + nPolys++; + } else { + nPolys = 0; + break; + } + } + + if (nPolys > 1) { // a single polypolygon, a single polygon falls through to the else + pt16hold = pt16ptr = (U_POINT16 *) malloc(totPoints * sizeof(U_POINT16)); + if (!pt16ptr) { + return(false); + } + + n16hold = n16ptr = (uint16_t *) malloc(nPolys * sizeof(uint16_t)); + if (!n16ptr) { + free(pt16hold); + return(false); + } + + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + + *n16ptr++ = pit.size_default(); // points in the subpath + + /** For each segment in the subpath */ + + Geom::Point p1 = pit.initialPoint(); // This point is special, it isn't in the iterator + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + Geom::Point p1 = cit->finalPoint(); + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + } + + } + rec = U_WMRPOLYPOLYGON_set(nPolys, n16hold, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRPOLYPOLYGON_set"); + } + free(pt16hold); + free(n16hold); + } else { // one or more polyline or polygons (but not all polygons, that would be the preceding case) + for (const auto & pit : pv) { + using Geom::X; + using Geom::Y; + + /* Malformatted Polylines with a sequence like M L M M L have been seen, the 2nd M does nothing + and that point must not go into the output. */ + if (!(pit.size_default())) { + continue; + } + /* Figure out how many points there are, make an array big enough to hold them, and store + all the points. This is the same for open or closed path. This gives the upper bound for + the number of points. The actual number used is calculated on the fly. + */ + int nPoints = 1 + pit.size_default(); + + pt16hold = pt16ptr = (U_POINT16 *) malloc(nPoints * sizeof(U_POINT16)); + if (!pt16ptr) { + break; + } + + /** For each segment in the subpath */ + + Geom::Point p1 = pit.initialPoint(); // This point is special, it isn't in the iterator + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + nPoints = 1; + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_default(); ++cit, nPoints++) { + Geom::Point p1 = cit->finalPoint(); + + p1[X] = (p1[X] * PX2WORLD); + p1[Y] = (p1[Y] * PX2WORLD); + *pt16ptr++ = point16_set((int32_t) round(p1[X]), (int32_t) round(p1[Y])); + } + + if (pit.end_default() == pit.end_closed()) { + rec = U_WMRPOLYGON_set(nPoints, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRPOLYGON_set"); + } + } else if (nPoints > 2) { + rec = U_WMRPOLYLINE_set(nPoints, pt16hold); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_POLYLINE_set"); + } + } else if (nPoints == 2) { + rec = U_WMRMOVETO_set(pt16hold[0]); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRMOVETO_set"); + } + rec = U_WMRLINETO_set(pt16hold[1]); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::print_pathv at U_WMRLINETO_set"); + } + } + free(pt16hold); + } + } + } + + // WMF has no fill or stroke commands, the draw does it with active pen/brush + + // clean out brush and pen, but only after all parts of the draw complete + if (use_fill) { + destroy_brush(); + } + if (use_stroke) { + destroy_pen(); + } + + return TRUE; +} + + +unsigned int PrintWmf::text(Inkscape::Extension::Print * /*mod*/, char const *text, Geom::Point const &p, + SPStyle const *const style) +{ + if (!wt || !text) { + return 0; + } + + char *rec = nullptr; + int ccount, newfont; + int fix90n = 0; + uint32_t hfont = 0; + Geom::Affine tf = m_tr_stack.top(); + double rot = -1800.0 * std::atan2(tf[1], tf[0]) / M_PI; // 0.1 degree rotation, - sign for MM_TEXT + double rotb = -std::atan2(tf[1], tf[0]); // rotation for baseline offset for superscript/subscript, used below + double dx, dy; + double ky; + + // the dx array is smuggled in like: text<nul>w1 w2 w3 ...wn<nul><nul>, where the widths are floats 7 characters wide, including the space + int ndx = 0; + int rtl = 0; + int16_t *adx; + smuggle_adxky_out(text, &adx, &ky, &rtl, &ndx, PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); // side effect: free() adx + + uint32_t textalignment; + if (rtl > 0) { + textalignment = U_TA_BASELINE | U_TA_LEFT; + } else { + textalignment = U_TA_BASELINE | U_TA_RIGHT | U_TA_RTLREADING; + } + if (textalignment != htextalignment) { + htextalignment = textalignment; + rec = U_WMRSETTEXTALIGN_set(textalignment); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMRSETTEXTALIGN_set"); + } + } + + char *text2 = strdup(text); // because U_Utf8ToUtf16le calls iconv which does not like a const char * + uint16_t *unicode_text = U_Utf8ToUtf16le(text2, 0, nullptr); + free(text2); + //translates Unicode as Utf16le to NonUnicode, if possible. If any translate, all will, and all to + //the same font, because of code in Layout::print + UnicodeToNon(unicode_text, &ccount, &newfont); + // The preceding hopefully handled conversions to symbol, wingdings or zapf dingbats. Now slam everything + // else down into latin1, which is all WMF can handle. If the language isn't English expect terrible results. + char *latin1_text = U_Utf16leToLatin1(unicode_text, 0, nullptr); + free(unicode_text); + + // in some cases a UTF string may reduce to NO latin1 characters, which returns NULL + if(!latin1_text){free(adx); return 0; } + + //PPT gets funky with text within +-1 degree of a multiple of 90, but only for SOME fonts.Snap those to the central value + //Some funky ones: Arial, Times New Roman + //Some not funky ones: Symbol and Verdana. + //Without a huge table we cannot catch them all, so just the most common problem ones. + FontfixParams params; + + if (FixPPTCharPos) { + switch (newfont) { + case CVTSYM: + _lookup_ppt_fontfix("Convert To Symbol", params); + break; + case CVTZDG: + _lookup_ppt_fontfix("Convert To Zapf Dingbats", params); + break; + case CVTWDG: + _lookup_ppt_fontfix("Convert To Wingdings", params); + break; + default: //also CVTNON + _lookup_ppt_fontfix(style->font_family.value(), params); + break; + } + if (params.f2 != 0 || params.f3 != 0) { + int irem = ((int) round(rot)) % 900 ; + if (irem <= 9 && irem >= -9) { + fix90n = 1; //assume vertical + rot = (double)(((int) round(rot)) - irem); + rotb = rot * M_PI / 1800.0; + if (std::abs(rot) == 900.0) { + fix90n = 2; + } + } + } + } + + /* + Note that text font sizes are stored into the WMF as fairly small integers and that limits their precision. + The WMF output files produced here have been designed so that the integer valued pt sizes + land right on an integer value in the WMF file, so those are exact. However, something like 18.1 pt will be + somewhat off, so that when it is read back in it becomes 18.11 pt. (For instance.) + */ + int textheight = round(-style->font_size.computed * PX2WORLD * std::min(tf.expansionX(), tf.expansionY())); + if (!hfont) { + + // Get font face name. Use changed font name if unicode mapped to one + // of the special fonts. + char *facename; + if (!newfont) { + facename = U_Utf8ToLatin1(style->font_family.value(), 0, nullptr); + } else { + facename = U_Utf8ToLatin1(FontName(newfont), 0, nullptr); + } + + // Scale the text to the minimum stretch. (It tends to stay within bounding rectangles even if + // it was streteched asymmetrically.) Few applications support text from WMF which is scaled + // differently by height/width, so leave lfWidth alone. + + U_FONT *puf = U_FONT_set( + textheight, + 0, + round(rot), + round(rot), + _translate_weight(style->font_weight.computed), + (style->font_style.computed == SP_CSS_FONT_STYLE_ITALIC), + style->text_decoration_line.underline, + style->text_decoration_line.line_through, + U_DEFAULT_CHARSET, + U_OUT_DEFAULT_PRECIS, + U_CLIP_DEFAULT_PRECIS, + U_DEFAULT_QUALITY, + U_DEFAULT_PITCH | U_FF_DONTCARE, + facename); + free(facename); + + rec = wcreatefontindirect_set(&hfont, wht, puf); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wcreatefontindirect_set"); + } + free(puf); + } + + rec = wselectobject_set(hfont, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wselectobject_set"); + } + + float rgb[3]; + style->fill.value.color.get_rgb_floatv(rgb); + // only change the text color when it needs to be changed + if (memcmp(htextcolor_rgb, rgb, 3 * sizeof(float))) { + memcpy(htextcolor_rgb, rgb, 3 * sizeof(float)); + rec = U_WMRSETTEXTCOLOR_set(U_RGB(255 * rgb[0], 255 * rgb[1], 255 * rgb[2])); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMRSETTEXTCOLOR_set"); + } + } + + + // Text alignment: + // - (x,y) coordinates received by this filter are those of the point where the text + // actually starts, and already takes into account the text object's alignment; + // - for this reason, the WMF text alignment must always be TA_BASELINE|TA_LEFT. + // this is set at the beginning of the file and never changed + + // Transparent text background, never changes, set at the beginning of the file + + Geom::Point p2 = p * tf; + + //Handle super/subscripts and vertical kerning + /* Previously used this, but vertical kerning was not supported + p2[Geom::X] -= style->baseline_shift.computed * std::sin( rotb ); + p2[Geom::Y] -= style->baseline_shift.computed * std::cos( rotb ); + */ + p2[Geom::X] += ky * std::sin(rotb); + p2[Geom::Y] += ky * std::cos(rotb); + + //Conditionally handle compensation for PPT WMF import bug (affects PPT 2003-2010, at least) + if (FixPPTCharPos) { + if (fix90n == 1) { //vertical + dx = 0.0; + dy = params.f3 * style->font_size.computed * std::cos(rotb); + } else if (fix90n == 2) { //horizontal + dx = params.f2 * style->font_size.computed * std::sin(rotb); + dy = 0.0; + } else { + dx = params.f1 * style->font_size.computed * std::sin(rotb); + dy = params.f1 * style->font_size.computed * std::cos(rotb); + } + p2[Geom::X] += dx; + p2[Geom::Y] += dy; + } + + p2[Geom::X] = (p2[Geom::X] * PX2WORLD); + p2[Geom::Y] = (p2[Geom::Y] * PX2WORLD); + + int32_t const xpos = (int32_t) round(p2[Geom::X]); + int32_t const ypos = (int32_t) round(p2[Geom::Y]); + + // The number of characters in the string is a bit fuzzy. ndx, the number of entries in adx is + // the number of VISIBLE characters, since some may combine from the UTF (8 originally, + // now 16) encoding. Conversely strlen() or wchar16len() would give the absolute number of + // encoding characters. Unclear if emrtext wants the former or the latter but for now assume the former. + + // This is currently being smuggled in from caller as part of text, works + // MUCH better than the fallback hack below + // uint32_t *adx = dx_set(textheight, U_FW_NORMAL, slen); // dx is needed, this makes one up + if (rtl > 0) { + rec = U_WMREXTTEXTOUT_set((U_POINT16) { + (int16_t) xpos, (int16_t) ypos + }, + ndx, U_ETO_NONE, latin1_text, adx, U_RCL16_DEF); + } else { // RTL text, U_TA_RTLREADING should be enough, but set this one too just in case + rec = U_WMREXTTEXTOUT_set((U_POINT16) { + (int16_t) xpos, (int16_t) ypos + }, + ndx, U_ETO_RTLREADING, latin1_text, adx, U_RCL16_DEF); + } + free(latin1_text); + free(adx); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at U_WMREXTTEXTOUTW_set"); + } + + rec = wdeleteobject_set(&hfont, wht); + if (!rec || wmf_append((U_METARECORD *)rec, wt, U_REC_FREE)) { + g_error("Fatal programming error in PrintWmf::text at wdeleteobject_set"); + } + + return 0; +} + +void PrintWmf::init() +{ + /* WMF print */ + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>Windows Metafile Print</name>\n" + "<id>org.inkscape.print.wmf</id>\n" + "<param gui-hidden=\"true\" name=\"destination\" type=\"string\"></param>\n" + "<param gui-hidden=\"true\" name=\"textToPath\" type=\"bool\">true</param>\n" + "<param gui-hidden=\"true\" name=\"pageBoundingBox\" type=\"bool\">true</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTCharPos\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTDashLine\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTGrad2Polys\" type=\"bool\">false</param>\n" + "<param gui-hidden=\"true\" name=\"FixPPTPatternAsHatch\" type=\"bool\">false</param>\n" + "<print/>\n" + "</inkscape-extension>", new PrintWmf()); + // clang-format on + + return; +} + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wmf-print.h b/src/extension/internal/wmf-print.h new file mode 100644 index 0000000..cc594fe --- /dev/null +++ b/src/extension/internal/wmf-print.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Windows Metafile printing - implementation + */ +/* Author: + * Ulf Erikson <ulferikson@users.sf.net> + * + * Copyright (C) 2006-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INKSCAPE_EXTENSION_INTERNAL_WMF_PRINT_H +#define SEEN_INKSCAPE_EXTENSION_INTERNAL_WMF_PRINT_H + +#include <3rdparty/libuemf/uwmf.h> +#include "extension/internal/metafile-print.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class PrintWmf : public PrintMetafile +{ + uint32_t hbrush, hpen, hbrush_null, hpen_null; + uint32_t hmiterlimit; // used to minimize redundant records that set this + + unsigned int print_pathv (Geom::PathVector const &pathv, const Geom::Affine &transform); + bool print_simple_shape (Geom::PathVector const &pathv, const Geom::Affine &transform); + +public: + PrintWmf(); + + /* Print functions */ + unsigned int setup (Inkscape::Extension::Print * module) override; + + unsigned int begin (Inkscape::Extension::Print * module, SPDocument *doc) override; + unsigned int finish (Inkscape::Extension::Print * module) override; + + /* Rendering methods */ + unsigned int fill (Inkscape::Extension::Print *module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int stroke (Inkscape::Extension::Print * module, + Geom::PathVector const &pathv, + Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, + Geom::OptRect const &bbox) override; + unsigned int image(Inkscape::Extension::Print *module, + unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style) override; + unsigned int text(Inkscape::Extension::Print *module, char const *text, + Geom::Point const &p, SPStyle const *style) override; + + static void init (); +protected: + static void smuggle_adxky_out(const char *string, int16_t **adx, double *ky, int *rtl, int *ndx, float scale); + + int create_brush(SPStyle const *style, PU_COLORREF fcolor) override; + void destroy_brush() override; + int create_pen(SPStyle const *style, const Geom::Affine &transform) override; + void destroy_pen() override; +}; + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + + +#endif /* __INKSCAPE_EXTENSION_INTERNAL_PRINT_WMF_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/internal/wpg-input.cpp b/src/extension/internal/wpg-input.cpp new file mode 100644 index 0000000..5e6f381 --- /dev/null +++ b/src/extension/internal/wpg-input.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This file came from libwpg as a source, their utility wpg2svg + * specifically. It has been modified to work as an Inkscape extension. + * The Inkscape extension code is covered by this copyright, but the + * rest is covered by the one below. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Abhishek Sharma + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +/* libwpg + * Copyright (C) 2006 Ariya Hidayat (ariya@kde.org) + * Copyright (C) 2005 Fridrich Strba (fridrich.strba@bluewin.ch) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * For further information visit http://libwpg.sourceforge.net + */ + +/* "This product is not manufactured, approved, or supported by + * Corel Corporation or Corel Corporation Limited." + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#include <cstdio> + +#ifdef WITH_LIBWPG + +#include "wpg-input.h" +#include "extension/system.h" +#include "extension/input.h" +#include "document.h" +#include "object/sp-root.h" +#include "util/units.h" +#include <cstring> + +#include "libwpg/libwpg.h" +#include <librevenge-stream/librevenge-stream.h> + +using librevenge::RVNGString; +using librevenge::RVNGFileStream; +using librevenge::RVNGInputStream; + +using namespace libwpg; + +namespace Inkscape { +namespace Extension { +namespace Internal { + + +SPDocument *WpgInput::open(Inkscape::Extension::Input * /*mod*/, const gchar * uri) +{ + #ifdef _WIN32 + // RVNGFileStream uses fopen() internally which unfortunately only uses ANSI encoding on Windows + // therefore attempt to convert uri to the system codepage + // even if this is not possible the alternate short (8.3) file name will be used if available + gchar * converted_uri = g_win32_locale_filename_from_utf8(uri); + RVNGInputStream* input = new RVNGFileStream(converted_uri); + g_free(converted_uri); + #else + RVNGInputStream* input = new RVNGFileStream(uri); + #endif + + if (input->isStructured()) { + RVNGInputStream* olestream = input->getSubStreamByName("PerfectOffice_MAIN"); + + if (olestream) { + delete input; + input = olestream; + } + } + + if (!WPGraphics::isSupported(input)) { + //! \todo Dialog here + // fprintf(stderr, "ERROR: Unsupported file format (unsupported version) or file is encrypted!\n"); + // printf("I'm giving up not supported\n"); + delete input; + return nullptr; + } + + librevenge::RVNGStringVector vec; + librevenge::RVNGSVGDrawingGenerator generator(vec, ""); + + if (!libwpg::WPGraphics::parse(input, &generator) || vec.empty() || vec[0].empty()) { + delete input; + return nullptr; + } + + RVNGString output("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"); + output.append(vec[0]); + + //printf("I've got a doc: \n%s", painter.document.c_str()); + + SPDocument * doc = SPDocument::createNewDocFromMem(output.cstr(), strlen(output.cstr()), TRUE); + + // Set viewBox if it doesn't exist + if (doc && !doc->getRoot()->viewBox_set) { + doc->setViewBox(Geom::Rect::from_xywh(0, 0, doc->getWidth().value(doc->getDisplayUnit()), doc->getHeight().value(doc->getDisplayUnit()))); + } + + delete input; + return doc; +} + +#include "clear-n_.h" + +void WpgInput::init() { + // clang-format off + Inkscape::Extension::build_from_mem( + "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n" + "<name>" N_("WPG Input") "</name>\n" + "<id>org.inkscape.input.wpg</id>\n" + "<input>\n" + "<extension>.wpg</extension>\n" + "<mimetype>image/x-wpg</mimetype>\n" + "<filetypename>" N_("WordPerfect Graphics (*.wpg)") "</filetypename>\n" + "<filetypetooltip>" N_("Vector graphics format used by Corel WordPerfect") "</filetypetooltip>\n" + "</input>\n" + "</inkscape-extension>", new WpgInput()); + // clang-format on +} // init + +} } } /* namespace Inkscape, Extension, Implementation */ +#endif /* WITH_LIBWPG */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/internal/wpg-input.h b/src/extension/internal/wpg-input.h new file mode 100644 index 0000000..67e4d91 --- /dev/null +++ b/src/extension/internal/wpg-input.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This code abstracts the libwpg interfaces into the Inkscape + * input extension interface. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __EXTENSION_INTERNAL_WPGOUTPUT_H__ +#define __EXTENSION_INTERNAL_WPGOUTPUT_H__ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifdef WITH_LIBWPG + +#include "../implementation/implementation.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +class WpgInput : public Inkscape::Extension::Implementation::Implementation { + WpgInput () = default;; +public: + SPDocument *open( Inkscape::Extension::Input *mod, + const gchar *uri ) override; + static void init( ); + +}; + +} } } /* namespace Inkscape, Extension, Implementation */ + +#endif /* WITH_LIBWPG */ +#endif /* __EXTENSION_INTERNAL_WPGOUTPUT_H__ */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/loader.cpp b/src/extension/loader.cpp new file mode 100644 index 0000000..d409d09 --- /dev/null +++ b/src/extension/loader.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Loader for external plug-ins. + * + * Authors: + * Moritz Eberl <moritz@semiodesk.com> + * + * Copyright (C) 2016 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "loader.h" + +#include <gmodule.h> + +#include "system.h" +#include <cstring> +#include "dependency.h" +#include "inkscape-version.h" + +namespace Inkscape { +namespace Extension { + +typedef Implementation::Implementation *(*_getImplementation)(); +typedef const gchar *(*_getInkscapeVersion)(); + +bool Loader::load_dependency(Dependency *dep) +{ + GModule *module = nullptr; + module = g_module_open(dep->get_name(), (GModuleFlags)0); + if (module == nullptr) { + return false; + } + return true; +} + +/** + * @brief Load the actual implementation of a plugin supplied by the plugin. + * @param doc The xml representation of the INX extension configuration. + * @return The implementation of the extension loaded from the plugin. + */ +Implementation::Implementation *Loader::load_implementation(Inkscape::XML::Document *doc) +{ + try { + + Inkscape::XML::Node *repr = doc->root(); + Inkscape::XML::Node *child_repr = repr->firstChild(); + + // Iterate over the xml content + while (child_repr != nullptr) { + char const *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + + // Deal with dependencies if we have them + if (!strcmp(chname, "dependency")) { + Dependency dep = Dependency(child_repr, nullptr); // TODO: Why is "this" not an extension? + // try to load it + bool success = load_dependency(&dep); + if( !success ){ + // Could not load dependency, we abort + const char *res = g_module_error(); + g_warning("Unable to load dependency %s of plugin %s.\nDetails: %s\n", dep.get_name(), "<todo>", res); + return nullptr; + } + } + + // Found a plugin to load + if (!strcmp(chname, "plugin")) { + + // The name of the plugin is actually the library file we want to load + if (const gchar *name = child_repr->attribute("name")) { + GModule *module = nullptr; + _getImplementation GetImplementation = nullptr; + _getInkscapeVersion GetInkscapeVersion = nullptr; + + // build the path where to look for the plugin + gchar *path = g_build_filename(_baseDirectory.c_str(), name, (char *) nullptr); + module = g_module_open(path, G_MODULE_BIND_LOCAL); + g_free(path); + + if (module == nullptr) { + // we were not able to load the plugin, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get a handle to the version function of the module + if (g_module_symbol(module, "GetInkscapeVersion", (gpointer *) &GetInkscapeVersion) == FALSE) { + // This didn't work, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get a handle to the function that delivers the implementation + if (g_module_symbol(module, "GetImplementation", (gpointer *) &GetImplementation) == FALSE) { + // This didn't work, write warning and abort + const char *res = g_module_error(); + g_warning("Unable to load extension %s.\nDetails: %s\n", name, res); + return nullptr; + } + + // Get version and test against this version + const gchar* version = GetInkscapeVersion(); + if( strcmp(version, version_string) != 0) { + // The versions are different, display warning. + g_warning("Plugin was built against Inkscape version %s, this is %s. The plugin might not be compatible.", version, version_string); + } + + + Implementation::Implementation *i = GetImplementation(); + return i; + } + } + + child_repr = child_repr->next(); + } + } catch (std::exception &e) { + g_warning("Unable to load extension."); + } + return nullptr; +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/extension/loader.h b/src/extension/loader.h new file mode 100644 index 0000000..abfb4fb --- /dev/null +++ b/src/extension/loader.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Loader for external plug-ins. + *//* + * + * Authors: + * Moritz Eberl <moritz@semiodesk.com> + * + * Copyright (C) 2016 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_LOADER_H_ +#define INKSCAPE_EXTENSION_LOADER_H_ + +#include "extension.h" + + +namespace Inkscape { + +namespace XML { +class Document; +} + +namespace Extension { + +/** This class contains the mechanism to load c++ plugins dynamically. +*/ +class Loader { + +public: + /** + * Sets a base directory where to look for the actual plugin to load. + * + * @param dir is the path where the plugin should be loaded from. + */ + void set_base_directory(std::string const &dir) { + _baseDirectory = dir; + } + + /** + * Loads plugin dependencies which are needed for the plugin to load. + * + * @param dep + */ + bool load_dependency(Dependency *dep); + + /** + * Load the actual implementation of a plugin supplied by the plugin. + * + * @param doc The xml representation of the INX extension configuration. + * @return The implementation of the extension loaded from the plugin. + */ + Implementation::Implementation *load_implementation(Inkscape::XML::Document *doc); + +private: + std::string _baseDirectory; /**< The base directory to load a plugin from */ + + +}; + +} // namespace Extension +} // namespace Inkscape */ + +#endif // _LOADER_H_ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/extension/output.cpp b/src/extension/output.cpp new file mode 100644 index 0000000..750d80b --- /dev/null +++ b/src/extension/output.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "output.h" + +#include "document.h" + +#include "io/sys.h" +#include "implementation/implementation.h" + +#include "prefdialog/prefdialog.h" + +#include "xml/repr.h" + + +/* Inkscape::Extension::Output */ + +namespace Inkscape { +namespace Extension { + +/** + \return None + \brief Builds a SPModuleOutput object from a XML description + \param module The module to be initialized + \param repr The XML description in a Inkscape::XML::Node tree + + Okay, so you want to build a SPModuleOutput object. + + This function first takes and does the build of the parent class, + which is SPModule. Then, it looks for the <output> section of the + XML description. Under there should be several fields which + describe the output module to excruciating detail. Those are parsed, + copied, and put into the structure that is passed in as module. + Overall, there are many levels of indentation, just to handle the + levels of indentation in the XML file. +*/ +Output::Output (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + mimetype = nullptr; + extension = nullptr; + filetypename = nullptr; + filetypetooltip = nullptr; + dataloss = true; + savecopyonly = false; + exported = false; + raster = false; + + if (repr != nullptr) { + Inkscape::XML::Node * child_repr; + + child_repr = repr->firstChild(); + + while (child_repr != nullptr) { + if (!strcmp(child_repr->name(), INKSCAPE_EXTENSION_NS "output")) { + + if (child_repr->attribute("raster") && !strcmp(child_repr->attribute("raster"), "true")) { + raster = true; + } + if (child_repr->attribute("is_exported") && !strcmp(child_repr->attribute("is_exported"), "true")) { + exported = true; + } + + child_repr = child_repr->firstChild(); + while (child_repr != nullptr) { + char const * chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') /* Allow _ for translation of tags */ + chname++; + if (!strcmp(chname, "extension")) { + g_free (extension); + extension = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "mimetype")) { + g_free (mimetype); + mimetype = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypename")) { + g_free (filetypename); + filetypename = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "filetypetooltip")) { + g_free (filetypetooltip); + filetypetooltip = g_strdup(child_repr->firstChild()->content()); + } + if (!strcmp(chname, "dataloss")) { + dataloss = strcmp(child_repr->firstChild()->content(), "false"); + } + if (!strcmp(chname, "savecopyonly")) { + savecopyonly = !strcmp(child_repr->firstChild()->content(), "true"); + } + + child_repr = child_repr->next(); + } + + break; + } + + child_repr = child_repr->next(); + } + + } +} + +/** + \brief Destroy an output extension +*/ +Output::~Output () +{ + g_free(mimetype); + g_free(extension); + g_free(filetypename); + g_free(filetypetooltip); + return; +} + +/** + \return Whether this extension checks out + \brief Validate this extension + + This function checks to make sure that the output extension has + a filename extension and a MIME type. Then it calls the parent + class' check function which also checks out the implementation. +*/ +bool +Output::check () +{ + if (extension == nullptr) + return FALSE; + if (mimetype == nullptr) + return FALSE; + + return Extension::check(); +} + +/** + \return IETF mime-type for the extension + \brief Get the mime-type that describes this extension +*/ +gchar * +Output::get_mimetype() +{ + return mimetype; +} + +/** + \return Filename extension for the extension + \brief Get the filename extension for this extension +*/ +gchar * +Output::get_extension() +{ + return extension; +} + +/** + \return The name of the filetype supported + \brief Get the name of the filetype supported +*/ +const char * +Output::get_filetypename(bool translated) +{ + const char *name; + + if (filetypename) + name = filetypename; + else + name = get_name(); + + if (name && translated && filetypename) { + return get_translation(name); + } else { + return name; + } +} + +/** + \return Tooltip giving more information on the filetype + \brief Get the tooltip for more information on the filetype +*/ +const char * +Output::get_filetypetooltip(bool translated) +{ + if (filetypetooltip && translated) { + return get_translation(filetypetooltip); + } else { + return filetypetooltip; + } +} + +/** + \return A dialog to get settings for this extension + \brief Create a dialog for preference for this extension + + Calls the implementation to get the preferences. +*/ +bool +Output::prefs () +{ + if (!loaded()) + set_state(Extension::STATE_LOADED); + if (!loaded()) return false; + + Gtk::Widget * controls; + controls = imp->prefs_output(this); + if (controls == nullptr) { + // std::cout << "No preferences for Output" << std::endl; + return true; + } + + Glib::ustring title = this->get_name(); + PrefDialog *dialog = new PrefDialog(title, controls); + int response = dialog->run(); + dialog->hide(); + + delete dialog; + + return (response == Gtk::RESPONSE_OK); +} + +/** + \return None + \brief Save a document as a file + \param doc Document to save + \param filename File to save the document as + + This function does a little of the dirty work involved in saving + a document so that the implementation only has to worry about getting + bits on the disk. + + The big thing that it does is remove and read the fields that are + only used at runtime and shouldn't be saved. One that may surprise + people is the output extension. This is not saved so that the IDs + could be changed, and old files will still work properly. +*/ +void +Output::save(SPDocument *doc, gchar const *filename, bool detachbase) +{ + if (!loaded()) + set_state(Extension::STATE_LOADED); + + if (loaded()) { + imp->setDetachBase(detachbase); + auto new_doc = doc->copy(); + imp->save(this, new_doc.get(), filename); + // This shouldn't be needed, but the unique_ptr is not destroying this SPDocument. + delete new_doc.release(); + } +} + +/** + \return None + \brief Save a rendered png as a raster output + \param png_filename source png file. + \param filename File to save the raster as + +*/ +void +Output::export_raster(const SPDocument *doc, std::string png_filename, gchar const *filename, bool detachbase) +{ + if (!loaded()) + set_state(Extension::STATE_LOADED); + + if (loaded()) { + imp->setDetachBase(detachbase); + imp->export_raster(this, doc, png_filename, filename); + } +} + +/** + * Adds a valid extension to the filename if it's missing. + */ +void +Output::add_extension(Glib::ustring &filename) +{ + auto current_ext = Inkscape::IO::get_file_extension(filename); + if (extension && current_ext != extension) { + filename = filename + extension; + } +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/output.h b/src/extension/output.h new file mode 100644 index 0000000..f457326 --- /dev/null +++ b/src/extension/output.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Johan Engelen <johan@shouraizou.nl> + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + + +#ifndef INKSCAPE_EXTENSION_OUTPUT_H__ +#define INKSCAPE_EXTENSION_OUTPUT_H__ + +#include "extension.h" +class SPDocument; + +namespace Inkscape { +namespace Extension { + +class Output : public Extension { + gchar *mimetype; /**< What is the mime type this inputs? */ + gchar *extension; /**< The extension of the input files */ + gchar *filetypename; /**< A userfriendly name for the file type */ + gchar *filetypetooltip; /**< A more detailed description of the filetype */ + bool dataloss; /**< The extension causes data loss on save */ + bool savecopyonly; /**< Limit output option to Save a Copy */ + bool raster; /**< Is the extension expecting a png file */ + bool exported; /**< Is the extension available in the export dialog */ + +public: + class save_failed {}; /**< Generic failure for an undescribed reason */ + class save_cancelled {}; /**< Saving was cancelled */ + class no_extension_found {}; /**< Failed because we couldn't find an extension to match the filename */ + class file_read_only {}; /**< The existing file can not be opened for writing */ + class export_id_not_found { /**< The object ID requested for export could not be found in the document */ + public: + const gchar * const id; + export_id_not_found(const gchar * const id = nullptr) : id{id} {}; + }; + + Output(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Output () override; + + bool check() override; + + void save (SPDocument *doc, + gchar const *filename, + bool detachbase = false); + void export_raster (const SPDocument *doc, + std::string png_filename, + gchar const *filename, + bool detachbase); + bool prefs (); + gchar * get_mimetype(); + gchar * get_extension(); + const char * get_filetypename(bool translated=false); + const char * get_filetypetooltip(bool translated=false); + bool causes_dataloss() { return dataloss; }; + bool savecopy_only() { return savecopyonly; }; + bool is_raster() { return raster; }; + bool is_exported() { return exported; }; + void add_extension(Glib::ustring &filename); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_OUTPUT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/patheffect.cpp b/src/extension/patheffect.cpp new file mode 100644 index 0000000..3ed53e7 --- /dev/null +++ b/src/extension/patheffect.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "patheffect.h" + +#include "db.h" + +#include "object/sp-defs.h" + +#include "xml/repr.h" + + +namespace Inkscape { +namespace Extension { + +PathEffect::PathEffect (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) +{ + +} + +PathEffect::~PathEffect (void) += default; + +void +PathEffect::processPath (SPDocument * /*doc*/, Inkscape::XML::Node * /*path*/, Inkscape::XML::Node * /*def*/) +{ + + +} + +void +PathEffect::processPathEffects (SPDocument * doc, Inkscape::XML::Node * path) +{ + gchar const * patheffectlist = path->attribute("inkscape:path-effects"); + if (patheffectlist == nullptr) + return; + + gchar ** patheffects = g_strsplit(patheffectlist, ";", 128); + Inkscape::XML::Node * defs = doc->getDefs()->getRepr(); + + for (int i = 0; (i < 128) && (patheffects[i] != nullptr); i++) { + gchar * patheffect = patheffects[i]; + + // This is weird, they should all be references... but anyway + if (patheffect[0] != '#') continue; + + Inkscape::XML::Node * prefs = sp_repr_lookup_child(defs, "id", &(patheffect[1])); + if (prefs == nullptr) { + + continue; + } + + gchar const * ext_id = prefs->attribute("extension"); + if (ext_id == nullptr) { + + continue; + } + + Inkscape::Extension::PathEffect * peffect; + peffect = dynamic_cast<Inkscape::Extension::PathEffect *>(Inkscape::Extension::db.get(ext_id)); + if (peffect != nullptr) { + peffect->processPath(doc, path, prefs); + } + } + + g_strfreev(patheffects); + return; +} + + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/patheffect.h b/src/extension/patheffect.h new file mode 100644 index 0000000..b2dd7a0 --- /dev/null +++ b/src/extension/patheffect.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_PATHEFFECT_H__ +#define INKSCAPE_EXTENSION_PATHEFFECT_H__ + +#include "document.h" +#include "extension.h" + +namespace Inkscape { +namespace Extension { + +class PathEffect : public Extension { + +public: + PathEffect(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~PathEffect() override; + + void processPath (SPDocument * doc, + Inkscape::XML::Node * path, + Inkscape::XML::Node * def); + static void processPathEffects (SPDocument * doc, + Inkscape::XML::Node * path); +}; /* PathEffect */ + + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_PATHEFFECT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/CMakeLists.txt b/src/extension/plugins/CMakeLists.txt new file mode 100644 index 0000000..dc15b4a --- /dev/null +++ b/src/extension/plugins/CMakeLists.txt @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +add_subdirectory(grid2) diff --git a/src/extension/plugins/grid2/CMakeLists.txt b/src/extension/plugins/grid2/CMakeLists.txt new file mode 100644 index 0000000..1d23d83 --- /dev/null +++ b/src/extension/plugins/grid2/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(grid_PART_SRCS grid.cpp) + +include_directories( ${CMAKE_BINARY_DIR}/src ) + +add_library(grid2 SHARED EXCLUDE_FROM_ALL ${grid_PART_SRCS}) + +target_link_libraries(grid2 inkscape_base) + diff --git a/src/extension/plugins/grid2/grid.cpp b/src/extension/plugins/grid2/grid.cpp new file mode 100644 index 0000000..5c94726 --- /dev/null +++ b/src/extension/plugins/grid2/grid.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file grid.cpp + + A plug-in to add a grid creation effect into Inkscape. +*/ +/* + * Copyright (C) 2004-2005 Ted Gould <ted@gould.cx> + * Copyright (C) 2007 MenTaLguY <mental@rydia.net> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include <gtkmm/adjustment.h> +#include <gtkmm/spinbutton.h> + +#include "desktop.h" + +#include "document.h" +#include "selection.h" +#include "2geom/geom.h" + +#include "object/sp-object.h" + +#include "svg/path-string.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "util/units.h" + +#include "grid.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool +Grid::load (Inkscape::Extension::Extension */*module*/) +{ + // std::cout << "Hey, I'm Grid, I'm loading!" << std::endl; + return TRUE; +} + +namespace { + +Glib::ustring build_lines(Geom::Rect bounding_area, + Geom::Point const &offset, Geom::Point const &spacing) +{ + + std::cout << "Building lines" << std::endl; + + Geom::Point point_offset(0.0, 0.0); + + SVG::PathString path_data; + + for ( int axis = Geom::X ; axis <= Geom::Y ; ++axis ) { + point_offset[axis] = offset[axis]; + + for (Geom::Point start_point = bounding_area.min(); + start_point[axis] + offset[axis] <= (bounding_area.max())[axis]; + start_point[axis] += spacing[axis]) { + Geom::Point end_point = start_point; + end_point[1-axis] = (bounding_area.max())[1-axis]; + + path_data.moveTo(start_point + point_offset) + .lineTo(end_point + point_offset); + } + } + std::cout << "Path data:" << path_data.c_str() << std::endl; + return path_data; +} + +} // namespace + +/** + \brief This actually draws the grid. + \param module The effect that was called (unused) + \param document What should be edited. +*/ +void +Grid::effect (Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + + std::cout << "Executing effect" << std::endl; + + Inkscape::Selection * selection = ((SPDesktop *)document)->selection; + + Geom::Rect bounding_area = Geom::Rect(Geom::Point(0,0), Geom::Point(100,100)); + if (selection->isEmpty()) { + /* get page size */ + SPDocument * doc = document->doc(); + bounding_area = Geom::Rect( Geom::Point(0,0), + Geom::Point(doc->getWidth().value("px"), doc->getHeight().value("px")) ); + } else { + Geom::OptRect bounds = selection->visualBounds(); + if (bounds) { + bounding_area = *bounds; + } + + gdouble doc_height = (document->doc())->getHeight().value("px"); + Geom::Rect temprec = Geom::Rect(Geom::Point(bounding_area.min()[Geom::X], doc_height - bounding_area.min()[Geom::Y]), + Geom::Point(bounding_area.max()[Geom::X], doc_height - bounding_area.max()[Geom::Y])); + + bounding_area = temprec; + } + + double scale = document->doc()->getDocumentScale().inverse()[Geom::X]; + + bounding_area *= Geom::Scale(scale); + Geom::Point spacings( scale * module->get_param_float("xspacing"), + scale * module->get_param_float("yspacing") ); + gdouble line_width = scale * module->get_param_float("lineWidth"); + Geom::Point offsets( scale * module->get_param_float("xoffset"), + scale * module->get_param_float("yoffset") ); + + Glib::ustring path_data(""); + + path_data = build_lines(bounding_area, offsets, spacings); + Inkscape::XML::Document * xml_doc = document->doc()->getReprDoc(); + + //XML Tree being used directly here while it shouldn't be. + Inkscape::XML::Node * current_layer = static_cast<SPDesktop *>(document)->currentLayer()->getRepr(); + Inkscape::XML::Node * path = xml_doc->createElement("svg:path"); + + path->setAttribute("d", path_data); + + std::ostringstream stringstream; + stringstream << "fill:none;stroke:#000000;stroke-width:" << line_width << "px"; + path->setAttribute("style", stringstream.str()); + + current_layer->appendChild(path); + Inkscape::GC::release(path); +} + +/** \brief A class to make an adjustment that uses Extension params */ +class PrefAdjustment : public Gtk::Adjustment { + /** Extension that this relates to */ + Inkscape::Extension::Extension * _ext; + /** The string which represents the parameter */ + char * _pref; +public: + /** \brief Make the adjustment using an extension and the string + describing the parameter. */ + PrefAdjustment(Inkscape::Extension::Extension * ext, char * pref) : + Gtk::Adjustment(0.0, 0.0, 10.0, 0.1), _ext(ext), _pref(pref) { + this->set_value(_ext->get_param_float(_pref)); + this->signal_value_changed().connect(sigc::mem_fun(this, &PrefAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class PrefAdjustment */ + +/** \brief A function to respond to the value_changed signal from the + adjustment. + + This function just grabs the value from the adjustment and writes + it to the parameter. Very simple, but yet beautiful. +*/ +void +PrefAdjustment::val_changed () +{ + // std::cout << "Value Changed to: " << this->get_value() << std::endl; + _ext->set_param_float(_pref, this->get_value()); + return; +} + +/** \brief A function to get the preferences for the grid + \param module Module which holds the params + \param view Unused today - may get style information in the future. + + Uses AutoGUI for creating the GUI. +*/ +Gtk::Widget * +Grid::prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + SPDocument * current_document = view->doc(); + + auto selected = ((SPDesktop *) view)->getSelection()->items(); + Inkscape::XML::Node * first_select = nullptr; + if (!selected.empty()) { + first_select = selected.front()->getRepr(); + } + + return module->autogui(current_document, first_select, changeSignal); +} + + + + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/grid2/grid.h b/src/extension/plugins/grid2/grid.h new file mode 100644 index 0000000..f6d7d57 --- /dev/null +++ b/src/extension/plugins/grid2/grid.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004-2005 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __GRID_H + +#include "extension/implementation/implementation.h" + + +#include <glib.h> +#include <gmodule.h> +#include "inkscape-version.cpp" + + + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +/** \brief Implementation class of the GIMP gradient plugin. This mostly + just creates a namespace for the GIMP gradient plugin today. +*/ +class Grid : public Inkscape::Extension::Implementation::Implementation { + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + Gtk::Widget * prefs_effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View * view, sigc::signal<void> * changeSignal, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +extern "C" G_MODULE_EXPORT Inkscape::Extension::Implementation::Implementation* GetImplementation() { return new Inkscape::Extension::Internal::Grid(); } +extern "C" G_MODULE_EXPORT const gchar* GetInkscapeVersion() { return Inkscape::version_string; } +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/grid2/libgrid2.inx b/src/extension/plugins/grid2/libgrid2.inx new file mode 100644 index 0000000..db95cd5 --- /dev/null +++ b/src/extension/plugins/grid2/libgrid2.inx @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!-- SPDX-License-Identifier: GPL-2.0-or-later --> +<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> +<_name>Grid2</_name> +<id>org.inkscape.effect.grid2</id> +<param name="lineWidth" _gui-text="Line Width:" type="float">1.0</param> +<param name="xspacing" _gui-text="Horizontal Spacing:" type="float" min="0.1" max="1000">10.0</param> +<param name="yspacing" _gui-text="Vertical Spacing:" type="float" min="0.1" max="1000">10.0</param> +<param name="xoffset" _gui-text="Horizontal Offset:" type="float" min="0.0" max="1000">0.0</param> +<param name="yoffset" _gui-text="Vertical Offset:" type="float" min="0.0" max="1000">0.0</param> +<effect> + <object-type>all</object-type> + <effects-menu> + <submenu _name="Render" > + <submenu _name="Grids" /> + </submenu> + </effects-menu> +<_menu-tip>Draw a path which is a grid</_menu-tip> +</effect> +<plugin name="libgrid2" /> +</inkscape-extension> diff --git a/src/extension/prefdialog/parameter-bool.cpp b/src/extension/prefdialog/parameter-bool.cpp new file mode 100644 index 0000000..609e727 --- /dev/null +++ b/src/extension/prefdialog/parameter-bool.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-bool.h" + +#include <gtkmm/box.h> +#include <gtkmm/checkbutton.h> + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + +namespace Inkscape { +namespace Extension { + +ParamBool::ParamBool(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + if (!strcmp(value, "true")) { + _value = true; + } else if (!strcmp(value, "false")) { + _value = false; + } else { + g_warning("Invalid default value ('%s') for parameter '%s' in extension '%s'", + value, _name, _extension->get_id()); + } + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getBool(pref_name(), _value); +} + +bool ParamBool::set(bool in) +{ + _value = in; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setBool(pref_name(), _value); + + return _value; +} + +bool ParamBool::get() const +{ + return _value; +} + +/** + * A check button which is Param aware. It works with the + * parameter to change it's value as the check button changes + * value. + */ +class ParamBoolCheckButton : public Gtk::CheckButton { +public: + /** + * Initialize the check button. + * This function sets the value of the checkbox to be that of the + * parameter, and then sets up a callback to \c on_toggle. + * + * @param param Which parameter to adjust on changing the check button + */ + ParamBoolCheckButton(ParamBool *param, char *label, sigc::signal<void> *changeSignal) + : Gtk::CheckButton(label) + , _pref(param) + , _changeSignal(changeSignal) { + this->set_active(_pref->get()); + this->signal_toggled().connect(sigc::mem_fun(this, &ParamBoolCheckButton::on_toggle)); + return; + } + + /** + * A function to respond to the check box changing. + * Adjusts the value of the preference to match that in the check box. + */ + void on_toggle (); + +private: + /** Param to change. */ + ParamBool *_pref; + sigc::signal<void> *_changeSignal; +}; + +void ParamBoolCheckButton::on_toggle() +{ + _pref->set(this->get_active()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } + return; +} + +std::string ParamBool::value_to_string() const +{ + if (_value) { + return "true"; + } + return "false"; +} + +Gtk::Widget *ParamBool::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + auto hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + hbox->set_homogeneous(false); + + ParamBoolCheckButton * checkbox = Gtk::manage(new ParamBoolCheckButton(this, _text, changeSignal)); + checkbox->show(); + hbox->pack_start(*checkbox, false, false); + + hbox->show(); + + return dynamic_cast<Gtk::Widget *>(hbox); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-bool.h b/src/extension/prefdialog/parameter-bool.h new file mode 100644 index 0000000..52fe06a --- /dev/null +++ b/src/extension/prefdialog/parameter-bool.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INK_EXTENSION_PARAMBOOL_H +#define SEEN_INK_EXTENSION_PARAMBOOL_H +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +/** + * A boolean parameter. + */ +class ParamBool : public InxParameter { +public: + ParamBool(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** + * Returns the current state/value. + */ + bool get() const; + + /** + * A function to set the state/value. + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place pref_name() is used. + * + * @param in The value to set to + */ + bool set(bool in); + + /** + * Creates a bool check button for a bool parameter. + * Builds a hbox with a label and a check button in it. + */ + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + /** + * Appends 'true' or 'false'. + * @todo investigate. Returning a value that can then be appended would probably work better/safer. + */ + std::string value_to_string() const override; + +private: + /** Internal value. */ + bool _value = true; +}; + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_PARAMBOOL_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-color.cpp b/src/extension/prefdialog/parameter-color.cpp new file mode 100644 index 0000000..b262afd --- /dev/null +++ b/src/extension/prefdialog/parameter-color.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> + * Christopher Brown <audiere@gmail.com> + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-color.h" + +#include <iostream> +#include <sstream> + +#include <gtkmm/box.h> +#include <gtkmm/colorbutton.h> +#include <gtkmm/label.h> + +#include "color.h" +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/color-notebook.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +ParamColor::ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + unsigned int _value = 0x000000ff; // default to black + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = strtoul(value, nullptr, 0); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getUInt(pref_name(), _value); + + _color.setValue(_value); + + _color_changed = _color.signal_changed.connect(sigc::mem_fun(this, &ParamColor::_onColorChanged)); + // TODO: SelectedColor does not properly emit signal_changed after dragging, so we also need the following + _color_released = _color.signal_released.connect(sigc::mem_fun(this, &ParamColor::_onColorChanged)); + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "colorbutton")) { + _mode = COLOR_BUTTON; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +ParamColor::~ParamColor() +{ + _color_changed.disconnect(); + _color_released.disconnect(); +} + +unsigned int ParamColor::set(unsigned int in) +{ + _color.setValue(in); + + return in; +} + +Gtk::Widget *ParamColor::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + if (changeSignal) { + _changeSignal = new sigc::signal<void>(*changeSignal); + } + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + if (_mode == COLOR_BUTTON) { + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + Gdk::RGBA rgba; + rgba.set_red_u (((_color.value() >> 24) & 255) << 8); + rgba.set_green_u(((_color.value() >> 16) & 255) << 8); + rgba.set_blue_u (((_color.value() >> 8) & 255) << 8); + rgba.set_alpha_u(((_color.value() >> 0) & 255) << 8); + + // TODO: It would be nicer to have a custom Gtk::ColorButton() implementation here, + // that wraps an Inkscape::UI::Widget::ColorNotebook into a new dialog + _color_button = Gtk::manage(new Gtk::ColorButton(rgba)); + _color_button->set_title(_text); + _color_button->set_use_alpha(); + _color_button->show(); + hbox->pack_end(*_color_button, false, false); + + _color_button->signal_color_set().connect(sigc::mem_fun(this, &ParamColor::_onColorButtonChanged)); + } else { + Gtk::Widget *selector = Gtk::manage(new Inkscape::UI::Widget::ColorNotebook(_color)); + hbox->pack_start(*selector, true, true, 0); + selector->show(); + } + hbox->show(); + return hbox; + +} + +void ParamColor::_onColorChanged() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setUInt(pref_name(), _color.value()); + + if (_changeSignal) + _changeSignal->emit(); +} + +void ParamColor::_onColorButtonChanged() +{ + Gdk::RGBA rgba = _color_button->get_rgba(); + unsigned int value = ((rgba.get_red_u() >> 8) << 24) + + ((rgba.get_green_u() >> 8) << 16) + + ((rgba.get_blue_u() >> 8) << 8) + + ((rgba.get_alpha_u() >> 8) << 0); + set(value); +} + +std::string ParamColor::value_to_string() const +{ + char value_string[16]; + snprintf(value_string, 16, "%u", _color.value()); + return value_string; +} + +}; /* namespace Extension */ +}; /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-color.h b/src/extension/prefdialog/parameter-color.h new file mode 100644 index 0000000..4fef4b6 --- /dev/null +++ b/src/extension/prefdialog/parameter-color.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_INK_EXTENSION_PARAMCOLOR_H__ +#define SEEN_INK_EXTENSION_PARAMCOLOR_H__ +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" +#include "ui/selected-color.h" + +namespace Gtk { +class Widget; +class ColorButton; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamColor : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, COLOR_BUTTON + }; + + ParamColor(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + ~ParamColor() override; + + /** Returns \c _value, with a \i const to protect it. */ + unsigned int get() const { return _color.value(); } + + unsigned int set(unsigned int in); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + + sigc::signal<void> *_changeSignal; + +private: + void _onColorChanged(); + void _onColorButtonChanged(); + + /** Internal value of this parameter */ + Inkscape::UI::SelectedColor _color; + + sigc::connection _color_changed; + sigc::connection _color_released; + + Gtk::ColorButton *_color_button; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; // class ParamColor + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_PARAMCOLOR_H__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-float.cpp b/src/extension/prefdialog/parameter-float.cpp new file mode 100644 index 0000000..7d8c843 --- /dev/null +++ b/src/extension/prefdialog/parameter-float.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-float.h" + +#include <iomanip> + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> + +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/spin-scale.h" +#include "ui/widget/spinbutton.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +ParamFloat::ParamFloat(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = g_ascii_strtod(value, nullptr); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getDouble(pref_name(), _value); + + // parse and apply limits + const char *min = xml->attribute("min"); + if (min) { + _min = g_ascii_strtod(min, nullptr); + } + + const char *max = xml->attribute("max"); + if (max) { + _max = g_ascii_strtod(max, nullptr); + } + + if (_value < _min) { + _value = _min; + } + + if (_value > _max) { + _value = _max; + } + + // parse precision + const char *precision = xml->attribute("precision"); + if (precision != nullptr) { + _precision = strtol(precision, nullptr, 0); + } + + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "full")) { + _mode = FULL; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set to. + */ +double ParamFloat::set(double in) +{ + _value = in; + if (_value > _max) { + _value = _max; + } + if (_value < _min) { + _value = _min; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble(pref_name(), _value); + + return _value; +} + +std::string ParamFloat::value_to_string() const +{ + static constexpr auto digits10 = std::numeric_limits<double>::digits10; // number of decimal digits that are ensured to be precise + return Glib::ustring::format(std::setprecision(digits10), _value); +} + +/** A class to make an adjustment that uses Extension params. */ +class ParamFloatAdjustment : public Gtk::Adjustment { + /** The parameter to adjust. */ + ParamFloat *_pref; + sigc::signal<void> *_changeSignal; +public: + /** Make the adjustment using an extension and the string + describing the parameter. */ + ParamFloatAdjustment(ParamFloat *param, sigc::signal<void> *changeSignal) + : Gtk::Adjustment(0.0, param->min(), param->max(), 0.1, 1.0, 0) + , _pref(param) + , _changeSignal(changeSignal) { + this->set_value(_pref->get()); + this->signal_value_changed().connect(sigc::mem_fun(this, &ParamFloatAdjustment::val_changed)); + return; + }; + + void val_changed (); +}; /* class ParamFloatAdjustment */ + +/** + * A function to respond to the value_changed signal from the adjustment. + * + * This function just grabs the value from the adjustment and writes + * it to the parameter. Very simple, but yet beautiful. + */ +void ParamFloatAdjustment::val_changed() +{ + _pref->set(this->get_value()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } + return; +} + +/** + * Creates a Float Adjustment for a float parameter. + * + * Builds a hbox with a label and a float adjustment in it. + */ +Gtk::Widget *ParamFloat::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + + auto pfa = new ParamFloatAdjustment(this, changeSignal); + Glib::RefPtr<Gtk::Adjustment> fadjust(pfa); + + if (_mode == FULL) { + + Glib::ustring text; + if (_text != nullptr) + text = _text; + UI::Widget::SpinScale *scale = Gtk::manage(new UI::Widget::SpinScale(text, fadjust, _precision)); + scale->set_size_request(400, -1); + scale->show(); + hbox->pack_start(*scale, true, true); + + } + else if (_mode == DEFAULT) { + + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + auto spin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(fadjust, 0.1, _precision)); + spin->show(); + hbox->pack_start(*spin, false, false); + } + + hbox->show(); + + return dynamic_cast<Gtk::Widget *>(hbox); +} + + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-float.h b/src/extension/prefdialog/parameter-float.h new file mode 100644 index 0000000..a74470a --- /dev/null +++ b/src/extension/prefdialog/parameter-float.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMFLOAT_H_SEEN +#define INK_EXTENSION_PARAMFLOAT_H_SEEN + +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamFloat : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, FULL + }; + + ParamFloat(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** Returns \c _value. */ + double get() const { return _value; } + + double set(double in); + + double max () { return _max; } + + double min () { return _min; } + + double precision () { return _precision; } + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + +private: + /** Internal value. */ + double _value = 0; + + /** limits */ + // TODO: do these defaults make sense or should we be unbounded by default? + double _min = 0; + double _max = 10; + + /** numeric precision (i.e. number of digits) */ + int _precision = 1; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* INK_EXTENSION_PARAMFLOAT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-int.cpp b/src/extension/prefdialog/parameter-int.cpp new file mode 100644 index 0000000..c94b75b --- /dev/null +++ b/src/extension/prefdialog/parameter-int.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-int.h" + +#include <gtkmm/adjustment.h> +#include <gtkmm/box.h> + +#include "preferences.h" + +#include "extension/extension.h" + +#include "ui/widget/spinbutton.h" +#include "ui/widget/spin-scale.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + + +ParamInt::ParamInt(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + if (xml->firstChild()) { + const char *value = xml->firstChild()->content(); + if (value) { + _value = strtol(value, nullptr, 0); + } + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getInt(pref_name(), _value); + + // parse and apply limits + const char *min = xml->attribute("min"); + if (min) { + _min = strtol(min, nullptr, 0); + } + + const char *max = xml->attribute("max"); + if (max) { + _max = strtol(max, nullptr, 0); + } + + if (_value < _min) { + _value = _min; + } + + if (_value > _max) { + _value = _max; + } + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "full")) { + _mode = FULL; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +/** + * A function to set the \c _value. + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set to. + */ +int ParamInt::set(int in) +{ + _value = in; + if (_value > _max) { + _value = _max; + } + if (_value < _min) { + _value = _min; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setInt(pref_name(), _value); + + return _value; +} + +/** A class to make an adjustment that uses Extension params. */ +class ParamIntAdjustment : public Gtk::Adjustment { + /** The parameter to adjust. */ + ParamInt *_pref; + sigc::signal<void> *_changeSignal; +public: + /** Make the adjustment using an extension and the string describing the parameter. */ + ParamIntAdjustment(ParamInt *param, sigc::signal<void> *changeSignal) + : Gtk::Adjustment(0.0, param->min(), param->max(), 1.0, 10.0, 0) + , _pref(param) + , _changeSignal(changeSignal) + { + this->set_value(_pref->get()); + this->signal_value_changed().connect(sigc::mem_fun(this, &ParamIntAdjustment::val_changed)); + }; + + void val_changed (); +}; /* class ParamIntAdjustment */ + +/** + * A function to respond to the value_changed signal from the adjustment. + * + * This function just grabs the value from the adjustment and writes + * it to the parameter. Very simple, but yet beautiful. + */ +void ParamIntAdjustment::val_changed() +{ + _pref->set((int)this->get_value()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } +} + +/** + * Creates a Int Adjustment for a int parameter. + * + * Builds a hbox with a label and a int adjustment in it. + */ +Gtk::Widget * +ParamInt::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + + auto pia = new ParamIntAdjustment(this, changeSignal); + Glib::RefPtr<Gtk::Adjustment> fadjust(pia); + + if (_mode == FULL) { + + Glib::ustring text; + if (_text != nullptr) + text = _text; + UI::Widget::SpinScale *scale = Gtk::manage(new UI::Widget::SpinScale(text, fadjust, 0)); + scale->set_size_request(400, -1); + scale->show(); + hbox->pack_start(*scale, true, true); + } else if (_mode == DEFAULT) { + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, true, true); + + auto spin = Gtk::manage(new Inkscape::UI::Widget::SpinButton(fadjust, 1.0, 0)); + spin->show(); + hbox->pack_start(*spin, false, false); + } + + hbox->show(); + + return dynamic_cast<Gtk::Widget *>(hbox); +} + +std::string ParamInt::value_to_string() const +{ + char value_string[32]; + snprintf(value_string, 32, "%d", _value); + return value_string; +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-int.h b/src/extension/prefdialog/parameter-int.h new file mode 100644 index 0000000..da43eb7 --- /dev/null +++ b/src/extension/prefdialog/parameter-int.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMINT_H_SEEN +#define INK_EXTENSION_PARAMINT_H_SEEN + +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class ParamInt : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, FULL + }; + + ParamInt(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** Returns \c _value. */ + int get() const { return _value; } + + int set(int in); + + int max () { return _max; } + + int min () { return _min; } + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + +private: + /** Internal value. */ + int _value = 0; + + /** limits */ + // TODO: do these defaults make sense or should we be unbounded by default? + int _min = 0; + int _max = 10; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* INK_EXTENSION_PARAMINT_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-notebook.cpp b/src/extension/prefdialog/parameter-notebook.cpp new file mode 100644 index 0000000..2f431db --- /dev/null +++ b/src/extension/prefdialog/parameter-notebook.cpp @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Notebook and NotebookPage parameters for extensions. + */ + +/* + * Authors: + * Johan Engelen <johan@shouraizou.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2006 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-notebook.h" + +#include <unordered_set> + +#include <gtkmm/box.h> +#include <gtkmm/notebook.h> + +#include "preferences.h" + +#include "extension/extension.h" + +#include "xml/node.h" + +namespace Inkscape { +namespace Extension { + + +ParamNotebook::ParamNotebookPage::ParamNotebookPage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read XML tree of page and parse parameters + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + chname++; + } + + if (InxWidget::is_valid_widget_name(chname)) { + InxWidget *widget = InxWidget::make(child_repr, _extension); + if (widget) { + _children.push_back(widget); + } + } else if (child_repr->type() == XML::NodeType::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') in notebook page in extension '%s'.", + chname, _extension->get_id()); + } else if (child_repr->type() != XML::NodeType::COMMENT_NODE){ + g_warning("Invalid child element found in notebook page in extension '%s'.", _extension->get_id()); + } + + child_repr = child_repr->next(); + } + } +} + + +/** + * Creates a notebookpage widget for a notebook. + * + * Builds a notebook page (a vbox) and puts parameters on it. + */ +Gtk::Widget *ParamNotebook::ParamNotebookPage::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box * vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox->set_border_width(GUI_BOX_MARGIN); + vbox->set_spacing(GUI_BOX_SPACING); + + // add parameters onto page (if any) + for (auto child : _children) { + Gtk::Widget *child_widget = child->get_widget(changeSignal); + if (child_widget) { + int indent = child->get_indent(); + child_widget->set_margin_start(indent *GUI_INDENTATION); + vbox->pack_start(*child_widget, false, true, 0); // fill=true does not have an effect here, but allows the + // child to choose to expand by setting hexpand/vexpand + + const char *tooltip = child->get_tooltip(); + if (tooltip) { + child_widget->set_tooltip_text(tooltip); + } + } + } + + vbox->show(); + + return dynamic_cast<Gtk::Widget *>(vbox); +} + +/** End ParamNotebookPage **/ + + + +/** ParamNotebook **/ + +ParamNotebook::ParamNotebook(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read XML tree to add pages (allow _page for backwards compatibility) + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (chname && (!strcmp(chname, INKSCAPE_EXTENSION_NS "page") || + !strcmp(chname, INKSCAPE_EXTENSION_NS "_page") )) { + ParamNotebookPage *page; + page = new ParamNotebookPage(child_repr, ext); + + if (page) { + _children.push_back(page); + } + } else if (child_repr->type() == XML::NodeType::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') for parameter '%s' in extension '%s'. Expected 'page'.", + chname, _name, _extension->get_id()); + } else if (child_repr->type() != XML::NodeType::COMMENT_NODE){ + g_warning("Invalid child element found in parameter '%s' in extension '%s'. Expected 'page'.", + _name, _extension->get_id()); + } + child_repr = child_repr->next(); + } + } + if (_children.empty()) { + g_warning("No (valid) pages for parameter '%s' in extension '%s'", _name, _extension->get_id()); + } + + // check for duplicate page names + std::unordered_set<std::string> names; + for (auto child : _children) { + ParamNotebookPage *page = static_cast<ParamNotebookPage *>(child); + auto ret = names.emplace(page->_name); + if (!ret.second) { + g_warning("Duplicate page name ('%s') for parameter '%s' in extension '%s'.", + page->_name, _name, _extension->get_id()); + } + } + + // get value (initialize with value of first page if pref is empty) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getString(pref_name()); + + if (_value.empty()) { + if (!_children.empty()) { + ParamNotebookPage *first_page = dynamic_cast<ParamNotebookPage *>(_children[0]); + _value = first_page->_name; + } + } +} + + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The number of the page to set as new value. + */ +const Glib::ustring& ParamNotebook::set(const int in) +{ + int i = in < _children.size() ? in : _children.size()-1; + ParamNotebookPage *page = dynamic_cast<ParamNotebookPage *>(_children[i]); + + if (page) { + _value = page->_name; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_name(), _value); + } + + return _value; +} + +std::string ParamNotebook::value_to_string() const +{ + return _value.raw(); +} + + +/** A special category of Gtk::Notebook to handle notebook parameters. */ +class NotebookWidget : public Gtk::Notebook { +private: + ParamNotebook *_pref; +public: + /** + * Build a notebookpage preference for the given parameter. + * @param pref Where to get the string (pagename) from, and where to put it when it changes. + */ + NotebookWidget(ParamNotebook *pref) + : Gtk::Notebook() + , _pref(pref) + , activated(false) + { + // don't have to set the correct page: this is done in ParamNotebook::get_widget hook function + this->signal_switch_page().connect(sigc::mem_fun(this, &NotebookWidget::changed_page)); + } + + void changed_page(Gtk::Widget *page, guint pagenum); + + bool activated; +}; + +/** + * Respond to the selected page of notebook changing. + * This function responds to the changing by reporting it to + * ParamNotebook. The change is only reported when the notebook + * is actually visible. This to exclude 'fake' changes when the + * notebookpages are added or removed. + */ +void NotebookWidget::changed_page(Gtk::Widget * /*page*/, guint pagenum) +{ + if (get_visible()) { + _pref->set((int)pagenum); + } +} + +/** + * Creates a Notebook widget for a notebook parameter. + * + * Builds a notebook and puts pages in it. + */ +Gtk::Widget *ParamNotebook::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + NotebookWidget *notebook = Gtk::manage(new NotebookWidget(this)); + + // add pages (if any) and switch to previously selected page + int current_page = -1; + int selected_page = -1; + for (auto child : _children) { + ParamNotebookPage *page = dynamic_cast<ParamNotebookPage *>(child); + g_assert(child); // A ParamNotebook has only children of type ParamNotebookPage. + // If we receive a non-page child here something is very wrong! + current_page++; + + Gtk::Widget *page_widget = page->get_widget(changeSignal); + + Glib::ustring page_text = page->_text; + if (page->_translatable != NO) { // translate unless explicitly marked untranslatable + page_text = page->get_translation(page_text.c_str()); + } + + notebook->append_page(*page_widget, page_text); + + if (_value == page->_name) { + selected_page = current_page; + } + } + if (selected_page >= 0) { + notebook->set_current_page(selected_page); + } + + notebook->show(); + + return static_cast<Gtk::Widget *>(notebook); +} + + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-notebook.h b/src/extension/prefdialog/parameter-notebook.h new file mode 100644 index 0000000..b64e5c6 --- /dev/null +++ b/src/extension/prefdialog/parameter-notebook.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMNOTEBOOK_H_SEEN +#define INK_EXTENSION_PARAMNOTEBOOK_H_SEEN + +/** \file + * Notebook parameter for extensions. + */ + +/* + * Author: + * Johan Engelen <johan@shouraizou.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2006 Author + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +#include <vector> + +#include <glibmm/ustring.h> + + +namespace Gtk { +class Widget; +} + + +namespace Inkscape { +namespace Extension { + +class Extension; + + +/** A class to represent a notebook parameter of an extension. */ +class ParamNotebook : public InxParameter { +private: + /** Internal value. */ + Glib::ustring _value; + + /** + * A class to represent the pages of a notebook parameter of an extension. + */ + class ParamNotebookPage : public InxParameter { + friend class ParamNotebook; + public: + ParamNotebookPage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + // ParamNotebookPage is not a real parameter (it has no value), so make sure it does not return one + std::string value_to_string() const override { return ""; }; + }; /* class ParamNotebookPage */ + +public: + ParamNotebook(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + + const Glib::ustring& get() { return _value; } + const Glib::ustring& set(const int in); +}; /* class ParamNotebook */ + + + + + +} // namespace Extension +} // namespace Inkscape + +#endif /* INK_EXTENSION_PARAMNOTEBOOK_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-optiongroup.cpp b/src/extension/prefdialog/parameter-optiongroup.cpp new file mode 100644 index 0000000..4652788 --- /dev/null +++ b/src/extension/prefdialog/parameter-optiongroup.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + *extension parameter for options with multiple predefined value choices + * + * Currently implemented as either Gtk::RadioButton or Gtk::ComboBoxText + */ + +/* + * Author: + * Johan Engelen <johan@shouraizou.nl> + * + * Copyright (C) 2006-2007 Johan Engelen + * Copyright (C) 2008 Jon A. Cruz + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-optiongroup.h" + +#include <unordered_set> + +#include <gtkmm/box.h> +#include <gtkmm/comboboxtext.h> +#include <gtkmm/radiobutton.h> + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + + +namespace Inkscape { +namespace Extension { + +ParamOptionGroup::ParamOptionGroup(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // Read valid optiongroup choices from XML tree, i,e. + // - <option> elements + // - <item> elements (for backwards-compatibility with params of type enum) + // - underscored variants of both (for backwards-compatibility) + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (chname && (!strcmp(chname, INKSCAPE_EXTENSION_NS "option") || + !strcmp(chname, INKSCAPE_EXTENSION_NS "_option") || + !strcmp(chname, INKSCAPE_EXTENSION_NS "item") || + !strcmp(chname, INKSCAPE_EXTENSION_NS "_item")) ) { + child_repr->setAttribute("name", "option"); // TODO: hack to allow options to be parameters + child_repr->setAttribute("gui-text", "option"); // TODO: hack to allow options to be parameters + ParamOptionGroupOption *param = new ParamOptionGroupOption(child_repr, ext, this); + choices.push_back(param); + } else if (child_repr->type() == XML::NodeType::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') for parameter '%s' in extension '%s'. Expected 'option'.", + chname, _name, _extension->get_id()); + } else if (child_repr->type() != XML::NodeType::COMMENT_NODE){ + g_warning("Invalid child element found in parameter '%s' in extension '%s'. Expected 'option'.", + _name, _extension->get_id()); + } + child_repr = child_repr->next(); + } + } + if (choices.empty()) { + g_warning("No (valid) choices for parameter '%s' in extension '%s'", _name, _extension->get_id()); + } + + // check for duplicate option texts and values + std::unordered_set<std::string> texts; + std::unordered_set<std::string> values; + for (auto choice : choices) { + auto ret1 = texts.emplace(choice->_text.raw()); + if (!ret1.second) { + g_warning("Duplicate option text ('%s') for parameter '%s' in extension '%s'.", + choice->_text.c_str(), _name, _extension->get_id()); + } + auto ret2 = values.emplace(choice->_value.raw()); + if (!ret2.second) { + g_warning("Duplicate option value ('%s') for parameter '%s' in extension '%s'.", + choice->_value.c_str(), _name, _extension->get_id()); + } + } + + // get value (initialize with value of first choice if pref is empty) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getString(pref_name()); + + if (_value.empty()) { + if (!choices.empty()) { + _value = choices[0]->_value; + } + } + + // parse appearance + // (we support "combo" and "radio"; "minimal" is for backwards-compatibility) + if (_appearance) { + if (!strcmp(_appearance, "combo") || !strcmp(_appearance, "minimal")) { + _mode = COMBOBOX; + } else if (!strcmp(_appearance, "radio")) { + _mode = RADIOBUTTON; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +ParamOptionGroup::~ParamOptionGroup () +{ + // destroy choice strings + for (auto choice : choices) { + delete choice; + } +} + + +/** + * A function to set the \c _value. + * + * This function sets ONLY the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set. + */ +const Glib::ustring& ParamOptionGroup::set(Glib::ustring in) +{ + if (contains(in)) { + _value = in; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_name(), _value.c_str()); + } else { + g_warning("Could not set value ('%s') for parameter '%s' in extension '%s'. Not a valid choice.", + in.c_str(), _name, _extension->get_id()); + } + + return _value; +} + +bool ParamOptionGroup::contains(const Glib::ustring text) const +{ + for (auto choice : choices) { + if (choice->_value == text) { + return true; + } + } + + return false; +} + +std::string ParamOptionGroup::value_to_string() const +{ + return _value.raw(); +} + +/** + * Returns the value for the options label parameter + */ +Glib::ustring ParamOptionGroup::value_from_label(const Glib::ustring label) +{ + Glib::ustring value; + + for (auto choice : choices) { + if (choice->_text == label) { + value = choice->_value; + break; + } + } + + return value; +} + + + +/** A special RadioButton class to use in ParamOptionGroup. */ +class RadioWidget : public Gtk::RadioButton { +private: + ParamOptionGroup *_pref; + sigc::signal<void> *_changeSignal; +public: + RadioWidget(Gtk::RadioButtonGroup& group, const Glib::ustring& label, + ParamOptionGroup *pref, sigc::signal<void> *changeSignal) + : Gtk::RadioButton(group, label) + , _pref(pref) + , _changeSignal(changeSignal) + { + add_changesignal(); + }; + + void add_changesignal() { + this->signal_toggled().connect(sigc::mem_fun(this, &RadioWidget::changed)); + }; + + void changed(); +}; + +/** + * Respond to the selected radiobutton changing. + * + * This function responds to the radiobutton selection changing by grabbing the value + * from the text box and putting it in the parameter. + */ +void RadioWidget::changed() +{ + if (this->get_active()) { + Glib::ustring value = _pref->value_from_label(this->get_label()); + _pref->set(value.c_str()); + } + + if (_changeSignal) { + _changeSignal->emit(); + } +} + + +/** A special ComboBoxText class to use in ParamOptionGroup. */ +class ComboWidget : public Gtk::ComboBoxText { +private: + ParamOptionGroup *_pref; + sigc::signal<void> *_changeSignal; + +public: + ComboWidget(ParamOptionGroup *pref, sigc::signal<void> *changeSignal) + : _pref(pref) + , _changeSignal(changeSignal) + { + this->signal_changed().connect(sigc::mem_fun(this, &ComboWidget::changed)); + } + + ~ComboWidget() override = default; + + void changed(); +}; + +void ComboWidget::changed() +{ + if (_pref) { + Glib::ustring value = _pref->value_from_label(get_active_text()); + _pref->set(value.c_str()); + } + + if (_changeSignal) { + _changeSignal->emit(); + } +} + + + +/** + * Creates the widget for the optiongroup parameter. + */ +Gtk::Widget *ParamOptionGroup::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + auto hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + hbox->pack_start(*label, false, false); + + if (_mode == COMBOBOX) { + ComboWidget *combo = Gtk::manage(new ComboWidget(this, changeSignal)); + + for (auto choice : choices) { + combo->append(choice->_text); + if (choice->_value == _value) { + combo->set_active_text(choice->_text); + } + } + + if (combo->get_active_row_number() == -1) { + combo->set_active(0); + } + + hbox->pack_end(*combo, false, false); + } else if (_mode == RADIOBUTTON) { + label->set_valign(Gtk::ALIGN_START); // align label and first radio + + Gtk::Box *radios = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 0)); + Gtk::RadioButtonGroup group; + + for (auto choice : choices) { + RadioWidget *radio = Gtk::manage(new RadioWidget(group, choice->_text, this, changeSignal)); + radios->pack_start(*radio, true, true); + if (choice->_value == _value) { + radio->set_active(); + } + } + + hbox->pack_end(*radios, false, false); + } + + hbox->show_all(); + return static_cast<Gtk::Widget *>(hbox); +} + + +ParamOptionGroup::ParamOptionGroupOption::ParamOptionGroupOption(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext, + const Inkscape::Extension::ParamOptionGroup *parent) + : InxParameter(xml, ext) +{ + // get content (=label) of option and translate it + const char *text = nullptr; + if (xml->firstChild()) { + text = xml->firstChild()->content(); + } + if (text) { + if (_translatable != NO) { // translate unless explicitly marked untranslatable + _text = get_translation(text); + } else { + _text = text; + } + } else { + g_warning("Missing content in option of parameter '%s' in extension '%s'.", + parent->_name, _extension->get_id()); + } + + // get string value of option + const char *value = xml->attribute("value"); + if (value) { + _value = value; + } else { + if (text) { + const char *name = xml->name(); + if (!strcmp(name, INKSCAPE_EXTENSION_NS "item") || !strcmp(name, INKSCAPE_EXTENSION_NS "_item")) { + _value = text; // use untranslated UI text as value (for backwards-compatibility) + } else { + _value = _text; // use translated UI text as value + } + } else { + g_warning("Missing value for option '%s' of parameter '%s' in extension '%s'.", + _text.c_str(), parent->_name, _extension->get_id()); + } + } +} + + + +} /* namespace Extension */ +} /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-optiongroup.h b/src/extension/prefdialog/parameter-optiongroup.h new file mode 100644 index 0000000..533b131 --- /dev/null +++ b/src/extension/prefdialog/parameter-optiongroup.h @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMOPTIONGROUP_H_SEEN +#define INK_EXTENSION_PARAMOPTIONGROUP_H_SEEN + +/** \file + * extension parameter for options with multiple predefined value choices + * + * Currently implemented as either Gtk::RadioButton or Gtk::ComboBoxText + */ + +/* + * Authors: + * Johan Engelen <johan@shouraizou.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2006-2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +#include <vector> + +#include <glibmm/ustring.h> + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace Extension { + +class Extension; + + + +// \brief A class to represent an optiongroup (option with multiple predefined value choices) parameter of an extension +class ParamOptionGroup : public InxParameter { +public: + enum AppearanceMode { + RADIOBUTTON, COMBOBOX + }; + + ParamOptionGroup(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + ~ParamOptionGroup() override; + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + + Glib::ustring value_from_label(const Glib::ustring label); + + const Glib::ustring& get() const { return _value; } + + const Glib::ustring& set(const Glib::ustring in); + + /** + * @returns true if text is a valid choice for this option group + * @param text string value to check (this is an actual option value, not the user-visible option name!) + */ + bool contains(const Glib::ustring text) const; + +private: + /** \brief Internal value. */ + Glib::ustring _value; + + /** appearance mode **/ + AppearanceMode _mode = RADIOBUTTON; + + /* For internal use only. */ + class ParamOptionGroupOption : public InxParameter { + friend class ParamOptionGroup; + public: + ParamOptionGroupOption(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext, + const Inkscape::Extension::ParamOptionGroup *parent); + private: + Glib::ustring _value; + Glib::ustring _text; + }; + + std::vector<ParamOptionGroupOption *> choices; /**< List of available options for the option group */ +}; /* class ParamOptionGroup */ + + + + + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /*INK_EXTENSION_PARAMOPTIONGROUP_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter-path.cpp b/src/extension/prefdialog/parameter-path.cpp new file mode 100644 index 0000000..83b54fc --- /dev/null +++ b/src/extension/prefdialog/parameter-path.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path parameter for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-path.h" + +#include <boost/algorithm/string/case_conv.hpp> +#include <boost/algorithm/string/join.hpp> + +#include <glibmm/i18n.h> +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> +#include <glibmm/regex.h> + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/dialog.h> +#include <gtkmm/entry.h> +#include <gtkmm/filechoosernative.h> + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + +namespace Inkscape { +namespace Extension { + +ParamPath::ParamPath(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + const char *value = nullptr; + if (xml->firstChild()) { + value = xml->firstChild()->content(); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getString(pref_name()).raw(); + + if (_value.empty() && value) { + _value = value; + } + + // parse selection mode + const char *mode = xml->attribute("mode"); + if (mode) { + if (!strcmp(mode, "file")) { + // this is the default + } else if (!strcmp(mode, "files")) { + _select_multiple = true; + } else if (!strcmp(mode, "folder")) { + _mode = FOLDER; + } else if (!strcmp(mode, "folders")) { + _mode = FOLDER; + _select_multiple = true; + } else if (!strcmp(mode, "file_new")) { + _mode = FILE_NEW; + } else if (!strcmp(mode, "folder_new")) { + _mode = FOLDER_NEW; + } else { + g_warning("Invalid value ('%s') for mode of parameter '%s' in extension '%s'", + mode, _name, _extension->get_id()); + } + } + + // parse filetypes + const char *filetypes = xml->attribute("filetypes"); + if (filetypes) { + _filetypes = Glib::Regex::split_simple("," , filetypes); + } +} + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * @param in The value to set to. + */ +const std::string& ParamPath::set(const std::string &in) +{ + _value = in; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_name(), _value); + + return _value; +} + +std::string ParamPath::value_to_string() const +{ + if (!Glib::path_is_absolute(_value) && !_value.empty()) { + return Glib::build_filename(_extension->get_base_directory(), _value); + } else { + return _value; + } +} + + +/** A special type of Gtk::Entry to handle path parameters. */ +class ParamPathEntry : public Gtk::Entry { +private: + ParamPath *_pref; + sigc::signal<void> *_changeSignal; +public: + /** + * Build a string preference for the given parameter. + * @param pref Where to get the string from, and where to put it + * when it changes. + */ + ParamPathEntry(ParamPath *pref, sigc::signal<void> *changeSignal) + : Gtk::Entry() + , _pref(pref) + , _changeSignal(changeSignal) + { + this->set_text(_pref->get()); + this->signal_changed().connect(sigc::mem_fun(this, &ParamPathEntry::changed_text)); + }; + void changed_text(); +}; + + +/** + * Respond to the text box changing. + * + * This function responds to the box changing by grabbing the value + * from the text box and putting it in the parameter. + */ +void ParamPathEntry::changed_text() +{ + auto data = this->get_text(); + _pref->set(data.c_str()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } +} + +/** + * Creates a text box for the string parameter. + * + * Builds a hbox with a label and a text box in it. + */ +Gtk::Widget *ParamPath::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + hbox->pack_start(*label, false, false); + + ParamPathEntry *textbox = Gtk::manage(new ParamPathEntry(this, changeSignal)); + textbox->show(); + hbox->pack_start(*textbox, true, true); + _entry = textbox; + + Gtk::Button *button = Gtk::manage(new Gtk::Button("…")); + button->show(); + hbox->pack_end(*button, false, false); + button->signal_clicked().connect(sigc::mem_fun(*this, &ParamPath::on_button_clicked)); + + hbox->show(); + + return dynamic_cast<Gtk::Widget *>(hbox); +} + +/** + * Create and show the file chooser dialog when the "…" button is clicked + * Then set the value of the ParamPathEntry holding the current value accordingly + */ +void ParamPath::on_button_clicked() +{ + // set-up action and dialog title according to 'mode' + Gtk::FileChooserAction action; + std::string dialog_title; + if (_mode == FILE) { + // pick the "save" variants here - otherwise the dialog will only accept existing files + action = Gtk::FILE_CHOOSER_ACTION_OPEN; + if (_select_multiple) { + dialog_title = _("Select existing files"); + } else { + dialog_title = _("Select existing file"); + } + } else if (_mode == FOLDER) { + // pick the "create" variant here - otherwise the dialog will only accept existing folders + action = Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER; + if (_select_multiple) { + dialog_title = _("Select existing folders"); + } else { + dialog_title = _("Select existing folder"); + } + } else if (_mode == FILE_NEW) { + action = Gtk::FILE_CHOOSER_ACTION_SAVE; + dialog_title = _("Choose file name"); + } else if (_mode == FOLDER_NEW) { + action = Gtk::FILE_CHOOSER_ACTION_CREATE_FOLDER; + dialog_title = _("Choose folder name"); + } else { + g_assert_not_reached(); + } + + // create file chooser dialog + auto file_chooser = Gtk::FileChooserNative::create(dialog_title + "…", action, _("Select")); + file_chooser->set_select_multiple(_select_multiple); + file_chooser->set_do_overwrite_confirmation(true); + file_chooser->set_create_folders(true); + + // set FileFilter according to 'filetype' attribute + if (!_filetypes.empty() && _mode != FOLDER && _mode != FOLDER_NEW) { + Glib::RefPtr<Gtk::FileFilter> file_filter = Gtk::FileFilter::create(); + + for (auto filetype : _filetypes) { + file_filter->add_pattern(Glib::ustring::compose("*.%1", filetype)); + } + + std::string filter_name = boost::algorithm::join(_filetypes, "+"); + boost::algorithm::to_upper(filter_name); + file_filter->set_name(filter_name); + + file_chooser->add_filter(file_filter); + } + + // set current file/folder suitable for current value + // (use basepath of first filename; relative paths are considered relative to .inx file's location) + if (!_value.empty()) { + std::string first_filename = _value.substr(0, _value.find("|")); + + if (!Glib::path_is_absolute(first_filename)) { + first_filename = Glib::build_filename(_extension->get_base_directory(), first_filename); + } + + std::string dirname = Glib::path_get_dirname(first_filename); + if (Glib::file_test(dirname, Glib::FILE_TEST_IS_DIR)) { + file_chooser->set_current_folder(dirname); + } + + if(_mode == FILE_NEW || _mode == FOLDER_NEW) { + file_chooser->set_current_name(Glib::path_get_basename(first_filename)); + } else { + if (Glib::file_test(first_filename, Glib::FILE_TEST_EXISTS)) { + // TODO: This does not seem to work (at least on Windows) + // file_chooser->set_filename(first_filename); + } + } + } + + // show dialog and parse result + int res = file_chooser->run(); + if (res == Gtk::ResponseType::RESPONSE_ACCEPT) { + std::vector<std::string> filenames = file_chooser->get_filenames(); + std::string filenames_joined = boost::algorithm::join(filenames, "|"); + _entry->set_text(filenames_joined); // let the ParamPathEntry handle the rest (including setting the preference) + } +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-path.h b/src/extension/prefdialog/parameter-path.h new file mode 100644 index 0000000..0fbf911 --- /dev/null +++ b/src/extension/prefdialog/parameter-path.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path parameter for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INK_EXTENSION_PARAM_PATH_H_SEEN +#define INK_EXTENSION_PARAM_PATH_H_SEEN + +#include "parameter.h" + + +namespace Inkscape { +namespace Extension { + +class ParamPathEntry; + +class ParamPath : public InxParameter { +public: + ParamPath(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** \brief Returns \c _value, with a \i const to protect it. */ + const std::string& get() const { return _value; } + const std::string& set(const std::string &in); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + +private: + enum Mode { + FILE, FOLDER, FILE_NEW, FOLDER_NEW + }; + + /** \brief Internal value. */ + std::string _value; + + /** selection mode for the file chooser: files or folders? */ + Mode _mode = FILE; + + /** selection mode for the file chooser: multiple items? */ + bool _select_multiple = false; + + /** filetypes that should be selectable in file chooser */ + std::vector<std::string> _filetypes; + + /** pointer to the parameters text entry + * keep this around, so we can update the value accordingly in \a on_button_clicked() */ + ParamPathEntry *_entry; + + void on_button_clicked(); +}; + + +} // namespace Extension +} // namespace Inkscape + +#endif /* INK_EXTENSION_PARAM_PATH_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter-string.cpp b/src/extension/prefdialog/parameter-string.cpp new file mode 100644 index 0000000..708731a --- /dev/null +++ b/src/extension/prefdialog/parameter-string.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter-string.h" + +#include <gtkmm/box.h> +#include <gtkmm/entry.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/textview.h> +#include <glibmm/regex.h> + +#include "xml/node.h" +#include "extension/extension.h" +#include "preferences.h" + +namespace Inkscape { +namespace Extension { + +ParamString::ParamString(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxParameter(xml, ext) +{ + // get value + const char *value = nullptr; + if (xml->firstChild()) { + value = xml->firstChild()->content(); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + _value = prefs->getString(pref_name()); + + if (_value.empty() && value) { + _value = value; + } + + // translate value + if (!_value.empty()) { + if (_translatable == YES) { // translate only if explicitly marked translatable + _value = get_translation(_value.c_str()); + } + } + + // max-length + const char *max_length = xml->attribute("max-length"); + if (!max_length) { + max_length = xml->attribute("max_length"); // backwards-compatibility with old name (underscore) + } + if (max_length) { + _max_length = strtoul(max_length, nullptr, 0); + } + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "multiline")) { + _mode = MULTILINE; + } else { + g_warning("Invalid value ('%s') for appearance of parameter '%s' in extension '%s'", + _appearance, _name, _extension->get_id()); + } + } +} + +/** + * A function to set the \c _value. + * + * This function sets the internal value, but it also sets the value + * in the preferences structure. To put it in the right place \c pref_name() is used. + * + * To copy the data into _value the old memory must be free'd first. + * It is important to note that \c g_free handles \c NULL just fine. Then + * the passed in value is duplicated using \c g_strdup(). + * + * @param in The value to set to. + */ +const Glib::ustring& ParamString::set(const Glib::ustring in) +{ + _value = in; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_name(), _value); + + return _value; +} + +std::string ParamString::value_to_string() const +{ + return _value.raw(); +} + + + +/** A special type of Gtk::Entry to handle string parameters. */ +class ParamStringEntry : public Gtk::Entry { +private: + ParamString *_pref; + sigc::signal<void> *_changeSignal; +public: + /** + * Build a string preference for the given parameter. + * @param pref Where to get the string from, and where to put it + * when it changes. + */ + ParamStringEntry(ParamString *pref, sigc::signal<void> *changeSignal) + : Gtk::Entry() + , _pref(pref) + , _changeSignal(changeSignal) + { + this->set_text(_pref->get()); + this->set_max_length(_pref->getMaxLength()); //Set the max length - default zero means no maximum + this->signal_changed().connect(sigc::mem_fun(this, &ParamStringEntry::changed_text)); + }; + void changed_text(); +}; + + +/** + * Respond to the text box changing. + * + * This function responds to the box changing by grabbing the value + * from the text box and putting it in the parameter. + */ +void ParamStringEntry::changed_text() +{ + Glib::ustring data = this->get_text(); + _pref->set(data.c_str()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } +} + + + +/** A special type of Gtk::TextView to handle multiline string parameters. */ +class ParamMultilineStringEntry : public Gtk::TextView { +private: + ParamString *_pref; + sigc::signal<void> *_changeSignal; +public: + /** + * Build a string preference for the given parameter. + * @param pref Where to get the string from, and where to put it + * when it changes. + */ + ParamMultilineStringEntry(ParamString *pref, sigc::signal<void> *changeSignal) + : Gtk::TextView() + , _pref(pref) + , _changeSignal(changeSignal) + { + // replace literal '\n' with actual newlines for multiline strings + Glib::ustring value = Glib::Regex::create("\\\\n")->replace_literal(_pref->get(), 0, "\n", (Glib::RegexMatchFlags)0); + + this->get_buffer()->set_text(value); + this->get_buffer()->signal_changed().connect(sigc::mem_fun(this, &ParamMultilineStringEntry::changed_text)); + }; + void changed_text(); +}; + +/** + * Respond to the text box changing. + * + * This function responds to the box changing by grabbing the value + * from the text box and putting it in the parameter. + */ +void ParamMultilineStringEntry::changed_text() +{ + Glib::ustring data = this->get_buffer()->get_text(); + + // always store newlines as literal '\n' + data = Glib::Regex::create("\n")->replace_literal(data, 0, "\\n", (Glib::RegexMatchFlags)0); + + _pref->set(data.c_str()); + if (_changeSignal != nullptr) { + _changeSignal->emit(); + } +} + + + +/** + * Creates a text box for the string parameter. + * + * Builds a hbox with a label and a text box in it. + */ +Gtk::Widget *ParamString::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, GUI_PARAM_WIDGETS_SPACING)); + + Gtk::Label *label = Gtk::manage(new Gtk::Label(_text, Gtk::ALIGN_START)); + label->show(); + box->pack_start(*label, false, false); + + if (_mode == MULTILINE) { + box->set_orientation(Gtk::ORIENTATION_VERTICAL); + + Gtk::ScrolledWindow *textarea = Gtk::manage(new Gtk::ScrolledWindow()); + textarea->set_vexpand(); + textarea->set_shadow_type(Gtk::SHADOW_IN); + + ParamMultilineStringEntry *entry = Gtk::manage(new ParamMultilineStringEntry(this, changeSignal)); + entry->show(); + + textarea->add(*entry); + textarea->show(); + + box->pack_start(*textarea, true, true); + } else { + Gtk::Widget *entry = Gtk::manage(new ParamStringEntry(this, changeSignal)); + entry->show(); + + box->pack_start(*entry, true, true); + } + + box->show(); + + return dynamic_cast<Gtk::Widget *>(box); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/parameter-string.h b/src/extension/prefdialog/parameter-string.h new file mode 100644 index 0000000..3af8311 --- /dev/null +++ b/src/extension/prefdialog/parameter-string.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INK_EXTENSION_PARAMSTRING_H_SEEN +#define INK_EXTENSION_PARAMSTRING_H_SEEN + +/* + * Copyright (C) 2005-2007 Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Jon A. Cruz <jon@joncruz.org> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +#include <glibmm/ustring.h> + + +namespace Inkscape { +namespace Extension { + +class ParamString : public InxParameter { +public: + enum AppearanceMode { + DEFAULT, MULTILINE + }; + + ParamString(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + /** \brief Returns \c _value, with a \i const to protect it. */ + const Glib::ustring& get() const { return _value; } + const Glib::ustring& set(const Glib::ustring in); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + + std::string value_to_string() const override; + + void setMaxLength(int maxLength) { _max_length = maxLength; } + int getMaxLength() { return _max_length; } + +private: + /** \brief Internal value. */ + Glib::ustring _value; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; + + /** \brief Maximum length of the string in characters (zero meaning unlimited). */ + int _max_length = 0; +}; + + +} // namespace Extension +} // namespace Inkscape + +#endif /* INK_EXTENSION_PARAMSTRING_H_SEEN */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/parameter.cpp b/src/extension/prefdialog/parameter.cpp new file mode 100644 index 0000000..b8d8386 --- /dev/null +++ b/src/extension/prefdialog/parameter.cpp @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Parameters for extensions. + */ +/* Author: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005-2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <list> + +#include <glibmm/i18n.h> +#include <sigc++/sigc++.h> + +#include "parameter.h" +#include "parameter-bool.h" +#include "parameter-color.h" +#include "parameter-float.h" +#include "parameter-int.h" +#include "parameter-notebook.h" +#include "parameter-optiongroup.h" +#include "parameter-path.h" +#include "parameter-string.h" +#include "widget.h" +#include "widget-label.h" + +#include "extension/extension.h" + +#include "object/sp-defs.h" + +#include "ui/widget/color-notebook.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + + +// Re-implement ParamDescription for backwards-compatibility, deriving from both, WidgetLabel and InxParameter. +// TODO: Should go away eventually... +class ParamDescription : public virtual WidgetLabel, public virtual InxParameter { +public: + ParamDescription(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : WidgetLabel(xml, ext) + , InxParameter(xml, ext) + {} + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override + { + return this->WidgetLabel::get_widget(changeSignal); + } + + // Well, no, I don't have a value! That's why I should not be an InxParameter! + std::string value_to_string() const override { return ""; } +}; + + + +InxParameter *InxParameter::make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext) +{ + InxParameter *param = nullptr; + + try { + const char *type = in_repr->attribute("type"); + if (!type) { + // we can't create a parameter without type + g_warning("Parameter without type in extension '%s'.", in_ext->get_id()); + } else if(!strcmp(type, "bool") || !strcmp(type, "boolean")) { // support "boolean" for backwards-compatibility + param = new ParamBool(in_repr, in_ext); + } else if (!strcmp(type, "int")) { + param = new ParamInt(in_repr, in_ext); + } else if (!strcmp(type, "float")) { + param = new ParamFloat(in_repr, in_ext); + } else if (!strcmp(type, "string")) { + param = new ParamString(in_repr, in_ext); + } else if (!strcmp(type, "path")) { + param = new ParamPath(in_repr, in_ext); + } else if (!strcmp(type, "description")) { + // support deprecated "description" for backwards-compatibility + in_repr->setAttribute("gui-text", "description"); // TODO: hack to allow descriptions to be parameters + param = new ParamDescription(in_repr, in_ext); + } else if (!strcmp(type, "notebook")) { + in_repr->setAttribute("gui-text", "notebook"); // notebooks have no 'gui-text' (but Parameters need one) + param = new ParamNotebook(in_repr, in_ext); + } else if (!strcmp(type, "optiongroup")) { + param = new ParamOptionGroup(in_repr, in_ext); + } else if (!strcmp(type, "enum")) { // support deprecated "enum" for backwards-compatibility + in_repr->setAttribute("appearance", "combo"); + param = new ParamOptionGroup(in_repr, in_ext); + } else if (!strcmp(type, "color")) { + param = new ParamColor(in_repr, in_ext); + } else { + g_warning("Unknown parameter type ('%s') in extension '%s'", type, in_ext->get_id()); + } + } catch (const param_no_name&) { + } catch (const param_no_text&) { + } + + // Note: param could equal nullptr + return param; +} + +bool InxParameter::get_bool() const +{ + ParamBool const *boolpntr = dynamic_cast<ParamBool const *>(this); + if (!boolpntr) { + throw param_not_bool_param(); + } + return boolpntr->get(); +} + +int InxParameter::get_int() const +{ + ParamInt const *intpntr = dynamic_cast<ParamInt const *>(this); + if (!intpntr) { + // This allows option groups to contain integers. Consider just using this. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + return prefs->getInt(this->pref_name()); + } + return intpntr->get(); +} + +double InxParameter::get_float() const +{ + ParamFloat const *floatpntr = dynamic_cast<ParamFloat const *>(this); + if (!floatpntr) { + throw param_not_float_param(); + } + return floatpntr->get(); +} + +const char *InxParameter::get_string() const +{ + ParamString const *stringpntr = dynamic_cast<ParamString const *>(this); + if (!stringpntr) { + throw param_not_string_param(); + } + return stringpntr->get().c_str(); +} + +const char *InxParameter::get_optiongroup() const +{ + ParamOptionGroup const *param = dynamic_cast<ParamOptionGroup const *>(this); + if (!param) { + throw param_not_optiongroup_param(); + } + return param->get().c_str(); +} + +bool InxParameter::get_optiongroup_contains(const char *value) const +{ + ParamOptionGroup const *param = dynamic_cast<ParamOptionGroup const *>(this); + if (!param) { + throw param_not_optiongroup_param(); + } + return param->contains(value); +} + +unsigned int InxParameter::get_color() const +{ + ParamColor const *param = dynamic_cast<ParamColor const *>(this); + if (!param) { + throw param_not_color_param(); + } + return param->get(); +} + +bool InxParameter::set_bool(bool in) +{ + ParamBool * boolpntr = dynamic_cast<ParamBool *>(this); + if (boolpntr == nullptr) + throw param_not_bool_param(); + return boolpntr->set(in); +} + +int InxParameter::set_int(int in) +{ + ParamInt *intpntr = dynamic_cast<ParamInt *>(this); + if (intpntr == nullptr) + throw param_not_int_param(); + return intpntr->set(in); +} + +double InxParameter::set_float(double in) +{ + ParamFloat * floatpntr; + floatpntr = dynamic_cast<ParamFloat *>(this); + if (floatpntr == nullptr) + throw param_not_float_param(); + return floatpntr->set(in); +} + +const char *InxParameter::set_string(const char *in) +{ + ParamString * stringpntr = dynamic_cast<ParamString *>(this); + if (stringpntr == nullptr) + throw param_not_string_param(); + return stringpntr->set(in).c_str(); +} + +const char *InxParameter::set_optiongroup(const char *in) +{ + ParamOptionGroup *param = dynamic_cast<ParamOptionGroup *>(this); + if (!param) { + throw param_not_optiongroup_param(); + } + return param->set(in).c_str(); +} + +unsigned int InxParameter::set_color(unsigned int in) +{ + ParamColor*param = dynamic_cast<ParamColor *>(this); + if (param == nullptr) + throw param_not_color_param(); + return param->set(in); +} + + +InxParameter::InxParameter(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *ext) + : InxWidget(in_repr, ext) +{ + // name (mandatory for all parameters) + const char *name = in_repr->attribute("name"); + if (name) { + _name = g_strstrip(g_strdup(name)); + } + if (!_name || !strcmp(_name, "")) { + g_warning("Parameter without name in extension '%s'.", _extension->get_id()); + throw param_no_name(); + } + + // gui-text + const char *gui_text = in_repr->attribute("gui-text"); + if (!gui_text) { + gui_text = in_repr->attribute("_gui-text"); // backwards-compatibility with underscored variants + } + if (gui_text) { + if (_translatable != NO) { // translate unless explicitly marked untranslatable + gui_text = get_translation(gui_text); + } + _text = g_strdup(gui_text); + } + if (!_text && !_hidden) { + g_warning("Parameter '%s' in extension '%s' is visible but does not have a 'gui-text'.", + _name, _extension->get_id()); + throw param_no_text(); + } + + // gui-description (optional) + const char *gui_description = in_repr->attribute("gui-description"); + if (!gui_description) { + gui_description = in_repr->attribute("_gui-description"); // backwards-compatibility with underscored variants + } + if (gui_description) { + if (_translatable != NO) { // translate unless explicitly marked untranslatable + gui_description = get_translation(gui_description); + } + _description = g_strdup(gui_description); + } +} + +InxParameter::~InxParameter() +{ + g_free(_name); + _name = nullptr; + + g_free(_text); + _text = nullptr; + + g_free(_description); + _description = nullptr; +} + +Glib::ustring InxParameter::pref_name() const +{ + return Glib::ustring::compose("/extensions/%1.%2", _extension->get_id(), _name); +} + +std::string InxParameter::value_to_string() const +{ + // if we end up here we're missing a definition of ::string() in one of the subclasses + g_critical("InxParameter::value_to_string called from parameter '%s' in extension '%s'", _name, _extension->get_id()); + g_assert_not_reached(); + return ""; +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/parameter.h b/src/extension/prefdialog/parameter.h new file mode 100644 index 0000000..3ae68a3 --- /dev/null +++ b/src/extension/prefdialog/parameter.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Parameters for extensions. + */ +/* Authors: + * Ted Gould <ted@gould.cx> + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 2005-2006 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_PARAM_H__ +#define SEEN_INK_EXTENSION_PARAM_H__ + +#include "widget.h" + + +namespace Glib { +class ustring; +} + + +namespace Inkscape { +namespace Extension { + +/** + * A class to represent the parameter of an extension. + * + * This is really a super class that allows them to abstract all + * the different types of parameters into some that can be passed + * around. There is also a few functions that are used by all the + * different parameters. + */ +class InxParameter : public InxWidget { +public: + InxParameter(Inkscape::XML::Node *in_repr, + Inkscape::Extension::Extension *ext); + + ~InxParameter() override; + + /** Wrapper to cast to the object and use its function. */ + bool get_bool() const; + + /** Wrapper to cast to the object and use it's function. */ + int get_int() const; + + /** Wrapper to cast to the object and use it's function. */ + double get_float() const; + + /** Wrapper to cast to the object and use it's function. */ + const char *get_string() const; + + /** Wrapper to cast to the object and use it's function. */ + const char *get_optiongroup() const; + bool get_optiongroup_contains(const char *value) const; + + /** Wrapper to cast to the object and use it's function. */ + unsigned int get_color() const; + + /** Wrapper to cast to the object and use it's function. */ + bool set_bool(bool in); + + /** Wrapper to cast to the object and use it's function. */ + int set_int(int in); + + /** Wrapper to cast to the object and use it's function. */ + double set_float(double in); + + /** Wrapper to cast to the object and use it's function. */ + const char *set_string(const char *in); + + /** Wrapper to cast to the object and use it's function. */ + const char *set_optiongroup(const char *in); + + /** Wrapper to cast to the object and use it's function. */ + unsigned int set_color(unsigned int in); + + char const *name() const { return _name; } + + /** + * Creates a new extension parameter for usage in a prefdialog. + * + * The type of widget created is parsed from the XML representation passed in, + * and the suitable subclass constructor is called. + * + * Called from base-class method of the same name. + * + * @param in_repr The XML representation describing the widget. + * @param in_ext The extension the widget belongs to. + * @return a pointer to a new Widget if applicable, null otherwise.. + */ + static InxParameter *make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext); + + const char *get_tooltip() const override { return _description; } + + /** + * Gets the current value of the parameter in a string form. + * + * @return String representation of the parameter's value. + * + * \internal Must be implemented by all derived classes. + * Unfortunately it seems we can't make this a pure virtual function, + * as InxParameter is not supposed to be abstract. + */ + virtual std::string value_to_string() const; + + /** Recommended spacing between the widgets making up a single Parameter (e.g. label and input) (in px) */ + const static int GUI_PARAM_WIDGETS_SPACING = 4; + + + /** An error class for when a parameter is called on a type it is not */ + class param_no_name {}; + class param_no_text {}; + class param_not_bool_param {}; + class param_not_color_param {}; + class param_not_float_param {}; + class param_not_int_param {}; + class param_not_optiongroup_param {}; + class param_not_string_param {}; + + +protected: + /** The name of this parameter. */ + char *_name = nullptr; + + /** Parameter text to show as the GUI label. */ + char *_text = nullptr; + + /** Extended description of the parameter (currently shown as tooltip on hover). */ + char *_description = nullptr; + + + /* **** member functions **** */ + + /** + * Build preference name for the current parameter. + * + * Returns a preference name that can be used with setters and getters from Inkscape::Preferences. + * The name is assembled from a fixed root ("/extensions/"), extension ID and parameter name. + * + * @return: Preference name + */ + Glib::ustring pref_name() const; +}; + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_PARAM_H__ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/prefdialog.cpp b/src/extension/prefdialog/prefdialog.cpp new file mode 100644 index 0000000..4c96e4d --- /dev/null +++ b/src/extension/prefdialog/prefdialog.cpp @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2005-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "prefdialog.h" + +#include <gtkmm/checkbutton.h> +#include <gtkmm/separator.h> +#include <glibmm/i18n.h> + +#include "ui/dialog-events.h" +#include "xml/repr.h" + +// Used to get SP_ACTIVE_DESKTOP +#include "inkscape.h" +#include "document.h" +#include "document-undo.h" + +#include "extension/effect.h" +#include "extension/execution-env.h" +#include "extension/implementation/implementation.h" + +#include "parameter.h" + + +namespace Inkscape { +namespace Extension { + + +/** \brief Creates a new preference dialog for extension preferences + \param name Name of the Extension whose dialog this is (should already be translated) + \param controls The extension specific widgets in the dialog + + This function initializes the dialog with the name of the extension + in the title. It adds a few buttons and sets up handlers for + them. It also places the passed-in widgets into the dialog. +*/ +PrefDialog::PrefDialog (Glib::ustring name, Gtk::Widget * controls, Effect * effect) : + Gtk::Dialog(name, true), + _name(name), + _button_ok(nullptr), + _button_cancel(nullptr), + _button_preview(nullptr), + _param_preview(nullptr), + _effect(effect), + _exEnv(nullptr) +{ + this->set_default_size(0,0); // we want the window to be as small as possible instead of clobbering up space + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + if (controls == nullptr) { + if (_effect == nullptr) { + std::cout << "AH!!! No controls and no effect!!!" << std::endl; + return; + } + controls = _effect->get_imp()->prefs_effect(_effect, SP_ACTIVE_DESKTOP, &_signal_param_change, nullptr); + _signal_param_change.connect(sigc::mem_fun(this, &PrefDialog::param_change)); + } + + hbox->pack_start(*controls, true, true, 0); + hbox->show(); + + this->get_content_area()->pack_start(*hbox, true, true, 0); + + _button_cancel = add_button(_effect == nullptr ? _("_Cancel") : _("_Close"), Gtk::RESPONSE_CANCEL); + _button_ok = add_button(_effect == nullptr ? _("_OK") : _("_Apply"), Gtk::RESPONSE_OK); + set_default_response(Gtk::RESPONSE_OK); + _button_ok->grab_focus(); + + if (_effect != nullptr && !_effect->no_live_preview) { + if (_param_preview == nullptr) { + XML::Document * doc = sp_repr_read_mem(live_param_xml, strlen(live_param_xml), nullptr); + if (doc == nullptr) { + std::cout << "Error encountered loading live parameter XML !!!" << std::endl; + return; + } + _param_preview = InxParameter::make(doc->root(), _effect); + } + + auto sep = Gtk::manage(new Gtk::Separator()); + sep->show(); + + this->get_content_area()->pack_start(*sep, false, false, InxWidget::GUI_BOX_SPACING); + + hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + hbox->set_border_width(InxWidget::GUI_BOX_MARGIN); + _button_preview = _param_preview->get_widget(&_signal_preview); + _button_preview->show(); + hbox->pack_start(*_button_preview, true, true, 0); + hbox->show(); + + this->get_content_area()->pack_start(*hbox, false, false, 0); + + Gtk::Box *hbox = dynamic_cast<Gtk::Box *>(_button_preview); + if (hbox != nullptr) { + _checkbox_preview = dynamic_cast<Gtk::CheckButton *>(hbox->get_children().front()); + } + + preview_toggle(); + _signal_preview.connect(sigc::mem_fun(this, &PrefDialog::preview_toggle)); + } + + // Set window modality for effects that don't use live preview + if (_effect != nullptr && _effect->no_live_preview) { + set_modal(false); + } + + return; +} + +PrefDialog::~PrefDialog ( ) +{ + if (_param_preview != nullptr) { + delete _param_preview; + _param_preview = nullptr; + } + + if (_exEnv != nullptr) { + _exEnv->cancel(); + delete _exEnv; + _exEnv = nullptr; + _effect->set_execution_env(_exEnv); + } + + if (_effect != nullptr) { + _effect->set_pref_dialog(nullptr); + } + return; +} + +void +PrefDialog::preview_toggle () { + SPDocument *document = SP_ACTIVE_DOCUMENT; + bool modified = document->isModifiedSinceSave(); + if(_param_preview->get_bool()) { + if (_exEnv == nullptr) { + set_modal(true); + _exEnv = new ExecutionEnv(_effect, SP_ACTIVE_DESKTOP, nullptr, false, false); + _effect->set_execution_env(_exEnv); + _exEnv->run(); + } + } else { + set_modal(false); + if (_exEnv != nullptr) { + _exEnv->cancel(); + _exEnv->undo(); + _exEnv->reselect(); + delete _exEnv; + _exEnv = nullptr; + _effect->set_execution_env(_exEnv); + } + } + document->setModifiedSinceSave(modified); +} + +void +PrefDialog::param_change () { + if (_exEnv != nullptr) { + if (!_effect->loaded()) { + _effect->set_state(Extension::STATE_LOADED); + } + _timersig.disconnect(); + _timersig = Glib::signal_timeout().connect(sigc::mem_fun(this, &PrefDialog::param_timer_expire), + 250, /* ms */ + Glib::PRIORITY_DEFAULT_IDLE); + } + + return; +} + +bool +PrefDialog::param_timer_expire () { + if (_exEnv != nullptr) { + _exEnv->cancel(); + _exEnv->undo(); + _exEnv->reselect(); + _exEnv->run(); + } + + return false; +} + +void +PrefDialog::on_response (int signal) { + if (signal == Gtk::RESPONSE_OK) { + if (_exEnv == nullptr) { + if (_effect != nullptr) { + _effect->effect(SP_ACTIVE_DESKTOP); + } else { + // Shutdown run() + return; + } + } else { + if (_exEnv->wait()) { + _exEnv->commit(); + } else { + _exEnv->undo(); + _exEnv->reselect(); + } + delete _exEnv; + _exEnv = nullptr; + _effect->set_execution_env(_exEnv); + } + } + + if (_param_preview != nullptr) { + _checkbox_preview->set_active(false); + } + + if ((signal == Gtk::RESPONSE_CANCEL || signal == Gtk::RESPONSE_DELETE_EVENT) && _effect != nullptr) { + delete this; + } + return; +} + +#include "extension/internal/clear-n_.h" + +const char * PrefDialog::live_param_xml = "<param name=\"__live_effect__\" type=\"bool\" gui-text=\"" N_("Live preview") "\" gui-description=\"" N_("Is the effect previewed live on canvas?") "\">false</param>"; + +}; }; /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/prefdialog.h b/src/extension/prefdialog/prefdialog.h new file mode 100644 index 0000000..9fe8acd --- /dev/null +++ b/src/extension/prefdialog/prefdialog.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2005,2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_DIALOG_H__ +#define INKSCAPE_EXTENSION_DIALOG_H__ + +#include <gtkmm/dialog.h> +#include <glibmm/value.h> +#include <glibmm/ustring.h> + +namespace Gtk { +class CheckButton; +} + +namespace Inkscape { +namespace Extension { +class Effect; +class ExecutionEnv; +class InxParameter; + +/** \brief A class to represent the preferences for an extension */ +class PrefDialog : public Gtk::Dialog { + /** \brief Name of the extension */ + Glib::ustring _name; + + /** \brief A pointer to the OK button */ + Gtk::Button *_button_ok; + /** \brief A pointer to the CANCEL button */ + Gtk::Button *_button_cancel; + + /** \brief Button to control live preview */ + Gtk::Widget *_button_preview; + /** \brief Checkbox for the preview */ + Gtk::CheckButton *_checkbox_preview; + + /** \brief Parameter to control live preview */ + InxParameter *_param_preview; + + /** \brief XML to define the live effects parameter on the dialog */ + static const char * live_param_xml; + + /** \brief Signal that the user is changing the live effect state */ + sigc::signal<void> _signal_preview; + /** \brief Signal that one of the parameters change */ + sigc::signal<void> _signal_param_change; + + /** \brief If this is the preferences for an effect, the effect + that we're working with. */ + Effect *_effect; + /** \brief If we're executing in preview mode here is the execution + environment for the effect. */ + ExecutionEnv *_exEnv; + + /** \brief The timer used to make it so that parameters don't respond + directly and allows for changes. */ + sigc::connection _timersig; + + void preview_toggle(); + void param_change(); + bool param_timer_expire(); + void on_response (int signal) override; + +public: + PrefDialog (Glib::ustring name, + Gtk::Widget * controls = nullptr, + Effect * effect = nullptr); + ~PrefDialog () override; +}; + + +};}; /* namespace Inkscape, Extension */ + +#endif /* INKSCAPE_EXTENSION_DIALOG_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-box.cpp b/src/extension/prefdialog/widget-box.cpp new file mode 100644 index 0000000..12c7b9e --- /dev/null +++ b/src/extension/prefdialog/widget-box.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Box widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-box.h" + +#include <gtkmm/box.h> + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetBox::WidgetBox(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ + // Decide orientation based on tagname (hbox vs. vbox) + const char *tagname = xml->name(); + if (!strncmp(tagname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + tagname += strlen(INKSCAPE_EXTENSION_NS); + } + if (!strcmp(tagname, "hbox")) { + _orientation = HORIZONTAL; + } else if (!strcmp(tagname, "vbox")) { + _orientation = VERTICAL; + } else { + g_assert_not_reached(); + } + + // Read XML tree of box and parse child widgets + if (xml) { + Inkscape::XML::Node *child_repr = xml->firstChild(); + while (child_repr) { + const char *chname = child_repr->name(); + if (!strncmp(chname, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + chname += strlen(INKSCAPE_EXTENSION_NS); + } + if (chname[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + chname++; + } + + if (InxWidget::is_valid_widget_name(chname)) { + InxWidget *widget = InxWidget::make(child_repr, _extension); + if (widget) { + _children.push_back(widget); + } + } else if (child_repr->type() == XML::NodeType::ELEMENT_NODE) { + g_warning("Invalid child element ('%s') in box widget in extension '%s'.", + chname, _extension->get_id()); + } else if (child_repr->type() != XML::NodeType::COMMENT_NODE){ + g_warning("Invalid child element found in box widget in extension '%s'.", _extension->get_id()); + } + + child_repr = child_repr->next(); + } + } +} + +Gtk::Widget *WidgetBox::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Orientation orientation; + if (_orientation == HORIZONTAL) { + orientation = Gtk::ORIENTATION_HORIZONTAL; + } else { + orientation = Gtk::ORIENTATION_VERTICAL; + } + + Gtk::Box *box = Gtk::manage(new Gtk::Box(orientation)); + // box->set_border_width(GUI_BOX_MARGIN); // leave at zero for now, so box is purely for layouting (not grouping) + // revisit this later, possibly implementing GtkFrame or similar + box->set_spacing(GUI_BOX_SPACING); + + if (_orientation == HORIZONTAL) { + box->set_vexpand(false); + } else { + box->set_hexpand(false); + } + + // add child widgets onto page (if any) + for (auto child : _children) { + Gtk::Widget *child_widget = child->get_widget(changeSignal); + if (child_widget) { + int indent = child->get_indent(); + child_widget->set_margin_start(indent * GUI_INDENTATION); + box->pack_start(*child_widget, false, true, 0); // fill=true does not have an effect here, but allows the + // child to choose to expand by setting hexpand/vexpand + + const char *tooltip = child->get_tooltip(); + if (tooltip) { + child_widget->set_tooltip_text(tooltip); + } + } + } + + box->show(); + + return dynamic_cast<Gtk::Widget *>(box); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-box.h b/src/extension/prefdialog/widget-box.h new file mode 100644 index 0000000..6fd7b90 --- /dev/null +++ b/src/extension/prefdialog/widget-box.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Box widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_BOX_H +#define SEEN_INK_EXTENSION_WIDGET_BOX_H + +#include "widget.h" + +#include <glibmm/ustring.h> + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A box widget */ +class WidgetBox : public InxWidget { +public: + WidgetBox(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; +private: + enum Orientation { + HORIZONTAL, VERTICAL + }; + + /** Layout orientation of the box (default is vertical) **/ + Orientation _orientation = VERTICAL; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_BOX_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-image.cpp b/src/extension/prefdialog/widget-image.cpp new file mode 100644 index 0000000..4f5e11d --- /dev/null +++ b/src/extension/prefdialog/widget-image.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Image widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-image.h" + +#include <glibmm/fileutils.h> +#include <glibmm/miscutils.h> +#include <gtkmm/image.h> + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetImage::WidgetImage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ + std::string image_path; + + // get path to image + const char *content = nullptr; + if (xml->firstChild()) { + content = xml->firstChild()->content(); + } + if (content) { + image_path = content; + } else { + g_warning("Missing path for image widget in extension '%s'.", _extension->get_id()); + return; + } + + // make sure path is absolute (relative paths are relative to .inx file's location) + if (!Glib::path_is_absolute(image_path)) { + image_path = Glib::build_filename(_extension->get_base_directory(), image_path); + } + + // check if image exists + if (Glib::file_test(image_path, Glib::FILE_TEST_IS_REGULAR)) { + _image_path = image_path; + } else { + g_warning("Image file ('%s') not found for image widget in extension '%s'.", + image_path.c_str(), _extension->get_id()); + } + + // parse width/height attributes + const char *width = xml->attribute("width"); + const char *height = xml->attribute("height"); + if (width && height) { + _width = strtoul(width, nullptr, 0); + _height = strtoul(height, nullptr, 0); + } +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetImage::get_widget(sigc::signal<void> * /*changeSignal*/) +{ + if (_hidden || _image_path.empty()) { + return nullptr; + } + + Gtk::Image *image = Gtk::manage(new Gtk::Image(_image_path)); + + // resize if requested + if (_width && _height) { + Glib::RefPtr<Gdk::Pixbuf> pixbuf = image->get_pixbuf(); + pixbuf = pixbuf->scale_simple(_width, _height, Gdk::INTERP_BILINEAR); + image->set(pixbuf); + } + + image->show(); + + return dynamic_cast<Gtk::Widget *>(image); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-image.h b/src/extension/prefdialog/widget-image.h new file mode 100644 index 0000000..95dd4d5 --- /dev/null +++ b/src/extension/prefdialog/widget-image.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Image widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_IMAGE_H +#define SEEN_INK_EXTENSION_WIDGET_IMAGE_H + +#include "widget.h" + +#include <string> + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A label widget */ +class WidgetImage : public InxWidget { +public: + WidgetImage(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; +private: + /** \brief Path to image file (relative paths are relative to the .inx file location). */ + std::string _image_path; + + /** desired width of image when rendered on screen (in px) */ + unsigned int _width = 0; + /** desired height of image when rendered on screen (in px) */ + unsigned int _height = 0; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_IMAGE_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-label.cpp b/src/extension/prefdialog/widget-label.cpp new file mode 100644 index 0000000..56ba0c1 --- /dev/null +++ b/src/extension/prefdialog/widget-label.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Description widget for extensions + *//* + * Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2005-2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-label.h" + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <glibmm/markup.h> +#include <glibmm/regex.h> + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetLabel::WidgetLabel(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ + // construct the text content by concatenating all (non-empty) text nodes, + // removing all other nodes (e.g. comment nodes) and replacing <extension:br> elements with "<br/>" + Inkscape::XML::Node * cur_child = xml->firstChild(); + while (cur_child != nullptr) { + if (cur_child->type() == XML::NodeType::TEXT_NODE && cur_child->content() != nullptr) { + _value += cur_child->content(); + } else if (cur_child->type() == XML::NodeType::ELEMENT_NODE && !g_strcmp0(cur_child->name(), "extension:br")) { + _value += "<br/>"; + } + cur_child = cur_child->next(); + } + + // do replacements in the source string to account for the attribute xml:space="preserve" + // (those should match replacements potentially performed by xgettext to allow for proper translation) + if (g_strcmp0(xml->attribute("xml:space"), "preserve") == 0) { + // xgettext copies the source string verbatim in this case, so no changes needed + } else { + // remove all whitespace from start/end of string and replace intermediate whitespace with a single space + _value = Glib::Regex::create("^\\s+|\\s+$")->replace_literal(_value, 0, "", (Glib::RegexMatchFlags)0); + _value = Glib::Regex::create("\\s+")->replace_literal(_value, 0, " ", (Glib::RegexMatchFlags)0); + } + + // translate value + if (!_value.empty()) { + if (_translatable != NO) { // translate unless explicitly marked untranslatable + _value = get_translation(_value.c_str()); + } + } + + // finally replace all remaining <br/> with a real newline character + _value = Glib::Regex::create("<br/>")->replace_literal(_value, 0, "\n", (Glib::RegexMatchFlags)0); + + // parse appearance + if (_appearance) { + if (!strcmp(_appearance, "header")) { + _mode = HEADER; + } else if (!strcmp(_appearance, "url")) { + _mode = URL; + } else { + g_warning("Invalid value ('%s') for appearance of label widget in extension '%s'", + _appearance, _extension->get_id()); + } + } +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetLabel::get_widget(sigc::signal<void> * /*changeSignal*/) +{ + if (_hidden) { + return nullptr; + } + + Glib::ustring newtext = _value; + + Gtk::Label *label = Gtk::manage(new Gtk::Label()); + if (_mode == HEADER) { + label->set_markup(Glib::ustring("<b>") + Glib::Markup::escape_text(newtext) + Glib::ustring("</b>")); + label->set_margin_top(5); + label->set_margin_bottom(5); + } else if (_mode == URL) { + Glib::ustring escaped_url = Glib::Markup::escape_text(newtext); + label->set_markup(Glib::ustring::compose("<a href='%1'>%1</a>", escaped_url)); + } else { + label->set_text(newtext); + } + label->set_line_wrap(); + label->set_xalign(0); + + // TODO: Ugly "fix" for gtk3 width/height calculation of labels. + // - If not applying any limits long labels will make the window grow horizontally until it uses up + // most of the available space (i.e. most of the screen area) which is ridiculously wide. + // - By using "set_default_size(0,0)" in prefidalog.cpp we tell the window to shrink as much as possible, + // however this can result in a much too narrow dialog instead and a lot of unnecessary wrapping. + // - Here we set a lower limit of GUI_MAX_LINE_LENGTH characters per line that long texts will always use. + // This means texts can not shrink anymore (they can still grow, though) and it's also necessary + // to prevent https://bugzilla.gnome.org/show_bug.cgi?id=773572 + int len = newtext.length(); + label->set_width_chars(len > GUI_MAX_LINE_LENGTH ? GUI_MAX_LINE_LENGTH : len); + + label->show(); + + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + hbox->pack_start(*label, true, true); + hbox->show(); + + return hbox; +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-label.h b/src/extension/prefdialog/widget-label.h new file mode 100644 index 0000000..1c16550 --- /dev/null +++ b/src/extension/prefdialog/widget-label.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Description widget for extensions + *//* + * Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> * + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2005-2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_LABEL_H +#define SEEN_INK_EXTENSION_WIDGET_LABEL_H + +#include "widget.h" + +#include <glibmm/ustring.h> + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A label widget */ +class WidgetLabel : public InxWidget { +public: + enum AppearanceMode { + DEFAULT, HEADER, URL + }; + + WidgetLabel(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; +private: + /** \brief Internal value. */ + Glib::ustring _value; + + /** appearance mode **/ + AppearanceMode _mode = DEFAULT; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_LABEL_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-separator.cpp b/src/extension/prefdialog/widget-separator.cpp new file mode 100644 index 0000000..d78894b --- /dev/null +++ b/src/extension/prefdialog/widget-separator.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Separator widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-separator.h" + +#include <gtkmm/separator.h> + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetSeparator::WidgetSeparator(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetSeparator::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Separator *separator = Gtk::manage(new Gtk::Separator()); + separator->show(); + + return dynamic_cast<Gtk::Widget *>(separator); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-separator.h b/src/extension/prefdialog/widget-separator.h new file mode 100644 index 0000000..9533aba --- /dev/null +++ b/src/extension/prefdialog/widget-separator.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Separator widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H +#define SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H + +#include "widget.h" + +#include <glibmm/ustring.h> + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A separator widget */ +class WidgetSeparator : public InxWidget { +public: + WidgetSeparator(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_SEPARATOR_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget-spacer.cpp b/src/extension/prefdialog/widget-spacer.cpp new file mode 100644 index 0000000..8ffa6ac --- /dev/null +++ b/src/extension/prefdialog/widget-spacer.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Spacer widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "widget-spacer.h" + +#include <gtkmm/box.h> + +#include "xml/node.h" +#include "extension/extension.h" + +namespace Inkscape { +namespace Extension { + + +WidgetSpacer::WidgetSpacer(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext) + : InxWidget(xml, ext) +{ + // get size + const char *size = xml->attribute("size"); + if (size) { + _size = strtol(size, nullptr, 0); + if (_size == 0) { + if (!strcmp(size, "expand")) { + _expand = true; + } else { + g_warning("Invalid value ('%s') for size spacer in extension '%s'", size, _extension->get_id()); + } + } + } +} + +/** \brief Create a label for the description */ +Gtk::Widget *WidgetSpacer::get_widget(sigc::signal<void> *changeSignal) +{ + if (_hidden) { + return nullptr; + } + + Gtk::Box *spacer = Gtk::manage(new Gtk::Box()); + spacer->set_border_width(_size/2); + + if (_expand) { + spacer->set_hexpand(); + spacer->set_vexpand(); + } + + spacer->show(); + + return dynamic_cast<Gtk::Widget *>(spacer); +} + +} /* namespace Extension */ +} /* namespace Inkscape */ diff --git a/src/extension/prefdialog/widget-spacer.h b/src/extension/prefdialog/widget-spacer.h new file mode 100644 index 0000000..467b5f9 --- /dev/null +++ b/src/extension/prefdialog/widget-spacer.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Spacer widget for extensions + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_SPACER_H +#define SEEN_INK_EXTENSION_WIDGET_SPACER_H + +#include "widget.h" + +#include <glibmm/ustring.h> + +namespace Gtk { + class Widget; +} + +namespace Inkscape { +namespace Xml { + class Node; +} + +namespace Extension { + +/** \brief A separator widget */ +class WidgetSpacer : public InxWidget { +public: + WidgetSpacer(Inkscape::XML::Node *xml, Inkscape::Extension::Extension *ext); + + Gtk::Widget *get_widget(sigc::signal<void> *changeSignal) override; + +private: + /** size of the spacer in px */ + int _size = GUI_BOX_MARGIN; + + /** should the spacer be flexible and expand? */ + bool _expand = false; +}; + +} /* namespace Extension */ +} /* namespace Inkscape */ + +#endif /* SEEN_INK_EXTENSION_WIDGET_SPACER_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/prefdialog/widget.cpp b/src/extension/prefdialog/widget.cpp new file mode 100644 index 0000000..18639c5 --- /dev/null +++ b/src/extension/prefdialog/widget.cpp @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Parameters for extensions. + *//* + * Author: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" +#include "widget.h" +#include "widget-box.h" +#include "widget-image.h" +#include "widget-label.h" +#include "widget-separator.h" +#include "widget-spacer.h" + +#include <algorithm> +#include <cstring> + +#include <sigc++/sigc++.h> + +#include "extension/extension.h" + +#include "xml/node.h" + + +namespace Inkscape { +namespace Extension { + +InxWidget *InxWidget::make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext) +{ + InxWidget *widget = nullptr; + + const char *name = in_repr->name(); + if (!strncmp(name, INKSCAPE_EXTENSION_NS_NC, strlen(INKSCAPE_EXTENSION_NS_NC))) { + name += strlen(INKSCAPE_EXTENSION_NS); + } + if (name[0] == '_') { // allow leading underscore in tag names for backwards-compatibility + name++; + } + + // decide on widget type based on tag name + // keep in sync with list of names supported in InxWidget::is_valid_widget_name() below + if (!name) { + // we can't create a widget without name + g_warning("InxWidget without name in extension '%s'.", in_ext->get_id()); + } else if (!strcmp(name, "hbox") || !strcmp(name, "vbox")) { + widget = new WidgetBox(in_repr, in_ext); + } else if (!strcmp(name, "image")) { + widget = new WidgetImage(in_repr, in_ext); + } else if (!strcmp(name, "label")) { + widget = new WidgetLabel(in_repr, in_ext); + } else if (!strcmp(name, "separator")) { + widget = new WidgetSeparator(in_repr, in_ext); + } else if (!strcmp(name, "spacer")) { + widget = new WidgetSpacer(in_repr, in_ext); + } else if (!strcmp(name, "param")) { + widget = InxParameter::make(in_repr, in_ext); + } else { + g_warning("Unknown widget name ('%s') in extension '%s'", name, in_ext->get_id()); + } + + // Note: widget could equal nullptr + return widget; +} + +bool InxWidget::is_valid_widget_name(const char *name) +{ + // keep in sync with names supported in InxWidget::make() above + static const std::vector<std::string> valid_names = + {"hbox", "vbox", "image", "label", "separator", "spacer", "param"}; + + if (std::find(valid_names.begin(), valid_names.end(), name) != valid_names.end()) { + return true; + } else { + return false; + } +} + + +InxWidget::InxWidget(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *ext) + : _extension(ext) +{ + // translatable (optional) + const char *translatable = in_repr->attribute("translatable"); + if (translatable) { + if (!strcmp(translatable, "yes")) { + _translatable = YES; + } else if (!strcmp(translatable, "no")) { + _translatable = NO; + } else { + g_warning("Invalid value ('%s') for translatable attribute of widget '%s' in extension '%s'", + translatable, in_repr->name(), _extension->get_id()); + } + } + + // context (optional) + const char *context = in_repr->attribute("context"); + if (!context) { + context = in_repr->attribute("msgctxt"); // backwards-compatibility with previous name + } + if (context) { + _context = g_strdup(context); + } + + // gui-hidden (optional) + const char *gui_hidden = in_repr->attribute("gui-hidden"); + if (gui_hidden != nullptr) { + if (strcmp(gui_hidden, "true") == 0) { + _hidden = true; + } + } + + // indent (optional) + const char *indent = in_repr->attribute("indent"); + if (indent != nullptr) { + _indent = strtol(indent, nullptr, 0); + } + + // appearance (optional, does not apply to all parameters) + const char *appearance = in_repr->attribute("appearance"); + if (appearance) { + _appearance = g_strdup(appearance); + } +} + +InxWidget::~InxWidget() +{ + for (auto child : _children) { + delete child; + } + + g_free(_context); + _context = nullptr; + + g_free(_appearance); + _appearance = nullptr; +} + +Gtk::Widget * +InxWidget::get_widget(sigc::signal<void> * /*changeSignal*/) +{ + // if we end up here we're missing a definition of ::get_widget() in one of the subclasses + g_critical("InxWidget::get_widget called from widget of type '%s' in extension '%s'", + typeid(this).name(), _extension->get_id()); + g_assert_not_reached(); + return nullptr; +} + +const char *InxWidget::get_translation(const char* msgid) { + return _extension->get_translation(msgid, _context); +} + +void InxWidget::get_widgets(std::vector<InxWidget *> &list) +{ + list.push_back(this); + for (auto child : _children) { + child->get_widgets(list); + } +} + +} // namespace Extension +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/prefdialog/widget.h b/src/extension/prefdialog/widget.h new file mode 100644 index 0000000..843655c --- /dev/null +++ b/src/extension/prefdialog/widget.h @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Base class for extension widgets. + *//* + * Authors: + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INK_EXTENSION_WIDGET_H +#define SEEN_INK_EXTENSION_WIDGET_H + +#include <string> +#include <vector> + +#include <sigc++/sigc++.h> + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace XML { +class Node; +} + +namespace Extension { + +class Extension; + + +/** + * Base class to represent all widgets of an extension (including parameters) + */ +class InxWidget { +public: + InxWidget(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext); + + virtual ~InxWidget(); + + /** + * Creates a new extension widget for usage in a prefdialog. + * + * The type of widget created is parsed from the XML representation passed in, + * and the suitable subclass constructor is called. + * + * For specialized widget types (like parameters) we defer to the subclass function of the same name. + * + * @param in_repr The XML representation describing the widget. + * @param in_ext The extension the widget belongs to. + * @return a pointer to a new Widget if applicable, null otherwise.. + */ + static InxWidget *make(Inkscape::XML::Node *in_repr, Inkscape::Extension::Extension *in_ext); + + /** Checks if name is a valid widget name, i.e. a widget can be constructed from it using make() */ + static bool is_valid_widget_name(const char *name); + + /** Return the instance's GTK::Widget representation for usage in a GUI + * + * @param changeSignal Can be used to subscribe to parameter changes. + * Will be emitted whenever a parameter value changes. + * + * @teturn A Gtk::Widget for the \a InxWidget. \c nullptr if the widget is hidden. + */ + virtual Gtk::Widget *get_widget(sigc::signal<void> *changeSignal); + + virtual const char *get_tooltip() const { return nullptr; } // tool-tips are exclusive to InxParameters for now + + /** Indicates if the widget is hidden or not */ + bool get_hidden() const { return _hidden; } + + /** Indentation level of the widget */ + int get_indent() const { return _indent; } + + + /** + * Recursively construct a list containing the current widget and all of it's child widgets (if it has any) + * + * @param list Reference to a vector of pointers to \a InxWidget that will be appended with the new \a InxWidgets + */ + virtual void get_widgets(std::vector<InxWidget *> &list); + + + /** Recommended margin of boxes containing multiple widgets (in px) */ + const static int GUI_BOX_MARGIN = 10; + /** Recommended spacing between multiple widgets packed into a box (in px) */ + const static int GUI_BOX_SPACING = 4; + /** Recommended indentation width of widgets(in px) */ + const static int GUI_INDENTATION = 12; + /** Recommended maximum line length for wrapping textual wdgets (in chars) */ + const static int GUI_MAX_LINE_LENGTH = 60; + +protected: + enum Translatable { + UNSET, YES, NO + }; + + /** Which extension is this Widget attached to. */ + Inkscape::Extension::Extension *_extension = nullptr; + + /** Child widgets of this widget (might be empty if there are none) */ + std::vector<InxWidget *> _children; + + /** Whether the widget is visible. */ + bool _hidden = false; + + /** Indentation level of the widget. */ + int _indent = 0; + + /** Appearance of the widget (not used by all widgets). */ + char *_appearance = nullptr; + + /** Is widget translatable? */ + Translatable _translatable = UNSET; + + /** context for translation of translatable strings. */ + char *_context = nullptr; + + + /* **** member functions **** */ + + /** gets the gettext translation for msgid + * + * Handles translation domain of the extension and message context of the widget internally + * + * @param msgid String to translate + * @return Translated string + */ + const char *get_translation(const char* msgid); +}; + +} // namespace Extension +} // namespace Inkscape + +#endif // SEEN_INK_EXTENSION_WIDGET_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/extension/print.cpp b/src/extension/print.cpp new file mode 100644 index 0000000..458b6f2 --- /dev/null +++ b/src/extension/print.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "implementation/implementation.h" +#include "print.h" + +/* Inkscape::Extension::Print */ + +namespace Inkscape { +namespace Extension { + +Print::Print (Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory) + : Extension(in_repr, in_imp, base_directory) + , base(nullptr) + , drawing(nullptr) + , root(nullptr) + , dkey(0) +{ +} + +Print::~Print () += default; + +bool +Print::check () +{ + return Extension::check(); +} + +unsigned int +Print::setup () +{ + return imp->setup(this); +} + +unsigned int +Print::set_preview () +{ + return imp->set_preview(this); +} + +unsigned int +Print::begin (SPDocument *doc) +{ + return imp->begin(this, doc); +} + +unsigned int +Print::finish () +{ + return imp->finish(this); +} + +unsigned int +Print::bind (const Geom::Affine &transform, float opacity) +{ + return imp->bind (this, transform, opacity); +} + +unsigned int +Print::release () +{ + return imp->release(this); +} + +unsigned int +Print::fill (Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return imp->fill (this, pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +Print::stroke (Geom::PathVector const &pathv, Geom::Affine const &ctm, SPStyle const *style, + Geom::OptRect const &pbox, Geom::OptRect const &dbox, Geom::OptRect const &bbox) +{ + return imp->stroke (this, pathv, ctm, style, pbox, dbox, bbox); +} + +unsigned int +Print::image (unsigned char *px, unsigned int w, unsigned int h, unsigned int rs, + const Geom::Affine &transform, const SPStyle *style) +{ + return imp->image (this, px, w, h, rs, transform, style); +} + +unsigned int +Print::text (char const *text, Geom::Point const &p, SPStyle const *style) +{ + return imp->text (this, text, p, style); +} + +bool +Print::textToPath () +{ + return imp->textToPath(this); +} + +//whether embed font in print output (EPS especially) +bool +Print::fontEmbedded () +{ + return imp->fontEmbedded(this); +} + +} } /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/print.h b/src/extension/print.h new file mode 100644 index 0000000..c4bc267 --- /dev/null +++ b/src/extension/print.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Ted Gould <ted@gould.cx> + * Abhishek Sharma + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_PRINT_H__ +#define INKSCAPE_EXTENSION_PRINT_H__ + +#include <2geom/affine.h> +#include <2geom/generic-rect.h> +#include <2geom/pathvector.h> +#include <2geom/point.h> +#include "extension.h" + +class SPItem; +class SPStyle; + +namespace Inkscape { + +class Drawing; +class DrawingItem; + +namespace Extension { + +class Print : public Extension { + +public: /* TODO: These are public for the short term, but this should be fixed */ + SPItem *base; + Inkscape::Drawing *drawing; + Inkscape::DrawingItem *root; + unsigned int dkey; + +public: + Print(Inkscape::XML::Node *in_repr, Implementation::Implementation *in_imp, std::string *base_directory); + ~Print() override; + + bool check() override; + + /* FALSE means user hit cancel */ + unsigned int setup (); + unsigned int set_preview (); + + unsigned int begin (SPDocument *doc); + unsigned int finish (); + + /* Rendering methods */ + unsigned int bind (Geom::Affine const &transform, + float opacity); + unsigned int release (); + unsigned int comment (const char * comment); + unsigned int fill (Geom::PathVector const &pathv, + Geom::Affine const &ctm, + SPStyle const *style, + Geom::OptRect const &pbox, + Geom::OptRect const &dbox, + Geom::OptRect const &bbox); + unsigned int stroke (Geom::PathVector const &pathv, + Geom::Affine const &transform, + SPStyle const *style, + Geom::OptRect const &pbox, + Geom::OptRect const &dbox, + Geom::OptRect const &bbox); + unsigned int image (unsigned char *px, + unsigned int w, + unsigned int h, + unsigned int rs, + Geom::Affine const &transform, + SPStyle const *style); + unsigned int text (char const *text, + Geom::Point const &p, + SPStyle const *style); + bool textToPath (); + bool fontEmbedded (); +}; + +} } /* namespace Inkscape, Extension */ +#endif /* INKSCAPE_EXTENSION_PRINT_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/system.cpp b/src/extension/system.cpp new file mode 100644 index 0000000..f4ba2dc --- /dev/null +++ b/src/extension/system.cpp @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is file is kind of the junk file. Basically everything that + * didn't fit in one of the other well defined areas, well, it's now + * here. Which is good in someways, but this file really needs some + * definition. Hopefully that will come ASAP. + * + * Authors: + * Ted Gould <ted@gould.cx> + * Johan Engelen <johan@shouraizou.nl> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2006-2007 Johan Engelen + * Copyright (C) 2002-2004 Ted Gould + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/interface.h" + +#include "system.h" +#include "preferences.h" +#include "extension.h" +#include "db.h" +#include "input.h" +#include "output.h" +#include "effect.h" +#include "patheffect.h" +#include "print.h" +#include "implementation/script.h" +#include "implementation/xslt.h" +#include "xml/rebase-hrefs.h" +#include "io/sys.h" +#include "inkscape.h" +#include "document-undo.h" +#include "loader.h" + +#include <glibmm/miscutils.h> + +namespace Inkscape { +namespace Extension { + +static void open_internal(Inkscape::Extension::Extension *in_plug, gpointer in_data); +static void save_internal(Inkscape::Extension::Extension *in_plug, gpointer in_data); + +/** + * \return A new document created from the filename passed in + * \brief This is a generic function to use the open function of + * a module (including Autodetect) + * \param key Identifier of which module to use + * \param filename The file that should be opened + * + * First things first, are we looking at an autodetection? Well if that's the case then the module + * needs to be found, and that is done with a database lookup through the module DB. The foreach + * function is called, with the parameter being a gpointer array. It contains both the filename + * (to find its extension) and where to write the module when it is found. + * + * If there is no autodetection, then the module database is queried with the key given. + * + * If everything is cool at this point, the module is loaded, and there is possibility for + * preferences. If there is a function, then it is executed to get the dialog to be displayed. + * After it is finished the function continues. + * + * Lastly, the open function is called in the module itself. + */ +SPDocument *open(Extension *key, gchar const *filename) +{ + Input *imod = nullptr; + + if (key == nullptr) { + gpointer parray[2]; + parray[0] = (gpointer)filename; + parray[1] = (gpointer)&imod; + db.foreach(open_internal, (gpointer)&parray); + } else { + imod = dynamic_cast<Input *>(key); + } + + bool last_chance_svg = false; + if (key == nullptr && imod == nullptr) { + last_chance_svg = true; + imod = dynamic_cast<Input *>(db.get(SP_MODULE_KEY_INPUT_SVG)); + } + + if (imod == nullptr) { + throw Input::no_extension_found(); + } + + // Hide pixbuf extensions depending on user preferences. + //g_warning("Extension: %s", imod->get_id()); + + bool show = true; + if (strlen(imod->get_id()) > 21) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool ask = prefs->getBool("/dialogs/import/ask"); + bool ask_svg = prefs->getBool("/dialogs/import/ask_svg"); + Glib::ustring id = Glib::ustring(imod->get_id(), 22); + if (id.compare("org.inkscape.input.svg") == 0) { + if (ask_svg && prefs->getBool("/options/onimport", false)) { + show = true; + imod->set_gui(true); + } else { + show = false; + imod->set_gui(false); + } + } else if(strlen(imod->get_id()) > 27) { + id = Glib::ustring(imod->get_id(), 28); + if (!ask && id.compare( "org.inkscape.input.gdkpixbuf") == 0) { + show = false; + imod->set_gui(false); + } + } + } + imod->set_state(Extension::STATE_LOADED); + + if (!imod->loaded()) { + throw Input::open_failed(); + } + + if (!imod->prefs(filename)) { + throw Input::open_cancelled(); + } + + SPDocument *doc = imod->open(filename); + + if (!doc) { + if (last_chance_svg) { + if ( INKSCAPE.use_gui() ) { + sp_ui_error_dialog(_("Could not detect file format. Tried to open it as an SVG anyway but this also failed.")); + } else { + g_warning("%s", _("Could not detect file format. Tried to open it as an SVG anyway but this also failed.")); + } + } + throw Input::open_failed(); + } + // If last_chance_svg is true here, it means we successfully opened a file as an svg + // and there's no need to warn the user about it, just do it. + + doc->setDocumentFilename(filename); + if (!show) { + imod->set_gui(true); + } + + return doc; +} + +/** + * \return none + * \brief This is the function that searches each module to see + * if it matches the filename for autodetection. + * \param in_plug The module to be tested + * \param in_data An array of pointers containing the filename, and + * the place to put a successfully found module. + * + * Basically this function only looks at input modules as it is part of the open function. If the + * module is an input module, it then starts to take it apart, and the data that is passed in. + * Because the data being passed in is in such a weird format, there are a few casts to make it + * easier to use. While it looks like a lot of local variables, they'll all get removed by the + * compiler. + * + * First thing that is checked is if the filename is shorter than the extension itself. There is + * no way for a match in that case. If it's long enough then there is a string compare of the end + * of the filename (for the length of the extension), and the extension itself. If this passes + * then the pointer passed in is set to the current module. + */ +static void +open_internal(Extension *in_plug, gpointer in_data) +{ + auto imod = dynamic_cast<Input *>(in_plug); + if (imod && !imod->deactivated()) { + gpointer *parray = (gpointer *)in_data; + gchar const *filename = (gchar const *)parray[0]; + Input **pimod = (Input **)parray[1]; + + // skip all the rest if we already found a function to open it + // since they're ordered by preference now. + if (!*pimod) { + gchar const *ext = imod->get_extension(); + + gchar *filenamelower = g_utf8_strdown(filename, -1); + gchar *extensionlower = g_utf8_strdown(ext, -1); + + if (g_str_has_suffix(filenamelower, extensionlower)) { + *pimod = imod; + } + + g_free(filenamelower); + g_free(extensionlower); + } + } + + return; +} + +/** + * \return None + * \brief This is a generic function to use the save function of + * a module (including Autodetect) + * \param key Identifier of which module to use + * \param doc The document to be saved + * \param filename The file that the document should be saved to + * \param official (optional) whether to set :output_module and :modified in the + * document; is true for normal save, false for temporary saves + * + * First things first, are we looking at an autodetection? Well if that's the case then the module + * needs to be found, and that is done with a database lookup through the module DB. The foreach + * function is called, with the parameter being a gpointer array. It contains both the filename + * (to find its extension) and where to write the module when it is found. + * + * If there is no autodetection the module database is queried with the key given. + * + * If everything is cool at this point, the module is loaded, and there is possibility for + * preferences. If there is a function, then it is executed to get the dialog to be displayed. + * After it is finished the function continues. + * + * Lastly, the save function is called in the module itself. + */ +void +save(Extension *key, SPDocument *doc, gchar const *filename, bool check_overwrite, bool official, + Inkscape::Extension::FileSaveMethod save_method) +{ + Output *omod; + if (key == nullptr) { + gpointer parray[2]; + parray[0] = (gpointer)filename; + parray[1] = (gpointer)&omod; + omod = nullptr; + db.foreach(save_internal, (gpointer)&parray); + + /* This is a nasty hack, but it is required to ensure that + autodetect will always save with the Inkscape extensions + if they are available. */ + if (omod != nullptr && !strcmp(omod->get_id(), SP_MODULE_KEY_OUTPUT_SVG)) { + omod = dynamic_cast<Output *>(db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE)); + } + /* If autodetect fails, save as Inkscape SVG */ + if (omod == nullptr) { + // omod = dynamic_cast<Output *>(db.get(SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE)); use exception and let user choose + } + } else { + omod = dynamic_cast<Output *>(key); + } + + if (!dynamic_cast<Output *>(omod)) { + g_warning("Unable to find output module to handle file: %s\n", filename); + throw Output::no_extension_found(); + } + + omod->set_state(Extension::STATE_LOADED); + if (!omod->loaded()) { + throw Output::save_failed(); + } + + if (!omod->prefs()) { + throw Output::save_cancelled(); + } + + gchar *fileName = g_strdup(filename); + + if (check_overwrite && !sp_ui_overwrite_file(fileName)) { + g_free(fileName); + throw Output::no_overwrite(); + } + + // test if the file exists and is writable + // the test only checks the file attributes and might pass where ACL does not allow writes + if (Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS) && !Inkscape::IO::file_is_writable(filename)) { + g_free(fileName); + throw Output::file_read_only(); + } + + Inkscape::XML::Node *repr = doc->getReprRoot(); + + + // remember attributes in case this is an unofficial save and/or overwrite fails + gchar *saved_filename = g_strdup(doc->getDocumentFilename()); + gchar *saved_output_extension = nullptr; + gchar *saved_dataloss = nullptr; + bool saved_modified = doc->isModifiedSinceSave(); + saved_output_extension = g_strdup(get_file_save_extension(save_method).c_str()); + saved_dataloss = g_strdup(repr->attribute("inkscape:dataloss")); + if (official) { + // The document is changing name/uri. + doc->changeFilenameAndHrefs(fileName); + } + + // Update attributes: + { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + // also save the extension for next use + store_file_extension_in_prefs (omod->get_id(), save_method); + // set the "dataloss" attribute if the chosen extension is lossy + repr->removeAttribute("inkscape:dataloss"); + if (omod->causes_dataloss()) { + repr->setAttribute("inkscape:dataloss", "true"); + } + } + doc->setModifiedSinceSave(false); + } + + try { + omod->save(doc, fileName); + } + catch(...) { + // revert attributes in case of official and overwrite + if(check_overwrite && official) { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + store_file_extension_in_prefs (saved_output_extension, save_method); + repr->setAttribute("inkscape:dataloss", saved_dataloss); + } + doc->changeFilenameAndHrefs(saved_filename); + } + doc->setModifiedSinceSave(saved_modified); + // free used resources + g_free(saved_output_extension); + g_free(saved_dataloss); + g_free(saved_filename); + + g_free(fileName); + + throw; + } + + // If it is an unofficial save, set the modified attributes back to what they were. + if ( !official) { + { + DocumentUndo::ScopedInsensitive _no_undo(doc); + store_file_extension_in_prefs (saved_output_extension, save_method); + repr->setAttribute("inkscape:dataloss", saved_dataloss); + } + doc->setModifiedSinceSave(saved_modified); + + g_free(saved_output_extension); + g_free(saved_dataloss); + } + + g_free(fileName); + return; +} + +/** + * \return none + * \brief This is the function that searches each module to see + * if it matches the filename for autodetection. + * \param in_plug The module to be tested + * \param in_data An array of pointers containing the filename, and + * the place to put a successfully found module. + * + * Basically this function only looks at output modules as it is part of the open function. If the + * module is an output module, it then starts to take it apart, and the data that is passed in. + * Because the data being passed in is in such a weird format, there are a few casts to make it + * easier to use. While it looks like a lot of local variables, they'll all get removed by the + * compiler. + * + * First thing that is checked is if the filename is shorter than the extension itself. There is + * no way for a match in that case. If it's long enough then there is a string compare of the end + * of the filename (for the length of the extension), and the extension itself. If this passes + * then the pointer passed in is set to the current module. + */ +static void +save_internal(Extension *in_plug, gpointer in_data) +{ + auto omod = dynamic_cast<Output *>(in_plug); + if (omod && !omod->deactivated()) { + gpointer *parray = (gpointer *)in_data; + gchar const *filename = (gchar const *)parray[0]; + Output **pomod = (Output **)parray[1]; + + // skip all the rest if we already found someone to save it + // since they're ordered by preference now. + if (!*pomod) { + gchar const *ext = omod->get_extension(); + + gchar *filenamelower = g_utf8_strdown(filename, -1); + gchar *extensionlower = g_utf8_strdown(ext, -1); + + if (g_str_has_suffix(filenamelower, extensionlower)) { + *pomod = omod; + } + + g_free(filenamelower); + g_free(extensionlower); + } + } + + return; +} + +Print * +get_print(gchar const *key) +{ + return dynamic_cast<Print *>(db.get(key)); +} + +/** + * \return true if extension successfully parsed, false otherwise + * A true return value does not guarantee an extension was actually registered, + * but indicates no errors occurred while parsing the extension. + * \brief Creates a module from a Inkscape::XML::Document describing the module + * \param doc The XML description of the module + * + * This function basically has two segments. The first is that it goes through the Repr tree + * provided, and determines what kind of module this is, and what kind of implementation to use. + * All of these are then stored in two enums that are defined in this function. This makes it + * easier to add additional types (which will happen in the future, I'm sure). + * + * Second, there is case statements for these enums. The first one is the type of module. This is + * the one where the module is actually created. After that, then the implementation is applied to + * get the load and unload functions. If there is no implementation then these are not set. This + * case could apply to modules that are built in (like the SVG load/save functions). + */ +bool +build_from_reprdoc(Inkscape::XML::Document *doc, Implementation::Implementation *in_imp, std::string* baseDir) +{ + ModuleImpType module_implementation_type = MODULE_UNKNOWN_IMP; + ModuleFuncType module_functional_type = MODULE_UNKNOWN_FUNC; + + g_return_val_if_fail(doc != nullptr, false); + + Inkscape::XML::Node *repr = doc->root(); + + if (strcmp(repr->name(), INKSCAPE_EXTENSION_NS "inkscape-extension")) { + g_warning("Extension definition started with <%s> instead of <" INKSCAPE_EXTENSION_NS "inkscape-extension>. Extension will not be created. See http://wiki.inkscape.org/wiki/index.php/Extensions for reference.\n", repr->name()); + return false; + } + + Inkscape::XML::Node *child_repr = repr->firstChild(); + while (child_repr != nullptr) { + char const *element_name = child_repr->name(); + /* printf("Child: %s\n", child_repr->name()); */ + if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "input")) { + module_functional_type = MODULE_INPUT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "output")) { + module_functional_type = MODULE_OUTPUT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "effect")) { + module_functional_type = MODULE_FILTER; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "print")) { + module_functional_type = MODULE_PRINT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "path-effect")) { + module_functional_type = MODULE_PATH_EFFECT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "script")) { + module_implementation_type = MODULE_EXTENSION; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "xslt")) { + module_implementation_type = MODULE_XSLT; + } else if (!strcmp(element_name, INKSCAPE_EXTENSION_NS "plugin")) { + module_implementation_type = MODULE_PLUGIN; + } + + //Inkscape::XML::Node *old_repr = child_repr; + child_repr = child_repr->next(); + //Inkscape::GC::release(old_repr); + } + + Implementation::Implementation *imp; + if (in_imp == nullptr) { + switch (module_implementation_type) { + case MODULE_EXTENSION: { + Implementation::Script *script = new Implementation::Script(); + imp = static_cast<Implementation::Implementation *>(script); + break; + } + case MODULE_XSLT: { + Implementation::XSLT *xslt = new Implementation::XSLT(); + imp = static_cast<Implementation::Implementation *>(xslt); + break; + } + case MODULE_PLUGIN: { + Inkscape::Extension::Loader loader = Inkscape::Extension::Loader(); + if( baseDir != nullptr){ + loader.set_base_directory ( *baseDir ); + } + imp = loader.load_implementation(doc); + break; + } + default: { + imp = nullptr; + break; + } + } + } else { + imp = in_imp; + } + + Extension *module = nullptr; + try { + switch (module_functional_type) { + case MODULE_INPUT: { + module = new Input(repr, imp, baseDir); + break; + } + case MODULE_OUTPUT: { + module = new Output(repr, imp, baseDir); + break; + } + case MODULE_FILTER: { + module = new Effect(repr, imp, baseDir); + break; + } + case MODULE_PRINT: { + module = new Print(repr, imp, baseDir); + break; + } + case MODULE_PATH_EFFECT: { + module = new PathEffect(repr, imp, baseDir); + break; + } + default: { + g_warning("Extension of unknown type!"); // TODO: Should not happen! Is this even useful? + module = new Extension(repr, imp, baseDir); + break; + } + } + } catch (const Extension::extension_no_id& e) { + g_warning("Building extension failed. Extension does not have a valid ID"); + } catch (const Extension::extension_no_name& e) { + g_warning("Building extension failed. Extension does not have a valid name"); + } catch (const Extension::extension_not_compatible& e) { + return true; // This is not an actual error; just silently ignore the extension + } + + if (module) { + return true; + } + + return false; +} + +/** + * \brief This function creates a module from a filename of an + * XML description. + * \param filename The file holding the XML description of the module. + * + * This function calls build_from_reprdoc with using sp_repr_read_file to create the reprdoc. + */ +void +build_from_file(gchar const *filename) +{ + std::string dir = Glib::path_get_dirname(filename); + + Inkscape::XML::Document *doc = sp_repr_read_file(filename, INKSCAPE_EXTENSION_URI); + if (!doc) { + g_critical("Inkscape::Extension::build_from_file() - XML description loaded from '%s' not valid.", filename); + return; + } + + if (!build_from_reprdoc(doc, nullptr, &dir)) { + g_warning("Inkscape::Extension::build_from_file() - Could not parse extension from '%s'.", filename); + } + + Inkscape::GC::release(doc); +} + +/** + * \brief This function creates a module from a buffer holding an + * XML description. + * \param buffer The buffer holding the XML description of the module. + * + * This function calls build_from_reprdoc with using sp_repr_read_mem to create the reprdoc. It + * finds the length of the buffer using strlen. + */ +void +build_from_mem(gchar const *buffer, Implementation::Implementation *in_imp) +{ + Inkscape::XML::Document *doc = sp_repr_read_mem(buffer, strlen(buffer), INKSCAPE_EXTENSION_URI); + if (!doc) { + g_critical("Inkscape::Extension::build_from_mem() - XML description loaded from memory buffer not valid."); + return; + } + + if (!build_from_reprdoc(doc, in_imp, nullptr)) { + g_critical("Inkscape::Extension::build_from_mem() - Could not parse extension from memory buffer."); + } + + Inkscape::GC::release(doc); +} + +/* + * TODO: Is it guaranteed that the returned extension is valid? If so, we can remove the check for + * filename_extension in sp_file_save_dialog(). + */ +Glib::ustring +get_file_save_extension (Inkscape::Extension::FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring extension; + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + extension = prefs->getString("/dialogs/save_as/default"); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + extension = prefs->getString("/dialogs/save_copy/default"); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + extension = SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE; + break; + case FILE_SAVE_METHOD_EXPORT: + /// \todo no default extension set for Export? defaults to SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE is ok? + break; + } + + if(extension.empty()) { + extension = SP_MODULE_KEY_OUTPUT_SVG_INKSCAPE; + } + + return extension; +} + +Glib::ustring +get_file_save_path (SPDocument *doc, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring path; + bool use_current_dir = true; + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + { + use_current_dir = prefs->getBool("/dialogs/save_as/use_current_dir", true); + if (doc->getDocumentFilename() && use_current_dir) { + path = Glib::path_get_dirname(doc->getDocumentFilename()); + } else { + path = prefs->getString("/dialogs/save_as/path"); + } + break; + } + case FILE_SAVE_METHOD_TEMPORARY: + path = prefs->getString("/dialogs/save_as/path"); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + use_current_dir = prefs->getBool("/dialogs/save_copy/use_current_dir", prefs->getBool("/dialogs/save_as/use_current_dir", true)); + if (doc->getDocumentFilename() && use_current_dir) { + path = Glib::path_get_dirname(doc->getDocumentFilename()); + } else { + path = prefs->getString("/dialogs/save_copy/path"); + } + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + if (doc->getDocumentFilename()) { + path = Glib::path_get_dirname(doc->getDocumentFilename()); + } else { + // FIXME: should we use the save_as path here or something else? Maybe we should + // leave this as a choice to the user. + path = prefs->getString("/dialogs/save_as/path"); + } + break; + case FILE_SAVE_METHOD_EXPORT: + /// \todo no default path set for Export? + // defaults to g_get_home_dir() + break; + } + + if(path.empty()) { + path = g_get_home_dir(); // Is this the most sensible solution? Note that we should avoid + // g_get_current_dir because this leads to problems on OS X where + // Inkscape opens the dialog inside application bundle when it is + // invoked for the first teim. + } + + return path; +} + +void +store_file_extension_in_prefs (Glib::ustring extension, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + prefs->setString("/dialogs/save_as/default", extension); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + prefs->setString("/dialogs/save_copy/default", extension); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + case FILE_SAVE_METHOD_EXPORT: + // do nothing + break; + } +} + +void +store_save_path_in_prefs (Glib::ustring path, FileSaveMethod method) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + switch (method) { + case FILE_SAVE_METHOD_SAVE_AS: + case FILE_SAVE_METHOD_TEMPORARY: + prefs->setString("/dialogs/save_as/path", path); + break; + case FILE_SAVE_METHOD_SAVE_COPY: + prefs->setString("/dialogs/save_copy/path", path); + break; + case FILE_SAVE_METHOD_INKSCAPE_SVG: + case FILE_SAVE_METHOD_EXPORT: + // do nothing + break; + } +} + +} } /* namespace Inkscape::Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/system.h b/src/extension/system.h new file mode 100644 index 0000000..937cdde --- /dev/null +++ b/src/extension/system.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * This is file is kind of the junk file. Basically everything that + * didn't fit in one of the other well defined areas, well, it's now + * here. Which is good in someways, but this file really needs some + * definition. Hopefully that will come ASAP. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2002-2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_SYSTEM_H__ +#define INKSCAPE_EXTENSION_SYSTEM_H__ + +#include <glibmm/ustring.h> + +class SPDocument; + +namespace Inkscape { + +namespace Extension { +class Extension; +class Print; + +namespace Implementation { +class Implementation; +} + +/** + * Used to distinguish between the various invocations of the save dialogs (and thus to determine + * the file type and save path offered in the dialog) + */ +enum FileSaveMethod { + FILE_SAVE_METHOD_SAVE_AS, + FILE_SAVE_METHOD_SAVE_COPY, + FILE_SAVE_METHOD_EXPORT, + // Fallback for special cases (e.g., when saving a document for the first time or after saving + // it in a lossy format) + FILE_SAVE_METHOD_INKSCAPE_SVG, + // For saving temporary files; we return the same data as for FILE_SAVE_METHOD_SAVE_AS + FILE_SAVE_METHOD_TEMPORARY, +}; + +SPDocument *open(Extension *key, gchar const *filename); +void save(Extension *key, SPDocument *doc, gchar const *filename, + bool check_overwrite, bool official, + Inkscape::Extension::FileSaveMethod save_method); +Print *get_print(gchar const *key); +void build_from_file(gchar const *filename); +void build_from_mem(gchar const *buffer, Implementation::Implementation *in_imp); + +/** + * Determine the desired default file extension depending on the given file save method. + * The returned string is guaranteed to be non-empty. + * + * @param method the file save method of the dialog + * @return the corresponding default file extension + */ +Glib::ustring get_file_save_extension (FileSaveMethod method); + +/** + * Determine the desired default save path depending on the given FileSaveMethod. + * The returned string is guaranteed to be non-empty. + * + * @param method the file save method of the dialog + * @param doc the file's document + * @return the corresponding default save path + */ +Glib::ustring get_file_save_path (SPDocument *doc, FileSaveMethod method); + +/** + * Write the given file extension back to prefs so that it can be used later on. + * + * @param extension the file extension which should be written to prefs + * @param method the file save method of the dialog + */ +void store_file_extension_in_prefs (Glib::ustring extension, FileSaveMethod method); + +/** + * Write the given path back to prefs so that it can be used later on. + * + * @param path the path which should be written to prefs + * @param method the file save method of the dialog + */ +void store_save_path_in_prefs (Glib::ustring path, FileSaveMethod method); + +} } /* namespace Inkscape::Extension */ + +#endif /* INKSCAPE_EXTENSION_SYSTEM_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/timer.cpp b/src/extension/timer.cpp new file mode 100644 index 0000000..4533b0d --- /dev/null +++ b/src/extension/timer.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Here is where the extensions can get timed on when they load and + * unload. All of the timing is done in here. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/main.h> + +#include "extension.h" +#include "timer.h" + +namespace Inkscape { +namespace Extension { + +#define TIMER_SCALE_VALUE 20 + +ExpirationTimer * ExpirationTimer::timer_list = nullptr; +ExpirationTimer * ExpirationTimer::idle_start = nullptr; +long ExpirationTimer::timeout = 240; +bool ExpirationTimer::timer_started = false; + +/** \brief Create a new expiration timer + \param in_extension Which extension this timer is related to + + This function creates the timer, and sets the time to the current + time, plus what ever the current timeout is. Also, if this is + the first timer extension, the timer is kicked off. This function + also sets up the circularly linked list of all the timers. +*/ +ExpirationTimer::ExpirationTimer (Extension * in_extension): + locked(0), + extension(in_extension) +{ + /* Fix Me! */ + if (timer_list == nullptr) { + next = this; + timer_list = this; + } else { + next = timer_list->next; + timer_list->next = this; + } + + expiration = Glib::DateTime::create_now_utc().add_seconds(timeout); + + if (!timer_started) { + Glib::signal_timeout().connect(sigc::ptr_fun(&timer_func), timeout * 1000 / TIMER_SCALE_VALUE); + timer_started = true; + } + + return; +} + +/** \brief Deletes a \c ExpirationTimer + + The most complex thing that this function does is remove the timer + from the circularly linked list. If this is the only entry in the + list that is easy, otherwise all the entries must be found, and this + one removed from the list. +*/ +ExpirationTimer::~ExpirationTimer() +{ + if (this != next) { + /* This will remove this entry from the circularly linked + list. */ + ExpirationTimer * prev; + for (prev = timer_list; + prev->next != this; + prev = prev->next){}; + prev->next = next; + + if (idle_start == this) + idle_start = next; + + /* This may occur more than you think, just because the guy + doing most of the deletions is the idle function, who tracks + where it is looking using the \c timer_list variable. */ + if (timer_list == this) + timer_list = next; + } else { + /* If we're the only entry in the list, the list needs to go + to NULL */ + timer_list = nullptr; + idle_start = nullptr; + } + + return; +} + +/** \brief Touches the timer to extend the length before it expires + + Basically it adds more time to the timer. One thing that is kinda + tricky is that it adds half the time remaining back into the timer. + This allows for some extensions that are used regularly to having + extended expiration times. So, in the end, they stay loaded longer. + Extensions that are only used once will expire at a standard rate + set by \c timeout. +*/ +void +ExpirationTimer::touch () +{ + auto const current = Glib::DateTime::create_now_utc(); + + auto time_left = expiration.difference(current); + if (time_left < 0) time_left = 0; + time_left /= 2; + + expiration = current.add(time_left).add_seconds(timeout); + return; +} + +/** \brief Check to see if the timer has expired + + Checks the time against the current time. +*/ +bool +ExpirationTimer::expired () const +{ + if (locked > 0) return false; + + auto const current = Glib::DateTime::create_now_utc(); + return expiration.difference(current) < 0; +} + +// int idle_cnt = 0; + +/** \brief This function goes in the idle loop to find expired extensions + \return Whether the function should be requeued or not + + This function first insures that there is a timer list, and then checks + to see if the one on the top of the list has expired. If it has + expired it unloads the module. By unloading the module, the timer + gets deleted (happens in the unload function). If the list is + no empty, the function returns that it should be dequeued and sets + the \c timer_started variable so that the timer will be reissued when + a timer is added. If there is entries left, but the next one is + where this function started, then the timer is set up. The timer + will then re-add the idle loop function when it runs. +*/ +bool +ExpirationTimer::idle_func () +{ + // std::cout << "Idle func pass: " << idle_cnt++ << " timer list: " << timer_list << std::endl; + + /* see if this is the last */ + if (timer_list == nullptr) { + timer_started = false; + return false; + } + + /* evaluate current */ + if (timer_list->expired()) { + timer_list->extension->set_state(Extension::STATE_UNLOADED); + } + + /* see if this is the last */ + if (timer_list == nullptr) { + timer_started = false; + return false; + } + + if (timer_list->next == idle_start) { + /* if so, set up the timer and return FALSE */ + /* Note: This may cause one to be missed on the evaluation if + the one before it expires and it is last in the list. + While this could be taken care of, it isn't worth the + complexity for this lazy removal that we're doing. It + should get picked up next time */ + Glib::signal_timeout().connect(sigc::ptr_fun(&timer_func), timeout * 1000 / TIMER_SCALE_VALUE); + return false; + } + + /* If nothing else, continue on */ + timer_list = timer_list->next; + return true; +} + +/** \brief A timer function to set up the idle function + \return Always false -- to disable the timer + + This function sets up the idle loop when it runs. The idle loop is + the one that unloads all the extensions. +*/ +bool +ExpirationTimer::timer_func () +{ + // std::cout << "Timer func" << std::endl; + idle_start = timer_list; + // idle_cnt = 0; + Glib::signal_idle().connect(sigc::ptr_fun(&idle_func)); + return false; +} + +}; }; /* namespace Inkscape, Extension */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/timer.h b/src/extension/timer.h new file mode 100644 index 0000000..ce9c495 --- /dev/null +++ b/src/extension/timer.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Here is where the extensions can get timed on when they load and + * unload. All of the timing is done in here. + * + * Authors: + * Ted Gould <ted@gould.cx> + * + * Copyright (C) 2004 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_EXTENSION_TIMER_H__ +#define INKSCAPE_EXTENSION_TIMER_H__ + +#include <cstddef> +#include <sigc++/sigc++.h> +#include <glibmm/datetime.h> + +namespace Inkscape { +namespace Extension { + +class Extension; + +class ExpirationTimer { + /** \brief Circularly linked list of all timers */ + static ExpirationTimer * timer_list; + /** \brief Which timer was on top when we started the idle loop */ + static ExpirationTimer * idle_start; + /** \brief What the current timeout is (in seconds) */ + static long timeout; + /** \brief Has the timer been started? */ + static bool timer_started; + + /** \brief Is this extension locked from being unloaded? */ + int locked; + /** \brief Next entry in the list */ + ExpirationTimer * next; + /** \brief When this timer expires */ + Glib::DateTime expiration; + /** \brief What extension this function relates to */ + Extension * extension; + + bool expired() const; + + static bool idle_func (); + static bool timer_func (); + +public: + ExpirationTimer(Extension * in_extension); + virtual ~ExpirationTimer(); + + void touch (); + void lock () { locked++; }; + void unlock () { locked--; }; + + /** \brief Set the timeout variable */ + static void set_timeout (long in_seconds) { timeout = in_seconds; }; +}; + +}; }; /* namespace Inkscape, Extension */ + +#endif /* INKSCAPE_EXTENSION_TIMER_H__ */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |