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