add HDR support to heif load/save (#2596)

* heifload done, doing save

* finish save, add tests, docs
This commit is contained in:
John Cupitt 2022-02-18 11:16:15 +00:00 committed by GitHub
parent 0388e54bd2
commit e985e23c09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 253 additions and 21 deletions

View File

@ -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

View File

@ -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

View File

@ -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 );
}

View File

@ -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 <config.h>
#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;

View File

@ -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,

View File

@ -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):