Fix gif dispose handling for DISPOSE_BACKGROUND and DISPOSE_PREVIOUS

- Add 'scratch' field to gif that holds temporary 'scratch buffer' used for rendering frames
- For DISPOSE_BACKGROUND: Set background color to transparent instead of 0
- For DISPOSE_BACKGROUND: Write background pixels into scratch after rendering current frame, so it will be used in next frame
- For DISPOSE_PREVIOUS: Save frames that are not disposed into 'previous' field in gif, when DISPOSE_PREVIOUS is specified start with that previous frame. see http://webreference.com/content/studio/disposal.html
- Add "ANIMEXTS1.0" to Application Extension parser
- Graphic Control Extension parser refactor
- Compare file contents to expected images for animated gifs in foreign tests
This commit is contained in:
alon-ne 2020-02-06 17:36:13 +02:00 committed by GitHub
parent 165a3a3855
commit f88dab9ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 765 additions and 434 deletions

View File

@ -4,6 +4,17 @@
- add max and min to region shrink [rgluskin]
- allow \ as an escape character in vips_break_token() [akemrir]
- tiffsave has a "depth" param to set max pyr depth
- libtiff LOGLUV images load and save as libvips XYZ
- add gifload_source
- revise vipsthumbnail flags
- add VIPS_LEAK env var
- add vips_pipe_read_limit_set(), --vips-pipe-read-limit,
VIPS_PIPE_READ_LIMIT
31/1/19 started 8.9.2
- fix a deadlock with --vips-leak [DarthSim]
- better gifload behaviour for DISPOSAL_UNSPECIFIED [DarthSim]
- ban ppm max_value < 0
20/6/19 started 8.9.1
- don't use the new source loaders for new_from_file or new_from_buffer, it

View File

@ -1640,9 +1640,19 @@ vips_foreign_save_build( VipsObject *object )
save->background ) )
return( -1 );
if( save->page_height )
if( save->page_height ) {
VipsImage *x;
if( vips_copy( ready, &x, NULL ) ) {
VIPS_UNREF( ready );
return( -1 );
}
VIPS_UNREF( ready );
ready = x;
vips_image_set_int( ready,
VIPS_META_PAGE_HEIGHT, save->page_height );
}
VIPS_UNREF( save->ready );
save->ready = ready;
@ -2097,6 +2107,7 @@ vips_foreign_operation_init( void )
extern GType vips_foreign_load_gif_file_get_type( void );
extern GType vips_foreign_load_gif_buffer_get_type( void );
extern GType vips_foreign_load_gif_source_get_type( void );
vips_foreign_load_csv_get_type();
vips_foreign_save_csv_get_type();
@ -2148,6 +2159,7 @@ vips_foreign_operation_init( void )
#ifdef HAVE_GIFLIB
vips_foreign_load_gif_file_get_type();
vips_foreign_load_gif_buffer_get_type();
vips_foreign_load_gif_source_get_type();
#endif /*HAVE_GIFLIB*/
#ifdef HAVE_GSF

View File

