File indexing completed on 2024-05-12 07:49:36
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"