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} )
......
......@@ -28,9 +28,9 @@
#include <ImfOutputFile.h>
#include <kapplication.h>
#include <kmessagebox.h>
#include <kio/netaccess.h>
#include <kio/deletejob.h>
#include <KoColorSpaceRegistry.h>
#include <KoCompositeOpRegistry.h>
......@@ -51,55 +51,6 @@
#include <metadata/kis_meta_data_store.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_>
struct Rgba {
......@@ -124,6 +75,13 @@ struct ExrGroupLayerInfo : public ExrLayerInfoBase {
KisGroupLayerSP groupLayer;
};
enum ImageType {
IT_UNKNOWN,
IT_FLOAT16,
IT_FLOAT32,
IT_UNSUPPORTED
};
struct ExrPaintLayerInfo : public ExrLayerInfoBase {
ExrPaintLayerInfo() : imageType(IT_UNKNOWN) {
}
......@@ -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_>
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
KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width);
do {
// XXX: For now unmultiply the alpha, though compositing will be faster if we
// keep it premultiplied.
_T_ unmultipliedRed = *rgba;
_T_* dst = reinterpret_cast<_T_*>(it->rawData());
......@@ -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_>
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;
......@@ -222,22 +338,15 @@ void decodeData4(Imf::InputFile& file, ExrPaintLayerInfo& info, KisPaintLayerSP
KisHLineIteratorSP it = layer->paintDevice()->createHLineIteratorNG(0, y, width);
do {
// XXX: For now unmultiply the alpha, though compositing will be faster if we
// keep it premultiplied.
_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;
if (hasAlpha) {
unmultiplyAlpha(rgba);
}
typename KoRgbTraits<_T_>::Pixel* dst = reinterpret_cast<typename KoRgbTraits<_T_>::Pixel*>(it->rawData());
dst->red = unmultipliedRed;
dst->green = unmultipliedGreen;
dst->blue = unmultipliedBlue;
dst->red = rgba->r;
dst->green = rgba->g;
dst->blue = rgba->b;
if (hasAlpha) {
dst->alpha = rgba->a;
} else {
......@@ -442,9 +551,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
}
// 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;
}
......@@ -452,9 +561,9 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
for (int i = 0; i < groups.size(); ++i) {
ExrGroupLayerInfo& info = groups[i];
Q_ASSERT(info.parent == 0 || info.parent->groupLayer);
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_image->rootLayer();
info.groupLayer = new KisGroupLayer(m_image, info.name, OPACITY_OPAQUE_U8);
m_image->addNode(info.groupLayer, groupLayerParent);
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_d->image->rootLayer();
info.groupLayer = new KisGroupLayer(m_d->image, info.name, OPACITY_OPAQUE_U8);
m_d->image->addNode(info.groupLayer, groupLayerParent);
}
// Load the layers
......@@ -462,7 +571,7 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
ExrPaintLayerInfo& info = informationObjects[i];
if (info.colorSpace) {
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());
layer->setCompositeOp(COMPOSITE_OVER);
......@@ -491,10 +600,10 @@ KisImageBuilder_Result exrConverter::decode(const KUrl& uri)
// Decode the data
switch (imageType) {
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;
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;
case IT_UNKNOWN:
case IT_UNSUPPORTED:
......@@ -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));
}
// Add the layer
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_image->rootLayer();
m_image->addNode(layer, groupLayerParent);
KisGroupLayerSP groupLayerParent = (info.parent) ? info.parent->groupLayer : m_d->image->rootLayer();
m_d->image->addNode(layer, groupLayerParent);
} else {
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)
KisImageWSP exrConverter::image()
{
return m_image;
return m_d->image;
}
struct ExrPaintLayerSaveInfo {
......@@ -613,19 +722,14 @@ void EncoderImpl<_T_, size, alphaPos>::encodeData(int line)
do {
const _T_* dst = reinterpret_cast < const _T_* >(it->oldRawData());
if (alphaPos == -1) {
for (int i = 0; i < size; ++i) {
rgba->data[i] = dst[i];
}
} else {
_T_ alpha = dst[alphaPos];
for (int i = 0; i < size; ++i) {
if (i != alphaPos) {
rgba->data[i] = dst[i] * alpha;
}
}
rgba->data[alphaPos] = alpha;
for (int i = 0; i < size; ++i) {
rgba->data[i] = dst[i];
}
if (alphaPos != -1) {
multiplyAlpha<_T_, ExrPixel, size, alphaPos>(rgba);
}
++rgba;
} while (it->nextPixel());
}
......@@ -861,7 +965,7 @@ KisImageBuilder_Result exrConverter::buildFile(const KUrl& uri, KisGroupLayerSP
void exrConverter::cancel()
{
m_stop = true;
qWarning() << "WARNING: Cancelling of an EXR loading is not supported!";
}
#include "exr_converter.moc"
......
......@@ -53,7 +53,7 @@ class exrConverter : public QObject
{
Q_OBJECT
public:
exrConverter(KisDoc2 *doc);
exrConverter(KisDoc2 *doc, bool showNotifications);
virtual ~exrConverter();
public:
KisImageBuilder_Result buildImage(const KUrl& uri);
......@@ -68,10 +68,8 @@ private:
public slots:
virtual void cancel();
private:
KisImageWSP m_image;
KisDoc2 *m_doc;
bool m_stop;
KIO::TransferJob *m_job;
struct Private;
const QScopedPointer<Private> m_d;
};
#endif
......@@ -103,7 +103,7 @@ KoFilter::ConversionStatus exrExport::convert(const QByteArray& from, const QByt
KUrl url;
url.setPath(filename);
exrConverter kpc(input);
exrConverter kpc(input, !m_chain->manager()->getBatchMode());
if (widget.flatten->isChecked()) {
image->refreshGraph();
......
......@@ -22,6 +22,7 @@
#include <kpluginfactory.h>
#include <KoFilterChain.h>
#include <KoFilterManager.h>
#include <kis_doc2.h>
#include <kis_image.h>
......@@ -62,7 +63,7 @@ KoFilter::ConversionStatus exrImport::convert(const QByteArray&, const QByteArra
if (url.isEmpty())
return KoFilter::FileNotFound;
exrConverter ib(doc);
exrConverter ib(doc, !m_chain->manager()->getBatchMode());
switch (ib.buildImage(url)) {
......
......@@ -23,7 +23,7 @@
#include <QCoreApplication>
#include <qtest_kde.h>
#include <half.h>
#include "filestest.h"
#ifndef FILES_DATA_DIR
......@@ -35,6 +35,62 @@ void KisExrTest::testFiles()
{
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();
}
QTEST_KDEMAIN(KisExrTest, GUI)
#include "kis_exr_test.moc"
......@@ -26,6 +26,7 @@ class KisExrTest : public QObject
Q_OBJECT
private slots:
void testFiles();
void testRoundTrip();
};
#endif
......@@ -216,6 +216,45 @@ inline bool comparePaintDevices(QPoint & pt, const KisPaintDeviceSP dev1, const
return true;
}
template <typename channel_type>
inline bool comparePaintDevicesClever(const KisPaintDeviceSP dev1, const KisPaintDeviceSP dev2, channel_type alphaThreshold = 0)
{
QRect rc1 = dev1->exactBounds();
QRect rc2 = dev2->exactBounds();
if (rc1 != rc2) {
qDebug() << "Devices have different size" << ppVar(rc1) << ppVar(rc2);
return false;
}
KisHLineConstIteratorSP iter1 = dev1->createHLineConstIteratorNG(0, 0, rc1.width());
KisHLineConstIteratorSP iter2 = dev2->createHLineConstIteratorNG(0, 0, rc1.width());
int pixelSize = dev1->pixelSize();
for (int y = 0; y < rc1.height(); ++y) {
do {
if (memcmp(iter1->oldRawData(), iter2->oldRawData(), pixelSize) != 0) {
const channel_type* p1 = reinterpret_cast<const channel_type*>(iter1->oldRawData());
const channel_type* p2 = reinterpret_cast<const channel_type*>(iter2->oldRawData());
if (p1[3] < alphaThreshold && p2[3] < alphaThreshold) continue;
qDebug() << "Failed compare paint devices:" << iter1->x() << iter1->y();
qDebug() << "src:" << p1[0] << p1[1] << p1[2] << p1[3];
qDebug() << "dst:" << p2[0] << p2[1] << p2[2] << p2[3];
return false;
}
} while (iter1->nextPixel() && iter2->nextPixel());
iter1->nextRow();
iter2->nextRow();
}
return true;
}
#ifdef FILES_OUTPUT_DIR
inline bool checkQImage(const QImage &image, const QString &testName,
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment