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

source code of kimageformats/src/imageformats/eps.cpp