[argyllcms] Colorport CGATS to Argyll CTI3 conversion utility

  • From: "David R. Gangola" <dgangola@xxxxxxxxxxxx>
  • To: argyllcms@xxxxxxxxxxxxx
  • Date: Mon, 06 Feb 2006 15:07:47 -0800

Hello, List!

I've been using an Eye-One with Argyll, capturing measurements with Xrite's Colorport utility. I had been using Perl to "massage" the data from one dialect to another. Not wanting to contribute something in Perl, I've written it in C. It has been compiled with MingW, and gcc under Cygwin. I don't have access to a Mac, but it should not be using any platform-dependant functions. The source has been passed through bcpp, as my coding style differs from the norm.

As user-friendly and polished as Colorport is, I'm bothered by it's spectral range limitation. Although the Eye-One measures from 380 to 730 nm, Colorport limits the reported values to 400 to 700 nm. I don't have a feel for how much benefit the extra 5 bands (380, 390, 710, 720, 730 nm) would provide. Does anyone know what would be gained from the missing data?

Here is the utility, in the hope that some will find it of value.

Cheers,
   Dave

cpxchg.c:
/*============================================================================*/
// Colorport <-> Argyll CGATS file converter.
// Author: David R. Gangola
// Date: 2006-02-05
//
// This program converts between the Colorport and Argyll dialects of CGATS
// files. Argyll .ti1, or .ti2 files may be exported to Colorport, and
// Colorport .txt files imported as Argyll .ti3 files.
//
// Copyright 2006 David R. Gangola
// All rights reserved.
//
// This program 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 2
// of the License, or (at your option) any later version.
//
// This program 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 this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
/*============================================================================*/


#include <stdio.h>
#include "config.h"
#include "cgats.h"

// --- This could be a header file ----------------------------
#define FTYPE_CGATS     0
#define FTYPE_CTI       1
#define MAX_STRLEN      32
#define MIN_STRING_ARGS     1
#define MAX_STRING_ARGS     2
#define CPXCHG_VERSION_STR  "1.0"

int main(int argc, char *argv[]);
void usage(void);
char* cgats_rename(cgats* p_cgats, char** ptr, char* name);
char* cgats_rename_kword(cgats* p_cgats, int table, char* old, char* new);
char* cgats_rename_kdata(cgats* p_cgats, int table, char* kword, char* value);
char* cgats_rename_field(cgats* p_cgats, int table, char* old, char* new);
char* convert_filename(char* input_filename, char *output_filename, char* ext);
int work(char* input_filename, char* output_filename, char* instrument_name);
// --- This could be the end of a header file -----------------


//
// Main
// Parse the command line in a platform independent manner.
// In other words, the hard way.  The basic mechanism was "stolen"
// from Graeme's work, but don't blame him for errors.
//
int main(int argc, char *argv[])
{
   int fa;
   int nfa;
   char* na;
   char* output_filename;
   char* instrument_name;
   int string_argc;
   char* string_argv[MAX_STRING_ARGS];

   string_argc = 0;
   string_argv[1] = NULL;
   output_filename = NULL;
   instrument_name = NULL;
   for(fa = 1; fa < argc; fa++) {
       nfa = fa;
       if(argv[fa][0] == '-') {
           na = NULL;

if(argv[fa][2] != '\000') {
na = &argv[fa][2];
}
else {
if((fa + 1) < argc) {
if(argv[fa + 1][0] != '-') {
nfa = fa + 1;
na = argv[nfa];
}
}
}
if(argv[fa][1] == 'h') { // -h : help
usage();
}
else if(argv[fa][1] == 'i') { // -i : instrument name
fa = nfa;
if(na == NULL) {
usage ();
}
instrument_name = na;
}
else if(argv[fa][1] == 'o') { // -o : output file
fa = nfa;
if(na == NULL) {
usage ();
}
output_filename = na;
}
else {
usage();
}
}
else { // bare string arguments
if(string_argc > (MAX_STRING_ARGS - 1)) {
usage();
}
else {
string_argv[string_argc++] = argv[fa];
}
}
}


// check for 1 - 2 string args
if((string_argc < MIN_STRING_ARGS) || (string_argc > MAX_STRING_ARGS)) {
usage();
}


//
// End of command line parsing - check for alternate output file argument
//
if(output_filename) {
if(string_argc > 1) {
usage();
}
string_argc++;
string_argv[1] = output_filename;
}


   //
   // And now, do the actual work
   //
   return(work(string_argv[0], string_argv[1], instrument_name));
}


//
// Usage Message and catch-all error handler.
//
void usage(void)
{
fprintf(stderr, "Converts between XRite ColorPort CGATS .txt files and Argyll .ti1, ti2, or .ti3 format.\n");
fprintf(stderr, "Version %s, Argyll %s\n", CPXCHG_VERSION_STR, ARGYLL_VERSION_STR);
fprintf(stderr, "Author: David R. Gangola, licensed under the GPL\n");
fprintf(stderr, "\n");
fprintf(stderr, "Usage: cpxchg [options] <input file> [<output file>]\n");
fprintf(stderr, " -i <instrument name> Supply a different name for target instrument.\n");
fprintf(stderr, " -o <output file name> Specify an output file name (instead of second argument).\n");
fprintf(stderr, "\n");
fprintf(stderr, " <input file> Input file name with extension.\n");
fprintf(stderr, " <output file> Output file name, may be omitted, extension may be omitted.\n");
exit(1);
}



//
// Generic rename routine for strings within CGATS structure.
// Frees the memory for the old string, allocates new memory, and copies string to new memory.
//
char* cgats_rename(cgats* p_cgats, char** ptr, char* name)
{
p_cgats->al->free(p_cgats->al, *ptr); // discard the old name
// malloc string space
if(!(*ptr = p_cgats->al->malloc(p_cgats->al, (strlen(name) + 1) * sizeof(char)))) {
fprintf(stderr, "cgats_rename: Failed to al->malloc space for new field/keyword name.\n");
}
else {
strcpy(*ptr, name);
}
return(*ptr);
}



// // CGATS struct keyword rename. Given old keyword, renames to new // char* cgats_rename_kword(cgats* p_cgats, int table, char* old, char* new) { int i;

   if((i = p_cgats->find_kword(p_cgats, table, old)) >= 0) {
       return(cgats_rename(p_cgats, &p_cgats->t[table].ksym[i], new));
   }
   return(NULL);
}


//
// CGATS struct keyword data "rename". Given old keyword data, replaces with new value.
//
char* cgats_rename_kdata(cgats* p_cgats, int table, char* kword, char* value)
{
int i;


   if((i = p_cgats->find_kword(p_cgats, table, kword)) >= 0) {
       return(cgats_rename(p_cgats, &p_cgats->t[table].kdata[i], value));
   }
   return(NULL);
}


// // CGATS struct field rename. Given old field name, renames to new // char* cgats_rename_field(cgats* p_cgats, int table, char* old, char* new) { int i;

   if((i = p_cgats->find_field(p_cgats, table, old)) >= 0) {
       return(cgats_rename(p_cgats, &p_cgats->t[table].fsym[i], new));
   }
   return(NULL);
}


//
// Take a look at the output filename given. If it exists, check for an extension.
// If no extension, create one. If no filename, derive one from the input filename.
//
char* convert_filename(char* input_filename, char *output_filename, char* ext)
{
int length;
char* ptr;


if(output_filename) {
// look for the last dir separator char
if(!((ptr = strrchr(output_filename, '\\')) || (ptr = strrchr(output_filename, '/')))) {
ptr = output_filename;
}
if(!(ptr = strrchr(ptr, '.'))) { // no extension?
if(!(ptr = (char *)malloc(strlen(output_filename) + strlen(ext) + 1 + 1))) {
fprintf(stderr, "convert_filename(): Can't malloc memory for filename.\n");
}
strcpy(ptr, output_filename);
strcat(ptr, ".");
strcat(ptr, ext);
}
else { // supplied filename looks good
ptr = output_filename;
}
}
else { // no filename supplied
// look for the last dir separator char
if(!((ptr = strrchr(input_filename, '\\')) || (ptr = strrchr(input_filename, '/')))) {
ptr = input_filename;
}
if(!(ptr = strrchr(ptr, '.'))) { // no extension?
if(!(ptr = (char *)malloc(strlen(input_filename) + strlen(ext) + 1 + 1))) {
fprintf(stderr, "convert_filename(): Can't malloc memory for filename.\n");
}
strcpy(ptr, input_filename);
strcat(ptr, ".");
strcat(ptr, ext);
}
else { // append ext to input file base name
length = ptr - input_filename;
if(!(ptr = (char *)malloc(length + strlen(ext) + 1 + 1))) {
fprintf(stderr, "convert_filename(): Can't malloc memory for filename.\n");
}
strncpy(ptr, input_filename, length);
*(ptr + length) = 0;
strcat(ptr, ".");
strcat(ptr, ext);
}
}
if(strcmp(input_filename, ptr) == 0) { // simple check for in == out, this can be fooled.
fprintf(stderr, "convert_filename: Input and Output filenames are the same.\n");
exit(1); // exit and leave memory allocated, but don't trash input file
}
return(ptr);
}



