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