[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: