1 /*************************************************************************
2 ** SVGTree.cpp                                                          **
3 **                                                                      **
4 ** This file is part of dvisvgm -- the DVI to SVG converter             **
5 ** Copyright (C) 2005-2015 Martin Gieseking <martin.gieseking@uos.de>   **
6 **                                                                      **
7 ** This program is free software; you can redistribute it and/or        **
8 ** modify it under the terms of the GNU General Public License as       **
9 ** published by the Free Software Foundation; either version 3 of       **
10 ** the License, or (at your option) any later version.                  **
11 **                                                                      **
12 ** This program is distributed in the hope that it will be useful, but  **
13 ** WITHOUT ANY WARRANTY; without even the implied warranty of           **
14 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the         **
15 ** GNU General Public License for more details.                         **
16 **                                                                      **
17 ** You should have received a copy of the GNU General Public License    **
18 ** along with this program; if not, see <http://www.gnu.org/licenses/>. **
19 *************************************************************************/
20 
21 #include <config.h>
22 #include <algorithm>
23 #include <sstream>
24 #include <string>
25 #include "BoundingBox.h"
26 #include "DependencyGraph.h"
27 #include "DVIToSVG.h"
28 #include "Font.h"
29 #include "FontManager.h"
30 #include "SVGTree.h"
31 #include "XMLDocument.h"
32 #include "XMLNode.h"
33 #include "XMLString.h"
34 
35 using namespace std;
36 
37 
38 // static class variables
39 bool SVGTree::CREATE_STYLE=true;
40 bool SVGTree::USE_FONTS=true;
41 bool SVGTree::CREATE_USE_ELEMENTS=false;
42 bool SVGTree::RELATIVE_PATH_CMDS=false;
43 bool SVGTree::MERGE_CHARS=true;
44 double SVGTree::ZOOM_FACTOR=1.0;
45 
46 
SVGTree()47 SVGTree::SVGTree () : _vertical(false), _font(0), _color(Color::BLACK), _matrix(1) {
48 	_xchanged = _ychanged = false;
49 	_fontnum = 0;
50 	reset();
51 }
52 
53 
54 /** Clears the SVG tree and initializes the root element. */
reset()55 void SVGTree::reset () {
56 	_doc.clear();
57 	_root = new XMLElementNode("svg");
58 	_root->addAttribute("version", "1.1");
59 	_root->addAttribute("xmlns", "http://www.w3.org/2000/svg");
60 	_root->addAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
61 	_doc.setRootNode(_root);
62 	_page = _text = _span = _defs = 0;
63 }
64 
65 
66 /** Sets the bounding box of the document.
67  *  @param[in] bbox bounding box in PS point units */
setBBox(const BoundingBox & bbox)68 void SVGTree::setBBox (const BoundingBox &bbox) {
69 	if (ZOOM_FACTOR >= 0) {
70 		_root->addAttribute("width", XMLString(bbox.width()*ZOOM_FACTOR)+"pt");
71 		_root->addAttribute("height", XMLString(bbox.height()*ZOOM_FACTOR)+"pt");
72 	}
73 	_root->addAttribute("viewBox", bbox.toSVGViewBox());
74 }
75 
76 
setColor(const Color & c)77 void SVGTree::setColor (const Color &c) {
78 	if (!_font.get() || _font.get()->color() == Color::BLACK)
79 		_color.set(c);
80 }
81 
82 
setFont(int num,const Font * font)83 void SVGTree::setFont (int num, const Font *font) {
84 	_font.set(font);
85 	_fontnum = num;
86 	// set default color assigned to the font
87 	if (font->color() != Color::BLACK && _color.get() != font->color())
88 		_color.set(font->color());
89 }
90 
91 
92 /** Starts a new page.
93  *  @param[in] pageno number of new page */
newPage(int pageno)94 void SVGTree::newPage (int pageno) {
95 	_page = new XMLElementNode("g");
96 	if (pageno >= 0)
97 		_page->addAttribute("id", string("page")+XMLString(pageno));
98 	_root->append(_page);
99 	_text = _span = 0;
100 	while (!_pageContainerStack.empty())
101 		_pageContainerStack.pop();
102 }
103 
104 
appendToDefs(XMLNode * node)105 void SVGTree::appendToDefs (XMLNode *node) {
106 	if (!_defs) {
107 		_defs = new XMLElementNode("defs");
108 		_root->prepend(_defs);
109 	}
110 	_defs->append(node);
111 }
112 
113 
appendToPage(XMLNode * node)114 void SVGTree::appendToPage (XMLNode *node) {
115 	if (_pageContainerStack.empty())
116 		_page->append(node);
117 	else
118 		_pageContainerStack.top()->append(node);
119 }
120 
121 
prependToPage(XMLNode * node)122 void SVGTree::prependToPage (XMLNode *node) {
123 	if (_pageContainerStack.empty())
124 		_page->prepend(node);
125 	else
126 		_pageContainerStack.top()->prepend(node);
127 }
128 
129 
130 /** Appends a single charater to the current text node. If necessary, and depending on output mode
131  *  and further output states, new XML elements (text, tspan, g, ...) are created.
132  *  @param[in] c character to be added
133  *  @param[in] x x coordinate
134  *  @param[in] y y coordinate
135  *  @param[in] font font to be used */
appendChar(int c,double x,double y,const Font & font)136 void SVGTree::appendChar (int c, double x, double y, const Font &font) {
137 	XMLElementNode *node=_span;
138 	if (USE_FONTS) {
139 		// changes of fonts and transformations require a new text element
140 		if (!MERGE_CHARS || !_text || _font.changed() || _matrix.changed() || _vertical.changed()) {
141 			newTextNode(x, y);
142 			node = _text;
143 			_color.changed(true);
144 		}
145 		if (MERGE_CHARS && (_xchanged || _ychanged || (_color.changed() && _color.get() != Color::BLACK))) {
146 			// if drawing position was explicitly changed, create a new tspan element
147 			_span = new XMLElementNode("tspan");
148 			if (_xchanged) {
149 				if (_vertical) {
150 					// align glyphs designed for horizontal layout properly
151 					if (const PhysicalFont *pf = dynamic_cast<const PhysicalFont*>(_font.get()))
152 						if (!pf->getMetrics()->verticalLayout())
153 							x += pf->scaledAscent()/2.5; // move vertical baseline to the right by strikethrough offset
154 				}
155 				_span->addAttribute("x", x);
156 				_xchanged = false;
157 			}
158 			if (_ychanged) {
159 				_span->addAttribute("y", y);
160 				_ychanged = false;
161 			}
162 			if (_color.get() != font.color()) {
163 					_span->addAttribute("fill", _color.get().rgbString());
164 				_color.changed(false);
165 			}
166 			_text->append(_span);
167 			node = _span;
168 		}
169 		if (!node) {
170 			if (!_text)
171 				newTextNode(x, y);
172 			node = _text;
173 		}
174 		node->append(XMLString(font.unicode(c), false));
175 		if (!MERGE_CHARS && _color.get() != font.color()) {
176 			node->addAttribute("fill", _color.get().rgbString());
177 			_color.changed(false);
178 		}
179 	}
180 	else {
181 		if (_color.changed() || _matrix.changed()) {
182 			bool set_color = (_color.changed() && _color.get() != Color::BLACK);
183 			bool set_matrix = (_matrix.changed() && !_matrix.get().isIdentity());
184 			if (set_color || set_matrix) {
185 				_span = new XMLElementNode("g");
186 				if (_color.get() != Color::BLACK)
187 					_span->addAttribute("fill", _color.get().rgbString());
188 				if (!_matrix.get().isIdentity())
189 					_span->addAttribute("transform", _matrix.get().getSVG());
190 				appendToPage(_span);
191 				node = _span;
192 				_color.changed(false);
193 				_matrix.changed(false);
194 			}
195 			else if (_color.get() == Color::BLACK && _matrix.get().isIdentity())
196 				node = _span = 0;
197 		}
198 		if (!node)
199 			node = _pageContainerStack.empty() ? _page : _pageContainerStack.top();
200 		if (font.verticalLayout()) {
201 			// move glyph graphics so that its origin is located at the top center position
202 			GlyphMetrics metrics;
203 			font.getGlyphMetrics(c, _vertical, metrics);
204 			x -= metrics.wl;
205 			if (const PhysicalFont *pf = dynamic_cast<const PhysicalFont*>(&font)) {
206 				// Center glyph between top and bottom border of the TFM box.
207 				// This is just an approximation used until I find a way to compute
208 				// the exact location in vertical mode.
209 				GlyphMetrics exact_metrics;
210 				pf->getExactGlyphBox(c, exact_metrics, false);
211 				y += exact_metrics.h+(metrics.d-exact_metrics.h-exact_metrics.d)/2;
212 			}
213 			else
214 				y += metrics.d;
215 		}
216 		Matrix rotation(1);
217 		if (_vertical && !font.verticalLayout()) {
218 			// alphabetic text designed for horizontal mode
219 			// must be rotated by 90 degrees if in vertical mode
220 			rotation.translate(-x, -y);
221 			rotation.rotate(90);
222 			rotation.translate(x, y);
223 		}
224 		if (CREATE_USE_ELEMENTS) {
225 			ostringstream oss;
226 			oss << "#g" << FontManager::instance().fontID(_font) << '-' << c;
227 			XMLElementNode *use = new XMLElementNode("use");
228 			use->addAttribute("x", XMLString(x));
229 			use->addAttribute("y", XMLString(y));
230 			use->addAttribute("xlink:href", oss.str());
231 			if (!rotation.isIdentity())
232 				use->addAttribute("transform", rotation.getSVG());
233 			node->append(use);
234 		}
235 		else {
236 			Glyph glyph;
237 			const PhysicalFont *pf = dynamic_cast<const PhysicalFont*>(&font);
238 			if (pf && pf->getGlyph(c, glyph)) {
239 				double sx = pf->scaledSize()/pf->unitsPerEm();
240 				double sy = -sx;
241 				ostringstream oss;
242 				glyph.writeSVG(oss, RELATIVE_PATH_CMDS, sx, sy, x, y);
243 				XMLElementNode *glyph_node = new XMLElementNode("path");
244 				glyph_node->addAttribute("d", oss.str());
245 				if (!rotation.isIdentity())
246 					glyph_node->addAttribute("transform", rotation.getSVG());
247 				node->append(glyph_node);
248 			}
249 		}
250 	}
251 }
252 
253 
254 /** Creates a new text element. This is a helper function used by appendChar().
255  *  @param[in] x current x coordinate
256  *  @param[in] y current y coordinate */
newTextNode(double x,double y)257 void SVGTree::newTextNode (double x, double y) {
258 	_text = new XMLElementNode("text");
259 	_span = 0; // no tspan in text element yet
260 	if (USE_FONTS) {
261 		const Font *font = _font.get();
262 		if (CREATE_STYLE || !font)
263 			_text->addAttribute("class", string("f")+XMLString(_fontnum));
264 		else {
265 			_text->addAttribute("font-family", font->name());
266 			_text->addAttribute("font-size", XMLString(font->scaledSize()));
267 			if (font->color() != Color::BLACK)
268 				_text->addAttribute("fill", font->color().rgbString());
269 		}
270 		if (_vertical) {
271 			_text->addAttribute("writing-mode", "tb");
272 			// align glyphs designed for horizontal layout properly
273 			if (const PhysicalFont *pf = dynamic_cast<const PhysicalFont*>(_font.get()))
274 				if (!pf->getMetrics()->verticalLayout()) { // alphabetic text designed for horizontal layout?
275 					x += pf->scaledAscent()/2.5; // move vertical baseline to the right by strikethrough offset
276 					_text->addAttribute("glyph-orientation-vertical", 90); // ensure rotation
277 				}
278 		}
279 	}
280 	_text->addAttribute("x", x);
281 	_text->addAttribute("y", y);
282 	if (!_matrix.get().isIdentity())
283 		_text->addAttribute("transform", _matrix.get().getSVG());
284 	appendToPage(_text);
285 	_vertical.changed(false);
286 	_font.changed(false);
287 	_matrix.changed(false);
288 	_xchanged = false;
289 	_ychanged = false;
290 }
291 
292 
transformPage(const Matrix * usermatrix)293 void SVGTree::transformPage (const Matrix *usermatrix) {
294 	if (usermatrix && !usermatrix->isIdentity())
295 		_page->addAttribute("transform", usermatrix->getSVG());
296 }
297 
298 
299 /** Creates an SVG element for a single glyph.
300  *  @param[in] c character number
301  *  @param[in] font font to extract the glyph from
302  *  @param[in] cb pointer to callback object for sending feedback to the glyph tracer (may be 0)
303  *  @return pointer to element node if glyph exists, 0 otherwise */
createGlyphNode(int c,const PhysicalFont & font,GFGlyphTracer::Callback * cb)304 static XMLElementNode* createGlyphNode (int c, const PhysicalFont &font, GFGlyphTracer::Callback *cb) {
305 	Glyph glyph;
306 	if (!font.getGlyph(c, glyph, cb) || (!SVGTree::USE_FONTS && !SVGTree::CREATE_USE_ELEMENTS))
307 		return 0;
308 
309 	double sx=1.0, sy=1.0;
310 	double upem = font.unitsPerEm();
311 	XMLElementNode *glyph_node=0;
312 	if (SVGTree::USE_FONTS) {
313 		double extend = font.style() ? font.style()->extend : 1;
314 		glyph_node = new XMLElementNode("glyph");
315 		glyph_node->addAttribute("unicode", XMLString(font.unicode(c), false));
316 		glyph_node->addAttribute("horiz-adv-x", XMLString(font.hAdvance(c)*extend));
317 		glyph_node->addAttribute("vert-adv-y", XMLString(font.vAdvance(c)));
318 		string name = font.glyphName(c);
319 		if (!name.empty())
320 			glyph_node->addAttribute("glyph-name", name);
321 	}
322 	else {
323 		ostringstream oss;
324 		oss << 'g' << FontManager::instance().fontID(&font) << '-' << c;
325 		glyph_node = new XMLElementNode("path");
326 		glyph_node->addAttribute("id", oss.str());
327 		sx = font.scaledSize()/upem;
328 		sy = -sx;
329 	}
330 	ostringstream oss;
331 	glyph.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS, sx, sy);
332 	glyph_node->addAttribute("d", oss.str());
333 	return glyph_node;
334 }
335 
336 
appendFontStyles(const set<const Font * > & fonts)337 void SVGTree::appendFontStyles (const set<const Font*> &fonts) {
338 	if (CREATE_STYLE && USE_FONTS && !fonts.empty() && _defs) {
339 		XMLElementNode *styleNode = new XMLElementNode("style");
340 		styleNode->addAttribute("type", "text/css");
341 		_root->insertAfter(styleNode, _defs);
342 		typedef map<int, const Font*> SortMap;
343 		SortMap sortmap;
344 		FORALL(fonts, set<const Font*>::const_iterator, it)
345 			if (!dynamic_cast<const VirtualFont*>(*it))   // skip virtual fonts
346 				sortmap[FontManager::instance().fontID(*it)] = *it;
347 		ostringstream style;
348 		// add font style definitions in ascending order
349 		FORALL(sortmap, SortMap::const_iterator, it) {
350 			style << "text.f"     << it->first << ' '
351 				<< "{font-family:" << it->second->name()
352 				<< ";font-size:"   << XMLString(it->second->scaledSize()) << "px";
353 			if (it->second->color() != Color::BLACK)
354 				style << ";fill:" << it->second->color().rgbString();
355 			style << "}\n";
356 		}
357 		XMLCDataNode *cdata = new XMLCDataNode(style.str());
358 		styleNode->append(cdata);
359 	}
360 }
361 
362 
363 /** Appends glyph definitions of a given font to the defs section of the SVG tree.
364  *  @param[in] font font to be appended
365  *  @param[in] chars codes of the characters whose glyph outlines should be appended
366  *  @param[in] cb pointer to callback object for sending feedback to the glyph tracer (may be 0) */
append(const PhysicalFont & font,const set<int> & chars,GFGlyphTracer::Callback * cb)367 void SVGTree::append (const PhysicalFont &font, const set<int> &chars, GFGlyphTracer::Callback *cb) {
368 	if (chars.empty())
369 		return;
370 
371 	if (USE_FONTS) {
372 		XMLElementNode *fontNode = new XMLElementNode("font");
373 		string fontname = font.name();
374 		fontNode->addAttribute("id", fontname);
375 		fontNode->addAttribute("horiz-adv-x", XMLString(font.hAdvance()));
376 		appendToDefs(fontNode);
377 
378 		XMLElementNode *faceNode = new XMLElementNode("font-face");
379 		faceNode->addAttribute("font-family", fontname);
380 		faceNode->addAttribute("units-per-em", XMLString(font.unitsPerEm()));
381 		if (font.type() != PhysicalFont::MF && !font.verticalLayout()) {
382 			faceNode->addAttribute("ascent", XMLString(font.ascent()));
383 			faceNode->addAttribute("descent", XMLString(font.descent()));
384 		}
385 		fontNode->append(faceNode);
386 		FORALL(chars, set<int>::const_iterator, i)
387 			fontNode->append(createGlyphNode(*i, font, cb));
388 	}
389 	else if (CREATE_USE_ELEMENTS && &font != font.uniqueFont()) {
390 		// If the same character is used in various sizes, we don't want to embed the complete (lengthy) path
391 		// descriptions multiple times. Because they would only differ by a scale factor, it's better to
392 		// reference the already embedded path together with a transformation attribute and let the SVG renderer
393 		// scale the glyphs properly. This is only necessary if we don't want to use font but path elements.
394 		FORALL(chars, set<int>::const_iterator, it) {
395 			ostringstream oss;
396 			XMLElementNode *use = new XMLElementNode("use");
397 			oss << 'g' << FontManager::instance().fontID(&font) << '-' << *it;
398 			use->addAttribute("id", oss.str());
399 			oss.str("");
400 			oss << "#g" << FontManager::instance().fontID(font.uniqueFont()) << '-' << *it;
401 			use->addAttribute("xlink:href", oss.str());
402 			double scale = font.scaledSize()/font.uniqueFont()->scaledSize();
403 			if (scale != 1.0) {
404 				oss.str("");
405 				oss << "scale(" << scale << ')';
406 				use->addAttribute("transform", oss.str());
407 			}
408 			appendToDefs(use);
409 		}
410 	}
411 	else {
412 		FORALL(chars, set<int>::const_iterator, i)
413 			appendToDefs(createGlyphNode(*i, font, cb));
414 	}
415 }
416 
417 
418 /** Pushes a new context element that will take all following nodes added to the page. */
pushContextElement(XMLElementNode * node)419 void SVGTree::pushContextElement (XMLElementNode *node) {
420 	if (_pageContainerStack.empty())
421 		_page->append(node);
422 	else
423 		_pageContainerStack.top()->append(node);
424 	_pageContainerStack.push(node);
425 	_text = _span = 0;  // ensure the creation of a new text element for the following characters added
426 }
427 
428 
429 /** Pops the current context element and restored the previous one. */
popContextElement()430 void SVGTree::popContextElement () {
431 	if (!_pageContainerStack.empty()) {
432 		_pageContainerStack.pop();
433 		_text = _span = 0; // ensure the creation of a new text element for the following characters added
434 	}
435 }
436 
437 
438 /** Extracts the ID from a local URL reference like url(#abcde) */
extract_id_from_url(const string & url)439 inline string extract_id_from_url (const string &url) {
440 	return url.substr(5, url.length()-6);
441 }
442 
443 
444 /** Removes elements present in the SVH tree that are not required.
445  *  For now, only clipPath elements are removed. */
removeRedundantElements()446 void SVGTree::removeRedundantElements () {
447 	vector<XMLElementNode*> clipElements;
448 	if (!_defs || !_defs->getDescendants("clipPath", 0, clipElements))
449 		return;
450 
451 	// collect dependencies between clipPath elements in the defs section of the SVG tree
452 	DependencyGraph<string> idTree;
453 	for (vector<XMLElementNode*>::iterator it=clipElements.begin(); it != clipElements.end(); ++it) {
454 		if (const char *id = (*it)->getAttributeValue("id")) {
455 			if (const char *url = (*it)->getAttributeValue("clip-path"))
456 				idTree.insert(extract_id_from_url(url), id);
457 			else
458 				idTree.insert(id);
459 		}
460 	}
461 	// collect elements that reference a clipPath (have a clip-path attribute)
462 	vector<XMLElementNode*> descendants;
463 	_page->getDescendants(0, "clip-path", descendants);
464 	// remove referenced IDs and their dependencies from the dependency graph
465 	for (vector<XMLElementNode*>::iterator it=descendants.begin(); it != descendants.end(); ++it) {
466 		string idref = extract_id_from_url((*it)->getAttributeValue("clip-path"));
467 		idTree.removeDependencyPath(idref);
468 	}
469 	descendants.clear();
470 	vector<string> ids;
471 	idTree.getKeys(ids);
472 	for (vector<string>::iterator it=ids.begin(); it != ids.end(); ++it) {
473 		XMLElementNode *node = _defs->getFirstDescendant("clipPath", "id", it->c_str());
474 		_defs->remove(node);
475 	}
476 }
477 
478