/************************************************************************* ** GraphicsPath.hpp ** ** ** ** 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 . ** *************************************************************************/ #pragma once #include #include #include #include #include #include #include #include #include "BoundingBox.hpp" #include "EllipticalArc.hpp" #include "Matrix.hpp" #include "Pair.hpp" #include "utility.hpp" #include "XMLString.hpp" template class GraphicsPath; namespace gp { /// Base class for all path data commands, like moveto, lineto, curveto, etc. struct CommandBase {}; /** Base class for all path data commands with NUM_POINTS point parameters * @tparam NUM_POINTS number of parameter pairs representing points, e.g. 1 for moveto and lineto */ template class Command : public CommandBase { friend class GraphicsPath; public: int numPoints () const {return NUM_POINTS;} Pair& point (int n) {return points[n];} const Pair& point (int n) const {return points[n];} /** Transforms the command by a given transformation matrix. * @params[in] matrix describes the affine transformation to apply * @params[in] currentPoint the untransformed end point of the preceding command */ void transform (const Matrix &matrix, const Pair ¤tPoint) { for (Pair &p : points) p = matrix * p; } /** Returns true if all points are identical to those of another command. */ bool pointsEqual (const Command &cmd) const { for (int i=0; i < NUM_POINTS; i++) if (points[i] != cmd.points[i]) return false; return true; } protected: explicit Command () =default; explicit Command (std::array, NUM_POINTS> &&pts) : points(std::move(pts)) {} protected: std::array, NUM_POINTS> points; }; template struct MoveTo : public Command { explicit MoveTo (const Pair &p) : Command({p}) {} }; template struct LineTo : public Command { explicit LineTo (const Pair &p) : Command({p}) {} }; template struct CubicTo : public Command { explicit CubicTo (const Pair &p1, const Pair &p2, const Pair &p3) : Command({p1, p2, p3}) {} }; template struct QuadTo : public Command { explicit QuadTo (const Pair &p1, const Pair &p2) : Command({p1, p2}) {} }; template struct ClosePath : public Command { ClosePath () : Command() {} }; template struct ArcTo : Command { ArcTo (T rxx, T ryy, double xrot, bool laf, bool sf, const Pair &pp) : Command({pp}), rx(rxx < 0 ? -rxx : rxx), ry(ryy < 0 ? -ryy : ryy), xrotation(xrot), largeArcFlag(laf), sweepFlag(sf) {} bool operator == (const ArcTo &arc) const { return rx == arc.rx && ry == arc.ry && xrotation == arc.xrotation && largeArcFlag == arc.largeArcFlag && sweepFlag == arc.sweepFlag && this->points[0] == arc.points[0]; } void transform (const Matrix &matrix, const Pair ¤tPoint); bool operator != (const ArcTo &arc) const {return !(*this == arc);} T rx, ry; ///< length of the semi-major and semi-minor axes double xrotation; ///< rotation of the semi-major axis in degrees bool largeArcFlag; ///< if true, the longer arc from start to end point is chosen, else the shorter one bool sweepFlag; ///< if true, arc is drawn in direction of positive angles, else the opposite direction }; /** Applies an affine transformation described by a given matrix to the arc segment. * @params[in] matrix describes the affine transformation to apply * @params[in] currentPoint the untransformed end point of the preceding command */ template void ArcTo::transform (const Matrix &matrix, const Pair ¤tPoint) { EllipticalArc arc(currentPoint, rx, ry, math::deg2rad(xrotation), largeArcFlag, sweepFlag, this->points[0]); arc.transform(matrix); rx = arc.rx(); ry = arc.ry(); xrotation = math::rad2deg(arc.rotationAngle()); largeArcFlag = arc.largeArc(); sweepFlag = arc.sweepPositive(); this->points[0] = Pair(arc.endPoint()); } /** Returns true if two path command objects are identical (same command and same parameters). */ template inline typename std::enable_if::value, bool>::type operator == (const Cmd1 &cmd1, const Cmd2 &cmd2) { if (std::is_convertible::value && std::is_convertible::value) return cmd1.pointsEqual(cmd2); return false; } /** Returns true if two path command objects differ (different commands or different parameters). */ template inline typename std::enable_if::value, bool>::type operator != (const Cmd1 &cmd1, const Cmd2 &cmd2) { if (std::is_convertible::value && std::is_convertible::value) return !cmd1.pointsEqual(cmd2); return true; } } // namespace gp template class GraphicsPath { friend class PathClipper; public: enum class WindingRule {EVEN_ODD, NON_ZERO}; using Point = Pair; protected: static XMLString to_param_str (double v, double s, double d, bool leadingSpace) { XMLString str(v*s + d); if (leadingSpace && (str[0] != '-')) str.insert(0, " "); return str; } static XMLString to_param_str (double val, double prev, double s, double d, bool leadingSpace) { XMLString str((val-prev)*s + d); if (leadingSpace && (str[0] != '-')) str.insert(0, " "); return str; } static std::string to_param_str (const Point &p, double sx, double sy, double dx, double dy, bool leadingSpace) { return to_param_str(p.x(), sx, dx, leadingSpace) + to_param_str(p.y(), sy, dy, true); } static std::string to_param_str (const Point &p, const Point &prev, double sx, double sy, double dx, double dy, bool leadingSpace) { return to_param_str(p.x()-prev.x(), sx, dx, leadingSpace) + to_param_str(p.y()-prev.y(), sy, dy, true); } using MoveTo = gp::MoveTo; using LineTo = gp::LineTo; using CubicTo = gp::CubicTo; using QuadTo = gp::QuadTo; using ArcTo = gp::ArcTo; using ClosePath = gp::ClosePath; /// Variant representing a single path command using CommandVariant = mpark::variant; class IterationVisitor; public: /** Base class providing several template methods being called when executing * GraphicsPath::iterate(). */ class IterationActions { friend class IterationVisitor; public: virtual ~IterationActions () =default; virtual void moveto (const Point &p) {} virtual void lineto (const Point &p) {} virtual void hlineto (const T &x) {} virtual void vlineto (const T &y) {} virtual void quadto (const Point &p) {} virtual void quadto (const Point &p1, const Point &p2) {} virtual void cubicto (const Point &p1, const Point &p2) {} virtual void cubicto (const Point &p1, const Point &p2, const Point &p3) {} virtual void arcto (T rx, T ry, double angle, bool largeArcFlag, bool sweepFlag, const Point &p) {} virtual void closepath () {} virtual bool quit () {return false;} virtual void finished () {} const Point& startPoint () const {return _startPoint;} const Point& currentPoint () const {return _currentPoint;} private: Point _startPoint; ///< first point of the current sub-path Point _currentPoint; ///< point reached by preceding path command, or (0,0) otherwise }; protected: class ModificationActions : public IterationActions { friend class GraphicsPath; public: explicit ModificationActions (GraphicsPath &path) : _path(path) {} protected: GraphicsPath& path () {return _path;} int commandPos () const {return _commandPos;} private: GraphicsPath &_path; int _commandPos=0; ///< number of command in path being processed }; class WriteActions : public IterationActions { public: WriteActions (std::ostream &os, bool rel, double sx, double sy, double dx, double dy) : _os(os), _relative(rel), _sx(sx), _sy(sy), _dx(dx), _dy(dy) {} void moveto (const Point &p) override {write('M', {p});} void lineto (const Point &p) override {write('L', {p});} void hlineto (const T &x) override {write('H', x, this->currentPoint().x(), _sx, _dx);} void vlineto (const T &y) override {write('V', y, this->currentPoint().y(), _sy, _dy);} void quadto (const Point &p) override {write('T', {p});} void quadto (const Point &p1, const Point &p2) override {write('Q', {p1, p2});} void cubicto (const Point &p1, const Point &p2) override {write('S', {p1, p2});} void cubicto (const Point &p1, const Point &p2, const Point &p3) override {write('C', {p1, p2, p3});} void closepath () override {_os << (_relative ? 'z' : 'Z');} void arcto (T rx, T ry, double angle, bool largeArcFlag, bool sweepFlag, const Point &p) override { Point diff = p-this->currentPoint(); if (std::abs(diff.x()) < 1e-7 && std::abs(diff.y()) < 1e-7) return; if (std::abs(rx) < 1e-7 && std::abs(ry) < 1e-7) lineto(p); else { if (std::abs(std::abs(_sx) - std::abs(_sy)) < 1e-7) { // symmetric scaling? angle *= math::sgn(_sx) * math::sgn(_sy); rx *= std::abs(_sx); ry *= std::abs(_sx); } else { // asymmetric scaling => compute new shape parameters EllipticalArc arc(this->currentPoint(), double(rx), double(ry), math::deg2rad(angle), largeArcFlag, sweepFlag, p); arc.transform(ScalingMatrix(_sx, _sy)); angle = math::rad2deg(arc.rotationAngle()); rx = arc.rx(); ry = arc.ry(); } _os << (_relative ? 'a' : 'A') << to_param_str(rx, 1.0, 0, false) << to_param_str(ry, 1.0, 0, true) << to_param_str(angle, 1.0, 0, true) << ' ' << (largeArcFlag ? 1 : 0) << ' ' << (sweepFlag ? 1 : 0); if (_relative) _os << to_param_str(p, this->currentPoint(), _sx, _sy, _dx, _dy, true); else _os << to_param_str(p, _sx, _sy, _dx, _dy, true); } } protected: void write (char cmdchar, std::initializer_list points) const { int count=0; if (_relative) { _os << char(tolower(cmdchar)); for (const Point &p : points) _os << to_param_str(p, this->currentPoint(), _sx, _sy, _dx, _dy, count++ > 0); } else { _os << cmdchar; for (const Point &p : points) _os << to_param_str(p, _sx, _sy, _dx, _dy, count++ > 0); } } void write (char cmdchar, T val, T relval, double s, double d) const { if (_relative) _os << char(tolower(cmdchar)) << to_param_str(val, relval, s, d, false); else _os << cmdchar << to_param_str(val, s, d, false); } private: std::ostream &_os; ///< write output to this stream bool _relative; ///< if true, use relative coordinates in path commands double _sx, _sy; ///< horizontal and vertical scaling factors double _dx, _dy; ///< horizontal and vertical translation values }; /////////////////////////////////////////////////////////////////////////////// /** Calls the corresponding template method of an Action object for the current path command. * If parameter 'useShortCmds' is true, the visitor operators check whether a command * can be shortened due to special cases, e.g. horizontal or vertical lines, smooth * curve connections etc. Otherwise, the full command templates are triggered. */ class IterationVisitor { public: IterationVisitor (IterationActions &actions, bool useShortCmds, double eps=1e-7) : _actions(actions), _shortCommandsActive(useShortCmds), _eps(eps) {} void setPrevCommand (const CommandVariant &prevCommand) { _prevCommand = &prevCommand; } void operator () (const MoveTo &cmd) { _actions.moveto(cmd.points[0]); _actions._startPoint = _actions._currentPoint = cmd.points[0]; } void operator () (const LineTo &cmd) { Point diff = abs(_actions._currentPoint-cmd.points[0]); if (diff.x() >= _eps || diff.y() >= _eps) { if (!_shortCommandsActive) _actions.lineto(cmd.points[0]); else { if (diff.x() < _eps) _actions.vlineto(cmd.points[0].y()); else if (diff.y() < _eps) _actions.hlineto(cmd.points[0].x()); else _actions.lineto(cmd.points[0]); } } _actions._currentPoint = cmd.points[0]; } void operator () (const CubicTo &cmd) { bool smooth=false; if (_shortCommandsActive) { if (auto *prevCubic = mpark::get_if(_prevCommand)) { Point diff = abs(cmd.points[0] - prevCubic->points[2]*T(2) + prevCubic->points[1]); if ((smooth = (diff.x() < _eps && diff.y() < _eps))) _actions.cubicto(cmd.points[1], cmd.points[2]); } } if (!smooth) _actions.cubicto(cmd.points[0], cmd.points[1], cmd.points[2]); _actions._currentPoint = cmd.points[2]; } void operator () (const QuadTo &cmd) { bool smooth=false; if (_shortCommandsActive) { if (auto *prevQuad = mpark::get_if(_prevCommand)) { Point diff = abs(cmd.points[0] - prevQuad->points[1] * T(2) + prevQuad->points[0]); if ((smooth = (diff.x() < _eps && diff.y() < _eps))) // is reflection? _actions.quadto(cmd.points[1]); } } if (!smooth) _actions.quadto(cmd.points[0], cmd.points[1]); _actions._currentPoint = cmd.points[1]; } void operator () (const ClosePath &cmd) { _actions.closepath(); _actions._currentPoint = _actions._startPoint; } void operator () (const ArcTo &cmd) { _actions.arcto(cmd.rx, cmd.ry, cmd.xrotation, cmd.largeArcFlag, cmd.sweepFlag, cmd.points[0]); _actions._currentPoint = cmd.points[0]; } private: IterationActions &_actions; bool _shortCommandsActive=false; double _eps=1e-7; const CommandVariant *_prevCommand=nullptr; }; /////////////////////////////////////////////////////////////////////////////// /** Transforms all Point parameters of a path command. */ class TransformVisior { public: explicit TransformVisior (const Matrix &m) : matrix(m) {} template void operator () (Cmd &cmd) { Point cp = cmd.point(cmd.numPoints()-1); cmd.transform(matrix, _currentPoint); _currentPoint = cp; } void operator () (MoveTo &cmd) { Point cp = cmd.point(0); cmd.transform(matrix, _currentPoint); _startPoint = _currentPoint = cp; } void operator () (ClosePath &cmd) { _currentPoint = _startPoint; } private: const Matrix &matrix; Point _startPoint, _currentPoint; ///< untransformed start end current point }; public: explicit GraphicsPath (WindingRule wr=WindingRule::NON_ZERO) : _windingRule(wr) {} void setWindingRule (WindingRule wr) {_windingRule = wr;} WindingRule windingRule () const {return _windingRule;} void clear () { _commands.clear(); } /// Returns true if the path is empty, i.e. there is nothing to draw bool empty () const { return _commands.empty(); } /// Returns the number of path commands used to describe the path. size_t size () const { return _commands.size(); } const Point& startPoint () const {return _startPoint;} const Point& finalPoint () const {return _finalPoint;} /// Insert another path at the beginning of this one. void prepend (const GraphicsPath &path) { _commands.insert(_commands.begin(), path._commands.begin(), path._commands.end()); } void moveto (const T &x, const T &y) { moveto(Point(x, y)); } void moveto (const Point &p) { // avoid sequences of several MOVETOs; always use latest if (_commands.empty() || !mpark::get_if(&_commands.back())) _commands.emplace_back(MoveTo{p}); else mpark::get(_commands.back()).points[0] = p; _startPoint = _finalPoint = p; } void lineto (const T &x, const T &y) { lineto(Point(x, y)); } void lineto (const Point &p) { _commands.emplace_back(LineTo{p}); _finalPoint = p; } void quadto (const T &x1, const T &y1, const T &x2, const T &y2) { quadto(Point(x1, y1), Point(x2, y2)); } /** Creates a quadratic Bézier segment. */ void quadto (const Point &p1, const Point &p2) { _commands.emplace_back(QuadTo{p1, p2}); _finalPoint = p2; } /** Creates a quadratic Bézier segment smoothly extending a preceding one, i.e. the gradients * of the two curves are identical at the connection point. The control point of the second * curve is computed as the reflection of the preceding curve's control point at the connection * point. */ void quadto (const Point &p2) { Point p1; if (!_commands.empty()) { if (auto qto = mpark::get_if(&_commands.back())) p1 = _finalPoint*T(2) - qto->point(0); // reflect previous control point at current point else // previous command isn't a quadto? p1 = _finalPoint; // => use current point as control point } quadto(p1, p2); } void cubicto (const T &x1, const T &y1, const T &x2, const T &y2, const T &x3, const T &y3) { cubicto(Point(x1, y1), Point(x2, y2), Point(x3, y3)); } /** Creates a cubic Bézier segment. */ void cubicto (const Point &p1, const Point &p2, const Point &p3) { _commands.emplace_back(CubicTo{p1, p2, p3}); _finalPoint = p3; } /** Creates a cubic Bézier segment smoothly extending a preceding one, i.e. the gradients * of the two curves are identical at the connection point. The first control point of * the second curve is computed as the reflection of the preceding curve's second control * point at the connection point. */ void cubicto (const Point &p2, const Point &p3) { Point p1; if (!_commands.empty()) { if (auto cto = mpark::get_if(&_commands.back())) p1 = _finalPoint*T(2) - cto->point(1); // reflect previous control point at current point else // previous command isn't a cubicto? p1 = _finalPoint; // => use current point as control point } cubicto(p1, p2, p3); } void closepath () { if (!_commands.empty() && !mpark::get_if(&_commands.back())) { _commands.emplace_back(ClosePath{}); _finalPoint = _startPoint; } } void arcto (T rx, T ry, double angle, bool laf, bool sweep, const Point &p) { _commands.emplace_back(ArcTo{rx, ry, angle, laf, sweep, p}); _finalPoint = p; } /** Detects all open subpaths and closes them by adding a closePath command. * Most font formats only support closed outline paths so there are no explicit closePath statements * in the glyph's outline description. All open paths are automatically closed by the renderer. * This method detects all open paths and adds the missing closePath statement. */ void closeOpenSubPaths () { CommandVariant *prevCmd = nullptr; for (auto it=_commands.begin(); it != _commands.end(); ++it) { if (mpark::get_if(&*it) && prevCmd && !mpark::get_if(prevCmd)) { prevCmd = &*it; it = _commands.insert(it, ClosePath{})+1; } else prevCmd = &*it; } if (!_commands.empty() && !mpark::get_if(&_commands.back())) closepath(); } /** Removes redundant path commands commands. Currently, only removes movetos. */ void removeRedundantCommands () { // remove trailing moveto commands while (!_commands.empty() && mpark::get_if(&_commands.back())) _commands.pop_back(); // resolve intermediate sequences of moveto commands auto it=_commands.begin(); if (it == _commands.end()) return; auto prev = it++; while (it != _commands.end()) { if (!mpark::get_if(&*prev) || !mpark::get_if(&*it)) prev = it++; else { prev = _commands.erase(prev); // remove leading MOVETO and advance 'prev' to 'it' ++it; } } } /** Writes the path data as SVG path drawing command to a given output stream. * @param[in] os output stream used to write the SVG commands to * @param[in] relative if true, create relative rather than absolute coordinate values * @param[in] sx horizontal scale factor * @param[in] sy vertical scale factor * @param[in] dx horizontal translation in PS point units * @param[in] dy vertical translation in PS point units */ void writeSVG (std::ostream &os, bool relative, double sx=1.0, double sy=1.0, double dx=0.0, double dy=0.0) const { WriteActions actions(os, relative, sx, sy, dx, dy); iterate(actions, true); } /** Computes the bounding box of the current path. * @param[out] bbox the computed bounding box */ BoundingBox computeBBox () const { BoundingBox bbox; struct BBoxActions : IterationActions { explicit BBoxActions (BoundingBox &bb) : bbox(bb) {} void moveto (const Point &p) override {bbox.embed(p);} void lineto (const Point &p) override {bbox.embed(p);} void quadto (const Point &p1, const Point &p2) override {bbox.embed(p1); bbox.embed(p2);} void cubicto (const Point &p1, const Point &p2, const Point &p3) override {bbox.embed(p1); bbox.embed(p2); bbox.embed(p3);} void arcto (T rx, T ry, double angle, bool laf, bool sweep, const Point &p) override { bbox.embed(EllipticalArc(this->currentPoint(), double(rx), double(ry), angle, laf, sweep, p).getBBox()); } BoundingBox &bbox; } actions(bbox); iterate(actions, false); return bbox; } /** Checks whether the current path describes a dot/point only (with no extent). * @param[out] p coordinates of the point if path describes a dot * @return true if path is a dot/point */ bool isDot (Point &p) const { struct DotActions : IterationActions { DotActions () : differs(false) {} void moveto (const Point &p) override {point = p;} void lineto (const Point &p) override {differs = (p != point);} void quadto (const Point &p1, const Point &p2) override { differs = (point != p1 || point != p2);} void cubicto (const Point &p1, const Point &p2, const Point &p3) override {differs = (point != p1 || point != p2 || point != p3);} void arcto (T rx, T ry, double angle, bool largeArcFlag, bool sweepFlag, const Point &p) override { differs = (point != p);} bool quit () override {return differs;} Point point; bool differs; } actions; iterate(actions, false); p = actions.point; return !actions.differs; } /** Replaces all elliptic arcs with cubic Bézier curves. */ void approximateArcs () { struct ArcActions : ModificationActions { explicit ArcActions (GraphicsPath &path) : ModificationActions(path) {} void arcto (T rx, T ry, double angle, bool largeArcFlag, bool sweepFlag, const Point &p) override { EllipticalArc arc(this->currentPoint(), rx, ry, angle, largeArcFlag, sweepFlag, p); std::vector cmds; for (const CubicBezier &bezier : arc.approximate()) cmds.emplace_back(CubicTo{bezier.point(1), bezier.point(2), bezier.point(3)}); this->path().replace(this->commandPos(), cmds); } } actions(*this); iterate(actions); } /** Transforms the path according to a given Matrix. * @param[in] matrix Matrix describing the affine transformation */ void transform (const Matrix &matrix) { TransformVisior visior(matrix); for (CommandVariant &command : _commands) mpark::visit(visior, command); } /** Returns true if this path equals another one, i.e. it consists the same sequence * of commands and coordinates. */ bool operator == (const GraphicsPath &path) const { if (size() != path.size()) return false; return std::equal(_commands.begin(), _commands.end(), path._commands.begin()); } /** Returns true if this path differs from another one (command-wise). */ bool operator != (const GraphicsPath &path) const { if (size() != path.size()) return true; return !std::equal(_commands.begin(), _commands.end(), path._commands.begin()); } /** Iterates over all commands defining this path and calls the corresponding template methods. * In the case of successive bezier curve sequences, control points or tangent slopes are often * identical so that the path description contains redundant information. SVG provides shorthand * curve commands that require less parameters. If 'optimize' is true, this method detects such * command sequences. * @param[in] actions template methods called by each iteration step * @param[in] optimize if true, shorthand drawing commands (hlineto, vlineto,...) are considered */ void iterate (IterationActions &actions, bool optimize) const { double eps = XMLString::DECIMAL_PLACES > 0 ? std::pow(10, -XMLString::DECIMAL_PLACES) : 1e-7; IterationVisitor visitor(actions, optimize, eps); for (const CommandVariant &cmd : _commands) { if (actions.quit()) break; mpark::visit(visitor, cmd); visitor.setPrevCommand(cmd); } actions.finished(); } protected: /** Replaces a command by a sequence of other ones. * @param[in] pos position of command to replace (0-based) * @param[in] cmds commands to insert */ void replace (int pos, const std::vector &cmds) { auto it = _commands.end(); if (!_commands.empty()) { it = _commands.begin()+pos; it = _commands.erase(it); } _commands.insert(it, cmds.begin(), cmds.end()); } /** Iterates over all commands of the path and calls the corresponding template methods. * In contrast to the public iterate() method, this one allows to modify the command sequence. * @param[in] actions template methods called by each iteration step */ void iterate (ModificationActions &actions) { IterationVisitor visitor(actions, false); // no iterators here since they may be invalidated during path modifications for (size_t i=0; i < _commands.size(); i++) { if (actions.quit()) break; actions._commandPos = i; mpark::visit(visitor, _commands[i]); visitor.setPrevCommand(_commands[i]); } actions.finished(); } private: std::deque _commands; ///< sequence of path commands WindingRule _windingRule = WindingRule::NON_ZERO; Point _startPoint; ///< start point of final sub-path Point _finalPoint; ///< final point reached by last command in path };