KImageFormats

eps.cpp
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(buf, 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(buf + 2, 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(buf, 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(buf, 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(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(buf, 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(buf, BBOX_BUFLEN) > 0) {
95 if (strncmp(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(buf, "%*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())) {
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, &y1, &x2, &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(wantedWidth).arg(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;
202 converter.start(gsExec, gsArgs);
203 if (!converter.waitForStarted(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(x1 * xScale));
210 intro += " ";
211 intro += QByteArray::number(-qRound(y1 * yScale));
212 intro += " translate\n";
213 converter.write(intro);
214
215 io->reset();
216 if (ps_offset > 0) {
217 io->seek(ps_offset);
218 }
219
220 QByteArray buffer;
221 buffer.resize(4096);
222 bool limited = ps_size > 0;
223 qint64 remaining = ps_size;
224 qint64 count = io->read(buffer.data(), buffer.size());
225 while (count > 0) {
226 if (limited) {
227 if (count > remaining) {
228 count = remaining;
229 }
230 remaining -= count;
231 }
232 converter.write(buffer.constData(), count);
233 if (!limited || remaining > 0) {
234 count = io->read(buffer.data(), buffer.size());
235 }
236 }
237
238 converter.closeWriteChannel();
239 converter.waitForFinished(-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{
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(QPoint(0, 0), image);
274 p.end();
275
276 QProcess converter;
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"), 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"), gsArgs);
294
295 if (!converter.waitForStarted(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(2000))) {
302 device()->write(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(64);
318 int readBytes = head.size();
319 if (device->isSequential()) {
320 while (readBytes > 0) {
321 device->ungetChar(head[readBytes-- - 1]);
322 }
323 } else {
324 device->seek(oldPos);
325 }
326
327 return head.contains("%!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
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"
QFlags< Capability > Capabilities
const char * constData() const const
bool contains(QByteArrayView bv) const const
char * data()
bool isEmpty() const const
QByteArray number(double n, char format, int precision)
void resize(qsizetype newSize, char c)
qsizetype size() const const
QCoreApplication * instance()
QSize size() const const
void setDevice(QIODevice *device)
void setFormat(const QByteArray &format)
virtual qint64 bytesAvailable() const const
bool isOpen() const const
bool isReadable() const const
virtual bool isSequential() const const
bool isWritable() const const
virtual qint64 pos() const const
QByteArray read(qint64 maxSize)
QByteArray readAll()
QByteArray readLine(qint64 maxSize)
virtual bool reset()
virtual bool seek(qint64 pos)
void ungetChar(char c)
qint64 write(const QByteArray &data)
bool begin(QPaintDevice *device)
void drawImage(const QPoint &point, const QImage &image)
bool end()
ForwardedErrorChannel
void closeWriteChannel()
void setProcessChannelMode(ProcessChannelMode mode)
void setReadChannel(ProcessChannel channel)
void start(OpenMode mode)
QProcess::ProcessState state() const const
bool waitForFinished(int msecs)
virtual bool waitForReadyRead(int msecs) override
bool waitForStarted(int msecs)
QString findExecutable(const QString &executableName, const QStringList &paths)
QString arg(Args &&... args) const const
bool isEmpty() const const
virtual QString fileName() const const override
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:10:55 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.