1 /*
2     QImageIO Routines to read/write EPS images.
3     SPDX-FileCopyrightText: 1998 Dirk Schoenberger <dirk.schoenberger@freenet.de>
4     SPDX-FileCopyrightText: 2013 Alex Merry <alex.merry@kdemail.net>
5 
6     Includes code by Sven Wiegand <SWiegand@tfh-berlin.de> from KSnapshot
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 #include "eps_p.h"
11 
12 #include <QCoreApplication>
13 #include <QImage>
14 #include <QImageReader>
15 #include <QPainter>
16 #include <QPrinter>
17 #include <QProcess>
18 #include <QTemporaryFile>
19 
20 // logging category for this framework, default: log stuff >= warning
21 Q_LOGGING_CATEGORY(EPSPLUGIN, "kf.imageformats.plugins.eps", QtWarningMsg)
22 
23 //#define EPS_PERFORMANCE_DEBUG 1
24 
25 #define BBOX_BUFLEN 200
26 #define BBOX "%%BoundingBox:"
27 #define BBOX_LEN strlen(BBOX)
28 
seekToCodeStart(QIODevice * io,qint64 & ps_offset,qint64 & ps_size)29 static bool seekToCodeStart(QIODevice *io, qint64 &ps_offset, qint64 &ps_size)
30 {
31     char buf[4]; // We at most need to read 4 bytes at a time
32     ps_offset = 0L;
33     ps_size = 0L;
34 
35     if (io->read(buf, 2) != 2) { // Read first two bytes
36         qCDebug(EPSPLUGIN) << "EPS file has less than 2 bytes.";
37         return false;
38     }
39 
40     if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
41         qCDebug(EPSPLUGIN) << "normal EPS file";
42     } else if (buf[0] == char(0xc5) && buf[1] == char(0xd0)) { // Check start of MS-DOS EPS magic
43         // May be a MS-DOS EPS file
44         if (io->read(buf + 2, 2) != 2) { // Read further bytes of MS-DOS EPS magic
45             qCDebug(EPSPLUGIN) << "potential MS-DOS EPS file has less than 4 bytes.";
46             return false;
47         }
48         if (buf[2] == char(0xd3) && buf[3] == char(0xc6)) { // Check last bytes of MS-DOS EPS magic
49             if (io->read(buf, 4) != 4) { // Get offset of PostScript code in the MS-DOS EPS file.
50                 qCDebug(EPSPLUGIN) << "cannot read offset of MS-DOS EPS file";
51                 return false;
52             }
53             ps_offset // Offset is in little endian
54                 = qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
55             if (io->read(buf, 4) != 4) { // Get size of PostScript code in the MS-DOS EPS file.
56                 qCDebug(EPSPLUGIN) << "cannot read size of MS-DOS EPS file";
57                 return false;
58             }
59             ps_size // Size is in little endian
60                 = qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
61             qCDebug(EPSPLUGIN) << "Offset: " << ps_offset << " Size: " << ps_size;
62             if (!io->seek(ps_offset)) { // Get offset of PostScript code in the MS-DOS EPS file.
63                 qCDebug(EPSPLUGIN) << "cannot seek in MS-DOS EPS file";
64                 return false;
65             }
66             if (io->read(buf, 2) != 2) { // Read first two bytes of what should be the Postscript code
67                 qCDebug(EPSPLUGIN) << "PostScript code has less than 2 bytes.";
68                 return false;
69             }
70             if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
71                 qCDebug(EPSPLUGIN) << "MS-DOS EPS file";
72             } else {
73                 qCDebug(EPSPLUGIN) << "supposed Postscript code of a MS-DOS EPS file doe not start with %!.";
74                 return false;
75             }
76         } else {
77             qCDebug(EPSPLUGIN) << "wrong magic for potential MS-DOS EPS file!";
78             return false;
79         }
80     } else {
81         qCDebug(EPSPLUGIN) << "not an EPS file!";
82         return false;
83     }
84     return true;
85 }
86 
bbox(QIODevice * io,int * x1,int * y1,int * x2,int * y2)87 static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
88 {
89     char buf[BBOX_BUFLEN + 1];
90 
91     bool ret = false;
92 
93     while (io->readLine(buf, BBOX_BUFLEN) > 0) {
94         if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
95             // Some EPS files have non-integer values for the bbox
96             // We don't support that currently, but at least we parse it
97             float _x1;
98             float _y1;
99             float _x2;
100             float _y2;
101             if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
102                 qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
103                 *x1 = int(_x1);
104                 *y1 = int(_y1);
105                 *x2 = int(_x2);
106                 *y2 = int(_y2);
107                 ret = true;
108                 break;
109             }
110         }
111     }
112 
113     return ret;
114 }
115 
EPSHandler()116 EPSHandler::EPSHandler()
117 {
118 }
119 
canRead() const120 bool EPSHandler::canRead() const
121 {
122     if (canRead(device())) {
123         setFormat("eps");
124         return true;
125     }
126     return false;
127 }
128 
read(QImage * image)129 bool EPSHandler::read(QImage *image)
130 {
131     qCDebug(EPSPLUGIN) << "starting...";
132 
133     int x1;
134     int y1;
135     int x2;
136     int y2;
137 #ifdef EPS_PERFORMANCE_DEBUG
138     QTime dt;
139     dt.start();
140 #endif
141 
142     QIODevice *io = device();
143     qint64 ps_offset;
144     qint64 ps_size;
145 
146     // find start of PostScript code
147     if (!seekToCodeStart(io, ps_offset, ps_size)) {
148         return false;
149     }
150 
151     qCDebug(EPSPLUGIN) << "Offset:" << ps_offset << "; size:" << ps_size;
152 
153     // find bounding box
154     if (!bbox(io, &x1, &y1, &x2, &y2)) {
155         qCDebug(EPSPLUGIN) << "no bounding box found!";
156         return false;
157     }
158 
159     QTemporaryFile tmpFile;
160     if (!tmpFile.open()) {
161         qCWarning(EPSPLUGIN) << "Could not create the temporary file" << tmpFile.fileName();
162         return false;
163     }
164     qCDebug(EPSPLUGIN) << "temporary file:" << tmpFile.fileName();
165 
166     // x1, y1 -> translation
167     // x2, y2 -> new size
168 
169     x2 -= x1;
170     y2 -= y1;
171     qCDebug(EPSPLUGIN) << "origin point: " << x1 << "," << y1 << "  size:" << x2 << "," << y2;
172     double xScale = 1.0;
173     double yScale = 1.0;
174     int wantedWidth = x2;
175     int wantedHeight = y2;
176 
177     // create GS command line
178 
179     QStringList gsArgs;
180     gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
181            << QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
182            << QStringLiteral("-c")
183            << QStringLiteral(
184                   "0 0 moveto "
185                   "1000 0 lineto "
186                   "1000 1000 lineto "
187                   "0 1000 lineto "
188                   "1 1 254 255 div setrgbcolor fill "
189                   "0 0 0 setrgbcolor")
190            << QStringLiteral("-") << QStringLiteral("-c") << QStringLiteral("showpage quit");
191     qCDebug(EPSPLUGIN) << "Running gs with args" << gsArgs;
192 
193     QProcess converter;
194     converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
195     converter.start(QStringLiteral("gs"), gsArgs);
196     if (!converter.waitForStarted(3000)) {
197         qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
198         return false;
199     }
200 
201     QByteArray intro = "\n";
202     intro += QByteArray::number(-qRound(x1 * xScale));
203     intro += " ";
204     intro += QByteArray::number(-qRound(y1 * yScale));
205     intro += " translate\n";
206     converter.write(intro);
207 
208     io->reset();
209     if (ps_offset > 0) {
210         io->seek(ps_offset);
211     }
212 
213     QByteArray buffer;
214     buffer.resize(4096);
215     bool limited = ps_size > 0;
216     qint64 remaining = ps_size;
217     qint64 count = io->read(buffer.data(), buffer.size());
218     while (count > 0) {
219         if (limited) {
220             if (count > remaining) {
221                 count = remaining;
222             }
223             remaining -= count;
224         }
225         converter.write(buffer.constData(), count);
226         if (!limited || remaining > 0) {
227             count = io->read(buffer.data(), buffer.size());
228         }
229     }
230 
231     converter.closeWriteChannel();
232     converter.waitForFinished(-1);
233 
234     QImageReader ppmReader(tmpFile.fileName(), "ppm");
235     if (ppmReader.read(image)) {
236         qCDebug(EPSPLUGIN) << "success!";
237 #ifdef EPS_PERFORMANCE_DEBUG
238         qCDebug(EPSPLUGIN) << "Loading EPS took " << (float)(dt.elapsed()) / 1000 << " seconds";
239 #endif
240         return true;
241     } else {
242         qCDebug(EPSPLUGIN) << "Reading failed:" << ppmReader.errorString();
243         return false;
244     }
245 }
246 
write(const QImage & image)247 bool EPSHandler::write(const QImage &image)
248 {
249     QPrinter psOut(QPrinter::PrinterResolution);
250     QPainter p;
251 
252     QTemporaryFile tmpFile(QStringLiteral("XXXXXXXX.pdf"));
253     if (!tmpFile.open()) {
254         return false;
255     }
256 
257     psOut.setCreator(QStringLiteral("KDE EPS image plugin"));
258     psOut.setOutputFileName(tmpFile.fileName());
259     psOut.setOutputFormat(QPrinter::PdfFormat);
260     psOut.setFullPage(true);
261     const double multiplier = psOut.resolution() <= 0 ? 1.0 : 72.0 / psOut.resolution();
262     psOut.setPageSize(QPageSize(image.size() * multiplier, QPageSize::Point));
263 
264     // painting the pixmap to the "printer" which is a file
265     p.begin(&psOut);
266     p.drawImage(QPoint(0, 0), image);
267     p.end();
268 
269     QProcess converter;
270     converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
271     converter.setReadChannel(QProcess::StandardOutput);
272 
273     // pdftops comes with Poppler and produces much smaller EPS files than GhostScript
274     QStringList pdftopsArgs;
275     pdftopsArgs << QStringLiteral("-eps") << tmpFile.fileName() << QStringLiteral("-");
276     qCDebug(EPSPLUGIN) << "Running pdftops with args" << pdftopsArgs;
277     converter.start(QStringLiteral("pdftops"), pdftopsArgs);
278 
279     if (!converter.waitForStarted()) {
280         // GhostScript produces huge files, and takes a long time doing so
281         QStringList gsArgs;
282         gsArgs << QStringLiteral("-q") << QStringLiteral("-P-") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH") << QStringLiteral("-dSAFER")
283                << QStringLiteral("-sDEVICE=epswrite") << QStringLiteral("-sOutputFile=-") << QStringLiteral("-c") << QStringLiteral("save")
284                << QStringLiteral("pop") << QStringLiteral("-f") << tmpFile.fileName();
285         qCDebug(EPSPLUGIN) << "Failed to start pdftops; trying gs with args" << gsArgs;
286         converter.start(QStringLiteral("gs"), gsArgs);
287 
288         if (!converter.waitForStarted(3000)) {
289             qCWarning(EPSPLUGIN) << "Creating EPS files requires pdftops (from Poppler) or gs (from GhostScript)";
290             return false;
291         }
292     }
293 
294     while (converter.bytesAvailable() || (converter.state() == QProcess::Running && converter.waitForReadyRead(2000))) {
295         device()->write(converter.readAll());
296     }
297 
298     return true;
299 }
300 
canRead(QIODevice * device)301 bool EPSHandler::canRead(QIODevice *device)
302 {
303     if (!device) {
304         qCWarning(EPSPLUGIN) << "EPSHandler::canRead() called with no device";
305         return false;
306     }
307 
308     qint64 oldPos = device->pos();
309 
310     QByteArray head = device->readLine(64);
311     int readBytes = head.size();
312     if (device->isSequential()) {
313         while (readBytes > 0) {
314             device->ungetChar(head[readBytes-- - 1]);
315         }
316     } else {
317         device->seek(oldPos);
318     }
319 
320     return head.contains("%!PS-Adobe");
321 }
322 
capabilities(QIODevice * device,const QByteArray & format) const323 QImageIOPlugin::Capabilities EPSPlugin::capabilities(QIODevice *device, const QByteArray &format) const
324 {
325     // prevent bug #397040: when on app shutdown the clipboard content is to be copied to survive end of the app,
326     // QXcbIntegration looks for some QImageIOHandler to apply, querying the capabilities and picking any first.
327     // At that point this plugin no longer has its requirements e.g. to run the external process, so we have to deny.
328     // The capabilities seem to be queried on demand in Qt code and not cached, so it's fine to report based
329     // in current dynamic state
330     if (!QCoreApplication::instance()) {
331         return {};
332     }
333 
334     if (format == "eps" || format == "epsi" || format == "epsf") {
335         return Capabilities(CanRead | CanWrite);
336     }
337     if (!format.isEmpty()) {
338         return {};
339     }
340     if (!device->isOpen()) {
341         return {};
342     }
343 
344     Capabilities cap;
345     if (device->isReadable() && EPSHandler::canRead(device)) {
346         cap |= CanRead;
347     }
348     if (device->isWritable()) {
349         cap |= CanWrite;
350     }
351     return cap;
352 }
353 
create(QIODevice * device,const QByteArray & format) const354 QImageIOHandler *EPSPlugin::create(QIODevice *device, const QByteArray &format) const
355 {
356     QImageIOHandler *handler = new EPSHandler;
357     handler->setDevice(device);
358     handler->setFormat(format);
359     return handler;
360 }
361