diff --git a/ChangeLog b/ChangeLog index 1f5317f9..e9d18f60 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,7 +6,7 @@ - better vipsheader behaviour with complex field types - added vips_image_hasalpha() - added vips_thumbnail() / vips_thumbnail_buffer() -- webpload reads icc, xmp, exif metadata +- webpload/webpsave read and write icc, xmp, exif metadata 18/10/16 started 8.4.3 - fix error detection in gif_close, thanks aaron42net diff --git a/TODO b/TODO index be4ae29e..2c235732 100644 --- a/TODO +++ b/TODO @@ -2,6 +2,18 @@ test with ubuntu ... seems to have libwebpmux2 + test webp metadata read and write + + webpsave should support strip + + update docs + + need to check for >4gb overflow ... use guint64 for all size calculations + + webpsave needs hooking up to exif.c + + + - not sure about utf8 error messages on win - strange: diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index d58c2c39..d3c87b18 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -196,9 +196,14 @@ int vips__png_write_buf( VipsImage *in, typedef struct _VipsWebPNames { const char *vips; const char *webp; + + /* The webp flag bit for this chunk of metadata + */ + int flag; } VipsWebPNames; extern const VipsWebPNames vips__webp_names[]; +extern const int vips__n_webp_names; extern const char *vips__webp_suffs[]; int vips__iswebp_buffer( const void *buf, size_t len ); diff --git a/libvips/foreign/vips2webp.c b/libvips/foreign/vips2webp.c index 662aae5b..04f590c2 100644 --- a/libvips/foreign/vips2webp.c +++ b/libvips/foreign/vips2webp.c @@ -6,6 +6,8 @@ * - buffer write ignored lossless, thanks aaron42net * 2/5/16 Felix Bünemann * - used advanced encoding API, expose controls + * 8/11/16 + * - add metadata write */ /* @@ -89,26 +91,22 @@ typedef struct { uint8_t *mem; size_t size; size_t max_size; -} VipsWebPMemoryWriter; +} VipsWebPWriter; static void -init_memory_writer( VipsWebPMemoryWriter *writer ) { +vips_webp_writer_init( VipsWebPWriter *writer ) +{ writer->mem = NULL; writer->size = 0; writer->max_size = 0; } static int -memory_write( const uint8_t *data, size_t data_size, - const WebPPicture *picture ) { - VipsWebPMemoryWriter * const writer = - (VipsWebPMemoryWriter*) picture->custom_ptr; - +vips_webp_writer_append( VipsWebPWriter *writer, + const uint8_t *data, size_t data_size ) +{ size_t next_size; - if( !writer ) - return( 0 ); - next_size = writer->size + data_size; if( next_size > writer->max_size ) { @@ -117,13 +115,10 @@ memory_write( const uint8_t *data, size_t data_size, VIPS_MAX( 8192, VIPS_MAX( next_size, writer->max_size * 2 ) ); - if( !(new_mem = (uint8_t*) g_try_malloc( next_max_size )) ) + if( !(new_mem = (uint8_t *) + g_try_realloc( writer->mem, next_max_size )) ) return( 0 ); - if( writer->size > 0 ) - memcpy( new_mem, writer->mem, writer->size ); - - g_free( writer->mem ); writer->mem = new_mem; writer->max_size = next_max_size; } @@ -136,6 +131,78 @@ memory_write( const uint8_t *data, size_t data_size, return( 1 ); } +static int +vips_webp_writer_appendle( VipsWebPWriter *writer, uint32_t val, int n ) +{ + unsigned char buf[4]; + int i; + + g_assert( n < 4 ); + + for( i = 0; i < n; i++ ) { + buf[i] = (unsigned char) (val & 0xff); + val >>= 8; + } + + return( vips_webp_writer_append( writer, buf, n ) ); +} + +static int +vips_webp_writer_appendle32( VipsWebPWriter *writer, uint32_t val ) +{ + return( vips_webp_writer_appendle( writer, val, 4 ) ); +} + +static int +vips_webp_writer_appendle24( VipsWebPWriter *writer, uint32_t val ) +{ + return( vips_webp_writer_appendle( writer, val, 3 ) ); +} + +static int +vips_webp_writer_appendcc( VipsWebPWriter *writer, const char buf[4] ) +{ + return( vips_webp_writer_append( writer, (const uint8_t *) buf, 4 ) ); +} + +static gboolean +vips_webp_writer_appendc( VipsWebPWriter *writer, + const char fourcc[4], const uint8_t *data, size_t data_size ) +{ + const int zero = 0; + gboolean need_padding = (data_size & 1) != 0; + + if( !vips_webp_writer_appendcc( writer, fourcc ) || + !vips_webp_writer_appendle32( writer, data_size ) || + !vips_webp_writer_append( writer, data, data_size ) ) + return( 0 ); + + if( need_padding && + !vips_webp_writer_append( writer, (const uint8_t *) &zero, 1 ) ) + return( 0 ); + + return( 1 ); +} + +static void +vips_webp_writer_unset( VipsWebPWriter *writer ) +{ + VIPS_FREE( writer->mem ); +} + +static int +memory_write( const uint8_t *data, size_t data_size, + const WebPPicture *picture ) +{ + VipsWebPWriter * const writer = + (VipsWebPWriter *) picture->custom_ptr; + + if( !writer ) + return( 0 ); + + return( vips_webp_writer_append( writer, data, data_size ) ); +} + static int write_webp( WebPPicture *pic, VipsImage *in, int Q, gboolean lossless, VipsForeignWebpPreset preset, @@ -147,25 +214,23 @@ write_webp( WebPPicture *pic, VipsImage *in, webp_import import; if ( !WebPConfigPreset( &config, get_preset( preset ), Q ) ) { - vips_error( "vips2webp", - "%s", _( "config version error" ) ); + vips_error( "vips2webp", "%s", _( "config version error" ) ); return( -1 ); } #if WEBP_ENCODER_ABI_VERSION >= 0x0100 config.lossless = lossless || near_lossless; config.alpha_quality = alpha_q; - /* Smart subsampling requires use_argb because + /* Smart subsampling needs use_argb because * it is applied during RGB to YUV conversion. */ pic->use_argb = lossless || near_lossless || smart_subsample; #else - if( lossless || near_lossless ) - vips_warn( "vips2webp", - "%s", _( "lossless unsupported" ) ); + if( lossless || + near_lossless ) + vips_warn( "vips2webp", "%s", _( "lossless unsupported" ) ); if( alpha_q != 100 ) - vips_warn( "vips2webp", - "%s", _( "alpha_q unsupported" ) ); + vips_warn( "vips2webp", "%s", _( "alpha_q unsupported" ) ); #endif #if WEBP_ENCODER_ABI_VERSION >= 0x0209 @@ -175,16 +240,14 @@ write_webp( WebPPicture *pic, VipsImage *in, config.preprocessing |= 4; #else if( near_lossless ) - vips_warn( "vips2webp", - "%s", _( "near_lossless unsupported" ) ); + vips_warn( "vips2webp", "%s", _( "near_lossless unsupported" ) ); if( smart_subsample ) vips_warn( "vips2webp", "%s", _( "smart_subsample unsupported" ) ); #endif if( !WebPValidateConfig( &config ) ) { - vips_error( "vips2webp", - "%s", _( "invalid configuration" ) ); + vips_error( "vips2webp", "%s", _( "invalid configuration" ) ); return( -1 ); } @@ -202,15 +265,13 @@ write_webp( WebPPicture *pic, VipsImage *in, if( !import( pic, VIPS_IMAGE_ADDR( memory, 0, 0 ), VIPS_IMAGE_SIZEOF_LINE( memory ) ) ) { VIPS_UNREF( memory ); - vips_error( "vips2webp", - "%s", _( "picture memory error" ) ); + vips_error( "vips2webp", "%s", _( "picture memory error" ) ); return( -1 ); } if( !WebPEncode( &config, pic ) ) { VIPS_UNREF( memory ); - vips_error( "vips2webp", - "%s", _( "unable to encode" ) ); + vips_error( "vips2webp", "%s", _( "unable to encode" ) ); return( -1 ); } @@ -219,6 +280,196 @@ write_webp( WebPPicture *pic, VipsImage *in, return( 0 ); } +static int +vips_webp_add_chunk( VipsWebPWriter *writer, VipsImage *image, + const char *vips, const char webp[4] ) +{ + if( vips_image_get_typeof( image, vips) ) { + void *data; + size_t length; + + /* We've done this before, it can't fail now. + */ + (void) vips_image_get_blob( image, vips, &data, &length ); + + if( !vips_webp_writer_appendc( writer, webp, data, length ) ) + return( -1 ); + } + + return( 0 ); +} + +/* Turn @writer into a VP8X image with metadata from @image. + * + * Based (partly) on cwep.c + * + * We need to know some detail about the webp file format. The encoder will + * make something like this: + * + * 0 - 3 RIFF + * 4 - 7 size of this data chunk (byte 8 to end of file) + * 8 - 11 WEBP + * 12 - 15 VP8L (L for lossless, space for lossy, possibly 8 for alpha) + * 16 - 19 size of chunk (10 for VP8X, something else otherwise) + * 20 - 23 flags (VP8L has alpha in bit 29 of this; byte 20 has flags for VP8X) + * 24 - + * + * If there is metadata to add, we make a VP8X image, which looks like this: + * + * 0 - 3 RIFF + * 4 - 7 size of this data chunk (byte 8 to end of file) + * 8 - 11 WEBP + * 12 - 15 VP8X + * 16 - 19 10 (size of vp8x chunk) + * 20 - 23 flags + * 24 - 26 width - 1 (note: only 3 bytes) + * 27 - 29 height - 1 (note: only 3 bytes) + * 30 - + * + * Followed by ICCP, ANIM, image, EXIF and XMP chunks, in that order. + * + * See: + * https://developers.google.com/speed/webp/docs/riff_container + */ +static int +vips_webp_add_metadata( VipsWebPWriter *writer, VipsImage *image ) +{ + /* The image in @writer may be VP8X already. + */ + gboolean is_vp8x = !memcmp( writer->mem + 12, "VP8X", 4 ); + gboolean is_lossless = !memcmp( writer->mem + 12, "VP8L", 4 ); + + guint64 metadata_size; + uint32_t flags; + int i; + guint64 new_size; + VipsWebPWriter new; + + /* We have to find the size of the block we will write before we can + * start to write it. + */ + metadata_size = 0; + + /* If there are any flags there already, we add to them. + */ + flags = is_vp8x ? writer->mem[20] : 0; + + for( i = 0; i < vips__n_webp_names; i++ ) { + const char *vips = vips__webp_names[i].vips; + uint32_t flag = vips__webp_names[i].flag; + + if( vips_image_get_typeof( image, vips ) ) { + void *data; + size_t length; + + if( vips_image_get_blob( image, vips, &data, &length ) ) + return( -1 ); + + /* +8 since we have to prepend each chunk with a type + * char[4] and a length guint32. + * + * Chunks are always rounded up to an even size. + */ + metadata_size += length + 8 + (length & 1); + + flags |= flag; + } + } + + if( !metadata_size ) + /* No metadata to write, so we can just leave the image alone. + */ + return( 0 ); + + /* If it's not already vp8x, we'll need to add a vp8x header, and + * that'll add 18 bytes. -8 since size includes the RIFF header. + */ + new_size = writer->size - 8 + (is_vp8x ? 0 : 18) + metadata_size; + + vips_webp_writer_init( &new ); + + if( !vips_webp_writer_appendcc( &new, "RIFF" ) || + !vips_webp_writer_appendle32( &new, new_size ) || + !vips_webp_writer_appendcc( &new, "WEBP" ) || + !vips_webp_writer_appendcc( &new, "VP8X" ) || + !vips_webp_writer_appendle32( &new, 10 ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + + if( is_vp8x ) { + /* Copy the existing VP8X body and update the flag bits. + */ + if( vips_webp_writer_append( &new, writer->mem + 20, 10 ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + new.mem[20] = flags; + } + else { + /* We have to make a new vp8x header. + */ + + /* Presence of alpha is stored in the 29th bit of VP8L + * data. + * + * +12 gets us to the VP8L cc, 4 to skip that, + * another 4 to skip the length, then bit 8 - 3 == 5 + */ + if( is_lossless && + (writer->mem[12 + 8 + 3] & (1 << 5)) ) + flags |= 0x010; + + /* 10 is the length of the VPX8X header chunk. + */ + if( !vips_webp_writer_appendle32( &new, flags ) || + !vips_webp_writer_appendle24( &new, image->Xsize - 1 ) || + !vips_webp_writer_appendle24( &new, image->Ysize- 1 ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + } + + /* Extra chunks have to be in this order. + */ + if( vips_webp_add_chunk( &new, image, VIPS_META_ICC_NAME, "ICCP" ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + + /* The image chunk must come here. + */ + if( is_vp8x ) { + if( !vips_webp_writer_append( &new, + writer->mem + 30, writer->size - 30 ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + } + else { + if( !vips_webp_writer_append( &new, + writer->mem + 12, writer->size - 12 ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + } + + if( vips_webp_add_chunk( &new, image, VIPS_META_EXIF_NAME, "EXIF" ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + + if( vips_webp_add_chunk( &new, image, VIPS_META_XMP_NAME, "XMP " ) ) { + vips_webp_writer_unset( &new ); + return( -1 ); + } + + vips_webp_writer_unset( writer ); + *writer = new; + + return( 0 ); +} + int vips__webp_write_file( VipsImage *in, const char *filename, int Q, gboolean lossless, VipsForeignWebpPreset preset, @@ -226,7 +477,7 @@ vips__webp_write_file( VipsImage *in, const char *filename, int alpha_q ) { WebPPicture pic; - VipsWebPMemoryWriter writer; + VipsWebPWriter writer; FILE *fp; if( !WebPPictureInit( &pic ) ) { @@ -235,32 +486,37 @@ vips__webp_write_file( VipsImage *in, const char *filename, return( -1 ); } - init_memory_writer( &writer ); + vips_webp_writer_init( &writer ); pic.writer = memory_write; pic.custom_ptr = &writer; if( write_webp( &pic, in, Q, lossless, preset, smart_subsample, near_lossless, alpha_q ) ) { WebPPictureFree( &pic ); - g_free( writer.mem ); - return -1; + vips_webp_writer_unset( &writer ); + return( -1 ); } WebPPictureFree( &pic ); + if( vips_webp_add_metadata( &writer, in ) ) { + vips_webp_writer_unset( &writer ); + return( -1 ); + } + if( !(fp = vips__file_open_write( filename, FALSE )) ) { - g_free( writer.mem ); + vips_webp_writer_unset( &writer ); return( -1 ); } if( vips__file_write( writer.mem, writer.size, 1, fp ) ) { fclose( fp ); - g_free( writer.mem ); + vips_webp_writer_unset( &writer ); return( -1 ); } fclose( fp ); - g_free( writer.mem ); + vips_webp_writer_unset( &writer ); return( 0 ); } @@ -272,7 +528,7 @@ vips__webp_write_buffer( VipsImage *in, void **obuf, size_t *olen, int alpha_q ) { WebPPicture pic; - VipsWebPMemoryWriter writer; + VipsWebPWriter writer; if( !WebPPictureInit( &pic ) ) { vips_error( "vips2webp", @@ -280,19 +536,24 @@ vips__webp_write_buffer( VipsImage *in, void **obuf, size_t *olen, return( -1 ); } - init_memory_writer( &writer ); + vips_webp_writer_init( &writer ); pic.writer = memory_write; pic.custom_ptr = &writer; if( write_webp( &pic, in, Q, lossless, preset, smart_subsample, near_lossless, alpha_q ) ) { WebPPictureFree( &pic ); - g_free( writer.mem ); - return -1; + vips_webp_writer_unset( &writer ); + return( -1 ); } WebPPictureFree( &pic ); + if( vips_webp_add_metadata( &writer, in ) ) { + vips_webp_writer_unset( &writer ); + return( -1 ); + } + *obuf = writer.mem; *olen = writer.size; diff --git a/libvips/foreign/webp2vips.c b/libvips/foreign/webp2vips.c index 13493261..1d061a7a 100644 --- a/libvips/foreign/webp2vips.c +++ b/libvips/foreign/webp2vips.c @@ -212,10 +212,11 @@ read_new( const char *filename, const void *data, size_t length, int shrink ) /* Map vips metadata names to webp names. */ const VipsWebPNames vips__webp_names[] = { - { VIPS_META_ICC_NAME, "ICCP" }, - { VIPS_META_XMP_NAME, "XMP " }, - { VIPS_META_EXIF_NAME, "EXIF" } + { VIPS_META_ICC_NAME, "ICCP", 0x20 }, + { VIPS_META_XMP_NAME, "XMP ", 0x04 }, + { VIPS_META_EXIF_NAME, "EXIF", 0x08 } }; +const int vips__n_webp_names = VIPS_NUMBER( vips__webp_names ); static int read_header( Read *read, VipsImage *out ) @@ -245,10 +246,12 @@ read_header( Read *read, VipsImage *out ) } for( i = 0; i < VIPS_NUMBER( vips__webp_names ); i++ ) { + const char *vips = vips__webp_names[i].vips; + const char *webp = vips__webp_names[i].webp; + WebPData data; - if( WebPMuxGetChunk( mux, vips__webp_names[i].webp, &data ) == - WEBP_MUX_OK ) { + if( WebPMuxGetChunk( mux, webp, &data ) == WEBP_MUX_OK ) { void *blob; if( !(blob = vips_malloc( NULL, data.size )) ) { @@ -257,7 +260,7 @@ read_header( Read *read, VipsImage *out ) } memcpy( blob, data.bytes, data.size ); - vips_image_set_blob( out, vips__webp_names[i].vips, + vips_image_set_blob( out, vips, (VipsCallbackFn) vips_free, blob, data.size ); } }