Commit 5fd96454 authored by Dmitry Kazakov's avatar Dmitry Kazakov

Fixed loading of the EXR files with zero alpha and non-zero colors

EXR works with premultiplied colors, so it suports having zero alpha and
non-zero colors. Krita doesn't support premultiplied colors, so we just
set alpha to HALF_EPSILON on loading and reset it back to zero on saving.

This introduces subtle round-trip bugs, but they happen for alpha < 0.01
values only (which is 1%).

TODO:
Now we also need to reset default projection color of the image to black
to load the EXR properly

CCBUG:301443
parent c5edebce
#add_subdirectory(tests) add_subdirectory(tests)
include_directories( ${OPENEXR_INCLUDE_DIR} ) include_directories( ${OPENEXR_INCLUDE_DIR} )
......
...@@ -28,9 +28,9 @@ ...@@ -28,9 +28,9 @@
#include <ImfOutputFile.h> #include <ImfOutputFile.h>
#include <kapplication.h> #include <kapplication.h>
#include <kmessagebox.h>
#include <kio/netaccess.h> #include <kio/netaccess.h>
#include <kio/deletejob.h>
#include <KoColorSpaceRegistry.h> #include <KoColorSpaceRegistry.h>
#include <KoCompositeOpRegistry.h> #include <KoCompositeOpRegistry.h>
...@@ -51,55 +51,6 @@ ...@@ -51,55 +51,6 @@
#include <metadata/kis_meta_data_store.h> #include <metadata/kis_meta_data_store.h>
#include <metadata/kis_meta_data_value.h> #include <metadata/kis_meta_data_value.h>
exrConverter::exrConverter(KisDoc2 *doc)
{
m_doc = doc;
m_job = 0;
m_stop = false;
}
exrConverter::~exrConverter()
{
}
enum ImageType {
IT_UNKNOWN,
IT_FLOAT16,
IT_FLOAT32,
IT_UNSUPPORTED
};
ImageType imfTypeToKisType(Imf::PixelType type)
{
switch (type) {
case Imf::UINT:
case Imf::NUM_PIXELTYPES:
return IT_UNSUPPORTED;
case Imf::HALF:
return IT_FLOAT16;
case Imf::FLOAT:
return IT_FLOAT32;
default:
qFatal("Out of bound enum");
return IT_UNKNOWN;
}
}
const KoColorSpace* kisTypeToColorSpace(QString model, ImageType imageType)
{
switch (imageType) {
case IT_FLOAT16:
return KoColorSpaceRegistry::instance()->colorSpace(model, Float16BitsColorDepthID.id(), "");
case IT_FLOAT32:
return KoColorSpaceRegistry::instance()->colorSpace(model, Float32BitsColorDepthID.id(), "");
case IT_UNKNOWN:
case IT_UNSUPPORTED:
return 0;
default:
qFatal("Out of bound enum");
return 0;
}
}
template<typename _T_> template<typename _T_>
struct Rgba { struct Rgba {
...@@ -124,6 +75,13 @@ struct ExrGroupLayerInfo : public ExrLayerInfoBase { ...@@ -124,6 +75,13 @@ struct ExrGroupLayerInfo : public ExrLayerInfoBase {
KisGroupLayerSP groupLayer; KisGroupLayerSP groupLayer;
}; };
enum ImageType {
IT_UNKNOWN,
IT_FLOAT16,
IT_FLOAT32,
IT_UNSUPPORTED
};
struct ExrPaintLayerInfo : public ExrLayerInfoBase { struct ExrPaintLayerInfo : public ExrLayerInfoBase {
ExrPaintLayerInfo() : imageType(IT_UNKNOWN) { ExrPaintLayerInfo() : imageType(IT_UNKNOWN) {
} }
...@@ -148,6 +106,68 @@ void ExrPaintLayerInfo::updateImageType(ImageType channelType) ...@@ -148,6 +106,68 @@ void ExrPaintLayerInfo::updateImageType(ImageType channelType)
} }
} }
struct exrConverter::Private {
Private() : doc(0), warnedAboutZeroedAlpha(false),
showNotifications(false) {}
KisImageSP image;
KisDoc2 *doc;
bool warnedAboutZeroedAlpha;
bool showNotifications;
template <typename T>
void unmultiplyAlpha(Rgba<T> *pixel);
template<typename _T_>
void decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP layer, int width, int xstart, int ystart, int height, Imf::PixelType ptype);
};
exrConverter::exrConverter(KisDoc2 *doc, bool showNotifications)
: m_d(new Private)
{
m_d->doc = doc;
m_d->showNotifications = showNotifications;
}
exrConverter::~exrConverter()
{
}
ImageType imfTypeToKisType(Imf::PixelType type)
{
switch (type) {
case Imf::UINT:
case Imf::NUM_PIXELTYPES:
return IT_UNSUPPORTED;
case Imf::HALF:
return IT_FLOAT16;
case Imf::FLOAT:
return IT_FLOAT32;
default:
qFatal("Out of bound enum");
return IT_UNKNOWN;
}
}
const KoColorSpace* kisTypeToColorSpace(QString model, ImageType imageType)
{
switch (imageType) {
case IT_FLOAT16:
return KoColorSpaceRegistry::instance()->colorSpace(model, Float16BitsColorDepthID.id(), "");
case IT_FLOAT32:
return KoColorSpaceRegistry::instance()->colorSpace(model, Float32BitsColorDepthID.id(), "");
case IT_UNKNOWN:
case IT_UNSUPPORTED:
return 0;
default:
qFatal("Out of bound enum");
return 0;
}
}
template<typename _T_> template<typename _T_>
void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP layer, int width, int xstart, int ystart, int height, Imf::PixelType ptype) void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP layer, int width, int xstart, int ystart, int height, Imf::PixelType ptype)
{ {
...@@ -170,8 +190,6 @@ void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP ...@@ -170,8 +190,6 @@ void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP
KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width); KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width);
do { do {
// XXX: For now unmultiply the alpha, though compositing will be faster if we
// keep it premultiplied.
_T_ unmultipliedRed = *rgba; _T_ unmultipliedRed = *rgba;
_T_* dst = reinterpret_cast<_T_*>(it->rawData()); _T_* dst = reinterpret_cast<_T_*>(it->rawData());
...@@ -185,8 +203,106 @@ void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP ...@@ -185,8 +203,106 @@ void decodeData1(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP
} }
template <typename T>
static inline T alphaNoiseThreshold()
{
return static_cast<T>(0.01); // 1%
}
template <typename T>
void exrConverter::Private::unmultiplyAlpha(Rgba<T> *pixel)
{
if (pixel->a < HALF_EPSILON &&
(pixel->r > HALF_EPSILON ||
pixel->g > HALF_EPSILON ||
pixel->b > HALF_EPSILON)) {
T newAlpha = 0.0;
T r;
T g;
T b;
do {
newAlpha += HALF_EPSILON;
r = pixel->r / newAlpha;
g = pixel->g / newAlpha;
b = pixel->b / newAlpha;
} while (newAlpha < alphaNoiseThreshold<T>() &&
(r * newAlpha != pixel->r ||
g * newAlpha != pixel->g ||
b * newAlpha != pixel->b));
pixel->r = r;
pixel->g = g;
pixel->b = b;
pixel->a = newAlpha;
} else if (pixel->a > HALF_EPSILON) {
if (!this->warnedAboutZeroedAlpha &&
pixel->a < alphaNoiseThreshold<T>()) {
QString msg =
i18nc("@info",
"The image contains pixels with small Alpha value."
"<nl/><nl/>"
"Due to technical reasons, when saving this "
"image back to EXR, the alpha channel of these pixels will be "
"reset to zero (the color channels will stay untouched)."
"<nl/><nl/>"
"This will hardly make any visual difference just keep it in mind."
"<nl/><nl/>"
"<note>Range from <numid>%1</numid> to <numid>%2</numid></note>", HALF_EPSILON, alphaNoiseThreshold<T>());
if (this->showNotifications) {
KMessageBox::information(0, msg, i18nc("@title:window", "EXR image will be modified"), "dontNotifyEXRChangedAgain");
} else {
qWarning() << "WARNING:" << msg;
}
this->warnedAboutZeroedAlpha = true;
}
pixel->r /= pixel->a;
pixel->g /= pixel->a;
pixel->b /= pixel->a;
}
}
template <typename T, typename Pixel, int size, int alphaPos>
void multiplyAlpha(Pixel *pixel)
{
bool hasNonZeroColorData = false;
T alpha = pixel->data[alphaPos];
if (alpha > HALF_EPSILON) {
for (int i = 0; i < size; ++i) {
if (i != alphaPos) {
pixel->data[i] *= alpha;
if (pixel->data[i] > HALF_EPSILON) {
hasNonZeroColorData = true;
}
}
}
if (alpha < alphaNoiseThreshold<T>() &&
hasNonZeroColorData) {
alpha = 0.0;
}
pixel->data[alphaPos] = alpha;
}
}
template<typename _T_> template<typename _T_>
void decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP layer, int width, int xstart, int ystart, int height, Imf::PixelType ptype) void exrConverter::Private::decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP layer, int width, int xstart, int ystart, int height, Imf::PixelType ptype)
{ {
typedef Rgba<_T_> Rgba; typedef Rgba<_T_> Rgba;
...@@ -222,22 +338,15 @@ void decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP ...@@ -222,22 +338,15 @@ void decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP
KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width); KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width);
do { do {
// XXX: For now unmultiply the alpha, though compositing will be faster if we if (hasAlpha) {
// keep it premultiplied. unmultiplyAlpha(rgba);
_T_ unmultipliedRed = rgba->r;
_T_ unmultipliedGreen = rgba->g;
_T_ unmultipliedBlue = rgba->b;
if (hasAlpha && rgba -> a >= HALF_EPSILON) {
unmultipliedRed /= rgba->a;
unmultipliedGreen /= rgba->a;
unmultipliedBlue /= rgba->a;
} }
typename KoRgbTraits<_T_>::Pixel* dst = reinterpret_cast<typename KoRgbTraits<_T_>::Pixel*>(it->rawData()); typename KoRgbTraits<_T_>::Pixel* dst = reinterpret_cast<typename KoRgbTraits<_T_>::Pixel*>(it->rawData());
dst->red = unmultipliedRed; dst->red = rgba->r;
dst->green = unmultipliedGreen; dst->green = rgba->g;
dst->blue = unmultipliedBlue; dst->blue = rgba->b;
if (hasAlpha) { if (hasAlpha) {
dst->alpha = rgba->a; dst->alpha = rgba->a;
} else { } else {
...@@ -442,9 +551,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri) ...@@ -442,9 +551,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
} }
// Create the image // Create the image
m_image = new KisImage(m_doc->createUndoStore(), width, height, colorSpace, ""); m_d->image = new KisImage(m_d->doc->createUndoStore(), width, height, colorSpace, "");
if (!m_image) { if (!m_d->image) {
return KisImageBuilder_RESULT_FAILURE; return KisImageBuilder_RESULT_FAILURE;
} }
...@@ -452,9 +561,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri) ...@@ -452,9 +561,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
for (int i = 0; i < groups.size(); ++i) { for (int i = 0; i < groups.size(); ++i) {
ExrGroupLayerInfo& info = groups[i]; ExrGroupLayerInfo& info = groups[i];
Q_ASSERT(info.parent == 0 || info.parent->groupLayer); Q_ASSERT(info.parent == 0 || info.parent->groupLayer);
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_image->rootLayer(); KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_d->image->rootLayer();
info.groupLayer = new KisGroupLayer(m_image, info.name, OPACITY_OPAQUE_U8); info.groupLayer = new KisGroupLayer(m_d->image, info.name, OPACITY_OPAQUE_U8);
m_image->addNode(info.groupLayer, groupLayerParent); m_d->image->addNode(info.groupLayer, groupLayerParent);
} }
// Load the layers // Load the layers
...@@ -462,7 +571,7 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri) ...@@ -462,7 +571,7 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
ExrPaintLayerInfo& info = informationObjects[i]; ExrPaintLayerInfo& info = informationObjects[i];
if (info.colorSpace) { if (info.colorSpace) {
dbgFile << "Decoding " << info.name << " with " << info.channelMap.size() << " channels, and color space " << info.colorSpace->id(); dbgFile << "Decoding " << info.name << " with " << info.channelMap.size() << " channels, and color space " << info.colorSpace->id();
KisPaintLayerSP layer = new KisPaintLayer(m_image, info.name, OPACITY_OPAQUE_U8, info.colorSpace); KisPaintLayerSP layer = new KisPaintLayer(m_d->image, info.name, OPACITY_OPAQUE_U8, info.colorSpace);
KisTransaction("", layer->paintDevice()); KisTransaction("", layer->paintDevice());
layer->setCompositeOp(COMPOSITE_OVER); layer->setCompositeOp(COMPOSITE_OVER);
...@@ -491,10 +600,10 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri) ...@@ -491,10 +600,10 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
// Decode the data // Decode the data
switch (imageType) { switch (imageType) {
case IT_FLOAT16: case IT_FLOAT16:
decodeData4<half>(file, info, layer, width, dx, dy, height, Imf::HALF); m_d->decodeData4<half>(file, info, layer, width, dx, dy, height, Imf::HALF);
break; break;
case IT_FLOAT32: case IT_FLOAT32:
decodeData4<float>(file, info, layer, width, dx, dy, height, Imf::FLOAT); m_d->decodeData4<float>(file, info, layer, width, dx, dy, height, Imf::FLOAT);
break; break;
case IT_UNKNOWN: case IT_UNKNOWN:
case IT_UNSUPPORTED: case IT_UNSUPPORTED:
...@@ -516,8 +625,8 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri) ...@@ -516,8 +625,8 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
layer->metaData()->addEntry(KisMetaData::Entry(KisMetaData::SchemaRegistry::instance()->create("http://krita.org/exrchannels/1.0/" , "exrchannels"), "channelsmap", values)); layer->metaData()->addEntry(KisMetaData::Entry(KisMetaData::SchemaRegistry::instance()->create("http://krita.org/exrchannels/1.0/" , "exrchannels"), "channelsmap", values));
} }
// Add the layer // Add the layer
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_image->rootLayer(); KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_d->image->rootLayer();
m_image->addNode(layer, groupLayerParent); m_d->image->addNode(layer, groupLayerParent);
} else { } else {
dbgFile << "No decoding " << info.name << " with " << info.channelMap.size() << " channels, and lack of a color space"; dbgFile << "No decoding " << info.name << " with " << info.channelMap.size() << " channels, and lack of a color space";
} }
...@@ -551,7 +660,7 @@ KisImageBuilder_Result exrConverter::buildImage(const KUrl& uri) ...@@ -551,7 +660,7 @@ KisImageBuilder_Result exrConverter::buildImage(const KUrl& uri)
KisImageWSP exrConverter::image() KisImageWSP exrConverter::image()
{ {
return m_image; return m_d->image;
} }
struct ExrPaintLayerSaveInfo { struct ExrPaintLayerSaveInfo {
...@@ -613,19 +722,14 @@ void EncoderImpl<_T_, size, alphaPos>::encodeData(int line) ...@@ -613,19 +722,14 @@ void EncoderImpl<_T_, size, alphaPos>::encodeData(int line)
do { do {
const _T_* dst = reinterpret_cast < const _T_* >(it->oldRawData()); const _T_* dst = reinterpret_cast < const _T_* >(it->oldRawData());
if (alphaPos == -1) { for (int i = 0; i < size; ++i) {
for (int i = 0; i < size; ++i) { rgba->data[i] = dst[i];
rgba->data[i] = dst[i]; }
}
} else { if (alphaPos != -1) {
_T_ alpha = dst[alphaPos]; multiplyAlpha<_T_, ExrPixel, size, alphaPos>(rgba);
for (int i = 0; i < size; ++i) {
if (i != alphaPos) {
rgba->data[i] = dst[i] * alpha;
}
}
rgba->data[alphaPos] = alpha;
} }
++rgba; ++rgba;
} while (it->nextPixel()); } while (it->nextPixel());
} }
...@@ -861,7 +965,7 @@ KisImageBuilder_Result exrConverter::buildFile(const KUrl& uri, KisGroupLayerSP ...@@ -861,7 +965,7 @@ KisImageBuilder_Result exrConverter::buildFile(const KUrl& uri, KisGroupLayerSP
void exrConverter::cancel() void exrConverter::cancel()
{ {
m_stop = true; qWarning() << "WARNING: Cancelling of an EXR loading is not supported!";
} }
#include "exr_converter.moc" #include "exr_converter.moc"
......
...@@ -53,7 +53,7 @@ class exrConverter : public QObject ...@@ -53,7 +53,7 @@ class exrConverter : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
exrConverter(KisDoc2 *doc); exrConverter(KisDoc2 *doc, bool showNotifications);
virtual ~exrConverter(); virtual ~exrConverter();
public: public:
KisImageBuilder_Result buildImage(const KUrl& uri); KisImageBuilder_Result buildImage(const KUrl& uri);
...@@ -68,10 +68,8 @@ private: ...@@ -68,10 +68,8 @@ private:
public slots: public slots:
virtual void cancel(); virtual void cancel();
private: private:
KisImageWSP m_image; struct Private;
KisDoc2 *m_doc; const QScopedPointer<Private> m_d;
bool m_stop;
KIO::TransferJob *m_job;
}; };
#endif #endif
...@@ -103,7 +103,7 @@ KoFilter::ConversionStatus exrExport::convert(const QByteArray& from, const QByt ...@@ -103,7 +103,7 @@ KoFilter::ConversionStatus exrExport::convert(const QByteArray& from, const QByt
KUrl url; KUrl url;
url.setPath(filename); url.setPath(filename);
exrConverter kpc(input); exrConverter kpc(input, !m_chain->manager()->getBatchMode());
if (widget.flatten->isChecked()) { if (widget.flatten->isChecked()) {
image->refreshGraph(); image->refreshGraph();
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include <kpluginfactory.h> #include <kpluginfactory.h>
#include <KoFilterChain.h> #include <KoFilterChain.h>
#include <KoFilterManager.h>
#include <kis_doc2.h> #include <kis_doc2.h>
#include <kis_image.h> #include <kis_image.h>
...@@ -62,7 +63,7 @@ KoFilter::ConversionStatus exrImport::convert(const QByteArray&, const QByteArra ...@@ -62,7 +63,7 @@ KoFilter::ConversionStatus exrImport::convert(const QByteArray&, const QByteArra
if (url.isEmpty()) if (url.isEmpty())
return KoFilter::FileNotFound; return KoFilter::FileNotFound;
exrConverter ib(doc); exrConverter ib(doc, !m_chain->manager()->getBatchMode());
switch (ib.buildImage(url)) { switch (ib.buildImage(url)) {
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <qtest_kde.h> #include <qtest_kde.h>
#include <half.h>
#include "filestest.h" #include "filestest.h"
#ifndef FILES_DATA_DIR #ifndef FILES_DATA_DIR
...@@ -35,6 +35,62 @@ void KisExrTest::testFiles() ...@@ -35,6 +35,62 @@ void KisExrTest::testFiles()
{ {
TestUtil::testFiles(QString(FILES_DATA_DIR) + "/sources", QStringList(), QString(), 1); TestUtil::testFiles(QString(FILES_DATA_DIR) + "/sources", QStringList(), QString(), 1);
} }
void KisExrTest::testRoundTrip()
{
QString inputFileName(TestUtil::fetchDataFileLazy("CandleGlass.exr"));
KisDoc2 doc1;
KoFilterManager manager(&doc1);
manager.setBatchMode(true);
KoFilter::ConversionStatus status;
QString s = manager.importDocument(inputFileName, QString(),
status);
QCOMPARE(status, KoFilter::OK);
QVERIFY(doc1.image());
KTemporaryFile savedFile;
savedFile.setAutoRemove(false);
savedFile.setSuffix(".exr");
savedFile.open();
KUrl savedFileURL("file://" + savedFile.fileName());
QString savedFileName(savedFileURL.toLocalFile());
QString typeName;
KMimeType::Ptr t = KMimeType::findByUrl(savedFileURL, 0, true);
Q_ASSERT(t);
typeName = t->name();
QByteArray mimeType(typeName.toLatin1());
status = manager.exportDocument(savedFileName, mimeType);
QVERIFY(QFileInfo(savedFileName).exists());
{
KisDoc2 doc2;
KoFilterManager manager(&doc2);
manager.setBatchMode(true);
s = manager.importDocument(savedFileName, QString(), status);
QCOMPARE(status, KoFilter::OK);
QVERIFY(doc2.image());
QVERIFY(TestUtil::comparePaintDevicesClever<half>(
doc1.image()->root()->firstChild()->paintDevice(),
doc2.image()->root()->firstChild()->paintDevice(),
0.01 /* meaningless alpha */));
}
savedFile.close();
}