File indexing completed on 2024-04-28 03:54:41

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2005 Christoph Hormann <chris_hormann@gmx.de>
0004     SPDX-FileCopyrightText: 2005 Ignacio CastaƱo <castanyo@yahoo.es>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "hdr_p.h"
0010 #include "util_p.h"
0011 
0012 #include <QColorSpace>
0013 #include <QDataStream>
0014 #include <QFloat16>
0015 #include <QImage>
0016 #include <QLoggingCategory>
0017 #include <QRegularExpressionMatch>
0018 
0019 #include <QDebug>
0020 
0021 /* *** HDR_HALF_QUALITY ***
0022  * If defined, a 16-bits float image is created, otherwise a 32-bits float ones (default).
0023  */
0024 //#define HDR_HALF_QUALITY // default commented -> you should define it in your cmake file
0025 
0026 typedef unsigned char uchar;
0027 
0028 Q_LOGGING_CATEGORY(HDRPLUGIN, "kf.imageformats.plugins.hdr", QtWarningMsg)
0029 
0030 namespace // Private.
0031 {
0032 #define MAXLINE 1024
0033 #define MINELEN 8 // minimum scanline length for encoding
0034 #define MAXELEN 0x7fff // maximum scanline length for encoding
0035 
0036 // read an old style line from the hdr image file
0037 // if 'first' is true the first byte is already read
0038 static bool Read_Old_Line(uchar *image, int width, QDataStream &s)
0039 {
0040     int rshift = 0;
0041     int i;
0042 
0043     uchar *start = image;
0044     while (width > 0) {
0045         s >> image[0];
0046         s >> image[1];
0047         s >> image[2];
0048         s >> image[3];
0049 
0050         if (s.atEnd()) {
0051             return false;
0052         }
0053 
0054         if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
0055             // NOTE: we don't have an image sample that cover this code
0056             if (rshift > 31) {
0057                 return false;
0058             }
0059             for (i = image[3] << rshift; i > 0 && width > 0; i--) {
0060                 if (image == start) {
0061                     return false; // you cannot be here at the first run
0062                 }
0063                 // memcpy(image, image-4, 4);
0064                 (uint &)image[0] = (uint &)image[0 - 4];
0065                 image += 4;
0066                 width--;
0067             }
0068             rshift += 8;
0069         } else {
0070             image += 4;
0071             width--;
0072             rshift = 0;
0073         }
0074     }
0075     return true;
0076 }
0077 
0078 template<class float_T>
0079 void RGBE_To_QRgbLine(uchar *image, float_T *scanline, int width)
0080 {
0081     for (int j = 0; j < width; j++) {
0082         // v = ldexp(1.0, int(image[3]) - 128);
0083         float v;
0084         int e = qBound(-31, int(image[3]) - 128, 31);
0085         if (e > 0) {
0086             v = float(1 << e);
0087         } else {
0088             v = 1.0f / float(1 << -e);
0089         }
0090 
0091         auto j4 = j * 4;
0092         auto vn = v / 255.0f;
0093         scanline[j4] = float_T(std::min(float(image[0]) * vn, 1.0f));
0094         scanline[j4 + 1] = float_T(std::min(float(image[1]) * vn, 1.0f));
0095         scanline[j4 + 2] = float_T(std::min(float(image[2]) * vn, 1.0f));
0096         scanline[j4 + 3] = float_T(1.0f);
0097         image += 4;
0098     }
0099 }
0100 
0101 QImage::Format imageFormat()
0102 {
0103 #ifdef HDR_HALF_QUALITY
0104     return QImage::Format_RGBX16FPx4;
0105 #else
0106     return QImage::Format_RGBX32FPx4;
0107 #endif
0108 }
0109 
0110 // Load the HDR image.
0111 static bool LoadHDR(QDataStream &s, const int width, const int height, QImage &img)
0112 {
0113     uchar val;
0114     uchar code;
0115 
0116     // Create dst image.
0117     img = imageAlloc(width, height, imageFormat());
0118     if (img.isNull()) {
0119         qCDebug(HDRPLUGIN) << "Couldn't create image with size" << width << height << "and format RGB32";
0120         return false;
0121     }
0122 
0123     QByteArray lineArray;
0124     lineArray.resize(4 * width);
0125     uchar *image = reinterpret_cast<uchar *>(lineArray.data());
0126 
0127     for (int cline = 0; cline < height; cline++) {
0128 #ifdef HDR_HALF_QUALITY
0129         auto scanline = reinterpret_cast<qfloat16 *>(img.scanLine(cline));
0130 #else
0131         auto scanline = reinterpret_cast<float *>(img.scanLine(cline));
0132 #endif
0133 
0134         // determine scanline type
0135         if ((width < MINELEN) || (MAXELEN < width)) {
0136             Read_Old_Line(image, width, s);
0137             RGBE_To_QRgbLine(image, scanline, width);
0138             continue;
0139         }
0140 
0141         s >> val;
0142 
0143         if (s.atEnd()) {
0144             return true;
0145         }
0146 
0147         if (val != 2) {
0148             s.device()->ungetChar(val);
0149             Read_Old_Line(image, width, s);
0150             RGBE_To_QRgbLine(image, scanline, width);
0151             continue;
0152         }
0153 
0154         s >> image[1];
0155         s >> image[2];
0156         s >> image[3];
0157 
0158         if (s.atEnd()) {
0159             return true;
0160         }
0161 
0162         if ((image[1] != 2) || (image[2] & 128)) {
0163             image[0] = 2;
0164             Read_Old_Line(image + 4, width - 1, s);
0165             RGBE_To_QRgbLine(image, scanline, width);
0166             continue;
0167         }
0168 
0169         if ((image[2] << 8 | image[3]) != width) {
0170             qCDebug(HDRPLUGIN) << "Line of pixels had width" << (image[2] << 8 | image[3]) << "instead of" << width;
0171             return false;
0172         }
0173 
0174         // read each component
0175         for (int i = 0, len = int(lineArray.size()); i < 4; i++) {
0176             for (int j = 0; j < width;) {
0177                 s >> code;
0178                 if (s.atEnd()) {
0179                     qCDebug(HDRPLUGIN) << "Truncated HDR file";
0180                     return false;
0181                 }
0182                 if (code > 128) {
0183                     // run
0184                     code &= 127;
0185                     s >> val;
0186                     while (code != 0) {
0187                         auto idx = i + j * 4;
0188                         if (idx < len) {
0189                             image[idx] = val;
0190                         }
0191                         j++;
0192                         code--;
0193                     }
0194                 } else {
0195                     // non-run
0196                     while (code != 0) {
0197                         auto idx = i + j * 4;
0198                         if (idx < len) {
0199                             s >> image[idx];
0200                         }
0201                         j++;
0202                         code--;
0203                     }
0204                 }
0205             }
0206         }
0207 
0208         RGBE_To_QRgbLine(image, scanline, width);
0209     }
0210 
0211     return true;
0212 }
0213 
0214 static QSize readHeaderSize(QIODevice *device)
0215 {
0216     int len;
0217     QByteArray line(MAXLINE + 1, Qt::Uninitialized);
0218     QByteArray format;
0219 
0220     // Parse header
0221     do {
0222         len = device->readLine(line.data(), MAXLINE);
0223 
0224         if (line.startsWith("FORMAT=")) {
0225             format = line.mid(7, len - 7 - 1 /*\n*/);
0226         }
0227 
0228     } while ((len > 0) && (line[0] != '\n'));
0229 
0230     if (format != "32-bit_rle_rgbe") {
0231         qCDebug(HDRPLUGIN) << "Unknown HDR format:" << format;
0232         return QSize();
0233     }
0234 
0235     len = device->readLine(line.data(), MAXLINE);
0236     line.resize(len);
0237 
0238     /*
0239        TODO: handle flipping and rotation, as per the spec below
0240        The single resolution line consists of 4 values, a X and Y label each followed by a numerical
0241        integer value. The X and Y are immediately preceded by a sign which can be used to indicate
0242        flipping, the order of the X and Y indicate rotation. The standard coordinate system for
0243        Radiance images would have the following resolution string -Y N +X N. This indicates that the
0244        vertical axis runs down the file and the X axis is to the right (imagining the image as a
0245        rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
0246        indicate a vertical flip. If the X value appears before the Y value then that indicates that
0247        the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
0248        The reader can convince themselves that the 8 combinations cover all the possible image orientations
0249        and rotations.
0250     */
0251     QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY]) ([0-9]+) ([+\\-][XY]) ([0-9]+)\n"));
0252     QRegularExpressionMatch match = resolutionRegExp.match(QString::fromLatin1(line));
0253     if (!match.hasMatch()) {
0254         qCDebug(HDRPLUGIN) << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
0255         return QSize();
0256     }
0257 
0258     if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
0259         qCDebug(HDRPLUGIN) << "Unsupported image orientation in HDR file.";
0260         return QSize();
0261     }
0262 
0263     return QSize(match.captured(4).toInt(), match.captured(2).toInt());
0264 }
0265 
0266 } // namespace
0267 
0268 bool HDRHandler::read(QImage *outImage)
0269 {
0270     QDataStream s(device());
0271 
0272     QSize size = readHeaderSize(s.device());
0273     if (!size.isValid()) {
0274         return false;
0275     }
0276 
0277     QImage img;
0278     if (!LoadHDR(s, size.width(), size.height(), img)) {
0279         // qDebug() << "Error loading HDR file.";
0280         return false;
0281     }
0282     // The images read by Gimp and Photoshop (including those of the tests) are interpreted with linear color space.
0283     // By setting the linear color space, programs that support profiles display HDR files as in GIMP and Photoshop.
0284     img.setColorSpace(QColorSpace(QColorSpace::SRgbLinear));
0285 
0286     *outImage = img;
0287     return true;
0288 }
0289 
0290 bool HDRHandler::supportsOption(ImageOption option) const
0291 {
0292     if (option == QImageIOHandler::Size) {
0293         return true;
0294     }
0295     if (option == QImageIOHandler::ImageFormat) {
0296         return true;
0297     }
0298     return false;
0299 }
0300 
0301 QVariant HDRHandler::option(ImageOption option) const
0302 {
0303     QVariant v;
0304 
0305     if (option == QImageIOHandler::Size) {
0306         if (auto d = device()) {
0307             // transactions works on both random and sequential devices
0308             d->startTransaction();
0309             auto size = readHeaderSize(d);
0310             d->rollbackTransaction();
0311             if (size.isValid()) {
0312                 v = QVariant::fromValue(size);
0313             }
0314         }
0315     }
0316 
0317     if (option == QImageIOHandler::ImageFormat) {
0318         v = QVariant::fromValue(imageFormat());
0319     }
0320 
0321     return v;
0322 }
0323 
0324 HDRHandler::HDRHandler()
0325 {
0326 }
0327 
0328 bool HDRHandler::canRead() const
0329 {
0330     if (canRead(device())) {
0331         setFormat("hdr");
0332         return true;
0333     }
0334     return false;
0335 }
0336 
0337 bool HDRHandler::canRead(QIODevice *device)
0338 {
0339     if (!device) {
0340         qWarning("HDRHandler::canRead() called with no device");
0341         return false;
0342     }
0343 
0344     // the .pic taken from official test cases does not start with this string but can be loaded.
0345     if(device->peek(11) == "#?RADIANCE\n" || device->peek(7) == "#?RGBE\n") {
0346         return true;
0347     }
0348 
0349     // allow to load offical test cases: https://radsite.lbl.gov/radiance/framed.html
0350     device->startTransaction();
0351     QSize size = readHeaderSize(device);
0352     device->rollbackTransaction();
0353     if (size.isValid()) {
0354         return true;
0355     }
0356 
0357     return false;
0358 }
0359 
0360 QImageIOPlugin::Capabilities HDRPlugin::capabilities(QIODevice *device, const QByteArray &format) const
0361 {
0362     if (format == "hdr") {
0363         return Capabilities(CanRead);
0364     }
0365     if (!format.isEmpty()) {
0366         return {};
0367     }
0368     if (!device->isOpen()) {
0369         return {};
0370     }
0371 
0372     Capabilities cap;
0373     if (device->isReadable() && HDRHandler::canRead(device)) {
0374         cap |= CanRead;
0375     }
0376     return cap;
0377 }
0378 
0379 QImageIOHandler *HDRPlugin::create(QIODevice *device, const QByteArray &format) const
0380 {
0381     QImageIOHandler *handler = new HDRHandler;
0382     handler->setDevice(device);
0383     handler->setFormat(format);
0384     return handler;
0385 }
0386 
0387 #include "moc_hdr_p.cpp"