Code

Catch failures in fopen of XML files. Fixes #1374551.
[inkscape.git] / src / xml / repr-io.cpp
1 #define __SP_REPR_IO_C__
3 /*
4  * Dirty DOM-like  tree
5  *
6  * Authors:
7  *   Lauris Kaplinski <lauris@kaplinski.com>
8  *   bulia byak <buliabyak@users.sf.net>
9  *
10  * Copyright (C) 1999-2002 Lauris Kaplinski
11  *
12  * Released under GNU GPL, read the file 'COPYING' for more information
13  */
15 #ifdef HAVE_CONFIG_H
16 # include <config.h>
17 #endif
19 #include <stdexcept>
21 #include "xml/repr.h"
22 #include "xml/attribute-record.h"
24 #include "io/sys.h"
25 #include "io/uristream.h"
26 #include "io/gzipstream.h"
29 using Inkscape::IO::Writer;
30 using Inkscape::Util::List;
31 using Inkscape::Util::cons;
32 using Inkscape::XML::Document;
33 using Inkscape::XML::Node;
34 using Inkscape::XML::AttributeRecord;
36 static Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns);
37 static Node *sp_repr_svg_read_node (xmlNodePtr node, const gchar *default_ns, GHashTable *prefix_map);
38 static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, GHashTable *prefix_map);
39 static void sp_repr_write_stream_root_element (Node *repr, Writer &out, gboolean add_whitespace, gchar const *default_ns);
40 static void sp_repr_write_stream (Node *repr, Writer &out, gint indent_level, gboolean add_whitespace, Glib::QueryQuark elide_prefix);
41 static void sp_repr_write_stream_element (Node *repr, Writer &out, gint indent_level, gboolean add_whitespace, Glib::QueryQuark elide_prefix, List<AttributeRecord const> attributes);
43 #ifdef HAVE_LIBWMF
44 static xmlDocPtr sp_wmf_convert (const char * file_name);
45 static char * sp_wmf_image_name (void * context);
46 #endif /* HAVE_LIBWMF */
49 class XmlSource
50 {
51 public:
52     XmlSource()
53         : filename(0),
54           fp(0),
55           first(false),
56           dummy("x"),
57           instr(0),
58           gzin(0)
59     {
60     }
61     virtual ~XmlSource()
62     {
63         close();
64     }
66     void setFile( char const * filename );
68     static int readCb( void * context, char * buffer, int len);
69     static int closeCb(void * context);
72     int read( char * buffer, int len );
73     int close();
74 private:
75     const char* filename;
76     FILE* fp;
77     bool first;
78     Inkscape::URI dummy;
79     Inkscape::IO::UriInputStream* instr;
80     Inkscape::IO::GzipInputStream* gzin;
81 };
83 void XmlSource::setFile(char const *filename)
84 {
85     this->filename = filename;
86     fp = Inkscape::IO::fopen_utf8name(filename, "r");
87     if (fp == NULL) {
88         throw std::runtime_error("Could not open file for reading");
89     }
90     first = true;
91 }
94 int XmlSource::readCb( void * context, char * buffer, int len )
95 {
96     int retVal = -1;
97     if ( context ) {
98         XmlSource* self = static_cast<XmlSource*>(context);
99         retVal = self->read( buffer, len );
100     }
101     return retVal;
104 int XmlSource::closeCb(void * context)
106     if ( context ) {
107         XmlSource* self = static_cast<XmlSource*>(context);
108         self->close();
109     }
110     return 0;
113 int XmlSource::read( char *buffer, int len )
115     int retVal = 0;
116     size_t got = 0;
118     if ( first ) {
119         first = false;
120         char tmp[] = {0,0};
121         size_t some = fread( tmp, 1, 2, fp );
123         if ( (some >= 2) && (tmp[0] == 0x1f) && ((unsigned char)(tmp[1]) == 0x8b) ) {
124             //g_message(" the file being read is gzip'd. extract it");
125             fclose(fp);
126             fp = 0;
127             fp = Inkscape::IO::fopen_utf8name(filename, "r");
128             instr = new Inkscape::IO::UriInputStream(fp, dummy);
129             gzin = new Inkscape::IO::GzipInputStream(*instr);
130             int single = 0;
131             while ( (int)got < len && single >= 0 )
132             {
133                 single = gzin->get();
134                 if ( single >= 0 ) {
135                     buffer[got++] = 0x0ff & single;
136                 } else {
137                     break;
138                 }
139             }
140             //g_message(" extracted %d bytes this pass", got );
141         } else {
142             memcpy( buffer, tmp, some );
143             got = some;
144         }
145     } else if ( gzin ) {
146         int single = 0;
147         while ( (int)got < len && single >= 0 )
148         {
149             single = gzin->get();
150             if ( single >= 0 ) {
151                 buffer[got++] = 0x0ff & single;
152             } else {
153                 break;
154             }
155         }
156         //g_message(" extracted %d bytes this pass  b", got );
157     } else {
158         got = fread( buffer, 1, len, fp );
159     }
161     if ( feof(fp) ) {
162         retVal = got;
163     }
164     else if ( ferror(fp) ) {
165         retVal = -1;
166     }
167     else {
168         retVal = got;
169     }
171     return retVal;
174 int XmlSource::close()
176     if ( gzin ) {
177         gzin->close();
178         delete gzin;
179         gzin = 0;
180     }
181     if ( instr ) {
182         instr->close();
183         fp = 0;
184         delete instr;
185         instr = 0;
186     }
187     if ( fp ) {
188         fclose(fp);
189         fp = 0;
190     }
191     return 0;
194 /**
195  * Reads XML from a file, including WMF files, and returns the Document.
196  * The default namespace can also be specified, if desired.
197  */
198 Document *
199 sp_repr_read_file (const gchar * filename, const gchar *default_ns)
201     xmlDocPtr doc = 0;
202     Document * rdoc = 0;
204     xmlSubstituteEntitiesDefault(1);
206     g_return_val_if_fail (filename != NULL, NULL);
207     g_return_val_if_fail (Inkscape::IO::file_test( filename, G_FILE_TEST_EXISTS ), NULL);
209     // TODO: bulia, please look over
210     gsize bytesRead = 0;
211     gsize bytesWritten = 0;
212     GError* error = NULL;
213     // TODO: need to replace with our own fopen and reading
214     gchar* localFilename = g_filename_from_utf8 ( filename,
215                                  -1,  &bytesRead,  &bytesWritten, &error);
216     g_return_val_if_fail( localFilename != NULL, NULL );
218     Inkscape::IO::dump_fopen_call( filename, "N" );
220     XmlSource src;
221     try
222     {
223         src.setFile(filename);
224     }
225     catch (...)
226     {
227         return NULL;
228     }
230     xmlDocPtr doubleDoc = xmlReadIO( XmlSource::readCb,
231                                      XmlSource::closeCb,
232                                      &src,
233                                      localFilename,
234                                      NULL, //"UTF-8",
235                                      XML_PARSE_NOENT );
238 #ifdef HAVE_LIBWMF
239     if (strlen (localFilename) > 4) {
240         if ( (strcmp (localFilename + strlen (localFilename) - 4,".wmf") == 0)
241           || (strcmp (localFilename + strlen (localFilename) - 4,".WMF") == 0))
242             doc = sp_wmf_convert (localFilename);
243         else
244             doc = xmlParseFile (localFilename);
245     }
246     else {
247         doc = xmlParseFile (localFilename);
248     }
249 #else /* !HAVE_LIBWMF */
250     //doc = xmlParseFile (localFilename);
251 #endif /* !HAVE_LIBWMF */
253     //rdoc = sp_repr_do_read (doc, default_ns);
254     rdoc = sp_repr_do_read (doubleDoc, default_ns);
255     if (doc)
256         xmlFreeDoc (doc);
258     if ( localFilename != NULL )
259         g_free (localFilename);
261     if ( doubleDoc != NULL )
262     {
263         xmlFreeDoc( doubleDoc );
264     }
266     return rdoc;
269 /**
270  * Reads and parses XML from a buffer, returning it as an Document
271  */
272 Document *
273 sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns)
275     xmlDocPtr doc;
276     Document * rdoc;
278     xmlSubstituteEntitiesDefault(1);
280     g_return_val_if_fail (buffer != NULL, NULL);
282     doc = xmlParseMemory ((gchar *) buffer, length);
284     rdoc = sp_repr_do_read (doc, default_ns);
285     if (doc)
286         xmlFreeDoc (doc);
287     return rdoc;
290 namespace Inkscape {
292 struct compare_quark_ids {
293     bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const {
294         return a.id() < b.id();
295     }
296 };
300 namespace {
302 typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap;
304 Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) {
305     static PrefixMap prefix_map;
306     PrefixMap::iterator iter = prefix_map.find(qname);
307     if ( iter != prefix_map.end() ) {
308         return (*iter).second;
309     } else {
310         gchar const *name_string=g_quark_to_string(qname);
311         gchar const *prefix_end=strchr(name_string, ':');
312         if (prefix_end) {
313             Glib::Quark prefix=Glib::ustring(name_string, prefix_end);
314             prefix_map.insert(PrefixMap::value_type(qname, prefix));
315             return prefix;
316         } else {
317             return GQuark(0);
318         }
319     }
324 namespace {
326 void promote_to_svg_namespace(Node *repr) {
327     if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
328         GQuark code = repr->code();
329         if (!qname_prefix(code).id()) {
330             gchar *svg_name = g_strconcat("svg:", g_quark_to_string(code), NULL);
331             repr->setCodeUnsafe(g_quark_from_string(svg_name));
332             g_free(svg_name);
333         }
334         for ( Node *child = sp_repr_children(repr) ; child ; child = sp_repr_next(child) ) {
335             promote_to_svg_namespace(child);
336         }
337     }
342 /**
343  * Reads in a XML file to create a Document
344  */
345 Document *
346 sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns)
348     if (doc == NULL) return NULL;
349     xmlNodePtr node=xmlDocGetRootElement (doc);
350     if (node == NULL) return NULL;
352     GHashTable * prefix_map;
353     prefix_map = g_hash_table_new (g_str_hash, g_str_equal);
355     GSList *reprs=NULL;
356     Node *root=NULL;
358     for ( node = doc->children ; node != NULL ; node = node->next ) {
359         if (node->type == XML_ELEMENT_NODE) {
360             Node *repr=sp_repr_svg_read_node (node, default_ns, prefix_map);
361             reprs = g_slist_append(reprs, repr);
363             if (!root) {
364                 root = repr;
365             } else {
366                 root = NULL;
367                 break;
368             }
369         } else if ( node->type == XML_COMMENT_NODE ) {
370             Node *comment=sp_repr_svg_read_node(node, default_ns, prefix_map);
371             reprs = g_slist_append(reprs, comment);
372         }
373     }
375     Document *rdoc=NULL;
377     if (root != NULL) {
378         /* promote elements of SVG documents that don't use namespaces
379          * into the SVG namespace */
380         if ( default_ns && !strcmp(default_ns, SP_SVG_NS_URI)
381              && !strcmp(root->name(), "svg") )
382         {
383             promote_to_svg_namespace(root);
384         }
386         rdoc = sp_repr_document_new_list(reprs);
387     }
389     for ( GSList *iter = reprs ; iter ; iter = iter->next ) {
390         Node *repr=(Node *)iter->data;
391         Inkscape::GC::release(repr);
392     }
393     g_slist_free(reprs);
395     g_hash_table_destroy (prefix_map);
397     return rdoc;
400 gint
401 sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, GHashTable *prefix_map)
403     const xmlChar *prefix;
404     if ( ns && ns->href ) {
405         prefix = (xmlChar*)sp_xml_ns_uri_prefix ((gchar*)ns->href, (char*)ns->prefix);
406         g_hash_table_insert (prefix_map, (gpointer)prefix, (gpointer)ns->href);
407     } else {
408         prefix = NULL;
409     }
411     if (prefix)
412         return g_snprintf (p, len, "%s:%s", (gchar*)prefix, name);
413     else
414         return g_snprintf (p, len, "%s", name);
417 static Node *
418 sp_repr_svg_read_node (xmlNodePtr node, const gchar *default_ns, GHashTable *prefix_map)
420     Node *repr, *crepr;
421     xmlAttrPtr prop;
422     xmlNodePtr child;
423     gchar c[256];
425     if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) {
427         if (node->content == NULL || *(node->content) == '\0')
428             return NULL; // empty text node
430         bool preserve = (xmlNodeGetSpacePreserve (node) == 1);
432         xmlChar *p;
433         for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++)
434             ; // skip all whitespace
436         if (!(*p)) { // this is an all-whitespace node, and preserve == default
437             return NULL; // we do not preserve all-whitespace nodes unless we are asked to
438         }
440         Node *rdoc = sp_repr_new_text((const gchar *)node->content);
441         return rdoc;
442     }
444     if (node->type == XML_COMMENT_NODE)
445         return sp_repr_new_comment((const gchar *)node->content);
447     if (node->type == XML_ENTITY_DECL) return NULL;
449     sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map);
450     repr = sp_repr_new (c);
451     /* TODO remember node->ns->prefix if node->ns != NULL */
453     for (prop = node->properties; prop != NULL; prop = prop->next) {
454         if (prop->children) {
455             sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map);
456             repr->setAttribute(c, (gchar*)prop->children->content);
457             /* TODO remember prop->ns->prefix if prop->ns != NULL */
458         }
459     }
461     if (node->content)
462         repr->setContent((gchar*)node->content);
464     child = node->xmlChildrenNode;
465     for (child = node->xmlChildrenNode; child != NULL; child = child->next) {
466         crepr = sp_repr_svg_read_node (child, default_ns, prefix_map);
467         if (crepr) {
468             repr->appendChild(crepr);
469             Inkscape::GC::release(crepr);
470         }
471     }
473     return repr;
476 void
477 sp_repr_save_stream (Document *doc, FILE *fp, gchar const *default_ns, bool compress)
479     Node *repr;
480     const gchar *str;
482     Inkscape::URI dummy("x");
483     Inkscape::IO::UriOutputStream bout(fp, dummy);
484     Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : NULL;
485     Inkscape::IO::OutputStreamWriter *out  = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout );
487     /* fixme: do this The Right Way */
489     out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" );
491     str = ((Node *)doc)->attribute("doctype");
492     if (str) {
493         out->writeString( str );
494     }
496     repr = sp_repr_document_first_child(doc);
497     for ( repr = sp_repr_document_first_child(doc) ;
498           repr ; repr = sp_repr_next(repr) )
499     {
500         if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
501             sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns);
502         } else if ( repr->type() == Inkscape::XML::COMMENT_NODE ) {
503             sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0));
504             out->writeChar( '\n' );
505         } else {
506             sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0));
507         }
508     }
509     if ( out ) {
510         delete out;
511         out = NULL;
512     }
513     if ( gout ) {
514         delete gout;
515         gout = NULL;
516     }
519 /* Returns TRUE if file successfully saved; FALSE if not
520  */
521 gboolean
522 sp_repr_save_file (Document *doc, const gchar *filename,
523                    gchar const *default_ns)
525     if (filename == NULL) {
526         return FALSE;
527     }
528     bool compress = false;
529     {
530         if (strlen (filename) > 5) {
531             gchar tmp[] = {0,0,0,0,0,0};
532             strncpy( tmp, filename + strlen (filename) - 5, 6 );
533             tmp[5] = 0;
534             if ( strcasecmp(".svgz", tmp ) == 0 )
535             {
536                 //g_message("TIME TO COMPRESS THE OUTPUT FOR SVGZ");
537                 compress = true;
538             }
539         }
540     }
542     Inkscape::IO::dump_fopen_call( filename, "B" );
543     FILE *file = Inkscape::IO::fopen_utf8name(filename, "w");
544     if (file == NULL) {
545         return FALSE;
546     }
548     sp_repr_save_stream (doc, file, default_ns, compress);
550     if (fclose (file) != 0) {
551         return FALSE;
552     }
554     return TRUE;
557 void
558 sp_repr_print (Node * repr)
560     Inkscape::IO::StdOutputStream bout;
561     Inkscape::IO::OutputStreamWriter out(bout);
563     sp_repr_write_stream (repr, out, 0, TRUE, GQuark(0));
565     return;
568 /* (No doubt this function already exists elsewhere.) */
569 static void
570 repr_quote_write (Writer &out, const gchar * val)
572     if (!val) return;
574     for (; *val != '\0'; val++) {
575         switch (*val) {
576         case '"': out.writeString( "&quot;" ); break;
577         case '&': out.writeString( "&amp;" ); break;
578         case '<': out.writeString( "&lt;" ); break;
579         case '>': out.writeString( "&gt;" ); break;
580         default: out.writeChar( *val ); break;
581         }
582     }
585 namespace {
587 typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap;
588 typedef std::map<Glib::QueryQuark, Inkscape::Util::SharedCStringPtr, Inkscape::compare_quark_ids> NSMap;
590 gchar const *qname_local_name(Glib::QueryQuark qname) {
591     static LocalNameMap local_name_map;
592     LocalNameMap::iterator iter = local_name_map.find(qname);
593     if ( iter != local_name_map.end() ) {
594         return (*iter).second;
595     } else {
596         gchar const *name_string=g_quark_to_string(qname);
597         gchar const *prefix_end=strchr(name_string, ':');
598         if (prefix_end) {
599             return prefix_end + 1;
600         } else {
601             return name_string;
602         }
603     }
606 void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) {
607     using Inkscape::Util::SharedCStringPtr;
609     static const Glib::QueryQuark xml_prefix("xml");
611     NSMap::iterator iter=ns_map.find(prefix);
612     if ( iter == ns_map.end() ) {
613         if (prefix.id()) {
614             gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix));
615             if (uri) {
616                 ns_map.insert(NSMap::value_type(prefix, SharedCStringPtr::coerce(uri)));
617             } else if ( prefix != xml_prefix ) {
618                 g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix));
619             }
620         } else {
621             ns_map.insert(NSMap::value_type(prefix, SharedCStringPtr()));
622         }
623     }
626 void populate_ns_map(NSMap &ns_map, Node &repr) {
627     if ( repr.type() == Inkscape::XML::ELEMENT_NODE ) {
628         add_ns_map_entry(ns_map, qname_prefix(repr.code()));
629         for ( List<AttributeRecord const> iter=repr.attributeList() ;
630               iter ; ++iter )
631         {
632             Glib::QueryQuark prefix=qname_prefix(iter->key);
633             if (prefix.id()) {
634                 add_ns_map_entry(ns_map, prefix);
635             }
636         }
637         for ( Node *child=sp_repr_children(&repr) ;
638               child ; child = sp_repr_next(child) )
639         {
640             populate_ns_map(ns_map, *child);
641         }
642     }
647 void
648 sp_repr_write_stream_root_element (Node *repr, Writer &out, gboolean add_whitespace, gchar const *default_ns)
650     using Inkscape::Util::SharedCStringPtr;
651     g_assert(repr != NULL);
652     Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml");
654     NSMap ns_map;
655     populate_ns_map(ns_map, *repr);
657     Glib::QueryQuark elide_prefix=GQuark(0);
658     if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) {
659         elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, NULL));
660     }
662     List<AttributeRecord const> attributes=repr->attributeList();
663     for ( NSMap::iterator iter=ns_map.begin() ; iter != ns_map.end() ; ++iter ) 
664     {
665         Glib::QueryQuark prefix=(*iter).first;
666         SharedCStringPtr ns_uri=(*iter).second;
668         if (prefix.id()) {
669             if ( prefix != xml_prefix ) {
670                 if ( elide_prefix == prefix ) {
671                     attributes = cons(AttributeRecord(g_quark_from_static_string("xmlns"), ns_uri), attributes);
672                 }
674                 Glib::ustring attr_name="xmlns:";
675                 attr_name.append(g_quark_to_string(prefix));
676                 GQuark key = g_quark_from_string(attr_name.c_str());
677                 attributes = cons(AttributeRecord(key, ns_uri), attributes);
678             }
679         } else {
680             // if there are non-namespaced elements, we can't globally
681             // use a default namespace
682             elide_prefix = GQuark(0);
683         }
684     }
686     return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes);
689 void
690 sp_repr_write_stream (Node *repr, Writer &out, gint indent_level,
691                       gboolean add_whitespace, Glib::QueryQuark elide_prefix)
693     if (repr->type() == Inkscape::XML::TEXT_NODE) {
694         repr_quote_write (out, repr->content());
695     } else if (repr->type() == Inkscape::XML::COMMENT_NODE) {
696         out.printf( "<!--%s-->", repr->content() );
697     } else if (repr->type() == Inkscape::XML::ELEMENT_NODE) {
698         sp_repr_write_stream_element(repr, out, indent_level, add_whitespace, elide_prefix, repr->attributeList());
699     } else {
700         g_assert_not_reached();
701     }
704 void
705 sp_repr_write_stream_element (Node * repr, Writer & out, gint indent_level,
706                               gboolean add_whitespace,
707                               Glib::QueryQuark elide_prefix,
708                               List<AttributeRecord const> attributes)
710     Node *child;
711     gboolean loose;
712     gint i;
714     g_return_if_fail (repr != NULL);
716     if ( indent_level > 16 )
717         indent_level = 16;
719     if (add_whitespace) {
720         for ( i = 0 ; i < indent_level ; i++ ) {
721             out.writeString( "  " );
722         }
723     }
725     GQuark code = repr->code();
726     gchar const *element_name;
727     if ( elide_prefix == qname_prefix(code) ) {
728         element_name = qname_local_name(code);
729     } else {
730         element_name = g_quark_to_string(code);
731     }
732     out.printf( "<%s", element_name );
734     // if this is a <text> element, suppress formatting whitespace
735     // for its content and children:
736     gchar const *xml_space_attr = repr->attribute("xml:space");
737     if (xml_space_attr != NULL && !strcmp(xml_space_attr, "preserve")) {
738         add_whitespace = FALSE;
739     }
741     for ( List<AttributeRecord const> iter = attributes ;
742           iter ; ++iter )
743     {
744         out.writeString("\n");
745         for ( i = 0 ; i < indent_level + 1 ; i++ ) {
746             out.writeString("  ");
747         }
748         out.printf(" %s=\"", g_quark_to_string(iter->key));
749         repr_quote_write(out, iter->value);
750         out.writeChar('"');
751     }
753     loose = TRUE;
754     for (child = repr->firstChild() ; child != NULL; child = child->next()) {
755         if (child->type() == Inkscape::XML::TEXT_NODE) {
756             loose = FALSE;
757             break;
758         }
759     }
760     if (repr->firstChild()) {
761         out.writeString( ">" );
762         if (loose && add_whitespace) {
763             out.writeString( "\n" );
764         }
765         for (child = repr->firstChild(); child != NULL; child = child->next()) {
766             sp_repr_write_stream (child, out, (loose) ? (indent_level + 1) : 0, add_whitespace, elide_prefix);
767         }
769         if (loose && add_whitespace) {
770             for (i = 0; i < indent_level; i++) {
771                 out.writeString( "  " );
772             }
773         }
774         out.printf( "</%s>", element_name );
775     } else {
776         out.writeString( " />" );
777     }
779     // text elements cannot nest, so we can output newline
780     // after closing text
782     if (add_whitespace || !strcmp (repr->name(), "svg:text")) {
783         out.writeString( "\n" );
784     }
788 /*
789   Local Variables:
790   mode:c++
791   c-file-style:"stroustrup"
792   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
793   indent-tabs-mode:nil
794   fill-column:99
795   End:
796 */
797 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :