File indexing completed on 2024-04-28 15:25:39

0001 /*
0002     QImageIO Routines to read/write EPS images.
0003     SPDX-FileCopyrightText: 1998 Dirk Schoenberger <dirk.schoenberger@freenet.de>
0004     SPDX-FileCopyrightText: 2013 Alex Merry <alex.merry@kdemail.net>
0005 
0006     Includes code by Sven Wiegand <SWiegand@tfh-berlin.de> from KSnapshot
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 #include "eps_p.h"
0011 
0012 #include <QCoreApplication>
0013 #include <QImage>
0014 #include <QImageReader>
0015 #include <QPainter>
0016 #include <QPrinter>
0017 #include <QProcess>
0018 #include <QStandardPaths>
0019 #include <QTemporaryFile>
0020 
0021 // logging category for this framework, default: log stuff >= warning
0022 Q_LOGGING_CATEGORY(EPSPLUGIN, "kf.imageformats.plugins.eps", QtWarningMsg)
0023 
0024 //#define EPS_PERFORMANCE_DEBUG 1
0025 
0026 #define BBOX_BUFLEN 200
0027 #define BBOX "%%BoundingBox:"
0028 #define BBOX_LEN strlen(BBOX)
0029 
0030 static bool seekToCodeStart(QIODevice *io, qint64 &ps_offset, qint64 &ps_size)
0031 {
0032     char buf[4]; // We at most need to read 4 bytes at a time
0033     ps_offset = 0L;
0034     ps_size = 0L;
0035 
0036     if (io->read(buf, 2) != 2) { // Read first two bytes
0037         qCDebug(EPSPLUGIN) << "EPS file has less than 2 bytes.";
0038         return false;
0039     }
0040 
0041     if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
0042         qCDebug(EPSPLUGIN) << "normal EPS file";
0043     } else if (buf[0] == char(0xc5) && buf[1] == char(0xd0)) { // Check start of MS-DOS EPS magic
0044         // May be a MS-DOS EPS file
0045         if (io->read(buf + 2, 2) != 2) { // Read further bytes of MS-DOS EPS magic
0046             qCDebug(EPSPLUGIN) << "potential MS-DOS EPS file has less than 4 bytes.";
0047             return false;
0048         }
0049         if (buf[2] == char(0xd3) && buf[3] == char(0xc6)) { // Check last bytes of MS-DOS EPS magic
0050             if (io->read(buf, 4) != 4) { // Get offset of PostScript code in the MS-DOS EPS file.
0051                 qCDebug(EPSPLUGIN) << "cannot read offset of MS-DOS EPS file";
0052                 return false;
0053             }
0054             ps_offset // Offset is in little endian
0055                 = qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
0056             if (io->read(buf, 4) != 4) { // Get size of PostScript code in the MS-DOS EPS file.
0057                 qCDebug(EPSPLUGIN) << "cannot read size of MS-DOS EPS file";
0058                 return false;
0059             }
0060             ps_size // Size is in little endian
0061                 = qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
0062             qCDebug(EPSPLUGIN) << "Offset: " << ps_offset << " Size: " << ps_size;
0063             if (!io->seek(ps_offset)) { // Get offset of PostScript code in the MS-DOS EPS file.
0064                 qCDebug(EPSPLUGIN) << "cannot seek in MS-DOS EPS file";
0065                 return false;
0066             }
0067             if (io->read(buf, 2) != 2) { // Read first two bytes of what should be the Postscript code
0068                 qCDebug(EPSPLUGIN) << "PostScript code has less than 2 bytes.";
0069                 return false;
0070             }
0071             if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
0072                 qCDebug(EPSPLUGIN) << "MS-DOS EPS file";
0073             } else {
0074                 qCDebug(EPSPLUGIN) << "supposed Postscript code of a MS-DOS EPS file doe not start with %!.";
0075                 return false;
0076             }
0077         } else {
0078             qCDebug(EPSPLUGIN) << "wrong magic for potential MS-DOS EPS file!";
0079             return false;
0080         }
0081     } else {
0082         qCDebug(EPSPLUGIN) << "not an EPS file!";
0083         return false;
0084     }
0085     return true;
0086 }
0087 
0088 static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
0089 {
0090     char buf[BBOX_BUFLEN + 1];
0091 
0092     bool ret = false;
0093 
0094     while (io->readLine(buf, BBOX_BUFLEN) > 0) {
0095         if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
0096             // Some EPS files have non-integer values for the bbox
0097             // We don't support that currently, but at least we parse it
0098             float _x1;
0099             float _y1;
0100             float _x2;
0101             float _y2;
0102             if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
0103                 qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
0104                 *x1 = int(_x1);
0105                 *y1 = int(_y1);
0106                 *x2 = int(_x2);
0107                 *y2 = int(_y2);
0108                 ret = true;
0109                 break;
0110             }
0111         }
0112     }
0113 
0114     return ret;
0115 }
0116 
0117 EPSHandler::EPSHandler()
0118 {
0119 }
0120 
0121 bool EPSHandler::canRead() const
0122 {
0123     if (canRead(device())) {
0124         setFormat("eps");
0125         return true;
0126     }
0127     return false;
0128 }
0129 
0130 bool EPSHandler::read(QImage *image)
0131 {
0132     qCDebug(EPSPLUGIN) << "starting...";
0133 
0134     int x1;
0135     int y1;
0136     int x2;
0137     int y2;
0138 #ifdef EPS_PERFORMANCE_DEBUG
0139     QTime dt;
0140     dt.start();
0141 #endif
0142 
0143     QIODevice *io = device();
0144     qint64 ps_offset;
0145     qint64 ps_size;
0146 
0147     // find start of PostScript code
0148     if (!seekToCodeStart(io, ps_offset, ps_size)) {
0149         return false;
0150     }
0151 
0152     qCDebug(EPSPLUGIN) << "Offset:" << ps_offset << "; size:" << ps_size;
0153 
0154     // find bounding box
0155     if (!bbox(io, &x1, &y1, &x2, &y2)) {
0156         qCDebug(EPSPLUGIN) << "no bounding box found!";
0157         return false;
0158     }
0159 
0160     QTemporaryFile tmpFile;
0161     if (!tmpFile.open()) {
0162         qCWarning(EPSPLUGIN) << "Could not create the temporary file" << tmpFile.fileName();
0163         return false;
0164     }
0165     qCDebug(EPSPLUGIN) << "temporary file:" << tmpFile.fileName();
0166 
0167     // x1, y1 -> translation
0168     // x2, y2 -> new size
0169 
0170     x2 -= x1;
0171     y2 -= y1;
0172     qCDebug(EPSPLUGIN) << "origin point: " << x1 << "," << y1 << "  size:" << x2 << "," << y2;
0173     double xScale = 1.0;
0174     double yScale = 1.0;
0175     int wantedWidth = x2;
0176     int wantedHeight = y2;
0177 
0178     // create GS command line
0179 
0180     const QString gsExec = QStandardPaths::findExecutable(QStringLiteral("gs"));
0181     if (gsExec.isEmpty()) {
0182         qCWarning(EPSPLUGIN) << "Couldn't find gs exectuable (from GhostScript) in PATH.";
0183         return false;
0184     }
0185 
0186     QStringList gsArgs;
0187     gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
0188            << QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
0189            << QStringLiteral("-c")
0190            << QStringLiteral(
0191                   "0 0 moveto "
0192                   "1000 0 lineto "
0193                   "1000 1000 lineto "
0194                   "0 1000 lineto "
0195                   "1 1 254 255 div setrgbcolor fill "
0196                   "0 0 0 setrgbcolor")
0197            << QStringLiteral("-") << QStringLiteral("-c") << QStringLiteral("showpage quit");
0198     qCDebug(EPSPLUGIN) << "Running gs with args" << gsArgs;
0199 
0200     QProcess converter;
0201     converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
0202     converter.start(gsExec, gsArgs);
0203     if (!converter.waitForStarted(3000)) {
0204         qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
0205         return false;
0206     }
0207 
0208     QByteArray intro = "\n";
0209     intro += QByteArray::number(-qRound(x1 * xScale));
0210     intro += " ";
0211     intro += QByteArray::number(-qRound(y1 * yScale));
0212     intro += " translate\n";
0213     converter.write(intro);
0214 
0215     io->reset();
0216     if (ps_offset > 0) {
0217         io->seek(ps_offset);
0218     }
0219 
0220     QByteArray buffer;
0221     buffer.resize(4096);
0222     bool limited = ps_size > 0;
0223     qint64 remaining = ps_size;
0224     qint64 count = io->read(buffer.data(), buffer.size());
0225     while (count > 0) {
0226         if (limited) {
0227             if (count > remaining) {
0228                 count = remaining;
0229             }
0230             remaining -= count;
0231         }
0232         converter.write(buffer.constData(), count);
0233         if (!limited || remaining > 0) {
0234             count = io->read(buffer.data(), buffer.size());
0235         }
0236     }
0237 
0238     converter.closeWriteChannel();
0239     converter.waitForFinished(-1);
0240 
0241     QImageReader ppmReader(tmpFile.fileName(), "ppm");
0242     if (ppmReader.read(image)) {
0243         qCDebug(EPSPLUGIN) << "success!";
0244 #ifdef EPS_PERFORMANCE_DEBUG
0245         qCDebug(EPSPLUGIN) << "Loading EPS took " << (float)(dt.elapsed()) / 1000 << " seconds";
0246 #endif
0247         return true;
0248     } else {
0249         qCDebug(EPSPLUGIN) << "Reading failed:" << ppmReader.errorString();
0250         return false;
0251     }
0252 }
0253 
0254 bool EPSHandler::write(const QImage &image)
0255 {
0256     QPrinter psOut(QPrinter::PrinterResolution);
0257     QPainter p;
0258 
0259     QTemporaryFile tmpFile(QStringLiteral("XXXXXXXX.pdf"));
0260     if (!tmpFile.open()) {
0261         return false;
0262     }
0263 
0264     psOut.setCreator(QStringLiteral("KDE EPS image plugin"));
0265     psOut.setOutputFileName(tmpFile.fileName());
0266     psOut.setOutputFormat(QPrinter::PdfFormat);
0267     psOut.setFullPage(true);
0268     const double multiplier = psOut.resolution() <= 0 ? 1.0 : 72.0 / psOut.resolution();
0269     psOut.setPageSize(QPageSize(image.size() * multiplier, QPageSize::Point));
0270 
0271     // painting the pixmap to the "printer" which is a file
0272     p.begin(&psOut);
0273     p.drawImage(QPoint(0, 0), image);
0274     p.end();
0275 
0276     QProcess converter;
0277     converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
0278     converter.setReadChannel(QProcess::StandardOutput);
0279 
0280     // pdftops comes with Poppler and produces much smaller EPS files than GhostScript
0281     QStringList pdftopsArgs;
0282     pdftopsArgs << QStringLiteral("-eps") << tmpFile.fileName() << QStringLiteral("-");
0283     qCDebug(EPSPLUGIN) << "Running pdftops with args" << pdftopsArgs;
0284     converter.start(QStringLiteral("pdftops"), pdftopsArgs);
0285 
0286     if (!converter.waitForStarted()) {
0287         // GhostScript produces huge files, and takes a long time doing so
0288         QStringList gsArgs;
0289         gsArgs << QStringLiteral("-q") << QStringLiteral("-P-") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH") << QStringLiteral("-dSAFER")
0290                << QStringLiteral("-sDEVICE=epswrite") << QStringLiteral("-sOutputFile=-") << QStringLiteral("-c") << QStringLiteral("save")
0291                << QStringLiteral("pop") << QStringLiteral("-f") << tmpFile.fileName();
0292         qCDebug(EPSPLUGIN) << "Failed to start pdftops; trying gs with args" << gsArgs;
0293         converter.start(QStringLiteral("gs"), gsArgs);
0294 
0295         if (!converter.waitForStarted(3000)) {
0296             qCWarning(EPSPLUGIN) << "Creating EPS files requires pdftops (from Poppler) or gs (from GhostScript)";
0297             return false;
0298         }
0299     }
0300 
0301     while (converter.bytesAvailable() || (converter.state() == QProcess::Running && converter.waitForReadyRead(2000))) {
0302         device()->write(converter.readAll());
0303     }
0304 
0305     return true;
0306 }
0307 
0308 bool EPSHandler::canRead(QIODevice *device)
0309 {
0310     if (!device) {
0311         qCWarning(EPSPLUGIN) << "EPSHandler::canRead() called with no device";
0312         return false;
0313     }
0314 
0315     qint64 oldPos = device->pos();
0316 
0317     QByteArray head = device->readLine(64);
0318     int readBytes = head.size();
0319     if (device->isSequential()) {
0320         while (readBytes > 0) {
0321             device->ungetChar(head[readBytes-- - 1]);
0322         }
0323     } else {
0324         device->seek(oldPos);
0325     }
0326 
0327     return head.contains("%!PS-Adobe");
0328 }
0329 
0330 QImageIOPlugin::Capabilities EPSPlugin::capabilities(QIODevice *device, const QByteArray &format) const
0331 {
0332     // prevent bug #397040: when on app shutdown the clipboard content is to be copied to survive end of the app,
0333     // QXcbIntegration looks for some QImageIOHandler to apply, querying the capabilities and picking any first.
0334     // At that point this plugin no longer has its requirements e.g. to run the external process, so we have to deny.
0335     // The capabilities seem to be queried on demand in Qt code and not cached, so it's fine to report based
0336     // in current dynamic state
0337     if (!QCoreApplication::instance()) {
0338         return {};
0339     }
0340 
0341     if (format == "eps" || format == "epsi" || format == "epsf") {
0342         return Capabilities(CanRead | CanWrite);
0343     }
0344     if (!format.isEmpty()) {
0345         return {};
0346     }
0347     if (!device->isOpen()) {
0348         return {};
0349     }
0350 
0351     Capabilities cap;
0352     if (device->isReadable() && EPSHandler::canRead(device)) {
0353         cap |= CanRead;
0354     }
0355     if (device->isWritable()) {
0356         cap |= CanWrite;
0357     }
0358     return cap;
0359 }
0360 
0361 QImageIOHandler *EPSPlugin::create(QIODevice *device, const QByteArray &format) const
0362 {
0363     QImageIOHandler *handler = new EPSHandler;
0364     handler->setDevice(device);
0365     handler->setFormat(format);
0366     return handler;
0367 }
0368 
0369 #include "moc_eps_p.cpp"