1 /*************************************************************************
2 ** HtmlSpecialHandler.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 <cassert>
23 #include <sstream>
24 #include "HtmlSpecialHandler.h"
25 #include "InputReader.h"
26 #include "Message.h"
27 #include "SVGTree.h"
28 #include "XMLNode.h"
29 
30 using namespace std;
31 
32 // variable to select the link marker variant (none, underlined, boxed, or colored background)
33 HtmlSpecialHandler::MarkerType HtmlSpecialHandler::MARKER_TYPE = HtmlSpecialHandler::MT_LINE;
34 Color HtmlSpecialHandler::LINK_BGCOLOR;
35 Color HtmlSpecialHandler::LINK_LINECOLOR;
36 bool HtmlSpecialHandler::USE_LINECOLOR = false;
37 
38 
preprocess(const char * prefix,istream & is,SpecialActions * actions)39 void HtmlSpecialHandler::preprocess (const char *prefix, istream &is, SpecialActions *actions) {
40 	if (!actions)
41 		return;
42 	_actions = actions;
43 	StreamInputReader ir(is);
44 	ir.skipSpace();
45 	// collect page number and ID of named anchors
46 	map<string,string> attribs;
47 	if (ir.check("<a ") && ir.parseAttributes(attribs, '"') > 0) {
48 		map<string,string>::iterator it;
49 		if ((it = attribs.find("name")) != attribs.end())
50 			preprocessNameAnchor(it->second);
51 		else if ((it = attribs.find("href")) != attribs.end())
52 			preprocessHrefAnchor(it->second);
53 	}
54 }
55 
56 
preprocessNameAnchor(const string & name)57 void HtmlSpecialHandler::preprocessNameAnchor (const string &name) {
58 	NamedAnchors::iterator it = _namedAnchors.find(name);
59 	if (it == _namedAnchors.end()) {  // anchor completely undefined?
60 		int id = _namedAnchors.size()+1;
61 		_namedAnchors[name] = NamedAnchor(_actions->getCurrentPageNumber(), id, 0);
62 	}
63 	else if (it->second.id < 0) {  // anchor referenced but not defined yet?
64 		it->second.id *= -1;
65 		it->second.pageno = _actions->getCurrentPageNumber();
66 	}
67 	else
68 		Message::wstream(true) << "named hyperref anchor '" << name << "' redefined\n";
69 }
70 
71 
preprocessHrefAnchor(const string & uri)72 void HtmlSpecialHandler::preprocessHrefAnchor (const string &uri) {
73 	if (uri[0] != '#')
74 		return;
75 	string name = uri.substr(1);
76 	NamedAnchors::iterator it = _namedAnchors.find(name);
77 	if (it != _namedAnchors.end())  // anchor already defined?
78 		it->second.referenced = true;
79 	else {
80 		int id = _namedAnchors.size()+1;
81 		_namedAnchors[name] = NamedAnchor(0, -id, 0, true);
82 	}
83 }
84 
85 
process(const char * prefix,istream & is,SpecialActions * actions)86 bool HtmlSpecialHandler::process (const char *prefix, istream &is, SpecialActions *actions) {
87 	if (!actions)
88 		return true;
89 	_actions = actions;
90 	StreamInputReader ir(is);
91 	ir.skipSpace();
92 	map<string,string> attribs;
93 	map<string,string>::iterator it;
94 	if (ir.check("<a ") && ir.parseAttributes(attribs, '"') > 0) {
95 		if ((it = attribs.find("href")) != attribs.end())   // <a href="URI">
96 			processHrefAnchor(it->second);
97 		else if ((it = attribs.find("name")) != attribs.end())  // <a name="ID">
98 			processNameAnchor(it->second);
99 		else
100 			return false;  // none or only invalid attributes
101 	}
102 	else if (ir.check("</a>"))
103 		closeAnchor();
104 	else if (ir.check("<img src=")) {
105 	}
106 	else if (ir.check("<base ") && ir.parseAttributes(attribs, '"') > 0 && (it = attribs.find("href")) != attribs.end())
107 		_base = it->second;
108 	return true;
109 }
110 
111 
112 /** Handles anchors with href attribute: <a href="URI">...</a>
113  *  @param uri value of href attribute */
processHrefAnchor(string uri)114 void HtmlSpecialHandler::processHrefAnchor (string uri) {
115 	closeAnchor();
116 	string name;
117 	if (uri[0] == '#') {  // reference to named anchor?
118 		name = uri.substr(1);
119 		NamedAnchors::iterator it = _namedAnchors.find(name);
120 		if (it == _namedAnchors.end() || it->second.id < 0)
121 			Message::wstream(true) << "reference to undefined anchor '" << name << "'\n";
122 		else {
123 			int id = it->second.id;
124 			uri = "#loc"+XMLString(id);
125 			if (_actions->getCurrentPageNumber() != it->second.pageno) {
126 				ostringstream oss;
127 				oss << _actions->getSVGFilename(it->second.pageno) << uri;
128 				uri = oss.str();
129 			}
130 		}
131 	}
132 	if (!_base.empty() && uri.find("://") != string::npos) {
133 		if (*_base.rbegin() != '/' && uri[0] != '/' && uri[0] != '#')
134 			uri = "/" + uri;
135 		uri = _base + uri;
136 	}
137 	XMLElementNode *anchor = new XMLElementNode("a");
138 	anchor->addAttribute("xlink:href", uri);
139 	anchor->addAttribute("xlink:title", name.empty() ? uri : name);
140 	_actions->pushContextElement(anchor);
141 	_actions->bbox("{anchor}", true);  // start computing the bounding box of the linked area
142 	_depthThreshold = _actions->getDVIStackDepth();
143 	_anchorType = AT_HREF;
144 }
145 
146 
147 /** Handles anchors with name attribute: <a name="NAME">...</a>
148  *  @param name value of name attribute */
processNameAnchor(const string & name)149 void HtmlSpecialHandler::processNameAnchor (const string &name) {
150 	closeAnchor();
151 	NamedAnchors::iterator it = _namedAnchors.find(name);
152 	assert(it != _namedAnchors.end());
153 	it->second.pos = _actions->getY();
154 	_anchorType = AT_NAME;
155 }
156 
157 
158 /** Handles the closing tag (</a> of the current anchor element. */
closeAnchor()159 void HtmlSpecialHandler::closeAnchor () {
160 	if (_anchorType == AT_HREF) {
161 		markLinkedBox();
162 		_actions->popContextElement();
163 		_depthThreshold = 0;
164 	}
165 	_anchorType = AT_NONE;
166 }
167 
168 
169 /** Marks a single rectangular area of the linked part of the document with a line or
170  *  a box so that it's noticeable by the user. Additionally, an invisible rectangle is
171  *  placed over this area in order to avoid flickering of the mouse cursor when moving
172  *  it over the hyperlinked area. */
markLinkedBox()173 void HtmlSpecialHandler::markLinkedBox () {
174 	const BoundingBox &bbox = _actions->bbox("{anchor}");
175 	if (bbox.width() > 0 && bbox.height() > 0) {  // does the bounding box extend in both dimensions?
176 		if (MARKER_TYPE != MT_NONE) {
177 			const double linewidth = min(0.5, bbox.height()/15);
178 			XMLElementNode *rect = new XMLElementNode("rect");
179 			double x = bbox.minX();
180 			double y = bbox.maxY()+linewidth;
181 			double w = bbox.width();
182 			double h = linewidth;
183 			const Color &linecolor = USE_LINECOLOR ? LINK_LINECOLOR : _actions->getColor();
184 			if (MARKER_TYPE == MT_LINE)
185 				rect->addAttribute("fill", linecolor.rgbString());
186 			else {
187 				x -= linewidth;
188 				y = bbox.minY()-linewidth;
189 				w += 2*linewidth;
190 				h += bbox.height()+linewidth;
191 				if (MARKER_TYPE == MT_BGCOLOR) {
192 					rect->addAttribute("fill", LINK_BGCOLOR.rgbString());
193 					if (USE_LINECOLOR) {
194 						rect->addAttribute("stroke", linecolor.rgbString());
195 						rect->addAttribute("stroke-width", linewidth);
196 					}
197 				}
198 				else {  // LM_BOX
199 					rect->addAttribute("fill", "none");
200 					rect->addAttribute("stroke", linecolor.rgbString());
201 					rect->addAttribute("stroke-width", linewidth);
202 				}
203 			}
204 			rect->addAttribute("x", x);
205 			rect->addAttribute("y", y);
206 			rect->addAttribute("width", w);
207 			rect->addAttribute("height", h);
208 			_actions->prependToPage(rect);
209 			if (MARKER_TYPE == MT_BOX || MARKER_TYPE == MT_BGCOLOR) {
210 				// slightly enlarge the boxed area
211 				x -= linewidth;
212 				y -= linewidth;
213 				w += 2*linewidth;
214 				h += 2*linewidth;
215 			}
216 			_actions->embed(BoundingBox(x, y, x+w, y+h));
217 		}
218 		// Create an invisible rectangle around the linked area so that it's easier to access.
219 		// This is only necessary when using paths rather than real text elements together with fonts.
220 		if (!SVGTree::USE_FONTS) {
221 			XMLElementNode *rect = new XMLElementNode("rect");
222 			rect->addAttribute("x", bbox.minX());
223 			rect->addAttribute("y", bbox.minY());
224 			rect->addAttribute("width", bbox.width());
225 			rect->addAttribute("height", bbox.height());
226 			rect->addAttribute("fill", "white");
227 			rect->addAttribute("fill-opacity", 0);
228 			_actions->appendToPage(rect);
229 		}
230 	}
231 }
232 
233 
234 /** This method is called every time the DVI position changes. */
dviMovedTo(double x,double y)235 void HtmlSpecialHandler::dviMovedTo (double x, double y) {
236 	if (_actions && _anchorType != AT_NONE) {
237 		// Start a new box if the current depth of the DVI stack underruns
238 		// the initial threshold which indicates a line break.
239 		if (_actions->getDVIStackDepth() < _depthThreshold) {
240 			markLinkedBox();
241 			_depthThreshold = _actions->getDVIStackDepth();
242 			_actions->bbox("{anchor}", true);  // start a new box on the new line
243 		}
244 	}
245 }
246 
247 
dviEndPage(unsigned pageno)248 void HtmlSpecialHandler::dviEndPage (unsigned pageno) {
249 	if (_actions) {
250 		// create views for all collected named anchors defined on the recent page
251 		const BoundingBox &pagebox = _actions->bbox();
252 		for (NamedAnchors::iterator it=_namedAnchors.begin(); it != _namedAnchors.end(); ++it) {
253 			if (it->second.pageno == pageno && it->second.referenced) {  // current anchor referenced?
254 				ostringstream oss;
255 				oss << pagebox.minX() << ' ' << it->second.pos << ' '
256 					 << pagebox.width() << ' ' << pagebox.height();
257 				XMLElementNode *view = new XMLElementNode("view");
258 				view->addAttribute("id", "loc"+XMLString(it->second.id));
259 				view->addAttribute("viewBox", oss.str());
260 				_actions->appendToDefs(view);
261 			}
262 		}
263 		closeAnchor();
264 		_actions = 0;
265 	}
266 }
267 
268 
269 /** Sets the appearance of the link markers.
270  *  @param[in] marker string specifying the marker (format: type[:linecolor])
271  *  @return true on success */
setLinkMarker(const string & marker)272 bool HtmlSpecialHandler::setLinkMarker (const string &marker) {
273 	string type;  // "none", "box", "line", or a background color specifier
274 	string color; // optional line color specifier
275 	size_t seppos = marker.find(":");
276 	if (seppos == string::npos)
277 		type = marker;
278 	else {
279 		type = marker.substr(0, seppos);
280 		color = marker.substr(seppos+1);
281 	}
282 	if (type.empty() || type == "none")
283 		MARKER_TYPE = MT_NONE;
284 	else if (type == "line")
285 		MARKER_TYPE = MT_LINE;
286 	else if (type == "box")
287 		MARKER_TYPE = MT_BOX;
288 	else {
289 		if (!LINK_BGCOLOR.setName(type, false))
290 			return false;
291 		MARKER_TYPE = MT_BGCOLOR;
292 	}
293 	USE_LINECOLOR = false;
294 	if (MARKER_TYPE != MT_NONE && !color.empty()) {
295 		if (!LINK_LINECOLOR.setName(color, false))
296 			return false;
297 		USE_LINECOLOR = true;
298 	}
299 	return true;
300 }
301 
302 
prefixes() const303 const char** HtmlSpecialHandler::prefixes () const {
304 	static const char *pfx[] = {"html:", 0};
305 	return pfx;
306 }
307