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