File indexing completed on 2024-12-22 04:15:54

0001 /*
0002  * SPDX-FileCopyrightText: 2023 Rasyuqa A. H. <qampidh@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  *
0006  * Based on KImageFormats Radiance HDR loader
0007  *
0008  * SPDX-FileCopyrightText: 2005 Christoph Hormann <chris_hormann@gmx.de>
0009  * SPDX-FileCopyrightText: 2005 Ignacio CastaƱo <castanyo@yahoo.es>
0010  *
0011  * SPDX-License-Identifier: LGPL-2.0-or-later
0012  */
0013 
0014 #include <kpluginfactory.h>
0015 
0016 #include <QBuffer>
0017 #include <QByteArray>
0018 
0019 #include <cmath>
0020 #include <cstdint>
0021 #include <memory>
0022 
0023 #include <KisDocument.h>
0024 #include <KisImportExportErrorCode.h>
0025 #include <KoColorModelStandardIds.h>
0026 #include <KoColorProfile.h>
0027 #include <KoCompositeOpRegistry.h>
0028 #include <KoDialog.h>
0029 #include <kis_group_layer.h>
0030 #include <kis_iterator_ng.h>
0031 #include <kis_meta_data_backend_registry.h>
0032 #include <kis_paint_layer.h>
0033 #include <kis_painter.h>
0034 #include <kis_properties_configuration.h>
0035 #include <kis_sequential_iterator.h>
0036 
0037 #include "RGBEImport.h"
0038 
0039 K_PLUGIN_FACTORY_WITH_JSON(KisRGBEImportFactory, "krita_rgbe_import.json", registerPlugin<RGBEImport>();)
0040 
0041 class Q_DECL_HIDDEN RGBEImportData
0042 {
0043 public:
0044     KisPaintDeviceSP m_currentFrame{nullptr};
0045     KoID m_colorID;
0046     KoID m_depthID;
0047     float m_gamma = 1.0;
0048     float m_exposure = 1.0;
0049     const KoColorSpace *cs = nullptr;
0050 };
0051 
0052 namespace RGBEIMPORT
0053 {
0054 #define MAXLINE 1024
0055 #define MINELEN 8 // minimum scanline length for encoding
0056 #define MAXELEN 0x7fff // maximum scanline length for encoding
0057 
0058 // read an old style line from the hdr image file
0059 // if 'first' is true the first byte is already read
0060 static bool ReadOldLine(quint8 *image, int width, QDataStream &s)
0061 {
0062     int rshift = 0;
0063     int i;
0064 
0065     while (width > 0) {
0066         s >> image[0];
0067         s >> image[1];
0068         s >> image[2];
0069         s >> image[3];
0070 
0071         if (s.atEnd()) {
0072             return false;
0073         }
0074 
0075         if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
0076             for (i = image[3] << rshift; i > 0; i--) {
0077                 memcpy(image, image-4, 4);
0078                 image += 4;
0079                 width--;
0080             }
0081             rshift += 8;
0082         } else {
0083             image += 4;
0084             width--;
0085             rshift = 0;
0086         }
0087     }
0088     return true;
0089 }
0090 
0091 static void RGBEToPaintDevice(quint8 *image, int width, KisSequentialIterator &it)
0092 {
0093     for (int j = 0; j < width; j++) {
0094         it.nextPixel();
0095         auto *dst = reinterpret_cast<float *>(it.rawData());
0096 
0097         if (image[3]) {
0098             const float v = std::ldexp(1.0f, int(image[3]) - (128 + 8));
0099             const float pixelData[4] = {float(image[0]) * v,
0100                                         float(image[1]) * v,
0101                                         float(image[2]) * v,
0102                                         1.0f};
0103             memcpy(dst, pixelData, 4 * sizeof(float));
0104         } else {
0105             // Zero exponent handle
0106             const float pixelData[4] = {0.0f, 0.0f, 0.0f, 1.0f};
0107             memcpy(dst, pixelData, 4 * sizeof(float));
0108         }
0109 
0110         image += 4;
0111     }
0112 }
0113 
0114 // Load the HDR image.
0115 static bool LoadHDR(QDataStream &s, const int width, const int height, KisSequentialIterator &it)
0116 {
0117     quint8 val;
0118     quint8 code;
0119 
0120     QByteArray lineArray;
0121     lineArray.resize(4 * width);
0122     quint8 *image = (quint8 *)lineArray.data();
0123 
0124     for (int cline = 0; cline < height; cline++) {
0125         // determine scanline type
0126         if ((width < MINELEN) || (MAXELEN < width)) {
0127             ReadOldLine(image, width, s);
0128             RGBEToPaintDevice(image, width, it);
0129             continue;
0130         }
0131 
0132         s >> val;
0133 
0134         if (s.atEnd()) {
0135             return true;
0136         }
0137 
0138         if (val != 2) {
0139             s.device()->ungetChar(val);
0140             ReadOldLine(image, width, s);
0141             RGBEToPaintDevice(image, width, it);
0142             continue;
0143         }
0144 
0145         s >> image[1];
0146         s >> image[2];
0147         s >> image[3];
0148 
0149         if (s.atEnd()) {
0150             return true;
0151         }
0152 
0153         if ((image[1] != 2) || (image[2] & 128)) {
0154             image[0] = 2;
0155             ReadOldLine(image + 4, width - 1, s);
0156             RGBEToPaintDevice(image, width, it);
0157             continue;
0158         }
0159 
0160         if ((image[2] << 8 | image[3]) != width) {
0161             dbgFile << "Line of pixels had width" << (image[2] << 8 | image[3]) << "instead of" << width;
0162             return false;
0163         }
0164 
0165         // read each component
0166         for (int i = 0; i < 4; i++) {
0167             for (int j = 0; j < width;) {
0168                 s >> code;
0169                 if (s.atEnd()) {
0170                     dbgFile << "Truncated HDR file";
0171                     return false;
0172                 }
0173                 if (code > 128) {
0174                     // run
0175                     code &= 127;
0176                     s >> val;
0177                     while (code != 0) {
0178                         image[i + j * 4] = val;
0179                         j++;
0180                         code--;
0181                     }
0182                 } else {
0183                     // non-run
0184                     while (code != 0) {
0185                         s >> image[i + j * 4];
0186                         j++;
0187                         code--;
0188                     }
0189                 }
0190             }
0191         }
0192 
0193         RGBEToPaintDevice(image, width, it);
0194     }
0195 
0196     return true;
0197 }
0198 
0199 } // namespace RGBEIMPORT
0200 
0201 RGBEImport::RGBEImport(QObject *parent, const QVariantList &)
0202     : KisImportExportFilter(parent)
0203 {
0204 }
0205 
0206 KisImportExportErrorCode
0207 RGBEImport::convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP /*configuration*/)
0208 {
0209     if (!io->isReadable()) {
0210         errFile << "Cannot read image contents";
0211         return ImportExportCodes::NoAccessToRead;
0212     }
0213 
0214     if (!(io->peek(11) == "#?RADIANCE\n" || io->peek(7) == "#?RGBE\n")) {
0215         errFile << "Invalid RGBE header!";
0216         return ImportExportCodes::ErrorWhileReading;
0217     }
0218 
0219     RGBEImportData d{};
0220 
0221     int len;
0222     QByteArray line(MAXLINE + 1, Qt::Uninitialized);
0223     QByteArray rawFormat;
0224     QByteArray rawGamma;
0225     QByteArray rawExposure;
0226     QByteArray rawHeaderInfo;
0227 
0228     // Parse header
0229     do {
0230         len = io->readLine(line.data(), MAXLINE);
0231         if (line.startsWith("# ")) {
0232             rawHeaderInfo = line.mid(2, len - 2 - 1 /*\n*/);
0233         } else if (line.startsWith("GAMMA=")) {
0234             rawGamma = line.mid(6, len - 6 - 1 /*\n*/);
0235         } else if (line.startsWith("EXPOSURE=")) {
0236             rawExposure = line.mid(9, len - 9 - 1 /*\n*/);
0237         } else if (line.startsWith("FORMAT=")) {
0238             rawFormat = line.mid(7, len - 7 - 1 /*\n*/);
0239         }
0240     } while ((len > 0) && (line[0] != '\n'));
0241 
0242     if (rawFormat != "32-bit_rle_rgbe") {
0243         errFile << "Invalid RGBE format!";
0244         return ImportExportCodes::ErrorWhileReading;
0245     }
0246 
0247     const QString headerInfo = [&]() {
0248         if (!rawHeaderInfo.isEmpty()) {
0249             return QString(rawHeaderInfo).trimmed();
0250         }
0251         return QString();
0252     }();
0253 
0254     // Unused fields, I don't know what to do with gamma and exposure fields yet.
0255     if (!rawGamma.isEmpty()) {
0256         bool gammaOk = false;
0257         const float gammaTemp = QString(rawGamma).toFloat(&gammaOk);
0258         if (gammaOk) {
0259             d.m_gamma = gammaTemp;
0260         }
0261     }
0262     if (!rawExposure.isEmpty()) {
0263         bool exposureOk = false;
0264         const float expTemp = QString(rawExposure).toFloat(&exposureOk);
0265         if (exposureOk) {
0266             d.m_exposure = expTemp;
0267         }
0268     }
0269 
0270     len = io->readLine(line.data(), MAXLINE);
0271     line.resize(len);
0272 
0273     /*
0274        TODO: handle flipping and rotation, as per the spec below
0275        The single resolution line consists of 4 values, a X and Y label each followed by a numerical
0276        integer value. The X and Y are immediately preceded by a sign which can be used to indicate
0277        flipping, the order of the X and Y indicate rotation. The standard coordinate system for
0278        Radiance images would have the following resolution string -Y N +X N. This indicates that the
0279        vertical axis runs down the file and the X axis is to the right (imagining the image as a
0280        rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
0281        indicate a vertical flip. If the X value appears before the Y value then that indicates that
0282        the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
0283        The reader can convince themselves that the 8 combinations cover all the possible image orientations
0284        and rotations.
0285     */
0286     QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY]) ([0-9]+) ([+\\-][XY]) ([0-9]+)\n"));
0287     QRegularExpressionMatch match = resolutionRegExp.match(QString::fromLatin1(line));
0288     if (!match.hasMatch()) {
0289         errFile << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
0290         return ImportExportCodes::InternalError;
0291     }
0292 
0293     if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
0294         errFile << "Unsupported image orientation in HDR file.";
0295         return ImportExportCodes::InternalError;
0296     }
0297 
0298     const int width = match.captured(4).toInt();
0299     const int height = match.captured(2).toInt();
0300 
0301     dbgFile << "RGBE image information:";
0302     dbgFile << "Program info:" << headerInfo;
0303     if (!rawGamma.isEmpty()) {
0304         dbgFile << "Gamma:" << d.m_gamma;
0305     } else {
0306         dbgFile << "No gamma metadata provided";
0307     }
0308     if (!rawExposure.isEmpty()) {
0309         dbgFile << "Exposure:" << d.m_exposure;
0310     } else {
0311         dbgFile << "No exposure metadata provided";
0312     }
0313     dbgFile << "Dimension:" << width << "x" << height;
0314 
0315     KisImageSP image;
0316     KisLayerSP layer;
0317 
0318     const KoColorProfile *profile = nullptr;
0319 
0320     d.m_colorID = RGBAColorModelID;
0321     d.m_depthID = Float32BitsColorDepthID;
0322 
0323     profile = KoColorSpaceRegistry::instance()->p709G10Profile();
0324     d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
0325 
0326     image = new KisImage(document->createUndoStore(), width, height, d.cs, "RGBE image");
0327     layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8);
0328     d.m_currentFrame = new KisPaintDevice(image->colorSpace());
0329 
0330     QDataStream stream(io);
0331     KisSequentialIterator it(d.m_currentFrame, {0, 0, width, height});
0332 
0333     if (!RGBEIMPORT::LoadHDR(stream, width, height, it)) {
0334         errFile << "Error loading HDR file.";
0335         return ImportExportCodes::InternalError;
0336     }
0337 
0338     layer->paintDevice()->makeCloneFrom(d.m_currentFrame, image->bounds());
0339     image->addNode(layer, image->rootLayer().data());
0340 
0341     document->setCurrentImage(image);
0342 
0343     return ImportExportCodes::OK;
0344 }
0345 
0346 #include <RGBEImport.moc>