diff --git a/README.md b/README.md index 91c3f1e..ff5afe2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The following image formats have read-only support: - Gimp (xcf) - Krita (kra) - OpenRaster (ora) +- Pixar raster (pxr) - 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 5e300b1..4367eac 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -67,6 +67,7 @@ kimageformats_read_tests( hdr pcx psd + pxr qoi ras rgb diff --git a/autotests/read/pxr/testcard_gray.png b/autotests/read/pxr/testcard_gray.png new file mode 100644 index 0000000..aceba2a Binary files /dev/null and b/autotests/read/pxr/testcard_gray.png differ diff --git a/autotests/read/pxr/testcard_gray.pxr b/autotests/read/pxr/testcard_gray.pxr new file mode 100644 index 0000000..3855248 Binary files /dev/null and b/autotests/read/pxr/testcard_gray.pxr differ diff --git a/autotests/read/pxr/testcard_rgb.png b/autotests/read/pxr/testcard_rgb.png new file mode 100644 index 0000000..d1721f5 Binary files /dev/null and b/autotests/read/pxr/testcard_rgb.png differ diff --git a/autotests/read/pxr/testcard_rgb.pxr b/autotests/read/pxr/testcard_rgb.pxr new file mode 100644 index 0000000..c981a9b Binary files /dev/null and b/autotests/read/pxr/testcard_rgb.pxr differ diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index cc3b042..55af814 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -87,6 +87,10 @@ kimageformats_add_plugin(kimg_psd SOURCES psd.cpp scanlineconverter.cpp) ################################## +kimageformats_add_plugin(kimg_pxr SOURCES pxr.cpp) + +################################## + kimageformats_add_plugin(kimg_qoi SOURCES qoi.cpp scanlineconverter.cpp) ################################## diff --git a/src/imageformats/pxr.cpp b/src/imageformats/pxr.cpp new file mode 100644 index 0000000..a598dc3 --- /dev/null +++ b/src/imageformats/pxr.cpp @@ -0,0 +1,267 @@ +/* + This file is part of the KDE project + SPDX-FileCopyrightText: 2024 Mirco Miranda + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "pxr_p.h" +#include "util_p.h" + +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(LOG_PXRPLUGIN) +Q_LOGGING_CATEGORY(LOG_PXRPLUGIN, "kf.imageformats.plugins.pxr", QtWarningMsg) + +class PxrHeader +{ +private: + QByteArray m_rawHeader; + + quint16 ui16(quint8 c1, quint8 c2) const { + return (quint16(c2) << 8) | quint16(c1); + } + + quint32 ui32(quint8 c1, quint8 c2, quint8 c3, quint8 c4) const { + return (quint32(c4) << 24) | (quint32(c3) << 16) | (quint32(c2) << 8) | quint32(c1); + } + +public: + PxrHeader() + { + + } + + bool isValid() const + { + return (m_rawHeader.size() == 512 && + m_rawHeader.startsWith(QByteArray::fromRawData("\x80\xE8\x00\x00", 4))); + } + + bool isSupported() const + { + return format() != QImage::Format_Invalid; + } + + qint32 width() const + { + if (!isValid()) { + return 0; + } + return qint32(ui16(m_rawHeader.at(418), m_rawHeader.at(419))); + } + + qint32 height() const + { + if (!isValid()) { + return 0; + } + return qint32(ui16(m_rawHeader.at(416), m_rawHeader.at(417))); + } + + QSize size() const + { + return QSize(width(), height()); + } + + qint32 channel() const + { + if (!isValid()) { + return 0; + } + return qint32(ui16(m_rawHeader.at(424), m_rawHeader.at(425))); + } + + qint32 depth() const + { + if (!isValid()) { + return 0; + } + return qint32(ui16(m_rawHeader.at(426), m_rawHeader.at(427))); + } + + // supposing the image offset (always 1024 on sample files) + qint32 offset() const + { + if (!isValid()) { + return 0; + } + return qint32(ui16(m_rawHeader.at(428), m_rawHeader.at(429))); + } + + QImage::Format format() const + { + if (channel() == 14 && depth() == 2) { + return QImage::Format_RGB888; + } + if (channel() == 8 && depth() == 2) { + return QImage::Format_Grayscale8; + } + return QImage::Format_Invalid; + } + + qsizetype strideSize() const + { + if (format() == QImage::Format_RGB888) { + return width() * 3; + } + if (format() == QImage::Format_Grayscale8) { + return width(); + } + return 0; + } + + bool read(QIODevice *d) + { + m_rawHeader = d->read(512); + return isValid(); + } + + bool peek(QIODevice *d) + { + d->startTransaction(); + auto ok = read(d); + d->rollbackTransaction(); + return ok; + } + + bool jumpToImageData(QIODevice *d) const + { + if (d->isSequential()) { + if (auto sz = std::max(offset() - qint32(m_rawHeader.size()), 0)) { + return d->read(sz).size() == sz; + } + return true; + } + return d->seek(offset()); + } +}; + +PXRHandler::PXRHandler() +{ +} + +bool PXRHandler::canRead() const +{ + if (canRead(device())) { + setFormat("pxr"); + return true; + } + return false; +} + +bool PXRHandler::canRead(QIODevice *device) +{ + if (!device) { + qCWarning(LOG_PXRPLUGIN) << "PXRHandler::canRead() called with no device"; + return false; + } + + PxrHeader h; + if (!h.peek(device)) { + return false; + } + + return h.isSupported(); +} + +bool PXRHandler::read(QImage *image) +{ + PxrHeader header; + + if (!header.read(device())) { + qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() invalid header"; + return false; + } + + auto img = imageAlloc(header.size(), header.format()); + if (img.isNull()) { + qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while allocating the image"; + return false; + } + + auto d = device(); + if (!header.jumpToImageData(d)) { + qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while seeking image data"; + return false; + } + + auto size = std::min(img.bytesPerLine(), header.strideSize()); + for (auto y = 0, h = img.height(); y < h; ++y) { + auto line = reinterpret_cast(img.scanLine(y)); + if (d->read(line, size) != size) { + qCWarning(LOG_PXRPLUGIN) << "PXRHandler::read() error while reading image scanline"; + return false; + } + } + + *image = img; + return true; +} + +bool PXRHandler::supportsOption(ImageOption option) const +{ + if (option == QImageIOHandler::Size) { + return true; + } + if (option == QImageIOHandler::ImageFormat) { + return true; + } + return false; +} + +QVariant PXRHandler::option(ImageOption option) const +{ + QVariant v; + + if (option == QImageIOHandler::Size) { + if (auto d = device()) { + PxrHeader h; + if (h.peek(d)) { + v = QVariant::fromValue(h.size()); + } + } + } + + if (option == QImageIOHandler::ImageFormat) { + if (auto d = device()) { + PxrHeader h; + if (h.peek(d)) { + v = QVariant::fromValue(h.format()); + } + } + } + + return v; +} + +QImageIOPlugin::Capabilities PXRPlugin::capabilities(QIODevice *device, const QByteArray &format) const +{ + if (format == "pxr") { + return Capabilities(CanRead); + } + if (!format.isEmpty()) { + return {}; + } + if (!device->isOpen()) { + return {}; + } + + Capabilities cap; + if (device->isReadable() && PXRHandler::canRead(device)) { + cap |= CanRead; + } + return cap; +} + +QImageIOHandler *PXRPlugin::create(QIODevice *device, const QByteArray &format) const +{ + QImageIOHandler *handler = new PXRHandler; + handler->setDevice(device); + handler->setFormat(format); + return handler; +} + +#include "moc_pxr_p.cpp" diff --git a/src/imageformats/pxr.json b/src/imageformats/pxr.json new file mode 100644 index 0000000..d7f617f --- /dev/null +++ b/src/imageformats/pxr.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "pxr" ], + "MimeTypes": [ "image/x-pxr" ] +} diff --git a/src/imageformats/pxr_p.h b/src/imageformats/pxr_p.h new file mode 100644 index 0000000..ddf38bb --- /dev/null +++ b/src/imageformats/pxr_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_PXR_P_H +#define KIMG_PXR_P_H + +#include + +class PXRHandler : public QImageIOHandler +{ +public: + PXRHandler(); + + 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 PXRPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "pxr.json") + +public: + Capabilities capabilities(QIODevice *device, const QByteArray &format) const override; + QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override; +}; + +#endif // KIMG_PXR_P_H