diff --git a/ChangeLog b/ChangeLog index 560fe5f0..dff7e9a2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,7 @@ 21/11/21 started 8.13 - configure fails for requested but unmet dependencies [remicollet] - add support for another quantiser [DarthSim] +- add support for HDR HEIC and AVIF images - add spngsave - jpeg2000 load left-justifies bitdepth - add "password" option to pdfload diff --git a/libvips/foreign/foreign.c b/libvips/foreign/foreign.c index 3c56f869..43ee72a0 100644 --- a/libvips/foreign/foreign.c +++ b/libvips/foreign/foreign.c @@ -2135,6 +2135,9 @@ vips_foreign_find_save_buffer( const char *name ) * If @thumbnail is %TRUE, then fetch a stored thumbnail rather than the * image. * + * The bitdepth of the heic image is recorded in the metadata item + * `heif-bitdepth`. + * * See also: vips_image_new_from_file(). * * Returns: 0 on success, -1 on error. @@ -2235,6 +2238,7 @@ vips_heifload_source( VipsSource *source, VipsImage **out, ... ) * Optional arguments: * * * @Q: %gint, quality factor + * * @bitdepth: %gint, set write bit depth to 8, 10, or 12 bits * * @lossless: %gboolean, enable lossless encoding * * @compression: #VipsForeignHeifCompression, write with this compression * * @effort: %gint, encoding effort @@ -2257,6 +2261,9 @@ vips_heifload_source( VipsSource *source, VipsImage **out, ... ) * Chroma subsampling is normally automatically disabled for Q >= 90. You can * force the subsampling mode with @subsample_mode. * + * Use @bitdepth to set the bitdepth of the output file. HEIC supports at + * least 8, 10 and 12 bits; other codecs may support more or fewer options. + * * See also: vips_image_write_to_file(), vips_heifload(). * * Returns: 0 on success, -1 on error. @@ -2284,6 +2291,7 @@ vips_heifsave( VipsImage *in, const char *filename, ... ) * Optional arguments: * * * @Q: %gint, quality factor + * * @bitdepth: %gint, set write bit depth to 8, 10, or 12 bits * * @lossless: %gboolean, enable lossless encoding * * @compression: #VipsForeignHeifCompression, write with this compression * * @effort: %gint, encoding effort @@ -2336,6 +2344,7 @@ vips_heifsave_buffer( VipsImage *in, void **buf, size_t *len, ... ) * Optional arguments: * * * @Q: %gint, quality factor + * * @bitdepth: %gint, set write bit depth to 8, 10, or 12 bits * * @lossless: %gboolean, enable lossless encoding * * @compression: #VipsForeignHeifCompression, write with this compression * * @effort: %gint, encoding effort diff --git a/libvips/foreign/heifload.c b/libvips/foreign/heifload.c index f36fab7e..9ce18e13 100644 --- a/libvips/foreign/heifload.c +++ b/libvips/foreign/heifload.c @@ -22,6 +22,8 @@ * - block broken thumbnails, if we can * 14/2/21 kleisauke * - move GObject part to heif2vips.c + * 22/12/21 + * - add >8 bit support */ /* @@ -159,6 +161,10 @@ typedef struct _VipsForeignLoadHeif { int page_width; int page_height; + /* Eg. 8 or 12, typically. + */ + int bits_per_pixel; + /* The page number currently in @handle. */ int page_no; @@ -453,6 +459,8 @@ vips_foreign_load_heif_set_header( VipsForeignLoadHeif *heif, VipsImage *out ) int n_metadata; struct heif_error error; VipsForeignHeifCompression compression; + VipsInterpretation interpretation; + VipsBandFormat format; /* We take the metadata from the non-thumbnail first page. HEIC * thumbnails don't have metadata. @@ -462,7 +470,8 @@ vips_foreign_load_heif_set_header( VipsForeignLoadHeif *heif, VipsImage *out ) /* Verify dimensions */ - if ( heif->page_width < 1 || heif->page_height < 1 ) { + if( heif->page_width < 1 || + heif->page_height < 1 ) { vips_error( "heifload", "%s", _( "bad dimensions" ) ); return( -1 ); } @@ -474,6 +483,11 @@ vips_foreign_load_heif_set_header( VipsForeignLoadHeif *heif, VipsImage *out ) #endif /*DEBUG*/ bands = heif->has_alpha ? 4 : 3; +#ifdef DEBUG + printf( "heif_image_handle_get_luma_bits_per_pixel() = %d\n", + heif_image_handle_get_luma_bits_per_pixel( heif->handle ) ); +#endif /*DEBUG*/ + /* FIXME .. IPTC as well? */ n_metadata = heif_image_handle_get_list_of_metadata_block_IDs( @@ -637,6 +651,17 @@ vips_foreign_load_heif_set_header( VipsForeignLoadHeif *heif, VipsImage *out ) vips_enum_nick( VIPS_TYPE_FOREIGN_HEIF_COMPRESSION, compression ) ); + vips_image_set_int( out, "heif-bitdepth", heif->bits_per_pixel ); + + if( heif->bits_per_pixel > 8 ) { + interpretation = VIPS_INTERPRETATION_RGB16; + format = VIPS_FORMAT_USHORT; + } + else { + interpretation = VIPS_INTERPRETATION_sRGB; + format = VIPS_FORMAT_UCHAR; + } + /* FIXME .. we always decode to RGB in generate. We should check for * all grey images, perhaps. */ @@ -644,7 +669,7 @@ vips_foreign_load_heif_set_header( VipsForeignLoadHeif *heif, VipsImage *out ) return( -1 ); vips_image_init_fields( out, heif->page_width, heif->page_height * heif->n, bands, - VIPS_FORMAT_UCHAR, VIPS_CODING_NONE, VIPS_INTERPRETATION_sRGB, + format, VIPS_CODING_NONE, interpretation, 1.0, 1.0 ); VIPS_SETSTR( load->out->filename, @@ -733,6 +758,10 @@ vips_foreign_load_heif_header( VipsForeignLoad *load ) heif_image_handle_get_width( thumb_handle ) ); printf( " height = %d\n", heif_image_handle_get_height( thumb_handle ) ); + printf( " bits_per_pixel = %d\n", + heif_image_handle_get_luma_bits_per_pixel( + thumb_handle ) ); + } } #endif /*DEBUG*/ @@ -744,6 +773,14 @@ vips_foreign_load_heif_header( VipsForeignLoad *load ) return( -1 ); heif->page_width = heif_image_handle_get_width( heif->handle ); heif->page_height = heif_image_handle_get_height( heif->handle ); + heif->bits_per_pixel = + heif_image_handle_get_luma_bits_per_pixel( heif->handle ); + if( heif->bits_per_pixel < 0 ) { + vips_error( class->nickname, + "%s", _( "undefined bits per pixel" ) ); + return( -1 ); + } + for( i = heif->page + 1; i < heif->page + heif->n; i++ ) { if( vips_foreign_load_heif_set_page( heif, i, heif->thumbnail ) ) @@ -751,7 +788,10 @@ vips_foreign_load_heif_header( VipsForeignLoad *load ) if( heif_image_handle_get_width( heif->handle ) != heif->page_width || heif_image_handle_get_height( heif->handle ) - != heif->page_height ) { + != heif->page_height || + heif_image_handle_get_luma_bits_per_pixel( + heif->handle ) + != heif->bits_per_pixel ) { vips_error( class->nickname, "%s", _( "not all pages are the same size" ) ); return( -1 ); @@ -761,6 +801,7 @@ vips_foreign_load_heif_header( VipsForeignLoad *load ) #ifdef DEBUG printf( "page_width = %d\n", heif->page_width ); printf( "page_height = %d\n", heif->page_height ); + printf( "bits_per_pixel = %d\n", heif->bits_per_pixel ); printf( "n_top = %d\n", heif->n_top ); for( i = 0; i < heif->n_top; i++ ) { @@ -771,6 +812,8 @@ vips_foreign_load_heif_header( VipsForeignLoad *load ) heif_image_handle_get_width( heif->handle ) ); printf( " height = %d\n", heif_image_handle_get_height( heif->handle ) ); + printf( " bits_per_pixel = %d\n", + heif_image_handle_get_luma_bits_per_pixel( heif->handle ) ); printf( " has_depth = %d\n", heif_image_handle_has_depth_image( heif->handle ) ); printf( " has_alpha = %d\n", @@ -838,6 +881,25 @@ vips__heif_image_print( struct heif_image *img ) } #endif /*DEBUG*/ +/* Pick a chroma format. Shared with heifsave. + */ +int +vips__heif_chroma( int bits_per_pixel, gboolean has_alpha ) +{ + if( bits_per_pixel == 8 ) { + if( has_alpha ) + return( heif_chroma_interleaved_RGBA ); + else + return( heif_chroma_interleaved_RGB ); + } + else { + if( has_alpha ) + return( heif_chroma_interleaved_RRGGBBAA_BE ); + else + return( heif_chroma_interleaved_RRGGBB_BE ); + } +} + static int vips_foreign_load_heif_generate( VipsRegion *or, void *seq, void *a, void *b, gboolean *stop ) @@ -861,18 +923,14 @@ vips_foreign_load_heif_generate( VipsRegion *or, if( !heif->img ) { struct heif_error error; struct heif_decoding_options *options; - enum heif_chroma chroma = heif->has_alpha ? - heif_chroma_interleaved_RGBA : - heif_chroma_interleaved_RGB; + enum heif_chroma chroma = + vips__heif_chroma( heif->bits_per_pixel, + heif->has_alpha ); options = heif_decoding_options_alloc(); -#ifdef HAVE_HEIF_DECODING_OPTIONS_CONVERT_HDR_TO_8BIT - /* VIPS_FORMAT_UCHAR is assumed so downsample HDR to 8bpc - */ - options->convert_hdr_to_8bit = TRUE; -#endif /*HAVE_HEIF_DECODING_OPTIONS_CONVERT_HDR_TO_8BIT*/ error = heif_decode_image( heif->handle, &heif->img, - heif_colorspace_RGB, chroma, + heif_colorspace_RGB, + chroma, options ); heif_decoding_options_free( options ); if( error.code ) { @@ -914,6 +972,26 @@ vips_foreign_load_heif_generate( VipsRegion *or, heif->data + heif->stride * line, VIPS_IMAGE_SIZEOF_LINE( or->im ) ); + /* We may need to swap bytes and shift to fill 16 bits. + */ + if( heif->bits_per_pixel > 8 ) { + int shift = 16 - heif->bits_per_pixel; + int ne = VIPS_REGION_N_ELEMENTS( or ); + + int i; + VipsPel *p; + + p = VIPS_REGION_ADDR( or, 0, r->top ); + for( i = 0; i < ne; i++ ) { + /* We've asked for big endian, we must write native. + */ + guint16 v = ((p[0] << 8) | p[1]) << shift; + + *((guint16 *) p) = v; + p += 2; + } + } + return( 0 ); } diff --git a/libvips/foreign/heifsave.c b/libvips/foreign/heifsave.c index cf9af771..f18ff78c 100644 --- a/libvips/foreign/heifsave.c +++ b/libvips/foreign/heifsave.c @@ -12,6 +12,8 @@ * - move GObject part to vips2heif.c * 30/7/21 * - rename "speed" as "effort" for consistency with other savers + * 22/12/21 + * - add >8 bit support */ /* @@ -46,6 +48,21 @@ #define DEBUG */ +/* + * + +TODO: + + what about a 16-bit PNG saved with bitdepth=8? does this work? + + no! + + what about a 8-bit PNG saved with bitdepth=12? does this work? + + * + */ + + #ifdef HAVE_CONFIG_H #include #endif /*HAVE_CONFIG_H*/ @@ -75,6 +92,10 @@ typedef struct _VipsForeignSaveHeif { */ int Q; + /* bitdepth to save at for >8 bit images. + */ + int bitdepth; + /* Lossless compression. */ gboolean lossless; @@ -277,6 +298,73 @@ vips_foreign_save_heif_write_page( VipsForeignSaveHeif *heif, int page ) return( 0 ); } +static int +vips_foreign_save_heif_pack( VipsForeignSaveHeif *heif, + VipsPel *q, VipsPel *p, int ne ) +{ + int i; + + if( heif->image->BandFmt == VIPS_FORMAT_UCHAR && + heif->bitdepth == 8 ) + /* Most common cases -- 8 bit to 8 bit. + */ + memcpy( q, p, ne ); + else if( heif->image->BandFmt == VIPS_FORMAT_UCHAR && + heif->bitdepth > 8 ) { + /* 8-bit source, write a bigendian short, shifted up. + */ + int shift = heif->bitdepth - 8; + + for( i = 0; i < ne; i++ ) { + guint16 v = p[i] << shift; + + q[0] = v >> 8; + q[1] = v; + + q += 2; + } + } + else if( heif->image->BandFmt == VIPS_FORMAT_USHORT && + heif->bitdepth <= 8 ) { + /* 16-bit native byte order source, 8 bit write. + */ + int shift = 16 - heif->bitdepth; + + for( i = 0; i < ne; i++ ) { + guint16 v = *((gushort *) p) >> shift; + + q[i] = v; + + p += 2; + } + } + else if( heif->image->BandFmt == VIPS_FORMAT_USHORT && + heif->bitdepth > 8 ) { + /* 16-bit native byte order source, 16 bit bigendian write. + */ + int shift = 16 - heif->bitdepth; + + for( i = 0; i < ne; i++ ) { + guint16 v = *((gushort *) p) >> shift; + + q[0] = v >> 8; + q[1] = v; + + p += 2; + q += 2; + } + } + else { + VipsObjectClass *class = VIPS_OBJECT_CLASS( heif ); + + vips_error( class->nickname, + "%s", _( "unimplemeted format conversion" ) ); + return( -1 ); + } + + return( 0 ); +} + static int vips_foreign_save_heif_write_block( VipsRegion *region, VipsRect *area, void *a ) @@ -297,11 +385,12 @@ vips_foreign_save_heif_write_block( VipsRegion *region, VipsRect *area, */ int page = (area->top + y) / heif->page_height; int line = (area->top + y) % heif->page_height; - VipsPel *p = VIPS_REGION_ADDR( region, 0, area->top + y ); VipsPel *q = heif->data + line * heif->stride; - memcpy( q, p, VIPS_IMAGE_SIZEOF_LINE( region->im ) ); + if( vips_foreign_save_heif_pack( heif, + q, p, VIPS_REGION_N_ELEMENTS( region ) ) ) + return( -1 ); /* Did we just write the final line? Write as a new page * into the output. @@ -350,6 +439,13 @@ vips_foreign_save_heif_build( VipsObject *object ) !vips_object_argument_isset( object, "effort" ) ) heif->effort = 9 - heif->speed; + /* Default 12 bit save for ushort. HEIC (for example) implements + * 8 / 10 / 12. + */ + if( !vips_object_argument_isset( object, "bitdepth" ) ) + heif->bitdepth = save->ready->BandFmt == VIPS_FORMAT_UCHAR ? + 8 : 12; + /* Make a copy of the image in case we modify the metadata eg. for * exif_update. */ @@ -419,9 +515,8 @@ vips_foreign_save_heif_build( VipsObject *object ) #endif /*DEBUG*/ error = heif_image_create( heif->page_width, heif->page_height, heif_colorspace_RGB, - vips_image_hasalpha( heif->image ) ? - heif_chroma_interleaved_RGBA : - heif_chroma_interleaved_RGB, + vips__heif_chroma( heif->bitdepth, + vips_image_hasalpha( heif->image ) ), &heif->img ); if( error.code ) { vips__heif_error( &error ); @@ -430,7 +525,7 @@ vips_foreign_save_heif_build( VipsObject *object ) error = heif_image_add_plane( heif->img, heif_channel_interleaved, heif->page_width, heif->page_height, - vips_image_hasalpha( heif->image ) ? 32 : 24 ); + heif->bitdepth ); if( error.code ) { vips__heif_error( &error ); return( -1 ); @@ -465,13 +560,12 @@ vips_foreign_save_heif_build( VipsObject *object ) return( 0 ); } -/* Save a bit of typing. - */ #define UC VIPS_FORMAT_UCHAR +#define US VIPS_FORMAT_USHORT static int vips_heif_bandfmt[10] = { /* UC C US S UI I F X D DX */ - UC, UC, UC, UC, UC, UC, UC, UC, UC, UC + UC, UC, US, US, US, US, US, US, US, US }; static void @@ -499,6 +593,13 @@ vips_foreign_save_heif_class_init( VipsForeignSaveHeifClass *class ) G_STRUCT_OFFSET( VipsForeignSaveHeif, Q ), 1, 100, 50 ); + VIPS_ARG_INT( class, "bitdepth", 11, + _( "Bit depth" ), + _( "Number of bits per pixel" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveHeif, bitdepth ), + 1, 16, 12 ); + VIPS_ARG_BOOL( class, "lossless", 13, _( "Lossless" ), _( "Enable lossless compression" ), @@ -543,6 +644,7 @@ vips_foreign_save_heif_init( VipsForeignSaveHeif *heif ) { heif->ctx = heif_context_alloc(); heif->Q = 50; + heif->bitdepth = 12; heif->compression = VIPS_FOREIGN_HEIF_COMPRESSION_HEVC; heif->effort = 4; heif->subsample_mode = VIPS_FOREIGN_SUBSAMPLE_AUTO; diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index d290f9e4..38d6314d 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -229,6 +229,7 @@ void *vips__foreign_nifti_map( VipsNiftiMapFn fn, void *a, void *b ); extern const char *vips__heic_suffs[]; extern const char *vips__avif_suffs[]; extern const char *vips__heif_suffs[]; +int vips__heif_chroma( int bits_per_pixel, gboolean has_alpha ); extern const char *vips__jp2k_suffs[]; int vips__foreign_load_jp2k_decompress( VipsImage *out, diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py index 68176a04..31548032 100644 --- a/test/test-suite/test_foreign.py +++ b/test/test-suite/test_foreign.py @@ -1238,6 +1238,47 @@ class TestForeign: y = pyvips.Image.new_from_buffer(buf, "") assert y.get("exif-ifd0-XPComment").startswith("banana") + @skip_if_no("heifsave") + @pytest.mark.skipif(sys.platform == "darwin", reason="fails with latest libheif/aom from Homebrew") + def test_heicsave_16_to_12(self): + rgb16 = self.colour.colourspace("rgb16") + data = rgb16.heifsave_buffer(lossless=True) + im = pyvips.Image.heifload_buffer(data) + + assert(im.width == rgb16.width) + assert(im.format == rgb16.format) + assert(im.interpretation == rgb16.interpretation) + assert(im.get("heif-bitdepth") == 12) + # good grief, some kind of lossless + assert((im - rgb16).abs().max() < 3000) + + @skip_if_no("heifsave") + @pytest.mark.skipif(sys.platform == "darwin", reason="fails with latest libheif/aom from Homebrew") + def test_heicsave_16_to_8(self): + rgb16 = self.colour.colourspace("rgb16") + data = rgb16.heifsave_buffer(lossless=True, bitdepth=8) + im = pyvips.Image.heifload_buffer(data) + + assert(im.width == rgb16.width) + assert(im.format == "uchar") + assert(im.interpretation == "srgb") + assert(im.get("heif-bitdepth") == 8) + # good grief, some kind of lossless + assert((im - rgb16 / 256).abs().max() < 80) + + @skip_if_no("heifsave") + @pytest.mark.skipif(sys.platform == "darwin", reason="fails with latest libheif/aom from Homebrew") + def test_heicsave_8_to_16(self): + data = self.colour.heifsave_buffer(lossless=True, bitdepth=12) + im = pyvips.Image.heifload_buffer(data) + + assert(im.width == self.colour.width) + assert(im.format == "ushort") + assert(im.interpretation == "rgb16") + assert(im.get("heif-bitdepth") == 12) + # good grief, some kind of lossless + assert((im - self.colour * 256).abs().max() < 3000) + @skip_if_no("jp2kload") def test_jp2kload(self): def jp2k_valid(im):