Krita Source Code Documentation
Loading...
Searching...
No Matches
RGBEImport.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2023 Rasyuqa A. H. <qampidh@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 *
6 * Based on KImageFormats Radiance HDR loader
7 *
8 * SPDX-FileCopyrightText: 2005 Christoph Hormann <chris_hormann@gmx.de>
9 * SPDX-FileCopyrightText: 2005 Ignacio CastaƱo <castanyo@yahoo.es>
10 *
11 * SPDX-License-Identifier: LGPL-2.0-or-later
12 */
13
14#include <kpluginfactory.h>
15
16#include <QBuffer>
17#include <QByteArray>
18
19#include <cmath>
20#include <cstdint>
21#include <memory>
22
23#include <KisDocument.h>
26#include <KoColorProfile.h>
28#include <KoDialog.h>
29#include <kis_group_layer.h>
30#include <kis_iterator_ng.h>
32#include <kis_paint_layer.h>
33#include <kis_painter.h>
36
37#include "RGBEImport.h"
38
39K_PLUGIN_FACTORY_WITH_JSON(KisRGBEImportFactory, "krita_rgbe_import.json", registerPlugin<RGBEImport>();)
40
41class Q_DECL_HIDDEN RGBEImportData
42{
43public:
44 KisPaintDeviceSP m_currentFrame{nullptr};
47 float m_gamma = 1.0;
48 float m_exposure = 1.0;
49 const KoColorSpace *cs = nullptr;
50};
51
52namespace RGBEIMPORT
53{
54#define MAXLINE 1024
55#define MINELEN 8 // minimum scanline length for encoding
56#define MAXELEN 0x7fff // maximum scanline length for encoding
57
58// read an old style line from the hdr image file
59// if 'first' is true the first byte is already read
60static bool ReadOldLine(quint8 *image, int width, QDataStream &s)
61{
62 int rshift = 0;
63 int i;
64
65 while (width > 0) {
66 s >> image[0];
67 s >> image[1];
68 s >> image[2];
69 s >> image[3];
70
71 if (s.atEnd()) {
72 return false;
73 }
74
75 if ((image[0] == 1) && (image[1] == 1) && (image[2] == 1)) {
76 for (i = image[3] << rshift; i > 0; i--) {
77 memcpy(image, image-4, 4);
78 image += 4;
79 width--;
80 }
81 rshift += 8;
82 } else {
83 image += 4;
84 width--;
85 rshift = 0;
86 }
87 }
88 return true;
89}
90
91static void RGBEToPaintDevice(quint8 *image, int width, KisSequentialIterator &it)
92{
93 for (int j = 0; j < width; j++) {
94 it.nextPixel();
95 auto *dst = reinterpret_cast<float *>(it.rawData());
96
97 if (image[3]) {
98 const float v = std::ldexp(1.0f, int(image[3]) - (128 + 8));
99 const float pixelData[4] = {float(image[0]) * v,
100 float(image[1]) * v,
101 float(image[2]) * v,
102 1.0f};
103 memcpy(dst, pixelData, 4 * sizeof(float));
104 } else {
105 // Zero exponent handle
106 const float pixelData[4] = {0.0f, 0.0f, 0.0f, 1.0f};
107 memcpy(dst, pixelData, 4 * sizeof(float));
108 }
109
110 image += 4;
111 }
112}
113
114// Load the HDR image.
115static bool LoadHDR(QDataStream &s, const int width, const int height, KisSequentialIterator &it)
116{
117 quint8 val;
118 quint8 code;
119
120 QByteArray lineArray;
121 lineArray.resize(4 * width);
122 quint8 *image = (quint8 *)lineArray.data();
123
124 for (int cline = 0; cline < height; cline++) {
125 // determine scanline type
126 if ((width < MINELEN) || (MAXELEN < width)) {
127 ReadOldLine(image, width, s);
128 RGBEToPaintDevice(image, width, it);
129 continue;
130 }
131
132 s >> val;
133
134 if (s.atEnd()) {
135 return true;
136 }
137
138 if (val != 2) {
139 s.device()->ungetChar(val);
140 ReadOldLine(image, width, s);
141 RGBEToPaintDevice(image, width, it);
142 continue;
143 }
144
145 s >> image[1];
146 s >> image[2];
147 s >> image[3];
148
149 if (s.atEnd()) {
150 return true;
151 }
152
153 if ((image[1] != 2) || (image[2] & 128)) {
154 image[0] = 2;
155 ReadOldLine(image + 4, width - 1, s);
156 RGBEToPaintDevice(image, width, it);
157 continue;
158 }
159
160 if ((image[2] << 8 | image[3]) != width) {
161 dbgFile << "Line of pixels had width" << (image[2] << 8 | image[3]) << "instead of" << width;
162 return false;
163 }
164
165 // read each component
166 for (int i = 0; i < 4; i++) {
167 for (int j = 0; j < width;) {
168 s >> code;
169 if (s.atEnd()) {
170 dbgFile << "Truncated HDR file";
171 return false;
172 }
173 if (code > 128) {
174 // run
175 code &= 127;
176 s >> val;
177 while (code != 0) {
178 image[i + j * 4] = val;
179 j++;
180 code--;
181 }
182 } else {
183 // non-run
184 while (code != 0) {
185 s >> image[i + j * 4];
186 j++;
187 code--;
188 }
189 }
190 }
191 }
192
193 RGBEToPaintDevice(image, width, it);
194 }
195
196 return true;
197}
198
199} // namespace RGBEIMPORT
200
201RGBEImport::RGBEImport(QObject *parent, const QVariantList &)
202 : KisImportExportFilter(parent)
203{
204}
205
207RGBEImport::convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP /*configuration*/)
208{
209 if (!io->isReadable()) {
210 errFile << "Cannot read image contents";
212 }
213
214 if (!(io->peek(11) == "#?RADIANCE\n" || io->peek(7) == "#?RGBE\n")) {
215 errFile << "Invalid RGBE header!";
217 }
218
220
221 int len;
222 QByteArray line(MAXLINE + 1, Qt::Uninitialized);
223 QByteArray rawFormat;
224 QByteArray rawGamma;
225 QByteArray rawExposure;
226 QByteArray rawHeaderInfo;
227
228 // Parse header
229 do {
230 len = io->readLine(line.data(), MAXLINE);
231 if (line.startsWith("# ")) {
232 rawHeaderInfo = line.mid(2, len - 2 - 1 /*\n*/);
233 } else if (line.startsWith("GAMMA=")) {
234 rawGamma = line.mid(6, len - 6 - 1 /*\n*/);
235 } else if (line.startsWith("EXPOSURE=")) {
236 rawExposure = line.mid(9, len - 9 - 1 /*\n*/);
237 } else if (line.startsWith("FORMAT=")) {
238 rawFormat = line.mid(7, len - 7 - 1 /*\n*/);
239 }
240 } while ((len > 0) && (line[0] != '\n'));
241
242 if (rawFormat != "32-bit_rle_rgbe") {
243 errFile << "Invalid RGBE format!";
245 }
246
247 const QString headerInfo = [&]() {
248 if (!rawHeaderInfo.isEmpty()) {
249 return QString(rawHeaderInfo).trimmed();
250 }
251 return QString();
252 }();
253
254 // Unused fields, I don't know what to do with gamma and exposure fields yet.
255 if (!rawGamma.isEmpty()) {
256 bool gammaOk = false;
257 const float gammaTemp = QString(rawGamma).toFloat(&gammaOk);
258 if (gammaOk) {
259 d.m_gamma = gammaTemp;
260 }
261 }
262 if (!rawExposure.isEmpty()) {
263 bool exposureOk = false;
264 const float expTemp = QString(rawExposure).toFloat(&exposureOk);
265 if (exposureOk) {
266 d.m_exposure = expTemp;
267 }
268 }
269
270 len = io->readLine(line.data(), MAXLINE);
271 line.resize(len);
272
273 /*
274 TODO: handle flipping and rotation, as per the spec below
275 The single resolution line consists of 4 values, a X and Y label each followed by a numerical
276 integer value. The X and Y are immediately preceded by a sign which can be used to indicate
277 flipping, the order of the X and Y indicate rotation. The standard coordinate system for
278 Radiance images would have the following resolution string -Y N +X N. This indicates that the
279 vertical axis runs down the file and the X axis is to the right (imagining the image as a
280 rectangular block of data). A -X would indicate a horizontal flip of the image. A +Y would
281 indicate a vertical flip. If the X value appears before the Y value then that indicates that
282 the image is stored in column order rather than row order, that is, it is rotated by 90 degrees.
283 The reader can convince themselves that the 8 combinations cover all the possible image orientations
284 and rotations.
285 */
286 QRegularExpression resolutionRegExp(QStringLiteral("([+\\-][XY]) ([0-9]+) ([+\\-][XY]) ([0-9]+)\n"));
287 QRegularExpressionMatch match = resolutionRegExp.match(QString::fromLatin1(line));
288 if (!match.hasMatch()) {
289 errFile << "Invalid HDR file, the first line after the header didn't have the expected format:" << line;
291 }
292
293 if ((match.captured(1).at(1) != u'Y') || (match.captured(3).at(1) != u'X')) {
294 errFile << "Unsupported image orientation in HDR file.";
296 }
297
298 const int width = match.captured(4).toInt();
299 const int height = match.captured(2).toInt();
300
301 dbgFile << "RGBE image information:";
302 dbgFile << "Program info:" << headerInfo;
303 if (!rawGamma.isEmpty()) {
304 dbgFile << "Gamma:" << d.m_gamma;
305 } else {
306 dbgFile << "No gamma metadata provided";
307 }
308 if (!rawExposure.isEmpty()) {
309 dbgFile << "Exposure:" << d.m_exposure;
310 } else {
311 dbgFile << "No exposure metadata provided";
312 }
313 dbgFile << "Dimension:" << width << "x" << height;
314
315 KisImageSP image;
316 KisLayerSP layer;
317
318 const KoColorProfile *profile = nullptr;
319
320 d.m_colorID = RGBAColorModelID;
321 d.m_depthID = Float32BitsColorDepthID;
322
324 d.cs = KoColorSpaceRegistry::instance()->colorSpace(d.m_colorID.id(), d.m_depthID.id(), profile);
325
326 image = new KisImage(document->createUndoStore(), width, height, d.cs, "RGBE image");
327 layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8);
328 d.m_currentFrame = new KisPaintDevice(image->colorSpace());
329
330 QDataStream stream(io);
331 KisSequentialIterator it(d.m_currentFrame, {0, 0, width, height});
332
333 if (!RGBEIMPORT::LoadHDR(stream, width, height, it)) {
334 errFile << "Error loading HDR file.";
336 }
337
338 layer->paintDevice()->makeCloneFrom(d.m_currentFrame, image->bounds());
339 image->addNode(layer, image->rootLayer().data());
340
341 document->setCurrentImage(image);
342
344}
345
346#include <RGBEImport.moc>
qreal v
qreal u
const KoID Float32BitsColorDepthID("F32", ki18n("32-bit float/channel"))
const KoID RGBAColorModelID("RGBA", ki18n("RGB/Alpha"))
const quint8 OPACITY_OPAQUE_U8
#define MAXLINE
#define MINELEN
#define MAXELEN
KisGroupLayerSP rootLayer() const
const KoColorSpace * colorSpace() const
QString nextLayerName(const QString &baseName="") const
Definition kis_image.cc:715
QRect bounds() const override
The base class for import and export filters.
void makeCloneFrom(KisPaintDeviceSP src, const QRect &rect)
ALWAYS_INLINE quint8 * rawData()
Definition KoID.h:30
RGBEImport(QObject *parent, const QVariantList &)
KisImportExportErrorCode convert(KisDocument *document, QIODevice *io, KisPropertiesConfigurationSP configuration=nullptr) override
K_PLUGIN_FACTORY_WITH_JSON(KritaASCCDLFactory, "kritaasccdl.json", registerPlugin< KritaASCCDL >();) KritaASCCDL
#define errFile
Definition kis_debug.h:115
#define dbgFile
Definition kis_debug.h:53
static void RGBEToPaintDevice(quint8 *image, int width, KisSequentialIterator &it)
static bool LoadHDR(QDataStream &s, const int width, const int height, KisSequentialIterator &it)
static bool ReadOldLine(quint8 *image, int width, QDataStream &s)
virtual KisPaintDeviceSP paintDevice() const =0
bool addNode(KisNodeSP node, KisNodeSP parent=KisNodeSP(), KisNodeAdditionFlags flags=KisNodeAdditionFlag::None)
const KoColorSpace * colorSpace(const QString &colorModelId, const QString &colorDepthId, const KoColorProfile *profile)
static KoColorSpaceRegistry * instance()
const KoColorProfile * p709G10Profile() const