@ -34,6 +34,14 @@
* - check image and frame bounds, since giflib does not
* 1/9/19
* - improve early close again
* 30/1/19
* - rework on top of VipsSource
* - add gifload_source
* 31/1/20
* - treat DISPOSAL_UNSPECIFIED as _DO_NOT, since that's what many GIFs
* in the wild appear to do
* 5/2/20
* - Fix handling of DISPOSE_BACKGROUND and DISPOSE_PREVIOUS
*/
/*
@ -108,6 +116,13 @@
#define DISPOSE_PREVIOUS 3
#endif
#define NO_TRANSPARENT_INDEX -1
#define TRANSPARENT_MASK 0x01
#define GIF_TRANSPARENT_COLOR 0x00ffffff
#define DISPOSE_MASK 0x07
#define DISPOSE_SHIFT 2
#define VIPS_TYPE_FOREIGN_LOAD_GIF (vips_foreign_load_gif_get_type())
#define VIPS_FOREIGN_LOAD_GIF( obj ) \
(G_TYPE_CHECK_INSTANCE_CAST( (obj), \
@ -115,10 +130,6 @@
#define VIPS_FOREIGN_LOAD_GIF_CLASS( klass ) \
(G_TYPE_CHECK_CLASS_CAST( (klass), \
VIPS_TYPE_FOREIGN_LOAD_GIF, VipsForeignLoadGifClass))
#define VIPS_IS_FOREIGN_LOAD_GIF( obj ) \
(G_TYPE_CHECK_INSTANCE_TYPE( (obj), VIPS_TYPE_FOREIGN_LOAD_GIF ))
#define VIPS_IS_FOREIGN_LOAD_GIF_CLASS( klass ) \
(G_TYPE_CHECK_CLASS_TYPE( (klass), VIPS_TYPE_FOREIGN_LOAD_GIF ))
#define VIPS_FOREIGN_LOAD_GIF_GET_CLASS( obj ) \
(G_TYPE_INSTANCE_GET_CLASS( (obj), \
VIPS_TYPE_FOREIGN_LOAD_GIF, VipsForeignLoadGifClass ))
@ -134,6 +145,10 @@ typedef struct _VipsForeignLoadGif {
*/
int n;
/* Load from this source (set by subclasses).
*/
VipsSource *source;
GifFileType *file;
/* We decompress the whole thing to a huge RGBA memory image, and
@ -166,6 +181,10 @@ typedef struct _VipsForeignLoadGif {
*/
VipsImage *frame;
/* A memory scratch buffer the sized as one frame, used together with frame for rendering
*/
VipsImage *scratch;
/* A copy of the previous frame, in case we need a DISPOSE_PREVIOUS.
*/
VipsImage *previous;
@ -194,7 +213,7 @@ typedef struct _VipsForeignLoadGif {
/* As we scan the file, the index of the transparent pixel for this
* frame.
*/
int transparency;
int transparent_index;
/* Params for DGifOpen(). Set by subclasses, called by base class in
* _open().
@ -203,23 +222,7 @@ typedef struct _VipsForeignLoadGif {
} VipsForeignLoadGif;
typedef struct _VipsForeignLoadGifClass {
VipsForeignLoadClass parent_class;
/* Open the reader (eg. the FILE we are reading from). giflib is
* created in _header and freed in _dispose.
*/
int (*open)( VipsForeignLoadGif *gif );
/* Rewind the reader, eg. fseek() back to the start.
*/
void (*rewind)( VipsForeignLoadGif *gif );
/* Close the reader.
*/
void (*close)( VipsForeignLoadGif *gif );
} VipsForeignLoadGifClass;
typedef VipsForeignLoadClass VipsForeignLoadGifClass;
G_DEFINE_ABSTRACT_TYPE( VipsForeignLoadGif, vips_foreign_load_gif,
VIPS_TYPE_FOREIGN_LOAD );
@ -318,8 +321,6 @@ vips_foreign_load_gif_error( VipsForeignLoadGif *gif )
static int
vips_foreign_load_gif_close_giflib( VipsForeignLoadGif *gif )
{
VipsForeignLoadGifClass *class = VIPS_FOREIGN_LOAD_GIF_GET_CLASS( gif );
VIPS_DEBUG_MSG( "vips_foreign_load_gif_close_giflib:\n" );
#ifdef HAVE_GIFLIB_5
@ -346,39 +347,55 @@ vips_foreign_load_gif_close_giflib( VipsForeignLoadGif *gif )
}
#endif
class->close( gif );
if( gif->source )
vips_source_minimise( gif->source );
return( 0 );
}
/* Callback from the gif loader.
*
* Read up to len bytes into buffer, return number of bytes read, 0 for EOF.
*/
static int
vips_giflib_read( GifFileType *file, GifByteType *buf, int n )
{
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) file->UserData;
gint64 read;
read = vips_source_read( gif->source, buf, n );
if( read == 0 )
gif->eof = TRUE;
return( (int) read );
}
/* Open any underlying file resource, then giflib.
*/
static int
vips_foreign_load_gif_open_giflib( VipsForeignLoadGif *gif )
{
VipsForeignLoadGifClass *class = VIPS_FOREIGN_LOAD_GIF_GET_CLASS( gif );
VIPS_DEBUG_MSG( "vips_foreign_load_gif_open_giflib:\n" );
if( class->open( gif ) )
return( -1 );
g_assert( !gif->file );
/* Must always rewind before opening giflib again.
*/
class->rewind( gif );
vips_source_rewind( gif->source );
#ifdef HAVE_GIFLIB_5
{
int error;
if( !(gif->file = DGifOpen( gif, gif->read_func, &error )) ) {
if( !(gif->file = DGifOpen( gif, vips_giflib_read, &error )) ) {
vips_foreign_load_gif_error_vips( gif, error );
(void) vips_foreign_load_gif_close_giflib( gif );
return( -1 );
}
}
#else
if( !(gif->file = DGifOpen( gif, gif->read_func )) ) {
if( !(gif->file = DGifOpen( gif, vips_giflib_read )) ) {
vips_foreign_load_gif_error_vips( gif, GifLastError() );
(void) vips_foreign_load_gif_close_giflib( gif );
return( -1 );
@ -400,7 +417,9 @@ vips_foreign_load_gif_dispose( GObject *gobject )
vips_foreign_load_gif_close_giflib( gif );
VIPS_UNREF( gif->source );
VIPS_UNREF( gif->frame );
VIPS_UNREF( gif->scratch );
VIPS_UNREF( gif->previous );
VIPS_FREE( gif->comment );
VIPS_FREE( gif->line );
@ -423,30 +442,18 @@ vips_foreign_load_gif_get_flags( VipsForeignLoad *load )
}
static gboolean
vips_foreign_load_gif_is_a_buffer( const void *buf, size_t len )
vips_foreign_load_gif_is_a_source( VipsSource *source )
{
const guchar *str = (const guchar *) buf;
const unsigned char *data;
if( len >= 4 &&
str[0] == 'G' &&
str[1] == 'I' &&
str[2] == 'F' &&
str[3] == '8' )
return( 1 );
if( (data = vips_source_sniff( source, 4 )) &&
data[0] == 'G' &&
data[1] == 'I' &&
data[2] == 'F' &&
data[3] == '8' )
return( TRUE );
return( 0 );
}
static gboolean
vips_foreign_load_gif_is_a( const char *filename )
{
unsigned char buf[4];
if( vips__get_bytes( filename, buf, 4 ) == 4 &&
vips_foreign_load_gif_is_a_buffer( buf, 4 ) )
return( 1 );
return( 0 );
return( FALSE );
}
/* Make sure delays is allocated and large enough.
@ -562,7 +569,8 @@ vips_foreign_load_gif_scan_application_ext( VipsForeignLoadGif *gif,
*/
have_netscape = FALSE;
if( extension[0] == 11 &&
vips_isprefix( "NETSCAPE2.0", (const char*) (extension + 1) ) )
(vips_isprefix( "NETSCAPE2.0", (const char*) (extension + 1) ) ||
vips_isprefix( "ANIMEXTS1.0", (const char*) (extension + 1) )))
have_netscape = TRUE;
while( extension != NULL ) {
@ -621,7 +629,7 @@ vips_foreign_load_gif_scan_extension( VipsForeignLoadGif *gif )
switch( ext_code ) {
case GRAPHICS_EXT_FUNC_CODE:
if( extension[0] == 4 &&
extension[1] & 0x1 ) {
extension[1] & TRANSPARENT_MASK ) {
VIPS_DEBUG_MSG( "gifload: has transp.\n" );
gif->has_transparency = TRUE;
}
@ -686,7 +694,8 @@ vips_foreign_load_gif_set_header( VipsForeignLoadGif *gif, VipsImage *image )
* Not the correct behavior as loop=1 became gif-loop=0
* but we want to keep the old behavior untouched!
*/
vips_image_set_int( image, "gif-loop", gif->loop == 0 ? 0 : gif->loop - 1 );
vips_image_set_int( image,
"gif-loop", gif->loop == 0 ? 0 : gif->loop - 1 );
if( gif->delays ) {
/* The deprecated gif-delay field is in centiseconds.
@ -838,28 +847,21 @@ vips_foreign_load_gif_build_cmap( VipsForeignLoadGif *gif )
}
static void
vips_foreign_load_gif_render_line( VipsForeignLoadGif *gif,
int width, VipsPel * restrict q, VipsPel * restrict p )
vips_foreign_load_gif_render_line( VipsForeignLoadGif *gif, int width, VipsPel * restrict dst)
{
guint32 *iq;
guint32 *idst;
idst = (guint32 *) dst;
int x;
iq = (guint32 *) q;
for( x = 0; x < width; x++ ) {
VipsPel v = p[x];
if( v == gif->transparency ) {
/* In DISPOSE_DO_NOT mode, the previous frame shows
* through (ie. we do nothing). In all other modes,
* it's just transparent.
*/
if( gif->dispose != DISPOSE_DO_NOT )
iq[x] = 0;
}
else
VipsPel v = gif->line[x];
if( v != gif->transparent_index ) {
/* Blast in the RGBA for this value.
*/
iq[x] = gif->cmap[v];
idst[x] = gif->cmap[v];
}
}
}
@ -880,26 +882,14 @@ vips_foreign_load_gif_render( VipsForeignLoadGif *gif )
*/
vips_foreign_load_gif_build_cmap( gif );
/* BACKGROUND means we reset the frame to 0 (transparent) before we
* render the next set of pixels.
/* PREVIOUS means we init the frame with the last un-disposed frame. So the last un-disposed frame is used as
* a backdrop for the new frame.
*/
if( gif->dispose == DISPOSE_BACKGROUND )
memset( VIPS_IMAGE_ADDR( gif->frame, 0, 0 ), 0,
VIPS_IMAGE_SIZEOF_IMAGE( gif->frame ) );
/* PREVIOUS means we init the frame with the frame before last, ie. we
* undo the last render.
*
* Anything other than PREVIOUS, we must update the previous buffer,
*/
if( gif->dispose == DISPOSE_PREVIOUS )
memcpy( VIPS_IMAGE_ADDR( gif->frame, 0, 0 ),
if( gif->dispose == DISPOSE_PREVIOUS ) {
memcpy( VIPS_IMAGE_ADDR( gif->scratch, 0, 0 ),
VIPS_IMAGE_ADDR( gif->previous, 0, 0 ),
VIPS_IMAGE_SIZEOF_IMAGE( gif->frame ) );
else
memcpy( VIPS_IMAGE_ADDR( gif->previous, 0, 0 ),
VIPS_IMAGE_ADDR( gif->frame, 0, 0 ),
VIPS_IMAGE_SIZEOF_IMAGE( gif->frame ) );
VIPS_IMAGE_SIZEOF_IMAGE( gif->scratch ) );
}
/* giflib does not check that the Left / Top / Width / Height for this
* Image is inside the canvas.
@ -932,50 +922,88 @@ vips_foreign_load_gif_render( VipsForeignLoadGif *gif )
for( i = 0; i < 4; i++ ) {
int y;
for( y = InterlacedOffset[i]; y < file->Image.Height; y += InterlacedJumps[i] ) {
for( y = InterlacedOffset[i];
y < file->Image.Height;
y += InterlacedJumps[i] ) {
VipsPel *q = VIPS_IMAGE_ADDR( gif->frame,
file->Image.Left, file->Image.Top + y );
if( DGifGetLine( gif->file, gif->line,
file->Image.Width ) == GIF_ERROR ) {
if( DGifGetLine( gif->file, gif->line, file->Image.Width ) == GIF_ERROR ) {
vips_foreign_load_gif_error( gif );
return( -1 );
}
vips_foreign_load_gif_render_line( gif,
file->Image.Width, q, gif->line );
VipsPel *dst = VIPS_IMAGE_ADDR(gif->scratch, file->Image.Left, file->Image.Top + y );
vips_foreign_load_gif_render_line( gif, file->Image.Width, dst );
}
}
}
else {
int y;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_render: "
"non-interlaced frame of %d x %d pixels at %d x %d\n",
file->Image.Width, file->Image.Height,
file->Image.Left, file->Image.Top );
int y;
for( y = 0; y < file->Image.Height; y++ ) {
VipsPel *q = VIPS_IMAGE_ADDR( gif->frame,
file->Image.Left, file->Image.Top + y );
if( DGifGetLine( gif->file, gif->line,
file->Image.Width ) == GIF_ERROR ) {
if( DGifGetLine( gif->file, gif->line, file->Image.Width ) == GIF_ERROR ) {
vips_foreign_load_gif_error( gif );
return( -1 );
}
vips_foreign_load_gif_render_line( gif,
file->Image.Width, q, gif->line );
VipsPel *dst = VIPS_IMAGE_ADDR(gif->scratch, file->Image.Left, file->Image.Top + y );
vips_foreign_load_gif_render_line( gif, file->Image.Width, dst );
}
}
/* Copy the result to frame, which then is picked up from outside
*/
memcpy( VIPS_IMAGE_ADDR( gif->frame, 0, 0 ),
VIPS_IMAGE_ADDR(gif->scratch, 0, 0 ),
VIPS_IMAGE_SIZEOF_IMAGE( gif->frame ) );
/* BACKGROUND means we reset the frame to transparent before we
* render the next set of pixels.
*/
if( gif->dispose == DISPOSE_BACKGROUND ) {
int y1;
int x1;
for( y1 = file->Image.Top; y1 < file->Image.Top + file->Image.Height; y1++ ) {
for (x1 = file->Image.Left; x1 < file->Image.Left + file->Image.Width; x1++ ) {
*((guint32 *) VIPS_IMAGE_ADDR(gif->scratch, x1, y1 )) = GIF_TRANSPARENT_COLOR;
}
}
}
else if( gif->dispose == DISPOSAL_UNSPECIFIED || gif->dispose == DISPOSE_DO_NOT) {
/* Copy the frame to previous, so it can be restored if DISPOSE_PREVIOUS is specified in a later frame.
*/
memcpy( VIPS_IMAGE_ADDR( gif->previous, 0, 0 ),
VIPS_IMAGE_ADDR(gif->frame, 0, 0 ),
VIPS_IMAGE_SIZEOF_IMAGE( gif->previous ) );
}
/* Reset values, as Graphic Control Extension is optional
*/
gif->dispose = DISPOSAL_UNSPECIFIED;
gif->transparent_index = NO_TRANSPARENT_INDEX;
return( 0 );
}
#ifdef VIPS_DEBUG
static const char *
dispose2str( int dispose )
{
switch( dispose ) {
case DISPOSAL_UNSPECIFIED: return( "DISPOSAL_UNSPECIFIED" );
case DISPOSE_DO_NOT: return( "DISPOSE_DO_NOT" );
case DISPOSE_BACKGROUND: return( "DISPOSE_BACKGROUND" );
case DISPOSE_PREVIOUS: return( "DISPOSE_PREVIOUS" );
default: return( "<unknown>" );
}
}
#endif /*VIPS_DEBUG*/
static int
vips_foreign_load_gif_extension( VipsForeignLoadGif *gif )
{
@ -993,20 +1021,24 @@ vips_foreign_load_gif_extension( VipsForeignLoadGif *gif )
if( extension &&
ext_code == GRAPHICS_EXT_FUNC_CODE &&
extension[0] == 4 ) {
int flags = extension[1];
/* Bytes are flags, delay low, delay high,
* transparency. Flag bit 1 means transparency
* is being set.
*/
gif->transparency = -1;
if( extension[1] & 0x1 )
gif->transparency = extension[4];
gif->transparent_index = ( flags & TRANSPARENT_MASK ) ? extension[4] : NO_TRANSPARENT_INDEX;
VIPS_DEBUG_MSG("vips_foreign_load_gif_extension: "
"transparency = %d\n", gif->transparent_index);
/* Set the current dispose mode. This is read during frame load
* to set the meaning of background and transparent pixels.
*/
gif->dispose = (extension[1] >> 2) & 0x7;
gif->dispose = ( flags >> DISPOSE_SHIFT ) & DISPOSE_MASK;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_extension: "
"dispose = %d\n", gif->dispose );
"dispose = %s\n", dispose2str( gif->dispose ) );
}
while( extension != NULL )
@ -1079,7 +1111,6 @@ vips_foreign_load_gif_generate( VipsRegion *or,
{
VipsRect *r = &or->valid;
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) a;
VipsForeignLoadGifClass *class = VIPS_FOREIGN_LOAD_GIF_GET_CLASS( gif );
int y;
@ -1090,11 +1121,6 @@ vips_foreign_load_gif_generate( VipsRegion *or,
r->left, r->top, r->width, r->height );
#endif /*DEBUG_VERBOSE*/
/* May have been minimised. Reopen the fp if necessary.
*/
if( class->open( gif ) )
return( -1 );
for( y = 0; y < r->height; y++ ) {
/* The page for this output line, and the line number in page.
*/
@ -1169,9 +1195,7 @@ vips_foreign_load_gif_generate( VipsRegion *or,
static void
vips_foreign_load_gif_minimise( VipsObject *object, VipsForeignLoadGif *gif )
{
VipsForeignLoadGifClass *class = VIPS_FOREIGN_LOAD_GIF_GET_CLASS( gif );
class->close( gif );
vips_source_minimise( gif->source );
}
static int
@ -1197,6 +1221,16 @@ vips_foreign_load_gif_load( VipsForeignLoad *load )
if( vips_image_write_prepare( gif->frame ) )
return( -1 );
/* An additional scratch buffer, same size as gif->frame. Used together with gif->frame for rendering.
*/
gif->scratch = vips_image_new_memory();
vips_image_init_fields( gif->scratch,
gif->file->SWidth, gif->file->SHeight, 4, VIPS_FORMAT_UCHAR,
VIPS_CODING_NONE, VIPS_INTERPRETATION_sRGB, 1.0, 1.0 );
if( vips_image_write_prepare( gif->scratch ) )
return( -1 );
/* A copy of the previous state of the frame, in case we have to
* process a DISPOSE_PREVIOUS.
*/
@ -1231,22 +1265,6 @@ vips_foreign_load_gif_load( VipsForeignLoad *load )
return( 0 );
}
static int
vips_foreign_load_gif_open( VipsForeignLoadGif *gif )
{
return( 0 );
}
static void
vips_foreign_load_gif_rewind( VipsForeignLoadGif *gif )
{
}
static void
vips_foreign_load_gif_close( VipsForeignLoadGif *gif )
{
}
static void
vips_foreign_load_gif_class_init( VipsForeignLoadGifClass *class )
{
@ -1267,10 +1285,6 @@ vips_foreign_load_gif_class_init( VipsForeignLoadGifClass *class )
vips_foreign_load_gif_get_flags_filename;
load_class->get_flags = vips_foreign_load_gif_get_flags;
class->open = vips_foreign_load_gif_open;
class->rewind = vips_foreign_load_gif_rewind;
class->close = vips_foreign_load_gif_close;
VIPS_ARG_INT( class, "page", 20,
_( "Page" ),
_( "Load this page from the file" ),
@ -1291,12 +1305,12 @@ static void
vips_foreign_load_gif_init( VipsForeignLoadGif *gif )
{
gif->n = 1;
gif->transparency = -1;
gif->transparent_index = NO_TRANSPARENT_INDEX;
gif->delays = NULL;
gif->delays_length = 0;
gif->loop = 1;
gif->comment = NULL;
gif->dispose = 0;
gif->dispose = DISPOSAL_UNSPECIFIED;
vips_foreign_load_gif_allocate_delays( gif );
}
@ -1308,14 +1322,6 @@ typedef struct _VipsForeignLoadGifFile {
*/
char *filename;
/* The FILE* we read from.
*/
FILE *fp;
/* If we close and reopen, save the ftell point here.
*/
long seek_position;
} VipsForeignLoadGifFile;
typedef VipsForeignLoadGifClass VipsForeignLoadGifFileClass;
@ -1323,82 +1329,22 @@ typedef VipsForeignLoadGifClass VipsForeignLoadGifFileClass;
G_DEFINE_TYPE( VipsForeignLoadGifFile, vips_foreign_load_gif_file,
vips_foreign_load_gif_get_type() );
/* Our input function for file open. We can't use DGifOpenFileName(), since
* that just calls open() and won't work with unicode on win32. We can't use
* DGifOpenFileHandle() since that's an fd from open() and you can't pass those
* across DLL boundaries on Windows.
*/
static int
vips_giflib_file_read( GifFileType *gfile, GifByteType *buffer, int n )
vips_foreign_load_gif_file_build( VipsObject *object )
{
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) gfile->UserData;
VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif;
VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) object;
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) object;
if( feof( file->fp ) )
gif->eof = TRUE;
return( (int) fread( (void *) buffer, 1, n, file->fp ) );
}
/* We have to have _open() as a vfunc since we want to be able to reopen in
* _generate if we have been closed during _minimise.
*/
static int
vips_foreign_load_gif_file_open( VipsForeignLoadGif *gif )
{
VipsForeignLoad *load = (VipsForeignLoad *) gif;
VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_file_open:\n" );
if( !file->fp ) {
if( !(file->fp =
vips__file_open_read( file->filename, NULL, FALSE )) )
if( file->filename )
if( !(gif->source =
vips_source_new_from_file( file->filename )) )
return( -1 );
/* Restore the read point if we are reopening.
*/
if( file->seek_position != -1 )
fseek( file->fp, file->seek_position, SEEK_SET );
if( VIPS_OBJECT_CLASS( vips_foreign_load_gif_file_parent_class )->
build( object ) )
return( -1 );
VIPS_SETSTR( load->out->filename, file->filename );
gif->read_func = vips_giflib_file_read;
}
return( VIPS_FOREIGN_LOAD_GIF_CLASS(
vips_foreign_load_gif_file_parent_class )->open( gif ) );
}
static void
vips_foreign_load_gif_file_rewind( VipsForeignLoadGif *gif )
{
VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_file_rewind:\n" );
if( file->fp ) {
file->seek_position = 0;
fseek( file->fp, file->seek_position, SEEK_SET );
}
VIPS_FOREIGN_LOAD_GIF_CLASS(
vips_foreign_load_gif_file_parent_class )->rewind( gif );
}
static void
vips_foreign_load_gif_file_close( VipsForeignLoadGif *gif )
{
VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_file_close:\n" );
if( file->fp ) {
file->seek_position = ftell( file->fp );
VIPS_FREEF( fclose, file->fp );
}
VIPS_FOREIGN_LOAD_GIF_CLASS(
vips_foreign_load_gif_file_parent_class )->close( gif );
return( 0 );
}
static const char *vips_foreign_gif_suffs[] = {
@ -1406,6 +1352,20 @@ static const char *vips_foreign_gif_suffs[] = {
NULL
};
static gboolean
vips_foreign_load_gif_file_is_a( const char *filename )
{
VipsSource *source;
gboolean result;
if( !(source = vips_source_new_from_file( filename )) )
return( FALSE );
result = vips_foreign_load_gif_is_a_source( source );
VIPS_UNREF( source );
return( result );
}
static void
vips_foreign_load_gif_file_class_init(
VipsForeignLoadGifFileClass *class )
@ -1414,21 +1374,17 @@ vips_foreign_load_gif_file_class_init(
VipsObjectClass *object_class = (VipsObjectClass *) class;
VipsForeignClass *foreign_class = (VipsForeignClass *) class;
VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;
VipsForeignLoadGifClass *gif_class = (VipsForeignLoadGifClass *) class;
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
object_class->nickname = "gifload";
object_class->description = _( "load GIF with giflib" );
object_class->build = vips_foreign_load_gif_file_build;
foreign_class->suffs = vips_foreign_gif_suffs;
load_class->is_a = vips_foreign_load_gif_is_a;
gif_class->open = vips_foreign_load_gif_file_open;
gif_class->rewind = vips_foreign_load_gif_file_rewind;
gif_class->close = vips_foreign_load_gif_file_close;
load_class->is_a = vips_foreign_load_gif_file_is_a;
VIPS_ARG_STRING( class, "filename", 1,
_( "Filename" ),
@ -1442,7 +1398,6 @@ vips_foreign_load_gif_file_class_init(
static void
vips_foreign_load_gif_file_init( VipsForeignLoadGifFile *file )
{
file->seek_position = -1;
}
typedef struct _VipsForeignLoadGifBuffer {
@ -1450,12 +1405,7 @@ typedef struct _VipsForeignLoadGifBuffer {
/* Load from a buffer.
*/
VipsArea *buf;
/* Current read point, bytes left in buffer.
*/
VipsPel *p;
size_t bytes_to_go;
VipsArea *blob;
} VipsForeignLoadGifBuffer;
@ -1464,59 +1414,38 @@ typedef VipsForeignLoadGifClass VipsForeignLoadGifBufferClass;
G_DEFINE_TYPE( VipsForeignLoadGifBuffer, vips_foreign_load_gif_buffer,
vips_foreign_load_gif_get_type() );
/* Callback from the gif loader.
*
* Read up to len bytes into buffer, return number of bytes read, 0 for EOF.
*/
static int
vips_giflib_buffer_read( GifFileType *file, GifByteType *buf, int n )
vips_foreign_load_gif_buffer_build( VipsObject *object )
{
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) file->UserData;
VipsForeignLoadGifBuffer *buffer = (VipsForeignLoadGifBuffer *) gif;
size_t will_read = VIPS_MIN( n, buffer->bytes_to_go );
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) object;
VipsForeignLoadGifBuffer *buffer =
(VipsForeignLoadGifBuffer *) object;
memcpy( buf, buffer->p, will_read );
buffer->p += will_read;
buffer->bytes_to_go -= will_read;
if( buffer->blob &&
!(gif->source = vips_source_new_from_memory(
VIPS_AREA( buffer->blob )->data,
VIPS_AREA( buffer->blob )->length )) )
return( -1 );
if( will_read == 0 )
gif->eof = TRUE;
if( VIPS_OBJECT_CLASS( vips_foreign_load_gif_buffer_parent_class )->
build( object ) )
return( -1 );
return( will_read );
return( 0 );
}
static int
vips_foreign_load_gif_buffer_open( VipsForeignLoadGif *gif )
static gboolean
vips_foreign_load_gif_buffer_is_a_buffer( const void *buf, size_t len )
{
VipsForeignLoadGifBuffer *buffer = (VipsForeignLoadGifBuffer *) gif;
VipsSource *source;
gboolean result;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_buffer_open:\n" );
if( !(source = vips_source_new_from_memory( buf, len )) )
return( FALSE );
result = vips_foreign_load_gif_is_a_source( source );
VIPS_UNREF( source );
/* We can open several times -- make sure we don't move the read point
* if we reopen.
*/
if( !buffer->p ) {
buffer->p = buffer->buf->data;
buffer->bytes_to_go = buffer->buf->length;
gif->read_func = vips_giflib_buffer_read;
}
return( VIPS_FOREIGN_LOAD_GIF_CLASS(
vips_foreign_load_gif_buffer_parent_class )->open( gif ) );
}
static void
vips_foreign_load_gif_buffer_rewind( VipsForeignLoadGif *gif )
{
VipsForeignLoadGifBuffer *buffer = (VipsForeignLoadGifBuffer *) gif;
VIPS_DEBUG_MSG( "vips_foreign_load_gif_buffer_rewind:\n" );
buffer->p = buffer->buf->data;
buffer->bytes_to_go = buffer->buf->length;
VIPS_FOREIGN_LOAD_GIF_CLASS(
vips_foreign_load_gif_buffer_parent_class )->rewind( gif );
return( result );
}
static void
@ -1526,24 +1455,21 @@ vips_foreign_load_gif_buffer_class_init(
GObjectClass *gobject_class = G_OBJECT_CLASS( class );
VipsObjectClass *object_class = (VipsObjectClass *) class;
VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;
VipsForeignLoadGifClass *gif_class = (VipsForeignLoadGifClass *) class;
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
object_class->nickname = "gifload_buffer";
object_class->description = _( "load GIF with giflib" );
object_class->build = vips_foreign_load_gif_buffer_build;
load_class->is_a_buffer = vips_foreign_load_gif_is_a_buffer;
gif_class->open = vips_foreign_load_gif_buffer_open;
gif_class->rewind = vips_foreign_load_gif_buffer_rewind;
load_class->is_a_buffer = vips_foreign_load_gif_buffer_is_a_buffer;
VIPS_ARG_BOXED( class, "buffer", 1,
_( "Buffer" ),
_( "Buffer to load from" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsForeignLoadGifBuffer, buf ),
G_STRUCT_OFFSET( VipsForeignLoadGifBuffer, blob ),
VIPS_TYPE_BLOB );
}
@ -1553,6 +1479,70 @@ vips_foreign_load_gif_buffer_init( VipsForeignLoadGifBuffer *buffer )
{
}
typedef struct _VipsForeignLoadGifSource {
VipsForeignLoadGif parent_object;
/* Load from a source.
*/
VipsSource *source;
} VipsForeignLoadGifSource;
typedef VipsForeignLoadGifClass VipsForeignLoadGifSourceClass;
G_DEFINE_TYPE( VipsForeignLoadGifSource, vips_foreign_load_gif_source,
vips_foreign_load_gif_get_type() );
static int
vips_foreign_load_gif_source_build( VipsObject *object )
{
VipsForeignLoadGif *gif = (VipsForeignLoadGif *) object;
VipsForeignLoadGifSource *source =
(VipsForeignLoadGifSource *) object;
if( source->source ) {
gif->source = source->source;
g_object_ref( gif->source );
}
if( VIPS_OBJECT_CLASS( vips_foreign_load_gif_source_parent_class )->
build( object ) )
return( -1 );
return( 0 );
}
static void
vips_foreign_load_gif_source_class_init(
VipsForeignLoadGifSourceClass *class )
{
GObjectClass *gobject_class = G_OBJECT_CLASS( class );
VipsObjectClass *object_class = (VipsObjectClass *) class;
VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
object_class->nickname = "gifload_source";
object_class->description = _( "load GIF with giflib" );
object_class->build = vips_foreign_load_gif_source_build;
load_class->is_a_source = vips_foreign_load_gif_is_a_source;
VIPS_ARG_OBJECT( class, "source", 1,
_( "Source" ),
_( "Source to load from" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsForeignLoadGifSource, source ),
VIPS_TYPE_SOURCE );
}
static void
vips_foreign_load_gif_source_init( VipsForeignLoadGifSource *source )
{
}
#endif /*HAVE_GIFLIB*/
/**
@ -1637,3 +1627,32 @@ vips_gifload_buffer( void *buf, size_t len, VipsImage **out, ... )
return( result );
}
/**
* vips_gifload_source:
* @source: source to load
* @out: (out): image to write
* @...: %NULL-terminated list of optional named arguments
*
* Optional arguments:
*
* * @page: %gint, page (frame) to read
* * @n: %gint, load this many pages
*
* Exactly as vips_gifload(), but read from a source.
*
* See also: vips_gifload().
*
* Returns: 0 on success, -1 on error.
*/
int
vips_gifload_source( VipsSource *source, VipsImage **out, ... )
{
va_list ap;
int result;
va_start( ap, out );
result = vips_call_split( "gifload_source", ap, source, out );
va_end( ap );
return( result );
}

View File

@ -35,6 +35,8 @@
* - redone with source/target
* - sequential load, plus mmap for filename sources
* - faster plus lower memory use
* 02/02/2020
* - ban max_vaue < 0
*/
/*
@ -259,6 +261,12 @@ vips_foreign_load_ppm_parse_header( VipsForeignLoadPpm *ppm )
if( get_int( ppm->sbuf, &ppm->max_value ) )
return( -1 );
/* max_value must be > 0 and <= 65535, according to
* the spec, but we allow up to 32 bits per pixel.
*/
if( ppm->max_value < 0 )
ppm->max_value = 0;
if( ppm->max_value > 255 )
ppm->bits = 16;
if( ppm->max_value > 65535 )

View File

@ -193,6 +193,8 @@
* - switch to source input
* 18/11/19
* - support ASSOCALPHA in any alpha band
* 27/1/20
* - read logluv images as XYZ
*/
/*
@ -294,6 +296,10 @@ typedef struct _RtiffHeader {
*/
uint32 read_height;
tsize_t read_size;
/* Scale factor to get absolute cd/m2 from XYZ.
*/
double stonits;
} RtiffHeader;
/* Scanline-type process function.
@ -911,6 +917,52 @@ rtiff_parse_labs( Rtiff *rtiff, VipsImage *out )
return( 0 );
}
/* libtiff delivers logluv as illuminant-free 0-1 XYZ in 3 x float.
*/
static void
rtiff_logluv_line( Rtiff *rtiff, VipsPel *q, VipsPel *p, int n, void *dummy )
{
int samples_per_pixel = rtiff->header.samples_per_pixel;
float *p1;
float *q1;
int x;
int i;
p1 = (float *) p;
q1 = (float *) q;
for( x = 0; x < n; x++ ) {
q1[0] = VIPS_D65_X0 * p1[0];
q1[1] = VIPS_D65_Y0 * p1[1];
q1[2] = VIPS_D65_Z0 * p1[2];
for( i = 3; i < samples_per_pixel; i++ )
q1[i] = p1[i];
q1 += samples_per_pixel;
p1 += samples_per_pixel;
}
}
/* LOGLUV images arrive from libtiff as float xyz.
*/
static int
rtiff_parse_logluv( Rtiff *rtiff, VipsImage *out )
{
if( rtiff_check_min_samples( rtiff, 3 ) ||
rtiff_check_interpretation( rtiff, PHOTOMETRIC_LOGLUV ) )
return( -1 );
out->Bands = rtiff->header.samples_per_pixel;
out->BandFmt = VIPS_FORMAT_FLOAT;
out->Coding = VIPS_CODING_NONE;
out->Type = VIPS_INTERPRETATION_XYZ;
rtiff->sfn = rtiff_logluv_line;
return( 0 );
}
/* Per-scanline process function for 1 bit images.
*/
static void
@ -1406,6 +1458,9 @@ rtiff_pick_reader( Rtiff *rtiff )
return( rtiff_parse_labs );
}
if( photometric_interpretation == PHOTOMETRIC_LOGLUV )
return( rtiff_parse_logluv );
if( photometric_interpretation == PHOTOMETRIC_MINISWHITE ||
photometric_interpretation == PHOTOMETRIC_MINISBLACK ) {
if( bits_per_sample == 1 )
@ -1436,6 +1491,15 @@ rtiff_set_header( Rtiff *rtiff, VipsImage *out )
TIFFSetField( rtiff->tiff,
TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB );
/* Ask for LOGLUV as 3 x float XYZ.
*/
if( rtiff->header.photometric_interpretation == PHOTOMETRIC_LOGLUV ) {
TIFFSetField( rtiff->tiff,
TIFFTAG_SGILOGDATAFMT, SGILOGDATAFMT_FLOAT );
vips_image_set_double( out, "stonits", rtiff->header.stonits );
}
out->Xsize = rtiff->header.width;
out->Ysize = rtiff->header.height * rtiff->n;
@ -2198,10 +2262,12 @@ rtiff_header_read( Rtiff *rtiff, RtiffHeader *header )
TIFFGetFieldDefaulted( rtiff->tiff,
TIFFTAG_COMPRESSION, &header->compression );
if( header->compression == COMPRESSION_JPEG )
/* We want to always expand subsampled YCBCR images to full
* RGB.
/* Request YCbCr expansion. libtiff complains if you do this for
* non-jpg images. We must set this here since it changes the result
* of scanline_size.
*/
if( header->compression == COMPRESSION_JPEG )
TIFFSetField( rtiff->tiff,
TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB );
else if( header->photometric_interpretation == PHOTOMETRIC_YCBCR ) {
@ -2223,6 +2289,26 @@ rtiff_header_read( Rtiff *rtiff, RtiffHeader *header )
}
}
if( header->photometric_interpretation == PHOTOMETRIC_LOGLUV ) {
if( header->compression != COMPRESSION_SGILOG &&
header->compression != COMPRESSION_SGILOG24 ) {
vips_error( "tiff2vips",
"%s", _( "not SGI-compressed LOGLUV" ) );
return( -1 );
}
/* Always get LOGLUV as 3 x float XYZ. We must set this here
* since it'll change the value of scanline_size.
*/
TIFFSetField( rtiff->tiff,
TIFFTAG_SGILOGDATAFMT, SGILOGDATAFMT_FLOAT );
}
/* For logluv, the calibration factor to get to absolute luminance.
*/
if( !TIFFGetField( rtiff->tiff, TIFFTAG_STONITS, &header->stonits ) )
header->stonits = 1.0;
/* Arbitrary sanity-checking limits.
*/
if( header->width <= 0 ||

View File

@ -389,7 +389,7 @@ vips_foreign_load_tiff_buffer_build( VipsObject *object )
VIPS_AREA( buffer->blob )->length )) )
return( -1 );
if( VIPS_OBJECT_CLASS( vips_foreign_load_tiff_file_parent_class )->
if( VIPS_OBJECT_CLASS( vips_foreign_load_tiff_buffer_parent_class )->
build( object ) )
return( -1 );
@ -456,8 +456,8 @@ vips_foreign_load_tiff_buffer_init( VipsForeignLoadTiffBuffer *buffer )
* during load
*
* Read a TIFF file into a VIPS image. It is a full baseline TIFF 6 reader,
* with extensions for tiled images, multipage images, LAB colour space,
* pyramidal images and JPEG compression. including CMYK and YCbCr.
* with extensions for tiled images, multipage images, XYZ and LAB colour
* space, pyramidal images and JPEG compression, including CMYK and YCbCr.
*
* @page means load this page from the file. By default the first page (page
* 0) is read.

View File

@ -551,6 +551,10 @@ vips_foreign_save_tiff_buffer_init( VipsForeignSaveTiffBuffer *buffer )
* good for 1-bit images, and deflate is the best lossless compression TIFF
* can do.
*
* XYZ images are automatically saved as libtiff LOGLUV with SGILOG compression.
* Float LAB images are saved as float CIELAB. Set @squash to save as 8-bit
* CIELAB.
*
* Use @Q to set the JPEG compression factor. Default 75.
*
* User @level to set the ZSTD compression level. Use @lossless to

View File

@ -189,6 +189,8 @@
* - "squash" now squashes 3-band float LAB down to LABQ
* 26/1/20
* - add "depth" to set pyr depth
* 27/1/20
* - write XYZ images as logluv
*/
/*
@ -608,7 +610,6 @@ wtiff_write_header( Wtiff *wtiff, Layer *layer )
{
TIFF *tif = layer->tif;
int format;
int orientation;
/* Output base header fields.
@ -702,6 +703,21 @@ wtiff_write_header( Wtiff *wtiff, Layer *layer )
photometric = PHOTOMETRIC_CIELAB;
colour_bands = 3;
}
else if( wtiff->input->Type == VIPS_INTERPRETATION_XYZ ) {
double stonits;
photometric = PHOTOMETRIC_LOGLUV;
/* Tell libtiff we will write as float XYZ.
*/
TIFFSetField( tif,
TIFFTAG_SGILOGDATAFMT, SGILOGDATAFMT_FLOAT );
stonits = 1.0;
if( vips_image_get_typeof( wtiff->ready, "stonits" ) )
vips_image_get_double( wtiff->ready,
"stonits", &stonits );
TIFFSetField( tif, TIFFTAG_STONITS, stonits );
colour_bands = 3;
}
else if( wtiff->ready->Type == VIPS_INTERPRETATION_CMYK &&
wtiff->ready->Bands >= 4 ) {
photometric = PHOTOMETRIC_SEPARATED;
@ -764,7 +780,12 @@ wtiff_write_header( Wtiff *wtiff, Layer *layer )
TIFFSetField( tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE );
/* Sample format.
*
* Don't set for logluv: libtiff does this for us.
*/
if( wtiff->input->Type != VIPS_INTERPRETATION_XYZ ) {
int format;
format = SAMPLEFORMAT_UINT;
if( vips_band_format_isuint( wtiff->ready->BandFmt ) )
format = SAMPLEFORMAT_UINT;
@ -775,6 +796,7 @@ wtiff_write_header( Wtiff *wtiff, Layer *layer )
else if( vips_band_format_iscomplex( wtiff->ready->BandFmt ) )
format = SAMPLEFORMAT_COMPLEXIEEEFP;
TIFFSetField( tif, TIFFTAG_SAMPLEFORMAT, format );
}
return( 0 );
}
@ -1039,6 +1061,11 @@ wtiff_new( VipsImage *input, const char *filename,
return( NULL );
}
/* XYZ images are written as libtiff LOGLUV.
*/
if( wtiff->ready->Type == VIPS_INTERPRETATION_XYZ )
wtiff->compression = COMPRESSION_SGILOG;
/* Multipage image?
*/
if( wtiff->page_height < wtiff->ready->Ysize ) {
@ -1331,6 +1358,31 @@ LabS2Lab16( VipsPel *q, VipsPel *p, int n, int samples_per_pixel )
}
}
/* Convert VIPS D65 XYZ to TIFF scaled float illuminant-free xyz.
*/
static void
XYZ2tiffxyz( VipsPel *q, VipsPel *p, int n, int samples_per_pixel )
{
float *p1 = (float *) p;
float *q1 = (float *) q;
int x;
for( x = 0; x < n; x++ ) {
int i;
q1[0] = p1[0] / VIPS_D65_X0;
q1[1] = p1[1] / VIPS_D65_Y0;
q1[2] = p1[2] / VIPS_D65_Z0;
for( i = 3; i < samples_per_pixel; i++ )
q1[i] = p1[i];
q1 += samples_per_pixel;
p1 += samples_per_pixel;
}
}
/* Pack the pixels in @area from @in into a TIFF tile buffer.
*/
static void
@ -1358,6 +1410,8 @@ wtiff_pack2tiff( Wtiff *wtiff, Layer *layer,
LabQ2LabC( q, p, area->width );
else if( wtiff->squash )
eightbit2onebit( wtiff, q, p, area->width );
else if( wtiff->input->Type == VIPS_INTERPRETATION_XYZ )
XYZ2tiffxyz( q, p, area->width, in->im->Bands );
else if( (in->im->Bands == 1 || in->im->Bands == 2) &&
wtiff->miniswhite )
invert_band0( wtiff, q, p, area->width );
@ -1449,6 +1503,10 @@ wtiff_layer_write_strip( Wtiff *wtiff, Layer *layer, VipsRegion *strip )
LabS2Lab16( wtiff->tbuf, p, im->Xsize, im->Bands );
p = wtiff->tbuf;
}
else if( wtiff->input->Type == VIPS_INTERPRETATION_XYZ ) {
XYZ2tiffxyz( wtiff->tbuf, p, im->Xsize, im->Bands );
p = wtiff->tbuf;
}
else if( wtiff->squash ) {
eightbit2onebit( wtiff, wtiff->tbuf, p, im->Xsize );
p = wtiff->tbuf;

View File

@ -89,6 +89,8 @@ GType vips_connection_get_type( void );
const char *vips_connection_filename( VipsConnection *connection );
const char *vips_connection_nick( VipsConnection *connection );
void vips_pipe_read_limit_set( gint64 limit );
#define VIPS_TYPE_SOURCE (vips_source_get_type())
#define VIPS_SOURCE( obj ) \
(G_TYPE_CHECK_INSTANCE_CAST( (obj), \

View File

@ -597,6 +597,8 @@ int vips_gifload( const char *filename, VipsImage **out, ... )
__attribute__((sentinel));
int vips_gifload_buffer( void *buf, size_t len, VipsImage **out, ... )
__attribute__((sentinel));
int vips_gifload_source( VipsSource *source, VipsImage **out, ... )
__attribute__((sentinel));
int vips_heifload( const char *filename, VipsImage **out, ... )
__attribute__((sentinel));

View File

@ -192,6 +192,8 @@ int vips__view_image( struct _VipsImage *image );
*/
extern int _vips__argument_id;
void vips__meta_init( void );
#ifdef __cplusplus
}
#endif /*__cplusplus*/

View File

@ -62,6 +62,7 @@ extern "C" {
* @VIPS_REGION_SHRINK_MODE: use the mode
* @VIPS_REGION_SHRINK_MAX: use the maximum
* @VIPS_REGION_SHRINK_MIN: use the minimum
* @VIPS_REGION_SHRINK_NEAREST: use the top-left pixel
*
* How to calculate the output pixels when shrinking a 2x2 region.
*/
@ -71,6 +72,7 @@ typedef enum {
VIPS_REGION_SHRINK_MODE,
VIPS_REGION_SHRINK_MAX,
VIPS_REGION_SHRINK_MIN,
VIPS_REGION_SHRINK_NEAREST,
VIPS_REGION_SHRINK_LAST
} VipsRegionShrink;

View File

@ -905,6 +905,7 @@ vips_region_shrink_get_type( void )
{VIPS_REGION_SHRINK_MODE, "VIPS_REGION_SHRINK_MODE", "mode"},
{VIPS_REGION_SHRINK_MAX, "VIPS_REGION_SHRINK_MAX", "max"},
{VIPS_REGION_SHRINK_MIN, "VIPS_REGION_SHRINK_MIN", "min"},
{VIPS_REGION_SHRINK_NEAREST, "VIPS_REGION_SHRINK_NEAREST", "nearest"},
{VIPS_REGION_SHRINK_LAST, "VIPS_REGION_SHRINK_LAST", "last"},
{0, NULL, NULL}
};

View File

@ -36,6 +36,8 @@
* - add vips_image_get_n_pages()
* 20/6/19
* - add vips_image_get/set_array_int()
* 31/1/19
* - lock for metadata changes
*/
/*
@ -130,6 +132,11 @@
* these types, it can be copied between images efficiently.
*/
/* Use in various small places where we need a mutex and it's not worth
* making a private one.
*/
static GMutex *vips__meta_lock = NULL;
/* We have to keep the gtype as a string, since we statically init this.
*/
typedef struct _HeaderField {
@ -934,10 +941,10 @@ meta_cp( VipsImage *dst, const VipsImage *src )
/* We lock with vips_image_set() to stop races in highly-
* threaded applications.
*/
g_mutex_lock( vips__global_lock );
g_mutex_lock( vips__meta_lock );
vips_slist_map2( src->meta_traverse,
(VipsSListMap2Fn) meta_cp_field, dst, NULL );
g_mutex_unlock( vips__global_lock );
g_mutex_unlock( vips__meta_lock );
}
return( 0 );
@ -1025,13 +1032,6 @@ vips_image_set( VipsImage *image, const char *name, GValue *value )
g_assert( name );
g_assert( value );
/* If this image is shared, block metadata changes.
*/
if( G_OBJECT( image )->ref_count > 1 ) {
g_warning( "can't set metadata \"%s\" on shared image", name );
return;
}
meta_init( image );
/* We lock between modifying metadata and copying metadata between
@ -1041,9 +1041,9 @@ vips_image_set( VipsImage *image, const char *name, GValue *value )
* metadata copy on another -- this can lead to crashes in
* highly-threaded applications.
*/
g_mutex_lock( vips__global_lock );
g_mutex_lock( vips__meta_lock );
(void) meta_new( image, name, value );
g_mutex_unlock( vips__global_lock );
g_mutex_unlock( vips__meta_lock );
/* If we're setting an EXIF data block, we need to automatically expand
* out all the tags. This will set things like xres/yres too.
@ -1240,14 +1240,6 @@ vips_image_remove( VipsImage *image, const char *name )
result = FALSE;
/* If this image is shared, block metadata changes.
*/
if( G_OBJECT( image )->ref_count > 1 ) {
g_warning( "can't remove metadata \"%s\" on shared image",
name );
return( result );
}
if( image->meta ) {
/* We lock between modifying metadata and copying metadata
* between images, see meta_cp().
@ -1256,9 +1248,9 @@ vips_image_remove( VipsImage *image, const char *name )
* racing with metadata copy on another -- this can lead to
* crashes in highly-threaded applications.
*/
g_mutex_lock( vips__global_lock );
g_mutex_lock( vips__meta_lock );
result = g_hash_table_remove( image->meta, name );
g_mutex_unlock( vips__global_lock );
g_mutex_unlock( vips__meta_lock );
}
return( result );
@ -2027,3 +2019,12 @@ vips_image_get_history( VipsImage *image )
return( image->Hist ? image->Hist : "" );
}
/* Called during vips_init().
*/
void
vips__meta_init( void )
{
if( !vips__meta_lock )
vips__meta_lock = vips_g_mutex_new();
}

View File

@ -127,6 +127,8 @@ int vips__leak = 0;
GQuark vips__image_pixels_quark = 0;
#endif /*DEBUG_LEAK*/
static gint64 vips_pipe_read_limit = 1024 * 1024 * 1024;
/**
* vips_get_argv0:
*
@ -382,6 +384,7 @@ vips_init( const char *argv0 )
vips__threadpool_init();
vips__buffer_init();
vips__meta_init();
/* This does an unsynchronised static hash table init on first call --
* we have to make sure we do this single-threaded. See:
@ -427,19 +430,20 @@ vips_init( const char *argv0 )
g_free( locale );
bind_textdomain_codeset( GETTEXT_PACKAGE, "UTF-8" );
/* Deprecated, this is just for compat.
*/
if( g_getenv( "VIPS_INFO" ) ||
g_getenv( "IM_INFO" ) )
vips_info_set( TRUE );
if( g_getenv( "VIPS_PROFILE" ) )
vips_profile_set( TRUE );
/* Default various settings from env.
*/
if( g_getenv( "VIPS_LEAK" ) )
vips_leak_set( TRUE );
if( g_getenv( "VIPS_TRACE" ) )
vips_cache_set_trace( TRUE );
if( g_getenv( "VIPS_PIPE_READ_LIMIT" ) )
vips_pipe_read_limit =
g_ascii_strtoll( g_getenv( "VIPS_PIPE_READ_LIMIT" ),
NULL, 10 );
vips_pipe_read_limit_set( vips_pipe_read_limit );
/* Register base vips types.
*/
@ -814,6 +818,9 @@ static GOptionEntry option_entries[] = {
{ "vips-version", 0, G_OPTION_FLAG_NO_ARG,
G_OPTION_ARG_CALLBACK, (gpointer) &vips_lib_version_cb,
N_( "print libvips version" ), NULL },
{ "vips-pipe-read-limit", 0, 0,
G_OPTION_ARG_INT64, (gpointer) &vips_pipe_read_limit,
N_( "read at most this many bytes from a pipe" ), NULL },
{ NULL }
};
@ -1192,8 +1199,8 @@ vips_version( int flag )
* vips_leak_set:
* @leak: turn leak checking on or off
*
* Turn on or off vips leak checking. See also --vips-leak and
* vips_add_option_entries().
* Turn on or off vips leak checking. See also --vips-leak,
* vips_add_option_entries() and the `VIPS_LEAK` environment variable.
*
* You should call this very early in your program.
*/

View File

@ -1235,6 +1235,8 @@ vips_region_shrink_uncoded_mean( VipsRegion *from,
* IS stable with respect to the initial arrangement of input values
*/
#define SHRINK_TYPE_MEDIAN( TYPE ) { \
int ls = VIPS_REGION_LSKIP( from ); \
\
for( x = 0; x < target->width; x++ ) { \
TYPE *tp = (TYPE *) p; \
TYPE *tp1 = (TYPE *) (p + ls); \
@ -1265,6 +1267,8 @@ vips_region_shrink_uncoded_mean( VipsRegion *from,
* IS stable with respect to the initial arrangement of input values
*/
#define SHRINK_TYPE_MODE( TYPE ) { \
int ls = VIPS_REGION_LSKIP( from ); \
\
for( x = 0; x < target->width; x++ ) { \
TYPE *tp = (TYPE *) p; \
TYPE *tp1 = (TYPE *) (p + ls); \
@ -1283,14 +1287,14 @@ vips_region_shrink_uncoded_mean( VipsRegion *from,
tq[z] = v[index]; \
} \
\
/* Move on two pels in input. \
*/ \
p += ps << 1; \
q += ps; \
} \
}
#define SHRINK_TYPE_MAX( TYPE ) { \
int ls = VIPS_REGION_LSKIP( from ); \
\
for( x = 0; x < target->width; x++ ) { \
TYPE *tp = (TYPE *) p; \
TYPE *tp1 = (TYPE *) (p + ls); \
@ -1303,14 +1307,14 @@ vips_region_shrink_uncoded_mean( VipsRegion *from,
); \
} \
\
/* Move on two pels in input. \
*/ \
p += ps << 1; \
q += ps; \
} \
}
#define SHRINK_TYPE_MIN( TYPE ) { \
int ls = VIPS_REGION_LSKIP( from ); \
\
for( x = 0; x < target->width; x++ ) { \
TYPE *tp = (TYPE *) p; \
TYPE *tp1 = (TYPE *) (p + ls); \
@ -1323,8 +1327,19 @@ vips_region_shrink_uncoded_mean( VipsRegion *from,
); \
} \
\
/* Move on two pels in input. \
*/ \
p += ps << 1; \
q += ps; \
} \
}
#define SHRINK_TYPE_NEAREST( TYPE ) { \
for( x = 0; x < target->width; x++ ) { \
TYPE *tp = (TYPE *) p; \
TYPE *tq = (TYPE *) q; \
\
for( z = 0; z < nb; z++ ) \
tq[z] = tp[z]; \
\
p += ps << 1; \
q += ps; \
} \
@ -1335,7 +1350,6 @@ static void \
vips_region_shrink_uncoded_ ## OP( VipsRegion *from, \
VipsRegion *to, const VipsRect *target ) \
{ \
int ls = VIPS_REGION_LSKIP( from ); \
int ps = VIPS_IMAGE_SIZEOF_PEL( from->im ); \
int nb = from->im->Bands; \
\
@ -1377,6 +1391,7 @@ VIPS_REGION_SHRINK( MAX );
VIPS_REGION_SHRINK( MIN );
VIPS_REGION_SHRINK( MODE );
VIPS_REGION_SHRINK( MEDIAN );
VIPS_REGION_SHRINK( NEAREST );
/* Generate area @target in @to using pixels in @from. Non-complex.
*/
@ -1405,6 +1420,10 @@ vips_region_shrink_uncoded( VipsRegion *from,
vips_region_shrink_uncoded_MIN( from, to, target );
break;
case VIPS_REGION_SHRINK_NEAREST:
vips_region_shrink_uncoded_NEAREST( from, to, target );
break;
default:
g_assert_not_reached();
}

View File

@ -1,7 +1,10 @@
/* A byte source/sink .. it can be a pipe, file descriptor, memory area,
* socket, node.js stream, etc.
*
* J.Cupitt, 19/6/14
* 19/6/14
*
* 3/2/20
* - add vips_pipe_read_limit_set()
*/
/*
@ -84,10 +87,40 @@
#define MODE_READWRITE BINARYIZE (O_RDWR)
#define MODE_WRITE BINARYIZE (O_WRONLY | O_CREAT | O_TRUNC)
/* -1 on a pipe isn't actually unbounded. Have a limit to prevent
* huge sources accidentally filling memory.
*
* This can be configured with vips_pipe_read_limit_set().
*/
static gint64 vips__pipe_read_limit = 1024 * 1024 * 1024;
/**
* vips_pipe_read_limit_set:
* @limit: maximum number of bytes to buffer from a pipe
*
* If a source does not support mmap or seek and the source is
* used with a loader that can only work from memory, then the data will be
* automatically read into memory to EOF before the loader starts. This can
* produce high memory use if the descriptor represents a large object.
*
* Use vips_pipe_read_limit_set() to limit the size of object that
* will be read in this way. The default is 1GB.
*
* Set a value of -1 to mean no limit.
*
* See also: `--vips-pipe-read-limit` and the environment variable
* `VIPS_PIPE_READ_LIMIT`.
*/
void
vips_pipe_read_limit_set( gint64 limit )
{
vips__pipe_read_limit = limit;
}
G_DEFINE_TYPE( VipsSource, vips_source, VIPS_TYPE_CONNECTION );
/* We can't test for seekability or length during _build, since the read and
* seek signal handlers may not have been connected yet. Instead, we test
* seek signal handlers might not have been connected yet. Instead, we test
* when we first need to know.
*/
static int
@ -184,6 +217,9 @@ vips_source_sanity( VipsSource *source )
g_assert( source->length == -1 );
}
else {
/* Something like a seekable file.
*/
/* After we're done with the header, the sniff buffer should
* be gone.
*/
@ -377,6 +413,14 @@ vips_source_new_from_descriptor( int descriptor )
*
* Create an source attached to a file.
*
* If this descriptor does not support mmap and the source is
* used with a loader that can only work from memory, then the data will be
* automatically read into memory to EOF before the loader starts. This can
* produce high memory use if the descriptor represents a large object.
*
* Use vips_pipe_read_limit_set() to limit the size of object that
* will be read in this way. The default is 1GB.
*
* Returns: a new source.
*/
VipsSource *
@ -715,19 +759,14 @@ vips_source_read( VipsSource *source, void *buffer, size_t length )
return( total_read );
}
/* -1 on a pipe isn't actually unbounded. Have a limit to prevent
* huge sources accidentally filling memory.
*
* 1gb. Why not.
*/
static const int vips_pipe_read_limit = 1024 * 1024 * 1024;
/* Read to a position. -1 means read to end of source. Does not change
* read_position.
*/
static int
vips_source_pipe_read_to_position( VipsSource *source, gint64 target )
{
const char *nick = vips_connection_nick( VIPS_CONNECTION( source ) );
gint64 old_read_position;
unsigned char buffer[4096];
@ -742,7 +781,7 @@ vips_source_pipe_read_to_position( VipsSource *source, gint64 target )
(target < 0 ||
(source->length != -1 &&
target > source->length)) ) {
vips_error( vips_connection_nick( VIPS_CONNECTION( source ) ),
vips_error( nick,
_( "bad read to %" G_GINT64_FORMAT ), target );
return( -1 );
}
@ -760,9 +799,9 @@ vips_source_pipe_read_to_position( VipsSource *source, gint64 target )
break;
if( target == -1 &&
source->read_position > vips_pipe_read_limit ) {
vips_error( vips_connection_nick( VIPS_CONNECTION( source ) ),
"%s", _( "pipe too long" ) );
vips__pipe_read_limit != -1 &&
source->read_position > vips__pipe_read_limit ) {
vips_error( nick, "%s", _( "pipe too long" ) );
return( -1 );
}
}
@ -1013,6 +1052,7 @@ vips_source_map_blob( VipsSource *source )
gint64
vips_source_seek( VipsSource *source, gint64 offset, int whence )
{
const char *nick = vips_connection_nick( VIPS_CONNECTION( source ) );
VipsSourceClass *class = VIPS_SOURCE_GET_CLASS( source );
gint64 new_pos;
@ -1039,8 +1079,7 @@ vips_source_seek( VipsSource *source, gint64 offset, int whence )
break;
default:
vips_error( vips_connection_nick( VIPS_CONNECTION( source ) ),
"%s", _( "bad 'whence'" ) );
vips_error( nick, "%s", _( "bad 'whence'" ) );
return( -1 );
}
}
@ -1066,8 +1105,7 @@ vips_source_seek( VipsSource *source, gint64 offset, int whence )
break;
default:
vips_error( vips_connection_nick( VIPS_CONNECTION( source ) ),
"%s", _( "bad 'whence'" ) );
vips_error( nick, "%s", _( "bad 'whence'" ) );
return( -1 );
}
}
@ -1081,7 +1119,7 @@ vips_source_seek( VipsSource *source, gint64 offset, int whence )
if( new_pos < 0 ||
(source->length != -1 &&
new_pos > source->length) ) {
vips_error( vips_connection_nick( VIPS_CONNECTION( source ) ),
vips_error( nick,
_( "bad seek to %" G_GINT64_FORMAT ), new_pos );
return( -1 );
}

View File

@ -27,6 +27,11 @@ SVG_FILE = os.path.join(IMAGES, "logo.svg")
SVGZ_FILE = os.path.join(IMAGES, "logo.svgz")
SVG_GZ_FILE = os.path.join(IMAGES, "logo.svg.gz")
GIF_ANIM_FILE = os.path.join(IMAGES, "cogs.gif")
GIF_ANIM_EXPECTED_PNG_FILE = os.path.join(IMAGES, "cogs.png")
GIF_ANIM_DISPOSE_BACKGROUND_FILE = os.path.join(IMAGES, "dispose-background.gif")
GIF_ANIM_DISPOSE_BACKGROUND_EXPECTED_PNG_FILE = os.path.join(IMAGES, "dispose-background.png")
GIF_ANIM_DISPOSE_PREVIOUS_FILE = os.path.join(IMAGES, "dispose-previous.gif")
GIF_ANIM_DISPOSE_PREVIOUS_EXPECTED_PNG_FILE = os.path.join(IMAGES, "dispose-previous.png")
DICOM_FILE = os.path.join(IMAGES, "dicom_test_image.dcm")
BMP_FILE = os.path.join(IMAGES, "MARBLES.BMP")
NIFTI_FILE = os.path.join(IMAGES, "avg152T1_LR_nifti.nii.gz")

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,5 +1,5 @@
# vim: set fileencoding=utf-8 :
import filecmp
import sys
import os
import shutil
@ -12,6 +12,9 @@ from helpers import \
ANALYZE_FILE, GIF_FILE, WEBP_FILE, EXR_FILE, FITS_FILE, OPENSLIDE_FILE, \
PDF_FILE, SVG_FILE, SVGZ_FILE, SVG_GZ_FILE, GIF_ANIM_FILE, DICOM_FILE, \
BMP_FILE, NIFTI_FILE, ICO_FILE, HEIC_FILE, TRUNCATED_FILE, \
GIF_ANIM_EXPECTED_PNG_FILE, \
GIF_ANIM_DISPOSE_BACKGROUND_FILE, GIF_ANIM_DISPOSE_BACKGROUND_EXPECTED_PNG_FILE, \
GIF_ANIM_DISPOSE_PREVIOUS_FILE, GIF_ANIM_DISPOSE_PREVIOUS_EXPECTED_PNG_FILE, \
temp_filename, assert_almost_equal_objects, have, skip_if_no
@ -419,11 +422,14 @@ class TestForeign:
assert a.height == b.height
assert a.avg() == b.avg()
# region-shrink added in 8.7
x = pyvips.Image.new_from_file(TIF_FILE)
buf = x.tiffsave_buffer(tile=True, pyramid=True, region_shrink="mean")
buf = x.tiffsave_buffer(tile=True, pyramid=True, region_shrink="mode")
buf = x.tiffsave_buffer(tile=True, pyramid=True, region_shrink="median")
buf = x.tiffsave_buffer(tile=True, pyramid=True, region_shrink="max")
buf = x.tiffsave_buffer(tile=True, pyramid=True, region_shrink="min")
buf = x.tiffsave_buffer(tile=True, pyramid=True,
region_shrink="nearest")
@skip_if_no("magickload")
def test_magickload(self):
@ -703,6 +709,38 @@ class TestForeign:
x2 = pyvips.Image.new_from_file(GIF_ANIM_FILE, page=1, n=-1)
assert x2.height == 4 * x1.height
animation = pyvips.Image.new_from_file(GIF_ANIM_FILE, n=-1)
filename = temp_filename(self.tempdir, '.png')
animation.write_to_file(filename)
# Uncomment to see output file
# animation.write_to_file('cogs.png')
assert filecmp.cmp(GIF_ANIM_EXPECTED_PNG_FILE, filename, shallow=False)
@skip_if_no("gifload")
def test_gifload_animation_dispose_background(self):
animation = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_BACKGROUND_FILE, n=-1)
filename = temp_filename(self.tempdir, '.png')
animation.write_to_file(filename)
# Uncomment to see output file
# animation.write_to_file('dispose-background.png')
assert filecmp.cmp(GIF_ANIM_DISPOSE_BACKGROUND_EXPECTED_PNG_FILE, filename, shallow=False)
@skip_if_no("gifload")
def test_gifload_animation_dispose_previous(self):
animation = pyvips.Image.new_from_file(GIF_ANIM_DISPOSE_PREVIOUS_FILE, n=-1)
filename = temp_filename(self.tempdir, '.png')
animation.write_to_file(filename)
# Uncomment to see output file
# animation.write_to_file('dispose-previous.png')
assert filecmp.cmp(GIF_ANIM_DISPOSE_PREVIOUS_EXPECTED_PNG_FILE, filename, shallow=False)
@skip_if_no("svgload")
def test_svgload(self):
def svg_valid(im):

