diff --git a/ChangeLog b/ChangeLog index 45886456..abcc175c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,7 @@ - add new_from_image() to Python as well - slight change to cpp new_from_image() to match py/C behaviour - vips_conv(), vips_compass(), vips_convsep() default to FLOAT precision +- add FORCE resize mode to break aspect ratio 23/4/17 started 8.5.5 - doc polishing diff --git a/TODO b/TODO index 989041b5..f2a0a90f 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,6 @@ - vips_compass() needs docs -- cpp bandjoin should use bandjoin_const() where possibel ... currently +- cpp bandjoin should use bandjoin_const() where possible ... currently uses new_from_image - not sure about utf8 error messages on win diff --git a/doc/How-it-opens-files.md b/doc/How-it-opens-files.md index 3e4e4165..cdc68440 100644 --- a/doc/How-it-opens-files.md +++ b/doc/How-it-opens-files.md @@ -102,7 +102,7 @@ example: $ vips shrink fred.png jim.png 10 10 ``` -meaning shrink `fred.png` by a factor of 10 in both axies and write as +meaning shrink `fred.png` by a factor of 10 in both axes and write as `jim.png`. You can imagine this operation running without needing `fred.png` to be diff --git a/libvips/include/vips/resample.h b/libvips/include/vips/resample.h index 0a47e5ba..ecee25ce 100644 --- a/libvips/include/vips/resample.h +++ b/libvips/include/vips/resample.h @@ -51,6 +51,7 @@ typedef enum { VIPS_SIZE_BOTH, VIPS_SIZE_UP, VIPS_SIZE_DOWN, + VIPS_SIZE_FORCE, VIPS_SIZE_LAST } VipsSize; diff --git a/libvips/iofuncs/enumtypes.c b/libvips/iofuncs/enumtypes.c index ae0a18b8..d0b3f445 100644 --- a/libvips/iofuncs/enumtypes.c +++ b/libvips/iofuncs/enumtypes.c @@ -831,6 +831,7 @@ vips_size_get_type( void ) {VIPS_SIZE_BOTH, "VIPS_SIZE_BOTH", "both"}, {VIPS_SIZE_UP, "VIPS_SIZE_UP", "up"}, {VIPS_SIZE_DOWN, "VIPS_SIZE_DOWN", "down"}, + {VIPS_SIZE_FORCE, "VIPS_SIZE_FORCE", "force"}, {VIPS_SIZE_LAST, "VIPS_SIZE_LAST", "last"}, {0, NULL, NULL} }; diff --git a/libvips/resample/resample.c b/libvips/resample/resample.c index 88f42038..57ba1763 100644 --- a/libvips/resample/resample.c +++ b/libvips/resample/resample.c @@ -87,9 +87,10 @@ * @VIPS_SIZE_BOTH: size both up and down * @VIPS_SIZE_UP: only upsize * @VIPS_SIZE_DOWN: only downsize + * @VIPS_SIZE_FORCE: force size, that is, break aspect ratio * - * Controls whether an operation should upsize, downsize, or both up and - * downsize. + * Controls whether an operation should upsize, downsize, both up and + * downsize, or force a size. * * See also: vips_thumbnail(). */ diff --git a/libvips/resample/thumbnail.c b/libvips/resample/thumbnail.c index a81bb99d..00278a2f 100644 --- a/libvips/resample/thumbnail.c +++ b/libvips/resample/thumbnail.c @@ -5,6 +5,8 @@ * - from vipsthumbnail.c * 6/1/17 * - add @size parameter + * 4/5/17 + * - add FORCE */ /* @@ -135,21 +137,27 @@ vips_thumbnail_finalize( GObject *gobject ) /* Calculate the shrink factor, taking into account auto-rotate, the fit mode, * and so on. + * + * The hshrink/vshrink are the amount to shrink the input image axes by in + * order for the output axes (ie. after rotation) to match the required + * thumbnail->width, thumbnail->height and fit mode. */ -static double +static void vips_thumbnail_calculate_shrink( VipsThumbnail *thumbnail, - int input_width, int input_height ) + int input_width, int input_height, double *hshrink, double *vshrink ) { + /* If we will be rotating, swap the target width and height. + */ gboolean rotate = - thumbnail->angle == VIPS_ANGLE_D90 || - thumbnail->angle == VIPS_ANGLE_D270; - int width = thumbnail->auto_rotate && rotate ? - input_height : input_width; - int height = thumbnail->auto_rotate && rotate ? - input_width : input_height; + (thumbnail->angle == VIPS_ANGLE_D90 || + thumbnail->angle == VIPS_ANGLE_D270) && + thumbnail->auto_rotate; + int target_width = rotate ? + thumbnail->height : thumbnail->width; + int target_height = rotate ? + thumbnail->width : thumbnail->height; VipsDirection direction; - double shrink; /* Calculate the horizontal and vertical shrink we'd need to fit the * image to the bounding box, and pick the biggest. @@ -157,33 +165,53 @@ vips_thumbnail_calculate_shrink( VipsThumbnail *thumbnail, * In crop mode, we aim to fill the bounding box, so we must use the * smaller axis. */ - double horizontal = (double) width / thumbnail->width; - double vertical = (double) height / thumbnail->height; + *hshrink = (double) input_width / target_width; + *vshrink = (double) input_height / target_height; if( thumbnail->crop != VIPS_INTERESTING_NONE ) { - if( horizontal < vertical ) + if( *hshrink < *vshrink ) direction = VIPS_DIRECTION_HORIZONTAL; else direction = VIPS_DIRECTION_VERTICAL; } else { - if( horizontal < vertical ) + if( *hshrink < *vshrink ) direction = VIPS_DIRECTION_VERTICAL; else direction = VIPS_DIRECTION_HORIZONTAL; } - shrink = direction == VIPS_DIRECTION_HORIZONTAL ? - horizontal : vertical; + if( thumbnail->size != VIPS_SIZE_FORCE ) { + if( direction == VIPS_DIRECTION_HORIZONTAL ) + *vshrink = *hshrink; + else + *hshrink = *vshrink; + } - /* Restrict to only upsize, only downsize, or both. - */ - if( thumbnail->size == VIPS_SIZE_UP ) - shrink = VIPS_MIN( 1, shrink ); - if( thumbnail->size == VIPS_SIZE_DOWN ) - shrink = VIPS_MAX( 1, shrink ); + if( thumbnail->size == VIPS_SIZE_UP ) { + *hshrink = VIPS_MIN( 1, *hshrink ); + *vshrink = VIPS_MIN( 1, *vshrink ); + } + else if( thumbnail->size == VIPS_SIZE_DOWN ) { + *hshrink = VIPS_MAX( 1, *hshrink ); + *vshrink = VIPS_MAX( 1, *vshrink ); + } +} - return( shrink ); +/* Just the common part of the shrink: the bit by which both axes must be + * shrunk. + */ +static double +vips_thumbnail_calculate_common_shrink( VipsThumbnail *thumbnail, + int width, int height ) +{ + double hshrink; + double vshrink; + + vips_thumbnail_calculate_shrink( thumbnail, width, height, + &hshrink, &vshrink ); + + return( VIPS_MIN( hshrink, vshrink ) ); } /* Find the best jpeg preload shrink. @@ -191,8 +219,8 @@ vips_thumbnail_calculate_shrink( VipsThumbnail *thumbnail, static int vips_thumbnail_find_jpegshrink( VipsThumbnail *thumbnail, int width, int height ) { - double shrink = - vips_thumbnail_calculate_shrink( thumbnail, width, height ); + double shrink = vips_thumbnail_calculate_common_shrink( thumbnail, + width, height ); /* We can't use pre-shrunk images in linear mode. libjpeg shrinks in Y * (of YCbCR), not linear space. @@ -228,7 +256,7 @@ vips_thumbnail_open( VipsThumbnail *thumbnail ) VipsThumbnailClass *class = VIPS_THUMBNAIL_GET_CLASS( thumbnail ); VipsImage *im; - int shrink; + double shrink; double scale; if( class->get_info( thumbnail ) ) @@ -237,24 +265,28 @@ vips_thumbnail_open( VipsThumbnail *thumbnail ) g_info( "input size is %d x %d", thumbnail->input_width, thumbnail->input_height ); - shrink = 1; + shrink = 1.0; scale = 1.0; if( vips_isprefix( "VipsForeignLoadJpeg", thumbnail->loader ) ) { shrink = vips_thumbnail_find_jpegshrink( thumbnail, thumbnail->input_width, thumbnail->input_height ); - g_info( "loading jpeg with factor %d pre-shrink", shrink ); + + g_info( "loading jpeg with factor %g pre-shrink", shrink ); } else if( vips_isprefix( "VipsForeignLoadPdf", thumbnail->loader ) || vips_isprefix( "VipsForeignLoadSvg", thumbnail->loader ) ) { - scale = 1.0 / vips_thumbnail_calculate_shrink( thumbnail, - thumbnail->input_width, thumbnail->input_height ); + shrink = vips_thumbnail_calculate_common_shrink( thumbnail, + thumbnail->input_width, thumbnail->input_height ); + scale = 1.0 / shrink; + g_info( "loading PDF/SVG with factor %g pre-scale", scale ); } else if( vips_isprefix( "VipsForeignLoadWebp", thumbnail->loader ) ) { - shrink = vips_thumbnail_calculate_shrink( thumbnail, - thumbnail->input_width, thumbnail->input_height ); - g_info( "loading webp with factor %d pre-shrink", shrink ); + shrink = vips_thumbnail_calculate_common_shrink( thumbnail, + thumbnail->input_width, thumbnail->input_height ); + + g_info( "loading webp with factor %g pre-shrink", shrink ); } if( !(im = class->open( thumbnail, shrink, scale )) ) @@ -272,7 +304,8 @@ vips_thumbnail_build( VipsObject *object ) VIPS_INTERPRETATION_scRGB : VIPS_INTERPRETATION_sRGB; VipsImage *in; - double shrink; + double hshrink; + double vshrink; /* TRUE if we've done the import of an ICC transform and still need to * export. @@ -373,12 +406,13 @@ vips_thumbnail_build( VipsObject *object ) in = t[3]; } - shrink = vips_thumbnail_calculate_shrink( thumbnail, - in->Xsize, in->Ysize ); + vips_thumbnail_calculate_shrink( thumbnail, + in->Xsize, in->Ysize, &hshrink, &vshrink ); /* Use centre convention to better match imagemagick. */ - if( vips_resize( in, &t[4], 1.0 / shrink, + if( vips_resize( in, &t[4], 1.0 / hshrink, + "vscale", 1.0 / vshrink, "centre", TRUE, NULL ) ) return( -1 ); @@ -639,8 +673,6 @@ vips_thumbnail_file_open( VipsThumbnail *thumbnail, int shrink, double scale ) { VipsThumbnailFile *file = (VipsThumbnailFile *) thumbnail; - /* We can't use UNBUFERRED safely on very-many-core systems. - */ if( shrink != 1 ) return( vips_image_new_from_file( file->filename, "access", VIPS_ACCESS_SEQUENTIAL, @@ -697,7 +729,7 @@ vips_thumbnail_file_init( VipsThumbnailFile *file ) * Optional arguments: * * * @height: %gint, target height in pixels - * * @size: #VipsSize, upsize, downsize or both + * * @size: #VipsSize, upsize, downsize, both or force * * @auto_rotate: %gboolean, rotate upright using orientation tag * * @crop: #VipsInteresting, shrink and crop to fill target * * @linear: %gboolean, perform shrink in linear light @@ -719,19 +751,22 @@ vips_thumbnail_file_init( VipsThumbnailFile *file ) * @height rectangle, with any excess cropped away. See vips_smartcrop() for * details on the cropping strategy. * - * Normally the operation will upsize or downsize as required. If @size is set + * Normally the operation will upsize or downsize as required to fit the image + * inside or outside the target size. If @size is set * to #VIPS_SIZE_UP, the operation will only upsize and will just * copy if asked to downsize. * If @size is set * to #VIPS_SIZE_DOWN, the operation will only downsize and will just * copy if asked to upsize. + * If @size is #VIPS_SIZE_FORCE, the image aspect ratio will be broken and the + * image will be forced to fit the target. * * Normally any orientation tags on the input image (such as EXIF tags) are * interpreted to rotate the image upright. If you set @auto_rotate to %FALSE, * these tags will not be interpreted. * * Shrinking is normally done in sRGB colourspace. Set @linear to shrink in - * linear light colourspace instead --- this can give better results, but can + * linear light colourspace instead. This can give better results, but can * also be far slower, since tricks like JPEG shrink-on-load cannot be used in * linear space. * @@ -866,7 +901,7 @@ vips_thumbnail_buffer_init( VipsThumbnailBuffer *buffer ) * Optional arguments: * * * @height: %gint, target height in pixels - * * @size: #VipsSize, upsize, downsize or both + * * @size: #VipsSize, upsize, downsize, both or force * * @auto_rotate: %gboolean, rotate upright using orientation tag * * @crop: #VipsInteresting, shrink and crop to fill target * * @linear: %gboolean, perform shrink in linear light diff --git a/man/vipsthumbnail.1 b/man/vipsthumbnail.1 index e83046b5..a57873a5 100644 --- a/man/vipsthumbnail.1 +++ b/man/vipsthumbnail.1 @@ -94,7 +94,7 @@ option. .TP .B -c, --crop Crop the output image down. The image is shrunk so as to completely fill the -bounding box in both axies, then any excess is cropped off. +bounding box in both axes, then any excess is cropped off. .TP .B -d, --delete diff --git a/test/images/Landscape_6.jpg b/test/images/Landscape_6.jpg new file mode 100644 index 00000000..c692f195 Binary files /dev/null and b/test/images/Landscape_6.jpg differ diff --git a/test/test_resample.py b/test/test_resample.py index da8e209f..a152e979 100755 --- a/test/test_resample.py +++ b/test/test_resample.py @@ -107,6 +107,7 @@ class TestResample(unittest.TestCase): def setUp(self): self.jpeg_file = "images/йцук.jpg" + self.rotated_jpeg_file = "images/Landscape_6.jpg" def test_affine(self): im = Vips.Image.new_from_file(self.jpeg_file) @@ -216,12 +217,38 @@ class TestResample(unittest.TestCase): self.assertNotEqual(im.width, 300) self.assertEqual(im.height, 100) - # with @crop, should fit both width and height - im = Vips.Image.thumbnail(self.jpeg_file, 100, - height = 300, crop = True) + # force should fit width and height ... although this jpg has an + # orientation tag, we ignore it unless autorot is on + im = Vips.Image.thumbnail(self.rotated_jpeg_file, 100, height = 300, + size = "force") self.assertEqual(im.width, 100) self.assertEqual(im.height, 300) + # with force + autorot, we spin the image, but the output size should + # not change + im = Vips.Image.thumbnail(self.rotated_jpeg_file, 100, height = 300, + size = "force", auto_rotate = True) + self.assertEqual(im.width, 100) + self.assertEqual(im.height, 300) + + # with @crop, should fit both width and height + im = Vips.Image.thumbnail(self.jpeg_file, 100, + height = 300, crop = "centre") + self.assertEqual(im.width, 100) + self.assertEqual(im.height, 300) + + # with size up, should not downsize + im = Vips.Image.thumbnail(self.jpeg_file, 100, size = "up") + self.assertEqual(im.width, im_orig.width) + im = Vips.Image.thumbnail(self.jpeg_file, 10000, size = "up") + self.assertEqual(im.width, 10000) + + # with size down, should not upsize + im = Vips.Image.thumbnail(self.jpeg_file, 100, size = "down") + self.assertEqual(im.width, 100) + im = Vips.Image.thumbnail(self.jpeg_file, 10000, size = "down") + self.assertEqual(im.width, im_orig.width) + im1 = Vips.Image.thumbnail(self.jpeg_file, 100) with open(self.jpeg_file, 'rb') as f: buf = f.read() diff --git a/tools/vipsthumbnail.c b/tools/vipsthumbnail.c index 5c5f5f00..b41fc47e 100644 --- a/tools/vipsthumbnail.c +++ b/tools/vipsthumbnail.c @@ -90,6 +90,8 @@ * 6/1/17 * - fancy geometry strings * - support VipSize restrictions + * 4/5/17 + * - add ! geo modifier */ #ifdef HAVE_CONFIG_H @@ -333,7 +335,7 @@ thumbnail_parse_geometry( const char *geometry ) p++; } - /* Get the final < or >. + /* Get the final <>! */ while( isspace( *p ) ) p++; @@ -341,6 +343,8 @@ thumbnail_parse_geometry( const char *geometry ) size_restriction = VIPS_SIZE_UP; else if( *p == '>' ) size_restriction = VIPS_SIZE_DOWN; + else if( *p == '!' ) + size_restriction = VIPS_SIZE_FORCE; else if( *p != '\0' || (thumbnail_width == VIPS_MAX_COORD && thumbnail_height == VIPS_MAX_COORD) ) { @@ -348,18 +352,30 @@ thumbnail_parse_geometry( const char *geometry ) return( -1 ); } - /* If --crop is set, both width and height must be specified, - * since we'll need a complete bounding box to fill. + /* If force is set and one of width or height isn't set, copy from the + * one that is. */ - if( (crop_image || smartcrop_image) && - (thumbnail_width == VIPS_MAX_COORD || - thumbnail_height == VIPS_MAX_COORD) ) { - vips_error( "thumbnail", - "both width and height must be given if " - "crop is enabled" ); - return( -1 ); + if( size_restriction == VIPS_SIZE_FORCE ) { + if( thumbnail_width == VIPS_MAX_COORD ) + thumbnail_width = thumbnail_height; + if( thumbnail_height == VIPS_MAX_COORD ) + thumbnail_height = thumbnail_width; } + /* If --crop is set or force is set, both width and height must be + * specified, since we'll need a complete bounding box to fill. + */ + if( crop_image || + smartcrop_image || + size_restriction == VIPS_SIZE_FORCE ) + if( thumbnail_width == VIPS_MAX_COORD || + thumbnail_height == VIPS_MAX_COORD ) { + vips_error( "thumbnail", + "both width and height must be given if " + "crop is enabled" ); + return( -1 ); + } + return( 0 ); }