diff --git a/README.md b/README.md index ff5afe2..fc555df 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The following image formats have read-only support: - Krita (kra) - OpenRaster (ora) - Pixar raster (pxr) +- Portable FloatMap (pfm) - Photoshop documents (psd, psb, pdd, psdt) - Radiance HDR (hdr) - Sun Raster (im1, im8, im24, im32, ras, sun) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 4367eac..b7cd46d 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -66,6 +66,7 @@ endmacro() kimageformats_read_tests( hdr pcx + pfm psd pxr qoi diff --git a/autotests/read/pfm/testcard_gray.pfm b/autotests/read/pfm/testcard_gray.pfm new file mode 100644 index 0000000..5ad2869 Binary files /dev/null and b/autotests/read/pfm/testcard_gray.pfm differ diff --git a/autotests/read/pfm/testcard_gray.png b/autotests/read/pfm/testcard_gray.png new file mode 100644 index 0000000..3da44c6 Binary files /dev/null and b/autotests/read/pfm/testcard_gray.png differ diff --git a/autotests/read/pfm/testcard_rgb.pfm b/autotests/read/pfm/testcard_rgb.pfm new file mode 100644 index 0000000..07d7d69 Binary files /dev/null and b/autotests/read/pfm/testcard_rgb.pfm differ diff --git a/autotests/read/pfm/testcard_rgb.png b/autotests/read/pfm/testcard_rgb.png new file mode 100644 index 0000000..d3ffd94 Binary files /dev/null and b/autotests/read/pfm/testcard_rgb.png differ diff --git a/autotests/read/pfm/testcard_rgb_ps.pfm b/autotests/read/pfm/testcard_rgb_ps.pfm new file mode 100644 index 0000000..d1e3925 Binary files /dev/null and b/autotests/read/pfm/testcard_rgb_ps.pfm differ diff --git a/autotests/read/pfm/testcard_rgb_ps.png b/autotests/read/pfm/testcard_rgb_ps.png new file mode 100644 index 0000000..5c047ad Binary files /dev/null and b/autotests/read/pfm/testcard_rgb_ps.png differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 55af814..c42bdae 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -83,6 +83,10 @@ kimageformats_add_plugin(kimg_pic SOURCES pic.cpp) ################################## +kimageformats_add_plugin(kimg_pfm SOURCES pfm.cpp) + +################################## + kimageformats_add_plugin(kimg_psd SOURCES psd.cpp scanlineconverter.cpp) ################################## diff --git a/src/imageformats/pfm.cpp b/src/imageformats/pfm.cpp new file mode 100644 index 0000000..3fd764f --- /dev/null +++ b/src/imageformats/pfm.cpp @@ -0,0 +1,324 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +/* + * See also: https://www.pauldebevec.com/Research/HDR/PFM/ + */ + +#include "pfm_p.h" +#include "util_p.h" + +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(LOG_PFMPLUGIN) +Q_LOGGING_CATEGORY(LOG_PFMPLUGIN, "kf.imageformats.plugins.pfm", QtWarningMsg) + +class PfmHeader +{ +private: + /*! + * \brief m_bw True if grayscale. + */ + bool m_bw; + + /*! + * \brief m_ps True if saved by Photoshop (Photoshop variant). + * + * When \a false the format of the header is the following (GIMP): + * [type] + * [xres] [yres] + * [byte_order] + * + * When \a true the format of the header is the following (Photoshop): + * [type] + * [xres] + * [yres] + * [byte_order] + */ + bool m_ps; + + /*! + * \brief m_width The image width. + */ + qint32 m_width; + + /*! + * \brief m_height The image height. + */ + qint32 m_height; + + /*! + * \brief m_byteOrder The image byte orger. + */ + QDataStream::ByteOrder m_byteOrder; + +public: + PfmHeader() : + m_bw(false), + m_ps(false), + m_width(0), + m_height(0), + m_byteOrder(QDataStream::BigEndian) + { + + } + + bool isValid() const + { + return (m_width > 0 && m_height > 0); + } + + bool isBlackAndWhite() const + { + return m_bw; + } + + bool isPhotoshop() const + { + return m_ps; + } + + qint32 width() const + { + return m_width; + } + + qint32 height() const + { + return m_height; + } + + QSize size() const + { + return QSize(m_width, m_height); + } + + QDataStream::ByteOrder byteOrder() const + { + return m_byteOrder; + } + + QImage::Format format() const + { + if (isValid()) { + return isBlackAndWhite() ? QImage::Format_Grayscale16 : QImage::Format_RGBX32FPx4; + } + return QImage::Format_Invalid; + } + + bool read(QIODevice *d) + { + auto pf = d->read(3); + if (pf == QByteArray("PF\n")) { + m_bw = false; + } else if (pf == QByteArray("Pf\n")) { + m_bw = true; + } else { + return false; + } + auto wh = QString::fromLatin1(d->readLine(128)); + auto list = wh.split(QStringLiteral(" ")); + if (list.size() == 1) { + m_ps = true; // try for Photoshop version + list << QString::fromLatin1(d->readLine(128)); + } + if (list.size() != 2) { + return false; + } + auto ok_o = false; + auto ok_w = false; + auto ok_h = false; + auto o = QString::fromLatin1(d->readLine(128)).toDouble(&ok_o); + auto w = list.first().toInt(&ok_w); + auto h = list.last().toInt(&ok_h); + if (!ok_o || !ok_w || !ok_h || o == 0) { + return false; + } + m_width = w; + m_height = h; + m_byteOrder = o > 0 ? QDataStream::BigEndian : QDataStream::LittleEndian; + return isValid(); + } + + bool peek(QIODevice *d) + { + d->startTransaction(); + auto ok = read(d); + d->rollbackTransaction(); + return ok; + } +} ; + +PFMHandler::PFMHandler() +{ +} + +bool PFMHandler::canRead() const +{ + if (canRead(device())) { + setFormat("pfm"); + return true; + } + return false; +} + +bool PFMHandler::canRead(QIODevice *device) +{ + if (!device) { + qCWarning(LOG_PFMPLUGIN) << "PFMHandler::canRead() called with no device"; + return false; + } + + PfmHeader h; + if (!h.peek(device)) { + return false; + } + + return h.isValid(); +} + +bool PFMHandler::read(QImage *image) +{ + PfmHeader header; + + if (!header.read(device())) { + qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() invalid header"; + return false; + } + + QDataStream s(device()); + s.setFloatingPointPrecision(QDataStream::SinglePrecision); + s.setByteOrder(header.byteOrder()); + + auto img = imageAlloc(header.size(), header.format()); + if (img.isNull()) { + qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() error while allocating the image"; + return false; + } + + for (auto y = 0, h = img.height(); y < h; ++y) { + float f; + if (header.isBlackAndWhite()) { + auto line = reinterpret_cast(img.scanLine(header.isPhotoshop() ? y : h - y - 1)); + for (auto x = 0, w = img.width(); x < w; ++x) { + s >> f; + // QColorSpace does not handle gray linear profile, so I have to convert to non-linear + f = f < 0.0031308f ? (f * 12.92f) : (1.055 * std::pow(f, 1.0 / 2.4) - 0.055); + line[x] = quint16(std::clamp(f, float(0), float(1)) * std::numeric_limits::max() + float(0.5)); + + if (s.status() != QDataStream::Ok) { + qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data"; + return false; + } + } + } else { + auto line = reinterpret_cast(img.scanLine(header.isPhotoshop() ? y : h - y - 1)); + for (auto x = 0, n = img.width() * 4; x < n; x += 4) { + s >> f; + line[x] = std::clamp(f, float(0), float(1)); + s >> f; + line[x + 1] = std::clamp(f, float(0), float(1)); + s >> f; + line[x + 2] = std::clamp(f, float(0), float(1)); + line[x + 3] = float(1); + + if (s.status() != QDataStream::Ok) { + qCWarning(LOG_PFMPLUGIN) << "PFMHandler::read() detected corrupted data"; + return false; + } + } + } + } + + if (!header.isBlackAndWhite()) { + img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear)); + } + + *image = img; + return true; +} + +bool PFMHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) { + return true; + } + if (option == QImageIOHandler::ImageFormat) { + return true; + } + if (option == QImageIOHandler::Endianness) { + return true; + } + return false; +} + +QVariant PFMHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + if (auto d = device()) { + PfmHeader h; + if (h.peek(d)) { + v = QVariant::fromValue(h.size()); + } + } + } + + if (option == QImageIOHandler::ImageFormat) { + if (auto d = device()) { + PfmHeader h; + if (h.peek(d)) { + v = QVariant::fromValue(h.format()); + } + } + } + + if (option == QImageIOHandler::Endianness) { + if (auto d = device()) { + PfmHeader h; + if (h.peek(d)) { + v = QVariant::fromValue(h.byteOrder()); + } + } + } + + return v; +} + +QImageIOPlugin::Capabilities PFMPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "pfm") { + return Capabilities(CanRead); + } + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && PFMHandler::canRead(device)) { + cap |= CanRead; + } + return cap; +} + +QImageIOHandler *PFMPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new PFMHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} + +#include "moc_pfm_p.cpp" diff --git a/src/imageformats/pfm.json b/src/imageformats/pfm.json new file mode 100644 index 0000000..4c4d855 --- /dev/null +++ b/src/imageformats/pfm.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "pfm" ], + "MimeTypes": [ "image/x-pfm" ] +} diff --git a/src/imageformats/pfm_p.h b/src/imageformats/pfm_p.h new file mode 100644 index 0000000..b605d87 --- /dev/null +++ b/src/imageformats/pfm_p.h @@ -0,0 +1,37 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KIMG_PFM_P_H +#define KIMG_PFM_P_H + +#include + +class PFMHandler : public QImageIOHandler +{ +public: + PFMHandler(); + + bool canRead() const override; + bool read(QImage *image) override; + + bool supportsOption(QImageIOHandler::ImageOption option) const override; + QVariant option(QImageIOHandler::ImageOption option) const override; + + static bool canRead(QIODevice *device); +}; + +class PFMPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "pfm.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_PFM_P_H