KImageFormats

eps.cpp
1 /*
2  QImageIO Routines to read/write EPS images.
3  SPDX-FileCopyrightText: 1998 Dirk Schoenberger <[email protected]>
4  SPDX-FileCopyrightText: 2013 Alex Merry <[email protected]>
5 
6  Includes code by Sven Wiegand <[email protected]> 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 
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 
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 
116 EPSHandler::EPSHandler()
117 {
118 }
119 
120 bool EPSHandler::canRead() const
121 {
122  if (canRead(device())) {
123  setFormat("eps");
124  return true;
125  }
126  return false;
127 }
128 
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;
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 
247 bool EPSHandler::write(const QImage &image)
248 {
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;
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 
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 
323 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
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 
354 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 }
bool isWritable() const const
bool end()
virtual bool seek(qint64 pos)
virtual bool waitForReadyRead(int msecs) override
bool isEmpty() const const
QString errorString() const const
bool isReadable() const const
virtual bool isSequential() const const
virtual qint64 pos() const const
void resize(int size)
int elapsed() const const
const char * constData() const const
QByteArray number(int n, int base)
QByteArray readAll()
void setDevice(QIODevice *device)
qint64 read(char *data, qint64 maxSize)
bool isOpen() const const
QCoreApplication * instance()
virtual bool reset()
bool waitForStarted(int msecs)
virtual QString fileName() const const override
void setProcessChannelMode(QProcess::ProcessChannelMode mode)
void drawImage(const QRectF &target, const QImage &image, const QRectF &source, Qt::ImageConversionFlags flags)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
bool contains(char ch) const const
QSize size() const const
char * data()
qint64 write(const char *data, qint64 maxSize)
typedef Capabilities
void start()
void setFormat(const QByteArray &format)
void setReadChannel(QProcess::ProcessChannel channel)
int size() const const
void closeWriteChannel()
bool begin(QPaintDevice *device)
void ungetChar(char c)
virtual qint64 bytesAvailable() const const override
ForwardedErrorChannel
void start(const QString &program, const QStringList &arguments, QIODevice::OpenMode mode)
QProcess::ProcessState state() const const
qint64 readLine(char *data, qint64 maxSize)
bool waitForFinished(int msecs)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Thu Dec 2 2021 22:45:39 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.