File indexing completed on 2024-05-12 16:06:39

0001 // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*-
0002 //
0003 // ghostscript_interface
0004 //
0005 // Part of KDVI - A framework for multipage text/gfx viewers
0006 //
0007 // SPDX-FileCopyrightText: 2004 Stefan Kebekus
0008 // SPDX-License-Identifier: GPL-2.0-or-later
0009 
0010 #include <config.h>
0011 
0012 #include "debug_dvi.h"
0013 #include "dviFile.h"
0014 #include "pageNumber.h"
0015 #include "psgs.h"
0016 #include "psheader.cpp"
0017 
0018 #include <KLocalizedString>
0019 #include <KProcess>
0020 #include <QTemporaryFile>
0021 #include <QUrl>
0022 
0023 #include <QDir>
0024 #include <QLoggingCategory>
0025 #include <QPainter>
0026 #include <QPixmap>
0027 #include <QStandardPaths>
0028 #include <QTextStream>
0029 #include <QTimer>
0030 
0031 //#define DEBUG_PSGS
0032 
0033 // extern char psheader[];
0034 
0035 pageInfo::pageInfo(const QString &_PostScriptString)
0036 {
0037     PostScriptString = new QString(_PostScriptString);
0038     background = Qt::white;
0039     permanentBackground = Qt::white;
0040 }
0041 
0042 pageInfo::~pageInfo()
0043 {
0044     if (PostScriptString != nullptr) {
0045         delete PostScriptString;
0046     }
0047 }
0048 
0049 // ======================================================
0050 
0051 ghostscript_interface::ghostscript_interface()
0052 {
0053     PostScriptHeaderString = new QString();
0054 
0055     knownDevices.append(QStringLiteral("png16m"));
0056     knownDevices.append(QStringLiteral("jpeg"));
0057     knownDevices.append(QStringLiteral("pnn"));
0058     knownDevices.append(QStringLiteral("pnnraw"));
0059     gsDevice = knownDevices.begin();
0060 }
0061 
0062 ghostscript_interface::~ghostscript_interface()
0063 {
0064     if (PostScriptHeaderString != nullptr) {
0065         delete PostScriptHeaderString;
0066     }
0067     qDeleteAll(pageList);
0068 }
0069 
0070 void ghostscript_interface::setPostScript(const quint16 page, const QString &PostScript)
0071 {
0072 #ifdef DEBUG_PSGS
0073     qCDebug(OkularDviDebug) << "ghostscript_interface::setPostScript( " << page << ", ... )";
0074 #endif
0075 
0076     if (pageList.value(page) == nullptr) {
0077         pageInfo *info = new pageInfo(PostScript);
0078         // Check if dict is big enough
0079         if (pageList.count() > pageList.capacity() - 2) {
0080             pageList.reserve(pageList.capacity() * 2);
0081         }
0082         pageList.insert(page, info);
0083     } else {
0084         *(pageList.value(page)->PostScriptString) = PostScript;
0085     }
0086 }
0087 
0088 void ghostscript_interface::setIncludePath(const QString &_includePath)
0089 {
0090     if (_includePath.isEmpty()) {
0091         includePath = QLatin1Char('*'); // Allow all files
0092     } else {
0093         includePath = _includePath + QStringLiteral("/*");
0094     }
0095 }
0096 
0097 void ghostscript_interface::setBackgroundColor(const quint16 page, const QColor &background_color, bool permanent)
0098 {
0099 #ifdef DEBUG_PSGS
0100     qCDebug(OkularDviDebug) << "ghostscript_interface::setBackgroundColor( " << page << ", " << background_color << " )";
0101 #endif
0102 
0103     if (pageList.value(page) == nullptr) {
0104         pageInfo *info = new pageInfo(QString());
0105         info->background = background_color;
0106         if (permanent) {
0107             info->permanentBackground = background_color;
0108         }
0109         // Check if dict is big enough
0110         if (pageList.count() > pageList.capacity() - 2) {
0111             pageList.reserve(pageList.capacity() * 2);
0112         }
0113         pageList.insert(page, info);
0114     } else {
0115         pageList.value(page)->background = background_color;
0116         if (permanent) {
0117             pageList.value(page)->permanentBackground = background_color;
0118         }
0119     }
0120 }
0121 
0122 void ghostscript_interface::restoreBackgroundColor(const quint16 page)
0123 {
0124 #ifdef DEBUG_PSGS
0125     qCDebug(OkularDviDebug) << "ghostscript_interface::restoreBackgroundColor( " << page << " )";
0126 #endif
0127     if (pageList.value(page) == nullptr) {
0128         return;
0129     }
0130 
0131     pageInfo *info = pageList.value(page);
0132     info->background = info->permanentBackground;
0133 }
0134 
0135 // Returns the background color for a certain page. This color is
0136 // always guaranteed to be valid
0137 
0138 QColor ghostscript_interface::getBackgroundColor(const quint16 page) const
0139 {
0140 #ifdef DEBUG_PSGS
0141     qCDebug(OkularDviDebug) << "ghostscript_interface::getBackgroundColor( " << page << " )";
0142 #endif
0143 
0144     if (pageList.value(page) == nullptr) {
0145         return Qt::white;
0146     } else {
0147         return pageList.value(page)->background;
0148     }
0149 }
0150 
0151 void ghostscript_interface::clear()
0152 {
0153     PostScriptHeaderString->truncate(0);
0154 
0155     // Deletes all items, removes temporary files, etc.
0156     qDeleteAll(pageList);
0157     pageList.clear();
0158 }
0159 
0160 void ghostscript_interface::gs_generate_graphics_file(const quint16 page, const QString &filename, long magnification)
0161 {
0162 #ifdef DEBUG_PSGS
0163     qCDebug(OkularDviDebug) << "ghostscript_interface::gs_generate_graphics_file( " << page << ", " << filename << " )";
0164 #endif
0165 
0166     if (knownDevices.isEmpty()) {
0167         qCCritical(OkularDviDebug) << "No known devices found";
0168         return;
0169     }
0170 
0171     // Make sure gs is in PATH and not just in the CWD
0172     static const QString gsFullPath = QStandardPaths::findExecutable(QStringLiteral("gs"));
0173     if (gsFullPath.isEmpty()) {
0174         qCCritical(OkularDviDebug) << "gs is not in path";
0175         return;
0176     }
0177 
0178     pageInfo *info = pageList.value(page);
0179 
0180     // Generate a PNG-file
0181     // Step 1: Write the PostScriptString to a File
0182     QTemporaryFile PSfile(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps"));
0183     PSfile.setAutoRemove(false);
0184     PSfile.open();
0185     const QString PSfileName = PSfile.fileName();
0186 
0187     QTextStream os(&PSfile);
0188     os << "%!PS-Adobe-2.0\n"
0189        << "%%Creator: kdvi\n"
0190        << "%%Title: KDVI temporary PostScript\n"
0191        << "%%Pages: 1\n"
0192        << "%%PageOrder: Ascend\n"
0193        // HSize and VSize in 1/72 inch
0194        << "%%BoundingBox: 0 0 " << (qint32)(72 * (pixel_page_w / resolution)) << ' ' << (qint32)(72 * (pixel_page_h / resolution)) << '\n'
0195        << "%%EndComments\n"
0196        << "%!\n"
0197        << psheader
0198        << "TeXDict begin "
0199        // HSize in (1/(65781.76*72))inch
0200        << (qint32)(72 * 65781 * (pixel_page_w / resolution))
0201        << ' '
0202        // VSize in (1/(65781.76*72))inch
0203        << (qint32)(72 * 65781 * (pixel_page_h / resolution))
0204        << ' '
0205        // Magnification
0206        << (qint32)(magnification)
0207        // dpi and vdpi
0208        << " 300 300"
0209        // Name
0210        << " (test.dvi)"
0211        << " @start end\n"
0212        << "TeXDict begin\n"
0213        // Start page
0214        << "1 0 bop 0 0 a \n";
0215 
0216     if (!PostScriptHeaderString->toLatin1().isNull()) {
0217         os << PostScriptHeaderString->toLatin1();
0218     }
0219 
0220     if (info->background != Qt::white) {
0221         QString colorCommand = QStringLiteral("gsave %1 %2 %3 setrgbcolor clippath fill grestore\n").arg(info->background.red() / 255.0).arg(info->background.green() / 255.0).arg(info->background.blue() / 255.0);
0222         os << colorCommand.toLatin1();
0223     }
0224 
0225     if (!info->PostScriptString->isNull()) {
0226         os << *(info->PostScriptString);
0227     }
0228 
0229     os << "end\n"
0230        << "showpage \n";
0231 
0232     PSfile.close();
0233 
0234     // Step 2: Call GS with the File
0235     QFile::remove(filename);
0236     KProcess proc;
0237     proc.setOutputChannelMode(KProcess::SeparateChannels);
0238     QStringList argus;
0239     argus << gsFullPath;
0240     argus << QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dDELAYSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH");
0241     argus << QStringLiteral("-sDEVICE=%1").arg(*gsDevice);
0242     argus << QStringLiteral("-sOutputFile=%1").arg(filename);
0243     argus << QStringLiteral("-sExtraIncludePath=%1").arg(includePath);
0244     argus << QStringLiteral("-g%1x%2").arg(pixel_page_w).arg(pixel_page_h); // page size in pixels
0245     argus << QStringLiteral("-r%1").arg(resolution);                        // resolution in dpi
0246     argus << QStringLiteral("-dTextAlphaBits=4 -dGraphicsAlphaBits=2");     // Antialiasing
0247     argus << QStringLiteral("-c") << QStringLiteral("<< /PermitFileReading [ ExtraIncludePath ] /PermitFileWriting [] /PermitFileControl [] >> setuserparams .locksafe");
0248     argus << QStringLiteral("-f") << PSfileName;
0249 
0250 #ifdef DEBUG_PSGS
0251     qCDebug(OkularDviDebug) << argus.join(" ");
0252 #endif
0253 
0254     proc << argus;
0255     int res = proc.execute();
0256 
0257     if (res) {
0258         // Starting ghostscript did not work.
0259         // TODO: Issue error message, switch PS support off.
0260         qCCritical(OkularDviDebug) << "ghostview could not be started";
0261     }
0262 
0263     PSfile.remove();
0264 
0265     // Check if gs has indeed produced a file.
0266     if (QFile::exists(filename) == false) {
0267         qCCritical(OkularDviDebug) << "GS did not produce output.";
0268 
0269         // No. Check is the reason is that the device is not compiled into
0270         // ghostscript. If so, try again with another device.
0271         QString GSoutput;
0272         proc.setReadChannel(QProcess::StandardOutput);
0273         while (proc.canReadLine()) {
0274             GSoutput = QString::fromLocal8Bit(proc.readLine());
0275             if (GSoutput.contains(QStringLiteral("Unknown device"))) {
0276                 qCDebug(OkularDviDebug) << QString::fromLatin1(
0277                                                "The version of ghostview installed on this computer does not support "
0278                                                "the '%1' ghostview device driver.")
0279                                                .arg(*gsDevice);
0280                 knownDevices.erase(gsDevice);
0281                 gsDevice = knownDevices.begin();
0282                 if (knownDevices.isEmpty()) {
0283                     // TODO: show a requestor of some sort.
0284                     Q_EMIT error(i18n("The version of Ghostview that is installed on this computer does not contain "
0285                                       "any of the Ghostview device drivers that are known to Okular. PostScript "
0286                                       "support has therefore been turned off in Okular."),
0287                                  -1);
0288                 } else {
0289                     qCDebug(OkularDviDebug) << QStringLiteral("Okular will now try to use the '%1' device driver.").arg(*gsDevice);
0290                     gs_generate_graphics_file(page, filename, magnification);
0291                 }
0292                 return;
0293             }
0294         }
0295     }
0296 }
0297 
0298 void ghostscript_interface::graphics(const quint16 page, double dpi, long magnification, QPainter *paint)
0299 {
0300 #ifdef DEBUG_PSGS
0301     qCDebug(OkularDviDebug) << "ghostscript_interface::graphics( " << page << ", " << dpi << ", ... ) called.";
0302 #endif
0303 
0304     if (paint == nullptr) {
0305         qCCritical(OkularDviDebug) << "ghostscript_interface::graphics(PageNumber page, double dpi, long magnification, QPainter *paint) called with paint == 0";
0306         return;
0307     }
0308 
0309     resolution = dpi;
0310 
0311     pixel_page_w = paint->viewport().width();
0312     pixel_page_h = paint->viewport().height();
0313 
0314     pageInfo *info = pageList.value(page);
0315 
0316     // No PostScript? Then return immediately.
0317     if ((info == nullptr) || (info->PostScriptString->isEmpty())) {
0318 #ifdef DEBUG_PSGS
0319         qCDebug(OkularDviDebug) << "No PostScript found. Not drawing anything.";
0320 #endif
0321         return;
0322     }
0323 
0324     QTemporaryFile gfxFile;
0325     gfxFile.open();
0326     const QString gfxFileName = gfxFile.fileName();
0327     // We are want the filename, not the file.
0328     gfxFile.close();
0329 
0330     gs_generate_graphics_file(page, gfxFileName, magnification);
0331 
0332     QImage MemoryCopy(gfxFileName);
0333     paint->drawImage(0, 0, MemoryCopy);
0334     return;
0335 }
0336 
0337 QString ghostscript_interface::locateEPSfile(const QString &filename, const QUrl &base)
0338 {
0339     // If the base URL indicates that the DVI file is local, try to find
0340     // the graphics file in the directory where the DVI file resides
0341     if (base.isLocalFile()) {
0342         QString path = base.path(); // -> "/bar/foo.dvi"
0343         QFileInfo fi1(path);
0344         QFileInfo fi2(fi1.dir(), filename);
0345         if (fi2.exists()) {
0346             return fi2.absoluteFilePath();
0347         }
0348     }
0349 
0350     // Otherwise, use kpsewhich to find the eps file.
0351     // Make sure kpsewhich is in PATH and not just in the CWD
0352     static const QString fullPath = QStandardPaths::findExecutable(QStringLiteral("kpsewhich"));
0353     if (fullPath.isEmpty()) {
0354         return {};
0355     }
0356 
0357     KProcess proc;
0358     proc << fullPath << filename;
0359     proc.execute();
0360     return QString::fromLocal8Bit(proc.readLine().trimmed());
0361 }