blob: 9bffe4460702e26ad9dc922afd46498901b36c94 [file] [log] [blame]
/* Transform images with little cms
*
* 26/4/02 JC
* 26/8/05
* - attach profiles and intents to output images
* - added im_icc_import_embedded() to import with an embedded profile
* 12/5/06
* - lock around cmsDoTransform
* 23/1/07
* - set RGB16 on 16-bit RGB export
* 6/4/09
* - catch lcms error messages
* 2/11/09
* - gtkdoc
* - small cleanups
* - call attach_profile() before im_wrapone() so the profile will get
* written if we are wrinting to a file
*/
/*
This file is part of VIPS.
VIPS is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <vips/intl.h>
#ifndef HAVE_LCMS
#include <vips/vips.h>
#include <vips/thread.h>
int
im_icc_present( void )
{
return( 0 );
}
int
im_icc_transform( IMAGE *in, IMAGE *out,
const char *input_profile_filename,
const char *output_profile_filename,
VipsIntent intent )
{
im_error( "im_icc_transform", "%s",
_( "lcms library not linked to this VIPS" ) );
return( -1 );
}
int
im_icc_import( IMAGE *in, IMAGE *out,
const char *input_profile_filename, VipsIntent intent )
{
im_error( "im_icc_import", "%s",
_( "lmcs library not linked to this VIPS" ) );
return( -1 );
}
int
im_icc_import_embedded( IMAGE *in, IMAGE *out, VipsIntent intent )
{
im_error( "im_icc_import", "%s",
_( "lmcs library not linked to this VIPS" ) );
return( -1 );
}
int
im_icc_export_depth( IMAGE *in, IMAGE *out, int depth,
const char *output_profile_filename, VipsIntent intent )
{
im_error( "im_icc_export_depth", "%s",
_( "lmcs library not linked to this VIPS" ) );
return( -1 );
}
int
im_icc_ac2rc( IMAGE *in, IMAGE *out, const char *profile_filename )
{
im_error( "im_icc_ac2rc", "%s",
_( "lmcs library not linked to this VIPS" ) );
return( -1 );
}
#else
#include <stdio.h>
#include <math.h>
#include <assert.h>
/* Has to be before VIPS to avoid nameclashes.
*/
#include <lcms.h>
#include <icc34.h>
#include <vips/vips.h>
#include <vips/colour.h>
#include <vips/region.h>
#include <vips/util.h>
#ifdef WITH_DMALLOC
#include <dmalloc.h>
#endif /*WITH_DMALLOC*/
/**
* VipsIntent:
* @IM_INTENT_PERCEPTUAL:
* @IM_INTENT_RELATIVE_COLORIMETRIC:
* @IM_INTENT_SATURATION:
* @IM_INTENT_ABSOLUTE_COLORIMETRIC:
*
* The rendering intent. #IM_INTENT_ABSOLUTE_COLORIMETRIC is best for
* scientific work, #IM_INTENT_RELATIVE_COLORIMETRIC is usually best for
* accurate communication with other imaging libraries.
*/
/* Call lcms with up to this many pixels at once.
*/
#define PIXEL_BUFFER_SIZE (10000)
static const char *
decode_intent( VipsIntent intent )
{
switch( intent ) {
case IM_INTENT_PERCEPTUAL: return( "PERCEPTUAL" );
case IM_INTENT_RELATIVE_COLORIMETRIC: return( "RELATIVE" );
case IM_INTENT_SATURATION: return( "SATURATION" );
case IM_INTENT_ABSOLUTE_COLORIMETRIC: return( "ABSOLUTE" );
default: return( "<unknown>" );
}
}
/**
* im_icc_present:
*
* VIPS can optionally be built without the ICC library. Use this function to
* test for its availability.
*
* Returns: non-zero if the ICC library is present.
*/
int
im_icc_present( void )
{
return( 1 );
}
/* Global state for a transform.
*/
typedef struct {
IMAGE *in;
IMAGE *out;
const char *input_profile_filename;
const char *output_profile_filename;
VipsIntent intent;
cmsHPROFILE in_profile;
cmsHPROFILE out_profile;
cmsHTRANSFORM trans;
/* We need to single-thread calls to LCMS.
*/
GMutex *lock;
} Icc;
/* Error from lcms.
*/
static int
icc_error( int code, const char *text )
{
if( code == LCMS_ERRC_WARNING )
im_warn( "im_icc", "%s", text );
else
im_error( "im_icc", "%s", text );
return( 0 );
}
static int
icc_destroy( Icc *icc )
{
IM_FREEF( cmsDeleteTransform, icc->trans );
IM_FREEF( cmsCloseProfile, icc->in_profile );
IM_FREEF( cmsCloseProfile, icc->out_profile );
IM_FREEF( g_mutex_free, icc->lock );
return( 0 );
}
static Icc *
icc_new( IMAGE *in, IMAGE *out, VipsIntent intent )
{
Icc *icc;
/* Ask lcms not to abort on error.
*/
cmsErrorAction( LCMS_ERROR_IGNORE );
cmsSetErrorHandler( icc_error );
if( !(icc = IM_NEW( out, Icc )) )
return( NULL );
icc->in = in;
icc->out = out;
icc->input_profile_filename = NULL;
icc->output_profile_filename = NULL;
icc->intent = intent;
icc->in_profile = 0;
icc->out_profile = 0;
icc->trans = 0;
icc->lock = g_mutex_new();
if( im_add_close_callback( out,
(im_callback_fn) icc_destroy, icc, NULL ) )
return( NULL );
return( icc );
}
static Icc *
icc_new_file( IMAGE *in, IMAGE *out,
const char *input_profile_filename,
const char *output_profile_filename,
VipsIntent intent )
{
Icc *icc;
if( !(icc = icc_new( in, out, intent )) )
return( NULL );
if( input_profile_filename ) {
icc->input_profile_filename =
im_strdup( out, input_profile_filename );
if( !(icc->in_profile = cmsOpenProfileFromFile(
input_profile_filename, "r" )) )
im_error( "im_icc_transform",
_( "unable to open profile \"%s\"" ),
input_profile_filename );
}
if( output_profile_filename ) {
icc->output_profile_filename =
im_strdup( out, output_profile_filename );
if( !(icc->out_profile = cmsOpenProfileFromFile(
output_profile_filename, "r" )) )
im_error( "im_icc_transform",
_( "unable to open profile \"%s\"" ),
output_profile_filename );
}
if( !output_profile_filename )
icc->out_profile = cmsCreateLabProfile( NULL );
if( !input_profile_filename )
icc->in_profile = cmsCreateLabProfile( NULL );
if( !icc->in_profile || !icc->out_profile ) {
im_error( "im_icc_transform",
"%s", _( "unable to create profiles" ) );
return( NULL );
}
return( icc );
}
static Icc *
icc_new_mem( IMAGE *in, IMAGE *out,
void *data, int data_length,
VipsIntent intent )
{
Icc *icc;
if( !(icc = icc_new( in, out, intent )) )
return( NULL );
if( !(icc->in_profile = cmsOpenProfileFromMem( data, data_length )) ) {
im_error( "im_icc_transform",
"%s", _( "unable to read profile" ) );
return( NULL );
}
icc->out_profile = cmsCreateLabProfile( NULL );
return( icc );
}
/* Pack a buffer of floats into lcms's fixed-point formats. Cut from
* lcms-1.0.8.
*/
static void
encode_lab( float *lab, WORD *fixed, int n )
{
int i;
for( i = 0; i < n; i++ ) {
float L = lab[0];
float a = lab[1];
float b = lab[2];
if( L < 0 )
L = 0;
if( L > 100. )
L = 100.;
if( a < -128. )
a = -128;
if( a > 127.9961 )
a = 127.9961;
if( b < -128. )
b = -128;
if( b > 127.9961 )
b = 127.9961;
fixed[0] = L * 652.800 + 0.5;
fixed[1] = (a + 128.0) * 256.0 + 0.5;
fixed[2] = (b + 128.0) * 256.0 + 0.5;
lab += 3;
fixed += 3;
}
}
static void
decode_lab( WORD *fixed, float *lab, int n )
{
int i;
for( i = 0; i < n; i++ ) {
lab[0] = (double) fixed[0] / 652.800;
lab[1] = ((double) fixed[1] / 256.0) - 128.0;
lab[2] = ((double) fixed[2] / 256.0) - 128.0;
lab += 3;
fixed += 3;
}
}
static void
transform_buf( PEL *in, PEL *out, int n, Icc *icc )
{
g_mutex_lock( icc->lock );
cmsDoTransform( icc->trans, in, out, n );
g_mutex_unlock( icc->lock );
}
static int
attach_profile( IMAGE *im, const char *filename )
{
char *data;
unsigned int data_length;
if( !(data = im__file_read_name( filename, VIPS_ICC_DIR,
&data_length )) )
return( -1 );
if( im_meta_set_blob( im, IM_META_ICC_NAME,
(im_callback_fn) im_free, data, data_length ) ) {
im_free( data );
return( -1 );
}
return( 0 );
}
/**
* im_icc_transform:
* @in: input image
* @out: output image
* @input_profile_filename: get the input profile from here
* @output_profile_filename: get the output profile from here
* @intent: transform with this intent
*
* Transform an image with the ICC library. The input image is moved to
* profile-connection space with the input profile and then to the output
* space with the output profile.
*
* Use im_icc_import() and im_icc_export_depth() to do either the first or
* second half of this operation in isolation.
*
* See also: im_icc_import(), im_icc_export_depth().
*
* Returns: 0 on success, -1 on error.
*/
int
im_icc_transform( IMAGE *in, IMAGE *out,
const char *input_profile_filename,
const char *output_profile_filename,
VipsIntent intent )
{
Icc *icc;
DWORD in_icc_format;
DWORD out_icc_format;
if( im_check_uncoded( "im_icc_transform", in ) )
return( -1 );
if( !(icc = icc_new_file( in, out,
input_profile_filename, output_profile_filename, intent )) )
return( -1 );
if( !cmsIsIntentSupported( icc->in_profile,
intent, LCMS_USED_AS_INPUT ) )
im_warn( "im_icc_transform",
_( "intent %d (%s) not supported by "
"profile \"%s\"; falling back to default intent "
"(usually PERCEPTUAL)" ),
intent, decode_intent( intent ),
input_profile_filename );
if( !cmsIsIntentSupported( icc->out_profile,
intent, LCMS_USED_AS_OUTPUT ) )
im_warn( "im_icc_transform",
_( "intent %d (%s) not supported by "
"profile \"%s\"; falling back to default intent "
"(usually PERCEPTUAL)" ),
intent, decode_intent( intent ),
output_profile_filename );
switch( cmsGetColorSpace( icc->in_profile ) ) {
case icSigCmykData:
if( in->Bands != 4 ) {
im_error( "im_icc_transform",
"%s", _( "CMYK input profile "
"needs a 4 band input image" ) );
return( -1 );
}
in_icc_format = COLORSPACE_SH( PT_CMYK ) | CHANNELS_SH( 4 );
break;
case icSigRgbData:
if( in->Bands != 3 ) {
im_error( "im_icc_transform",
"%s", _( "RGB input profile "
"needs a 3 band input image" ) );
return( -1 );
}
in_icc_format = COLORSPACE_SH( PT_RGB ) | CHANNELS_SH( 3 );
break;
default:
im_error( "im_icc_transform",
_( "unimplemented input color space 0x%x" ),
cmsGetColorSpace( icc->in_profile ) );
return( -1 );
}
/* Prepare the output image.
*/
if( im_cp_desc( out, in ) )
return( -1 );
switch( cmsGetColorSpace( icc->out_profile ) ) {
case icSigCmykData:
out->Type = IM_TYPE_CMYK;
out->BandFmt = IM_BANDFMT_UCHAR;
out->Bands = 4;
out_icc_format = TYPE_CMYK_8;
break;
case icSigRgbData:
out->Type = IM_TYPE_RGB;
out->BandFmt = IM_BANDFMT_UCHAR;
out->Bands = 3;
out_icc_format = TYPE_RGB_8;
break;
default:
im_error( "im_icc_transform",
_( "unimplemented output color space 0x%x" ),
cmsGetColorSpace( icc->out_profile ) );
return( -1 );
}
switch( in->BandFmt ) {
case IM_BANDFMT_UCHAR:
in_icc_format |= BYTES_SH( 1 );
break;
case IM_BANDFMT_USHORT:
in_icc_format |= BYTES_SH( 2 );
break;
default:
im_error( "im_icc_transform",
"%s", _( "uchar or ushort input only" ) );
return( -1 );
}
if( !(icc->trans = cmsCreateTransform( icc->in_profile, in_icc_format,
icc->out_profile, out_icc_format, intent, 0 )) )
return( -1 );
if( attach_profile( out, output_profile_filename ) )
return( -1 );
/* Process!
*/
if( im_wrapone( in, out,
(im_wrapone_fn) transform_buf, icc, NULL ) )
return( -1 );
return( 0 );
}
static void
import_buf( PEL *in, float *out, int n, Icc *icc )
{
/* Buffer of encoded 16-bit pixels we write to.
*/
WORD encoded[3 * PIXEL_BUFFER_SIZE];
while( n > 0 ) {
const int chunk = IM_MIN( n, PIXEL_BUFFER_SIZE );
g_mutex_lock( icc->lock );
cmsDoTransform( icc->trans, in, encoded, chunk );
g_mutex_unlock( icc->lock );
decode_lab( encoded, out, chunk );
in += chunk * IM_IMAGE_SIZEOF_PEL( icc->in );
out += chunk * 3;
n -= chunk;
}
}
static int
icc_import( IMAGE *in, IMAGE *out, Icc *icc )
{
DWORD icc_format;
if( im_check_uncoded( "im_icc_import", in ) )
return( -1 );
if( !cmsIsIntentSupported( icc->in_profile,
icc->intent, LCMS_USED_AS_INPUT ) )
im_warn( "im_icc_import",
_( "intent %d (%s) not supported by "
"profile; falling back to default intent "
"(usually PERCEPTUAL)" ),
icc->intent, decode_intent( icc->intent ) );
/* Prepare the output image.
*/
if( im_cp_desc( out, in ) )
return( -1 );
out->Type = IM_TYPE_LAB;
out->BandFmt = IM_BANDFMT_FLOAT;
out->Bands = 3;
switch( cmsGetColorSpace( icc->in_profile ) ) {
case icSigCmykData:
if( in->Bands != 4 ) {
im_error( "im_icc_import",
"%s", _( "CMYK profile needs a "
"4 band input image" ) );
return( -1 );
}
icc_format = COLORSPACE_SH( PT_CMYK ) | CHANNELS_SH( 4 );
break;
case icSigRgbData:
if( in->Bands != 3 ) {
im_error( "im_icc_import",
"%s", _( "RGB profile needs a "
"3 band input image" ) );
return( -1 );
}
icc_format = COLORSPACE_SH( PT_RGB ) | CHANNELS_SH( 3 );
break;
default:
im_error( "im_icc_import", _( "unimplemented input color "
"space 0x%x" ), cmsGetColorSpace( icc->in_profile ) );
return( -1 );
}
switch( in->BandFmt ) {
case IM_BANDFMT_UCHAR:
icc_format |= BYTES_SH( 1 );
break;
case IM_BANDFMT_USHORT:
icc_format |= BYTES_SH( 2 );
break;
default:
im_error( "im_icc_transform",
"%s", _( "uchar or ushort input only" ) );
return( -1 );
}
if( !(icc->trans = cmsCreateTransform( icc->in_profile, icc_format,
icc->out_profile, TYPE_Lab_16, icc->intent, 0 )) )
return( -1 );
/* Process!
*/
if( im_wrapone( in, out, (im_wrapone_fn) import_buf, icc, NULL ) )
return( -1 );
return( 0 );
}
/**
* im_icc_import:
* @in: input image
* @out: output image
* @input_profile_filename: get the input profile from here
* @intent: transform with this intent
*
* Import an image with the ICC library. The input image in device space
* is moved to D65 LAB with the input profile.
*
* See also: im_icc_transform(), im_icc_import_embedded().
*
* Returns: 0 on success, -1 on error.
*/
int
im_icc_import( IMAGE *in, IMAGE *out,
const char *input_profile_filename, VipsIntent intent )
{
Icc *icc;
if( !(icc = icc_new_file( in, out,
input_profile_filename, NULL, intent )) ||
icc_import( in, out, icc ) )
return( -1 );
return( 0 );
}
/**
* im_icc_import_embedded:
* @in: input image
* @out: output image
* @intent: transform with this intent
*
* Import an image with the ICC library. The input image in device space
* is moved to D65 LAB with the input profile attached to the image under the
* name #IM_META_ICC_NAME.
*
* See also: im_icc_transform(), im_icc_import().
*
* Returns: 0 on success, -1 on error.
*/
int
im_icc_import_embedded( IMAGE *in, IMAGE *out, VipsIntent intent )
{
Icc *icc;
void *data;
size_t data_length;
if( im_header_get_typeof( in, IM_META_ICC_NAME ) == 0 ) {
im_error( "im_icc_import_embedded",
"%s", _( "no embedded profile" ) );
return( -1 );
}
if( im_meta_get_blob( in, IM_META_ICC_NAME, &data, &data_length ) ||
!(icc = icc_new_mem( in, out, data, data_length, intent )) ||
icc_import( in, out, icc ) )
return( -1 );
return( 0 );
}
static void
export_buf( float *in, PEL *out, int n, Icc *icc )
{
/* Buffer of encoded 16-bit pixels we transform.
*/
WORD encoded[3 * PIXEL_BUFFER_SIZE];
while( n > 0 ) {
const int chunk = IM_MIN( n, PIXEL_BUFFER_SIZE );
encode_lab( in, encoded, chunk );
g_mutex_lock( icc->lock );
cmsDoTransform( icc->trans, encoded, out, chunk );
g_mutex_unlock( icc->lock );
in += chunk * 3;
out += chunk * IM_IMAGE_SIZEOF_PEL( icc->out );
n -= chunk;
}
}
/**
* im_icc_export_depth:
* @in: input image
* @out: output image
* @depth: depth to export at
* @output_profile_filename: use this profile
* @intent: transform with this intent
*
* Export an image with the ICC library. The input image in
* D65 LAB is transformed to device space using the supplied profile.
* @depth can be 8 or 16, for 8 or 16-bit image export.
*
* See also: im_icc_transform(), im_icc_import().
*
* Returns: 0 on success, -1 on error.
*/
int
im_icc_export_depth( IMAGE *in, IMAGE *out, int depth,
const char *output_profile_filename, VipsIntent intent )
{
Icc *icc;
DWORD icc_format;
/* Do IM_CODING_LABQ too.
*/
if( in->Coding == IM_CODING_LABQ ) {
IMAGE *t1 = im_open_local( out, "im_icc_export:1", "p" );
if( !t1 || im_LabQ2Lab( in, t1 ) )
return( -1 );
in = t1;
}
/* Do IM_CODING_RAD.
*/
if( in->Coding == IM_CODING_RAD ) {
IMAGE *t1 = im_open_local( out, "im_icc_export:1", "p" );
if( !t1 || im_rad2float( in, t1 ) )
return( -1 );
in = t1;
}
/* Check input image.
*/
if( im_check_uncoded( "im_icc_export", in ) ||
im_check_bands( "im_icc_export", in, 3 ) ||
im_check_format( "im_icc_export", in, IM_BANDFMT_FLOAT ) )
return( -1 );
if( depth != 8 && depth != 16 ) {
im_error( "im_icc_export", "%s", _( "unsupported bit depth" ) );
return( -1 );
}
if( !(icc = icc_new_file( in, out,
NULL, output_profile_filename, intent )) )
return( -1 );
if( !cmsIsIntentSupported( icc->out_profile,
intent, LCMS_USED_AS_OUTPUT ) )
im_warn( "im_icc_export",
_( "intent %d (%s) not supported by "
"profile \"%s\"; falling back to default intent "
"(usually PERCEPTUAL)" ),
intent, decode_intent( intent ),
output_profile_filename );
/* Prepare the output image.
*/
if( im_cp_desc( out, in ) )
return( -1 );
switch( cmsGetColorSpace( icc->out_profile ) ) {
case icSigCmykData:
out->Type = IM_TYPE_CMYK;
out->BandFmt = depth == 8 ?
IM_BANDFMT_UCHAR : IM_BANDFMT_USHORT;
out->Bands = 4;
icc_format = depth == 8 ? TYPE_CMYK_8 : TYPE_CMYK_16;
break;
case icSigRgbData:
out->Type = depth == 8 ?
IM_TYPE_RGB : IM_TYPE_RGB16;
out->BandFmt = depth == 8 ?
IM_BANDFMT_UCHAR : IM_BANDFMT_USHORT;
out->Bands = 3;
icc_format = depth == 8 ? TYPE_RGB_8 : TYPE_RGB_16;
break;
default:
im_error( "im_icc_export", _( "unimplemented output color "
"space 0x%x" ), cmsGetColorSpace( icc->out_profile ) );
return( -1 );
}
if( !(icc->trans = cmsCreateTransform( icc->in_profile, TYPE_Lab_16,
icc->out_profile, icc_format, intent, 0 )) )
return( -1 );
if( attach_profile( out, output_profile_filename ) )
return( -1 );
/* Process!
*/
if( im_wrapone( in, out, (im_wrapone_fn) export_buf, icc, NULL ) )
return( -1 );
return( 0 );
}
/**
* im_icc_ac2rc:
* @in: input image
* @out: output image
* @profile_filename: use this profile
*
* Transform an image from absolute to relative colorimetry using the
* MediaWhitePoint stored in the ICC profile.
*
* See also: im_icc_transform(), im_icc_import().
*
* Returns: 0 on success, -1 on error.
*/
int
im_icc_ac2rc( IMAGE *in, IMAGE *out, const char *profile_filename )
{
cmsHPROFILE profile;
cmsCIEXYZ media;
double add[3];
double mul[3];
IMAGE *t[2];
if( !(profile = cmsOpenProfileFromFile( profile_filename, "r" )) )
return( -1 );
if( !cmsTakeMediaWhitePoint( &media, profile ) ) {
im_error( "im_icc_ac2rc", "%s", _( "unable to get media "
"white point" ) );
return( -1 );
}
cmsCloseProfile( profile );
add[0] = 0.0;
add[1] = 0.0;
add[2] = 0.0;
mul[0] = IM_D50_X0 / (media.X * 100.0);
mul[1] = IM_D50_Y0 / (media.Y * 100.0);
mul[2] = IM_D50_Z0 / (media.Z * 100.0);
/* Do IM_CODING_LABQ too.
*/
if( in->Coding == IM_CODING_LABQ ) {
IMAGE *t1 = im_open_local( out, "im_icc_ac2rc-1", "p" );
if( !t1 || im_LabQ2Lab( in, t1 ) )
return( -1 );
in = t1;
}
/* Do IM_CODING_RAD.
*/
if( in->Coding == IM_CODING_RAD ) {
IMAGE *t1 = im_open_local( out, "im_icc_export:1", "p" );
if( !t1 || im_rad2float( in, t1 ) )
return( -1 );
in = t1;
}
if( im_open_local_array( out, t, 2, "im_icc_ac2rc-2", "p" ) ||
im_Lab2XYZ_temp( in, t[0], IM_D50_X0, IM_D50_Y0, IM_D50_Z0 ) ||
im_lintra_vec( 3, mul, t[0], add, t[1] ) ||
im_XYZ2Lab_temp( t[1], out, IM_D50_X0, IM_D50_Y0, IM_D50_Z0 ) )
return( -1 );
return( 0 );
}
#endif /*HAVE_LCMS*/