From f88dab9ccdecd1948239fffbd86629db247e5c05 Mon Sep 17 00:00:00 2001 From: alon-ne Date: Thu, 6 Feb 2020 17:36:13 +0200 Subject: [PATCH] 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 --- ChangeLog | 11 + libvips/foreign/foreign.c | 14 +- libvips/foreign/gifload.c | 707 +++++++++--------- libvips/foreign/ppmload.c | 8 + libvips/foreign/tiff2vips.c | 96 ++- libvips/foreign/tiffload.c | 6 +- libvips/foreign/tiffsave.c | 4 + libvips/foreign/vips2tiff.c | 78 +- libvips/include/vips/connection.h | 2 + libvips/include/vips/foreign.h | 2 + libvips/include/vips/private.h | 2 + libvips/include/vips/region.h | 2 + libvips/iofuncs/enumtypes.c | 1 + libvips/iofuncs/header.c | 43 +- libvips/iofuncs/init.c | 23 +- libvips/iofuncs/region.c | 33 +- libvips/iofuncs/source.c | 74 +- test/test-suite/helpers/helpers.py | 5 + test/test-suite/images/cogs.png | Bin 0 -> 4016 bytes test/test-suite/images/dispose-background.gif | Bin 0 -> 7489 bytes test/test-suite/images/dispose-background.png | Bin 0 -> 15709 bytes test/test-suite/images/dispose-previous.gif | Bin 0 -> 1092 bytes test/test-suite/images/dispose-previous.png | Bin 0 -> 2370 bytes test/test-suite/test_foreign.py | 42 +- tools/vipsthumbnail.c | 46 +- 25 files changed, 765 insertions(+), 434 deletions(-) create mode 100644 test/test-suite/images/cogs.png create mode 100644 test/test-suite/images/dispose-background.gif create mode 100644 test/test-suite/images/dispose-background.png create mode 100644 test/test-suite/images/dispose-previous.gif create mode 100644 test/test-suite/images/dispose-previous.png diff --git a/ChangeLog b/ChangeLog index dbbb6984..7c2e8aa9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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 diff --git a/libvips/foreign/foreign.c b/libvips/foreign/foreign.c index aec3337a..86632973 100644 --- a/libvips/foreign/foreign.c +++ b/libvips/foreign/foreign.c @@ -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 diff --git a/libvips/foreign/gifload.c b/libvips/foreign/gifload.c index 0ff2273b..20580e83 100644 --- a/libvips/foreign/gifload.c +++ b/libvips/foreign/gifload.c @@ -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,9 +181,13 @@ typedef struct _VipsForeignLoadGif { */ VipsImage *frame; - /* A copy of the previous frame, in case we need a DISPOSE_PREVIOUS. - */ - VipsImage *previous; + /* 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; /* The position of @frame, in pages. */ @@ -191,10 +210,10 @@ typedef struct _VipsForeignLoadGif { */ guint32 cmap[256]; - /* As we scan the file, the index of the transparent pixel for this - * frame. - */ - int transparency; + /* As we scan the file, the index of the transparent pixel for this + * frame. + */ + 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,42 +347,58 @@ 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 )) ) { - vips_foreign_load_gif_error_vips( gif, GifLastError() ); +#else + 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 ); + return( -1 ); } #endif @@ -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. @@ -459,7 +466,7 @@ vips_foreign_load_gif_allocate_delays( VipsForeignLoadGif *gif ) int i; gif->delays_length = gif->delays_length + gif->n_pages + 64; - gif->delays = (int *) g_realloc( gif->delays, + gif->delays = (int *) g_realloc( gif->delays, gif->delays_length * sizeof( int ) ); for( i = old; i < gif->delays_length; i++ ) gif->delays[i] = 40; @@ -499,7 +506,7 @@ vips_foreign_load_gif_code_next( VipsForeignLoadGif *gif, /* Quickly scan an image record. */ static int -vips_foreign_load_gif_scan_image( VipsForeignLoadGif *gif ) +vips_foreign_load_gif_scan_image( VipsForeignLoadGif *gif ) { VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( gif ); GifFileType *file = gif->file; @@ -508,7 +515,7 @@ vips_foreign_load_gif_scan_image( VipsForeignLoadGif *gif ) GifByteType *extension; if( DGifGetImageDesc( gif->file ) == GIF_ERROR ) { - vips_foreign_load_gif_error( gif ); + vips_foreign_load_gif_error( gif ); return( -1 ); } @@ -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 ) { @@ -574,7 +582,7 @@ vips_foreign_load_gif_scan_application_ext( VipsForeignLoadGif *gif, extension[0] == 3 && extension[1] == 1 ) { gif->loop = extension[2] | (extension[3] << 8); - if( gif->loop != 0 ) + if( gif->loop != 0 ) gif->loop += 1; } } @@ -621,14 +629,14 @@ 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; } /* giflib uses centiseconds, we use ms. */ - gif->delays[gif->n_pages] = + gif->delays[gif->n_pages] = (extension[2] | (extension[3] << 8)) * 10; while( extension != NULL ) @@ -686,16 +694,17 @@ 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. */ vips_image_set_int( image, "gif-delay", VIPS_RINT( gif->delays[0] / 10.0 ) ); - vips_image_set_array_int( image, + vips_image_set_array_int( image, "delay", gif->delays, gif->n_pages ); - } + } else vips_image_set_int( image, "gif-delay", 4 ); @@ -722,7 +731,7 @@ vips_foreign_load_gif_scan( VipsForeignLoadGif *gif ) gif->n_pages = 0; do { - if( DGifGetRecordType( gif->file, &record ) == GIF_ERROR ) + if( DGifGetRecordType( gif->file, &record ) == GIF_ERROR ) continue; switch( record ) { @@ -785,7 +794,7 @@ vips_foreign_load_gif_header( VipsForeignLoad *load ) gif->file->SWidth > VIPS_MAX_COORD || gif->file->SHeight <= 0 || gif->file->SHeight > VIPS_MAX_COORD ) { - vips_error( class->nickname, + vips_error( class->nickname, "%s", _( "image size out of bounds" ) ); (void) vips_foreign_load_gif_close_giflib( gif ); @@ -794,7 +803,7 @@ vips_foreign_load_gif_header( VipsForeignLoad *load ) /* Allocate a line buffer now that we have the GIF width. */ - if( !(gif->line = + if( !(gif->line = VIPS_ARRAY( NULL, gif->file->SWidth, GifPixelType )) || vips_foreign_load_gif_scan( gif ) || vips_foreign_load_gif_set_header( gif, load->out ) ) { @@ -838,29 +847,22 @@ 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; - int x; + guint32 *idst; + idst = (guint32 *) dst; - iq = (guint32 *) q; - for( x = 0; x < width; x++ ) { - VipsPel v = p[x]; + int x; + for( x = 0; x < width; 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 - /* Blast in the RGBA for this value. - */ - iq[x] = gif->cmap[v]; - } + VipsPel v = gif->line[x]; + + if( v != gif->transparent_index ) { + /* Blast in the RGBA for this value. + */ + idst[x] = gif->cmap[v]; + } + } } /* Render the current gif frame into an RGBA buffer. GIFs can accumulate, @@ -869,37 +871,25 @@ vips_foreign_load_gif_render_line( VipsForeignLoadGif *gif, static int vips_foreign_load_gif_render( VipsForeignLoadGif *gif ) { - GifFileType *file = gif->file; + GifFileType *file = gif->file; - if( DGifGetImageDesc( file ) == GIF_ERROR ) { - vips_foreign_load_gif_error( gif ); - return( -1 ); - } + if( DGifGetImageDesc( file ) == GIF_ERROR ) { + vips_foreign_load_gif_error( gif ); + return( -1 ); + } - /* Update the colour map for this frame. - */ - vips_foreign_load_gif_build_cmap( gif ); + /* Update the colour map for this frame. + */ + vips_foreign_load_gif_build_cmap( gif ); - /* BACKGROUND means we reset the frame to 0 (transparent) before we - * render the next set of pixels. - */ - 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 ), - 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 ) ); + /* 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_PREVIOUS ) { + memcpy( VIPS_IMAGE_ADDR( gif->scratch, 0, 0 ), + VIPS_IMAGE_ADDR( gif->previous, 0, 0 ), + 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 ) { + vips_foreign_load_gif_error( gif ); + return( -1 ); + } - if( DGifGetLine( gif->file, gif->line, - file->Image.Width ) == GIF_ERROR ) { - vips_foreign_load_gif_error( gif ); - return( -1 ); - } + VipsPel *dst = VIPS_IMAGE_ADDR(gif->scratch, file->Image.Left, file->Image.Top + y ); - vips_foreign_load_gif_render_line( gif, - file->Image.Width, q, gif->line ); - } - } - } - else { - int y; + vips_foreign_load_gif_render_line( gif, file->Image.Width, dst ); + } + } + } + else { - 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 ); + 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 ); - for( y = 0; y < file->Image.Height; y++ ) { - VipsPel *q = VIPS_IMAGE_ADDR( gif->frame, - file->Image.Left, file->Image.Top + y ); + int y; + for( y = 0; y < file->Image.Height; y++ ) { - if( DGifGetLine( gif->file, gif->line, - file->Image.Width ) == GIF_ERROR ) { - vips_foreign_load_gif_error( gif ); - return( -1 ); - } + 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 ); - return( 0 ); + 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( "" ); + } +} +#endif /*VIPS_DEBUG*/ + static int vips_foreign_load_gif_extension( VipsForeignLoadGif *gif ) { @@ -993,21 +1021,25 @@ vips_foreign_load_gif_extension( VipsForeignLoadGif *gif ) if( extension && ext_code == GRAPHICS_EXT_FUNC_CODE && extension[0] == 4 ) { - /* 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]; - /* 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; + int flags = extension[1]; + + /* Bytes are flags, delay low, delay high, + * transparency. Flag bit 1 means transparency + * is being set. + */ + 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 = ( 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 ) if( vips_foreign_load_gif_ext_next( gif, &extension ) ) @@ -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. */ @@ -1215,8 +1249,8 @@ vips_foreign_load_gif_load( VipsForeignLoad *load ) /* Close input immediately at end of read. */ - g_signal_connect( t[0], "minimise", - G_CALLBACK( vips_foreign_load_gif_minimise ), gif ); + g_signal_connect( t[0], "minimise", + G_CALLBACK( vips_foreign_load_gif_minimise ), gif ); /* Strips 8 pixels high to avoid too many tiny regions. */ @@ -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" ), @@ -1290,13 +1304,13 @@ vips_foreign_load_gif_class_init( VipsForeignLoadGifClass *class ) static void vips_foreign_load_gif_init( VipsForeignLoadGif *gif ) { - gif->n = 1; - gif->transparency = -1; + gif->n = 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 ) -{ - VipsForeignLoadGif *gif = (VipsForeignLoadGif *) gfile->UserData; - VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif; - - 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 ) +vips_foreign_load_gif_file_build( VipsObject *object ) { - VipsForeignLoad *load = (VipsForeignLoad *) gif; - VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) gif; + VipsForeignLoadGifFile *file = (VipsForeignLoadGifFile *) object; + VipsForeignLoadGif *gif = (VipsForeignLoadGif *) object; - 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 ); +} diff --git a/libvips/foreign/ppmload.c b/libvips/foreign/ppmload.c index 78709f4b..6df91058 100644 --- a/libvips/foreign/ppmload.c +++ b/libvips/foreign/ppmload.c @@ -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 ) diff --git a/libvips/foreign/tiff2vips.c b/libvips/foreign/tiff2vips.c index 56935afb..c8b7873e 100644 --- a/libvips/foreign/tiff2vips.c +++ b/libvips/foreign/tiff2vips.c @@ -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; @@ -2104,7 +2168,7 @@ rtiff_read_stripwise( Rtiff *rtiff, VipsImage *out ) /* Double check: in memcpy mode, the vips linesize should exactly * match the tiff line size. */ - if( rtiff->memcpy ) { + if( rtiff->memcpy ) { size_t vips_line_size; /* Lines are smaller in plane-separated mode. @@ -2198,13 +2262,15 @@ rtiff_header_read( Rtiff *rtiff, RtiffHeader *header ) TIFFGetFieldDefaulted( rtiff->tiff, TIFFTAG_COMPRESSION, &header->compression ); + + /* 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 ) - /* We want to always expand subsampled YCBCR images to full - * RGB. - */ TIFFSetField( rtiff->tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB ); - else if( header->photometric_interpretation == PHOTOMETRIC_YCBCR ) { + else if( header->photometric_interpretation == PHOTOMETRIC_YCBCR ) { /* We rely on the jpg decompressor to upsample chroma * subsampled images. If there is chroma subsampling but * no jpg compression, we have to give up. @@ -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 || diff --git a/libvips/foreign/tiffload.c b/libvips/foreign/tiffload.c index 70950410..20b2b094 100644 --- a/libvips/foreign/tiffload.c +++ b/libvips/foreign/tiffload.c @@ -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. diff --git a/libvips/foreign/tiffsave.c b/libvips/foreign/tiffsave.c index 281d486a..8644a820 100644 --- a/libvips/foreign/tiffsave.c +++ b/libvips/foreign/tiffsave.c @@ -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 diff --git a/libvips/foreign/vips2tiff.c b/libvips/foreign/vips2tiff.c index 63e338ac..0edcbe7e 100644 --- a/libvips/foreign/vips2tiff.c +++ b/libvips/foreign/vips2tiff.c @@ -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,17 +780,23 @@ 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. */ - format = SAMPLEFORMAT_UINT; - if( vips_band_format_isuint( wtiff->ready->BandFmt ) ) + if( wtiff->input->Type != VIPS_INTERPRETATION_XYZ ) { + int format; + format = SAMPLEFORMAT_UINT; - else if( vips_band_format_isint( wtiff->ready->BandFmt ) ) - format = SAMPLEFORMAT_INT; - else if( vips_band_format_isfloat( wtiff->ready->BandFmt ) ) - format = SAMPLEFORMAT_IEEEFP; - else if( vips_band_format_iscomplex( wtiff->ready->BandFmt ) ) - format = SAMPLEFORMAT_COMPLEXIEEEFP; - TIFFSetField( tif, TIFFTAG_SAMPLEFORMAT, format ); + if( vips_band_format_isuint( wtiff->ready->BandFmt ) ) + format = SAMPLEFORMAT_UINT; + else if( vips_band_format_isint( wtiff->ready->BandFmt ) ) + format = SAMPLEFORMAT_INT; + else if( vips_band_format_isfloat( wtiff->ready->BandFmt ) ) + format = SAMPLEFORMAT_IEEEFP; + 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; diff --git a/libvips/include/vips/connection.h b/libvips/include/vips/connection.h index 66cd5474..d12a33a6 100644 --- a/libvips/include/vips/connection.h +++ b/libvips/include/vips/connection.h @@ -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), \ diff --git a/libvips/include/vips/foreign.h b/libvips/include/vips/foreign.h index 2447b3d1..1f84f4f9 100644 --- a/libvips/include/vips/foreign.h +++ b/libvips/include/vips/foreign.h @@ -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)); diff --git a/libvips/include/vips/private.h b/libvips/include/vips/private.h index e28735fc..8716cf36 100644 --- a/libvips/include/vips/private.h +++ b/libvips/include/vips/private.h @@ -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*/ diff --git a/libvips/include/vips/region.h b/libvips/include/vips/region.h index bcce3c47..d52a3fa7 100644 --- a/libvips/include/vips/region.h +++ b/libvips/include/vips/region.h @@ -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; diff --git a/libvips/iofuncs/enumtypes.c b/libvips/iofuncs/enumtypes.c index 2f571331..767a43f9 100644 --- a/libvips/iofuncs/enumtypes.c +++ b/libvips/iofuncs/enumtypes.c @@ -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} }; diff --git a/libvips/iofuncs/header.c b/libvips/iofuncs/header.c index 6a6347a3..dba1c767 100644 --- a/libvips/iofuncs/header.c +++ b/libvips/iofuncs/header.c @@ -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(); +} diff --git a/libvips/iofuncs/init.c b/libvips/iofuncs/init.c index bd5a90cc..d3a3310c 100644 --- a/libvips/iofuncs/init.c +++ b/libvips/iofuncs/init.c @@ -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. */ diff --git a/libvips/iofuncs/region.c b/libvips/iofuncs/region.c index ab201ff5..50b4dc83 100644 --- a/libvips/iofuncs/region.c +++ b/libvips/iofuncs/region.c @@ -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(); } diff --git a/libvips/iofuncs/source.c b/libvips/iofuncs/source.c index 3c286007..af482d11 100644 --- a/libvips/iofuncs/source.c +++ b/libvips/iofuncs/source.c @@ -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 ); } diff --git a/test/test-suite/helpers/helpers.py b/test/test-suite/helpers/helpers.py index 68343d00..c493d55e 100644 --- a/test/test-suite/helpers/helpers.py +++ b/test/test-suite/helpers/helpers.py @@ -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") diff --git a/test/test-suite/images/cogs.png b/test/test-suite/images/cogs.png new file mode 100644 index 0000000000000000000000000000000000000000..2256a6a81ca3cec95dfe51b6729c5667a60b40a6 GIT binary patch literal 4016 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 wzEFQ}rPy&oO6%rC3o}RwG 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 literal 0 HcmV?d00001 diff --git a/test/test-suite/images/dispose-background.png b/test/test-suite/images/dispose-background.png new file mode 100644 index 0000000000000000000000000000000000000000..0e29eb523ac0896d4743239c3231c5bdf738a326 GIT binary patch literal 15709 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 literal 0 HcmV?d00001 diff --git a/test/test-suite/images/dispose-previous.gif b/test/test-suite/images/dispose-previous.gif new file mode 100644 index 0000000000000000000000000000000000000000..c4b5b0e654e2c8f6322bb1c9504562b79c8c0476 GIT binary patch literal 1092 zcmZ?wbhEHbOkqf2_`tyMUzOqi|Nn|VSy;Ikm>6^zfB+=J!1TYTf92`7{EO#oxz)Y- z-X4F3wnr9K&)k+}Rc*V#b$pSx{HNOMYUe&zJzLFEFE6R?UUM|&m7AwI^nWnpD z1#OO)7IyIJ%6)x>XMOS(_`m*gd3W>f(?ub*>-NRps<3+b<9c|=qJ`=&%$pM{IwTf( zoX}wL6qLve)ZCbHlf%h!=gXNAjhtGVzZ{~hUaTltm38XW&tETgeXx|irP;l^e7ET7 z>d4=#rFD4bOvti%C4FJfov1lBJhHqW+G5VmdHZKg@7%Y4dFyP?+TP=E>-Ae#cP=kl zn=fB^zn_21y#D;RdS~VJ@7OkMfA&z@VuJjka)#ZUDIOQ(Tv~GT68hZ6rXlUT)J0e%6PTsdHeyP#h!^^|3nPg^8Exjb>aYMh5 z`)q5qxY#B6QtOt&&*44^dung;Jgc5@VybZ`r}?zq5(UD$y_I+)Lo0*(&(4f5{AVMk zm7!wqzW?i=%Ehm)u8DrE$-n@NJ6X3BWJt)gVqioDbpU-ehhZ@kzYcMx}mTCD^@cv6Xjs^h54b*;(A$HQqTabDDXU^rXhDkv=kqdxN%b zG%Ts}F)_xqOW8T6B;wL#mkFAlbuNWLkG;$c zL$9^mWJ~p!ZG7?TmKniUq$=%&a{{lo+`o_#JWH(f@s`YOY=M*gS_4dzx2*`@UG}$n;|LHn-dSm6PKXzB#-yey+68FUUmreBgq7x2(=Dla$p}51@ zV#W336$>kxIUYrHs4=Z{zM0tBT&eMO@lK7I9KSTAPFi|KbylsM^|JE#truAbCAh7% zqaSb2ynRaBadvV!x6B%EMI9FLg)+zY9B3<1JF8+V7Up}WZST>k26uVo^z8iN^6L8L nt=o6*-n)N%^7{7v@%i=r^Vjb`fB*e|euD)E8eeiTFjxZsX#5zQ literal 0 HcmV?d00001 diff --git a/test/test-suite/images/dispose-previous.png b/test/test-suite/images/dispose-previous.png new file mode 100644 index 0000000000000000000000000000000000000000..c22310227b4d938d7e14fba1a4c21899906a6747 GIT binary patch literal 2370 zcmbtWdo&Y#AKy@BLoU%IYMHh%QoUX1(HJvRnMSdcW=te9xfO}o+?D21o+$U*m}@dZ z$~mlX-*PXR{#Kr25l9i!;e(Q#6`x(l6)Nikev8=flq=-o&W%7iC}JO7nDAe;dnD& zM*-KqdJ~ymfd#CM_7W&n8DXyVqL`#W4T4#;ox%m# z1E{C?j&1H)Rcqdv$JUva5E+B2Ra0NVSL`{JKDM3wusx!_4T;mtSt#k_vMePWO)#sOpH{jT`v|;%tAo$MrQoFPE~!D)*mAbJxBKqr}TNPeCu2ch9nXil>MLcRuJb zOI;p?*9oB#t=C}F-0|8XPcpkbm~noqlZIy_8S9qeT}C9Ii(jTs>%*czGag z*O1rht0I?r(IJuMQ_&$fvr&Q8El-6MKZ1@hd%Mh2#iG0o z=hrLQvuOT;iNpxhH+%c1SFj(p33G7m_l5G0`u92WY~Sh}&v0?Dvby0b??=v3KePk{)25F@_*|fNnI44&5 zuW2_%UfY0YKL^fDR`{cBIHRRsPrDlDIPj$_FfDs6lcR8P4k;u*Y;|*1w0mD`Y2v~C z=E?&l(Qg0Top zAgzL+JY`VZJbpp?8`gR@G<#y8;zZRlp)_aQVTYhxQmk36k>6uVbEpS~pRGEIA2zoh zqN^kweJ2rB=_?Bq7AV9mL==AR`zl+URL6;>ua|d9{0Hy<6TbK8JKs-?3rhCr3MARs82I*hE?J|4b)*~w4 ziAvjr-n|yUrjk>wHSYS8>%D^qsShZ7++^QmrAVb@?QbzMeBMDS>HIN|srYVoE?G%% z8UsnoFrL|q({foP(;8XR`h2rU!0yAa=b^>#G=wRVFAToomgQg&8EFnAK7 z?z@5G49V^TY>)|go0yw=5VY{XuL*DVk!ohMdk;CD<<`k)Rm08*1@LLh|yMgsk8jq`5R-3G>SaPV)t!zL4xO@Z| zHjDDIluGqVxGq~|i6jcb_3drk&JyA4wLz-hCw&FK=+$j6Ja*&J8VHDa0EOjEw90W} zO`^wG$6zTcTbFd3N8Fz!>K6JcA$IbxI!G$OEd+|?ns)*deYOxS0P3w^|`fnT(hppn13P-BHMjH18}EyQV%ueR+)J`lyJ&&(=%n)2F23;Pj7okzZK7afee1q|Y&_}0^>jNw ztTp=#J(Zm#=k8Cc0{&Crb6HIVff8boLUn1Bq2t?tz01vs9@IGkLHVRwPpw-`4OsLK zu?uH(cMK`fG(#Sls>EdK->`Zp?foW9p}u%aW89`o_Qt%u{yB7A_f5I?gCo~q$kLbB zW#f>Y6yx!l#OQV@N}QVdK;Pmd*Qcr3sQwx(&GzEmxka{TXlP4Ka`(DO4_?*eC4?s6 z)-W-I72RX%0r0ZrCm3-1VfJq3ZAd!^xuy<@O@8=&;tprNHT$gVh5A4YaP^|<=laUr z+C|n<^xCq7aYS-I*febuZ_i##;l+N`ym8qkBRW%mp{}G$MhXywm5=(j7yU{1Jkzyd zSmH)OBtqol%W`crV+;0=O=>)4!9(F4@Jpb!)w(Kfv~c2WB~ZH_P=buyA2-Y%}Sv9*rxV+wdsZGH6_>Dski+f3lLB8DdKW z1g4(Vm2rppNr^W}F$nD*=LENprM^PEFUwC>dRlXo{m{XuzC_bf<#X8SebS5ZMoBL} zHeo?V{#WmZL<)qdA(7199gM{VciO%aAr*h+MbC@>&I=xc|F?O8^gt>2I5Z!8jYaak z4DVe!*PoGhj62hx07F>Oo!mb?z7m2!0XdrDSu;OadvK=x?l_}RAKDFjX)JH_g DIK4@2 literal 0 HcmV?d00001 diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py index 4c18af71..cb4b4b21 100644 --- a/test/test-suite/test_foreign.py +++ b/test/test-suite/test_foreign.py @@ -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): diff --git a/tools/vipsthumbnail.c b/tools/vipsthumbnail.c index 961d447d..3e9c37cc 100644 --- a/tools/vipsthumbnail.c +++ b/tools/vipsthumbnail.c @@ -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 );