Pipette and "On Preview Widgets" branch. See issue 227
The pipette part is already working quite nice but need to be finished. The widgets part needs more work...
This commit is contained in:
949
rtengine/dcp.cc
Normal file
949
rtengine/dcp.cc
Normal file
@@ -0,0 +1,949 @@
|
||||
/*
|
||||
* This file is part of RawTherapee.
|
||||
*
|
||||
* Copyright (c) 2012 Oliver Duis <www.oliverduis.de>
|
||||
*
|
||||
* RawTherapee is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* RawTherapee is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with RawTherapee. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <cstring>
|
||||
|
||||
#include "dcp.h"
|
||||
#include "safegtk.h"
|
||||
#include "iccmatrices.h"
|
||||
#include "iccstore.h"
|
||||
#include "rawimagesource.h"
|
||||
#include "improcfun.h"
|
||||
#include "rt_math.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace rtengine;
|
||||
using namespace rtexif;
|
||||
|
||||
static void Invert3x3(const double (*A)[3], double (*B)[3]) {
|
||||
|
||||
double a00 = A[0][0];
|
||||
double a01 = A[0][1];
|
||||
double a02 = A[0][2];
|
||||
double a10 = A[1][0];
|
||||
double a11 = A[1][1];
|
||||
double a12 = A[1][2];
|
||||
double a20 = A[2][0];
|
||||
double a21 = A[2][1];
|
||||
double a22 = A[2][2];
|
||||
double temp [3][3];
|
||||
|
||||
temp[0][0] = a11 * a22 - a21 * a12;
|
||||
temp[0][1] = a21 * a02 - a01 * a22;
|
||||
temp[0][2] = a01 * a12 - a11 * a02;
|
||||
temp[1][0] = a20 * a12 - a10 * a22;
|
||||
temp[1][1] = a00 * a22 - a20 * a02;
|
||||
temp[1][2] = a10 * a02 - a00 * a12;
|
||||
temp[2][0] = a10 * a21 - a20 * a11;
|
||||
temp[2][1] = a20 * a01 - a00 * a21;
|
||||
temp[2][2] = a00 * a11 - a10 * a01;
|
||||
|
||||
double det = a00 * temp[0][0] + a01 * temp[1][0] + a02 * temp[2][0];
|
||||
|
||||
if (fabs(det) < 1.0E-10) {
|
||||
abort(); // can't be inverted, we shouldn't be dealing with such matrices
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int k = 0; k < 3; k++) {
|
||||
B[j][k] = temp[j][k] / det;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void Multiply3x3(const double (*A)[3], const double (*B)[3], double (*C)[3]) {
|
||||
|
||||
// use temp to support having output same as input
|
||||
double M[3][3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
M[i][j] = 0;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
M[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
memcpy(C, M, 3 * 3 * sizeof(double));
|
||||
}
|
||||
|
||||
static void Multiply3x3_v3(const double (*A)[3], const double B[3], double C[3]) {
|
||||
|
||||
// use temp to support having output same as input
|
||||
double M[3] = { 0, 0, 0 };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
M[i] += A[i][j] * B[j];
|
||||
}
|
||||
}
|
||||
memcpy(C, M, 3 * sizeof(double));
|
||||
}
|
||||
|
||||
static void Mix3x3(const double (*A)[3], double mulA, const double (*B)[3], double mulB, double (*C)[3]) {
|
||||
|
||||
double M[3][3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
M[i][j] = A[i][j] * mulA + B[i][j] * mulB;
|
||||
}
|
||||
}
|
||||
memcpy(C, M, 3 * 3 * sizeof(double));
|
||||
}
|
||||
|
||||
static void MapWhiteMatrix(const double white1[3], const double white2[3], double (*B)[3]) {
|
||||
|
||||
// code adapted from dng_color_spec::MapWhiteMatrix
|
||||
|
||||
// Use the linearized Bradford adaptation matrix.
|
||||
double Mb[3][3] = { { 0.8951, 0.2664, -0.1614 }, { -0.7502, 1.7135, 0.0367 }, { 0.0389, -0.0685, 1.0296 }};
|
||||
|
||||
double w1[3];
|
||||
Multiply3x3_v3(Mb, white1, w1);
|
||||
double w2[3];
|
||||
Multiply3x3_v3(Mb, white2, w2);
|
||||
|
||||
// Negative white coordinates are kind of meaningless.
|
||||
w1[0] = std::max(w1[0], 0.0);
|
||||
w1[1] = std::max(w1[1], 0.0);
|
||||
w1[2] = std::max(w1[2], 0.0);
|
||||
w2[0] = std::max(w2[0], 0.0);
|
||||
w2[1] = std::max(w2[1], 0.0);
|
||||
w2[2] = std::max(w2[2], 0.0);
|
||||
|
||||
// Limit scaling to something reasonable.
|
||||
double A[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
A[0][0] = std::max(0.1, std::min(w1[0] > 0.0 ? w2[0] / w1[0] : 10.0, 10.0));
|
||||
A[1][1] = std::max(0.1, std::min(w1[1] > 0.0 ? w2[1] / w1[1] : 10.0, 10.0));
|
||||
A[2][2] = std::max(0.1, std::min(w1[2] > 0.0 ? w2[2] / w1[2] : 10.0, 10.0));
|
||||
|
||||
double temp[3][3];
|
||||
Invert3x3(Mb, temp);
|
||||
Multiply3x3(temp, A, temp);
|
||||
Multiply3x3(temp, Mb, B);
|
||||
}
|
||||
|
||||
enum dngCalibrationIlluminant {
|
||||
lsUnknown = 0,
|
||||
lsDaylight = 1,
|
||||
lsFluorescent = 2,
|
||||
lsTungsten = 3,
|
||||
lsFlash = 4,
|
||||
lsFineWeather = 9,
|
||||
lsCloudyWeather = 10,
|
||||
lsShade = 11,
|
||||
lsDaylightFluorescent = 12, // D 5700 - 7100K
|
||||
lsDayWhiteFluorescent = 13, // N 4600 - 5500K
|
||||
lsCoolWhiteFluorescent = 14, // W 3800 - 4500K
|
||||
lsWhiteFluorescent = 15, // WW 3250 - 3800K
|
||||
lsWarmWhiteFluorescent = 16, // L 2600 - 3250K
|
||||
lsStandardLightA = 17,
|
||||
lsStandardLightB = 18,
|
||||
lsStandardLightC = 19,
|
||||
lsD55 = 20,
|
||||
lsD65 = 21,
|
||||
lsD75 = 22,
|
||||
lsD50 = 23,
|
||||
lsISOStudioTungsten = 24,
|
||||
lsOther = 255
|
||||
};
|
||||
|
||||
// should probably be moved to colortemp.cc
|
||||
static double calibrationIlluminantToTemperature(int light) {
|
||||
|
||||
// these temperatures are those found in DNG SDK reference code.
|
||||
switch (light) {
|
||||
case lsStandardLightA:
|
||||
case lsTungsten:
|
||||
return 2850.0;
|
||||
case lsISOStudioTungsten:
|
||||
return 3200.0;
|
||||
case lsD50:
|
||||
return 5000.0;
|
||||
case lsD55:
|
||||
case lsDaylight:
|
||||
case lsFineWeather:
|
||||
case lsFlash:
|
||||
case lsStandardLightB:
|
||||
return 5500.0;
|
||||
case lsD65:
|
||||
case lsStandardLightC:
|
||||
case lsCloudyWeather:
|
||||
return 6500.0;
|
||||
case lsD75:
|
||||
case lsShade:
|
||||
return 7500.0;
|
||||
case lsDaylightFluorescent:
|
||||
return (5700.0 + 7100.0) * 0.5;
|
||||
case lsDayWhiteFluorescent:
|
||||
return (4600.0 + 5500.0) * 0.5;
|
||||
case lsCoolWhiteFluorescent:
|
||||
case lsFluorescent:
|
||||
return (3800.0 + 4500.0) * 0.5;
|
||||
case lsWhiteFluorescent:
|
||||
return (3250.0 + 3800.0) * 0.5;
|
||||
case lsWarmWhiteFluorescent:
|
||||
return (2600.0 + 3250.0) * 0.5;
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void DCPProfile::MakeXYZCAM(ColorTemp &wb, int preferredIlluminant, double (*mXYZCAM)[3]) const
|
||||
{
|
||||
// code adapted from dng_color_spec::FindXYZtoCamera
|
||||
// note that we do not support monochrome or colorplanes > 3 (no reductionMatrix support)
|
||||
// we do not support cameracalibration either
|
||||
|
||||
bool hasFwd1 = hasForwardMatrix1;
|
||||
bool hasFwd2 = hasForwardMatrix2;
|
||||
bool hasCol1 = hasColorMatrix1;
|
||||
bool hasCol2 = hasColorMatrix2;
|
||||
if (preferredIlluminant == 1) {
|
||||
if (hasFwd1) hasFwd2 = false;
|
||||
if (hasCol1) hasCol2 = false;
|
||||
} else if (preferredIlluminant == 2) {
|
||||
if (hasFwd2) hasFwd1 = false;
|
||||
if (hasCol2) hasCol1 = false;
|
||||
}
|
||||
|
||||
// mix if we have two matrices
|
||||
double mix;
|
||||
if (wb.getTemp() <= temperature1) {
|
||||
mix = 1.0;
|
||||
} else if (wb.getTemp() >= temperature2) {
|
||||
mix = 0.0;
|
||||
} else {
|
||||
double invT = 1.0 / wb.getTemp();
|
||||
mix = (invT - (1.0 / temperature2)) / ((1.0 / temperature1) - (1.0 / temperature2));
|
||||
}
|
||||
|
||||
if (hasFwd1 || hasFwd2) {
|
||||
// always prefer ForwardMatrix ahead of ColorMatrix
|
||||
double mFwd[3][3];
|
||||
if (hasFwd1 && hasFwd2) {
|
||||
// interpolate
|
||||
if (mix >= 1.0) {
|
||||
memcpy(mFwd, mForwardMatrix1, sizeof(mFwd));
|
||||
} else if (mix <= 0.0) {
|
||||
memcpy(mFwd, mForwardMatrix2, sizeof(mFwd));
|
||||
} else {
|
||||
Mix3x3(mForwardMatrix1, mix, mForwardMatrix2, 1.0 - mix, mFwd);
|
||||
}
|
||||
} else if (hasFwd1) {
|
||||
memcpy(mFwd, mForwardMatrix1, sizeof(mFwd));
|
||||
} else {
|
||||
memcpy(mFwd, mForwardMatrix2, sizeof(mFwd));
|
||||
}
|
||||
ConvertDNGForwardMatrix2XYZCAM(mFwd,mXYZCAM,wb);
|
||||
} else {
|
||||
// Colormatrix
|
||||
double mCol[3][3];
|
||||
if (hasCol1 && hasCol2) {
|
||||
// interpolate
|
||||
if (mix >= 1.0) {
|
||||
memcpy(mCol, mColorMatrix1, sizeof(mCol));
|
||||
} else if (mix <= 0.0) {
|
||||
memcpy(mCol, mColorMatrix2, sizeof(mCol));
|
||||
} else {
|
||||
Mix3x3(mColorMatrix1, mix, mColorMatrix2, 1.0 - mix, mCol);
|
||||
}
|
||||
} else if (hasCol1) {
|
||||
memcpy(mCol, mColorMatrix1, sizeof(mCol));
|
||||
} else {
|
||||
memcpy(mCol, mColorMatrix2, sizeof(mCol));
|
||||
}
|
||||
ConvertDNGMatrix2XYZCAM(mCol,mXYZCAM);
|
||||
}
|
||||
}
|
||||
|
||||
const DCPProfile::HSBModify* DCPProfile::MakeHueSatMap(ColorTemp &wb, int preferredIlluminant, HSBModify **deleteHandle) const {
|
||||
|
||||
*deleteHandle = NULL;
|
||||
if (!aDeltas1) {
|
||||
return NULL;
|
||||
}
|
||||
if (!aDeltas2) {
|
||||
return aDeltas1;
|
||||
}
|
||||
|
||||
if (preferredIlluminant == 1) {
|
||||
return aDeltas1;
|
||||
} else if (preferredIlluminant == 2) {
|
||||
return aDeltas2;
|
||||
}
|
||||
|
||||
// Interpolate based on color temperature.
|
||||
if (temperature1 <= 0.0 || temperature2 <= 0.0 || temperature1 == temperature2) {
|
||||
return aDeltas1;
|
||||
}
|
||||
bool reverseOrder = temperature1 > temperature2;
|
||||
double t1, t2;
|
||||
if (reverseOrder) {
|
||||
t1 = temperature2;
|
||||
t2 = temperature1;
|
||||
} else {
|
||||
t1 = temperature1;
|
||||
t2 = temperature2;
|
||||
}
|
||||
|
||||
double mix;
|
||||
if (wb.getTemp() <= t1) {
|
||||
mix = 1.0;
|
||||
} else if (wb.getTemp() >= t2) {
|
||||
mix = 0.0;
|
||||
} else {
|
||||
double invT = 1.0 / wb.getTemp();
|
||||
mix = (invT - (1.0 / t2)) / ((1.0 / t1) - (1.0 / t2));
|
||||
}
|
||||
|
||||
if (reverseOrder) {
|
||||
mix = 1.0 - mix;
|
||||
}
|
||||
|
||||
if (mix >= 1.0) {
|
||||
return aDeltas1;
|
||||
} else if (mix <= 0.0) {
|
||||
return aDeltas2;
|
||||
}
|
||||
|
||||
// Interpolate between the tables.
|
||||
HSBModify *aDeltas = new HSBModify[DeltaInfo.iArrayCount];
|
||||
*deleteHandle = aDeltas;
|
||||
float w1 = (float)mix;
|
||||
float w2 = 1.0f - (float)mix;
|
||||
for (int i = 0; i < DeltaInfo.iArrayCount; i++) {
|
||||
aDeltas[i].fHueShift = w1 * aDeltas1[i].fHueShift + w2 * aDeltas2[i].fHueShift;
|
||||
aDeltas[i].fSatScale = w1 * aDeltas1[i].fSatScale + w2 * aDeltas2[i].fSatScale;
|
||||
aDeltas[i].fValScale = w1 * aDeltas1[i].fValScale + w2 * aDeltas2[i].fValScale;
|
||||
}
|
||||
return aDeltas;
|
||||
}
|
||||
|
||||
DCPProfile::DCPProfile(Glib::ustring fname, bool isRTProfile) {
|
||||
const int TIFFFloatSize=4;
|
||||
const int TagColorMatrix1=50721, TagColorMatrix2=50722, TagProfileHueSatMapDims=50937;
|
||||
const int TagForwardMatrix1=50964, TagForwardMatrix2=50965;
|
||||
const int TagProfileHueSatMapData1=50938, TagProfileHueSatMapData2=50939;
|
||||
const int TagCalibrationIlluminant1=50778, TagCalibrationIlluminant2=50779;
|
||||
const int TagProfileLookTableData=50982, TagProfileLookTableDims=50981; // ProfileLookup is the low quality variant
|
||||
const int TagProfileToneCurve=50940;
|
||||
|
||||
aDeltas1=aDeltas2=aLookTable=NULL;
|
||||
|
||||
FILE *pFile = safe_g_fopen(fname, "rb");
|
||||
|
||||
TagDirectory *tagDir=ExifManager::parseTIFF(pFile, false);
|
||||
|
||||
Tag* tag = tagDir->getTag(TagCalibrationIlluminant1); iLightSource1 = (tag!=NULL ? tag->toInt(0,rtexif::SHORT) : -1);
|
||||
tag = tagDir->getTag(TagCalibrationIlluminant2); iLightSource2 = (tag!=NULL ? tag->toInt(0,rtexif::SHORT) : -1);
|
||||
temperature1 = calibrationIlluminantToTemperature(iLightSource1);
|
||||
temperature2 = calibrationIlluminantToTemperature(iLightSource2);
|
||||
|
||||
bool hasSecondHueSat = tagDir->getTag(TagProfileHueSatMapData2)!=NULL; // some profiles have two matrices, but just one huesat
|
||||
|
||||
// Fetch Forward Matrices, if any
|
||||
hasForwardMatrix1 = false;
|
||||
hasForwardMatrix2 = false;
|
||||
hasColorMatrix1 = false;
|
||||
hasColorMatrix2 = false;
|
||||
hasToneCurve = false;
|
||||
tag = tagDir->getTag(TagForwardMatrix1);
|
||||
if (tag) {
|
||||
hasForwardMatrix1 = true;
|
||||
for (int row=0;row<3;row++) {
|
||||
for (int col=0;col<3;col++) {
|
||||
mForwardMatrix1[col][row]=(float)tag->toDouble((col+row*3)*8);
|
||||
}
|
||||
}
|
||||
}
|
||||
tag = tagDir->getTag(TagForwardMatrix2);
|
||||
if (tag) {
|
||||
hasForwardMatrix2 = true;
|
||||
for (int row=0;row<3;row++) {
|
||||
for (int col=0;col<3;col++) {
|
||||
mForwardMatrix2[col][row]=(float)tag->toDouble((col+row*3)*8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color Matrix (1 is always there)
|
||||
tag = tagDir->getTag(TagColorMatrix1);
|
||||
if (!tag) {
|
||||
// FIXME: better error handling
|
||||
fprintf(stderr, "Bad DCP, no ColorMatrix1\n");
|
||||
abort();
|
||||
}
|
||||
hasColorMatrix1 = true;
|
||||
|
||||
for (int row=0;row<3;row++) {
|
||||
for (int col=0;col<3;col++) {
|
||||
mColorMatrix1[col][row]=(float)tag->toDouble((col+row*3)*8);
|
||||
}
|
||||
}
|
||||
|
||||
tag=tagDir->getTag(TagProfileLookTableDims);
|
||||
if (tag!=NULL) {
|
||||
LookInfo.iHueDivisions=tag->toInt(0); LookInfo.iSatDivisions=tag->toInt(4); LookInfo.iValDivisions=tag->toInt(8);
|
||||
|
||||
tag = tagDir->getTag(TagProfileLookTableData);
|
||||
LookInfo.iArrayCount = tag->getCount()/3;
|
||||
|
||||
aLookTable =new HSBModify[LookInfo.iArrayCount];
|
||||
|
||||
for (int i=0;i<LookInfo.iArrayCount;i++) {
|
||||
aLookTable[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
|
||||
aLookTable[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
|
||||
aLookTable[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
|
||||
}
|
||||
|
||||
// precalculated constants for table application
|
||||
LookInfo.pc.hScale = (LookInfo.iHueDivisions < 2) ? 0.0f : (LookInfo.iHueDivisions * (1.0f / 6.0f));
|
||||
LookInfo.pc.sScale = (float) (LookInfo.iSatDivisions - 1);
|
||||
LookInfo.pc.vScale = (float) (LookInfo.iValDivisions - 1);
|
||||
LookInfo.pc.maxHueIndex0 = LookInfo.iHueDivisions - 1;
|
||||
LookInfo.pc.maxSatIndex0 = LookInfo.iSatDivisions - 2;
|
||||
LookInfo.pc.maxValIndex0 = LookInfo.iValDivisions - 2;
|
||||
LookInfo.pc.hueStep = LookInfo.iSatDivisions;
|
||||
LookInfo.pc.valStep = LookInfo.iHueDivisions * LookInfo.pc.hueStep;
|
||||
}
|
||||
|
||||
tag = tagDir->getTag(TagProfileHueSatMapDims);
|
||||
if (tag!=NULL) {
|
||||
DeltaInfo.iHueDivisions=tag->toInt(0); DeltaInfo.iSatDivisions=tag->toInt(4); DeltaInfo.iValDivisions=tag->toInt(8);
|
||||
|
||||
tag = tagDir->getTag(TagProfileHueSatMapData1);
|
||||
DeltaInfo.iArrayCount = tag->getCount()/3;
|
||||
|
||||
aDeltas1=new HSBModify[DeltaInfo.iArrayCount];
|
||||
|
||||
for (int i=0;i<DeltaInfo.iArrayCount;i++) {
|
||||
aDeltas1[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
|
||||
aDeltas1[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
|
||||
aDeltas1[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
|
||||
}
|
||||
|
||||
DeltaInfo.pc.hScale = (DeltaInfo.iHueDivisions < 2) ? 0.0f : (DeltaInfo.iHueDivisions * (1.0f / 6.0f));
|
||||
DeltaInfo.pc.sScale = (float) (DeltaInfo.iSatDivisions - 1);
|
||||
DeltaInfo.pc.vScale = (float) (DeltaInfo.iValDivisions - 1);
|
||||
DeltaInfo.pc.maxHueIndex0 = DeltaInfo.iHueDivisions - 1;
|
||||
DeltaInfo.pc.maxSatIndex0 = DeltaInfo.iSatDivisions - 2;
|
||||
DeltaInfo.pc.maxValIndex0 = DeltaInfo.iValDivisions - 2;
|
||||
DeltaInfo.pc.hueStep = DeltaInfo.iSatDivisions;
|
||||
DeltaInfo.pc.valStep = DeltaInfo.iHueDivisions * DeltaInfo.pc.hueStep;
|
||||
}
|
||||
|
||||
if (iLightSource2!=-1) {
|
||||
// Second matrix
|
||||
tag = tagDir->getTag(TagColorMatrix2);
|
||||
hasColorMatrix2 = true;
|
||||
|
||||
for (int row=0;row<3;row++) {
|
||||
for (int col=0;col<3;col++) {
|
||||
mColorMatrix2[col][row]= (tag!=NULL ? (float)tag->toDouble((col+row*3)*8) : mColorMatrix1[col][row]);
|
||||
}
|
||||
}
|
||||
|
||||
// Second huesatmap
|
||||
if (hasSecondHueSat) {
|
||||
aDeltas2=new HSBModify[DeltaInfo.iArrayCount];
|
||||
|
||||
// Saturation maps. Need to be unwinded.
|
||||
tag = tagDir->getTag(TagProfileHueSatMapData2);
|
||||
|
||||
for (int i=0;i<DeltaInfo.iArrayCount;i++) {
|
||||
aDeltas2[i].fHueShift=tag->toDouble((i*3)*TIFFFloatSize);
|
||||
aDeltas2[i].fSatScale=tag->toDouble((i*3+1)*TIFFFloatSize);
|
||||
aDeltas2[i].fValScale=tag->toDouble((i*3+2)*TIFFFloatSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read tone curve points, if any, but disable to RTs own profiles
|
||||
// the DCP tone curve is subjective and of low quality in comparison to RTs tone curves
|
||||
tag = tagDir->getTag(TagProfileToneCurve);
|
||||
if (tag!=NULL && !isRTProfile) {
|
||||
std::vector<double> cPoints;
|
||||
cPoints.push_back(double(DCT_Spline)); // The first value is the curve type
|
||||
|
||||
// push back each X/Y coordinates in a loop
|
||||
bool curve_is_linear = true;
|
||||
for (int i=0;i<tag->getCount(); i+= 2) {
|
||||
double x = tag->toDouble((i+0)*TIFFFloatSize);
|
||||
double y = tag->toDouble((i+1)*TIFFFloatSize);
|
||||
if (x != y) {
|
||||
curve_is_linear = false;
|
||||
}
|
||||
cPoints.push_back( x );
|
||||
cPoints.push_back( y );
|
||||
}
|
||||
|
||||
if (!curve_is_linear) {
|
||||
// Create the curve
|
||||
DiagonalCurve rawCurve(cPoints, CURVES_MIN_POLY_POINTS);
|
||||
|
||||
toneCurve.Set((Curve*)&rawCurve);
|
||||
hasToneCurve = true;
|
||||
}
|
||||
}
|
||||
|
||||
willInterpolate = false;
|
||||
if (hasForwardMatrix1) {
|
||||
if (hasForwardMatrix2) {
|
||||
if (memcmp(mForwardMatrix1, mForwardMatrix2, sizeof(mForwardMatrix1)) != 0) {
|
||||
// common that forward matrices are the same!
|
||||
willInterpolate = true;
|
||||
}
|
||||
if (aDeltas1 && aDeltas2) {
|
||||
// we assume tables are different
|
||||
willInterpolate = true;
|
||||
}
|
||||
}
|
||||
} else if (hasColorMatrix1 && hasColorMatrix2) {
|
||||
if (memcmp(mColorMatrix1, mColorMatrix2, sizeof(mColorMatrix1)) != 0) {
|
||||
willInterpolate = true;
|
||||
}
|
||||
if (aDeltas1 && aDeltas2) {
|
||||
willInterpolate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pFile!=NULL) fclose(pFile);
|
||||
delete tagDir;
|
||||
}
|
||||
|
||||
DCPProfile::~DCPProfile() {
|
||||
delete[] aDeltas1; delete[] aDeltas2;
|
||||
}
|
||||
|
||||
// Convert DNG color matrix to xyz_cam compatible matrix
|
||||
void DCPProfile::ConvertDNGMatrix2XYZCAM(const double (*mColorMatrix)[3], double (*mXYZCAM)[3]) const {
|
||||
int i,j,k;
|
||||
|
||||
double cam_xyz[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (i=0; i<3; i++)
|
||||
for (j=0; j<3; j++)
|
||||
for (k=0; k<3; k++)
|
||||
cam_xyz[i][j] += mColorMatrix[j][k] * (i==k);
|
||||
|
||||
|
||||
// Multiply out XYZ colorspace
|
||||
double cam_rgb[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (i=0; i < 3; i++)
|
||||
for (j=0; j < 3; j++)
|
||||
for (k=0; k < 3; k++)
|
||||
cam_rgb[i][j] += cam_xyz[i][k] * xyz_sRGB[k][j];
|
||||
|
||||
// Normalize cam_rgb so that: cam_rgb * (1,1,1) is (1,1,1,1)
|
||||
double num;
|
||||
for (i=0; i<3; i++) {
|
||||
for (num=j=0; j<3; j++) num += cam_rgb[i][j];
|
||||
for (j=0; j<3; j++) cam_rgb[i][j] /= num;
|
||||
}
|
||||
|
||||
double rgb_cam[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
RawImageSource::inverse33 (cam_rgb, rgb_cam);
|
||||
|
||||
for (i=0; i<3; i++)
|
||||
for (j=0; j<3; j++) mXYZCAM[i][j]=0;
|
||||
|
||||
for (i=0; i<3; i++)
|
||||
for (j=0; j<3; j++)
|
||||
for (k=0; k<3; k++)
|
||||
mXYZCAM[i][j] += xyz_sRGB[i][k] * rgb_cam[k][j];
|
||||
}
|
||||
|
||||
void DCPProfile::HSDApply(const HSDTableInfo &ti, const HSBModify *tableBase, const float hs, const float ss, const float vs, float &h, float &s, float &v) const {
|
||||
|
||||
// Apply the HueSatMap. Ported from Adobes reference implementation
|
||||
float hueShift, satScale, valScale;
|
||||
|
||||
if (ti.iValDivisions < 2) // Optimize most common case of "2.5D" table.
|
||||
{
|
||||
float hScaled = hs * ti.pc.hScale;
|
||||
float sScaled = ss * ti.pc.sScale;
|
||||
|
||||
int hIndex0 = max((int)hScaled, 0);
|
||||
int sIndex0 = max(min((int)sScaled,ti.pc.maxSatIndex0),0);
|
||||
|
||||
int hIndex1 = hIndex0 + 1;
|
||||
|
||||
if (hIndex0 >= ti.pc.maxHueIndex0)
|
||||
{
|
||||
hIndex0 = ti.pc.maxHueIndex0;
|
||||
hIndex1 = 0;
|
||||
}
|
||||
|
||||
float hFract1 = hScaled - (float) hIndex0;
|
||||
float sFract1 = sScaled - (float) sIndex0;
|
||||
|
||||
float hFract0 = 1.0f - hFract1;
|
||||
float sFract0 = 1.0f - sFract1;
|
||||
|
||||
const HSBModify *entry00 = tableBase + hIndex0 * ti.pc.hueStep + sIndex0;
|
||||
const HSBModify *entry01 = entry00 + (hIndex1 - hIndex0) * ti.pc.hueStep;
|
||||
|
||||
float hueShift0 = hFract0 * entry00->fHueShift + hFract1 * entry01->fHueShift;
|
||||
float satScale0 = hFract0 * entry00->fSatScale + hFract1 * entry01->fSatScale;
|
||||
float valScale0 = hFract0 * entry00->fValScale + hFract1 * entry01->fValScale;
|
||||
|
||||
entry00++;
|
||||
entry01++;
|
||||
|
||||
float hueShift1 = hFract0 * entry00->fHueShift +
|
||||
hFract1 * entry01->fHueShift;
|
||||
|
||||
float satScale1 = hFract0 * entry00->fSatScale +
|
||||
hFract1 * entry01->fSatScale;
|
||||
|
||||
float valScale1 = hFract0 * entry00->fValScale +
|
||||
hFract1 * entry01->fValScale;
|
||||
|
||||
hueShift = sFract0 * hueShift0 + sFract1 * hueShift1;
|
||||
satScale = sFract0 * satScale0 + sFract1 * satScale1;
|
||||
valScale = sFract0 * valScale0 + sFract1 * valScale1;
|
||||
|
||||
} else {
|
||||
|
||||
float hScaled = hs * ti.pc.hScale;
|
||||
float sScaled = ss * ti.pc.sScale;
|
||||
float vScaled = vs * ti.pc.vScale;
|
||||
|
||||
int hIndex0 = (int) hScaled;
|
||||
int sIndex0 = max(min((int)sScaled,ti.pc.maxSatIndex0),0);
|
||||
int vIndex0 = max(min((int)vScaled,ti.pc.maxValIndex0),0);
|
||||
|
||||
int hIndex1 = hIndex0 + 1;
|
||||
|
||||
if (hIndex0 >= ti.pc.maxHueIndex0)
|
||||
{
|
||||
hIndex0 = ti.pc.maxHueIndex0;
|
||||
hIndex1 = 0;
|
||||
}
|
||||
|
||||
float hFract1 = hScaled - (float) hIndex0;
|
||||
float sFract1 = sScaled - (float) sIndex0;
|
||||
float vFract1 = vScaled - (float) vIndex0;
|
||||
|
||||
float hFract0 = 1.0f - hFract1;
|
||||
float sFract0 = 1.0f - sFract1;
|
||||
float vFract0 = 1.0f - vFract1;
|
||||
|
||||
const HSBModify *entry00 = tableBase + vIndex0 * ti.pc.valStep + hIndex0 * ti.pc.hueStep + sIndex0;
|
||||
|
||||
const HSBModify *entry01 = entry00 + (hIndex1 - hIndex0) * ti.pc.hueStep;
|
||||
|
||||
const HSBModify *entry10 = entry00 + ti.pc.valStep;
|
||||
const HSBModify *entry11 = entry01 + ti.pc.valStep;
|
||||
|
||||
float hueShift0 = vFract0 * (hFract0 * entry00->fHueShift +
|
||||
hFract1 * entry01->fHueShift) +
|
||||
vFract1 * (hFract0 * entry10->fHueShift +
|
||||
hFract1 * entry11->fHueShift);
|
||||
|
||||
float satScale0 = vFract0 * (hFract0 * entry00->fSatScale +
|
||||
hFract1 * entry01->fSatScale) +
|
||||
vFract1 * (hFract0 * entry10->fSatScale +
|
||||
hFract1 * entry11->fSatScale);
|
||||
|
||||
float valScale0 = vFract0 * (hFract0 * entry00->fValScale +
|
||||
hFract1 * entry01->fValScale) +
|
||||
vFract1 * (hFract0 * entry10->fValScale +
|
||||
hFract1 * entry11->fValScale);
|
||||
|
||||
entry00++;
|
||||
entry01++;
|
||||
entry10++;
|
||||
entry11++;
|
||||
|
||||
float hueShift1 = vFract0 * (hFract0 * entry00->fHueShift +
|
||||
hFract1 * entry01->fHueShift) +
|
||||
vFract1 * (hFract0 * entry10->fHueShift +
|
||||
hFract1 * entry11->fHueShift);
|
||||
|
||||
float satScale1 = vFract0 * (hFract0 * entry00->fSatScale +
|
||||
hFract1 * entry01->fSatScale) +
|
||||
vFract1 * (hFract0 * entry10->fSatScale +
|
||||
hFract1 * entry11->fSatScale);
|
||||
|
||||
float valScale1 = vFract0 * (hFract0 * entry00->fValScale +
|
||||
hFract1 * entry01->fValScale) +
|
||||
vFract1 * (hFract0 * entry10->fValScale +
|
||||
hFract1 * entry11->fValScale);
|
||||
|
||||
hueShift = sFract0 * hueShift0 + sFract1 * hueShift1;
|
||||
satScale = sFract0 * satScale0 + sFract1 * satScale1;
|
||||
valScale = sFract0 * valScale0 + sFract1 * valScale1;
|
||||
}
|
||||
|
||||
hueShift *= (6.0f / 360.0f); // Convert to internal hue range.
|
||||
|
||||
h += hueShift;
|
||||
s *= satScale; // no clipping here, we are RT float :-)
|
||||
v *= valScale;
|
||||
}
|
||||
|
||||
// Convert DNG forward matrix to xyz_cam compatible matrix
|
||||
void DCPProfile::ConvertDNGForwardMatrix2XYZCAM(const double (*mForwardMatrix)[3], double (*mXYZCAM)[3], ColorTemp &wb) const {
|
||||
|
||||
// Convert ForwardMatrix (white-balanced CameraRGB -> XYZ D50 matrix)
|
||||
// into a ColorMatrix (XYZ -> CameraRGB)
|
||||
|
||||
double X, Z;
|
||||
ColorTemp::temp2mulxyz(wb.getTemp(), wb.getGreen(), wb.getMethod(), X, Z);
|
||||
|
||||
const double white_xyz[3] = { X, 1, Z };
|
||||
const double white_d50[3] = { 0.3457, 0.3585, 0.2958 }; // D50
|
||||
|
||||
// Cancel out the white balance to get a CameraRGB -> XYZ D50 matrixx (CameraToPCS in dng terminology)
|
||||
double whiteDiag[3][3] = {{white_xyz[0], 0, 0}, {0, white_xyz[1], 0}, {0, 0, white_xyz[2]}};
|
||||
double whiteDiagInv[3][3];
|
||||
Invert3x3(whiteDiag, whiteDiagInv);
|
||||
double rgb2xyzD50[3][3];
|
||||
Multiply3x3(mForwardMatrix, whiteDiagInv, rgb2xyzD50);
|
||||
|
||||
// Through chromatic adaptation convert XYZ D50 to XYZ camera white
|
||||
double whiteMatrix[3][3];
|
||||
MapWhiteMatrix(white_d50, white_xyz, whiteMatrix);
|
||||
double rgb2xyz[3][3];
|
||||
Multiply3x3(rgb2xyzD50, whiteMatrix, rgb2xyz);
|
||||
|
||||
// Now we have CameraRGB -> XYZ, invert so we get XYZ -> CameraRGB (ColorMatrix format)
|
||||
double dngColorMatrix[3][3];
|
||||
Invert3x3(rgb2xyz, dngColorMatrix);
|
||||
|
||||
// now we can run the ordinary ColorMatrix conversion
|
||||
ConvertDNGMatrix2XYZCAM(dngColorMatrix, mXYZCAM);
|
||||
}
|
||||
|
||||
void DCPProfile::Apply(Imagefloat *pImg, int preferredIlluminant, Glib::ustring workingSpace, ColorTemp &wb, float rawWhiteFac, bool useToneCurve) const {
|
||||
|
||||
TMatrix mWork = iccStore->workingSpaceInverseMatrix (workingSpace);
|
||||
|
||||
double mXYZCAM[3][3];
|
||||
MakeXYZCAM(wb, preferredIlluminant, mXYZCAM);
|
||||
HSBModify *deleteTableHandle;
|
||||
const HSBModify *deltaBase = MakeHueSatMap(wb, preferredIlluminant, &deleteTableHandle);
|
||||
|
||||
useToneCurve&=toneCurve;
|
||||
|
||||
if (deltaBase == NULL && aLookTable == NULL && !useToneCurve) {
|
||||
//===== The fast path: no LUT and not tone curve- Calculate matrix for direct conversion raw>working space
|
||||
double mat[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (int i=0; i<3; i++)
|
||||
for (int j=0; j<3; j++)
|
||||
for (int k=0; k<3; k++)
|
||||
mat[i][j] += mWork[i][k] * mXYZCAM[k][j];
|
||||
|
||||
// Apply the matrix part
|
||||
#pragma omp parallel for
|
||||
for (int y=0; y<pImg->height; y++) {
|
||||
float newr, newg, newb;
|
||||
for (int x=0; x<pImg->width; x++) {
|
||||
newr = mat[0][0]*pImg->r(y,x) + mat[0][1]*pImg->g(y,x) + mat[0][2]*pImg->b(y,x);
|
||||
newg = mat[1][0]*pImg->r(y,x) + mat[1][1]*pImg->g(y,x) + mat[1][2]*pImg->b(y,x);
|
||||
newb = mat[2][0]*pImg->r(y,x) + mat[2][1]*pImg->g(y,x) + mat[2][2]*pImg->b(y,x);
|
||||
|
||||
pImg->r(y,x) = newr; pImg->g(y,x) = newg; pImg->b(y,x) = newb;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
//===== LUT available- Calculate matrix for conversion raw>ProPhoto
|
||||
double m2ProPhoto[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (int i=0; i<3; i++)
|
||||
for (int j=0; j<3; j++)
|
||||
for (int k=0; k<3; k++)
|
||||
m2ProPhoto[i][j] += prophoto_xyz[i][k] * mXYZCAM[k][j];
|
||||
|
||||
double m2Work[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (int i=0; i<3; i++)
|
||||
for (int j=0; j<3; j++)
|
||||
for (int k=0; k<3; k++)
|
||||
m2Work[i][j] += mWork[i][k] * xyz_prophoto[k][j];
|
||||
|
||||
bool useRawWhite=fabs(rawWhiteFac)>0.001;
|
||||
|
||||
// Convert to prophoto and apply LUT
|
||||
#pragma omp parallel for
|
||||
for (int y=0; y<pImg->height; y++) {
|
||||
float newr, newg, newb, h,s,v,hs,ss,vs;
|
||||
for (int x=0; x<pImg->width; x++) {
|
||||
newr = m2ProPhoto[0][0]*pImg->r(y,x) + m2ProPhoto[0][1]*pImg->g(y,x) + m2ProPhoto[0][2]*pImg->b(y,x);
|
||||
newg = m2ProPhoto[1][0]*pImg->r(y,x) + m2ProPhoto[1][1]*pImg->g(y,x) + m2ProPhoto[1][2]*pImg->b(y,x);
|
||||
newb = m2ProPhoto[2][0]*pImg->r(y,x) + m2ProPhoto[2][1]*pImg->g(y,x) + m2ProPhoto[2][2]*pImg->b(y,x);
|
||||
|
||||
// if point is in negative area, just the matrix, but not the LUT
|
||||
if ((deltaBase || aLookTable) && newr>=0 && newg>=0 && newb>=0) {
|
||||
Color::rgb2hsv(newr, newg, newb, h , s, v);
|
||||
h*=6.f; // RT calculates in [0,1]
|
||||
|
||||
if (useRawWhite) {
|
||||
// Retro-calculate what the point was like before RAW white came in
|
||||
Color::rgb2hsv(newr/rawWhiteFac, newg/rawWhiteFac, newb/rawWhiteFac, hs, ss, vs);
|
||||
hs*=6.f; // RT calculates in [0,1]
|
||||
} else {
|
||||
hs=h; ss=s; vs=v;
|
||||
}
|
||||
|
||||
if (deltaBase) {
|
||||
HSDApply(DeltaInfo, deltaBase, hs, ss, vs, h, s, v);
|
||||
}
|
||||
if (aLookTable) {
|
||||
HSDApply(LookInfo, aLookTable, hs, ss, vs, h, s, v);
|
||||
}
|
||||
|
||||
// RT range correction
|
||||
if (h < 0.0f) h += 6.0f;
|
||||
if (h >= 6.0f) h -= 6.0f;
|
||||
h/=6.f;
|
||||
Color::hsv2rgb( h, s, v, newr, newg, newb);
|
||||
}
|
||||
|
||||
// tone curve
|
||||
if (useToneCurve) toneCurve.Apply(newr, newg, newb);
|
||||
|
||||
pImg->r(y,x) = m2Work[0][0]*newr + m2Work[0][1]*newg + m2Work[0][2]*newb;
|
||||
pImg->g(y,x) = m2Work[1][0]*newr + m2Work[1][1]*newg + m2Work[1][2]*newb;
|
||||
pImg->b(y,x) = m2Work[2][0]*newr + m2Work[2][1]*newg + m2Work[2][2]*newb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteTableHandle) delete[] deleteTableHandle;
|
||||
}
|
||||
|
||||
// Integer variant is legacy, only used for thumbs. Simply take the matrix here
|
||||
void DCPProfile::Apply(Image16 *pImg, int preferredIlluminant, Glib::ustring workingSpace, ColorTemp &wb, bool useToneCurve) const {
|
||||
TMatrix mWork = iccStore->workingSpaceInverseMatrix (workingSpace);
|
||||
|
||||
double mXYZCAM[3][3];
|
||||
MakeXYZCAM(wb, preferredIlluminant, mXYZCAM);
|
||||
|
||||
useToneCurve&=toneCurve;
|
||||
|
||||
// Calculate matrix for direct conversion raw>working space
|
||||
double mat[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}};
|
||||
for (int i=0; i<3; i++)
|
||||
for (int j=0; j<3; j++)
|
||||
for (int k=0; k<3; k++)
|
||||
mat[i][j] += mWork[i][k] * mXYZCAM[k][j];
|
||||
|
||||
// Apply the matrix part
|
||||
#pragma omp parallel for
|
||||
for (int y=0; y<pImg->height; y++) {
|
||||
float newr, newg, newb;
|
||||
for (int x=0; x<pImg->width; x++) {
|
||||
newr = mat[0][0]*pImg->r(y,x) + mat[0][1]*pImg->g(y,x) + mat[0][2]*pImg->b(y,x);
|
||||
newg = mat[1][0]*pImg->r(y,x) + mat[1][1]*pImg->g(y,x) + mat[1][2]*pImg->b(y,x);
|
||||
newb = mat[2][0]*pImg->r(y,x) + mat[2][1]*pImg->g(y,x) + mat[2][2]*pImg->b(y,x);
|
||||
|
||||
// tone curve
|
||||
if (useToneCurve) toneCurve.Apply(newr, newg, newb);
|
||||
|
||||
pImg->r(y,x) = CLIP((int)newr); pImg->g(y,x) = CLIP((int)newg); pImg->b(y,x) = CLIP((int)newb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Generates as singleton
|
||||
DCPStore* DCPStore::getInstance()
|
||||
{
|
||||
static DCPStore* instance_ = 0;
|
||||
if ( instance_ == 0 )
|
||||
{
|
||||
static MyMutex smutex_;
|
||||
MyMutex::MyLock lock(smutex_);
|
||||
if ( instance_ == 0 )
|
||||
{
|
||||
instance_ = new DCPStore();
|
||||
}
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Reads all profiles from the given profiles dir
|
||||
void DCPStore::init (Glib::ustring rtProfileDir) {
|
||||
MyMutex::MyLock lock(mtx);
|
||||
|
||||
fileStdProfiles.clear();
|
||||
|
||||
Glib::ustring rootDirName=rtProfileDir;
|
||||
|
||||
if (rootDirName!="") {
|
||||
std::deque<Glib::ustring> qDirs;
|
||||
|
||||
qDirs.push_front(rootDirName);
|
||||
|
||||
while (!qDirs.empty()) {
|
||||
// process directory
|
||||
Glib::ustring dirname = qDirs.back();
|
||||
qDirs.pop_back();
|
||||
|
||||
Glib::Dir* dir = NULL;
|
||||
try {
|
||||
if (!safe_file_test (dirname, Glib::FILE_TEST_IS_DIR)) return;
|
||||
dir = new Glib::Dir (dirname);
|
||||
}
|
||||
catch (Glib::Exception& fe) {
|
||||
return;
|
||||
}
|
||||
dirname = dirname + "/";
|
||||
for (Glib::DirIterator i = dir->begin(); i!=dir->end(); ++i) {
|
||||
Glib::ustring fname = dirname + *i;
|
||||
Glib::ustring sname = *i;
|
||||
// ignore directories
|
||||
if (!safe_file_test (fname, Glib::FILE_TEST_IS_DIR)) {
|
||||
size_t lastdot = sname.find_last_of ('.');
|
||||
if (lastdot!=Glib::ustring::npos && lastdot<=sname.size()-4 && (!sname.casefold().compare (lastdot, 4, ".dcp"))) {
|
||||
Glib::ustring camShortName = sname.substr(0,lastdot).uppercase();
|
||||
fileStdProfiles[camShortName]=fname; // they will be loaded and cached on demand
|
||||
}
|
||||
} else qDirs.push_front(fname); // for later scanning
|
||||
}
|
||||
delete dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DCPProfile* DCPStore::getProfile (Glib::ustring filename, bool isRTProfile) {
|
||||
MyMutex::MyLock lock(mtx);
|
||||
|
||||
std::map<Glib::ustring, DCPProfile*>::iterator r = profileCache.find (filename);
|
||||
if (r!=profileCache.end()) return r->second;
|
||||
|
||||
// Add profile
|
||||
profileCache[filename]=new DCPProfile(filename, isRTProfile);
|
||||
|
||||
return profileCache[filename];
|
||||
}
|
||||
|
||||
DCPProfile* DCPStore::getStdProfile(Glib::ustring camShortName) {
|
||||
Glib::ustring name2=camShortName.uppercase();
|
||||
|
||||
// Warning: do NOT use map.find(), since it does not seem to work reliably here
|
||||
for (std::map<Glib::ustring, Glib::ustring>::iterator i=fileStdProfiles.begin();i!=fileStdProfiles.end();i++)
|
||||
if (name2==(*i).first) return getProfile((*i).second, true);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool DCPStore::isValidDCPFileName(Glib::ustring filename) const {
|
||||
if (!safe_file_test (filename, Glib::FILE_TEST_EXISTS) || safe_file_test (filename, Glib::FILE_TEST_IS_DIR)) return false;
|
||||
size_t pos=filename.find_last_of ('.');
|
||||
return pos>0 && (!filename.casefold().compare (pos, 4, ".dcp") || !filename.casefold().compare (pos, 4, ".dng"));
|
||||
}
|
||||
Reference in New Issue
Block a user