/************************************************************************* ** PSInterpreter.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 #include "FileFinder.hpp" #include "FileSystem.hpp" #include "InputReader.hpp" #include "Message.hpp" #include "PSInterpreter.hpp" #include "SignalHandler.hpp" #include "utility.hpp" using namespace std; /** Constructs a new PSInterpreter object. * @param[in] actions template methods to be executed after recognizing the corresponding PS operator. */ PSInterpreter::PSInterpreter (PSActions *actions) : _mode(PS_NONE), _actions(actions) { } void PSInterpreter::init () { if (!_initialized) { vector gsargs { "gs", // dummy name "-q", // be quiet, suppress gs banner "-dNODISPLAY", // we don't need a display device "-dNOPAUSE", // keep going "-dWRITESYSTEMDICT", // leave systemdict writable as some operators must be replaced "-dNOPROMPT" }; if (int gsrev = _gs.revision()) { gsargs.emplace_back(gsrev == 922 ? "-dREALLYDELAYBIND" : "-dDELAYBIND"); // As of GS 9.50, -dSAFER is active by default which leads to warnings // in conjunction with -dDELAYBIND and -dWRITESYSTEMDICT. // Thus, -dDELAYSAFER (or -dNOSAFER) must be added. // https://www.ghostscript.com/doc/9.50/Use.htm#Safer if (gsrev >= 950) { gsargs.emplace_back("-dDELAYSAFER"); gsargs.emplace_back("-dALLOWPSTRANSPARENCY"); } // GS 9.55.0 introduced a new, C-based PDF interpreter which is enabled by default // as of GS 9.56.0. Since dvisvgm relies on the old PS-based interpreter for its // PDF support, we try to disable the new one. // https://www.ghostscript.com/doc/9.56.0/Use.htm#PDF_switches // As of GS 10.02.0, option NEWPDF has been deprecated and has no effect any longer. if (gsrev >= 9560 && gsrev < 10020) gsargs.emplace_back("-dNEWPDF=false"); } _gs.init(gsargs.size(), gsargs.data(), this); _gs.set_stdio(input, output, error); _initialized = true; // Before executing any random PS code redefine some operators and run // initializing PS code. This cannot be done in the constructor because we // need the completely initialized PSInterpreter object here. execute(PSDEFS); } } PSActions* PSInterpreter::setActions (PSActions *actions) { PSActions *old_actions = _actions; _actions = actions; return old_actions; } /** Checks if the given status value returned by Ghostscript indicates an error. * @param[in] status status value returned by Ghostscript after the execution of a PS snippet * @throw PSException if the status value indicates a PostScript error */ void PSInterpreter::checkStatus (int status) { if (status < 0) { _mode = PS_QUIT; if (status < -100) throw PSException("fatal error"); if (_errorMessage.empty()) throw PSException(_gs.error_name(status)); throw PSException(_errorMessage); } } /** Executes a chunk of PostScript code. * @param[in] str buffer containing the code * @param[in] len number of characters in buffer * @param[in] flush If true, a final 'flush' is sent which forces the output buffer to be written immediately. * @return true if the assigned number of bytes have been read */ bool PSInterpreter::execute (const char *str, size_t len, bool flush) { init(); if (_mode == PS_QUIT) return false; int status=0; if (_mode == PS_NONE) { _gs.run_string_begin(0, &status); _mode = PS_RUNNING; } checkStatus(status); bool complete=false; if (_bytesToRead > 0 && len >= _bytesToRead) { len = _bytesToRead; complete = true; } // feed Ghostscript with code chunks that are not larger than 64KB // => see documentation of gsapi_run_string_foo() const char *p=str; while (_mode == PS_RUNNING && len > 0) { SignalHandler::instance().check(); size_t chunksize = min(len, (size_t)0xffff); _gs.run_string_continue(p, chunksize, 0, &status); p += chunksize; len -= chunksize; if (_bytesToRead > 0) _bytesToRead -= chunksize; if (status == -101) // e_Quit _mode = PS_QUIT; else checkStatus(status); } if (flush) { // force writing contents of output buffer _gs.run_string_continue("\nflush ", 7, 0, &status); } return complete; } /** Executes a chunk of PostScript code read from a stream. The method returns on EOF. * @param[in] is the input stream * @param[in] flush If true, a final 'flush' is sent which forces the output buffer to be written immediately. * @return true if the assigned number of bytes have been read */ bool PSInterpreter::execute (istream &is, bool flush) { char buf[4096]; bool finished = false; while (is && !is.eof() && !finished) { is.read(buf, 4096); finished = execute(buf, is.gcount(), false); } execute("\n", 1, flush); return finished; } bool PSInterpreter::executeRaw (const string &str, int n) { _rawData.clear(); ostringstream oss; oss << str << ' ' << n << " (raw) prcmd\n"; execute(oss.str()); return !_rawData.empty(); } /** This callback function handles input from stdin to Ghostscript. Currently not needed. * @param[in] inst pointer to calling instance of PSInterpreter * @param[in] buf takes the read characters * @param[in] len size of buffer buf * @return number of characters read */ int GSDLLCALL PSInterpreter::input (void *inst, char *buf, int len) { return 0; } /** This callback function handles output from Ghostscript to stdout. It looks for * emitted commands staring with "dvi." and executes them by calling method callActions. * Ghostscript sends the text in chunks by several calls of this function. * Unfortunately, the PostScript specification wants error messages also to be sent to stdout * instead of stderr. Thus, we must collect and concatenate the chunks until an evaluable text * snippet is completely received. Furthermore, error messages have to be recognized and to be * filtered out. * @param[in] inst pointer to calling instance of PSInterpreter * @param[in] buf contains the characters to be output * @param[in] len number of characters in buf * @return number of processed characters (equals 'len') */ int GSDLLCALL PSInterpreter::output (void *inst, const char *buf, int len) { auto self = static_cast(inst); if (self && self->_actions) { const size_t MAXLEN = 512; // maximal line length (longer lines are of no interest) const char *end = buf+len-1; // last position of buf for (const char *first=buf, *last=buf; first <= end; last++, first=last) { // move first and last to begin and end of the next line, respectively while (last < end && *last != '\n') last++; size_t linelength = last-first+1; if (linelength > MAXLEN) // skip long lines since they don't contain any relevant information continue; vector &linebuf = self->_linebuf; // just a shorter name... if ((*last == '\n' || !self->active()) || self->_inError) { if (linelength + linebuf.size() > 1) { // prefix "dvi." plus final newline SplittedCharInputBuffer ib(linebuf.empty() ? nullptr : &linebuf[0], linebuf.size(), first, linelength); BufferInputReader in(ib); if (self->_inError) self->_errorMessage += string(first, linelength); else { in.skipSpace(); if (in.check("Unrecoverable error: ")) { self->_errorMessage.clear(); while (!in.eof()) self->_errorMessage += char(in.get()); self->_inError = true; } else if (in.check("dvi.")) self->callActions(in); } } linebuf.clear(); } else { // no line end found => // save remaining characters and prepend them to the next incoming chunk of characters if (linebuf.size() + linelength > MAXLEN) linebuf.clear(); // don't care for long lines else { size_t currsize = linebuf.size(); linebuf.resize(currsize+linelength); memcpy(&linebuf[currsize], first, linelength); } } } } return len; } /** Evaluates a command emitted by Ghostscript and invokes the corresponding * method of interface class PSActions. * @param[in] in reader pointing to the next command */ void PSInterpreter::callActions (InputReader &in) { struct Operator { int pcount; // number of parameters (< 0 : variable number of parameters) void (PSActions::*handler)(vector &p); // operation handler }; static const unordered_map operators { {"applyscalevals", { 3, &PSActions::applyscalevals}}, {"clip", { 0, &PSActions::clip}}, {"clippath", { 0, &PSActions::clippath}}, {"closepath", { 0, &PSActions::closepath}}, {"curveto", { 6, &PSActions::curveto}}, {"eoclip", { 0, &PSActions::eoclip}}, {"eofill", { 0, &PSActions::eofill}}, {"fill", { 0, &PSActions::fill}}, {"grestore", { 0, &PSActions::grestore}}, {"grestoreall", { 0, &PSActions::grestoreall}}, {"gsave", { 0, &PSActions::gsave}}, {"image", { 3, &PSActions::image}}, {"initclip", { 0, &PSActions::initclip}}, {"lineto", { 2, &PSActions::lineto}}, {"makepattern", {-1, &PSActions::makepattern}}, {"moveto", { 2, &PSActions::moveto}}, {"newpath", { 1, &PSActions::newpath}}, {"querypos", { 2, &PSActions::querypos}}, {"raw", {-1, nullptr}}, {"restore", { 1, &PSActions::restore}}, {"rotate", { 1, &PSActions::rotate}}, {"save", { 1, &PSActions::save}}, {"scale", { 2, &PSActions::scale}}, {"setalphaisshape", { 1, &PSActions::setalphaisshape}}, {"setblendmode", { 1, &PSActions::setblendmode}}, {"setcolorspace", { 1, &PSActions::setcolorspace}}, {"setcmykcolor", { 4, &PSActions::setcmykcolor}}, {"setdash", {-1, &PSActions::setdash}}, {"setfillconstantalpha", { 1, &PSActions::setfillconstantalpha}}, {"setgray", { 1, &PSActions::setgray}}, {"sethsbcolor", { 3, &PSActions::sethsbcolor}}, {"setlinecap", { 1, &PSActions::setlinecap}}, {"setlinejoin", { 1, &PSActions::setlinejoin}}, {"setlinewidth", { 1, &PSActions::setlinewidth}}, {"setmatrix", { 6, &PSActions::setmatrix}}, {"setmiterlimit", { 1, &PSActions::setmiterlimit}}, {"setnulldevice", { 1, &PSActions::setnulldevice}}, {"setpagedevice", { 0, &PSActions::setpagedevice}}, {"setpattern", {-1, &PSActions::setpattern}}, {"setrgbcolor", { 3, &PSActions::setrgbcolor}}, {"setstrokeconstantalpha", { 1, &PSActions::setstrokeconstantalpha}}, {"shfill", {-1, &PSActions::shfill}}, {"stroke", { 0, &PSActions::stroke}}, {"translate", { 2, &PSActions::translate}}, }; if (_actions) { in.skipSpace(); auto it = operators.find(in.getWord()); if (it != operators.end()) { if (!it->second.handler) { // raw string data received? _rawData.clear(); in.skipSpace(); while (!in.eof()) { _rawData.push_back(in.getString()); in.skipSpace(); } } else { // collect parameters vector params; int pcount = it->second.pcount; if (pcount < 0) { // variable number of parameters? in.skipSpace(); while (!in.eof()) { // read all available parameters params.push_back(in.getString()); in.skipSpace(); } } else { // fix number of parameters for (int i=0; i < pcount; i++) { in.skipSpace(); params.push_back(in.getString()); } } // convert parameter strings to doubles vector v(params.size()); transform(params.begin(), params.end(), v.begin(), [](const string &str) { return stod(str); }); // call operator handler (_actions->*it->second.handler)(v); _actions->executed(); } } } } /** This callback function handles output from Ghostscript to stderr. * @param[in] inst pointer to calling instance of PSInterpreter * @param[in] buf contains the characters to be output * @param[in] len number of chars in buf * @return number of processed characters */ int GSDLLCALL PSInterpreter::error (void *inst, const char *buf, int len) { return len; } /** Returns the total number of pages of a PDF file. * @param[in] fname name/path of the PDF file */ int PSInterpreter::pdfPageCount (const string &fname) { executeRaw("\n("+FileSystem::ensureForwardSlashes(fname)+")@pdfpagecount ", 1); if (!_rawData.empty()) { size_t index; int ret = stoi(_rawData[0], &index, 10); if (index > 0) return ret; } return 0; } /** Returns the bounding box of a PDF page. If the selected page doesn't exist, * the "invalid" flag of the returned bounding box is set. * @param[in] fname name/path of the PDF file * @param[in] pageno page number * @return the bounding box of the given page */ BoundingBox PSInterpreter::pdfPageBox (const string &fname, int pageno) { BoundingBox pagebox; executeRaw("\n"+to_string(pageno)+"("+FileSystem::ensureForwardSlashes(fname)+")@pdfpagebox ", 4); if (_rawData.size() < 4) pagebox.invalidate(); else pagebox = BoundingBox(stod(_rawData[0]), stod(_rawData[1]), stod(_rawData[2]), stod(_rawData[3])); return pagebox; } vector PSInterpreter::getImageDeviceInfos () { vector infos { {"none", "no processing of bitmap images"}, {"jpeg", "color JPEG format"}, {"jpeggray", "grayscale JPEG format"}, {"png", "grayscale or 24-bit color PNG format"}, {"pnggray", "grayscale PNG format"}, {"pngmono", "black-and-white PNG format"}, {"pngmonod", "dithered black-and-white PNG format"}, {"png16", "4-bit color PNG format"}, {"png256", "8-bit color PNG format"}, {"png16m", "24-bit color PNG format"}, }; return infos; } void PSInterpreter::listImageDeviceInfos (ostream &os) { for (const PSDeviceInfo &info : getImageDeviceInfos()) os << setw(8) << left << info.name << " | " << info.description << '\n'; } /** Returns true if a given PS device name is known. The function deosn't * check whether the device is actually available. * @param[in] deviceStr device specifier of the form [:] */ bool PSInterpreter::imageDeviceKnown (string deviceStr) { if (deviceStr.empty() || !isalpha(deviceStr[0])) return false; deviceStr = deviceStr.substr(0, deviceStr.find(':')); // strip optional argument auto infos = getImageDeviceInfos(); auto it = find_if(infos.begin(), infos.end(), [&](const PSDeviceInfo &info) { return info.name == deviceStr; }); return it != infos.end(); } /** Sets the output device used to create bitmap images. * @param[in] deviceStr device specifier of the form [:] * @return true on success, false if device is not supported */ bool PSInterpreter::setImageDevice (const string &deviceStr) { auto params = util::split(deviceStr, ":"); string name = util::tolower(params[0]); if (!imageDeviceKnown(name)) return false; if (name != "jpeg" && name != "png" && name != "none") { // check if image device is supported by Ghostscript executeRaw("devicedict/"+name+" known{1}{0}ifelse\n", 1); if (_rawData.empty() || _rawData[0] != "1") throw PSException("output device '"+name+"' is not available"); } string ps = "/@imgdevice("+name+")store "; try { if (params.size() > 1) { // set JPEG quality level if given if (name.substr(0, 4) == "jpeg") { int quality = max(0, min(stoi(params[1]), 100)); ps += "/JPEGQ "+to_string(quality)+" def "; } else if (name == "pngmonod") { int minFeatureSize = max(0, min(stoi(params[1]), 4)); ps += "/MinFeatureSize "+to_string(minFeatureSize)+" def "; } } } catch (...) { throw PSException("invalid device option '"+params[1]+"' (integer expected)"); } execute(ps); return true; }