/************************************************************************* ** DvisvgmSpecialHandler.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2021 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include #include #include "Calculator.hpp" #include "DvisvgmSpecialHandler.hpp" #include "InputBuffer.hpp" #include "InputReader.hpp" #include "Length.hpp" #include "SpecialActions.hpp" #include "SVGTree.hpp" #include "utility.hpp" #include "XMLNode.hpp" #include "XMLString.hpp" using namespace std; DvisvgmSpecialHandler::DvisvgmSpecialHandler () : _currentMacro(_macros.end()), _defsParser(&SVGTree::appendToDefs, &SVGTree::pushDefsContext, &SVGTree::popDefsContext), _pageParser(&SVGTree::appendToPage, &SVGTree::pushPageContext, &SVGTree::popPageContext) { } void DvisvgmSpecialHandler::preprocess (const string&, istream &is, SpecialActions&) { struct Command { const char *name; void (DvisvgmSpecialHandler::*handler)(InputReader&); }; constexpr array commands {{ {"raw", &DvisvgmSpecialHandler::preprocessRaw}, {"rawdef", &DvisvgmSpecialHandler::preprocessRawDef}, {"rawset", &DvisvgmSpecialHandler::preprocessRawSet}, {"endrawset", &DvisvgmSpecialHandler::preprocessEndRawSet}, {"rawput", &DvisvgmSpecialHandler::preprocessRawPut} }}; StreamInputReader ir(is); const string cmdstr = ir.getWord(); auto it = find_if(commands.begin(), commands.end(), [&](const Command &cmd) { return cmd.name == cmdstr; }); if (it != commands.end()) { ir.skipSpace(); (this->*it->handler)(ir); } } void DvisvgmSpecialHandler::preprocessRawSet (InputReader &ir) { _nestingLevel++; string id = ir.getString(); if (id.empty()) throw SpecialException("definition of unnamed SVG fragment"); if (_nestingLevel > 1) throw SpecialException("nested definition of SVG fragment '" + id + "'"); _currentMacro = _macros.find(id); if (_currentMacro != _macros.end()) { _currentMacro = _macros.end(); throw SpecialException("redefinition of SVG fragment '" + id + "'"); } pair state = _macros.emplace(id, StringVector()); _currentMacro = state.first; } void DvisvgmSpecialHandler::preprocessEndRawSet (InputReader&) { if (_nestingLevel > 0 && --_nestingLevel == 0) _currentMacro = _macros.end(); } void DvisvgmSpecialHandler::preprocessRaw (InputReader &ir) { if (_currentMacro == _macros.end()) return; string str = ir.getLine(); if (!str.empty()) _currentMacro->second.emplace_back("P"+str); } void DvisvgmSpecialHandler::preprocessRawDef (InputReader &ir) { if (_currentMacro == _macros.end()) return; string str = ir.getLine(); if (!str.empty()) _currentMacro->second.emplace_back("D"+str); } void DvisvgmSpecialHandler::preprocessRawPut (InputReader &ir) { if (_currentMacro != _macros.end()) throw SpecialException("dvisvgm:rawput not allowed inside rawset/endrawset"); } /** Evaluates and executes a dvisvgm special statement. * @param[in] prefix special prefix read by the SpecialManager * @param[in] is the special statement is read from this stream * @param[in] actions object providing the actions that can be performed by the SpecialHandler */ bool DvisvgmSpecialHandler::process (const string &prefix, istream &is, SpecialActions &actions) { struct Command { const char *name; void (DvisvgmSpecialHandler::*handler)(InputReader&, SpecialActions&); }; constexpr array commands {{ {"raw", &DvisvgmSpecialHandler::processRaw}, {"rawdef", &DvisvgmSpecialHandler::processRawDef}, {"rawset", &DvisvgmSpecialHandler::processRawSet}, {"endrawset", &DvisvgmSpecialHandler::processEndRawSet}, {"rawput", &DvisvgmSpecialHandler::processRawPut}, {"bbox", &DvisvgmSpecialHandler::processBBox}, {"img", &DvisvgmSpecialHandler::processImg} }}; StreamInputReader ir(is); const string cmdstr = ir.getWord(); auto it = find_if(commands.begin(), commands.end(), [&](const Command &cmd) { return cmd.name == cmdstr; }); if (it != commands.end()) { ir.skipSpace(); (this->*it->handler)(ir, actions); } return true; } /** Replaces constants of the form {?name} by their corresponding value. * @param[in,out] str text to expand * @param[in] actions interfcae to the world outside the special handler */ static void expand_constants (string &str, SpecialActions &actions) { bool repl_bbox = true; while (repl_bbox) { size_t pos = str.find("{?bbox "); if (pos == string::npos) repl_bbox = false; else { size_t endpos = pos+7; while (endpos < str.length() && isalnum(str[endpos])) ++endpos; if (str[endpos] == '}') { BoundingBox &box=actions.bbox(str.substr(pos+7, endpos-pos-7)); str.replace(pos, endpos-pos+1, box.toSVGViewBox()); } else repl_bbox = false; } } struct Constant { const char *name; string val; }; const array constants {{ {"x", XMLString(actions.getX())}, {"y", XMLString(actions.getY())}, {"color", actions.getColor().svgColorString()}, {"matrix", actions.getMatrix().toSVG()}, {"nl", "\n"}, }}; for (const Constant &constant : constants) { const string pattern = string("{?")+constant.name+"}"; size_t pos = str.find(pattern); while (pos != string::npos) { str.replace(pos, strlen(constant.name)+3, constant.val); pos = str.find(pattern, pos+constant.val.length()); // look for further matches } } } /** Evaluates substrings of the form {?(expr)} where 'expr' is a math expression, * and replaces the substring by the computed value. * @param[in,out] str string to scan for expressions */ static void evaluate_expressions (string &str, const SpecialActions &actions) { size_t left = str.find("{?("); // start position of expression macro while (left != string::npos) { size_t right = str.find(")}", left+2); // end position of expression macro if (right == string::npos) break; Calculator calc; calc.setVariable("x", actions.getX()); calc.setVariable("y", actions.getY()); string expr = str.substr(left+3, right-left-3); // math expression to evaluate if (util::normalize_space(expr).empty()) // no expression given, e.g. {?( )} str.erase(left, right-left+2); // => replace with empty string else { try { double val = calc.eval(expr); XMLString valstr(val); str.replace(left, right-left+2, valstr); right = left+valstr.length()-1; } catch (CalculatorException &e) { throw SpecialException(string(e.what())+" in '{?("+expr+")}'"); } } left = str.find("{?(", right+1); // find next expression macro } } /** Processes raw SVG fragments from the input stream. The SVG data must represent * a single or multiple syntactically complete XML parts, like opening/closing tags, * comments, or CDATA blocks. These must not be split and distributed over several * 'raw' statements. Elements can be split but element tags can't. * Example: "" is ok. */ void DvisvgmSpecialHandler::processRaw (InputReader &ir, SpecialActions &actions) { if (_nestingLevel == 0) { string xml = ir.getLine(); if (!xml.empty()) { evaluate_expressions(xml, actions); expand_constants(xml, actions); _pageParser.parse(xml, actions); } } } void DvisvgmSpecialHandler::processRawDef (InputReader &ir, SpecialActions &actions) { if (_nestingLevel == 0) { string xml = ir.getLine(); if (!xml.empty()) { expand_constants(xml, actions); _defsParser.parse(xml, actions); } } } void DvisvgmSpecialHandler::processRawSet (InputReader&, SpecialActions&) { _nestingLevel++; } void DvisvgmSpecialHandler::processEndRawSet (InputReader&, SpecialActions&) { if (_nestingLevel > 0) _nestingLevel--; } void DvisvgmSpecialHandler::processRawPut (InputReader &ir, SpecialActions &actions) { if (_nestingLevel > 0) return; string id = ir.getString(); auto it = _macros.find(id); if (it == _macros.end()) throw SpecialException("undefined SVG fragment '" + id + "' referenced"); StringVector &defvector = it->second; for (string &defstr : defvector) { char &type = defstr[0]; string def = defstr.substr(1); if ((type == 'P' || type == 'D') && !def.empty()) { expand_constants(def, actions); if (type == 'P') _pageParser.parse(def, actions); else { // type == 'D' _defsParser.parse(def, actions); type = 'L'; // locked } } } } /** Embeds the virtual rectangle (x, y ,w , h) into the current bounding box, * where (x,y) is the lower left vertex composed of the current DVI position. * @param[in] w width of the rectangle in PS point units * @param[in] h height of the rectangle in PS point units * @param[in] d depth of the rectangle in PS point units * @param[in] transform if true, apply the current transformation matrix to the rectangle * @param[in] actions object providing the actions that can be performed by the SpecialHandler */ static void update_bbox (Length w, Length h, Length d, bool transform, SpecialActions &actions) { double x = actions.getX(); double y = actions.getY(); BoundingBox bbox1(x, y, x+w.bp(), y-h.bp()); BoundingBox bbox2(x, y, x+w.bp(), y+d.bp()); if (transform) { bbox1.transform(actions.getMatrix()); bbox2.transform(actions.getMatrix()); } actions.embed(bbox1); actions.embed(bbox2); } /** Reads a length value including a trailing unit specifier and returns it. */ static Length read_length (InputReader &ir) { Length length; ir.skipSpace(); if (!isalpha(ir.peek())) { double val = ir.getDouble(); string unit = isalpha(ir.peek()) ? ir.getString(2) : "pt"; length = Length(val, unit); } return length; } /** Evaluates the special dvisvgm:bbox. * variant 1: dvisvgm:bbox [r[el]] [] [transform] * variant 2: dvisvgm:bbox a[bs] [transform] * variant 3: dvisvgm:bbox f[ix] [transform] * variant 4: dvisvgm:bbox n[ew] * variant 5: dvisvgm:bbox lock | unlock */ void DvisvgmSpecialHandler::processBBox (InputReader &ir, SpecialActions &actions) { ir.skipSpace(); if (ir.check("lock")) actions.bbox().lock(); else if (ir.check("unlock")) actions.bbox().unlock(); else { int c = ir.peek(); try { if (!isalpha(c)) c = 'r'; // no mode specifier => relative box parameters else { while (!isspace(ir.peek())) // skip trailing characters ir.get(); if (c == 'n') { // "new": create new local bounding box ir.skipSpace(); string name; while (isalnum(ir.peek())) name += char(ir.get()); ir.skipSpace(); if (!name.empty() && ir.eof()) actions.bbox(name, true); // create new user box } else if (c == 'a' || c == 'f') { // "abs" or "fix" Length lengths[4]; for (Length &len : lengths) len = read_length(ir); BoundingBox b(lengths[0], lengths[1], lengths[2], lengths[3]); ir.skipSpace(); if (ir.check("transform")) b.transform(actions.getMatrix()); if (c == 'a') actions.embed(b); else { actions.bbox() = b; actions.bbox().lock(); } } } if (c == 'r') { Length w = read_length(ir); Length h = read_length(ir); Length d = read_length(ir); ir.skipSpace(); update_bbox(w, h, d, ir.check("transform"), actions); } } catch (const UnitException &e) { throw SpecialException(string("dvisvgm:bbox: ") + e.what()); } } } void DvisvgmSpecialHandler::processImg (InputReader &ir, SpecialActions &actions) { try { Length w = read_length(ir); Length h = read_length(ir); string f = ir.getString(); update_bbox(w, h, Length(0), false, actions); auto img = util::make_unique("image"); img->addAttribute("x", actions.getX()); img->addAttribute("y", actions.getY()); img->addAttribute("width", w.bp()); img->addAttribute("height", h.bp()); img->addAttribute("xlink:href", f); img->setTransform(actions.getMatrix()); actions.svgTree().appendToPage(std::move(img)); } catch (const UnitException &e) { throw SpecialException(string("dvisvgm:img: ") + e.what()); } } void DvisvgmSpecialHandler::dviPreprocessingFinished () { string id; if (_currentMacro != _macros.end()) id = _currentMacro->first; // ensure all pattern definitions are closed after pre-processing the whole DVI file _currentMacro = _macros.end(); _nestingLevel = 0; if (!id.empty()) throw SpecialException("missing dvisvgm:endrawset for SVG fragment '" + id + "'"); } void DvisvgmSpecialHandler::dviEndPage (unsigned, SpecialActions &actions) { _defsParser.finish(actions); _pageParser.finish(actions); actions.bbox().unlock(); for (auto &strvecpair : _macros) { StringVector &vec = strvecpair.second; for (string &str : vec) { // activate locked parts of a pattern again if (str[0] == 'L') str[0] = 'D'; } } } vector DvisvgmSpecialHandler::prefixes() const { vector pfx {"dvisvgm:"}; return pfx; } //////////////////////////////////////////////////////////////////////////////// /** Parses a fragment of XML code, creates corresponding XML nodes and adds them * to the SVG tree. The code may be split and processed by several calls of this * function. Incomplete chunks that can't be processed yet are stored and picked * up again together with the next incoming XML fragment. If a call of this function * is supposed to finish the parsing of an XML subtree, parameter 'finish' must be set. * @param[in] xml XML fragment to parse * @param[in] actions object providing the SVG tree functions * @param[in] finish if true, no more XML is expected and parsing is finished */ void DvisvgmSpecialHandler::XMLParser::parse (const string &xml, SpecialActions &actions, bool finish) { // collect/extract an XML fragment that only contains complete tags // incomplete tags are held back _xmlbuf += xml; size_t left=0; try { while (left != string::npos) { size_t right = _xmlbuf.find('<', left); if (left < right && left < _xmlbuf.length()) // plain text found? (actions.svgTree().*_append)(util::make_unique(_xmlbuf.substr(left, right-left))); if (right != string::npos) { left = right; if (_xmlbuf.compare(left, 9, "", left+9); if (right == string::npos) { if (finish) throw SpecialException("expected ']]>' at end of CDATA block"); break; } (actions.svgTree().*_append)(util::make_unique(_xmlbuf.substr(left+9, right-left-9))); right += 2; } else if (_xmlbuf.compare(left, 4, "", left+4); if (right == string::npos) { if (finish) throw SpecialException("expected '-->' at end of comment"); break; } (actions.svgTree().*_append)(util::make_unique(_xmlbuf.substr(left+4, right-left-4))); right += 2; } else if (_xmlbuf.compare(left, 2, "", left+2); if (right == string::npos) { if (finish) throw SpecialException("expected '?>' at end of processing instruction"); break; } (actions.svgTree().*_append)(util::make_unique(_xmlbuf.substr(left, right-left+2))); right++; } else if (_xmlbuf.compare(left, 2, "', left+2); if (right == string::npos) { if (finish) throw SpecialException("missing '>' at end of closing XML tag"); break; } closeElement(_xmlbuf.substr(left+2, right-left-2), actions); } else { right = _xmlbuf.find('>', left+1); if (right == string::npos) { if (finish) throw SpecialException("missing '>' or '/>' at end of opening XML tag"); break; } openElement(_xmlbuf.substr(left+1, right-left-1), actions); } } left = right; if (right != string::npos) left++; } } catch (const SpecialException &e) { _error = true; throw; } if (left == string::npos) _xmlbuf.clear(); else _xmlbuf.erase(0, left); } /** Processes an opening element tag. * @param[in] tag tag without leading and trailing angle brackets */ void DvisvgmSpecialHandler::XMLParser::openElement (const string &tag, SpecialActions &actions) { StringInputBuffer ib(tag); BufferInputReader ir(ib); string name = ir.getString("/ \t\n\r"); ir.skipSpace(); auto elemNode = util::make_unique(name); map attribs; if (ir.parseAttributes(attribs, true, "\"'")) { for (const auto &attrpair : attribs) elemNode->addAttribute(attrpair.first, attrpair.second); } ir.skipSpace(); if (ir.peek() == '/') // end of empty element tag (actions.svgTree().*_append)(std::move(elemNode)); else if (ir.peek() < 0) { // end of opening tag _nameStack.push_back(name); (actions.svgTree().*_pushContext)(std::move(elemNode)); } else throw SpecialException("'>' or '/>' expected at end of opening tag <"+name); } /** Processes a closing element tag. * @param[in] tag tag without leading and trailing angle brackets */ void DvisvgmSpecialHandler::XMLParser::closeElement (const string &tag, SpecialActions &actions) { StringInputBuffer ib(tag); BufferInputReader ir(ib); string name = ir.getString(" \t\n\r"); ir.skipSpace(); if (ir.peek() >= 0) throw SpecialException("'>' expected at end of closing tag "); if (_nameStack.back() != name) throw SpecialException("expected but found "); (actions.svgTree().*_popContext)(); _nameStack.pop_back(); } /** Processes any remaining XML fragments, checks for missing closing tags, * and resets the parser state. */ void DvisvgmSpecialHandler::XMLParser::finish (SpecialActions &actions) { if (!_xmlbuf.empty()) { if (!_error) parse("", actions, true); _xmlbuf.clear(); } string tags; while (!_nameStack.empty()) { tags += ", "; _nameStack.pop_back(); } if (!tags.empty() && !_error) { tags.resize(tags.length()-2); throw SpecialException("missing closing tag(s): "+tags); } }