View File

@ -96,6 +96,10 @@
* - add --intent
* 23/10/17
* - --size Nx didn't work, argh ... thanks jrochkind
* 3/2/20
* - add --no-rotate
* - add --import-profile / --export-profile names
* - back to -o for output
*/
#ifdef HAVE_CONFIG_H
@ -127,8 +131,8 @@ static char *import_profile = NULL;
static gboolean delete_profile = FALSE;
static gboolean linear_processing = FALSE;
static gboolean crop_image = FALSE;
static gboolean no_rotate_image = FALSE;
static char *smartcrop_image = NULL;
static gboolean rotate_image = FALSE;
static char *thumbnail_intent = NULL;
/* Deprecated and unused.
@ -138,25 +142,22 @@ static gboolean nodelete_profile = FALSE;
static gboolean verbose = FALSE;
static char *convolution_mask = NULL;
static char *interpolator = NULL;
static gboolean rotate_image = FALSE;
static GOptionEntry options[] = {
{ "size", 's', 0,
G_OPTION_ARG_STRING, &thumbnail_size,
N_( "shrink to SIZE or to WIDTHxHEIGHT" ),
N_( "SIZE" ) },
{ "output", 'o', G_OPTION_FLAG_HIDDEN,
{ "output", 'o', 0,
G_OPTION_ARG_STRING, &output_format,
N_( "set output to FORMAT" ),
N_( "output to FORMAT" ),
N_( "FORMAT" ) },
{ "format", 'f', 0,
G_OPTION_ARG_STRING, &output_format,
N_( "set output format string to FORMAT" ),
N_( "FORMAT" ) },
{ "eprofile", 'e', 0,
{ "export-profile", 'e', 0,
G_OPTION_ARG_FILENAME, &export_profile,
N_( "export with PROFILE" ),
N_( "PROFILE" ) },
{ "iprofile", 'i', 0,
{ "import-profile", 'i', 0,
G_OPTION_ARG_FILENAME, &import_profile,
N_( "import untagged images with PROFILE" ),
N_( "PROFILE" ) },
@ -171,13 +172,28 @@ static GOptionEntry options[] = {
G_OPTION_ARG_STRING, &thumbnail_intent,
N_( "ICC transform with INTENT" ),
N_( "INTENT" ) },
{ "rotate", 't', 0,
G_OPTION_ARG_NONE, &rotate_image,
N_( "auto-rotate" ), NULL },
{ "delete", 'd', 0,
G_OPTION_ARG_NONE, &delete_profile,
N_( "delete profile from exported image" ), NULL },
{ "no-rotate", 0, 0,
G_OPTION_ARG_NONE, &no_rotate_image,
N_( "don't auto-rotate" ), NULL },
{ "format", 'f', G_OPTION_FLAG_HIDDEN,
G_OPTION_ARG_STRING, &output_format,
N_( "set output format string to FORMAT" ),
N_( "FORMAT" ) },
{ "eprofile", 0, G_OPTION_FLAG_HIDDEN,
G_OPTION_ARG_FILENAME, &export_profile,
N_( "export with PROFILE" ),
N_( "PROFILE" ) },
{ "iprofile", 0, G_OPTION_FLAG_HIDDEN,
G_OPTION_ARG_FILENAME, &import_profile,
N_( "import untagged images with PROFILE" ),
N_( "PROFILE" ) },
{ "rotate", 't', G_OPTION_FLAG_HIDDEN,
G_OPTION_ARG_NONE, &rotate_image,
N_( "(deprecated, does nothing)" ), NULL },
{ "crop", 'c', G_OPTION_FLAG_HIDDEN,
G_OPTION_ARG_NONE, &crop_image,
N_( "(deprecated, crop exactly to SIZE)" ), NULL },
@ -280,11 +296,11 @@ thumbnail_process( VipsObject *process, const char *filename )
if( vips_thumbnail( filename, &image, thumbnail_width,
"height", thumbnail_height,
"size", size_restriction,
"auto_rotate", rotate_image,
"no-rotate", no_rotate_image,
"crop", interesting,
"linear", linear_processing,
"import_profile", import_profile,
"export_profile", export_profile,
"import-profile", import_profile,
"export-profile", export_profile,
"intent", intent,
NULL ) )
return( -1 );