/************************************************************************* ** PsSpecialHandler.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 "FileFinder.hpp" #include "FilePath.hpp" #include "FileSystem.hpp" #include "Message.hpp" #include "PathClipper.hpp" #include "PSPattern.hpp" #include "PSPreviewHandler.hpp" #include "PsSpecialHandler.hpp" #include "SpecialActions.hpp" #include "SVGElement.hpp" #include "TensorProductPatch.hpp" #include "TriangularPatch.hpp" #include "utility.hpp" using namespace std; bool PsSpecialHandler::COMPUTE_CLIPPATHS_INTERSECTIONS = false; bool PsSpecialHandler::SHADING_SEGMENT_OVERLAP = false; int PsSpecialHandler::SHADING_SEGMENT_SIZE = 20; double PsSpecialHandler::SHADING_SIMPLIFY_DELTA = 0.01; string PsSpecialHandler::BITMAP_FORMAT; bool PsSpecialHandler::EMBED_BITMAP_DATA = false; PsSpecialHandler::PsSpecialHandler () : _psi(this), _previewHandler(_psi) { _psi.setImageDevice(BITMAP_FORMAT); } PsSpecialHandler::~PsSpecialHandler () { _psi.setActions(nullptr); // ensure no further PS actions are performed } /** Initializes the PostScript handler. It's called by the first use of process(). The * deferred initialization speeds up the conversion of DVI files that doesn't contain * PS specials. */ void PsSpecialHandler::initialize () { if (_psSection == PS_NONE) { initgraphics(); // execute dvips prologue/header files for (const char *fname : {"tex.pro", "texps.pro", "special.pro", "color.pro"}) processHeaderFile(fname); // disable bop/eop operators to prevent side-effects by // unexpected bops/eops present in PS specials _psi.execute("\nTeXDict begin /bop{pop pop}def /eop{}def end "); _psSection = PS_HEADERS; // allow to process header specials now } } /** Set initial values of PS graphics state (see PS language reference (Red Book)), p. 612). */ void PsSpecialHandler::initgraphics () { _linewidth = 1; _linecap = _linejoin = 0; // butt end caps and miter joins _miterlimit = 4; _xmlnode = _savenode = nullptr; _isshapealpha = false; // opacity operators change constant component by default _opacity = Opacity(); _sx = _sy = _cos = 1.0; _pattern = nullptr; _makingPattern = false; _patternEnabled = false; _currentcolor = Color::BLACK; _dashoffset = 0; _dashpattern.clear(); _path.clear(); _clipStack.clear(); } void PsSpecialHandler::processHeaderFile (const char *name) { if (const char *path = FileFinder::instance().lookup(name, false)) { ifstream ifs(path); _psi.execute(string("%%BeginProcSet: ")+name+" 0 0\n", false); _psi.execute(ifs, false); _psi.execute("%%EndProcSet\n", false); } else Message::wstream(true) << "PostScript header file " << name << " not found\n"; } void PsSpecialHandler::enterBodySection () { if (_psSection == PS_HEADERS) { _psSection = PS_BODY; // don't process any PS header code ostringstream oss; // process collected header code if (!_headerCode.empty()) { oss << "\nTeXDict begin @defspecial " << _headerCode << "\n@fedspecial end"; _headerCode.clear(); } // push dictionary "TeXDict" with dvips definitions on dictionary stack // and initialize basic dvips PostScript variables oss << "\nTeXDict begin 0 0 1000 72 72 () @start 0 0 moveto "; _psi.execute(oss.str(), false); // Check for information generated by preview.sty. If the tightpage options // was set, don't execute the bop-hook but allow the PS filter to read // the bbox data present at the beginning of the page. _previewHandler.init(); if (!_previewHandler.tightpage()) _psi.execute("userdict/bop-hook known{bop-hook}if\n", false); } } /** Move PS graphic position to current DVI location. */ void PsSpecialHandler::moveToDVIPos () { if (_actions) { const double x = _actions->getX(); const double y = _actions->getY(); ostringstream oss; oss << '\n' << x << ' ' << y << " moveto "; _psi.execute(oss.str()); _currentpoint = DPair(x, y); } } /** Executes a PS snippet and optionally synchronizes the DVI cursor position * with the current PS point. * @param[in] is stream to read the PS code from * @param[in] updatePos if true, move the DVI drawing position to the current PS point */ void PsSpecialHandler::executeAndSync (istream &is, bool updatePos) { if (_actions && _actions->getColor() != _currentcolor) { // update the PS graphics state if the color has been changed by a color special double r, g, b; _actions->getColor().getRGB(r, g, b); ostringstream oss; oss << '\n' << r << ' ' << g << ' ' << b << " setrgbcolor "; _psi.execute(oss.str(), false); } _psi.execute(is); if (updatePos) { // retrieve current PS position (stored in _currentpoint) _psi.execute("\nquerypos "); if (_actions) { _actions->setX(_currentpoint.x()); _actions->setY(_currentpoint.y()); } } } void PsSpecialHandler::preprocess (const string &prefix, istream &is, SpecialActions &actions) { initialize(); if (_psSection != PS_HEADERS) return; _actions = &actions; if (prefix == "!") { _headerCode += "\n"; _headerCode += string(istreambuf_iterator(is), istreambuf_iterator()); } else if (prefix == "header=") { // read and execute PS header file string fname; is >> fname; processHeaderFile(fname.c_str()); } } static string filename_suffix (const string &fname) { string ret; auto pos = fname.rfind('.'); if (pos != string::npos) ret = util::tolower(fname.substr(pos+1)); return ret; } bool PsSpecialHandler::process (const string &prefix, istream &is, SpecialActions &actions) { // process PS headers only once (in prescan) if (prefix == "!" || prefix == "header=") return true; _actions = &actions; initialize(); if (_psSection != PS_BODY) enterBodySection(); if (prefix == "\"" || prefix == "pst:") { // read and execute literal PostScript code (isolated by a wrapping save/restore pair) moveToDVIPos(); _psi.execute("\n@beginspecial @setspecial "); executeAndSync(is, false); _psi.execute("\n@endspecial "); } else if (prefix == "psfile=" || prefix == "PSfile=" || prefix == "pdffile=") { if (_actions) { StreamInputReader in(is); string fname = in.getQuotedString(in.peek() == '"' ? "\"" : nullptr); fname = FileSystem::ensureForwardSlashes(fname); FileType fileType = FileType::EPS; if (prefix == "pdffile=") fileType = FileType::PDF; else { // accept selected non-PS files in psfile special string ext = filename_suffix(fname); if (ext == "pdf") fileType = FileType::PDF; else if (ext == "svg") fileType = FileType::SVG; else if (ext == "jpg" || ext == "jpeg" || ext == "png") fileType = FileType::BITMAP; } map attr; in.parseAttributes(attr, false); imgfile(fileType, fname, attr); } } else if (prefix == "ps::") { if (_actions) _actions->finishLine(); // reset DVI position on next DVI command if (is.peek() == '[') { // collect characters inside the brackets string code; for (int i=0; i < 9 && is.peek() != ']' && !is.eof(); ++i) code += char(is.get()); if (is.peek() == ']') code += char(is.get()); if (code == "[begin]" || code == "[nobreak]") { moveToDVIPos(); executeAndSync(is, true); } else { // no move to DVI position here if (code != "[end]") // PS array? _psi.execute(code); executeAndSync(is, true); } } else { // ps:: behaves like ps::[end] // no move to DVI position here executeAndSync(is, true); } } else { // ps: ... or PST: ... if (_actions) _actions->finishLine(); moveToDVIPos(); StreamInputReader in(is); if (in.check(" plotfile ")) { // ps: plotfile fname string fname = in.getString(); ifstream ifs(fname); if (ifs) _psi.execute(ifs); else Message::wstream(true) << "file '" << fname << "' not found in ps: plotfile\n"; } else { // ps: is almost identical to ps::[begin] but does // a final repositioning to the current DVI location executeAndSync(is, true); moveToDVIPos(); } } return true; } /** Handles a psfile/pdffile special which places an external EPS/PDF graphic * at the current DVI position. The lower left corner (llx,lly) of the * given bounding box is placed at the DVI position. * @param[in] filetype type of file to process (EPS or PDF) * @param[in] fname EPS/PDF file to be included * @param[in] attr attributes given with psfile/pdffile special */ void PsSpecialHandler::imgfile (FileType filetype, const string &fname, const map &attr) { // prevent warning about missing image file "/dev/null" which is // added by option "psfixbb" of the preview package if (fname == "/dev/null") return; map::const_iterator it; // bounding box of EPS figure in PS point units (lower left and upper right corner) double llx = (it = attr.find("llx")) != attr.end() ? stod(it->second) : 0; double lly = (it = attr.find("lly")) != attr.end() ? stod(it->second) : 0; double urx = (it = attr.find("urx")) != attr.end() ? stod(it->second) : 0; double ury = (it = attr.find("ury")) != attr.end() ? stod(it->second) : 0; int pageno = (it = attr.find("page")) != attr.end() ? stoi(it->second, nullptr, 10) : 1; _pdfProc = ((it = attr.find("proc")) != attr.end() ? it->second : ""); if (filetype == FileType::BITMAP || filetype == FileType::SVG) swap(lly, ury); else if (filetype == FileType::PDF && llx == 0 && lly == 0 && urx == 0 && ury == 0) { BoundingBox pagebox = _psi.pdfPageBox(fname, pageno); pagebox.transform(TranslationMatrix(-pagebox.minX(), -pagebox.minY())); if (pagebox.valid()) { llx = lly = 0.0; urx = pagebox.maxX(); ury = pagebox.maxY(); } } // desired width and height of the untransformed figure in PS point units double rwi = (it = attr.find("rwi")) != attr.end() ? stod(it->second)/10.0 : -1; double rhi = (it = attr.find("rhi")) != attr.end() ? stod(it->second)/10.0 : -1; if (rwi == 0 || rhi == 0 || urx-llx == 0 || ury-lly == 0) return; // user transformations (default values chosen according to dvips manual) // order of transformations: rotate, scale, translate/offset double hoffset = (it = attr.find("hoffset")) != attr.end() ? stod(it->second) : 0; double voffset = (it = attr.find("voffset")) != attr.end() ? stod(it->second) : 0; // double hsize = (it = attr.find("hsize")) != attr.end() ? stod(it->second) : 612; // double vsize = (it = attr.find("vsize")) != attr.end() ? stod(it->second) : 792; double hscale = (it = attr.find("hscale")) != attr.end() ? stod(it->second) : 100; double vscale = (it = attr.find("vscale")) != attr.end() ? stod(it->second) : 100; double angle = (it = attr.find("angle")) != attr.end() ? stod(it->second) : 0; bool clipToBbox = (attr.find("clip") != attr.end()); // compute factors to scale the bounding box to width rwi and height rhi double sx = rwi/abs(llx-urx); double sy = rhi/abs(lly-ury); if (sx == 0 || sy == 0) return; if (sx < 0) sx = sy; // rwi attribute not set if (sy < 0) sy = sx; // rhi attribute not set if (sx < 0) sx = sy = 1.0; // neither rwi nor rhi set // save current DVI position double x = _actions->getX(); double y = _actions->getY(); // all following drawings are relative to (0,0) _actions->setX(0); _actions->setY(0); moveToDVIPos(); auto imgNode = createImageNode(filetype, fname, pageno, BoundingBox(llx, lly, urx, ury), clipToBbox); if (imgNode.element) { // has anything been drawn? if (filetype == FileType::EPS || filetype == FileType::PDF) sy = -sy; // adapt orientation of y-coordinates imgNode.matrix.scale(sx, sy).rotate(-angle).scale(hscale/100, vscale/100); // apply transformation attributes imgNode.matrix.translate(x+hoffset, y-voffset); // move image to current DVI position imgNode.matrix.lmultiply(_actions->getMatrix()); // update bounding box BoundingBox bbox(0, 0, urx-llx, ury-lly); bbox.transform(imgNode.matrix); _actions->embed(bbox); // insert element containing the image data imgNode.matrix.rmultiply(TranslationMatrix(-llx, -lly)); // move lower left corner of image to origin imgNode.element->setTransform(imgNode.matrix); _actions->svgTree().appendToPage(std::move(imgNode.element)); } // restore DVI position _actions->setX(x); _actions->setY(y); moveToDVIPos(); } /** Returns path + basename of temporary bitmap images. */ static string image_base_path (const SpecialActions &actions) { FilePath imgpath = actions.getSVGFilePath(actions.getCurrentPageNumber()); return FileSystem::tmpdir() + "/" + imgpath.basename() + "-tmp-"; } /** Creates an XML element containing the image data depending on the file type. * @param[in] type file type of the image * @param[in] fname file name/path of image file * @param[in] pageno number of page to process (PDF only) * @param[in] bbox bounding box of the image * @param[in] clip if true, the image is clipped to its bounding box * @return pointer to the element or nullptr if there's no image data */ PsSpecialHandler::ImageNode PsSpecialHandler::createImageNode (FileType type, const string &fname, int pageno, BoundingBox bbox, bool clip) { ImageNode imgnode; string pathstr; if (const char *path = FileFinder::instance().lookup(fname, false)) pathstr = FileSystem::ensureForwardSlashes(path); if ((pathstr.empty() || !FileSystem::exists(pathstr)) && FileSystem::exists(fname)) pathstr = fname; if (pathstr.empty()) Message::wstream(true) << "file '" << fname << "' not found\n"; else if (type == FileType::BITMAP || type == FileType::SVG) imgnode = createBitmapNode(fname, pathstr, pageno, bbox); else if (type == FileType::EPS) imgnode = createPSNode(fname, pathstr, pageno, bbox, clip); else imgnode = createPDFNode(fname, pathstr, pageno, bbox, clip); return imgnode; } PsSpecialHandler::ImageNode PsSpecialHandler::createBitmapNode (const string &fname, const string &path, int pageno, BoundingBox bbox) { ImageNode imgnode(util::make_unique("image")); imgnode.element->addAttribute("x", 0); imgnode.element->addAttribute("y", 0); imgnode.element->addAttribute("width", bbox.width()); imgnode.element->addAttribute("height", bbox.height()); if (EMBED_BITMAP_DATA) imgnode.element->addAttribute("@@xlink:href", "data:" + util::mimetype(fname) + ";base64," + fname); else { string href = path; // Only reference the image with an absolute path if either an absolute path was given by the user // or a given plain filename is not present in the current working directory but was found through // the FileFinder, i.e. it's usually located somewhere in the texmf tree. if (!FilePath::isAbsolute(fname) && (fname.find('/') != string::npos || FilePath(fname).exists())) href = FilePath(path).relative(FilePath(_actions->getSVGFilePath(pageno))); imgnode.element->addAttribute("xlink:href", href); } return imgnode; } PsSpecialHandler::ImageNode PsSpecialHandler::createPSNode (const string &fname, const string &path, int pageno, BoundingBox bbox, bool clip) { ImageNode imgnode(util::make_unique("g")); // put SVG nodes created from the EPS/PDF file in this group _xmlnode = imgnode.element.get(); _psi.execute( "\n@beginspecial @setspecial" // enter special environment "/setpagedevice{@setpagedevice}def " // activate processing of operator "setpagedevice" "/@imgbase("+image_base_path(*_actions)+")store " // path and basename of image files "matrix setmatrix" // don't apply outer PS transformations "/FirstPage "+to_string(pageno)+" def" // set number of first page to convert (PDF only) "/LastPage "+to_string(pageno)+" def" // set number of last page to convert (PDF only) "(" + path + ")run " // execute file content "@endspecial\n" // leave special environment ); if (imgnode.element->empty()) imgnode.element.reset(nullptr); else if (clip) { // clip image to its bounding box if flag 'clip' is given auto clippath = util::make_unique("clipPath"); clippath->addAttribute("id", "imgclip"+ to_string(_imgClipCount)); clippath->append(bbox.createSVGPath()); imgnode.element->setClipPathUrl("imgclip" + to_string(_imgClipCount++)); _actions->svgTree().appendToDefs(std::move(clippath)); } _xmlnode = nullptr; // append following elements to page group again return imgnode; } PsSpecialHandler::ImageNode PsSpecialHandler::createPDFNode (const string &fname, const string &path, int pageno, BoundingBox bbox, bool clip) { if (_pdfProc == "gs" || (_pdfProc.empty() && _psi.supportsPDF())) return createPSNode(fname, path, pageno, bbox, clip); ImageNode imgnode; if (PDFHandler::available()) { // save SVG state auto savedFont = _actions->svgTree().getFontPair(); auto savedMatrix = _actions->svgTree().getMatrix(); auto savedColor = _actions->svgTree().getColor(); imgnode.element = util::make_unique("g"); _pdfHandler.assignSVGTree(_actions->svgTree()); imgnode.element = _pdfHandler.convert(path, pageno, std::move(imgnode.element)); // restore SVG state if (savedFont.second) _actions->svgTree().setFont(savedFont.first, *savedFont.second); _actions->svgTree().setMatrix(savedMatrix); _actions->svgTree().setColor(savedColor); if (imgnode.element->empty()) imgnode.element.reset(nullptr); else { imgnode.matrix.translate(0, -bbox.height()).scale(1, -1); if (clip) { auto clippath = util::make_unique("clipPath"); clippath->addAttribute("id", "imgclip" + to_string(_imgClipCount)); clippath->append(bbox.createSVGPath()); imgnode.element->setClipPathUrl("imgclip" + to_string(_imgClipCount++)); _actions->svgTree().appendToDefs(std::move(clippath)); } } } return imgnode; } /** Apply transformation to width, height, and depth set by preview package. * @param[in] matrix transformation matrix to apply * @param[out] w width * @param[out] h height * @param[out] d depth * @return true if the baseline is still horizontal after the transformation */ static bool transform_box_extents (const Matrix &matrix, double &w, double &h, double &d) { DPair shift = matrix*DPair(0,0); // the translation component of the matrix DPair ex = matrix*DPair(1,0)-shift; DPair ey = matrix*DPair(0,1)-shift; if (ex.y() != 0 && ey.x() != 0) // rotation != mod 90 degrees? return false; // => non-horizontal baseline, can't compute meaningful extents if (ex.y() == 0) // horizontal scaling or skewing? w *= abs(ex.x()); if (ey.x()==0 || ex.y()==0) { // vertical scaling? if (ey.y() < 0) swap(h, d); double sy = abs(ey.y())/ey.length(); if (sy < 1e-8) h = d = 0; else { h *= abs(ey.y()/sy); d *= abs(ey.y()/sy); } } return true; } void PsSpecialHandler::dviBeginPage (unsigned int pageno, SpecialActions &actions) { _psi.execute("/@imgbase("+image_base_path(actions)+")store\n"); // path and basename of image files _imgClipCount = 0; } void PsSpecialHandler::dviEndPage (unsigned, SpecialActions &actions) { _previewHandler.readDataFromStack(); BoundingBox bbox; if (_previewHandler.getBoundingBox(bbox)) { // is there any data written by preview package? double w=0, h=0, d=0; if (actions.getBBoxFormatString() == "preview" || actions.getBBoxFormatString() == "min") { if (actions.getBBoxFormatString() == "preview") { w = max(0.0, _previewHandler.width()); h = max(0.0, _previewHandler.height()); d = max(0.0, _previewHandler.depth()); actions.bbox() = bbox; Message::mstream() << "\napplying bounding box set by"; } else { w = actions.bbox().width(); h = max(0.0, -actions.bbox().minY()); d = max(0.0, actions.bbox().maxY()); Message::mstream() << "\ncomputing extents based on data set by"; } Message::mstream() << " preview package (version " << _previewHandler.version() << ")\n"; // apply page transformations to box extents Matrix pagetrans = actions.getPageTransformation(); bool isBaselineHorizontal = transform_box_extents(pagetrans, w, h, d); actions.bbox().lock(); if (!isBaselineHorizontal) Message::mstream() << "can't determine height, width, and depth due to non-horizontal baseline\n"; else { const double bp2pt = 72.27/72.0; Message::mstream() << "width=" << XMLString(w*bp2pt) << "pt, " "height=" << XMLString(h*bp2pt) << "pt, " "depth=" << XMLString(d*bp2pt) << "pt\n"; } #if 0 auto rect = util::make_unique("rect"); rect->addAttribute("x", actions.bbox().minX()); rect->addAttribute("y", actions.bbox().minY()); rect->addAttribute("width", w); rect->addAttribute("height", h+d); rect->addAttribute("fill", "none"); rect->addAttribute("stroke", "red"); rect->addAttribute("stroke-width", "0.1"); actions.appendToPage(std::move(rect)); if (d > 0) { auto line = util::make_unique("line"); line->addAttribute("x1", actions.bbox().minX()); line->addAttribute("y1", actions.bbox().minY()+h); line->addAttribute("x2", actions.bbox().maxX()); line->addAttribute("y2", actions.bbox().minY()+h); line->addAttribute("stroke", "blue"); line->addAttribute("stroke-width", "0.1"); actions.appendToPage(std::move(line)); } #endif } } // close dictionary TeXDict and execute end-hook if defined if (_psSection == PS_BODY) { _psi.execute("\nend userdict/end-hook known{end-hook}if initgraphics "); initgraphics(); // reset graphics state to default values _psSection = PS_HEADERS; } } /////////////////////////////////////////////////////// void PsSpecialHandler::setpagedevice (std::vector &p) { _linewidth = 1; _linecap = _linejoin = 0; // butt end caps and miter joins _miterlimit = 4; _isshapealpha = false; // opacity operators change constant component by default _opacity = Opacity(); _sx = _sy = _cos = 1.0; _pattern = nullptr; _currentcolor = Color::BLACK; _dashoffset = 0; _dashpattern.clear(); _path.clear(); } void PsSpecialHandler::gsave (vector&) { _clipStack.dup(); } void PsSpecialHandler::grestore (vector&) { _clipStack.pop(); } void PsSpecialHandler::grestoreall (vector&) { _clipStack.pop(-1, true); } void PsSpecialHandler::save (vector &p) { _clipStack.dup(static_cast(p[0])); } void PsSpecialHandler::restore (vector &p) { _clipStack.pop(static_cast(p[0])); } void PsSpecialHandler::moveto (vector &p) { _path.moveto(p[0], p[1]); } void PsSpecialHandler::lineto (vector &p) { _path.lineto(p[0], p[1]); } void PsSpecialHandler::curveto (vector &p) { _path.cubicto(p[0], p[1], p[2], p[3], p[4], p[5]); } void PsSpecialHandler::closepath (vector&) { _path.closepath(); } void PsSpecialHandler::setblendmode (vector &p) { int mode = static_cast(p[0]); static const Opacity::BlendMode blendmodes[] = { Opacity::BM_NORMAL, Opacity::BM_MULTIPLY, Opacity::BM_SCREEN, Opacity::BM_OVERLAY, Opacity::BM_SOFTLIGHT, Opacity::BM_HARDLIGHT, Opacity::BM_COLORDODGE, Opacity::BM_COLORBURN, Opacity::BM_DARKEN, Opacity::BM_LIGHTEN, Opacity::BM_DIFFERENCE, Opacity::BM_EXCLUSION, Opacity::BM_HUE, Opacity::BM_SATURATION, Opacity::BM_COLOR, Opacity::BM_LUMINOSITY }; if (mode < 0 || mode > 15) mode = Opacity::BM_NORMAL; _opacity.setBlendMode(blendmodes[mode]); } void PsSpecialHandler::setfillconstantalpha (vector &p) { if (_isshapealpha) _opacity.fillalpha().setShapeAlpha(p[0]); else _opacity.fillalpha().setConstAlpha(p[0]); if (_actions) _actions->setOpacity(_opacity); } void PsSpecialHandler::setstrokeconstantalpha (vector &p) { if (_isshapealpha) _opacity.strokealpha().setShapeAlpha(p[0]); else _opacity.strokealpha().setConstAlpha(p[0]); if (_actions) _actions->setOpacity(_opacity); } /** Draws the current path recorded by previously executed path commands (moveto, lineto,...). * @param[in] p not used */ void PsSpecialHandler::stroke (vector &p) { _path.removeRedundantCommands(); if ((_path.empty() && !_clipStack.prependedPath()) || !_actions) return; BoundingBox bbox; if (!_actions->getMatrix().isIdentity()) { _path.transform(_actions->getMatrix()); if (!_xmlnode) bbox.transform(_actions->getMatrix()); } if (_clipStack.prependedPath()) _path.prepend(*_clipStack.prependedPath()); unique_ptr path; Pair point; if (_path.isDot(point)) { // zero-length path? if (_linecap == 1) { // round line ends? => draw dot double x = point.x(); double y = point.y(); double r = _linewidth/2.0; path = util::make_unique("circle"); path->addAttribute("cx", x); path->addAttribute("cy", y); path->addAttribute("r", r); path->setFillColor(_actions->getColor()); bbox = BoundingBox(x-r, y-r, x+r, y+r); } } else { // compute bounding box bbox = _path.computeBBox(); bbox.expand(_linewidth/2); ostringstream oss; _path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); path = util::make_unique("path"); path->addAttribute("d", oss.str()); path->setStrokeColor(_actions->getColor()); path->setNoFillColor(); path->setStrokeWidth(_linewidth); path->setStrokeMiterLimit(_miterlimit); path->setStrokeLineCap(_linecap == 0 ? SVGElement::LC_BUTT : _linecap == 1 ? SVGElement::LC_ROUND : SVGElement::LC_SQUARE); path->setStrokeLineJoin(_linejoin == 0 ? SVGElement::LJ_MITER : _linecap == 1 ? SVGElement::LJ_ROUND : SVGElement::LJ_BEVEL); path->setStrokeOpacity(_opacity); path->setStrokeDash(_dashpattern, _dashoffset); } if (path && _clipStack.path() && !_makingPattern) { // assign clipping path and clip bounding box path->setClipPathUrl("clip"+XMLString(_clipStack.topID())); bbox.intersect(_clipStack.path()->computeBBox()); _clipStack.removePrependedPath(); } if (_xmlnode) _xmlnode->append(std::move(path)); else { _actions->svgTree().appendToPage(std::move(path)); _actions->embed(bbox); } _path.clear(); } /** Draws a closed path filled with the current color. * @param[in] p not used * @param[in] evenodd true: use even-odd fill algorithm, false: use nonzero fill algorithm */ void PsSpecialHandler::fill (vector &p, bool evenodd) { _path.removeRedundantCommands(); if ((_path.empty() && !_clipStack.prependedPath()) || (_patternEnabled && !_pattern) || !_actions) return; // compute bounding box BoundingBox bbox = _path.computeBBox(); if (!_actions->getMatrix().isIdentity()) { _path.transform(_actions->getMatrix()); if (!_xmlnode) bbox.transform(_actions->getMatrix()); } if (_clipStack.prependedPath()) _path.prepend(*_clipStack.prependedPath()); ostringstream oss; _path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); auto path = util::make_unique("path"); path->addAttribute("d", oss.str()); if (_pattern) path->setFillPatternUrl(XMLString(_pattern->svgID())); else if (_actions->getColor() != Color::BLACK || _makingPattern) path->setFillColor(_actions->getColor(), false); if (_clipStack.path() && !_makingPattern) { // clip path active and not inside pattern definition? // assign clipping path and clip bounding box path->setClipPathUrl("clip"+XMLString(_clipStack.topID())); bbox.intersect(_clipStack.path()->computeBBox()); _clipStack.removePrependedPath(); } path->setFillRule(evenodd ? SVGElement::FR_EVENODD : SVGElement::FR_NONZERO); path->setFillOpacity(_opacity); if (_xmlnode) _xmlnode->append(std::move(path)); else { _actions->svgTree().appendToPage(std::move(path)); _actions->embed(bbox); } _path.clear(); } /** Postprocesses the 'image' operation performed by the PS interpreter. If * the PS image operator succeeded, there's now a PNG file that must be embedded * into the SVG file. */ void PsSpecialHandler::image (std::vector &p) { int imgID = static_cast(p[0]); // ID of PNG file written if (imgID < 0) // no bitmap file written? return; double width = p[1]; double height = p[2]; string suffix = (BITMAP_FORMAT.substr(0, 3) == "png" ? ".png" : ".jpg"); string fname = image_base_path(*_actions)+to_string(imgID)+suffix; ifstream ifs(fname, ios::binary); if (ifs) { ifs.close(); auto image = util::make_unique("image"); double x = _actions->getX(); double y = _actions->getY(); image->addAttribute("x", x); image->addAttribute("y", y); image->addAttribute("width", util::to_string(width)); image->addAttribute("height", util::to_string(height)); // The current transformation matrix (CTM) maps the unit square to the rectangular region // of the target canvas showing the bitmap (see PS Reference Manual, 4.10.3). Therefore, // the local pixel coordinates of the original bitmap must be transformed by CTM*inv(M) to // get the target coordinates. M is the matrix that maps the unit square to the bitmap rectangle. Matrix matrix{width, 0, 0, 0, -height, height}; // maps unit square to bitmap rectangle matrix = matrix.invert().lmultiply(_actions->getMatrix()); image->addAttribute("transform", matrix.toSVG()); // To prevent memory issues, only add the filename to the href attribute and tag it by '@' // for later base64 encoding. image->addAttribute("@xlink:href", "data:"+util::mimetype(fname)+";base64,"+fname); // if set, assign clipping path to image if (_clipStack.path()) { auto group = util::make_unique("g"); group->setClipPathUrl("clip"+XMLString(_clipStack.topID())); group->append(std::move(image)); image = std::move(group); // handle the entire group as image to add } if (_xmlnode) _xmlnode->append(std::move(image)); else { _actions->svgTree().appendToPage(std::move(image)); BoundingBox bbox(x, y, x+width, y+height); bbox.transform(matrix); if (_clipStack.path()) bbox.intersect(_clipStack.path()->computeBBox()); _actions->embed(bbox); } } } /** Creates a Matrix object from a given sequence of 6 double values. * The given values must be arranged in PostScript matrix order. * @param[in] v vector containing the matrix values * @param[in] startindex vector index of first component * @param[out] matrix the generated matrix */ static void create_matrix (vector &v, int startindex, Matrix &matrix) { // Ensure vector p has 6 elements. If necessary, add missing ones // using corresponding values of the identity matrix. if (v.size()-startindex < 6) { v.resize(6+startindex); for (size_t i=v.size()-startindex; i < 6; ++i) v[i+startindex] = (i%3 ? 0 : 1); } // PS matrix [a b c d e f] equals ((a,b,0),(c,d,0),(e,f,1)). // Since PS uses left multiplications, we must transpose and reorder // the matrix to ((a,c,e),(b,d,f),(0,0,1)). This is done by the // following swaps. swap(v[startindex+1], v[startindex+2]); // => (a, c, b, d, e, f) swap(v[startindex+2], v[startindex+4]); // => (a, c, e, d, b, f) swap(v[startindex+3], v[startindex+4]); // => (a, c, e, b, d, f) matrix.set(v, startindex); } /** Starts the definition of a new fill pattern. This operator * expects 9 parameters for tiling patterns (see PS reference 4.9.2): * @param[in] p the 9 values defining a tiling pattern (see PS reference 4.9.2): * 0: pattern type (0:none, 1:tiling, 2:shading) * 1: pattern ID * 2-5: lower left and upper right coordinates of pattern box * 6: horizontal distance of adjacent tiles * 7: vertical distance of adjacent tiles * 8: paint type (1: colored pattern, 2: uncolored pattern) * 9-14: pattern matrix */ void PsSpecialHandler::makepattern (vector &p) { int pattern_type = static_cast(p[0]); switch (pattern_type) { case 0: // pattern definition completed _xmlnode = _savenode; _savenode = nullptr; _makingPattern = false; break; case 1: { // tiling pattern int id = static_cast(p[1]); BoundingBox bbox(p[2], p[3], p[4], p[5]); const double &xstep=p[6], &ystep=p[7]; // horizontal and vertical distance of adjacent tiles int paint_type = static_cast(p[8]); Matrix matrix; // transformation matrix given together with pattern definition create_matrix(p, 9, matrix); matrix.lmultiply(_actions->getMatrix()); unique_ptr pattern; if (paint_type == 1) pattern = util::make_unique(id, bbox, matrix, xstep, ystep); else pattern = util::make_unique(id, bbox, matrix, xstep, ystep); _savenode = _xmlnode; _xmlnode = pattern->getContainerNode(); // insert the following SVG elements into this node _patterns[id] = std::move(pattern); _makingPattern = true; break; } case 2: { // define a shading pattern } } } /** Selects a previously defined fill pattern. * 0: pattern ID * 1-3: (optional) RGB values for uncolored tiling patterns * further parameters depend on the pattern type */ void PsSpecialHandler::setpattern (vector &p) { int patternID = static_cast(p[0]); Color color; if (p.size() == 4) color.setRGB(p[1], p[2], p[3]); auto it = _patterns.find(patternID); if (it == _patterns.end()) _pattern = nullptr; else { it->second->setColor(color); it->second->apply(*_actions); _pattern = it->second->tiled() ? static_cast(it->second.get()) : nullptr; } } /** Clears the current clipping path. * @param[in] p not used */ void PsSpecialHandler::initclip (vector &) { _clipStack.pushEmptyPath(); } /** Assigns the current clipping path to the graphics path. */ void PsSpecialHandler::clippath (std::vector&) { if (!_clipStack.empty()) _clipStack.setPrependedPath(); } /** Assigns a new clipping path to the graphics state using the current path. * If the graphics state already contains a clipping path, the new one is * computed by intersecting the current clipping path with the current graphics * path (see PS language reference, 3rd edition, pp. 193, 542) * @param[in] p not used * @param[in] evenodd true: use even-odd fill algorithm, false: use nonzero fill algorithm */ void PsSpecialHandler::clip (vector&, bool evenodd) { clip(_path, evenodd); } /** Assigns a new clipping path to the graphics state using the current path. * If the graphics state already contains a clipping path, the new one is * computed by intersecting the current one with the given path. * @param[in] path path used to restrict the clipping region * @param[in] evenodd true: use even-odd fill algorithm, false: use nonzero fill algorithm */ void PsSpecialHandler::clip (Path path, bool evenodd) { // when this method is called, _path contains the clipping path if (path.empty() || !_actions) return; Path::WindingRule windingRule = evenodd ? Path::WindingRule::EVEN_ODD : Path::WindingRule::NON_ZERO; path.setWindingRule(windingRule); if (!_actions->getMatrix().isIdentity()) path.transform(_actions->getMatrix()); if (_clipStack.prependedPath()) path = PathClipper().unite(*_clipStack.prependedPath(), path); int oldID = _clipStack.topID(); ostringstream oss; bool pathReplaced; if (!COMPUTE_CLIPPATHS_INTERSECTIONS || oldID < 1) { pathReplaced = _clipStack.replace(path); path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); } else { // compute the intersection of the current clipping path with the current graphics path const Path *oldPath = _clipStack.path(); Path intersectedPath = PathClipper().intersect(*oldPath, path); pathReplaced = _clipStack.replace(intersectedPath); intersectedPath.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); } if (pathReplaced) { auto pathElem = util::make_unique("path"); pathElem->addAttribute("d", oss.str()); pathElem->setClipRule(evenodd ? SVGElement::FR_EVENODD : SVGElement::FR_NONZERO); int newID = _clipStack.topID(); auto clipElem = util::make_unique("clipPath"); clipElem->addAttribute("id", XMLString("clip")+XMLString(newID)); if (!COMPUTE_CLIPPATHS_INTERSECTIONS && oldID) clipElem->setClipPathUrl("clip"+XMLString(oldID)); clipElem->append(std::move(pathElem)); _actions->svgTree().appendToDefs(std::move(clipElem)); } } /** Applies a gradient fill to the current graphics path. Vector p contains the shading parameters * in the following order: * - shading type (6=Coons, 7=tensor product) * - color space (1=gray, 3=rgb, 4=cmyk) * - 1.0 followed by the background color components based on the declared color space, or 0.0 * - 1.0 followed by the bounding box coordinates, or 0.0 * - geometry and color parameters depending on the shading type */ void PsSpecialHandler::shfill (vector ¶ms) { if (params.size() < 9) return; // collect common data relevant for all shading types int shadingTypeID = static_cast(params[0]); ColorSpace colorSpace = Color::ColorSpace::RGB; switch (static_cast(params[1])) { case 1: colorSpace = Color::ColorSpace::GRAY; break; case 3: colorSpace = Color::ColorSpace::RGB; break; case 4: colorSpace = Color::ColorSpace::CMYK; break; } VectorIterator it(params); it += 2; // skip shading type and color space // Get color to fill the whole mesh area before drawing the gradient colors on top of that background. // This is an optional parameter to shfill. bool bgcolorGiven = static_cast(*it++); Color bgcolor; if (bgcolorGiven) bgcolor.set(colorSpace, it); // Get clipping rectangle to limit the drawing area of the gradient mesh. // This is an optional parameter to shfill too. bool bboxGiven = static_cast(*it++); if (bboxGiven) { // bounding box given Path bboxPath; const double &x1 = *it++; const double &y1 = *it++; const double &x2 = *it++; const double &y2 = *it++; bboxPath.moveto(x1, y1); bboxPath.lineto(x2, y1); bboxPath.lineto(x2, y2); bboxPath.lineto(x1, y2); bboxPath.closepath(); clip(bboxPath, false); } try { if (shadingTypeID == 5) processLatticeTriangularPatchMesh(colorSpace, it); else processSequentialPatchMesh(shadingTypeID, colorSpace, it); } catch (ShadingException &e) { Message::estream(false) << "PostScript error: " << e.what() << '\n'; it.invalidate(); // stop processing the remaining patch data } catch (IteratorException &e) { Message::estream(false) << "PostScript error: incomplete shading data\n"; } if (bboxGiven) _clipStack.pop(); } /** Reads position and color data of a single shading patch from the data vector. * @param[in] shadingTypeID PS shading type ID identifying the format of the subsequent patch data * @param[in] edgeflag edge flag specifying how to connect the current patch to the preceding one * @param[in] cspace color space used to compute the color gradient * @param[in,out] it iterator used to sequentially access the patch data * @param[out] points the points defining the geometry of the patch * @param[out] colors the colors assigned to the vertices of the patch */ static void read_patch_data (const ShadingPatch &patch, int edgeflag, VectorIterator &it, vector &points, vector &colors) { // number of control points and colors required to define a single patch int numPoints = patch.numPoints(edgeflag); int numColors = patch.numColors(edgeflag); points.resize(numPoints); colors.resize(numColors); if (patch.psShadingType() == 4) { // format of a free-form triangular patch definition, where eN denotes // the edge of the corresponding vertex: // edge flag = 0, x1, y1, {color1}, e2, x2, y2, {color2}, e3, x3, y3, {color3} // edge flag > 0, x1, y1, {color1} for (int i=0; i < numPoints; i++) { if (i > 0) ++it; // skip redundant edge flag from free-form triangular patch double x = *it++; double y = *it++; points[i] = DPair(x, y); colors[i].set(patch.colorSpace(), it); } } else if (patch.psShadingType() == 6 || patch.psShadingType() == 7) { // format of each Coons/tensor product patch definition: // edge flag = 0, x1, y1, ... , xn, yn, {color1}, {color2}, {color3}, {color4} // edge flag > 0, x5, y5, ... , xn, yn, {color3}, {color4} for (int i=0; i < numPoints; i++) { double x = *it++; double y = *it++; points[i] = DPair(x, y); } for (int i=0; i < numColors; i++) colors[i].set(patch.colorSpace(), it); } } class ShadingCallback : public ShadingPatch::Callback { public: ShadingCallback (SpecialActions &actions, XMLElement *parent, int clippathID) : _actions(actions) { auto group = util::make_unique("g"); _group = group.get(); if (parent) parent->append(std::move(group)); else actions.svgTree().appendToPage(std::move(group)); if (clippathID > 0) _group->setClipPathUrl("clip"+XMLString(clippathID)); } void patchSegment (GraphicsPath &path, const Color &color) override { if (!_actions.getMatrix().isIdentity()) path.transform(_actions.getMatrix()); // draw a single patch segment ostringstream oss; path.writeSVG(oss, SVGTree::RELATIVE_PATH_CMDS); auto pathElem = util::make_unique("path"); pathElem->addAttribute("d", oss.str()); pathElem->setFillColor(color); _group->append(std::move(pathElem)); } private: SpecialActions &_actions; SVGElement *_group; }; /** Handle all patch meshes whose patches and their connections can be processed sequentially. * This comprises free-form triangular, Coons, and tensor-product patch meshes. */ void PsSpecialHandler::processSequentialPatchMesh (int shadingTypeID, ColorSpace colorSpace, VectorIterator &it) { unique_ptr previousPatch; while (it.valid()) { int edgeflag = static_cast(*it++); vector points; vector colors; unique_ptr patch = ShadingPatch::create(shadingTypeID, colorSpace); read_patch_data(*patch, edgeflag, it, points, colors); patch->setPoints(points, edgeflag, previousPatch.get()); patch->setColors(colors, edgeflag, previousPatch.get()); ShadingCallback callback(*_actions, _xmlnode, _clipStack.topID()); #if 0 if (bgcolorGiven) { // fill whole patch area with given background color GraphicsPath outline = patch->getBoundaryPath(); callback.patchSegment(outline, bgcolor); } #endif patch->approximate(SHADING_SEGMENT_SIZE, SHADING_SEGMENT_OVERLAP, SHADING_SIMPLIFY_DELTA, callback); if (!_xmlnode) { // update bounding box BoundingBox bbox = patch->getBBox(); bbox.transform(_actions->getMatrix()); _actions->embed(bbox); } previousPatch = std::move(patch); } } void PsSpecialHandler::processLatticeTriangularPatchMesh (ColorSpace colorSpace, VectorIterator &it) { int verticesPerRow = static_cast(*it++); if (verticesPerRow < 2) return; struct PatchVertex { DPair point; Color color; }; // hold two adjacent rows of vertices and colors vector row1(verticesPerRow); vector row2(verticesPerRow); vector *rowptr1 = &row1; vector *rowptr2 = &row2; // read data of first row for (int i=0; i < verticesPerRow; i++) { PatchVertex &vertex = (*rowptr1)[i]; vertex.point.x(*it++); vertex.point.y(*it++); vertex.color.set(colorSpace, it); } LatticeTriangularPatch patch(colorSpace); ShadingCallback callback(*_actions, _xmlnode, _clipStack.topID()); while (it.valid()) { // read next row for (int i=0; i < verticesPerRow; i++) { PatchVertex &vertex = (*rowptr2)[i]; vertex.point.x(*it++); vertex.point.y(*it++); vertex.color.set(colorSpace, it); } // create triangular patches for the vertices of the two rows for (int i=0; i < verticesPerRow-1; i++) { const PatchVertex &v1 = (*rowptr1)[i], &v2 = (*rowptr1)[i+1]; const PatchVertex &v3 = (*rowptr2)[i], &v4 = (*rowptr2)[i+1]; patch.setPoints(v1.point, v2.point, v3.point); patch.setColors(v1.color, v2.color, v3.color); patch.approximate(SHADING_SEGMENT_SIZE, SHADING_SEGMENT_OVERLAP, SHADING_SIMPLIFY_DELTA, callback); patch.setPoints(v2.point, v3.point, v4.point); patch.setColors(v2.color, v3.color, v4.color); patch.approximate(SHADING_SEGMENT_SIZE, SHADING_SEGMENT_OVERLAP, SHADING_SIMPLIFY_DELTA, callback); } swap(rowptr1, rowptr2); } } /** Clears current path. */ void PsSpecialHandler::newpath (vector &p) { bool calledByNewpathOp = (p[0] > 0); if (calledByNewpathOp) // function triggered by PS operator 'newpath'? _clipStack.removePrependedPath(); _path.clear(); } void PsSpecialHandler::setmatrix (vector &p) { if (_actions) { Matrix m; create_matrix(p, 0, m); _actions->setMatrix(m); } } // In contrast to SVG, PostScript transformations are applied in // reverse order (M' = T*M). Thus, the transformation matrices must be // left-multiplied in the following methods scale(), translate() and rotate(). void PsSpecialHandler::scale (vector &p) { if (_actions) { Matrix m = _actions->getMatrix(); ScalingMatrix s(p[0], p[1]); m.rmultiply(s); _actions->setMatrix(m); } } void PsSpecialHandler::translate (vector &p) { if (_actions) { Matrix m = _actions->getMatrix(); TranslationMatrix t(p[0], p[1]); m.rmultiply(t); _actions->setMatrix(m); } } void PsSpecialHandler::rotate (vector &p) { if (_actions) { Matrix m = _actions->getMatrix(); RotationMatrix r(p[0]); m.rmultiply(r); _actions->setMatrix(m); } } void PsSpecialHandler::setgray (vector &p) { if (!_patternEnabled) _pattern = nullptr; _currentcolor.setGray(p[0]); if (_actions) _actions->setColor(_currentcolor); } void PsSpecialHandler::setrgbcolor (vector &p) { if (!_patternEnabled) _pattern= nullptr; _currentcolor.setRGB(p[0], p[1], p[2]); if (_actions) _actions->setColor(_currentcolor); } void PsSpecialHandler::setcmykcolor (vector &p) { if (!_patternEnabled) _pattern = nullptr; _currentcolor.setCMYK(p[0], p[1], p[2], p[3]); if (_actions) _actions->setColor(_currentcolor); } void PsSpecialHandler::sethsbcolor (vector &p) { if (!_patternEnabled) _pattern = nullptr; _currentcolor.setHSB(p[0], p[1], p[2]); if (_actions) _actions->setColor(_currentcolor); } /** Sets the dash parameters used for stroking. * @param[in] p dash pattern array m1,...,mn plus trailing dash offset */ void PsSpecialHandler::setdash (vector &p) { _dashpattern.clear(); for (size_t i=0; i < p.size()-1; i++) _dashpattern.push_back(scale(p[i])); _dashoffset = scale(p.back()); } /** This method is called by the PSInterpreter if an PS operator has been executed. */ void PsSpecialHandler::executed () { if (_actions) _actions->progress("ps"); } /** This method is called by PSInterpreter if the status of the output devices has changed. * @param[in] p 1 if output device is the nulldevice, 1 otherwise */ void PsSpecialHandler::setnulldevice (vector &p) { if (_actions) { if (p[0] != 0) _actions->lockOutput(); // prevent further SVG output else _actions->unlockOutput(); // enable SVG output again } } //////////////////////////////////////////// void PsSpecialHandler::ClippingStack::pushEmptyPath () { if (!_stack.empty()) _stack.emplace(Entry()); } void PsSpecialHandler::ClippingStack::push (const Path &path, int saveID) { shared_ptr prependedPath; if (!_stack.empty()) prependedPath = _stack.top().prependedPath; if (path.empty()) _stack.emplace(saveID); else _stack.emplace(path, ++_maxID, saveID); _stack.top().prependedPath = prependedPath; } /** Pops a single or several elements from the clipping stack. * The method distinguishes between the following cases: * 1) saveID < 0 and grestoreall == false: * pop top element if it was pushed by gsave (its saveID is < 0 as well) * 2) saveID < 0 and grestoreall == true * repeat popping until stack is empty or the top element was pushed * by save (its saveID is >= 0) * 3) saveID >= 0: * pop all elements until the saveID of the top element equals parameter saveID */ void PsSpecialHandler::ClippingStack::pop (int saveID, bool grestoreall) { if (_stack.empty()) return; if (saveID < 0) { // grestore? if (_stack.top().saveID < 0) // pushed by 'gsave'? _stack.pop(); // pop all further elements pushed by 'gsave' if grestoreall == true while (grestoreall && !_stack.empty() && _stack.top().saveID < 0) _stack.pop(); } else { // pop elements pushed by 'gsave' while (!_stack.empty() && _stack.top().saveID != saveID) _stack.pop(); // pop element pushed by 'save' if (!_stack.empty()) _stack.pop(); } } /** Returns a pointer to the path on top of the stack, or 0 if the stack is empty. */ const PsSpecialHandler::Path* PsSpecialHandler::ClippingStack::path () const { return _stack.empty() ? nullptr : _stack.top().path.get(); } /** Returns a pointer to the path on top of the stack, or 0 if the stack is empty. */ const PsSpecialHandler::Path* PsSpecialHandler::ClippingStack::prependedPath () const { return _stack.empty() ? nullptr : _stack.top().prependedPath.get(); } void PsSpecialHandler::ClippingStack::removePrependedPath () { if (!_stack.empty()) _stack.top().prependedPath = nullptr; } /** Pops all elements from the stack. */ void PsSpecialHandler::ClippingStack::clear() { while (!_stack.empty()) _stack.pop(); } /** Replaces the top path by a new one. * @param[in] path new path to put on the stack * @return true if the new path differs from the previous one */ bool PsSpecialHandler::ClippingStack::replace (const Path &path) { if (_stack.empty()) push(path, -1); else if (_stack.top().path && path == *_stack.top().path) return false; else { _stack.top().path = make_shared(path); _stack.top().pathID = ++_maxID; } return true; } /** Duplicates the top element, i.e. the top element is pushed again. */ void PsSpecialHandler::ClippingStack::dup (int saveID) { _stack.emplace(_stack.empty() ? Entry() : _stack.top()); _stack.top().saveID = saveID; } void PsSpecialHandler::ClippingStack::setPrependedPath () { if (!_stack.empty()) _stack.top().prependedPath = _stack.top().path; } vector PsSpecialHandler::prefixes () const { vector pfx { "header=", // read and execute PS header file prior to the following PS statements "pdffile=", // process PDF file "psfile=", // read and execute PS file "PSfile=", // dito "ps:", // execute literal PS code wrapped by @beginspecial and @endspecial "ps::", // execute literal PS code without additional adaption of the drawing position "!", // execute literal PS header code following this prefix "\"", // execute literal PS code following this prefix "pst:", // dito "PST:", // same as "ps:" }; return pfx; }