Friedrich W. H. Kossebau 34ed3bad27 Add explicit moc includes to sources for moc-covered headers
* speeds up incremental builds as changes to a header will not always
  need the full mocs_compilation.cpp for all the target's headers rebuild,
  while having a moc file sourced into a source file only adds minor
  extra costs, due to small own code and the used headers usually
  already covered by the source file, being for the same class/struct
* seems to not slow down clean builds, due to empty mocs_compilation.cpp
  resulting in those quickly processed, while the minor extra cost of the
  sourced moc files does not outweigh that in summary.
  Measured times actually improved by some percent points.
  (ideally CMake would just skip empty mocs_compilation.cpp & its object
  file one day)
* enables compiler to see all methods of a class in same compilation unit
  to do some sanity checks
* potentially more inlining in general, due to more in the compilation unit
* allows to keep using more forward declarations in the header, as with the
  moc code being sourced into the cpp file there definitions can be ensured
  and often are already for the needs of the normal class methods
2023-07-02 03:08:44 +02:00

370 lines
12 KiB
C++

/*
QImageIO Routines to read/write EPS images.
SPDX-FileCopyrightText: 1998 Dirk Schoenberger <dirk.schoenberger@freenet.de>
SPDX-FileCopyrightText: 2013 Alex Merry <alex.merry@kdemail.net>
Includes code by Sven Wiegand <SWiegand@tfh-berlin.de> from KSnapshot
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "eps_p.h"
#include <QCoreApplication>
#include <QImage>
#include <QImageReader>
#include <QPainter>
#include <QPrinter>
#include <QProcess>
#include <QStandardPaths>
#include <QTemporaryFile>
// logging category for this framework, default: log stuff >= warning
Q_LOGGING_CATEGORY(EPSPLUGIN, "kf.imageformats.plugins.eps", QtWarningMsg)
//#define EPS_PERFORMANCE_DEBUG 1
#define BBOX_BUFLEN 200
#define BBOX "%%BoundingBox:"
#define BBOX_LEN strlen(BBOX)
static bool seekToCodeStart(QIODevice *io, qint64 &ps_offset, qint64 &ps_size)
{
char buf[4]; // We at most need to read 4 bytes at a time
ps_offset = 0L;
ps_size = 0L;
if (io->read(buf, 2) != 2) { // Read first two bytes
qCDebug(EPSPLUGIN) << "EPS file has less than 2 bytes.";
return false;
}
if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
qCDebug(EPSPLUGIN) << "normal EPS file";
} else if (buf[0] == char(0xc5) && buf[1] == char(0xd0)) { // Check start of MS-DOS EPS magic
// May be a MS-DOS EPS file
if (io->read(buf + 2, 2) != 2) { // Read further bytes of MS-DOS EPS magic
qCDebug(EPSPLUGIN) << "potential MS-DOS EPS file has less than 4 bytes.";
return false;
}
if (buf[2] == char(0xd3) && buf[3] == char(0xc6)) { // Check last bytes of MS-DOS EPS magic
if (io->read(buf, 4) != 4) { // Get offset of PostScript code in the MS-DOS EPS file.
qCDebug(EPSPLUGIN) << "cannot read offset of MS-DOS EPS file";
return false;
}
ps_offset // Offset is in little endian
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
if (io->read(buf, 4) != 4) { // Get size of PostScript code in the MS-DOS EPS file.
qCDebug(EPSPLUGIN) << "cannot read size of MS-DOS EPS file";
return false;
}
ps_size // Size is in little endian
= qint64(((unsigned char)buf[0]) + ((unsigned char)buf[1] << 8) + ((unsigned char)buf[2] << 16) + ((unsigned char)buf[3] << 24));
qCDebug(EPSPLUGIN) << "Offset: " << ps_offset << " Size: " << ps_size;
if (!io->seek(ps_offset)) { // Get offset of PostScript code in the MS-DOS EPS file.
qCDebug(EPSPLUGIN) << "cannot seek in MS-DOS EPS file";
return false;
}
if (io->read(buf, 2) != 2) { // Read first two bytes of what should be the Postscript code
qCDebug(EPSPLUGIN) << "PostScript code has less than 2 bytes.";
return false;
}
if (buf[0] == '%' && buf[1] == '!') { // Check %! magic
qCDebug(EPSPLUGIN) << "MS-DOS EPS file";
} else {
qCDebug(EPSPLUGIN) << "supposed Postscript code of a MS-DOS EPS file doe not start with %!.";
return false;
}
} else {
qCDebug(EPSPLUGIN) << "wrong magic for potential MS-DOS EPS file!";
return false;
}
} else {
qCDebug(EPSPLUGIN) << "not an EPS file!";
return false;
}
return true;
}
static bool bbox(QIODevice *io, int *x1, int *y1, int *x2, int *y2)
{
char buf[BBOX_BUFLEN + 1];
bool ret = false;
while (io->readLine(buf, BBOX_BUFLEN) > 0) {
if (strncmp(buf, BBOX, BBOX_LEN) == 0) {
// Some EPS files have non-integer values for the bbox
// We don't support that currently, but at least we parse it
float _x1;
float _y1;
float _x2;
float _y2;
if (sscanf(buf, "%*s %f %f %f %f", &_x1, &_y1, &_x2, &_y2) == 4) {
qCDebug(EPSPLUGIN) << "BBOX: " << _x1 << " " << _y1 << " " << _x2 << " " << _y2;
*x1 = int(_x1);
*y1 = int(_y1);
*x2 = int(_x2);
*y2 = int(_y2);
ret = true;
break;
}
}
}
return ret;
}
EPSHandler::EPSHandler()
{
}
bool EPSHandler::canRead() const
{
if (canRead(device())) {
setFormat("eps");
return true;
}
return false;
}
bool EPSHandler::read(QImage *image)
{
qCDebug(EPSPLUGIN) << "starting...";
int x1;
int y1;
int x2;
int y2;
#ifdef EPS_PERFORMANCE_DEBUG
QTime dt;
dt.start();
#endif
QIODevice *io = device();
qint64 ps_offset;
qint64 ps_size;
// find start of PostScript code
if (!seekToCodeStart(io, ps_offset, ps_size)) {
return false;
}
qCDebug(EPSPLUGIN) << "Offset:" << ps_offset << "; size:" << ps_size;
// find bounding box
if (!bbox(io, &x1, &y1, &x2, &y2)) {
qCDebug(EPSPLUGIN) << "no bounding box found!";
return false;
}
QTemporaryFile tmpFile;
if (!tmpFile.open()) {
qCWarning(EPSPLUGIN) << "Could not create the temporary file" << tmpFile.fileName();
return false;
}
qCDebug(EPSPLUGIN) << "temporary file:" << tmpFile.fileName();
// x1, y1 -> translation
// x2, y2 -> new size
x2 -= x1;
y2 -= y1;
qCDebug(EPSPLUGIN) << "origin point: " << x1 << "," << y1 << " size:" << x2 << "," << y2;
double xScale = 1.0;
double yScale = 1.0;
int wantedWidth = x2;
int wantedHeight = y2;
// create GS command line
const QString gsExec = QStandardPaths::findExecutable(QStringLiteral("gs"));
if (gsExec.isEmpty()) {
qCWarning(EPSPLUGIN) << "Couldn't find gs exectuable (from GhostScript) in PATH.";
return false;
}
QStringList gsArgs;
gsArgs << QLatin1String("-sOutputFile=") + tmpFile.fileName() << QStringLiteral("-q") << QStringLiteral("-g%1x%2").arg(wantedWidth).arg(wantedHeight)
<< QStringLiteral("-dSAFER") << QStringLiteral("-dPARANOIDSAFER") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-sDEVICE=ppm")
<< QStringLiteral("-c")
<< QStringLiteral(
"0 0 moveto "
"1000 0 lineto "
"1000 1000 lineto "
"0 1000 lineto "
"1 1 254 255 div setrgbcolor fill "
"0 0 0 setrgbcolor")
<< QStringLiteral("-") << QStringLiteral("-c") << QStringLiteral("showpage quit");
qCDebug(EPSPLUGIN) << "Running gs with args" << gsArgs;
QProcess converter;
converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
converter.start(gsExec, gsArgs);
if (!converter.waitForStarted(3000)) {
qCWarning(EPSPLUGIN) << "Reading EPS files requires gs (from GhostScript)";
return false;
}
QByteArray intro = "\n";
intro += QByteArray::number(-qRound(x1 * xScale));
intro += " ";
intro += QByteArray::number(-qRound(y1 * yScale));
intro += " translate\n";
converter.write(intro);
io->reset();
if (ps_offset > 0) {
io->seek(ps_offset);
}
QByteArray buffer;
buffer.resize(4096);
bool limited = ps_size > 0;
qint64 remaining = ps_size;
qint64 count = io->read(buffer.data(), buffer.size());
while (count > 0) {
if (limited) {
if (count > remaining) {
count = remaining;
}
remaining -= count;
}
converter.write(buffer.constData(), count);
if (!limited || remaining > 0) {
count = io->read(buffer.data(), buffer.size());
}
}
converter.closeWriteChannel();
converter.waitForFinished(-1);
QImageReader ppmReader(tmpFile.fileName(), "ppm");
if (ppmReader.read(image)) {
qCDebug(EPSPLUGIN) << "success!";
#ifdef EPS_PERFORMANCE_DEBUG
qCDebug(EPSPLUGIN) << "Loading EPS took " << (float)(dt.elapsed()) / 1000 << " seconds";
#endif
return true;
} else {
qCDebug(EPSPLUGIN) << "Reading failed:" << ppmReader.errorString();
return false;
}
}
bool EPSHandler::write(const QImage &image)
{
QPrinter psOut(QPrinter::PrinterResolution);
QPainter p;
QTemporaryFile tmpFile(QStringLiteral("XXXXXXXX.pdf"));
if (!tmpFile.open()) {
return false;
}
psOut.setCreator(QStringLiteral("KDE EPS image plugin"));
psOut.setOutputFileName(tmpFile.fileName());
psOut.setOutputFormat(QPrinter::PdfFormat);
psOut.setFullPage(true);
const double multiplier = psOut.resolution() <= 0 ? 1.0 : 72.0 / psOut.resolution();
psOut.setPageSize(QPageSize(image.size() * multiplier, QPageSize::Point));
// painting the pixmap to the "printer" which is a file
p.begin(&psOut);
p.drawImage(QPoint(0, 0), image);
p.end();
QProcess converter;
converter.setProcessChannelMode(QProcess::ForwardedErrorChannel);
converter.setReadChannel(QProcess::StandardOutput);
// pdftops comes with Poppler and produces much smaller EPS files than GhostScript
QStringList pdftopsArgs;
pdftopsArgs << QStringLiteral("-eps") << tmpFile.fileName() << QStringLiteral("-");
qCDebug(EPSPLUGIN) << "Running pdftops with args" << pdftopsArgs;
converter.start(QStringLiteral("pdftops"), pdftopsArgs);
if (!converter.waitForStarted()) {
// GhostScript produces huge files, and takes a long time doing so
QStringList gsArgs;
gsArgs << QStringLiteral("-q") << QStringLiteral("-P-") << QStringLiteral("-dNOPAUSE") << QStringLiteral("-dBATCH") << QStringLiteral("-dSAFER")
<< QStringLiteral("-sDEVICE=epswrite") << QStringLiteral("-sOutputFile=-") << QStringLiteral("-c") << QStringLiteral("save")
<< QStringLiteral("pop") << QStringLiteral("-f") << tmpFile.fileName();
qCDebug(EPSPLUGIN) << "Failed to start pdftops; trying gs with args" << gsArgs;
converter.start(QStringLiteral("gs"), gsArgs);
if (!converter.waitForStarted(3000)) {
qCWarning(EPSPLUGIN) << "Creating EPS files requires pdftops (from Poppler) or gs (from GhostScript)";
return false;
}
}
while (converter.bytesAvailable() || (converter.state() == QProcess::Running && converter.waitForReadyRead(2000))) {
device()->write(converter.readAll());
}
return true;
}
bool EPSHandler::canRead(QIODevice *device)
{
if (!device) {
qCWarning(EPSPLUGIN) << "EPSHandler::canRead() called with no device";
return false;
}
qint64 oldPos = device->pos();
QByteArray head = device->readLine(64);
int readBytes = head.size();
if (device->isSequential()) {
while (readBytes > 0) {
device->ungetChar(head[readBytes-- - 1]);
}
} else {
device->seek(oldPos);
}
return head.contains("%!PS-Adobe");
}
QImageIOPlugin::Capabilities EPSPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
// prevent bug #397040: when on app shutdown the clipboard content is to be copied to survive end of the app,
// QXcbIntegration looks for some QImageIOHandler to apply, querying the capabilities and picking any first.
// At that point this plugin no longer has its requirements e.g. to run the external process, so we have to deny.
// The capabilities seem to be queried on demand in Qt code and not cached, so it's fine to report based
// in current dynamic state
if (!QCoreApplication::instance()) {
return {};
}
if (format == "eps" || format == "epsi" || format == "epsf") {
return Capabilities(CanRead | CanWrite);
}
if (!format.isEmpty()) {
return {};
}
if (!device->isOpen()) {
return {};
}
Capabilities cap;
if (device->isReadable() && EPSHandler::canRead(device)) {
cap |= CanRead;
}
if (device->isWritable()) {
cap |= CanWrite;
}
return cap;
}
QImageIOHandler *EPSPlugin::create(QIODevice *device, const QByteArray &format) const
{
QImageIOHandler *handler = new EPSHandler;
handler->setDevice(device);
handler->setFormat(format);
return handler;
}
#include "moc_eps_p.cpp"