1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Dirty DOM-like tree
4 *
5 * Authors:
6 * Lauris Kaplinski <lauris@kaplinski.com>
7 * bulia byak <buliabyak@users.sf.net>
8 *
9 * Copyright (C) 1999-2002 Lauris Kaplinski
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
14 #include <cstring>
15 #include <string>
16 #include <stdexcept>
17
18 #include <libxml/parser.h>
19 #include <libxml/xinclude.h>
20
21 #include "xml/repr.h"
22 #include "xml/attribute-record.h"
23 #include "xml/rebase-hrefs.h"
24 #include "xml/simple-document.h"
25 #include "xml/text-node.h"
26 #include "xml/node.h"
27
28 #include "io/sys.h"
29 #include "io/stream/stringstream.h"
30 #include "io/stream/gzipstream.h"
31 #include "io/stream/uristream.h"
32
33 #include "extension/extension.h"
34
35 #include "attribute-rel-util.h"
36 #include "attribute-sort-util.h"
37
38 #include "preferences.h"
39
40 #include <glibmm/miscutils.h>
41
42 using Inkscape::IO::Writer;
43 using Inkscape::XML::Document;
44 using Inkscape::XML::SimpleDocument;
45 using Inkscape::XML::Node;
46 using Inkscape::XML::AttributeRecord;
47 using Inkscape::XML::AttributeVector;
48 using Inkscape::XML::rebase_href_attrs;
49
50 Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns);
51 static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
52 static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
53 static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
54 bool add_whitespace, gchar const *default_ns,
55 int inlineattrs, int indent,
56 gchar const *old_href_abs_base,
57 gchar const *new_href_abs_base);
58
59 static void sp_repr_write_stream_element(Node *repr, Writer &out,
60 gint indent_level, bool add_whitespace,
61 Glib::QueryQuark elide_prefix,
62 const AttributeVector & attributes,
63 int inlineattrs, int indent,
64 gchar const *old_href_abs_base,
65 gchar const *new_href_abs_base);
66
67
68 class XmlSource
69 {
70 public:
XmlSource()71 XmlSource()
72 : filename(nullptr),
73 encoding(nullptr),
74 fp(nullptr),
75 firstFewLen(0),
76 LoadEntities(false),
77 cachedData(),
78 cachedPos(0),
79 instr(nullptr),
80 gzin(nullptr)
81 {
82 for (unsigned char & k : firstFew)
83 {
84 k=0;
85 }
86 }
~XmlSource()87 virtual ~XmlSource()
88 {
89 close();
90 if ( encoding ) {
91 g_free(encoding);
92 encoding = nullptr;
93 }
94 }
95
96 int setFile( char const * filename, bool load_entities );
97
98 xmlDocPtr readXml();
99
100 static int readCb( void * context, char * buffer, int len );
101 static int closeCb( void * context );
102
getEncoding() const103 char const* getEncoding() const { return encoding; }
104 int read( char * buffer, int len );
105 int close();
106 private:
107 const char* filename;
108 char* encoding;
109 FILE* fp;
110 unsigned char firstFew[4];
111 int firstFewLen;
112 bool LoadEntities; // Checks for SYSTEM Entities (requires cached data)
113 std::string cachedData;
114 unsigned int cachedPos;
115 Inkscape::IO::FileInputStream* instr;
116 Inkscape::IO::GzipInputStream* gzin;
117 };
118
setFile(char const * filename,bool load_entities=false)119 int XmlSource::setFile(char const *filename, bool load_entities=false)
120 {
121 int retVal = -1;
122
123 this->filename = filename;
124
125 fp = Inkscape::IO::fopen_utf8name(filename, "r");
126 if ( fp ) {
127 // First peek in the file to see what it is
128 memset( firstFew, 0, sizeof(firstFew) );
129
130 size_t some = fread( firstFew, 1, 4, fp );
131 if ( fp ) {
132 // first check for compression
133 if ( (some >= 2) && (firstFew[0] == 0x1f) && (firstFew[1] == 0x8b) ) {
134 //g_message(" the file being read is gzip'd. extract it");
135 fclose(fp);
136 fp = nullptr;
137 fp = Inkscape::IO::fopen_utf8name(filename, "r");
138 instr = new Inkscape::IO::FileInputStream(fp);
139 gzin = new Inkscape::IO::GzipInputStream(*instr);
140
141 memset( firstFew, 0, sizeof(firstFew) );
142 some = 0;
143 int single = 0;
144 while ( some < 4 && single >= 0 )
145 {
146 single = gzin->get();
147 if ( single >= 0 ) {
148 firstFew[some++] = 0x0ff & single;
149 } else {
150 break;
151 }
152 }
153 }
154
155 int encSkip = 0;
156 if ( (some >= 2) &&(firstFew[0] == 0xfe) && (firstFew[1] == 0xff) ) {
157 encoding = g_strdup("UTF-16BE");
158 encSkip = 2;
159 } else if ( (some >= 2) && (firstFew[0] == 0xff) && (firstFew[1] == 0xfe) ) {
160 encoding = g_strdup("UTF-16LE");
161 encSkip = 2;
162 } else if ( (some >= 3) && (firstFew[0] == 0xef) && (firstFew[1] == 0xbb) && (firstFew[2] == 0xbf) ) {
163 encoding = g_strdup("UTF-8");
164 encSkip = 3;
165 }
166
167 if ( encSkip ) {
168 memmove( firstFew, firstFew + encSkip, (some - encSkip) );
169 some -= encSkip;
170 }
171
172 firstFewLen = some;
173 retVal = 0; // no error
174 }
175 }
176 if(load_entities) {
177 this->cachedData = std::string("");
178 this->cachedPos = 0;
179
180 // First get data from file in typical way (cache it all)
181 char *buffer = new char [4096];
182 while(true) {
183 int len = this->read(buffer, 4096);
184 if(len <= 0) break;
185 buffer[len] = 0;
186 this->cachedData += buffer;
187 }
188 delete[] buffer;
189
190 // Check for SYSTEM or PUBLIC entities and remove them from the cache
191 GMatchInfo *info;
192 gint start, end;
193
194 GRegex *regex = g_regex_new(
195 "<!ENTITY\\s+[^>\\s]+\\s+(SYSTEM|PUBLIC\\s+\"[^>\"]+\")\\s+\"[^>\"]+\"\\s*>",
196 G_REGEX_CASELESS, G_REGEX_MATCH_NEWLINE_ANY, nullptr);
197
198 g_regex_match (regex, this->cachedData.c_str(), G_REGEX_MATCH_NEWLINE_ANY, &info);
199
200 while (g_match_info_matches (info)) {
201 if (g_match_info_fetch_pos (info, 1, &start, &end))
202 this->cachedData.erase(start, end - start);
203 g_match_info_next (info, nullptr);
204 }
205 g_match_info_free(info);
206 g_regex_unref(regex);
207 }
208 // Do this after loading cache, so reads don't return cache to fill cache.
209 this->LoadEntities = load_entities;
210 return retVal;
211 }
212
readXml()213 xmlDocPtr XmlSource::readXml()
214 {
215 int parse_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
216
217 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
218 bool allowNetAccess = prefs->getBool("/options/externalresources/xml/allow_net_access", false);
219 if (!allowNetAccess) parse_options |= XML_PARSE_NONET;
220
221 // Allow NOENT only if we're filtering out SYSTEM and PUBLIC entities
222 if (LoadEntities) parse_options |= XML_PARSE_NOENT;
223
224 auto doc = xmlReadIO( readCb, closeCb, this,
225 filename, getEncoding(), parse_options);
226
227 if (doc && doc->properties && xmlXIncludeProcessFlags(doc, XML_PARSE_NOXINCNODE) < 0) {
228 g_warning("XInclude processing failed for %s", filename);
229 }
230
231 return doc;
232 }
233
readCb(void * context,char * buffer,int len)234 int XmlSource::readCb( void * context, char * buffer, int len )
235 {
236 int retVal = -1;
237
238 if ( context ) {
239 XmlSource* self = static_cast<XmlSource*>(context);
240 retVal = self->read( buffer, len );
241 }
242 return retVal;
243 }
244
closeCb(void * context)245 int XmlSource::closeCb(void * context)
246 {
247 if ( context ) {
248 XmlSource* self = static_cast<XmlSource*>(context);
249 self->close();
250 }
251 return 0;
252 }
253
read(char * buffer,int len)254 int XmlSource::read( char *buffer, int len )
255 {
256 int retVal = 0;
257 size_t got = 0;
258
259 if ( LoadEntities ) {
260 if (cachedPos >= cachedData.length()) {
261 return -1;
262 } else {
263 retVal = cachedData.copy(buffer, len, cachedPos);
264 cachedPos += retVal;
265 return retVal; // Do NOT continue.
266 }
267 } else if ( firstFewLen > 0 ) {
268 int some = (len < firstFewLen) ? len : firstFewLen;
269 memcpy( buffer, firstFew, some );
270 if ( len < firstFewLen ) {
271 memmove( firstFew, firstFew + some, (firstFewLen - some) );
272 }
273 firstFewLen -= some;
274 got = some;
275 } else if ( gzin ) {
276 int single = 0;
277 while ( (static_cast<int>(got) < len) && (single >= 0) )
278 {
279 single = gzin->get();
280 if ( single >= 0 ) {
281 buffer[got++] = 0x0ff & single;
282 } else {
283 break;
284 }
285 }
286 } else {
287 got = fread( buffer, 1, len, fp );
288 }
289
290 if ( feof(fp) ) {
291 retVal = got;
292 } else if ( ferror(fp) ) {
293 retVal = -1;
294 } else {
295 retVal = got;
296 }
297
298 return retVal;
299 }
300
close()301 int XmlSource::close()
302 {
303 if ( gzin ) {
304 gzin->close();
305 delete gzin;
306 gzin = nullptr;
307 }
308 if ( instr ) {
309 instr->close();
310 fp = nullptr;
311 delete instr;
312 instr = nullptr;
313 }
314 if ( fp ) {
315 fclose(fp);
316 fp = nullptr;
317 }
318 return 0;
319 }
320
321 /**
322 * Reads XML from a file, and returns the Document.
323 * The default namespace can also be specified, if desired.
324 */
sp_repr_read_file(const gchar * filename,const gchar * default_ns)325 Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns)
326 {
327 xmlDocPtr doc = nullptr;
328 Document * rdoc = nullptr;
329
330 xmlSubstituteEntitiesDefault(1);
331
332 g_return_val_if_fail(filename != nullptr, NULL);
333 if (!Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) {
334 g_warning("Can't open file: %s (doesn't exist)", filename);
335 return nullptr;
336 }
337 /* fixme: A file can disappear at any time, including between now and when we actually try to
338 * open it. Get rid of the above test once we're sure that we correctly handle
339 * non-existence. */
340
341 // TODO: bulia, please look over
342 gsize bytesRead = 0;
343 gsize bytesWritten = 0;
344 GError* error = nullptr;
345 // TODO: need to replace with our own fopen and reading
346 gchar* localFilename = g_filename_from_utf8(filename, -1, &bytesRead, &bytesWritten, &error);
347 g_return_val_if_fail(localFilename != nullptr, NULL);
348
349 Inkscape::IO::dump_fopen_call(filename, "N");
350
351 XmlSource src;
352
353 if (src.setFile(filename) == 0) {
354 doc = src.readXml();
355 rdoc = sp_repr_do_read(doc, default_ns);
356 // For some reason, failed ns loading results in this
357 // We try a system check version of load with NOENT for adobe
358 if (rdoc && strcmp(rdoc->root()->name(), "ns:svg") == 0) {
359 xmlFreeDoc(doc);
360 src.setFile(filename, true);
361 doc = src.readXml();
362 rdoc = sp_repr_do_read(doc, default_ns);
363 }
364 }
365
366 if (doc) {
367 xmlFreeDoc(doc);
368 }
369
370 if (localFilename) {
371 g_free(localFilename);
372 }
373
374 return rdoc;
375 }
376
377 /**
378 * Reads and parses XML from a buffer, returning it as an Document
379 */
sp_repr_read_mem(const gchar * buffer,gint length,const gchar * default_ns)380 Document *sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns)
381 {
382 xmlDocPtr doc;
383 Document * rdoc;
384
385 xmlSubstituteEntitiesDefault(1);
386
387 g_return_val_if_fail (buffer != nullptr, NULL);
388
389 int parser_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
390 parser_options |= XML_PARSE_NONET; // TODO: should we allow network access?
391 // proper solution would be to check the preference "/options/externalresources/xml/allow_net_access"
392 // as done in XmlSource::readXml which gets called by the analogous sp_repr_read_file()
393 // but sp_repr_read_mem() seems to be called in locations where Inkscape::Preferences::get() fails badly
394 doc = xmlReadMemory (const_cast<gchar *>(buffer), length, nullptr, nullptr, parser_options);
395
396 rdoc = sp_repr_do_read (doc, default_ns);
397 if (doc) {
398 xmlFreeDoc (doc);
399 }
400 return rdoc;
401 }
402
403 /**
404 * Reads and parses XML from a buffer, returning it as an Document
405 */
sp_repr_read_buf(const Glib::ustring & buf,const gchar * default_ns)406 Document *sp_repr_read_buf (const Glib::ustring &buf, const gchar *default_ns)
407 {
408 return sp_repr_read_mem(buf.c_str(), buf.size(), default_ns);
409 }
410
411
412 namespace Inkscape {
413
414 struct compare_quark_ids {
operator ()Inkscape::compare_quark_ids415 bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const {
416 return a.id() < b.id();
417 }
418 };
419
420 }
421
422 namespace {
423
424 typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap;
425
qname_prefix(Glib::QueryQuark qname)426 Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) {
427 static PrefixMap prefix_map;
428 PrefixMap::iterator iter = prefix_map.find(qname);
429 if ( iter != prefix_map.end() ) {
430 return (*iter).second;
431 } else {
432 gchar const *name_string=g_quark_to_string(qname);
433 gchar const *prefix_end=strchr(name_string, ':');
434 if (prefix_end) {
435 Glib::Quark prefix=Glib::ustring(name_string, prefix_end);
436 prefix_map.insert(PrefixMap::value_type(qname, prefix));
437 return prefix;
438 } else {
439 return GQuark(0);
440 }
441 }
442 }
443
444 }
445
446 namespace {
447
promote_to_namespace(Node * repr,const gchar * prefix)448 void promote_to_namespace(Node *repr, const gchar *prefix) {
449 if ( repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
450 GQuark code = repr->code();
451 if (!qname_prefix(code).id()) {
452 gchar *svg_name = g_strconcat(prefix, ":", g_quark_to_string(code), NULL);
453 repr->setCodeUnsafe(g_quark_from_string(svg_name));
454 g_free(svg_name);
455 }
456 for ( Node *child = repr->firstChild() ; child ; child = child->next() ) {
457 promote_to_namespace(child, prefix);
458 }
459 }
460 }
461
462 }
463
464 /**
465 * Reads in a XML file to create a Document
466 */
sp_repr_do_read(xmlDocPtr doc,const gchar * default_ns)467 Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns)
468 {
469 if (doc == nullptr) {
470 return nullptr;
471 }
472 xmlNodePtr node=xmlDocGetRootElement (doc);
473 if (node == nullptr) {
474 return nullptr;
475 }
476
477 std::map<std::string, std::string> prefix_map;
478
479 Document *rdoc = new Inkscape::XML::SimpleDocument();
480
481 Node *root=nullptr;
482 for ( node = doc->children ; node != nullptr ; node = node->next ) {
483 if (node->type == XML_ELEMENT_NODE) {
484 Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
485 rdoc->appendChild(repr);
486 Inkscape::GC::release(repr);
487
488 if (!root) {
489 root = repr;
490 } else {
491 root = nullptr;
492 break;
493 }
494 } else if ( node->type == XML_COMMENT_NODE || node->type == XML_PI_NODE ) {
495 Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
496 rdoc->appendChild(repr);
497 Inkscape::GC::release(repr);
498 }
499 }
500
501 if (root != nullptr) {
502 /* promote elements of some XML documents that don't use namespaces
503 * into their default namespace */
504 if ( default_ns && !strchr(root->name(), ':') ) {
505 if ( !strcmp(default_ns, SP_SVG_NS_URI) ) {
506 promote_to_namespace(root, "svg");
507 }
508 if ( !strcmp(default_ns, INKSCAPE_EXTENSION_URI) ) {
509 promote_to_namespace(root, INKSCAPE_EXTENSION_NS_NC);
510 }
511 }
512
513
514 // Clean unnecessary attributes and style properties from SVG documents. (Controlled by
515 // preferences.) Note: internal Inkscape svg files will also be cleaned (filters.svg,
516 // icons.svg). How can one tell if a file is internal?
517 if ( !strcmp(root->name(), "svg:svg" ) ) {
518 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
519 bool clean = prefs->getBool("/options/svgoutput/check_on_reading");
520 if( clean ) {
521 sp_attribute_clean_tree( root );
522 }
523 }
524 }
525
526 return rdoc;
527 }
528
sp_repr_qualified_name(gchar * p,gint len,xmlNsPtr ns,const xmlChar * name,const gchar *,std::map<std::string,std::string> & prefix_map)529 gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar */*default_ns*/, std::map<std::string, std::string> &prefix_map)
530 {
531 const xmlChar *prefix;
532 if (ns){
533 if (ns->href ) {
534 prefix = reinterpret_cast<const xmlChar*>( sp_xml_ns_uri_prefix(reinterpret_cast<const gchar*>(ns->href),
535 reinterpret_cast<const char*>(ns->prefix)) );
536 prefix_map[reinterpret_cast<const char*>(prefix)] = reinterpret_cast<const char*>(ns->href);
537 }
538 else {
539 prefix = nullptr;
540 }
541 }
542 else {
543 prefix = nullptr;
544 }
545
546 if (prefix) {
547 return g_snprintf (p, len, "%s:%s", reinterpret_cast<const gchar*>(prefix), name);
548 } else {
549 return g_snprintf (p, len, "%s", name);
550 }
551 }
552
sp_repr_svg_read_node(Document * xml_doc,xmlNodePtr node,const gchar * default_ns,std::map<std::string,std::string> & prefix_map)553 static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map)
554 {
555 xmlAttrPtr prop;
556 xmlNodePtr child;
557 gchar c[256];
558
559 if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) {
560
561 if (node->content == nullptr || *(node->content) == '\0') {
562 return nullptr; // empty text node
563 }
564
565 // Since libxml2 2.9.0, only element nodes are checked, thus check parent.
566 // Note: this only handles XML's rules for white space. SVG's specific rules
567 // are handled in sp-string.cpp.
568 bool preserve = (xmlNodeGetSpacePreserve (node->parent) == 1);
569
570 xmlChar *p;
571 for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++)
572 ; // skip all whitespace
573
574 if (!(*p)) { // this is an all-whitespace node, and preserve == default
575 return nullptr; // we do not preserve all-whitespace nodes unless we are asked to
576 }
577
578 // We keep track of original node type so that CDATA sections are preserved on output.
579 return xml_doc->createTextNode(reinterpret_cast<gchar *>(node->content),
580 node->type == XML_CDATA_SECTION_NODE );
581 }
582
583 if (node->type == XML_COMMENT_NODE) {
584 return xml_doc->createComment(reinterpret_cast<gchar *>(node->content));
585 }
586
587 if (node->type == XML_PI_NODE) {
588 return xml_doc->createPI(reinterpret_cast<const gchar *>(node->name),
589 reinterpret_cast<const gchar *>(node->content));
590 }
591
592 if (node->type == XML_ENTITY_DECL) {
593 return nullptr;
594 }
595
596 sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map);
597 Node *repr = xml_doc->createElement(c);
598 /* TODO remember node->ns->prefix if node->ns != NULL */
599
600 for (prop = node->properties; prop != nullptr; prop = prop->next) {
601 if (prop->children) {
602 sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map);
603 repr->setAttribute(c, reinterpret_cast<gchar*>(prop->children->content));
604 /* TODO remember prop->ns->prefix if prop->ns != NULL */
605 }
606 }
607
608 if (node->content) {
609 repr->setContent(reinterpret_cast<gchar*>(node->content));
610 }
611
612 for (child = node->xmlChildrenNode; child != nullptr; child = child->next) {
613 Node *crepr = sp_repr_svg_read_node (xml_doc, child, default_ns, prefix_map);
614 if (crepr) {
615 repr->appendChild(crepr);
616 Inkscape::GC::release(crepr);
617 }
618 }
619
620 return repr;
621 }
622
623
sp_repr_save_writer(Document * doc,Inkscape::IO::Writer * out,gchar const * default_ns,gchar const * old_href_abs_base,gchar const * new_href_abs_base)624 static void sp_repr_save_writer(Document *doc, Inkscape::IO::Writer *out,
625 gchar const *default_ns,
626 gchar const *old_href_abs_base,
627 gchar const *new_href_abs_base)
628 {
629 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
630 bool inlineattrs = prefs->getBool("/options/svgoutput/inlineattrs");
631 int indent = prefs->getInt("/options/svgoutput/indent", 2);
632
633 /* fixme: do this The Right Way */
634 out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" );
635
636 const gchar *str = static_cast<Node *>(doc)->attribute("doctype");
637 if (str) {
638 out->writeString( str );
639 }
640
641 for (Node *repr = sp_repr_document_first_child(doc);
642 repr; repr = repr->next())
643 {
644 Inkscape::XML::NodeType const node_type = repr->type();
645 if ( node_type == Inkscape::XML::NodeType::ELEMENT_NODE ) {
646 sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns, inlineattrs, indent,
647 old_href_abs_base, new_href_abs_base);
648 } else {
649 sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0), inlineattrs, indent,
650 old_href_abs_base, new_href_abs_base);
651 if ( node_type == Inkscape::XML::NodeType::COMMENT_NODE ) {
652 out->writeChar('\n');
653 }
654 }
655 }
656 }
657
658
sp_repr_save_buf(Document * doc)659 Glib::ustring sp_repr_save_buf(Document *doc)
660 {
661 Inkscape::IO::StringOutputStream souts;
662 Inkscape::IO::OutputStreamWriter outs(souts);
663
664 sp_repr_save_writer(doc, &outs, SP_INKSCAPE_NS_URI, nullptr, nullptr);
665
666 outs.close();
667 Glib::ustring buf = souts.getString();
668
669 return buf;
670 }
671
672
sp_repr_save_stream(Document * doc,FILE * fp,gchar const * default_ns,bool compress,gchar const * const old_href_abs_base,gchar const * const new_href_abs_base)673 void sp_repr_save_stream(Document *doc, FILE *fp, gchar const *default_ns, bool compress,
674 gchar const *const old_href_abs_base,
675 gchar const *const new_href_abs_base)
676 {
677 Inkscape::IO::FileOutputStream bout(fp);
678 Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : nullptr;
679 Inkscape::IO::OutputStreamWriter *out = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout );
680
681 sp_repr_save_writer(doc, out, default_ns, old_href_abs_base, new_href_abs_base);
682
683 delete out;
684 delete gout;
685 }
686
687
688
689 /**
690 * Returns true if file successfully saved.
691 *
692 * \param filename The actual file to do I/O to, which might be a temp file.
693 *
694 * \param for_filename The base URI [actually filename] to assume for purposes of rewriting
695 * xlink:href attributes.
696 */
sp_repr_save_rebased_file(Document * doc,gchar const * const filename,gchar const * default_ns,gchar const * old_base,gchar const * for_filename)697 bool sp_repr_save_rebased_file(Document *doc, gchar const *const filename, gchar const *default_ns,
698 gchar const *old_base, gchar const *for_filename)
699 {
700 if (!filename) {
701 return false;
702 }
703
704 bool compress;
705 {
706 size_t const filename_len = strlen(filename);
707 compress = ( filename_len > 5
708 && strcasecmp(".svgz", filename + filename_len - 5) == 0 );
709 }
710
711 Inkscape::IO::dump_fopen_call( filename, "B" );
712 FILE *file = Inkscape::IO::fopen_utf8name(filename, "w");
713 if (file == nullptr) {
714 return false;
715 }
716
717 std::string old_href_abs_base;
718 std::string new_href_abs_base;
719
720 if (old_base) {
721 old_href_abs_base = old_base;
722 if (!Glib::path_is_absolute(old_href_abs_base)) {
723 old_href_abs_base = Glib::build_filename(Glib::get_current_dir(), old_href_abs_base);
724 }
725 }
726
727 if (for_filename) {
728 if (Glib::path_is_absolute(for_filename)) {
729 new_href_abs_base = Glib::path_get_dirname(for_filename);
730 } else {
731 std::string const cwd = Glib::get_current_dir();
732 std::string const for_abs_filename = Glib::build_filename(cwd, for_filename);
733 new_href_abs_base = Glib::path_get_dirname(for_abs_filename);
734 }
735
736 /* effic: Once we're confident that we never need (or never want) to resort
737 * to using sodipodi:absref instead of the xlink:href value,
738 * then we should do `if streq() { free them and set both to NULL; }'. */
739 }
740 sp_repr_save_stream(doc, file, default_ns, compress, old_href_abs_base.c_str(), new_href_abs_base.c_str());
741
742 if (fclose (file) != 0) {
743 return false;
744 }
745
746 return true;
747 }
748
749 /**
750 * Returns true iff file successfully saved.
751 */
sp_repr_save_file(Document * doc,gchar const * const filename,gchar const * default_ns)752 bool sp_repr_save_file(Document *doc, gchar const *const filename, gchar const *default_ns)
753 {
754 return sp_repr_save_rebased_file(doc, filename, default_ns, nullptr, nullptr);
755 }
756
757
758 /* (No doubt this function already exists elsewhere.) */
repr_quote_write(Writer & out,const gchar * val)759 static void repr_quote_write (Writer &out, const gchar * val)
760 {
761 if (val) {
762 for (; *val != '\0'; val++) {
763 switch (*val) {
764 case '"': out.writeString( """ ); break;
765 case '&': out.writeString( "&" ); break;
766 case '<': out.writeString( "<" ); break;
767 case '>': out.writeString( ">" ); break;
768 default: out.writeChar( *val ); break;
769 }
770 }
771 }
772 }
773
repr_write_comment(Writer & out,const gchar * val,bool addWhitespace,gint indentLevel,int indent)774 static void repr_write_comment( Writer &out, const gchar * val, bool addWhitespace, gint indentLevel, int indent )
775 {
776 if ( indentLevel > 16 ) {
777 indentLevel = 16;
778 }
779 if (addWhitespace && indent) {
780 for (gint i = 0; i < indentLevel; i++) {
781 for (gint j = 0; j < indent; j++) {
782 out.writeChar(' ');
783 }
784 }
785 }
786
787 out.printf("<!--%s-->", val);
788
789 if (addWhitespace) {
790 out.writeChar('\n');
791 }
792 }
793
794 namespace {
795
796 typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap;
797 typedef std::map<Glib::QueryQuark, Inkscape::Util::ptr_shared, Inkscape::compare_quark_ids> NSMap;
798
qname_local_name(Glib::QueryQuark qname)799 gchar const *qname_local_name(Glib::QueryQuark qname) {
800 static LocalNameMap local_name_map;
801 LocalNameMap::iterator iter = local_name_map.find(qname);
802 if ( iter != local_name_map.end() ) {
803 return (*iter).second;
804 } else {
805 gchar const *name_string=g_quark_to_string(qname);
806 gchar const *prefix_end=strchr(name_string, ':');
807 if (prefix_end) {
808 return prefix_end + 1;
809 } else {
810 return name_string;
811 }
812 }
813 }
814
add_ns_map_entry(NSMap & ns_map,Glib::QueryQuark prefix)815 void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) {
816 using Inkscape::Util::ptr_shared;
817 using Inkscape::Util::share_unsafe;
818
819 static const Glib::QueryQuark xml_prefix("xml");
820
821 NSMap::iterator iter=ns_map.find(prefix);
822 if ( iter == ns_map.end() ) {
823 if (prefix.id()) {
824 gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix));
825 if (uri) {
826 ns_map.insert(NSMap::value_type(prefix, share_unsafe(uri)));
827 } else if ( prefix != xml_prefix ) {
828 g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix));
829 }
830 } else {
831 ns_map.insert(NSMap::value_type(prefix, ptr_shared()));
832 }
833 }
834 }
835
populate_ns_map(NSMap & ns_map,Node & repr)836 void populate_ns_map(NSMap &ns_map, Node &repr) {
837 if ( repr.type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
838 add_ns_map_entry(ns_map, qname_prefix(repr.code()));
839 for ( const auto & iter : repr.attributeList() )
840 {
841 Glib::QueryQuark prefix=qname_prefix(iter.key);
842 if (prefix.id()) {
843 add_ns_map_entry(ns_map, prefix);
844 }
845 }
846 for ( Node *child=repr.firstChild() ;
847 child ; child = child->next() )
848 {
849 populate_ns_map(ns_map, *child);
850 }
851 }
852 }
853
854 }
855
sp_repr_write_stream_root_element(Node * repr,Writer & out,bool add_whitespace,gchar const * default_ns,int inlineattrs,int indent,gchar const * const old_href_base,gchar const * const new_href_base)856 static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
857 bool add_whitespace, gchar const *default_ns,
858 int inlineattrs, int indent,
859 gchar const *const old_href_base,
860 gchar const *const new_href_base)
861 {
862 using Inkscape::Util::ptr_shared;
863
864 g_assert(repr != nullptr);
865
866 // Clean unnecessary attributes and stype properties. (Controlled by preferences.)
867 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
868 bool clean = prefs->getBool("/options/svgoutput/check_on_writing");
869 if (clean) sp_attribute_clean_tree( repr );
870
871 // Sort attributes in a canonical order (helps with "diffing" SVG files).only if not set disable optimizations
872 bool sort = !prefs->getBool("/options/svgoutput/disable_optimizations") && prefs->getBool("/options/svgoutput/sort_attributes");
873 if (sort) sp_attribute_sort_tree( *repr );
874
875 Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml");
876
877 NSMap ns_map;
878 populate_ns_map(ns_map, *repr);
879
880 Glib::QueryQuark elide_prefix=GQuark(0);
881 if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) {
882 elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, nullptr));
883 }
884
885 auto attributes = repr->attributeList(); // copy
886
887 using Inkscape::Util::share_string;
888 for (auto iter : ns_map)
889 {
890 Glib::QueryQuark prefix=iter.first;
891 ptr_shared ns_uri=iter.second;
892
893 if (prefix.id()) {
894 if ( prefix != xml_prefix ) {
895 if ( elide_prefix == prefix ) {
896 //repr->setAttribute(share_string("xmlns"), share_string(ns_uri));
897 attributes.emplace_back(g_quark_from_static_string("xmlns"), ns_uri);
898 }
899
900 Glib::ustring attr_name="xmlns:";
901 attr_name.append(g_quark_to_string(prefix));
902 GQuark key = g_quark_from_string(attr_name.c_str());
903 //repr->setAttribute(share_string(attr_name.c_str()), share_string(ns_uri));
904 attributes.emplace_back(key, ns_uri);
905 }
906 } else {
907 // if there are non-namespaced elements, we can't globally
908 // use a default namespace
909 elide_prefix = GQuark(0);
910 }
911 }
912
913 return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes,
914 inlineattrs, indent, old_href_base, new_href_base);
915 }
916
sp_repr_write_stream(Node * repr,Writer & out,gint indent_level,bool add_whitespace,Glib::QueryQuark elide_prefix,int inlineattrs,int indent,gchar const * const old_href_base,gchar const * const new_href_base)917 void sp_repr_write_stream( Node *repr, Writer &out, gint indent_level,
918 bool add_whitespace, Glib::QueryQuark elide_prefix,
919 int inlineattrs, int indent,
920 gchar const *const old_href_base,
921 gchar const *const new_href_base)
922 {
923 switch (repr->type()) {
924 case Inkscape::XML::NodeType::TEXT_NODE: {
925 auto textnode = dynamic_cast<const Inkscape::XML::TextNode *>(repr);
926 assert(textnode);
927 if (textnode->is_CData()) {
928 // Preserve CDATA sections, not converting '&' to &, etc.
929 out.printf( "<![CDATA[%s]]>", repr->content() );
930 } else {
931 repr_quote_write( out, repr->content() );
932 }
933 break;
934 }
935 case Inkscape::XML::NodeType::COMMENT_NODE: {
936 repr_write_comment( out, repr->content(), add_whitespace, indent_level, indent );
937 break;
938 }
939 case Inkscape::XML::NodeType::PI_NODE: {
940 out.printf( "<?%s %s?>", repr->name(), repr->content() );
941 break;
942 }
943 case Inkscape::XML::NodeType::ELEMENT_NODE: {
944 sp_repr_write_stream_element( repr, out, indent_level,
945 add_whitespace, elide_prefix,
946 repr->attributeList(),
947 inlineattrs, indent,
948 old_href_base, new_href_base);
949 break;
950 }
951 case Inkscape::XML::NodeType::DOCUMENT_NODE: {
952 g_assert_not_reached();
953 break;
954 }
955 default: {
956 g_assert_not_reached();
957 }
958 }
959 }
960
961
sp_repr_write_stream_element(Node * repr,Writer & out,gint indent_level,bool add_whitespace,Glib::QueryQuark elide_prefix,const AttributeVector & attributes,int inlineattrs,int indent,gchar const * old_href_base,gchar const * new_href_base)962 void sp_repr_write_stream_element( Node * repr, Writer & out,
963 gint indent_level, bool add_whitespace,
964 Glib::QueryQuark elide_prefix,
965 const AttributeVector & attributes,
966 int inlineattrs, int indent,
967 gchar const *old_href_base,
968 gchar const *new_href_base )
969 {
970 Node *child = nullptr;
971 bool loose = false;
972 bool const add_whitespace_parent = add_whitespace;
973
974 g_return_if_fail (repr != nullptr);
975
976 if ( indent_level > 16 ) {
977 indent_level = 16;
978 }
979
980 if (add_whitespace && indent) {
981 for (gint i = 0; i < indent_level; i++) {
982 for (gint j = 0; j < indent; j++) {
983 out.writeChar(' ');
984 }
985 }
986 }
987
988 GQuark code = repr->code();
989 gchar const *element_name;
990 if ( elide_prefix == qname_prefix(code) ) {
991 element_name = qname_local_name(code);
992 } else {
993 element_name = g_quark_to_string(code);
994 }
995 out.printf( "<%s", element_name );
996
997 // If this is a <text> element, suppress formatting whitespace
998 // for its content and children:
999 if (strcmp(repr->name(), "svg:text") == 0 ||
1000 strcmp(repr->name(), "svg:flowRoot") == 0) {
1001 add_whitespace = false;
1002 } else {
1003 // Suppress formatting whitespace for xml:space="preserve"
1004 gchar const *xml_space_attr = repr->attribute("xml:space");
1005 if (g_strcmp0(xml_space_attr, "preserve") == 0) {
1006 add_whitespace = false;
1007 } else if (g_strcmp0(xml_space_attr, "default") == 0) {
1008 add_whitespace = true;
1009 }
1010 }
1011
1012 const auto rbd = rebase_href_attrs(old_href_base, new_href_base, attributes);
1013 for (const auto &iter : rbd) {
1014 if (!inlineattrs) {
1015 out.writeChar('\n');
1016 if (indent) {
1017 for ( gint i = 0 ; i < indent_level + 1 ; i++ ) {
1018 for ( gint j = 0 ; j < indent ; j++ ) {
1019 out.writeChar(' ');
1020 }
1021 }
1022 }
1023 }
1024 out.printf(" %s=\"", g_quark_to_string(iter.key));
1025 repr_quote_write(out, iter.value);
1026 out.writeChar('"');
1027 }
1028
1029 loose = TRUE;
1030 for (child = repr->firstChild() ; child != nullptr; child = child->next()) {
1031 if (child->type() == Inkscape::XML::NodeType::TEXT_NODE) {
1032 loose = FALSE;
1033 break;
1034 }
1035 }
1036
1037 if (repr->firstChild()) {
1038 out.writeChar('>');
1039 if (loose && add_whitespace) {
1040 out.writeChar('\n');
1041 }
1042 for (child = repr->firstChild(); child != nullptr; child = child->next()) {
1043 sp_repr_write_stream(child, out, ( loose ? indent_level + 1 : 0 ),
1044 add_whitespace, elide_prefix, inlineattrs, indent,
1045 old_href_base, new_href_base);
1046 }
1047
1048 if (loose && add_whitespace && indent) {
1049 for (gint i = 0; i < indent_level; i++) {
1050 for ( gint j = 0 ; j < indent ; j++ ) {
1051 out.writeChar(' ');
1052 }
1053 }
1054 }
1055 out.printf( "</%s>", element_name );
1056 } else {
1057 out.writeString( " />" );
1058 }
1059
1060 if (add_whitespace_parent) {
1061 out.writeChar('\n');
1062 }
1063 }
1064
1065
1066 /*
1067 Local Variables:
1068 mode:c++
1069 c-file-style:"stroustrup"
1070 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1071 indent-tabs-mode:nil
1072 fill-column:99
1073 End:
1074 */
1075 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
1076