//
// The guts of the program. After command line parsing, this does the actual work
//
int work(char* input_filename, char* output_filename, char* instrument_name)
{
int file_type;
int i; // working integer
char temp_str[MAX_STRLEN]; // working text string
char* write_filename; // final output filename
cgats *wcgs; // working cgats struct
int cur_tbl = 0; // current table within CGATS structure


int spectral_band; // working spectral band
int spectral_start = 9999; // starting wavelength
int spectral_count = 0; // number of bands
int spectral_end = 0; // ending wavelength
double spectral_norm; // "normal" reflectivity


int rgb_red_index; // column containing patch red value
int rgb_green_index;
int rgb_blue_index;
double rgb_scale; // scale factor to use in conversion


//
// Open the input CGATS file
//
wcgs = new_cgats (); // Create a new CGATS structure


wcgs->add_other(wcgs, "CTI1"); // Add types other than CGATS that are permissible
wcgs->add_other(wcgs, "CTI2"); // to read.
wcgs->add_other(wcgs, "CTI3");


if(wcgs->read_name(wcgs, input_filename)) { // read the input file
fprintf(stderr, "CGATS file read error: %s", wcgs->err);
wcgs->del(wcgs);
exit(1);
}
// Did we read a CGATS (hopefully Colorport) file?
if(wcgs->t[cur_tbl].tt == cgats_X || wcgs->t[cur_tbl].tt == cgats_5) {
file_type = FTYPE_CGATS;
}
else if(wcgs->t[cur_tbl].tt == tt_other) { // Was it an Argyll CTIx file?
file_type = FTYPE_CTI;
}
else { // Was it something else?
fprintf(stderr, "Unknown file type. Exiting.\n");
wcgs->del(wcgs);
exit(1);
}
if(wcgs->ntables != 1) { // Warn that we're not converting more than one table.
fprintf(stderr, "Input file contains more than one table - using only the first.\n");
}


//
// This transforms CTI to CGATS and back again. So, If we read CTI, we'll write CGATS. If we read CGATS, we write CTI.
//
// First, conversion from Colorport .txt to Argyll .ti3
//
if(file_type == FTYPE_CGATS) { // coming from Colorport
for(i = 0; i < wcgs->t[cur_tbl].nfields; i++) { // look through the fields for SPECTRAL_%3d's
// found one?
if(sscanf(wcgs->t[cur_tbl].fsym[i], "SPECTRAL_%3d", &spectral_band) == 1) {
// create new name
sprintf(temp_str, "SPEC_%3d", spectral_band);
cgats_rename_field(wcgs, cur_tbl, wcgs->t[cur_tbl].fsym[i], temp_str);
spectral_count++; // keep track of how many
if(spectral_start > spectral_band) { // is this the shortest wavelength yet?
spectral_start = spectral_band;
}
if(spectral_end < spectral_band) { // or maybe the longest?
spectral_end = spectral_band;
}
}
}
spectral_norm = 100.0;
wcgs->t[cur_tbl].tt = tt_other; // make it an "other" file
wcgs->t[cur_tbl].oi = 2; // we've already added three other types - use the third for CTI3
// Hmm, delete would violate the r/o comment in cgats.h, let's just rename it.
cgats_rename_kword(wcgs, cur_tbl, "ORIGINATOR", "VENDOR");
cgats_rename_kword(wcgs, cur_tbl, "DESCRIPTOR", "ORIGINATOR");
cgats_rename_kword(wcgs, cur_tbl, "INSTRUMENTATION", "TARGET_INSTRUMENT");
if(instrument_name) { // If a new instrument has been provided
// use that instead.
cgats_rename_kdata(wcgs, cur_tbl, "TARGET_INSTRUMENT", instrument_name);
}
wcgs->add_kword(wcgs, cur_tbl, "DEVICE_CLASS", "OUTPUT", NULL);
wcgs->add_kword(wcgs, cur_tbl, "DESCRIPTOR", "Argyll Calibration Target chart information 3", NULL);
if(wcgs->find_field(wcgs, cur_tbl, "XYZ_X") >= 0) { // if XYZ values exist,
// set COLOR_REP to RGB_XYZ
wcgs->add_kword(wcgs, cur_tbl, "COLOR_REP", "RGB_XYZ", NULL);
}
if(wcgs->find_field(wcgs, cur_tbl, "LAB_L") >= 0) { // if LAB,
// make it RGB_LAB
wcgs->add_kword(wcgs, cur_tbl, "COLOR_REP", "RGB_LAB", NULL);
}
sprintf(temp_str, "%d", spectral_count); // add the additional spectral keywords
wcgs->add_kword(wcgs, cur_tbl, "SPECTRAL_BANDS", temp_str, NULL);
sprintf(temp_str, "%d", spectral_start);
wcgs->add_kword(wcgs, cur_tbl, "SPECTRAL_START_NM", temp_str, NULL);
sprintf(temp_str, "%d", spectral_end);
wcgs->add_kword(wcgs, cur_tbl, "SPECTRAL_END_NM", temp_str, NULL);
sprintf(temp_str, "%6.2f", spectral_norm);
wcgs->add_kword(wcgs, cur_tbl, "SPECTRAL_NORM", temp_str, NULL);
rgb_scale = 100.0 / 255.0; // CP uses RGB values from 0 to 255, not 0 - 100
write_filename = convert_filename(input_filename, output_filename, "ti3");
}


//
// Conversion from Argyll .ti2 to Colorport .txt
// This is pretty simple, as Colorport will already parse a .ti3 file.
// Scaling the RGB values, and saving as .txt is the main thing.
//
if(file_type == FTYPE_CTI) { // going to Colorport
cgats_rename_kword(wcgs, cur_tbl, "TARGET_INSTRUMENT", "INSTRUMENTATION");
if(instrument_name) { // don't know how useful this is
cgats_rename_kdata(wcgs, cur_tbl, "INSTRUMENTATION", instrument_name);
}
wcgs->t[cur_tbl].tt = cgats_5; // make it CGATS
rgb_scale = 255.0 / 100.0; // Argyll uses RGB values from 0 to 100, not 0 to 255
write_filename = convert_filename(input_filename, output_filename, "txt");
}


//
// RGB values are scaled here. Find the appropriate columns, and run
// through the patches, multiplying by the scale factor.
//
// find the columns for red, green, and blue
rgb_red_index = wcgs->find_field(wcgs, cur_tbl, "RGB_R");
rgb_green_index = wcgs->find_field(wcgs, cur_tbl, "RGB_G");
rgb_blue_index = wcgs->find_field(wcgs, cur_tbl, "RGB_B");
for(i = 0; i < wcgs->t[cur_tbl].nsets; i++) { // run through the list
// scale the values
((cgats_set_elem*)wcgs->t[cur_tbl].fdata[i][rgb_red_index])->d *= rgb_scale;
((cgats_set_elem*)wcgs->t[cur_tbl].fdata[i][rgb_green_index])->d *= rgb_scale;
((cgats_set_elem*)wcgs->t[cur_tbl].fdata[i][rgb_blue_index])->d *= rgb_scale;
}


//
// Write the finished file
// I'm truncating the number of tables to 1 for the case of CTI1 files, which
// always have two tables.
//
i = wcgs->ntables; // save the number of tables
wcgs->ntables = 1; // Ok, this should not be done this way... but it works
if(wcgs->write_name(wcgs, write_filename)) {
fprintf(stderr, "Could not write file: %s", wcgs->err);
}
wcgs->ntables = i; // put the table count back so mem is freed properly


if(write_filename != output_filename) { // did we create a new filename?
free(write_filename); // free the memory
}


   //
   // Delete the CGATS struct, and we're done.
   //
   wcgs->del(wcgs);
   return(0);
}


Other related posts: