diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e614077..32d8c34a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: liblcms2-dev libpoppler-glib-dev librsvg2-dev libgif-dev libopenexr-dev libpango1.0-dev libgsf-1-dev libopenslide-dev libffi-dev + libopenjp2-7-dev - name: Install macOS dependencies if: contains(matrix.os, 'macos') @@ -69,6 +70,7 @@ jobs: librsvg libspng libtiff little-cms2 openexr openslide orc pango poppler webp + openjpeg - name: Install Clang 10 env: diff --git a/ChangeLog b/ChangeLog index b977a9e7..67264ff7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,8 @@ - add vips_fitsload_source(), vips_niftiload_source() - png and gif load note background colour as metadata [781545872] - add vips_image_[set|get]_array_double() +- add GIF load with libnsgif +- add JPEG2000 load and save 22/12/20 start 8.10.6 - don't seek on bad file descriptors [kleisauke] diff --git a/configure.ac b/configure.ac index 3df902da..9f906b8c 100644 --- a/configure.ac +++ b/configure.ac @@ -770,6 +770,36 @@ VIPS_CFLAGS="$VIPS_CFLAGS $NIFTI_CFLAGS" VIPS_INCLUDES="$VIPS_INCLUDES $NIFTI_INCLUDES" VIPS_LIBS="$VIPS_LIBS $NIFTI_LIBS" +# openjpeg +AC_ARG_WITH([libopenjp2], + AS_HELP_STRING([--without-libopenjp2], + [build without libopenjp2 (default: test)])) + +if test x"$with_libopenjp2" != x"no"; then + PKG_CHECK_MODULES(LIBOPENJP2, libopenjp2 >= 2.3, + [AC_DEFINE(HAVE_LIBOPENJP2,1, + [define if you have libopenjp2 >=2.2 installed.]) + with_libopenjp2=yes + PACKAGES_USED="$PACKAGES_USED libopenjp2" + ], + [AC_MSG_WARN([libopenjp2 not found; disabling libopenjp2 support]) + with_libopenjp2=no + ] + ) + + # 2.3 and earlier have threading problems + PKG_CHECK_MODULES(LIBOPENJP2_THREADING, libopenjp2 >= 2.4, + [AC_DEFINE(HAVE_LIBOPENJP2_THREADING,1,[define if your libopenjp2 threading works.]) + ], + [: + ] + ) +fi + +VIPS_CFLAGS="$VIPS_CFLAGS $LIBOPENJP2_CFLAGS" +VIPS_INCLUDES="$VIPS_INCLUDES $LIBOPENJP2_INCLUDES" +VIPS_LIBS="$VIPS_LIBS $LIBOPENJP2_LIBS" + # libheif AC_ARG_WITH([heif], AS_HELP_STRING([--without-heif], [build without libheif (default: test)])) @@ -1420,6 +1450,8 @@ PDF load with poppler-glib: $with_poppler SVG load with librsvg-2.0: $with_rsvg (requires librsvg-2.0 2.34.0 or later) EXR load with OpenEXR: $with_OpenEXR +JPEG2000 load/save with libopenjp2: $with_libopenjp2 + (requires libopenjp2 2.2 or later) slide load with OpenSlide: $with_openslide (requires openslide-3.3.0 or later) Matlab load with matio: $with_matio diff --git a/cplusplus/Doxyfile.in b/cplusplus/Doxyfile.in index 1019ff5a..db7c810a 100644 --- a/cplusplus/Doxyfile.in +++ b/cplusplus/Doxyfile.in @@ -263,12 +263,6 @@ TAB_SIZE = 8 ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all diff --git a/libvips/foreign/Makefile.am b/libvips/foreign/Makefile.am index 3af243b4..afe30463 100644 --- a/libvips/foreign/Makefile.am +++ b/libvips/foreign/Makefile.am @@ -5,70 +5,72 @@ endif noinst_LTLIBRARIES = libforeign.la libforeign_la_SOURCES = \ - pforeign.h \ - heifload.c \ - heifsave.c \ - niftiload.c \ - niftisave.c \ - quantise.c \ - exif.c \ - nsgifload.c \ + analyze2vips.c \ + analyzeload.c \ cairo.c \ - pdfload.c \ - pdfiumload.c \ - svgload.c \ - radiance.c \ - radload.c \ - radsave.c \ - ppmload.c \ - ppmsave.c \ csvload.c \ csvsave.c \ - matrixload.c \ - matrixsave.c \ - dzsave.c \ - rawload.c \ - rawsave.c \ - vipsload.c \ - vipssave.c \ dbh.h \ - analyzeload.c \ - analyze2vips.c \ - foreign.c \ - matlab.c \ - matload.c \ - magick.h \ - magick.c \ - magick2vips.c \ - magickload.c \ - magick7load.c \ - magicksave.c \ - spngload.c \ - pngload.c \ - pngsave.c \ - vipspng.c \ - openexr2vips.c \ - openexrload.c \ + dzsave.c \ + exif.c \ fits.c \ fitsload.c \ fitssave.c \ - tiff.h \ - tiff.c \ - vips2tiff.c \ - tiff2vips.c \ - tiffload.c \ - tiffsave.c \ - openslide2vips.c \ - openslideload.c \ - webpload.c \ - webpsave.c \ - webp2vips.c \ - vips2webp.c \ - vips2jpeg.c \ + foreign.c \ + heifload.c \ + heifsave.c \ + jp2kload.c \ + jp2ksave.c \ jpeg2vips.c \ jpeg.h \ jpegload.c \ - jpegsave.c + jpegsave.c \ + magick2vips.c \ + magick7load.c \ + magick.c \ + magick.h \ + magickload.c \ + magicksave.c \ + matlab.c \ + matload.c \ + matrixload.c \ + matrixsave.c \ + niftiload.c \ + niftisave.c \ + nsgifload.c \ + openexr2vips.c \ + openexrload.c \ + openslide2vips.c \ + openslideload.c \ + pdfiumload.c \ + pdfload.c \ + pforeign.h \ + pngload.c \ + pngsave.c \ + ppmload.c \ + ppmsave.c \ + quantise.c \ + radiance.c \ + radload.c \ + radsave.c \ + rawload.c \ + rawsave.c \ + spngload.c \ + svgload.c \ + tiff2vips.c \ + tiff.c \ + tiff.h \ + tiffload.c \ + tiffsave.c \ + vips2jpeg.c \ + vips2tiff.c \ + vips2webp.c \ + vipsload.c \ + vipspng.c \ + vipssave.c \ + webp2vips.c \ + webpload.c \ + webpsave.c EXTRA_DIST = libnsgif diff --git a/libvips/foreign/foreign.c b/libvips/foreign/foreign.c index 4ee11c28..9cb0e876 100644 --- a/libvips/foreign/foreign.c +++ b/libvips/foreign/foreign.c @@ -2176,6 +2176,13 @@ vips_foreign_operation_init( void ) extern GType vips_foreign_load_svg_buffer_get_type( void ); extern GType vips_foreign_load_svg_source_get_type( void ); + extern GType vips_foreign_load_jp2k_file_get_type( void ); + extern GType vips_foreign_load_jp2k_buffer_get_type( void ); + extern GType vips_foreign_load_jp2k_source_get_type( void ); + extern GType vips_foreign_save_jp2k_file_get_type( void ); + extern GType vips_foreign_save_jp2k_buffer_get_type( void ); + extern GType vips_foreign_save_jp2k_target_get_type( void ); + extern GType vips_foreign_load_heif_file_get_type( void ); extern GType vips_foreign_load_heif_buffer_get_type( void ); extern GType vips_foreign_load_heif_source_get_type( void ); @@ -2249,8 +2256,17 @@ vips_foreign_operation_init( void ) vips_foreign_load_svg_source_get_type(); #endif /*HAVE_RSVG*/ +#ifdef HAVE_LIBOPENJP2 + vips_foreign_load_jp2k_file_get_type(); + vips_foreign_load_jp2k_buffer_get_type(); + vips_foreign_load_jp2k_source_get_type(); + vips_foreign_save_jp2k_file_get_type(); + vips_foreign_save_jp2k_buffer_get_type(); + vips_foreign_save_jp2k_target_get_type(); +#endif /*HAVE_LIBOPENJP2*/ + #ifdef HAVE_NSGIF - vips_foreign_load_nsgif_file_get_type(); + vips_foreign_load_nsgif_file_get_type(); vips_foreign_load_nsgif_buffer_get_type(); vips_foreign_load_nsgif_source_get_type(); #endif /*HAVE_NSGIF*/ diff --git a/libvips/foreign/heifload.c b/libvips/foreign/heifload.c index 5c60a9f7..99a44ae6 100644 --- a/libvips/foreign/heifload.c +++ b/libvips/foreign/heifload.c @@ -230,7 +230,8 @@ vips_foreign_load_heif_build( VipsObject *object ) printf( "vips_foreign_load_heif_build:\n" ); #endif /*DEBUG*/ - if( vips_source_rewind( heif->source ) ) + if( heif->source && + vips_source_rewind( heif->source ) ) return( -1 ); if( !heif->ctx ) { @@ -245,7 +246,6 @@ vips_foreign_load_heif_build( VipsObject *object ) } } - if( VIPS_OBJECT_CLASS( vips_foreign_load_heif_parent_class )-> build( object ) ) return( -1 ); @@ -253,7 +253,6 @@ vips_foreign_load_heif_build( VipsObject *object ) return( 0 ); } - static const char *heif_magic[] = { "ftypheic", /* A regular heif image */ "ftypheix", /* Extended range (>8 bit) image */ diff --git a/libvips/foreign/heifsave.c b/libvips/foreign/heifsave.c index 67ab03bd..20ad3237 100644 --- a/libvips/foreign/heifsave.c +++ b/libvips/foreign/heifsave.c @@ -380,7 +380,7 @@ vips_foreign_save_heif_build( VipsObject *object ) chroma = heif->subsample_mode == VIPS_FOREIGN_SUBSAMPLE_OFF || ( heif->subsample_mode == VIPS_FOREIGN_SUBSAMPLE_AUTO && - heif->Q > 90 ) ? "444" : "420"; + heif->Q >= 90 ) ? "444" : "420"; error = heif_encoder_set_parameter_string( heif->encoder, "chroma", chroma ); if( error.code && @@ -738,7 +738,7 @@ vips_foreign_save_heif_target_init( VipsForeignSaveHeifTarget *target ) * Use @speed to control the CPU effort spent improving compression. * This is currently only applicable to AV1 encoders, defaults to 5. * - * Chroma subsampling is normally automatically disabled for Q > 90. You can + * Chroma subsampling is normally automatically disabled for Q >= 90. You can * force the subsampling mode with @subsample_mode. * * See also: vips_image_write_to_file(), vips_heifload(). diff --git a/libvips/foreign/jp2kload.c b/libvips/foreign/jp2kload.c new file mode 100644 index 00000000..842574c3 --- /dev/null +++ b/libvips/foreign/jp2kload.c @@ -0,0 +1,1233 @@ +/* load jpeg2000 + * + * 18/3/20 + * - from heifload.c + */ + +/* + + This file is part of VIPS. + + VIPS is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +/* +#define DEBUG_VERBOSE +#define DEBUG + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ +#include + +#include +#include +#include + +#include +#include +#include + +#ifdef HAVE_LIBOPENJP2 + +#include + +#include "pforeign.h" + +/* Surely enough ... does anyone do multispectral imaging with jp2k? + */ +#define MAX_BANDS (100) + +typedef struct _VipsForeignLoadJp2k { + VipsForeignLoad parent_object; + + /* Source to load from (set by subclasses). + */ + VipsSource *source; + + /* Page set by user, then we translate that into shrink factor. + */ + int page; + int shrink; + + /* Decompress state. + */ + opj_stream_t *stream; /* Source as an opj stream */ + OPJ_CODEC_FORMAT format; /* libopenjp2 format */ + opj_codec_t *codec; /* Decompress codec */ + opj_dparameters_t parameters; /* Core decompress params */ + opj_image_t *image; /* Read image to here */ + opj_codestream_info_v2_t *info; /* Tile geometry */ + + /* Number of errors reported during load -- use this to block load of + * corrupted images. + */ + int n_errors; + + /* If we need to upsample tiles read from opj. + */ + gboolean upsample; + + /* If we need to do ycc->rgb conversion on load. + */ + gboolean ycc_to_rgb; +} VipsForeignLoadJp2k; + +typedef VipsForeignLoadClass VipsForeignLoadJp2kClass; + +G_DEFINE_ABSTRACT_TYPE( VipsForeignLoadJp2k, vips_foreign_load_jp2k, + VIPS_TYPE_FOREIGN_LOAD ); + +static void +vips_foreign_load_jp2k_dispose( GObject *gobject ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) gobject; + +#ifdef DEBUG + printf( "vips_foreign_load_jp2k_dispose:\n" ); +#endif /*DEBUG*/ + + /* + * FIXME ... do we need this? seems to just cause warnings + * + if( jp2k->codec && + jp2k->stream ) + opj_end_decompress( jp2k->codec, jp2k->stream ); + * + */ + + if( jp2k->info ) + opj_destroy_cstr_info( &jp2k->info ); + VIPS_FREEF( opj_destroy_codec, jp2k->codec ); + VIPS_FREEF( opj_stream_destroy, jp2k->stream ); + VIPS_FREEF( opj_image_destroy, jp2k->image ); + VIPS_UNREF( jp2k->source ); + + G_OBJECT_CLASS( vips_foreign_load_jp2k_parent_class )-> + dispose( gobject ); +} + +static OPJ_SIZE_T +vips_foreign_load_jp2k_read_source( void *buffer, size_t length, void *client ) +{ + VipsSource *source = VIPS_SOURCE( client ); + gint64 bytes_read = vips_source_read( source, buffer, length ); + + /* openjpeg read uses -1 for both EOF and error return. + */ + return( bytes_read == 0 ? -1 : bytes_read ); +} + +static OPJ_OFF_T +vips_foreign_load_jp2k_skip_source( OPJ_OFF_T n_bytes, void *client ) +{ + VipsSource *source = VIPS_SOURCE( client ); + + if( vips_source_seek( source, n_bytes, SEEK_CUR ) == -1 ) + /* openjpeg skip uses -1 for both end of stream and error. + */ + return( -1 ); + + return( n_bytes ); +} + +static OPJ_BOOL +vips_foreign_load_jp2k_seek_source( OPJ_OFF_T position, void *client ) +{ + VipsSource *source = VIPS_SOURCE( client ); + + if( vips_source_seek( source, position, SEEK_SET ) == -1 ) + /* openjpeg seek uses FALSE for both end of stream and error. + */ + return( OPJ_FALSE ); + + return( OPJ_TRUE ); +} + +/* Make a libopenjp2 stream that wraps a VipsSource. + */ +static opj_stream_t * +vips_foreign_load_jp2k_stream( VipsSource *source ) +{ + opj_stream_t *stream; + + /* TRUE means a read stream. + */ + if( !(stream = opj_stream_create( OPJ_J2K_STREAM_CHUNK_SIZE, TRUE )) ) + return( NULL ); + + opj_stream_set_user_data( stream, source, NULL ); + /* Unfortunately, jp2k requires the length, so pipe sources will have + * to buffer in memory. + */ + opj_stream_set_user_data_length( stream, + vips_source_length( source ) ); + opj_stream_set_read_function( stream, + vips_foreign_load_jp2k_read_source ); + opj_stream_set_skip_function( stream, + vips_foreign_load_jp2k_skip_source ); + opj_stream_set_seek_function( stream, + vips_foreign_load_jp2k_seek_source ); + + return( stream ); +} + +static int +vips_foreign_load_jp2k_build( VipsObject *object ) +{ + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( object ); + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) object; + +#ifdef DEBUG + printf( "vips_foreign_load_jp2k_build:\n" ); +#endif /*DEBUG*/ + + /* Default parameters. + */ + jp2k->parameters.decod_format = -1; + jp2k->parameters.cod_format = -1; + opj_set_default_decoder_parameters( &jp2k->parameters ); + + /* Link the openjpeg stream to our VipsSource. + */ + if( jp2k->source ) { + jp2k->stream = vips_foreign_load_jp2k_stream( jp2k->source ); + if( !jp2k->stream ) { + vips_error( class->nickname, + "%s", _( "unable to create jp2k stream" ) ); + return( -1 ); + } + } + + if( VIPS_OBJECT_CLASS( vips_foreign_load_jp2k_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +#define JP2_RFC3745_MAGIC "\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a" +#define JP2_MAGIC "\x0d\x0a\x87\x0a" +/* position 45: "\xff\x52" */ +#define J2K_CODESTREAM_MAGIC "\xff\x4f\xff\x51" + +/* Return the image format. OpenJPEG supports several different image types. + */ +static OPJ_CODEC_FORMAT +vips_foreign_load_jp2k_get_format( VipsSource *source ) +{ + unsigned char *data; + + if( vips_source_sniff_at_most( source, &data, 12 ) < 12 ) + return( -1 ); + + /* There's also OPJ_CODEC_JPT for xxx.jpt files, but we don't support + * that. + */ + if( memcmp( data, JP2_RFC3745_MAGIC, 12) == 0 || + memcmp( data, JP2_MAGIC, 4 ) == 0 ) + return( OPJ_CODEC_JP2 ); + else if( memcmp( data, J2K_CODESTREAM_MAGIC, 4 ) == 0 ) + return( OPJ_CODEC_J2K ); + else + return( -1 ); +} + +static gboolean +vips_foreign_load_jp2k_is_a_source( VipsSource *source ) +{ + return( vips_foreign_load_jp2k_get_format( source ) != -1 ); +} + +static VipsForeignFlags +vips_foreign_load_jp2k_get_flags( VipsForeignLoad *load ) +{ + return( VIPS_FOREIGN_PARTIAL ); +} + +static void +vips_foreign_load_jp2k_error_callback( const char *msg, void *client ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) client; + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( jp2k ); + + vips_error( class->nickname, "%s", msg ); + jp2k->n_errors += 1; +} + +/* The openjpeg info and warning callbacks are incredibly chatty. + */ +static void +vips_foreign_load_jp2k_warning_callback( const char *msg, void *client ) +{ +#ifdef DEBUG + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( client ); + + g_warning( "%s: %s", class->nickname, msg ); +#endif /*DEBUG*/ +} + +/* The openjpeg info and warning callbacks are incredibly chatty. + */ +static void +vips_foreign_load_jp2k_info_callback( const char *msg, void *client ) +{ +#ifdef DEBUG + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( client ); + + g_info( "%s: %s", class->nickname, msg ); +#endif /*DEBUG*/ +} + +static void +vips_foreign_load_jp2k_attach_handlers( VipsForeignLoadJp2k *jp2k, + opj_codec_t *codec ) +{ + opj_set_info_handler( codec, + vips_foreign_load_jp2k_info_callback, jp2k ); + opj_set_warning_handler( codec, + vips_foreign_load_jp2k_warning_callback, jp2k ); + opj_set_error_handler( codec, + vips_foreign_load_jp2k_error_callback, jp2k ); +} + +#ifdef DEBUG +static void +vips_foreign_load_jp2k_print_image( opj_image_t *image ) +{ + printf( "image:\n" ); + printf( "x0 = %u, y0 = %u, x1 = %u, y1 = %u, numcomps = %u, " + "color_space = %u\n", + image->x0, image->y0, image->x1, image->y1, + image->numcomps, image->color_space ); + printf( "icc_profile_buf = %p, icc_profile_len = %x\n", + image->icc_profile_buf, image->icc_profile_len ); +} + +static void +vips_foreign_load_jp2k_print( VipsForeignLoadJp2k *jp2k ) +{ + int i; + + vips_foreign_load_jp2k_print_image( jp2k->image ); + + printf( "components:\n" ); + for( i = 0; i < jp2k->image->numcomps; i++ ) { + opj_image_comp_t *this = &jp2k->image->comps[i]; + + printf( "%i) dx = %u, dy = %u, w = %u, h = %u, " + "x0 = %u, y0 = %u\n", + i, this->dx, this->dy, this->w, this->h, + this->x0, this->y0 ); + printf( " prec = %x, bpp = %x, sgnd = %x, " + "resno_decoded = %u, factor = %u\n", + this->prec, this->bpp, this->sgnd, + this->resno_decoded, this->factor ); + printf( " data = %p, alpha = %u\n", + this->data, this->alpha ); + } + + printf( "info:\n" ); + printf( "tx0 = %u, ty0 = %d, tdx = %u, tdy = %u, tw = %u, th = %u\n", + jp2k->info->tx0, jp2k->info->ty0, + jp2k->info->tdx, jp2k->info->tdy, + jp2k->info->tw, jp2k->info->th ); + printf( "nbcomps = %u, tile_info = %p\n", + jp2k->info->nbcomps, jp2k->info->tile_info ); +} +#endif /*DEBUG*/ + +static int +vips_foreign_load_jp2k_set_header( VipsForeignLoadJp2k *jp2k, VipsImage *out ) +{ + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( jp2k ); + opj_image_comp_t *first = &jp2k->image->comps[0]; + + VipsBandFormat format; + VipsInterpretation interpretation; + + /* OpenJPEG only supports up to 31 bpp. Treat it as 32. + */ + if( first->prec <= 8 ) + format = first->sgnd ? VIPS_FORMAT_CHAR : VIPS_FORMAT_UCHAR; + else if( first->prec <= 16 ) + format = first->sgnd ? VIPS_FORMAT_SHORT : VIPS_FORMAT_USHORT; + else + format = first->sgnd ? VIPS_FORMAT_INT : VIPS_FORMAT_UINT; + + switch( jp2k->image->color_space ) { + case OPJ_CLRSPC_SYCC: + case OPJ_CLRSPC_EYCC: + /* Map these to RGB. + */ + interpretation = vips_format_sizeof( format ) == 1 ? + VIPS_INTERPRETATION_sRGB : + VIPS_INTERPRETATION_RGB16; + jp2k->ycc_to_rgb = TRUE; + break; + + case OPJ_CLRSPC_GRAY: + interpretation = vips_format_sizeof( format ) == 1 ? + VIPS_INTERPRETATION_B_W : + VIPS_INTERPRETATION_GREY16; + break; + + case OPJ_CLRSPC_SRGB: + interpretation = vips_format_sizeof( format ) == 1 ? + VIPS_INTERPRETATION_sRGB : + VIPS_INTERPRETATION_RGB16; + break; + + case OPJ_CLRSPC_CMYK: + interpretation = VIPS_INTERPRETATION_CMYK; + break; + + case OPJ_CLRSPC_UNSPECIFIED: + /* Try to guess something sensible. + */ + if( jp2k->image->numcomps < 3 ) + interpretation = vips_format_sizeof( format ) == 1 ? + VIPS_INTERPRETATION_B_W : + VIPS_INTERPRETATION_GREY16; + else + interpretation = vips_format_sizeof( format ) == 1 ? + VIPS_INTERPRETATION_sRGB : + VIPS_INTERPRETATION_RGB16; + + /* Unspecified with three bands and subsampling on bands 2 and + * 3 is usually YCC. + */ + if( jp2k->image->numcomps == 3 && + jp2k->image->comps[0].dx == 1 && + jp2k->image->comps[0].dy == 1 && + jp2k->image->comps[1].dx > 1 && + jp2k->image->comps[1].dy > 1 && + jp2k->image->comps[2].dx > 1 && + jp2k->image->comps[2].dy > 1) + jp2k->ycc_to_rgb = TRUE; + + break; + + default: + vips_error( class->nickname, + _( "unsupported colourspace %d" ), + jp2k->image->color_space ); + return( -1 ); + } + + /* Even though this is a tiled reader, we hint thinstrip since with + * the cache we are quite happy serving that if anything downstream + * would like it. + */ + vips_image_pipelinev( out, VIPS_DEMAND_STYLE_THINSTRIP, NULL ); + + vips_image_init_fields( out, + first->w, first->h, jp2k->image->numcomps, format, + VIPS_CODING_NONE, interpretation, 1.0, 1.0 ); + + /* openjpeg allows left and top of the coordinate grid to be + * non-zero. These are always in unshrunk coordinates. + */ + out->Xoffset = + -VIPS_ROUND_INT( (double) jp2k->image->x0 / jp2k->shrink ); + out->Yoffset = + -VIPS_ROUND_INT( (double) jp2k->image->y0 / jp2k->shrink ); + + if( jp2k->image->icc_profile_buf && + jp2k->image->icc_profile_len > 0 ) + vips_image_set_blob_copy( out, VIPS_META_ICC_NAME, + jp2k->image->icc_profile_buf, + jp2k->image->icc_profile_len ); + + /* Map number of layers in image to pages. + */ + if( jp2k->info && + jp2k->info->m_default_tile_info.tccp_info ) + vips_image_set_int( out, VIPS_META_N_PAGES, + jp2k->info->m_default_tile_info.tccp_info-> + numresolutions ); + + return( 0 ); +} + +static int +vips_foreign_load_jp2k_header( VipsForeignLoad *load ) +{ + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( load ); + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) load; + + opj_image_comp_t *first; + int i; + +#ifdef DEBUG + printf( "vips_foreign_load_jp2k_header:\n" ); +#endif /*DEBUG*/ + + jp2k->format = vips_foreign_load_jp2k_get_format( jp2k->source ); + vips_source_rewind( jp2k->source ); + if( !(jp2k->codec = opj_create_decompress( jp2k->format )) ) + return( -1 ); + + vips_foreign_load_jp2k_attach_handlers( jp2k, jp2k->codec ); + + jp2k->shrink = 1 << jp2k->page; + jp2k->parameters.cp_reduce = jp2k->page; + if( !opj_setup_decoder( jp2k->codec, &jp2k->parameters ) ) + return( -1 ); + +#ifdef HAVE_LIBOPENJP2_THREADING + /* Use eg. VIPS_CONCURRENCY etc. to set n-cpus, if this openjpeg has + * stable support. + */ + opj_codec_set_threads( jp2k->codec, vips_concurrency_get() ); +#endif /*HAVE_LIBOPENJP2_THREADING*/ + + if( !opj_read_header( jp2k->stream, jp2k->codec, &jp2k->image ) ) + return( -1 ); + if( !(jp2k->info = opj_get_cstr_info( jp2k->codec )) ) + return( -1 ); + +#ifdef DEBUG + vips_foreign_load_jp2k_print( jp2k ); +#endif /*DEBUG*/ + + /* We only allow images where all components have the same format. + */ + if( jp2k->image->numcomps > MAX_BANDS ) { + vips_error( class->nickname, + "%s", _( "too many image bands" ) ); + return( -1 ); + } + if( jp2k->image->numcomps == 0 ) { + vips_error( class->nickname, + "%s", _( "no image components" ) ); + return( -1 ); + } + first = &jp2k->image->comps[0]; + for( i = 1; i < jp2k->image->numcomps; i++ ) { + opj_image_comp_t *this = &jp2k->image->comps[i]; + + if( this->x0 != first->x0 || + this->y0 != first->y0 || + this->w * this->dx != first->w * first->dx || + this->h * this->dy != first->h * first->dy || + this->resno_decoded != first->resno_decoded || + this->factor != first->factor ) { + vips_error( class->nickname, + "%s", _( "components differ in geometry" ) ); + return( -1 ); + } + + if( this->prec != first->prec || + this->bpp != first->bpp || + this->sgnd != first->sgnd ) { + vips_error( class->nickname, + "%s", _( "components differ in precision" ) ); + return( -1 ); + } + + /* If dx/dy are not 1, we'll need to upsample components during + * tile packing. + */ + if( this->dx != first->dx || + this->dy != first->dy || + first->dx != 1 || + first->dy != 1 ) + jp2k->upsample = TRUE; + } + + if( vips_foreign_load_jp2k_set_header( jp2k, load->out ) ) + return( -1 ); + + VIPS_SETSTR( load->out->filename, + vips_connection_filename( VIPS_CONNECTION( jp2k->source ) ) ); + + return( 0 ); +} + +#define PACK( TYPE ) { \ + TYPE *tq = (TYPE *) q; \ + \ + for( x = 0; x < length; x++ ) { \ + for( i = 0; i < b; i++ ) \ + tq[i] = planes[i][x]; \ + \ + tq += b; \ + } \ +} + +#define PACK_UPSAMPLE( TYPE ) { \ + TYPE *tq = (TYPE *) q; \ + \ + for( x = 0; x < length; x++ ) { \ + for( i = 0; i < b; i++ ) { \ + int dx = jp2k->image->comps[i].dx; \ + int pixel = planes[i][x / dx]; \ + \ + tq[i] = pixel; \ + } \ + \ + tq += b; \ + } \ +} + +/* Pack the set of openjpeg components into a libvips region. left/top are the + * offsets into the tile in pixel coordinates where we should start reading. + */ +static void +vips_foreign_load_jp2k_pack( VipsForeignLoadJp2k *jp2k, + VipsImage *image, VipsPel *q, + int left, int top, int length ) +{ + int *planes[MAX_BANDS]; + int b = jp2k->image->numcomps; + + int x, i; + + for( i = 0; i < b; i++ ) { + opj_image_comp_t *comp = &jp2k->image->comps[i]; + + planes[i] = comp->data + (top / comp->dy) * comp->w + + (left / comp->dx); + } + + if( jp2k->upsample ) + switch( image->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + PACK_UPSAMPLE( unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + PACK_UPSAMPLE( unsigned short ); + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + PACK_UPSAMPLE( unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } + else + /* Fast no-upsample path. + */ + switch( image->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + PACK( unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + PACK( unsigned short ); + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + PACK( unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } +} + +/* ycc->rgb coversion adapted from openjpeg src/bin/common/color.c + * + * See also https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion + */ +#define YCC_TO_RGB( TYPE ) { \ + TYPE *tq = (TYPE *) q; \ + \ + for( x = 0; x < length; x++ ) { \ + int y = tq[0]; \ + int cb = tq[1] - offset; \ + int cr = tq[2] - offset; \ + \ + int r, g, b; \ + \ + r = y + (int)(1.402 * (float)cr); \ + tq[0] = VIPS_CLIP( 0, r, upb ); \ + \ + g = y - (int)(0.344 * (float)cb + 0.714 * (float)cr); \ + tq[1] = VIPS_CLIP( 0, g, upb ); \ + \ + b = y + (int)(1.772 * (float)cb); \ + tq[2] = VIPS_CLIP( 0, b, upb ); \ + \ + tq += 3; \ + } \ +} + +/* YCC->RGB for a line of pels. + */ +static void +vips_foreign_load_jp2k_ycc_to_rgb( VipsForeignLoadJp2k *jp2k, + VipsPel *q, int length ) +{ + VipsForeignLoad *load = (VipsForeignLoad *) jp2k; + int prec = jp2k->image->comps[0].prec; + int offset = 1 << (prec - 1); + int upb = (1 << prec) - 1; + + int x; + + switch( load->out->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + YCC_TO_RGB( unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + YCC_TO_RGB( unsigned short ); + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + YCC_TO_RGB( unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } +} + +/* Loop over the output region, painting in tiles from the file. + */ +static int +vips_foreign_load_jp2k_generate( VipsRegion *out, + void *seq, void *a, void *b, gboolean *stop ) +{ + VipsForeignLoad *load = (VipsForeignLoad *) a; + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) load; + VipsRect *r = &out->valid; + + /* jp2k get smaller with the layer size. + */ + int tile_width = VIPS_ROUND_UINT( + (double) jp2k->info->tdx / jp2k->shrink ); + int tile_height = VIPS_ROUND_UINT( + (double) jp2k->info->tdy / jp2k->shrink ); + + /* ... so tiles_across is always the same. + */ + int tiles_across = jp2k->info->tw; + + int x, y, z; + +#ifdef DEBUG_VERBOSE + printf( "vips_foreign_load_jp2k_generate: " + "left = %d, top = %d, width = %d, height = %d\n", + r->left, r->top, r->width, r->height ); +#endif /*DEBUG_VERBOSE*/ + + /* If openjpeg has flagged an error, the library is not in a known + * state and it's not safe to call again. + */ + if( jp2k->n_errors ) + return( 0 ); + + y = 0; + while( y < r->height ) { + VipsRect tile, hit; + + /* Not necessary, but it stops static analyzers complaining + * about a used-before-set. + */ + hit.height = 0; + + x = 0; + while( x < r->width ) { + /* Tile the xy falls in, in tile numbers. + */ + int tx = (r->left + x) / tile_width; + int ty = (r->top + y) / tile_height; + + /* Pixel coordinates of the tile that xy falls in. + */ + int xs = tx * tile_width; + int ys = ty * tile_height; + + int tile_index = ty * tiles_across + tx; + + /* Fetch the tile. + */ +#ifdef DEBUG_VERBOSE + printf( " fetch tile %d\n", tile_index ); +#endif /*DEBUG_VERBOSE*/ + if( !opj_get_decoded_tile( jp2k->codec, + jp2k->stream, jp2k->image, tile_index ) ) + return( -1 ); + + /* Intersect tile with request to get pixels we need + * to copy out. + */ + tile.left = xs; + tile.top = ys; + tile.width = tile_width; + tile.height = tile_height; + vips_rect_intersectrect( &tile, r, &hit ); + + /* Unpack hit pixels to buffer in vips layout. + */ + for( z = 0; z < hit.height; z++ ) { + VipsPel *q = VIPS_REGION_ADDR( out, + hit.left, hit.top + z ); + + vips_foreign_load_jp2k_pack( jp2k, + out->im, q, + hit.left - tile.left, + hit.top - tile.top + z, + hit.width ); + + if( jp2k->ycc_to_rgb ) + vips_foreign_load_jp2k_ycc_to_rgb( jp2k, + q, hit.width ); + } + + x += hit.width; + } + + /* This will be the same for all tiles in the row we've just + * done. + */ + y += hit.height; + } + + if( load->fail && + jp2k->n_errors > 0 ) + return( -1 ); + + return( 0 ); +} + +static int +vips_foreign_load_jp2k_load( VipsForeignLoad *load ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) load; + + /* jp2k tiles get smaller with the layer size, but we don't want tiny + * tiles for the libvips tile cache, so leave them at the base size. + */ + int tile_width = jp2k->info->tdx; + int tile_height = jp2k->info->tdy; + int tiles_across = jp2k->info->tw; + + VipsImage **t = (VipsImage **) + vips_object_local_array( VIPS_OBJECT( load ), 3 ); + +#ifdef DEBUG + printf( "vips_foreign_load_jp2k_load:\n" ); +#endif /*DEBUG*/ + + t[0] = vips_image_new(); + if( vips_foreign_load_jp2k_set_header( jp2k, t[0] ) ) + return( -1 ); + + if( vips_image_generate( t[0], + NULL, vips_foreign_load_jp2k_generate, NULL, jp2k, NULL ) ) + return( -1 ); + + /* Copy to out, adding a cache. Enough tiles for two complete + * rows, plus 50%. + */ + if( vips_tilecache( t[0], &t[1], + "tile_width", tile_width, + "tile_height", tile_height, + "max_tiles", (int) (2.5 * tiles_across), + NULL ) ) + return( -1 ); + if( vips_image_write( t[1], load->real ) ) + return( -1 ); + + return( 0 ); +} + +static void +vips_foreign_load_jp2k_class_init( VipsForeignLoadJp2kClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class; + + gobject_class->dispose = vips_foreign_load_jp2k_dispose; + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "jp2kload_base"; + object_class->description = _( "load JPEG2000 image" ); + object_class->build = vips_foreign_load_jp2k_build; + + load_class->get_flags = vips_foreign_load_jp2k_get_flags; + load_class->header = vips_foreign_load_jp2k_header; + load_class->load = vips_foreign_load_jp2k_load; + + VIPS_ARG_INT( class, "page", 20, + _( "Page" ), + _( "Load this page from the image" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignLoadJp2k, page ), + 0, 100000, 0 ); + +} + +static void +vips_foreign_load_jp2k_init( VipsForeignLoadJp2k *jp2k ) +{ +} + +typedef struct _VipsForeignLoadJp2kFile { + VipsForeignLoadJp2k parent_object; + + /* Filename for load. + */ + char *filename; + +} VipsForeignLoadJp2kFile; + +typedef VipsForeignLoadJp2kClass VipsForeignLoadJp2kFileClass; + +G_DEFINE_TYPE( VipsForeignLoadJp2kFile, vips_foreign_load_jp2k_file, + vips_foreign_load_jp2k_get_type() ); + +static int +vips_foreign_load_jp2k_file_build( VipsObject *object ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) object; + VipsForeignLoadJp2kFile *file = (VipsForeignLoadJp2kFile *) object; + + if( file->filename && + !(jp2k->source = vips_source_new_from_file( file->filename )) ) + return( -1 ); + + if( VIPS_OBJECT_CLASS( vips_foreign_load_jp2k_file_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +const char *vips__jp2k_suffs[] = + { ".j2k", ".jp2", ".jpt", ".j2c", ".jpc", NULL }; + +static int +vips_foreign_load_jp2k_is_a( const char *filename ) +{ + VipsSource *source; + gboolean result; + + if( !(source = vips_source_new_from_file( filename )) ) + return( FALSE ); + result = vips_foreign_load_jp2k_is_a_source( source ); + VIPS_UNREF( source ); + + return( result ); +} + +static void +vips_foreign_load_jp2k_file_class_init( + VipsForeignLoadJp2kFileClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsForeignClass *foreign_class = (VipsForeignClass *) 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 = "jp2kload"; + object_class->build = vips_foreign_load_jp2k_file_build; + + foreign_class->suffs = vips__jp2k_suffs; + + load_class->is_a = vips_foreign_load_jp2k_is_a; + + VIPS_ARG_STRING( class, "filename", 1, + _( "Filename" ), + _( "Filename to load from" ), + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET( VipsForeignLoadJp2kFile, filename ), + NULL ); + +} + +static void +vips_foreign_load_jp2k_file_init( VipsForeignLoadJp2kFile *jp2k ) +{ +} + +typedef struct _VipsForeignLoadJp2kBuffer { + VipsForeignLoadJp2k parent_object; + + /* Load from a buffer. + */ + VipsArea *buf; + +} VipsForeignLoadJp2kBuffer; + +typedef VipsForeignLoadJp2kClass VipsForeignLoadJp2kBufferClass; + +G_DEFINE_TYPE( VipsForeignLoadJp2kBuffer, vips_foreign_load_jp2k_buffer, + vips_foreign_load_jp2k_get_type() ); + +static int +vips_foreign_load_jp2k_buffer_build( VipsObject *object ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) object; + VipsForeignLoadJp2kBuffer *buffer = + (VipsForeignLoadJp2kBuffer *) object; + + if( buffer->buf ) + if( !(jp2k->source = vips_source_new_from_memory( + VIPS_AREA( buffer->buf )->data, + VIPS_AREA( buffer->buf )->length )) ) + return( -1 ); + + if( VIPS_OBJECT_CLASS( vips_foreign_load_jp2k_file_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +static gboolean +vips_foreign_load_jp2k_buffer_is_a( const void *buf, size_t len ) +{ + VipsSource *source; + gboolean result; + + if( !(source = vips_source_new_from_memory( buf, len )) ) + return( FALSE ); + result = vips_foreign_load_jp2k_is_a_source( source ); + VIPS_UNREF( source ); + + return( result ); +} + +static void +vips_foreign_load_jp2k_buffer_class_init( + VipsForeignLoadJp2kBufferClass *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 = "jp2kload_buffer"; + object_class->build = vips_foreign_load_jp2k_buffer_build; + + load_class->is_a_buffer = vips_foreign_load_jp2k_buffer_is_a; + + VIPS_ARG_BOXED( class, "buffer", 1, + _( "Buffer" ), + _( "Buffer to load from" ), + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET( VipsForeignLoadJp2kBuffer, buf ), + VIPS_TYPE_BLOB ); + +} + +static void +vips_foreign_load_jp2k_buffer_init( VipsForeignLoadJp2kBuffer *buffer ) +{ +} + +typedef struct _VipsForeignLoadJp2kSource { + VipsForeignLoadJp2k parent_object; + + /* Load from a source. + */ + VipsSource *source; + +} VipsForeignLoadJp2kSource; + +typedef VipsForeignLoadJp2kClass VipsForeignLoadJp2kSourceClass; + +G_DEFINE_TYPE( VipsForeignLoadJp2kSource, vips_foreign_load_jp2k_source, + vips_foreign_load_jp2k_get_type() ); + +static int +vips_foreign_load_jp2k_source_build( VipsObject *object ) +{ + VipsForeignLoadJp2k *jp2k = (VipsForeignLoadJp2k *) object; + VipsForeignLoadJp2kSource *source = + (VipsForeignLoadJp2kSource *) object; + + if( source->source ) { + jp2k->source = source->source; + g_object_ref( jp2k->source ); + } + + if( VIPS_OBJECT_CLASS( + vips_foreign_load_jp2k_source_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +static void +vips_foreign_load_jp2k_source_class_init( + VipsForeignLoadJp2kSourceClass *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 = "jp2kload_source"; + object_class->build = vips_foreign_load_jp2k_source_build; + + load_class->is_a_source = vips_foreign_load_jp2k_is_a_source; + + VIPS_ARG_OBJECT( class, "source", 1, + _( "Source" ), + _( "Source to load from" ), + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET( VipsForeignLoadJp2kSource, source ), + VIPS_TYPE_SOURCE ); + +} + +static void +vips_foreign_load_jp2k_source_init( + VipsForeignLoadJp2kSource *jp2k ) +{ +} + +#endif /*HAVE_LIBOPENJP2*/ + +/** + * vips_jp2kload: + * @filename: file to load + * @out: (out): decompressed image + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @page: %gint, load this page + * + * Read a JPEG2000 image. The loader supports 8, 16 and 32-bit int pixel + * values, signed and unsigned. + * It supports greyscale, RGB, YCC, CMYK and + * multispectral colour spaces. + * It will read any ICC profile on + * the image. + * + * It will only load images where all channels are the same format. + * + * Use @page to set the page to load, where page 0 is the base resolution + * image and higher-numbered pages are x2 reductions. Use the metadata item + * "n-pages" to find the number of pyramid layers. + * + * See also: vips_image_new_from_file(). + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2kload( const char *filename, VipsImage **out, ... ) +{ + va_list ap; + int result; + + va_start( ap, out ); + result = vips_call_split( "jp2kload", ap, filename, out ); + va_end( ap ); + + return( result ); +} + +/** + * vips_jp2kload_buffer: + * @buf: (array length=len) (element-type guint8): memory area to load + * @len: (type gsize): size of memory area + * @out: (out): image to write + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @page: %gint, load this page + * + * Exactly as vips_jp2kload(), but read from a source. + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2kload_buffer( void *buf, size_t len, VipsImage **out, ... ) +{ + va_list ap; + VipsBlob *blob; + int result; + + /* We don't take a copy of the data or free it. + */ + blob = vips_blob_new( NULL, buf, len ); + + va_start( ap, out ); + result = vips_call_split( "jp2kload_buffer", ap, blob, out ); + va_end( ap ); + + vips_area_unref( VIPS_AREA( blob ) ); + + return( result ); +} + +/** + * vips_jp2kload_source: + * @source: source to load from + * @out: (out): decompressed image + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @page: %gint, load this page + * + * Exactly as vips_jp2kload(), but read from a source. + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2kload_source( VipsSource *source, VipsImage **out, ... ) +{ + va_list ap; + int result; + + va_start( ap, out ); + result = vips_call_split( "jp2kload_source", ap, source, out ); + va_end( ap ); + + return( result ); +} diff --git a/libvips/foreign/jp2ksave.c b/libvips/foreign/jp2ksave.c new file mode 100644 index 00000000..961bdec0 --- /dev/null +++ b/libvips/foreign/jp2ksave.c @@ -0,0 +1,1181 @@ +/* save as jpeg2000 + * + * 18/3/20 + * - from jp2kload.c + */ + +/* + + This file is part of VIPS. + + VIPS is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + + */ + +/* + + These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk + + */ + +/* +#define DEBUG_VERBOSE +#define DEBUG + */ + +/* TODO + * + * - could support tiff-like depth parameter + * + * - could support png-like bitdepth parameter + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ +#include + +#include +#include +#include + +#include +#include + +#ifdef HAVE_LIBOPENJP2 + +#include + +#include "pforeign.h" + +/* Surely enough ... does anyone do multispectral imaging with jp2k? + */ +#define MAX_BANDS (100) + +typedef struct _VipsForeignSaveJp2k { + VipsForeignSave parent_object; + + /* Where to write (set by subclasses). + */ + VipsTarget *target; + + int tile_width; + int tile_height; + + /* Lossless mode. + */ + gboolean lossless; + + /* Quality factor. + */ + int Q; + + /* Chroma subsample mode. + */ + VipsForeignSubsample subsample_mode; + + /* Encoder state. + */ + opj_stream_t *stream; + opj_codec_t *codec; + opj_cparameters_t parameters; + opj_image_cmptparm_t comps[MAX_BANDS]; + opj_image_t *image; + + /* The line of tiles we are building, and the buffer we + * unpack to for output. + */ + VipsRegion *strip; + VipsPel *tile_buffer; + + /* If we need to downsample during unpacking. + */ + gboolean downsample; + + /* If we converto RGB to YCC during save. + */ + gboolean save_as_ycc; + + /* Accumulate a line of sums here during chroma subsample. + */ + VipsPel *accumulate; +} VipsForeignSaveJp2k; + +typedef VipsForeignSaveClass VipsForeignSaveJp2kClass; + +G_DEFINE_ABSTRACT_TYPE( VipsForeignSaveJp2k, vips_foreign_save_jp2k, + VIPS_TYPE_FOREIGN_SAVE ); + +static void +vips_foreign_save_jp2k_dispose( GObject *gobject ) +{ + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) gobject; + + VIPS_FREEF( opj_destroy_codec, jp2k->codec ); + VIPS_FREEF( opj_stream_destroy, jp2k->stream ); + VIPS_FREEF( opj_image_destroy, jp2k->image ); + + VIPS_UNREF( jp2k->target ); + VIPS_UNREF( jp2k->strip ); + + VIPS_FREE( jp2k->tile_buffer ); + VIPS_FREE( jp2k->accumulate ); + + G_OBJECT_CLASS( vips_foreign_save_jp2k_parent_class )-> + dispose( gobject ); +} + +static OPJ_SIZE_T +vips_foreign_save_jp2k_write_target( void *buffer, size_t length, void *client ) +{ + VipsTarget *target = VIPS_TARGET( client ); + + if( vips_target_write( target, buffer, length ) ) + return( 0 ); + + return( length ); +} + +/* Make a libopenjp2 output stream that wraps a VipsTarget. + */ +static opj_stream_t * +vips_foreign_save_jp2k_target( VipsTarget *target ) +{ + opj_stream_t *stream; + + /* FALSE means a write stream. + */ + if( !(stream = opj_stream_create( OPJ_J2K_STREAM_CHUNK_SIZE, FALSE )) ) + return( NULL ); + + opj_stream_set_user_data( stream, target, NULL ); + opj_stream_set_write_function( stream, + vips_foreign_save_jp2k_write_target ); + + return( stream ); +} + +static void +vips_foreign_save_jp2k_error_callback( const char *msg, void *client ) +{ + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( client ); + + vips_error( class->nickname, "%s", msg ); +} + +/* The openjpeg info and warning callbacks are incredibly chatty. + */ +static void +vips_foreign_save_jp2k_warning_callback( const char *msg, void *client ) +{ +#ifdef DEBUG + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( client ); + + g_warning( "%s: %s", class->nickname, msg ); +#endif /*DEBUG*/ +} + +/* The openjpeg info and warning callbacks are incredibly chatty. + */ +static void +vips_foreign_save_jp2k_info_callback( const char *msg, void *client ) +{ +#ifdef DEBUG + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( client ); + + g_info( "%s: %s", class->nickname, msg ); +#endif /*DEBUG*/ +} + +static void +vips_foreign_save_jp2k_attach_handlers( VipsForeignSaveJp2k *jp2k, + opj_codec_t *codec ) +{ + opj_set_info_handler( codec, + vips_foreign_save_jp2k_info_callback, jp2k ); + opj_set_warning_handler( codec, + vips_foreign_save_jp2k_warning_callback, jp2k ); + opj_set_error_handler( codec, + vips_foreign_save_jp2k_error_callback, jp2k ); +} + +/* See also https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion + */ +#define RGB_TO_YCC( TYPE ) { \ + TYPE *tq = (TYPE *) q; \ + \ + for( x = 0; x < tile->width; x++ ) { \ + int r = tq[0]; \ + int g = tq[1]; \ + int b = tq[2]; \ + \ + int y, cb, cr; \ + \ + y = 0.299 * r + 0.587 * g + 0.114 * b; \ + tq[0] = VIPS_CLIP( 0, y, upb ); \ + \ + cb = offset - (int)(0.168736 * r + 0.331264 * g - 0.5 * b); \ + tq[1] = VIPS_CLIP( 0, cb, upb ); \ + \ + cr = offset - (int)(-0.5 * r + 0.418688 * g + 0.081312 * b); \ + tq[2] = VIPS_CLIP( 0, cr, upb ); \ + \ + tq += 3; \ + } \ +} + +/* RGB->YCC for a line of pels. + */ +static void +vips_foreign_save_jp2k_rgb_to_ycc( VipsForeignSaveJp2k *jp2k, VipsRect *tile ) +{ + VipsForeignSave *save = (VipsForeignSave *) jp2k; + int prec = jp2k->image->comps[0].prec; + int offset = 1 << (prec - 1); + int upb = (1 << prec) - 1; + + int x, y; + + for( y = 0; y < tile->height; y++ ) { + VipsPel *q = VIPS_REGION_ADDR( jp2k->strip, + tile->left, tile->top + y ); + + switch( save->ready->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + RGB_TO_YCC( unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + RGB_TO_YCC( unsigned short ); + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + RGB_TO_YCC( unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } + } +} + +/* Shrink in three stages: + * 1. copy the first line of input pels to acc + * 2. add subsequent lines in comp.dy. + * 3. horizontal average to output line + */ +#define SHRINK( ACC_TYPE, PIXEL_TYPE ) { \ + ACC_TYPE *acc = (ACC_TYPE *) jp2k->accumulate; \ + PIXEL_TYPE *tq = (PIXEL_TYPE *) q; \ + const int n_pels = comp->dx * comp->dy; \ + \ + PIXEL_TYPE *tp; \ + ACC_TYPE *ap; \ + \ + tp = (PIXEL_TYPE *) p; \ + for( x = 0; x < tile->width; x++ ) { \ + acc[x] = *tp; \ + tp += n_bands; \ + } \ + \ + for( z = 1; z < comp->dy; z++ ) { \ + tp = (PIXEL_TYPE *) (p + z * lskip); \ + for( x = 0; x < tile->width; x++ ) { \ + acc[x] += *tp; \ + tp += n_bands; \ + } \ + } \ + \ + ap = acc; \ + for( x = 0; x < output_width; x++ ) { \ + ACC_TYPE sum; \ + \ + sum = 0; \ + for( z = 0; z < comp->dx; z++ ) \ + sum += ap[z]; \ + \ + tq[x] = (sum + n_pels / 2) / n_pels; \ + ap += comp->dx; \ + } \ +} + +static void +vips_foreign_save_jp2k_unpack_downsample( VipsForeignSaveJp2k *jp2k, + VipsRect *tile ) +{ + VipsForeignSave *save = (VipsForeignSave *) jp2k; + size_t sizeof_element = VIPS_IMAGE_SIZEOF_ELEMENT( save->ready ); + size_t lskip = VIPS_REGION_LSKIP( jp2k->strip ); + int n_bands = save->ready->Bands; + + VipsPel *q; + int x, y, z, band_index; + + q = jp2k->tile_buffer; + for( band_index = 0; band_index < n_bands; band_index++ ) { + opj_image_comp_t *comp = &jp2k->image->comps[band_index]; + + /* The number of pixels we write for this component. Round to + * nearest, and we may have to write half-pixels at the edges. + */ + int output_width = VIPS_ROUND_UINT( + (double) tile->width / comp->dx ); + int output_height = VIPS_ROUND_UINT( + (double) tile->height / comp->dy );; + + for( y = 0; y < output_height; y++ ) { + VipsPel *p = band_index * sizeof_element + + VIPS_REGION_ADDR( jp2k->strip, + tile->left, tile->top + y * comp->dy ); + + /* Shrink a line of pels to q. + */ + switch( save->ready->BandFmt ) { + case VIPS_FORMAT_CHAR: + SHRINK( int, signed char ); + break; + + case VIPS_FORMAT_UCHAR: + SHRINK( int, unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + SHRINK( int, signed short ); + break; + + case VIPS_FORMAT_USHORT: + SHRINK( int, unsigned short ); + break; + + case VIPS_FORMAT_INT: + SHRINK( gint64, signed int ); + break; + + case VIPS_FORMAT_UINT: + SHRINK( gint64, unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } + + q += sizeof_element * output_width; + } + } +} + +#define UNPACK( TYPE ) { \ + TYPE **tplanes = (TYPE **) planes; \ + TYPE *tp = (TYPE *) p; \ + \ + for( i = 0; i < b; i++ ) { \ + TYPE *q = tplanes[i]; \ + TYPE *tp1 = tp + i; \ + \ + for( x = 0; x < tile->width; x++ ) { \ + q[x] = *tp1; \ + tp1 += b; \ + } \ + \ + tplanes[i] += tile->width; \ + } \ +} + +static void +vips_foreign_save_jp2k_unpack( VipsForeignSaveJp2k *jp2k, VipsRect *tile ) +{ + VipsForeignSave *save = (VipsForeignSave *) jp2k; + size_t sizeof_element = VIPS_IMAGE_SIZEOF_ELEMENT( save->ready ); + int b = save->ready->Bands; + + VipsPel *planes[MAX_BANDS]; + int x, y, i; + + for( i = 0; i < b; i++ ) + planes[i] = jp2k->tile_buffer + + i * sizeof_element * tile->width * tile->height; + + for( y = 0; y < tile->height; y++ ) { + VipsPel *p = VIPS_REGION_ADDR( jp2k->strip, + tile->left, tile->top + y ); + + switch( save->ready->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + UNPACK( unsigned char ); + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + UNPACK( unsigned short ); + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + UNPACK( unsigned int ); + break; + + default: + g_assert_not_reached(); + break; + } + } +} + +static size_t +vips_foreign_save_jp2k_sizeof_tile( VipsForeignSaveJp2k *jp2k, VipsRect *tile ) +{ + VipsForeignSave *save = (VipsForeignSave *) jp2k; + size_t sizeof_element = VIPS_IMAGE_SIZEOF_ELEMENT( save->ready ); + + size_t size; + int i; + + size = 0; + for( i = 0; i < jp2k->image->numcomps; i++ ) { + opj_image_comp_t *comp = &jp2k->image->comps[i]; + + /* The number of pixels we write for this component. Round to + * nearest, and we may have to write half-pixels at the edges. + */ + int output_width = VIPS_ROUND_UINT( + (double) tile->width / comp->dx ); + int output_height = VIPS_ROUND_UINT( + (double) tile->height / comp->dy );; + + size += output_width * output_height * sizeof_element; + } + + return( size ); +} + +static int +vips_foreign_save_jp2k_write_tiles( VipsForeignSaveJp2k *jp2k ) +{ + VipsForeignSave *save = (VipsForeignSave *) jp2k; + int tiles_across = + VIPS_ROUND_UP( save->ready->Xsize, jp2k->tile_width ) / + jp2k->tile_width; + + int x; + + for( x = 0; x < save->ready->Xsize; x += jp2k->tile_width ) { + VipsRect tile; + size_t sizeof_tile; + int tile_index; + + tile.left = x; + tile.top = jp2k->strip->valid.top; + tile.width = jp2k->tile_width; + tile.height = jp2k->tile_height; + vips_rect_intersectrect( &tile, &jp2k->strip->valid, &tile ); + + if( jp2k->save_as_ycc ) + vips_foreign_save_jp2k_rgb_to_ycc( jp2k, &tile ); + + if( jp2k->downsample ) + vips_foreign_save_jp2k_unpack_downsample( jp2k, &tile ); + else + vips_foreign_save_jp2k_unpack( jp2k, &tile ); + + sizeof_tile = vips_foreign_save_jp2k_sizeof_tile( jp2k, &tile ); + tile_index = tiles_across * tile.top / jp2k->tile_height + + x / jp2k->tile_width; + if( !opj_write_tile( jp2k->codec, tile_index, + (VipsPel *) jp2k->tile_buffer, sizeof_tile, + jp2k->stream ) ) + return( -1 ); + } + + return( 0 ); +} + +static int +vips_foreign_save_jp2k_write_block( VipsRegion *region, VipsRect *area, + void *a ) +{ + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) a; + VipsForeignSave *save = (VipsForeignSave *) jp2k; + +#ifdef DEBUG_VERBOSE + printf( "vips_foreign_save_jp2k_write_block: y = %d, nlines = %d\n", + area->top, area->height ); +#endif /*DEBUG_VERBOSE*/ + + for(;;) { + VipsRect hit; + int y; + VipsRect strip_position; + + /* The intersection with the strip is the fresh pixels we + * have. + */ + vips_rect_intersectrect( area, &(jp2k->strip->valid), &hit ); + + /* Copy the new pixels into the strip. + */ + for( y = 0; y < hit.height; y++ ) { + VipsPel *p = VIPS_REGION_ADDR( region, + 0, hit.top + y ); + VipsPel *q = VIPS_REGION_ADDR( jp2k->strip, + 0, hit.top + y ); + + memcpy( q, p, VIPS_IMAGE_SIZEOF_LINE( region->im ) ); + } + + /* Have we failed to reach the bottom of the strip? We must + * have run out of fresh pixels, so we are done. + */ + if( VIPS_RECT_BOTTOM( &hit ) != + VIPS_RECT_BOTTOM( &jp2k->strip->valid ) ) + break; + + /* We have reached the bottom of the strip. Write this line of + * pixels and ove the strip down. + */ + if( vips_foreign_save_jp2k_write_tiles( jp2k ) ) + return( -1 ); + + strip_position.left = 0; + strip_position.top = jp2k->strip->valid.top + jp2k->tile_height; + strip_position.width = save->ready->Xsize; + strip_position.height = jp2k->tile_height; + if( vips_region_buffer( jp2k->strip, &strip_position ) ) + return( -1 ); + } + + return( 0 ); +} + +static int +vips_foreign_save_jp2k_build( VipsObject *object ) +{ + VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( object ); + VipsForeignSave *save = (VipsForeignSave *) object; + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) object; + + OPJ_COLOR_SPACE color_space; + int expected_bands; + int bits_per_pixel; + int i; + size_t sizeof_tile; + size_t sizeof_line; + VipsRect strip_position; + + if( VIPS_OBJECT_CLASS( vips_foreign_save_jp2k_parent_class )-> + build( object ) ) + return( -1 ); + + opj_set_default_encoder_parameters( &jp2k->parameters ); + + /* Analyze our arguments. + */ + + if( !vips_band_format_isint( save->ready->BandFmt ) ) { + vips_error( class->nickname, + "%s", _( "not an integer format" ) ); + return( -1 ); + } + + switch( jp2k->subsample_mode ) { + case VIPS_FOREIGN_SUBSAMPLE_AUTO: + jp2k->downsample = + !jp2k->lossless && + jp2k->Q < 90 && + save->ready->Xsize % 2 == 0 && + save->ready->Ysize % 2 == 0 && + (save->ready->Type == VIPS_INTERPRETATION_sRGB || + save->ready->Type == VIPS_INTERPRETATION_RGB16) && + save->ready->Bands == 3; + break; + + case VIPS_FOREIGN_SUBSAMPLE_ON: + jp2k->downsample = TRUE; + break; + + case VIPS_FOREIGN_SUBSAMPLE_OFF: + jp2k->downsample = FALSE; + break; + + default: + g_assert_not_reached(); + break; + } + + if( jp2k->downsample ) + jp2k->save_as_ycc = TRUE; + + /* CIELAB etc. do not seem to be well documented. + */ + switch( save->ready->Type ) { + case VIPS_INTERPRETATION_B_W: + case VIPS_INTERPRETATION_GREY16: + color_space = OPJ_CLRSPC_GRAY; + expected_bands = 1; + break; + + case VIPS_INTERPRETATION_sRGB: + case VIPS_INTERPRETATION_RGB16: + color_space = jp2k->save_as_ycc ? + OPJ_CLRSPC_SYCC : OPJ_CLRSPC_SRGB; + expected_bands = 3; + break; + + case VIPS_INTERPRETATION_CMYK: + color_space = OPJ_CLRSPC_CMYK; + expected_bands = 4; + break; + + default: + color_space = OPJ_CLRSPC_UNSPECIFIED; + expected_bands = save->ready->Bands; + break; + } + + switch( save->ready->BandFmt ) { + case VIPS_FORMAT_CHAR: + case VIPS_FORMAT_UCHAR: + bits_per_pixel = 8; + break; + + case VIPS_FORMAT_SHORT: + case VIPS_FORMAT_USHORT: + bits_per_pixel = 16; + break; + + case VIPS_FORMAT_INT: + case VIPS_FORMAT_UINT: + /* OpenJPEG only supports up to 31. + */ + bits_per_pixel = 31; + break; + + default: + g_assert_not_reached(); + break; + } + + /* Set parameters for compressor. + */ + + /* Always tile. + */ + jp2k->parameters.tile_size_on = OPJ_TRUE; + jp2k->parameters.cp_tdx = jp2k->tile_width; + jp2k->parameters.cp_tdy = jp2k->tile_height; + + /* Number of layers to write. Smallest layer is c. 2^5 on the smallest + * axis. + */ + jp2k->parameters.numresolution = VIPS_MAX( 1, + log( VIPS_MIN( save->ready->Xsize, save->ready->Ysize ) ) / + log( 2 ) - 4 ); +#ifdef DEBUG + printf( "vips_foreign_save_jp2k_build: numresolutions = %d\n", + jp2k->parameters.numresolution ); +#endif /*DEBUG*/ + + for( i = 0; i < save->ready->Bands; i++ ) { + jp2k->comps[i].dx = (jp2k->downsample && i > 0) ? 2 : 1; + jp2k->comps[i].dy = (jp2k->downsample && i > 0) ? 2 : 1; + jp2k->comps[i].w = save->ready->Xsize; + jp2k->comps[i].h = save->ready->Ysize; + jp2k->comps[i].x0 = 0; + jp2k->comps[i].y0 = 0; + jp2k->comps[i].prec = bits_per_pixel; + jp2k->comps[i].bpp = bits_per_pixel; + jp2k->comps[i].sgnd = + !vips_band_format_isuint( save->ready->BandFmt ); + } + + /* Makes three band images smaller, somehow. + */ + jp2k->parameters.tcp_mct = + (save->ready->Bands == 3 && !jp2k->downsample) ? 1 : 0; + + /* Lossy mode. + */ + if( !jp2k->lossless ) { + jp2k->parameters.irreversible = TRUE; + + /* Map Q to allowed distortion. + */ + jp2k->parameters.cp_disto_alloc = 1; + jp2k->parameters.cp_fixed_quality = TRUE; + jp2k->parameters.tcp_distoratio[0] = jp2k->Q; + jp2k->parameters.tcp_numlayers = 1; + } + + /* Create output image. + */ + + jp2k->image = opj_image_create( save->ready->Bands, + jp2k->comps, color_space ); + jp2k->image->x1 = save->ready->Xsize; + jp2k->image->y1 = save->ready->Ysize; + + /* Tag alpha channels. + */ + for( i = 0; i < save->ready->Bands; i++ ) + jp2k->image->comps[i].alpha = i >= expected_bands; + + /* Set up compressor. + */ + + jp2k->codec = opj_create_compress( OPJ_CODEC_J2K ); + vips_foreign_save_jp2k_attach_handlers( jp2k, jp2k->codec ); + if( !opj_setup_encoder( jp2k->codec, &jp2k->parameters, jp2k->image ) ) + return( -1 ); + +#ifdef HAVE_LIBOPENJP2_THREADING + /* Use eg. VIPS_CONCURRENCY etc. to set n-cpus, if this openjpeg has + * stable support. + */ + opj_codec_set_threads( jp2k->codec, vips_concurrency_get() ); +#endif /*HAVE_LIBOPENJP2_THREADING*/ + + if( !(jp2k->stream = vips_foreign_save_jp2k_target( jp2k->target )) ) + return( -1 ); + + if( !opj_start_compress( jp2k->codec, jp2k->image, jp2k->stream ) ) + return( -1 ); + + /* The buffer we repack tiles to for write. Large enough for one + * complete tile. + */ + sizeof_tile = VIPS_IMAGE_SIZEOF_PEL( save->ready ) * + jp2k->tile_width * jp2k->tile_height; + if( !(jp2k->tile_buffer = VIPS_ARRAY( NULL, sizeof_tile, VipsPel )) ) + return( -1 ); + + /* We need a line of sums for chroma subsample. At worst, gint64. + */ + sizeof_line = sizeof( gint64 ) * jp2k->tile_width; + if( !(jp2k->accumulate = VIPS_ARRAY( NULL, sizeof_line, VipsPel )) ) + return( -1 ); + + /* The line of tiles we are building. + */ + jp2k->strip = vips_region_new( save->ready ); + + /* Position strip at the top of the image, the height of a row of + * tiles. + */ + strip_position.left = 0; + strip_position.top = 0; + strip_position.width = save->ready->Xsize; + strip_position.height = jp2k->tile_height; + if( vips_region_buffer( jp2k->strip, &strip_position ) ) + return( -1 ); + + /* Write data. + */ + if( vips_sink_disc( save->ready, + vips_foreign_save_jp2k_write_block, jp2k ) ) + return( -1 ); + + opj_end_compress( jp2k->codec, jp2k->stream ); + + vips_target_finish( jp2k->target ); + + return( 0 ); +} + +static void +vips_foreign_save_jp2k_class_init( VipsForeignSaveJp2kClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + VipsForeignClass *foreign_class = (VipsForeignClass *) class; + VipsForeignSaveClass *save_class = (VipsForeignSaveClass *) class; + + gobject_class->dispose = vips_foreign_save_jp2k_dispose; + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "jp2ksave_base"; + object_class->description = _( "save image in HEIF format" ); + object_class->build = vips_foreign_save_jp2k_build; + + foreign_class->suffs = vips__jp2k_suffs; + + save_class->saveable = VIPS_SAVEABLE_ANY; + + VIPS_ARG_INT( class, "tile_width", 11, + _( "Tile width" ), + _( "Tile width in pixels" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2k, tile_width ), + 1, 32768, 512 ); + + VIPS_ARG_INT( class, "tile_height", 12, + _( "Tile height" ), + _( "Tile height in pixels" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2k, tile_height ), + 1, 32768, 512 ); + + VIPS_ARG_BOOL( class, "lossless", 13, + _( "Lossless" ), + _( "Enable lossless compression" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2k, lossless ), + FALSE ); + + VIPS_ARG_ENUM( class, "subsample_mode", 19, + _( "Subsample mode" ), + _( "Select chroma subsample operation mode" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2k, subsample_mode ), + VIPS_TYPE_FOREIGN_SUBSAMPLE, + VIPS_FOREIGN_SUBSAMPLE_AUTO ); + + VIPS_ARG_INT( class, "Q", 14, + _( "Q" ), + _( "Q factor" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2k, Q ), + 1, 100, 45 ); + +} + +static void +vips_foreign_save_jp2k_init( VipsForeignSaveJp2k *jp2k ) +{ + jp2k->tile_width = 512; + jp2k->tile_height = 512; + + /* 45 gives about the same filesize as default regular jpg. + */ + jp2k->Q = 45; + + jp2k->subsample_mode = VIPS_FOREIGN_SUBSAMPLE_AUTO; +} + +typedef struct _VipsForeignSaveJp2kFile { + VipsForeignSaveJp2k parent_object; + + /* Filename for save. + */ + char *filename; + +} VipsForeignSaveJp2kFile; + +typedef VipsForeignSaveJp2kClass VipsForeignSaveJp2kFileClass; + +G_DEFINE_TYPE( VipsForeignSaveJp2kFile, vips_foreign_save_jp2k_file, + vips_foreign_save_jp2k_get_type() ); + +static int +vips_foreign_save_jp2k_file_build( VipsObject *object ) +{ + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) object; + VipsForeignSaveJp2kFile *file = (VipsForeignSaveJp2kFile *) object; + + if( !(jp2k->target = vips_target_new_to_file( file->filename )) ) + return( -1 ); + + if( VIPS_OBJECT_CLASS( vips_foreign_save_jp2k_file_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +static void +vips_foreign_save_jp2k_file_class_init( VipsForeignSaveJp2kFileClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "jp2ksave"; + object_class->build = vips_foreign_save_jp2k_file_build; + + VIPS_ARG_STRING( class, "filename", 1, + _( "Filename" ), + _( "Filename to load from" ), + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2kFile, filename ), + NULL ); + +} + +static void +vips_foreign_save_jp2k_file_init( VipsForeignSaveJp2kFile *file ) +{ +} + +typedef struct _VipsForeignSaveJp2kBuffer { + VipsForeignSaveJp2k parent_object; + + /* Save to a buffer. + */ + VipsArea *buf; + +} VipsForeignSaveJp2kBuffer; + +typedef VipsForeignSaveJp2kClass VipsForeignSaveJp2kBufferClass; + +G_DEFINE_TYPE( VipsForeignSaveJp2kBuffer, vips_foreign_save_jp2k_buffer, + vips_foreign_save_jp2k_get_type() ); + +static int +vips_foreign_save_jp2k_buffer_build( VipsObject *object ) +{ + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) object; + VipsForeignSaveJp2kBuffer *buffer = + (VipsForeignSaveJp2kBuffer *) object; + + VipsBlob *blob; + + if( !(jp2k->target = vips_target_new_to_memory()) ) + return( -1 ); + + if( VIPS_OBJECT_CLASS( vips_foreign_save_jp2k_buffer_parent_class )-> + build( object ) ) + return( -1 ); + + g_object_get( jp2k->target, "blob", &blob, NULL ); + g_object_set( buffer, "buffer", blob, NULL ); + vips_area_unref( VIPS_AREA( blob ) ); + + return( 0 ); +} + +static void +vips_foreign_save_jp2k_buffer_class_init( + VipsForeignSaveJp2kBufferClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "jp2ksave_buffer"; + object_class->build = vips_foreign_save_jp2k_buffer_build; + + VIPS_ARG_BOXED( class, "buffer", 1, + _( "Buffer" ), + _( "Buffer to save to" ), + VIPS_ARGUMENT_REQUIRED_OUTPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2kBuffer, buf ), + VIPS_TYPE_BLOB ); + +} + +static void +vips_foreign_save_jp2k_buffer_init( VipsForeignSaveJp2kBuffer *buffer ) +{ +} + +typedef struct _VipsForeignSaveJp2kTarget { + VipsForeignSaveJp2k parent_object; + + VipsTarget *target; +} VipsForeignSaveJp2kTarget; + +typedef VipsForeignSaveJp2kClass VipsForeignSaveJp2kTargetClass; + +G_DEFINE_TYPE( VipsForeignSaveJp2kTarget, vips_foreign_save_jp2k_target, + vips_foreign_save_jp2k_get_type() ); + +static int +vips_foreign_save_jp2k_target_build( VipsObject *object ) +{ + VipsForeignSaveJp2k *jp2k = (VipsForeignSaveJp2k *) object; + VipsForeignSaveJp2kTarget *target = + (VipsForeignSaveJp2kTarget *) object; + + if( target->target ) { + jp2k->target = target->target; + g_object_ref( jp2k->target ); + } + + if( VIPS_OBJECT_CLASS( vips_foreign_save_jp2k_target_parent_class )-> + build( object ) ) + return( -1 ); + + return( 0 ); +} + +static void +vips_foreign_save_jp2k_target_class_init( + VipsForeignSaveJp2kTargetClass *class ) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS( class ); + VipsObjectClass *object_class = (VipsObjectClass *) class; + + gobject_class->set_property = vips_object_set_property; + gobject_class->get_property = vips_object_get_property; + + object_class->nickname = "jp2ksave_target"; + object_class->build = vips_foreign_save_jp2k_target_build; + + VIPS_ARG_OBJECT( class, "target", 1, + _( "Target" ), + _( "Target to save to" ), + VIPS_ARGUMENT_REQUIRED_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveJp2kTarget, target ), + VIPS_TYPE_TARGET ); + +} + +static void +vips_foreign_save_jp2k_target_init( VipsForeignSaveJp2kTarget *target ) +{ +} + +#endif /*HAVE_LIBOPENJP2*/ + +/** + * vips_jp2ksave: (method) + * @in: image to save + * @filename: file to write to + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @Q: %gint, quality factor + * * @lossless: %gboolean, enables lossless compression + * * @tile_width: %gint for tile size + * * @tile_height: %gint for tile size + * * @subsample_mode: #VipsForeignSubsample, chroma subsampling mode + * + * Write a VIPS image to a file in JPEG2000 format. + * The saver supports 8, 16 and 32-bit int pixel + * values, signed and unsigned. It supports greyscale, RGB, CMYK and + * multispectral images. + * + * Use @Q to set the compression quality factor. The default value of 45 + * produces file with approximately the same size as regular JPEG Q 75. + * + * Set @lossless to enable lossless compresion. + * + * Use @tile_width and @tile_height to set the tile size. The default is 512. + * + * Chroma subsampling is normally automatically disabled for Q >= 90. You can + * force the subsampling mode with @subsample_mode. + * + * This operation always writes a pyramid. + * + * See also: vips_image_write_to_file(), vips_jp2kload(). + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2ksave( VipsImage *in, const char *filename, ... ) +{ + va_list ap; + int result; + + va_start( ap, filename ); + result = vips_call_split( "jp2ksave", ap, in, filename ); + va_end( ap ); + + return( result ); +} + +/** + * vips_jp2ksave_buffer: (method) + * @in: image to save + * @buf: (array length=len) (element-type guint8): return output buffer here + * @len: (type gsize): return output length here + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @Q: %gint, quality factor + * * @lossless: %gboolean, enables lossless compression + * * @tile_width: %gint for tile size + * * @tile_height: %gint for tile size + * * @subsample_mode: #VipsForeignSubsample, chroma subsampling mode + * + * As vips_jp2ksave(), but save to a target. + * + * See also: vips_jp2ksave(), vips_image_write_to_target(). + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2ksave_buffer( VipsImage *in, void **buf, size_t *len, ... ) +{ + va_list ap; + VipsArea *area; + int result; + + area = NULL; + + va_start( ap, len ); + result = vips_call_split( "jp2ksave_buffer", ap, in, &area ); + va_end( ap ); + + if( !result && + area ) { + if( buf ) { + *buf = area->data; + area->free_fn = NULL; + } + if( len ) + *len = area->length; + + vips_area_unref( area ); + } + + return( result ); +} + +/** + * vips_jp2ksave_target: (method) + * @in: image to save + * @target: save image to this target + * @...: %NULL-terminated list of optional named arguments + * + * Optional arguments: + * + * * @Q: %gint, quality factor + * * @lossless: %gboolean, enables lossless compression + * * @tile_width: %gint for tile size + * * @tile_height: %gint for tile size + * * @subsample_mode: #VipsForeignSubsample, chroma subsampling mode + * + * As vips_jp2ksave(), but save to a target. + * + * See also: vips_jp2ksave(), vips_image_write_to_target(). + * + * Returns: 0 on success, -1 on error. + */ +int +vips_jp2ksave_target( VipsImage *in, VipsTarget *target, ... ) +{ + va_list ap; + int result; + + va_start( ap, target ); + result = vips_call_split( "jp2ksave_target", ap, in, target ); + va_end( ap ); + + return( result ); +} diff --git a/libvips/foreign/jpegsave.c b/libvips/foreign/jpegsave.c index cc6feac4..be3e2ec9 100644 --- a/libvips/foreign/jpegsave.c +++ b/libvips/foreign/jpegsave.c @@ -557,7 +557,7 @@ vips_foreign_save_jpeg_mime_init( VipsForeignSaveJpegMime *mime ) * If @strip is set, no EXIF data, IPTC data, ICC profile or XMP metadata is * written into the output file. * - * Chroma subsampling is normally automatically disabled for Q > 90. You can + * Chroma subsampling is normally automatically disabled for Q >= 90. You can * force the subsampling mode with @subsample_mode. * * If @trellis_quant is set and the version of libjpeg supports it diff --git a/libvips/foreign/pforeign.h b/libvips/foreign/pforeign.h index 18e472bf..433bf1fe 100644 --- a/libvips/foreign/pforeign.h +++ b/libvips/foreign/pforeign.h @@ -245,6 +245,8 @@ void vips__heif_error( struct heif_error *error ); struct heif_image; void vips__heif_image_print( struct heif_image *img ); +extern const char *vips__jp2k_suffs[]; + #ifdef __cplusplus } #endif /*__cplusplus*/ diff --git a/libvips/include/vips/foreign.h b/libvips/include/vips/foreign.h index 42a9bad5..7a86f2f9 100644 --- a/libvips/include/vips/foreign.h +++ b/libvips/include/vips/foreign.h @@ -372,7 +372,7 @@ int vips_openslideload_source( VipsSource *source, VipsImage **out, ... ) /** * VipsForeignSubsample: - * @VIPS_FOREIGN_SUBSAMPLE_AUTO: prevent subsampling when quality > 90 + * @VIPS_FOREIGN_SUBSAMPLE_AUTO: prevent subsampling when quality >= 90 * @VIPS_FOREIGN_SUBSAMPLE_ON: always perform subsampling * @VIPS_FOREIGN_SUBSAMPLE_OFF: never perform subsampling * @@ -676,6 +676,19 @@ int vips_niftiload_source( VipsSource *source, VipsImage **out, ... ) int vips_niftisave( VipsImage *in, const char *filename, ... ) __attribute__((sentinel)); +int vips_jp2kload( const char *filename, VipsImage **out, ... ) + __attribute__((sentinel)); +int vips_jp2kload_buffer( void *buf, size_t len, VipsImage **out, ... ) + __attribute__((sentinel)); +int vips_jp2kload_source( VipsSource *source, VipsImage **out, ... ) + __attribute__((sentinel)); +int vips_jp2ksave( VipsImage *in, const char *filename, ... ) + __attribute__((sentinel)); +int vips_jp2ksave_buffer( VipsImage *in, void **buf, size_t *len, ... ) + __attribute__((sentinel)); +int vips_jp2ksave_target( VipsImage *in, VipsTarget *target, ... ) + __attribute__((sentinel)); + /** * VipsForeignDzLayout: * @VIPS_FOREIGN_DZ_LAYOUT_DZ: use DeepZoom directory layout diff --git a/libvips/resample/thumbnail.c b/libvips/resample/thumbnail.c index 0d819be7..35443181 100644 --- a/libvips/resample/thumbnail.c +++ b/libvips/resample/thumbnail.c @@ -136,11 +136,11 @@ typedef struct _VipsThumbnail { int heif_thumbnail_width; int heif_thumbnail_height; - /* For TIFF sources, open subifds to get pyr layers. + /* Pyramids are stored in subifds. */ gboolean subifd_pyramid; - /* For TIFF sources, open pages to get pyr layers. + /* Pyramids are stored in pages. */ gboolean page_pyramid; @@ -250,18 +250,18 @@ vips_thumbnail_read_header( VipsThumbnail *thumbnail, VipsImage *image ) } } -/* Detect a TIFF pyramid made of pages following a roughly /2 shrink. +/* Detect a pyramid made of pages following a roughly /2 shrink. * * This may not be a pyr tiff, so no error if we can't find the layers. */ static void -vips_thumbnail_get_tiff_pyramid_page( VipsThumbnail *thumbnail ) +vips_thumbnail_get_pyramid_page( VipsThumbnail *thumbnail ) { VipsThumbnailClass *class = VIPS_THUMBNAIL_GET_CLASS( thumbnail ); int i; #ifdef DEBUG - printf( "vips_thumbnail_get_tiff_pyramid_page:\n" ); + printf( "vips_thumbnail_get_pyramid_page:\n" ); #endif /*DEBUG*/ /* Single-page docs can't be pyramids. @@ -301,7 +301,7 @@ vips_thumbnail_get_tiff_pyramid_page( VipsThumbnail *thumbnail ) /* Now set level_count. This signals that we've found a pyramid. */ #ifdef DEBUG - printf( "vips_thumbnail_get_tiff_pyramid_page: " + printf( "vips_thumbnail_get_pyramid_page: " "%d layer pyramid detected\n", thumbnail->n_pages ); #endif /*DEBUG*/ @@ -500,7 +500,7 @@ vips_thumbnail_find_jpegshrink( VipsThumbnail *thumbnail, return( 1 ); } -/* Find the best pyramid (openslide or tiff) level. +/* Find the best pyramid (openslide, tiff, etc.) level. */ static int vips_thumbnail_find_pyrlevel( VipsThumbnail *thumbnail, @@ -553,7 +553,21 @@ vips_thumbnail_open( VipsThumbnail *thumbnail ) thumbnail->subifd_pyramid = FALSE; thumbnail->page_pyramid = TRUE; - vips_thumbnail_get_tiff_pyramid_page( thumbnail ); + vips_thumbnail_get_pyramid_page( thumbnail ); + + if( thumbnail->level_count == 0 ) + thumbnail->page_pyramid = FALSE; + } + } + + /* jp2k uses page-based pyramids. + */ + if( vips_isprefix( "VipsForeignLoadJp2k", thumbnail->loader ) ) { + if( thumbnail->level_count == 0 ) { + thumbnail->subifd_pyramid = FALSE; + thumbnail->page_pyramid = TRUE; + + vips_thumbnail_get_pyramid_page( thumbnail ); if( thumbnail->level_count == 0 ) thumbnail->page_pyramid = FALSE; @@ -576,8 +590,9 @@ vips_thumbnail_open( VipsThumbnail *thumbnail ) factor = vips_thumbnail_find_jpegshrink( thumbnail, thumbnail->input_width, thumbnail->input_height ); else if( vips_isprefix( "VipsForeignLoadTiff", thumbnail->loader ) || + vips_isprefix( "VipsForeignLoadJp2k", thumbnail->loader ) || vips_isprefix( "VipsForeignLoadOpenslide", - thumbnail->loader ) ) { + thumbnail->loader ) ) { if( thumbnail->level_count > 0 ) factor = vips_thumbnail_find_pyrlevel( thumbnail, thumbnail->input_width, @@ -1112,7 +1127,19 @@ vips_thumbnail_file_open( VipsThumbnail *thumbnail, double factor ) "scale", 1.0 / factor, NULL ) ); } - + else if( vips_isprefix( "VipsForeignLoadJp2k", thumbnail->loader ) ) { + /* jp2k optionally uses page-based pyramids. + */ + if( thumbnail->page_pyramid ) + return( vips_image_new_from_file( file->filename, + "access", VIPS_ACCESS_SEQUENTIAL, + "page", (int) factor, + NULL ) ); + else + return( vips_image_new_from_file( file->filename, + "access", VIPS_ACCESS_SEQUENTIAL, + NULL ) ); + } else if( vips_isprefix( "VipsForeignLoadTiff", thumbnail->loader ) ) { /* We support three modes: subifd pyramids, page-based * pyramids, and simple multi-page TIFFs (no pyramid). @@ -1132,7 +1159,6 @@ vips_thumbnail_file_open( VipsThumbnail *thumbnail, double factor ) "access", VIPS_ACCESS_SEQUENTIAL, NULL ) ); } - else if( vips_isprefix( "VipsForeignLoadHeif", thumbnail->loader ) ) { return( vips_image_new_from_file( file->filename, "access", VIPS_ACCESS_SEQUENTIAL, @@ -1325,6 +1351,23 @@ vips_thumbnail_buffer_open( VipsThumbnail *thumbnail, double factor ) "scale", 1.0 / factor, NULL ) ); } + else if( vips_isprefix( "VipsForeignLoadJp2k", thumbnail->loader ) ) { + /* Optional page-based pyramids. + */ + if( thumbnail->page_pyramid ) + return( vips_image_new_from_buffer( + buffer->buf->data, buffer->buf->length, + buffer->option_string, + "access", VIPS_ACCESS_SEQUENTIAL, + "page", (int) factor, + NULL ) ); + else + return( vips_image_new_from_buffer( + buffer->buf->data, buffer->buf->length, + buffer->option_string, + "access", VIPS_ACCESS_SEQUENTIAL, + NULL ) ); + } else if( vips_isprefix( "VipsForeignLoadTiff", thumbnail->loader ) ) { /* We support three modes: subifd pyramids, page-based * pyramids, and simple multi-page TIFFs (no pyramid). @@ -1521,6 +1564,23 @@ vips_thumbnail_source_open( VipsThumbnail *thumbnail, double factor ) "scale", 1.0 / factor, NULL ) ); } + else if( vips_isprefix( "VipsForeignLoadJp2k", thumbnail->loader ) ) { + /* Optional page-based pyramids. + */ + if( thumbnail->page_pyramid ) + return( vips_image_new_from_source( + source->source, + source->option_string, + "access", VIPS_ACCESS_SEQUENTIAL, + "page", (int) factor, + NULL ) ); + else + return( vips_image_new_from_source( + source->source, + source->option_string, + "access", VIPS_ACCESS_SEQUENTIAL, + NULL ) ); + } else if( vips_isprefix( "VipsForeignLoadTiff", thumbnail->loader ) ) { /* We support three modes: subifd pyramids, page-based * pyramids, and simple multi-page TIFFs (no pyramid). diff --git a/test/test-suite/helpers/helpers.py b/test/test-suite/helpers/helpers.py index bac50248..0fa2bab6 100644 --- a/test/test-suite/helpers/helpers.py +++ b/test/test-suite/helpers/helpers.py @@ -56,6 +56,7 @@ MOSAIC_MARKS = [[489, 140], [66, 141], MOSAIC_VERTICAL_MARKS = [[388, 44], [364, 346], [384, 17], [385, 629], [527, 42], [503, 959]] +JP2K_FILE = os.path.join(IMAGES, "world.jp2") unsigned_formats = [pyvips.BandFormat.UCHAR, pyvips.BandFormat.USHORT, diff --git a/test/test-suite/images/world.jp2 b/test/test-suite/images/world.jp2 new file mode 100644 index 00000000..0f84bc50 Binary files /dev/null and b/test/test-suite/images/world.jp2 differ diff --git a/test/test-suite/test_foreign.py b/test/test-suite/test_foreign.py index b74142dc..496c46a9 100644 --- a/test/test-suite/test_foreign.py +++ b/test/test-suite/test_foreign.py @@ -17,7 +17,7 @@ from helpers import \ GIF_ANIM_DISPOSE_PREVIOUS_EXPECTED_PNG_FILE, \ temp_filename, assert_almost_equal_objects, have, skip_if_no, \ TIF1_FILE, TIF2_FILE, TIF4_FILE, WEBP_LOOKS_LIKE_SVG_FILE, \ - WEBP_ANIMATED_FILE + WEBP_ANIMATED_FILE, JP2K_FILE class TestForeign: tempdir = None @@ -1119,6 +1119,54 @@ class TestForeign: y = pyvips.Image.new_from_buffer(buf, "") assert y.get("exif-ifd0-Make").split(" ")[0] == "banana" + @skip_if_no("jp2kload") + def test_jp2kload(self): + def jp2k_valid(im): + a = im(402, 73) + assert_almost_equal_objects(a, [141, 144, 73], threshold=2) + assert im.width == 800 + assert im.height == 400 + assert im.bands == 3 + + self.file_loader("jp2kload", JP2K_FILE, jp2k_valid) + self.buffer_loader("jp2kload_buffer", JP2K_FILE, jp2k_valid) + + @skip_if_no("jp2ksave") + def test_jp2ksave(self): + self.save_load_buffer("jp2ksave_buffer", "jp2kload_buffer", + self.colour, 80) + + buf = self.colour.jp2ksave_buffer(lossless=True) + im2 = pyvips.Image.new_from_buffer(buf, "") + assert (self.colour == im2).min() == 255 + + # higher Q should mean a bigger buffer + b1 = self.mono.jp2ksave_buffer(Q=10) + b2 = self.mono.jp2ksave_buffer(Q=90) + assert len(b2) > len(b1) + + # disabling chroma subsample should mean a bigger buffer + b1 = self.colour.jp2ksave_buffer(subsample_mode="on") + b2 = self.colour.jp2ksave_buffer(subsample_mode="off") + assert len(b2) > len(b1) + + # enabling lossless should mean a bigger buffer + b1 = self.colour.jp2ksave_buffer(lossless=False) + b2 = self.colour.jp2ksave_buffer(lossless=True) + assert len(b2) > len(b1) + + # 16-bit colour load and save + im = self.colour.colourspace("rgb16") + buf = im.jp2ksave_buffer(lossless=True) + im2 = pyvips.Image.new_from_buffer(buf, "") + assert (im == im2).min() == 255 + + # openjpeg 32-bit load and save doesn't seem to work, comment out + # im = self.colour.colourspace("rgb16").cast("uint") << 14 + # buf = im.jp2ksave_buffer(lossless=True) + # im2 = pyvips.Image.new_from_buffer(buf, "") + # assert (im == im2).min() == 255 + if __name__ == '__main__': pytest.main()