1 //---------------------------------------------------------------------------------
3 // Little Color Management System
4 // Copyright (c) 1998-2010 Marti Maria Saguer
6 // Permission is hereby granted, free of charge, to any person obtaining
7 // a copy of this software and associated documentation files (the "Software"),
8 // to deal in the Software without restriction, including without limitation
9 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 // and/or sell copies of the Software, and to permit persons to whom the Software
11 // is furnished to do so, subject to the following conditions:
13 // The above copyright notice and this permission notice shall be included in
14 // all copies or substantial portions of the Software.
16 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
18 // THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 //---------------------------------------------------------------------------------
31 // ------------------------------------------------------------------------
33 static TIFF
*Tiff1
, *Tiff2
, *TiffDiff
;
34 static const char* TiffDiffFilename
;
35 static const char* CGATSout
;
44 static STAT ColorantStat
[4];
45 static STAT EuclideanStat
;
46 static STAT ColorimetricStat
;
48 static uint16 Channels
;
50 static cmsHPROFILE hLab
;
54 void ConsoleWarningHandler(const char* module
, const char* fmt
, va_list ap
)
56 char e
[512] = { '\0' };
58 strcat(strcpy(e
, module
), ": ");
60 vsprintf(e
+strlen(e
), fmt
, ap
);
64 fprintf(stderr
, "\nWarning");
65 fprintf(stderr
, " %s\n", e
);
71 void ConsoleErrorHandler(const char* module
, const char* fmt
, va_list ap
)
73 char e
[512] = { '\0' };
76 strcat(strcpy(e
, module
), ": ");
78 vsprintf(e
+strlen(e
), fmt
, ap
);
80 fprintf(stderr
, "\nError");
81 fprintf(stderr
, " %s\n", e
);
90 fprintf(stderr
, "Little cms TIFF compare utility. v1.0\n\n");
92 fprintf(stderr
, "usage: tiffdiff [flags] input.tif output.tif\n");
94 fprintf(stderr
, "\nflags:\n\n");
97 fprintf(stderr
, "%co<tiff> - Output TIFF file\n", SW
);
98 fprintf(stderr
, "%cg<CGATS> - Output results in CGATS file\n", SW
);
100 fprintf(stderr
, "\n");
102 fprintf(stderr
, "%cv - Verbose (show warnings)\n", SW
);
103 fprintf(stderr
, "%ch - This help\n", SW
);
115 void HandleSwitches(int argc
, char *argv
[])
119 while ((s
=xgetopt(argc
,argv
,"o:O:hHvVg:G:")) != EOF
) {
131 TiffDiffFilename
= xoptarg
;
147 FatalError("Unknown option - run without args to see valid ones");
154 void ClearStatistics(LPSTAT st
)
157 st
->n
= st
->x
= st
->x2
= st
->Peak
= 0;
164 void AddOnePixel(LPSTAT st
, double dE
)
167 st
-> x
+= dE
; st
->x2
+= (dE
* dE
); st
->n
+= 1.0;
168 if (dE
> st
->Peak
) st
->Peak
= dE
;
169 if (dE
< st
->Min
) st
->Min
= dE
;
173 double Std(LPSTAT st
)
175 return sqrt((st
->n
* st
->x2
- st
->x
* st
->x
) / (st
->n
*(st
->n
-1)));
179 double Mean(LPSTAT st
)
181 return st
->x
/st
->n
;
185 // Build up the pixeltype descriptor
188 cmsUInt32Number
GetInputPixelType(TIFF
*Bank
)
190 uint16 Photometric
, bps
, spp
, extra
, PlanarConfig
, *info
;
191 uint16 Compression
, reverse
= 0;
192 int ColorChannels
, IsPlanar
= 0, pt
= 0;
194 TIFFGetField(Bank
, TIFFTAG_PHOTOMETRIC
, &Photometric
);
195 TIFFGetFieldDefaulted(Bank
, TIFFTAG_BITSPERSAMPLE
, &bps
);
198 FatalError("Sorry, bilevel TIFFs has nothig to do with ICC profiles");
200 if (bps
!= 8 && bps
!= 16)
201 FatalError("Sorry, 8 or 16 bits per sample only");
203 TIFFGetFieldDefaulted(Bank
, TIFFTAG_SAMPLESPERPIXEL
, &spp
);
204 TIFFGetFieldDefaulted(Bank
, TIFFTAG_PLANARCONFIG
, &PlanarConfig
);
206 switch (PlanarConfig
)
208 case PLANARCONFIG_CONTIG
: IsPlanar
= 0; break;
209 case PLANARCONFIG_SEPARATE
: FatalError("Planar TIFF are not supported");
212 FatalError("Unsupported planar configuration (=%d) ", (int) PlanarConfig
);
215 // If Samples per pixel == 1, PlanarConfiguration is irrelevant and need
216 // not to be included.
218 if (spp
== 1) IsPlanar
= 0;
223 TIFFGetFieldDefaulted(Bank
, TIFFTAG_EXTRASAMPLES
, &extra
, &info
);
226 ColorChannels
= spp
- extra
;
228 switch (Photometric
) {
230 case PHOTOMETRIC_MINISWHITE
:
234 case PHOTOMETRIC_MINISBLACK
:
239 case PHOTOMETRIC_RGB
:
245 case PHOTOMETRIC_PALETTE
:
247 FatalError("Sorry, palette images not supported (at least on this version)");
249 case PHOTOMETRIC_SEPARATED
:
250 pt
= PixelTypeFromChanCount(ColorChannels
);
253 case PHOTOMETRIC_YCBCR
:
254 TIFFGetField(Bank
, TIFFTAG_COMPRESSION
, &Compression
);
259 TIFFGetFieldDefaulted(Bank
, TIFFTAG_YCBCRSUBSAMPLING
, &subx
, &suby
);
260 if (subx
!= 1 || suby
!= 1)
261 FatalError("Sorry, subsampled images not supported");
267 case PHOTOMETRIC_CIELAB
:
272 case PHOTOMETRIC_LOGLUV
: /* CIE Log2(L) (u',v') */
274 TIFFSetField(Bank
, TIFFTAG_SGILOGDATAFMT
, SGILOGDATAFMT_16BIT
);
275 pt
= PT_YUV
; // *ICCSpace = icSigLuvData;
276 bps
= 16; // 16 bits forced by LibTiff
280 FatalError("Unsupported TIFF color space (Photometric %d)", Photometric
);
283 // Convert bits per sample to bytes per sample
287 return (COLORSPACE_SH(pt
)|PLANAR_SH(IsPlanar
)|EXTRA_SH(extra
)|CHANNELS_SH(ColorChannels
)|BYTES_SH(bps
)|FLAVOR_SH(reverse
));
293 cmsUInt32Number
OpenEmbedded(TIFF
* tiff
, cmsHPROFILE
* PtrProfile
, cmsHTRANSFORM
* PtrXform
)
296 cmsUInt32Number EmbedLen
, dwFormat
= 0;
297 cmsUInt8Number
* EmbedBuffer
;
302 if (TIFFGetField(tiff
, TIFFTAG_ICCPROFILE
, &EmbedLen
, &EmbedBuffer
)) {
304 *PtrProfile
= cmsOpenProfileFromMem(EmbedBuffer
, EmbedLen
);
308 fprintf(stdout
, "Embedded profile found:\n");
309 PrintProfileInformation(*PtrProfile
);
313 dwFormat
= GetInputPixelType(tiff
);
314 *PtrXform
= cmsCreateTransform(*PtrProfile
, dwFormat
,
315 hLab
, TYPE_Lab_DBL
, INTENT_RELATIVE_COLORIMETRIC
, 0);
324 size_t PixelSize(cmsUInt32Number dwFormat
)
326 return T_BYTES(dwFormat
) * (T_CHANNELS(dwFormat
) + T_EXTRA(dwFormat
));
331 int CmpImages(TIFF
* tiff1
, TIFF
* tiff2
, TIFF
* diff
)
333 cmsUInt8Number
* buf1
, *buf2
, *buf3
=NULL
;
334 int row
, cols
, imagewidth
= 0, imagelength
= 0;
337 double dR
, dG
, dB
, dC
, dM
, dY
, dK
;
339 cmsHPROFILE hProfile1
= 0, hProfile2
= 0;
340 cmsHTRANSFORM xform1
= 0, xform2
= 0;
341 cmsUInt32Number dwFormat1
, dwFormat2
;
345 TIFFGetField(tiff1
, TIFFTAG_PHOTOMETRIC
, &Photometric
);
346 TIFFGetField(tiff1
, TIFFTAG_IMAGEWIDTH
, &imagewidth
);
347 TIFFGetField(tiff1
, TIFFTAG_IMAGELENGTH
, &imagelength
);
348 TIFFGetField(tiff1
, TIFFTAG_SAMPLESPERPIXEL
, &Channels
);
350 dwFormat1
= OpenEmbedded(tiff1
, &hProfile1
, &xform1
);
351 dwFormat2
= OpenEmbedded(tiff2
, &hProfile2
, &xform2
);
355 buf1
= (cmsUInt8Number
*)_TIFFmalloc(TIFFScanlineSize(tiff1
));
356 buf2
= (cmsUInt8Number
*)_TIFFmalloc(TIFFScanlineSize(tiff2
));
360 TIFFSetField(diff
, TIFFTAG_PHOTOMETRIC
, PHOTOMETRIC_MINISBLACK
);
361 TIFFSetField(diff
, TIFFTAG_COMPRESSION
, COMPRESSION_NONE
);
362 TIFFSetField(diff
, TIFFTAG_PLANARCONFIG
, PLANARCONFIG_CONTIG
);
364 TIFFSetField(diff
, TIFFTAG_IMAGEWIDTH
, imagewidth
);
365 TIFFSetField(diff
, TIFFTAG_IMAGELENGTH
, imagelength
);
367 TIFFSetField(diff
, TIFFTAG_SAMPLESPERPIXEL
, 1);
368 TIFFSetField(diff
, TIFFTAG_BITSPERSAMPLE
, 8);
370 buf3
= (cmsUInt8Number
*)_TIFFmalloc(TIFFScanlineSize(diff
));
375 for (row
= 0; row
< imagelength
; row
++) {
377 if (TIFFReadScanline(tiff1
, buf1
, row
, 0) < 0) goto Error
;
378 if (TIFFReadScanline(tiff2
, buf2
, row
, 0) < 0) goto Error
;
381 for (cols
= 0; cols
< imagewidth
; cols
++) {
384 switch (Photometric
) {
386 case PHOTOMETRIC_MINISWHITE
:
387 case PHOTOMETRIC_MINISBLACK
:
389 dE
= fabs(buf2
[cols
] - buf1
[cols
]);
391 AddOnePixel(&ColorantStat
[0], dE
);
392 AddOnePixel(&EuclideanStat
, dE
);
395 case PHOTOMETRIC_RGB
:
398 int index
= 3 * cols
;
400 dR
= fabs(buf2
[index
+0] - buf1
[index
+0]);
401 dG
= fabs(buf2
[index
+1] - buf1
[index
+1]);
402 dB
= fabs(buf2
[index
+2] - buf1
[index
+2]);
404 dE
= sqrt(dR
* dR
+ dG
* dG
+ dB
* dB
) / sqrt(3.);
407 AddOnePixel(&ColorantStat
[0], dR
);
408 AddOnePixel(&ColorantStat
[1], dG
);
409 AddOnePixel(&ColorantStat
[2], dB
);
410 AddOnePixel(&EuclideanStat
, dE
);
413 case PHOTOMETRIC_SEPARATED
:
416 int index
= 4 * cols
;
418 dC
= fabs(buf2
[index
+0] - buf1
[index
+0]);
419 dM
= fabs(buf2
[index
+1] - buf1
[index
+1]);
420 dY
= fabs(buf2
[index
+2] - buf1
[index
+2]);
421 dK
= fabs(buf2
[index
+3] - buf1
[index
+3]);
423 dE
= sqrt(dC
* dC
+ dM
* dM
+ dY
* dY
+ dK
* dK
) / 2.;
425 AddOnePixel(&ColorantStat
[0], dC
);
426 AddOnePixel(&ColorantStat
[1], dM
);
427 AddOnePixel(&ColorantStat
[2], dY
);
428 AddOnePixel(&ColorantStat
[3], dK
);
429 AddOnePixel(&EuclideanStat
, dE
);
433 FatalError("Unsupported channels: %d", Channels
);
437 if (xform1
&& xform2
) {
440 cmsCIELab Lab1
, Lab2
;
441 size_t index1
= cols
* PixelSize(dwFormat1
);
442 size_t index2
= cols
* PixelSize(dwFormat2
);
444 cmsDoTransform(xform1
, &buf1
[index1
], &Lab1
, 1);
445 cmsDoTransform(xform2
, &buf2
[index2
], &Lab2
, 1);
447 dE
= cmsDeltaE(&Lab1
, &Lab2
);
448 AddOnePixel(&ColorimetricStat
, dE
);
453 buf3
[cols
] = (cmsUInt8Number
) floor(dE
+ 0.5);
460 if (TIFFWriteScanline(diff
, buf3
, row
, 0) < 0) goto Error
;
470 if (hProfile1
) cmsCloseProfile(hProfile1
);
471 if (hProfile2
) cmsCloseProfile(hProfile2
);
472 if (xform1
) cmsDeleteTransform(xform1
);
473 if (xform2
) cmsDeleteTransform(xform2
);
474 _TIFFfree(buf1
); _TIFFfree(buf2
);
476 TIFFWriteDirectory(diff
);
477 if (buf3
!= NULL
) _TIFFfree(buf3
);
484 void AssureShortTagIs(TIFF
* tif1
, TIFF
* tiff2
, int tag
, int Val
, const char* Error
)
489 if (!TIFFGetField(tif1
, tag
, &v1
)) goto Err
;
490 if (v1
!= Val
) goto Err
;
492 if (!TIFFGetField(tiff2
, tag
, &v1
)) goto Err
;
493 if (v1
!= Val
) goto Err
;
497 FatalError("%s is not proper", Error
);
502 int CmpShortTag(TIFF
* tif1
, TIFF
* tif2
, int tag
)
506 if (!TIFFGetField(tif1
, tag
, &v1
)) return 0;
507 if (!TIFFGetField(tif2
, tag
, &v2
)) return 0;
513 int CmpLongTag(TIFF
* tif1
, TIFF
* tif2
, int tag
)
517 if (!TIFFGetField(tif1
, tag
, &v1
)) return 0;
518 if (!TIFFGetField(tif2
, tag
, &v2
)) return 0;
525 void EqualShortTag(TIFF
* tif1
, TIFF
* tif2
, int tag
, const char* Error
)
527 if (!CmpShortTag(tif1
, tif2
, tag
))
528 FatalError("%s is different", Error
);
534 void EqualLongTag(TIFF
* tif1
, TIFF
* tif2
, int tag
, const char* Error
)
536 if (!CmpLongTag(tif1
, tif2
, tag
))
537 FatalError("%s is different", Error
);
543 void AddOneCGATSRow(cmsHANDLE hIT8
, char *Name
, LPSTAT st
)
546 double Per100
= 100.0 * ((255.0 - Mean(st
)) / 255.0);
548 cmsIT8SetData(hIT8
, Name
, "SAMPLE_ID", Name
);
549 cmsIT8SetDataDbl(hIT8
, Name
, "PER100_EQUAL", Per100
);
550 cmsIT8SetDataDbl(hIT8
, Name
, "MEAN_DE", Mean(st
));
551 cmsIT8SetDataDbl(hIT8
, Name
, "STDEV_DE", Std(st
));
552 cmsIT8SetDataDbl(hIT8
, Name
, "MIN_DE", st
->Min
);
553 cmsIT8SetDataDbl(hIT8
, Name
, "MAX_DE", st
->Peak
);
559 void CreateCGATS(const char* TiffName1
, const char* TiffName2
)
561 cmsHANDLE hIT8
= cmsIT8Alloc(0);
565 cmsIT8SetSheetType(hIT8
, "TIFFDIFF");
568 sprintf(Buffer
, "Differences between %s and %s", TiffName1
, TiffName2
);
570 cmsIT8SetComment(hIT8
, Buffer
);
572 cmsIT8SetPropertyStr(hIT8
, "ORIGINATOR", "TIFFDIFF");
574 strcpy(Buffer
, ctime(<ime
));
575 Buffer
[strlen(Buffer
)-1] = 0; // Remove the nasty "\n"
577 cmsIT8SetPropertyStr(hIT8
, "CREATED", Buffer
);
579 cmsIT8SetComment(hIT8
, " ");
581 cmsIT8SetPropertyDbl(hIT8
, "NUMBER_OF_FIELDS", 6);
584 cmsIT8SetDataFormat(hIT8
, 0, "SAMPLE_ID");
585 cmsIT8SetDataFormat(hIT8
, 1, "PER100_EQUAL");
586 cmsIT8SetDataFormat(hIT8
, 2, "MEAN_DE");
587 cmsIT8SetDataFormat(hIT8
, 3, "STDEV_DE");
588 cmsIT8SetDataFormat(hIT8
, 4, "MIN_DE");
589 cmsIT8SetDataFormat(hIT8
, 5, "MAX_DE");
595 cmsIT8SetPropertyDbl(hIT8
, "NUMBER_OF_SETS", 3);
596 AddOneCGATSRow(hIT8
, "GRAY_PLANE", &ColorantStat
[0]);
600 cmsIT8SetPropertyDbl(hIT8
, "NUMBER_OF_SETS", 5);
601 AddOneCGATSRow(hIT8
, "R_PLANE", &ColorantStat
[0]);
602 AddOneCGATSRow(hIT8
, "G_PLANE", &ColorantStat
[1]);
603 AddOneCGATSRow(hIT8
, "B_PLANE", &ColorantStat
[2]);
608 cmsIT8SetPropertyDbl(hIT8
, "NUMBER_OF_SETS", 6);
609 AddOneCGATSRow(hIT8
, "C_PLANE", &ColorantStat
[0]);
610 AddOneCGATSRow(hIT8
, "M_PLANE", &ColorantStat
[1]);
611 AddOneCGATSRow(hIT8
, "Y_PLANE", &ColorantStat
[2]);
612 AddOneCGATSRow(hIT8
, "K_PLANE", &ColorantStat
[3]);
615 default: FatalError("Internal error: Bad ColorSpace");
619 AddOneCGATSRow(hIT8
, "EUCLIDEAN", &EuclideanStat
);
620 AddOneCGATSRow(hIT8
, "COLORIMETRIC", &ColorimetricStat
);
622 cmsIT8SaveToFile(hIT8
, CGATSout
);
626 int main(int argc
, char* argv
[])
630 Tiff1
= Tiff2
= TiffDiff
= NULL
;
632 InitUtils("tiffdiff");
634 HandleSwitches(argc
, argv
);
636 if ((argc
- xoptind
) != 2) {
641 TIFFSetErrorHandler(ConsoleErrorHandler
);
642 TIFFSetWarningHandler(ConsoleWarningHandler
);
644 Tiff1
= TIFFOpen(argv
[xoptind
], "r");
645 if (Tiff1
== NULL
) FatalError("Unable to open '%s'", argv
[xoptind
]);
647 Tiff2
= TIFFOpen(argv
[xoptind
+1], "r");
648 if (Tiff2
== NULL
) FatalError("Unable to open '%s'", argv
[xoptind
+1]);
650 if (TiffDiffFilename
) {
652 TiffDiff
= TIFFOpen(TiffDiffFilename
, "w");
653 if (TiffDiff
== NULL
) FatalError("Unable to create '%s'", TiffDiffFilename
);
658 AssureShortTagIs(Tiff1
, Tiff2
, TIFFTAG_PLANARCONFIG
, PLANARCONFIG_CONTIG
, "Planar Config");
659 AssureShortTagIs(Tiff1
, Tiff2
, TIFFTAG_BITSPERSAMPLE
, 8, "8 bit per sample");
661 EqualLongTag(Tiff1
, Tiff2
, TIFFTAG_IMAGEWIDTH
, "Image width");
662 EqualLongTag(Tiff1
, Tiff2
, TIFFTAG_IMAGELENGTH
, "Image length");
664 EqualShortTag(Tiff1
, Tiff2
, TIFFTAG_SAMPLESPERPIXEL
, "Samples per pixel");
667 hLab
= cmsCreateLab4Profile(NULL
);
669 ClearStatistics(&EuclideanStat
);
670 for (i
=0; i
< 4; i
++)
671 ClearStatistics(&ColorantStat
[i
]);
673 if (!CmpImages(Tiff1
, Tiff2
, TiffDiff
))
674 FatalError("Error comparing images");
677 CreateCGATS(argv
[xoptind
], argv
[xoptind
+1]);
681 double Per100
= 100.0 * ((255.0 - Mean(&EuclideanStat
)) / 255.0);
683 printf("Digital counts %g%% equal. mean %g, min %g, max %g, Std %g\n", Per100
, Mean(&EuclideanStat
),
686 Std(&EuclideanStat
));
688 if (ColorimetricStat
.n
> 0) {
690 Per100
= 100.0 * ((255.0 - Mean(&ColorimetricStat
)) / 255.0);
692 printf("dE Colorimetric %g%% equal. mean %g, min %g, max %g, Std %g\n", Per100
, Mean(&ColorimetricStat
),
693 ColorimetricStat
.Min
,
694 ColorimetricStat
.Peak
,
695 Std(&ColorimetricStat
));
700 if (hLab
) cmsCloseProfile(hLab
);
701 if (Tiff1
) TIFFClose(Tiff1
);
702 if (Tiff2
) TIFFClose(Tiff2
);
703 if (TiffDiff
) TIFFClose(TiffDiff
);