/************************************************************************* ** TpicSpecialHandler.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 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 #include "Color.hpp" #include "EllipticalArc.hpp" #include "InputBuffer.hpp" #include "InputReader.hpp" #include "GraphicsPath.hpp" #include "SpecialActions.hpp" #include "SVGElement.hpp" #include "SVGTree.hpp" #include "TpicSpecialHandler.hpp" #include "utility.hpp" using namespace std; TpicSpecialHandler::TpicSpecialHandler () { reset(); } void TpicSpecialHandler::dviEndPage (unsigned pageno, SpecialActions&) { reset(); } void TpicSpecialHandler::reset () { _points.clear(); _penwidth = 1.0; _grayLevel = -1.0; // no fill } /** Returns the current color used to fill closed paths or shapes. * The TPIC documentation only specifies grayscale colors set by a floating point value * in the range of 0 (white) and 1 (black). Most DVI drivers extend the color support * by taking color specials into account: The grayscale colors are only applied if * the DVI color is set to black. All other colors are directly used as fill color * without evaluating the TPIC gray level. */ Color TpicSpecialHandler::fillColor (bool grayOnly) const { if (_grayLevel >= 0 && (_dviColor == Color::BLACK || grayOnly)) return Color::WHITE*(1.0-_grayLevel); return _dviColor; } /** Adds stroke attributes to a given element depending on the pen width. * @param[in] elem element node the attributes are added to * @param[in] penwidth pen with used to compute the stroke parameters * @param[in] pencolor the drawing color * @param[in] ddist dash/dot distance of line in PS point units (0:solid line, >0:dashed line, <0:dotted line) */ static void add_stroke_attribs (SVGElement *elem, double penwidth, Color pencolor, double ddist) { if (penwidth > 0) { // attributes actually required? elem->setStrokeColor(pencolor); elem->setStrokeWidth(penwidth); vector dasharray; if (ddist > 0) dasharray.push_back(ddist); else if (ddist < 0) { dasharray.push_back(penwidth); dasharray.push_back(-ddist); } elem->setStrokeDash(dasharray); } } static unique_ptr create_ellipse_element (double cx, double cy, double rx, double ry) { bool is_circle = (rx == ry); auto elem = util::make_unique(is_circle ? "circle" : "ellipse"); elem->addAttribute("cx", cx); elem->addAttribute("cy", cy); if (is_circle) elem->addAttribute("r", rx); else { elem->addAttribute("rx", rx); elem->addAttribute("ry", ry); } return elem; } /** Creates SVG elements that draw lines through the recorded points. * @param[in] ddist dash/dot distance of line in PS point units (0:solid line, >0:dashed line, <0:dotted line) * @param[in] actions object providing the actions that can be performed by the SpecialHandler */ void TpicSpecialHandler::drawLines (double ddist, SpecialActions &actions) { if (!_points.empty() && (_penwidth > 0 || _grayLevel >= 0) && !actions.outputLocked()) { unique_ptr elem; if (_points.size() == 1) { const DPair &p = _points.back(); elem = create_ellipse_element(p.x()+actions.getX(), p.y()+actions.getY(), _penwidth/2.0, _penwidth/2.0); actions.embed(p, _penwidth/2.0); } else { if (_points.size() == 2 || (_grayLevel < 0 && _points.front() != _points.back())) { elem = util::make_unique("polyline"); elem->setNoFillColor(); elem->setStrokeLineCap(SVGElement::LC_ROUND); } else { while (_points.front() == _points.back()) _points.pop_back(); elem = util::make_unique("polygon"); if (_grayLevel < 0) elem->setNoFillColor(); else elem->setFillColor(fillColor(false)); } vector points; for (const DPair &p : _points) { double x = p.x()+actions.getX(); double y = p.y()+actions.getY(); points.emplace_back(x, y); actions.embed(DPair(x, y)); } elem->setPoints(points); add_stroke_attribs(elem.get(), _penwidth, Color::BLACK, ddist); } actions.svgTree().appendToPage(std::move(elem)); } reset(); } /** Stroke a quadratic spline through the midpoints of the lines defined by * the previously recorded points. The spline starts with a straight line * from the first point to the mid-point of the first line. The spline ends * with a straight line from the mid-point of the last line to the last point. * If ddist=0, the spline is stroked solid. Otherwise ddist denotes the length * of the dashes and the gaps inbetween. * @param[in] ddist length of dashes and gaps * @param[in] actions object providing the actions that can be performed by the SpecialHandler */ void TpicSpecialHandler::drawSplines (double ddist, SpecialActions &actions) { if (!_points.empty() && _penwidth > 0 && !actions.outputLocked()) { const size_t numPoints = _points.size(); if (numPoints < 3) { _grayLevel = -1; drawLines(ddist, actions); } else { DPair p(actions.getX(), actions.getY()); GraphicsPath path; path.moveto(p+_points[0]); DPair mid = p+_points[0]+(_points[1]-_points[0])/2.0; path.lineto(mid); actions.embed(p+_points[0]); for (size_t i=1; i < numPoints-1; i++) { const DPair p0 = p+_points[i-1]; const DPair p1 = p+_points[i]; const DPair p2 = p+_points[i+1]; mid = p1+(p2-p1)/2.0; path.quadto(p1, mid); actions.embed(mid); actions.embed((p0+p1*6.0+p2)/8.0, _penwidth); } if (_points[0] == _points[numPoints-1]) // closed path? path.closepath(); else { path.lineto(p+_points[numPoints-1]); actions.embed(p+_points[numPoints-1]); } auto pathElem = util::make_unique("path"); pathElem->setNoFillColor(); ostringstream oss; path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); pathElem->addAttribute("d", oss.str()); add_stroke_attribs(pathElem.get(), _penwidth, _dviColor, ddist); actions.svgTree().appendToPage(std::move(pathElem)); } } reset(); } /** Draws an elliptical arc. * @param[in] cx x-coordinate of arc center relative to current DVI position * @param[in] cy y-coordinate of arc center relative to current DVI position * @param[in] rx length of horizontal semi-axis * @param[in] ry length of vertical semi-axis * @param[in] angle1 starting angle (clockwise) relative to x-axis * @param[in] angle2 ending angle (clockwise) relative to x-axis * @param[in] actions object providing the actions that can be performed by the SpecialHandler */ void TpicSpecialHandler::drawArc (double cx, double cy, double rx, double ry, double angle1, double angle2, SpecialActions &actions) { if ((_penwidth > 0 || _grayLevel >= 0) && !actions.outputLocked()) { cx += actions.getX(); cy += actions.getY(); unique_ptr elem; bool closed=true; if (abs(angle2-angle1) >= math::TWO_PI) // closed ellipse? elem = create_ellipse_element(cx, cy, rx, ry); else { EllipticalArc arc(DPair(cx, cy), rx, ry, 0, -angle1, math::normalize_0_2pi(angle2-angle1)); GraphicsPath path; path.moveto(arc.startPoint()); path.arcto(rx, ry, 0, arc.largeArc(), arc.sweepPositive(), arc.endPoint()); if (_grayLevel >= 0) path.closepath(); else closed = false; elem = util::make_unique("path"); ostringstream oss; path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); elem->addAttribute("d", oss.str()); } if (_penwidth > 0) { elem->setStrokeWidth(_penwidth); elem->setStrokeColor(actions.getColor()); if (!closed) elem->setStrokeLineCap(SVGElement::LC_ROUND); } if (_grayLevel < 0) elem->setNoFillColor(); else elem->setFillColor(fillColor(true)); actions.svgTree().appendToPage(std::move(elem)); double pw = _penwidth/2.0; actions.embed(BoundingBox(cx-rx-pw, cy-ry-pw, cx+rx+pw, cy+ry+pw)); } reset(); } /** Computes the gray level based on the ratio of set bits to the total * number of bits of a given hex value. * @param[in] hexstr a sequence of hexadecimal digits * @return the computed gray level [0-1] */ static double bit_sequence_to_gray (const string &hexstr) { if (hexstr.empty()) return 1.0; int setbits=0; // number of bits set int totalbits=0; // number of bits processed constexpr int CHUNKBITS = 8*sizeof(unsigned long long); for (size_t pos=0; pos < hexstr.length(); pos+=CHUNKBITS/4) { size_t digitcount; // number of hex digits processed unsigned long long val = stoull(hexstr.substr(pos, CHUNKBITS/4), &digitcount, 16); setbits += bitset(val).count(); totalbits += 4*digitcount; } return totalbits == 0 ? 1.0 : 1.0-double(setbits)/double(totalbits); } /** Returns a unique integer for a TPIC command (consisting of two letters). */ constexpr int cmd_id (const char *cmd) { return (cmd[0] << 8) | cmd[1]; }; bool TpicSpecialHandler::process (const string &prefix, istream &is, SpecialActions &actions) { if (prefix.length() != 2) return false; _dviColor = actions.getColor(); const double mi2bp=0.072; // factor for milli-inch to PS points StreamInputBuffer ib(is); BufferInputReader ir(ib); switch (cmd_id(prefix.c_str())) { case cmd_id("pn"): // set pen width in milli-inches _penwidth = max(0.0, ir.getDouble()*mi2bp); break; case cmd_id("bk"): // set fill color to black _grayLevel = 1; break; case cmd_id("wh"): // set fill color to white _grayLevel = 0; break; case cmd_id("sh"): // set fill color to given gray level (0=white, 1=black) ir.skipSpace(); _grayLevel = ir.eof() ? 0.5 : max(0.0, min(1.0, ir.getDouble())); break; case cmd_id("tx"): { // set fill color depending on a sequence of bits (given as hex value) string hexstr; while (!ir.eof()) { ir.skipSpace(); int c = ir.get(); if (isxdigit(c)) hexstr += static_cast(tolower(c)); else break; } _grayLevel = bit_sequence_to_gray(hexstr); break; } case cmd_id("pa"): { // add point to path double x = ir.getDouble()*mi2bp; double y = ir.getDouble()*mi2bp; _points.emplace_back(x, y); break; } case cmd_id("fp"): // draw solid lines through recorded points; close and fill path if fill color was defined drawLines(0, actions); break; case cmd_id("ip"): // don't draw outlines but close the recorded path and fill the resulting polygon _penwidth = 0; drawLines(0, actions); break; case cmd_id("da"): // as fp but draw dashed lines drawLines(ir.getDouble()*72, actions); break; case cmd_id("dt"): // as fp but draw dotted lines drawLines(-ir.getDouble()*72, actions); break; case cmd_id("sp"): { // draw quadratic splines through recorded points double ddist = ir.getDouble()*72; drawSplines(ddist, actions); break; } case cmd_id("ar"): { // draw elliptical arc double cx = ir.getDouble()*mi2bp; double cy = ir.getDouble()*mi2bp; double rx = ir.getDouble()*mi2bp; double ry = ir.getDouble()*mi2bp; double a1 = ir.getDouble(); double a2 = ir.getDouble(); drawArc(cx, cy, rx, ry, a1, a2, actions); break; } case cmd_id("ia"): { // fill elliptical arc double cx = ir.getDouble()*mi2bp; double cy = ir.getDouble()*mi2bp; double rx = ir.getDouble()*mi2bp; double ry = ir.getDouble()*mi2bp; double a1 = ir.getDouble(); double a2 = ir.getDouble(); _penwidth = 0; drawArc(cx, cy, rx, ry, a1, a2, actions); break; } default: return false; } return true; } vector TpicSpecialHandler::prefixes () const { vector pfx {"ar", "bk", "da", "dt", "fp", "ia", "ip", "pa", "pn", "sh", "sp", "tx", "wh"}; return pfx; }