1716 lines
38 KiB
C
1716 lines
38 KiB
C
/* 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
|
|
* 2/8/10
|
|
* - add lcms2
|
|
* 12/7/11
|
|
* - import and export cast @in to an appropriate format for you
|
|
*/
|
|
|
|
/*
|
|
|
|
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>
|
|
|
|
#if defined( HAVE_LCMS )
|
|
|
|
#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>
|
|
|
|
/**
|
|
* 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( VipsPel *in, VipsPel *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 );
|
|
im_meta_set_blob( im, IM_META_ICC_NAME,
|
|
(im_callback_fn) im_free, data, data_length );
|
|
|
|
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( VipsPel *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;
|
|
}
|
|
}
|
|
|
|
/* Save a bit of typing.
|
|
*/
|
|
#define UC IM_BANDFMT_UCHAR
|
|
#define US IM_BANDFMT_USHORT
|
|
|
|
/* Type mapping: go to uchar or ushort.
|
|
*/
|
|
static int bandfmt_icc_import[10] = {
|
|
/* UC C US S UI I F X D DX */
|
|
UC, UC, US, US, US, US, US, US, US, US
|
|
};
|
|
|
|
static int
|
|
icc_import( IMAGE *in, IMAGE *out, Icc *icc )
|
|
{
|
|
IMAGE *t;
|
|
DWORD icc_format;
|
|
|
|
if( im_check_uncoded( "im_icc_import", in ) )
|
|
return( -1 );
|
|
|
|
/* Cast in to u8/u16.
|
|
*/
|
|
if( !(t = im_open_local( out, "im_maplut", "p" )) ||
|
|
im_clip2fmt( in, t, bandfmt_icc_import[in->BandFmt] ) )
|
|
return( -1 );
|
|
in = t;
|
|
|
|
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, VipsPel *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 )
|
|
{
|
|
IMAGE *t;
|
|
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;
|
|
}
|
|
|
|
/* Force to float anyway.
|
|
*/
|
|
if( !(t = im_open_local( out, "im_icc_export", "p" )) ||
|
|
im_clip2fmt( in, t, IM_BANDFMT_FLOAT ) )
|
|
return( -1 );
|
|
|
|
/* Check input image.
|
|
*/
|
|
if( im_check_uncoded( "im_icc_export", in ) ||
|
|
im_check_bands( "im_icc_export", in, 3 ) )
|
|
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 );
|
|
}
|
|
|
|
#elif defined( HAVE_LCMS2 )
|
|
|
|
#include <stdio.h>
|
|
#include <math.h>
|
|
#include <assert.h>
|
|
|
|
#include <lcms2.h>
|
|
|
|
#include <vips/vips.h>
|
|
#include <vips/colour.h>
|
|
#include <vips/region.h>
|
|
#include <vips/util.h>
|
|
|
|
/**
|
|
* VipsIntent:
|
|
* @IM_INTENT_PERCEPTUAL: perceptual rendering intent
|
|
* @IM_INTENT_RELATIVE_COLORIMETRIC: relative colorimetric rendering intent
|
|
* @IM_INTENT_SATURATION: saturation rendering intent
|
|
* @IM_INTENT_ABSOLUTE_COLORIMETRIC: absolute colorimetric rendering intent
|
|
*
|
|
* 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 void
|
|
icc_error( cmsContext context, cmsUInt32Number code, const char *text )
|
|
{
|
|
im_error( "im_icc", "%s", text );
|
|
}
|
|
|
|
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.
|
|
*/
|
|
cmsSetLogErrorHandler( 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;
|
|
cmsCIExyY white;
|
|
|
|
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 );
|
|
}
|
|
|
|
cmsWhitePointFromTemp( &white, 6500 );
|
|
if( !output_profile_filename )
|
|
icc->out_profile = cmsCreateLab4Profile( &white );
|
|
if( !input_profile_filename )
|
|
icc->in_profile = cmsCreateLab4Profile( &white );
|
|
|
|
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;
|
|
cmsCIExyY white;
|
|
|
|
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 );
|
|
}
|
|
|
|
/* Use D65.
|
|
*/
|
|
cmsWhitePointFromTemp( &white, 6500 );
|
|
icc->out_profile = cmsCreateLab4Profile( &white );
|
|
|
|
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, guint16 *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( guint16 *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( VipsPel *in, VipsPel *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;
|
|
cmsUInt32Number in_icc_format;
|
|
cmsUInt32Number 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 cmsSigCmykData:
|
|
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 cmsSigRgbData:
|
|
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 cmsSigCmykData:
|
|
out->Type = IM_TYPE_CMYK;
|
|
out->BandFmt = IM_BANDFMT_UCHAR;
|
|
out->Bands = 4;
|
|
out_icc_format = TYPE_CMYK_8;
|
|
break;
|
|
|
|
case cmsSigRgbData:
|
|
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 );
|
|
}
|
|
|
|
/* Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make
|
|
* calling cmsDoTrsnaform() from multiple threads safe.
|
|
*/
|
|
if( !(icc->trans = cmsCreateTransform( icc->in_profile, in_icc_format,
|
|
icc->out_profile, out_icc_format, intent,
|
|
cmsFLAGS_NOCACHE )) )
|
|
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( VipsPel *in, float *out, int n, Icc *icc )
|
|
{
|
|
/* Buffer of encoded 16-bit pixels we write to.
|
|
*/
|
|
guint16 encoded[3 * PIXEL_BUFFER_SIZE];
|
|
|
|
while( n > 0 ) {
|
|
const int chunk = IM_MIN( n, PIXEL_BUFFER_SIZE );
|
|
|
|
cmsDoTransform( icc->trans, in, encoded, chunk );
|
|
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 )
|
|
{
|
|
cmsUInt32Number 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 cmsSigCmykData:
|
|
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 cmsSigRgbData:
|
|
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 );
|
|
}
|
|
|
|
/* Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make
|
|
* calling cmsDoTrsnaform() from multiple threads safe.
|
|
*/
|
|
if( !(icc->trans = cmsCreateTransform( icc->in_profile, icc_format,
|
|
icc->out_profile, TYPE_Lab_16, icc->intent,
|
|
cmsFLAGS_NOCACHE )) )
|
|
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, VipsPel *out, int n, Icc *icc )
|
|
{
|
|
/* Buffer of encoded 16-bit pixels we transform.
|
|
*/
|
|
guint16 encoded[3 * PIXEL_BUFFER_SIZE];
|
|
|
|
while( n > 0 ) {
|
|
const int chunk = IM_MIN( n, PIXEL_BUFFER_SIZE );
|
|
|
|
encode_lab( in, encoded, chunk );
|
|
cmsDoTransform( icc->trans, encoded, out, chunk );
|
|
|
|
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;
|
|
cmsUInt32Number 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 cmsSigCmykData:
|
|
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 cmsSigRgbData:
|
|
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 );
|
|
}
|
|
|
|
/* Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make
|
|
* calling cmsDoTrsnaform() from multiple threads safe.
|
|
*/
|
|
if( !(icc->trans = cmsCreateTransform( icc->in_profile, TYPE_Lab_16,
|
|
icc->out_profile, icc_format, intent,
|
|
cmsFLAGS_NOCACHE )) )
|
|
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 X, Y, Z;
|
|
|
|
double add[3];
|
|
double mul[3];
|
|
|
|
IMAGE *t[2];
|
|
|
|
if( !(profile = cmsOpenProfileFromFile( profile_filename, "r" )) )
|
|
return( -1 );
|
|
|
|
if( !(media = cmsReadTag( profile, cmsSigMediaWhitePointTag )) ) {
|
|
im_error( "im_icc_ac2rc", "%s", _( "unable to get media "
|
|
"white point" ) );
|
|
return( -1 );
|
|
}
|
|
|
|
X = media->X;
|
|
Y = media->Y;
|
|
Z = media->Z;
|
|
|
|
cmsCloseProfile( profile );
|
|
|
|
add[0] = 0.0;
|
|
add[1] = 0.0;
|
|
add[2] = 0.0;
|
|
|
|
mul[0] = IM_D50_X0 / (X * 100.0);
|
|
mul[1] = IM_D50_Y0 / (Y * 100.0);
|
|
mul[2] = IM_D50_Z0 / (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 );
|
|
}
|
|
|
|
#else /*no lcms*/
|
|
|
|
#include <vips/vips.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 );
|
|
}
|
|
|
|
#endif /*HAVE_LCMS*/
|