diff --git a/libvips/foreign/jp2kload.c b/libvips/foreign/jp2kload.c index 53a31874..b28f7604 100644 --- a/libvips/foreign/jp2kload.c +++ b/libvips/foreign/jp2kload.c @@ -668,7 +668,9 @@ vips_foreign_load_jp2k_pack( VipsForeignLoadJp2k *jp2k, } } -/* ycc->rgb coversion adapted from openjpeg src/bin/common/color.c. +/* 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; \ @@ -816,7 +818,7 @@ vips_foreign_load_jp2k_generate( VipsRegion *out, hit.top - tile.top + z, hit.width ); - if( jp2k->ycc_to_rgb ) + //if( jp2k->ycc_to_rgb ) vips_foreign_load_jp2k_ycc_to_rgb( jp2k, q, hit.width ); } diff --git a/libvips/foreign/jp2ksave.c b/libvips/foreign/jp2ksave.c index 15b659b7..5bc9a8ba 100644 --- a/libvips/foreign/jp2ksave.c +++ b/libvips/foreign/jp2ksave.c @@ -76,6 +76,10 @@ typedef struct _VipsForeignSaveJp2k { */ int Q; + /* Chroma subsample mode. + */ + VipsForeignSubsample subsample_mode; + /* Encoder state. */ opj_stream_t *stream; @@ -84,11 +88,23 @@ typedef struct _VipsForeignSaveJp2k { opj_image_cmptparm_t comps[MAX_BANDS]; opj_image_t *image; - /* The line of tiles we are building, and a contiguous buffer we - * repack to for output. + /* 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; @@ -109,6 +125,7 @@ vips_foreign_save_jp2k_dispose( GObject *gobject ) VIPS_UNREF( jp2k->strip ); VIPS_FREE( jp2k->tile_buffer ); + VIPS_FREE( jp2k->accumulate ); G_OBJECT_CLASS( vips_foreign_save_jp2k_parent_class )-> dispose( gobject ); @@ -188,6 +205,176 @@ vips_foreign_save_jp2k_attach_handlers( VipsForeignSaveJp2k *jp2k, 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; \ @@ -246,11 +433,34 @@ vips_foreign_save_jp2k_unpack( VipsForeignSaveJp2k *jp2k, VipsRect *tile ) } } +static size_t +vips_foreign_save_jp2k_sizeof_tile( VipsForeignSaveJp2k *jp2k, VipsRect *tile ) +{ + 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 * (comp->bpp / 8); + } + + return( size ); +} + static int vips_foreign_save_jp2k_write_tiles( VipsForeignSaveJp2k *jp2k ) { VipsForeignSave *save = (VipsForeignSave *) jp2k; - size_t sizeof_pel = VIPS_IMAGE_SIZEOF_PEL( save->ready ); int tiles_across = VIPS_ROUND_UP( save->ready->Xsize, jp2k->tile_width ) / jp2k->tile_width; @@ -268,13 +478,20 @@ vips_foreign_save_jp2k_write_tiles( VipsForeignSaveJp2k *jp2k ) tile.height = jp2k->tile_height; vips_rect_intersectrect( &tile, &jp2k->strip->valid, &tile ); - vips_foreign_save_jp2k_unpack( jp2k, &tile ); + if( jp2k->save_as_ycc ) + vips_foreign_save_jp2k_rgb_to_ycc( jp2k, &tile ); - sizeof_tile = sizeof_pel * tile.width * tile.height; + 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, - jp2k->tile_buffer, sizeof_tile, jp2k->stream ) ) + (VipsPel *) jp2k->tile_buffer, sizeof_tile, + jp2k->stream ) ) return( -1 ); } @@ -349,6 +566,7 @@ vips_foreign_save_jp2k_build( VipsObject *object ) int expected_bands; int i; size_t sizeof_tile; + size_t sizeof_line; VipsRect strip_position; if( VIPS_OBJECT_CLASS( vips_foreign_save_jp2k_parent_class )-> @@ -361,6 +579,30 @@ vips_foreign_save_jp2k_build( VipsObject *object ) return( -1 ); } + switch( jp2k->subsample_mode ) { + case VIPS_FOREIGN_SUBSAMPLE_AUTO: + jp2k->downsample = + 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; + } + /* A JPEG2000 codestream. */ jp2k->codec = opj_create_compress( OPJ_CODEC_J2K ); @@ -386,21 +628,42 @@ vips_foreign_save_jp2k_build( VipsObject *object ) 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 = jp2k->comps[i].bpp = + 8 * vips_format_sizeof( save->ready->BandFmt ); + 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 ? 1 : 0; + + if( jp2k->downsample ) { + jp2k->save_as_ycc = TRUE; + + /* MCT does not work with subsample. + */ + jp2k->parameters.tcp_mct = FALSE; + } + /* Lossy mode. */ if( !jp2k->lossless ) { jp2k->parameters.irreversible = TRUE; - /* Allowed distortion + /* 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; - - /* Enable mct (YCC mode?). - */ - jp2k->parameters.tcp_mct = save->ready->Bands == 3 ? 1 : 0; } /* CIELAB etc. do not seem to be well documented. @@ -414,9 +677,9 @@ vips_foreign_save_jp2k_build( VipsObject *object ) case VIPS_INTERPRETATION_sRGB: case VIPS_INTERPRETATION_RGB16: - color_space = OPJ_CLRSPC_SRGB; + color_space = jp2k->save_as_ycc ? + OPJ_CLRSPC_SYCC : OPJ_CLRSPC_SRGB; expected_bands = 3; - break; case VIPS_INTERPRETATION_CMYK: @@ -430,24 +693,12 @@ vips_foreign_save_jp2k_build( VipsObject *object ) break; } - for( i = 0; i < save->ready->Bands; i++ ) { - jp2k->comps[i].dx = 1; - jp2k->comps[i].dy = 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 = jp2k->comps[i].bpp = - 8 * vips_format_sizeof( save->ready->BandFmt ); - jp2k->comps[i].sgnd = - !vips_band_format_isuint( save->ready->BandFmt ); - } 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. + /* Tag alpha channels. */ for( i = 0; i < save->ready->Bands; i++ ) jp2k->image->comps[i].alpha = i >= expected_bands; @@ -476,6 +727,12 @@ vips_foreign_save_jp2k_build( VipsObject *object ) 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 ); @@ -544,6 +801,14 @@ vips_foreign_save_jp2k_class_init( VipsForeignSaveJp2kClass *class ) 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" ), @@ -562,6 +827,8 @@ vips_foreign_save_jp2k_init( VipsForeignSaveJp2k *jp2k ) /* 45 gives about the same filesize as default regular jpg. */ jp2k->Q = 45; + + jp2k->subsample_mode = VIPS_FOREIGN_SUBSAMPLE_AUTO; } typedef struct _VipsForeignSaveJp2kFile { @@ -755,6 +1022,7 @@ vips_foreign_save_jp2k_target_init( VipsForeignSaveJp2kTarget *target ) * * @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 @@ -768,6 +1036,9 @@ vips_foreign_save_jp2k_target_init( VipsForeignSaveJp2kTarget *target ) * * 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(). @@ -800,6 +1071,7 @@ vips_jp2ksave( VipsImage *in, const char *filename, ... ) * * @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. * @@ -847,6 +1119,7 @@ vips_jp2ksave_buffer( VipsImage *in, void **buf, size_t *len, ... ) * * @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. *