add "premultiply" param to tiffsave

Some programs, like indesign, only work with premultiplied alpha in TIFF. To
make TIFFs which are compatible with these programs, we'll need an extra
TIFF save flag (perhaps premultiply?) to premultiply alpha and save as
EXTRASAMPLE_ASSOCALPHA.

see https://github.com/libvips/libvips/issues/2192
This commit is contained in:
John Cupitt 2021-05-01 20:08:06 +01:00
parent 33812d79ad
commit 03f76b73b4
5 changed files with 94 additions and 26 deletions

View File

@ -28,6 +28,7 @@
- add "rgba" flag to vips_text() to enable full colour text rendering
- move openslide, libheif, poppler and magick to loadable modules [kleisauke]
- better detection of invalid ICC profiles, better fallback paths
- add "premultiply" flag to tiffsave
22/12/20 start 8.10.6
- don't seek on bad file descriptors [kleisauke]

View File

@ -64,7 +64,8 @@ int vips__tiff_write( VipsImage *in, const char *filename,
int level,
gboolean lossless,
VipsForeignDzDepth depth,
gboolean subifd );
gboolean subifd,
gboolean premultiply );
int vips__tiff_write_buf( VipsImage *in,
void **obuf, size_t *olen,
@ -83,7 +84,8 @@ int vips__tiff_write_buf( VipsImage *in,
int level,
gboolean lossless,
VipsForeignDzDepth depth,
gboolean subifd );
gboolean subifd,
gboolean premultiply );
gboolean vips__istiff_source( VipsSource *source );
gboolean vips__istifftiled_source( VipsSource *source );

View File

@ -26,6 +26,8 @@
* 8/6/20
* - add bitdepth support for 2 and 4 bit greyscale images
* - deprecate "squash"
* 1/5/21
* - add "premultiply" flag
*/
/*
@ -105,6 +107,7 @@ typedef struct _VipsForeignSaveTiff {
gboolean lossless;
VipsForeignDzDepth depth;
gboolean subifd;
gboolean premultiply;
} VipsForeignSaveTiff;
@ -341,13 +344,20 @@ vips_foreign_save_tiff_class_init( VipsForeignSaveTiffClass *class )
G_STRUCT_OFFSET( VipsForeignSaveTiff, depth ),
VIPS_TYPE_FOREIGN_DZ_DEPTH, VIPS_FOREIGN_DZ_DEPTH_ONETILE );
VIPS_ARG_BOOL( class, "subifd", 24,
VIPS_ARG_BOOL( class, "subifd", 26,
_( "Sub-IFD" ),
_( "Save pyr layers as sub-IFDs" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveTiff, subifd ),
FALSE );
VIPS_ARG_BOOL( class, "premultiply", 27,
_( "Premultiply" ),
_( "Save with premultiplied alpha" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsForeignSaveTiff, premultiply ),
FALSE );
VIPS_ARG_BOOL( class, "rgbjpeg", 20,
_( "RGB JPEG" ),
_( "Output RGB JPEG rather than YCbCr" ),
@ -427,7 +437,8 @@ vips_foreign_save_tiff_file_build( VipsObject *object )
tiff->level,
tiff->lossless,
tiff->depth,
tiff->subifd ) )
tiff->subifd,
tiff->premultiply ) )
return( -1 );
return( 0 );
@ -500,7 +511,8 @@ vips_foreign_save_tiff_buffer_build( VipsObject *object )
tiff->level,
tiff->lossless,
tiff->depth,
tiff->subifd ) )
tiff->subifd,
tiff->premultiply ) )
return( -1 );
blob = vips_blob_new( (VipsCallbackFn) vips_area_free_cb, obuf, olen );
@ -567,6 +579,7 @@ vips_foreign_save_tiff_buffer_init( VipsForeignSaveTiffBuffer *buffer )
* * @lossless: %gboolean, WebP losssless mode
* * @depth: #VipsForeignDzDepth how deep to make the pyramid
* * @subifd: %gboolean write pyr layers as sub-ifds
* * @premultiply: %gboolean write premultiplied alpha
*
* Write a VIPS image to a file as TIFF.
*
@ -658,6 +671,9 @@ vips_foreign_save_tiff_buffer_init( VipsForeignSaveTiffBuffer *buffer )
* Set @subifd to save pyramid layers as sub-directories of the main image.
* Setting this option can improve compatibility with formats like OME.
*
* Set @premultiply tio save with premultiplied alpha. Some programs, such as
* InDesign, will only work with premultiplied alpha.
*
* See also: vips_tiffload(), vips_image_write_to_file().
*
* Returns: 0 on success, -1 on error.
@ -704,6 +720,7 @@ vips_tiffsave( VipsImage *in, const char *filename, ... )
* * @lossless: %gboolean, WebP losssless mode
* * @depth: #VipsForeignDzDepth how deep to make the pyramid
* * @subifd: %gboolean write pyr layers as sub-ifds
* * @premultiply: %gboolean write premultiplied alpha
*
* As vips_tiffsave(), but save to a memory buffer.
*

View File

@ -364,6 +364,7 @@ struct _Wtiff {
gboolean lossless; /* lossless mode */
VipsForeignDzDepth depth; /* Pyr depth */
gboolean subifd; /* Write pyr layers into subifds */
gboolean premultiply; /* Premultiply alpha */
/* True if we've detected a toilet-roll image, plus the page height,
* which has been checked to be a factor of im->Ysize. page_number
@ -818,9 +819,14 @@ wtiff_write_header( Wtiff *wtiff, Layer *layer )
/* EXTRASAMPLE_UNASSALPHA means generic extra
* alpha-like channels. ASSOCALPHA means pre-multipled
* alpha only.
*
* Make the first channel the premultiplied alpha, if
* we are premultiplying.
*/
for( i = 0; i < alpha_bands; i++ )
v[i] = EXTRASAMPLE_UNASSALPHA;
v[i] = i == 0 && wtiff->premultiply ?
EXTRASAMPLE_ASSOCALPHA :
EXTRASAMPLE_UNASSALPHA;
TIFFSetField( tif,
TIFFTAG_EXTRASAMPLES, alpha_bands, v );
}
@ -1083,24 +1089,56 @@ get_resunit( VipsForeignTiffResunit resunit )
static int
ready_to_write( Wtiff *wtiff )
{
if( vips_check_coding_known( "vips2tiff", wtiff->input ) )
VipsImage *input;
VipsImage *x;
input = wtiff->input;
g_object_ref( input );
if( vips_check_coding_known( "vips2tiff", input ) ) {
VIPS_UNREF( input );
return( -1 );
}
/* Premultiply any alpha, if necessary.
*/
if( wtiff->premultiply &&
vips_image_hasalpha( input ) ) {
VipsBandFormat start_format = input->BandFmt;
if( vips_premultiply( input, &x, NULL ) ) {
VIPS_UNREF( input );
return( -1 );
}
VIPS_UNREF( input );
input = x;
/* Premultiply always makes a float -- cast back again.
*/
if( vips_cast( input, &x, start_format, NULL ) ) {
VIPS_UNREF( input );
return( -1 );
}
VIPS_UNREF( input );
input = x;
}
/* "squash" float LAB down to LABQ.
*/
if( wtiff->bitdepth &&
wtiff->input->Bands == 3 &&
wtiff->input->BandFmt == VIPS_FORMAT_FLOAT &&
wtiff->input->Type == VIPS_INTERPRETATION_LAB ) {
if( vips_Lab2LabQ( wtiff->input, &wtiff->ready, NULL ) )
input->Bands == 3 &&
input->BandFmt == VIPS_FORMAT_FLOAT &&
input->Type == VIPS_INTERPRETATION_LAB ) {
if( vips_Lab2LabQ( input, &x, NULL ) ) {
VIPS_UNREF( input );
return( -1 );
wtiff->bitdepth = 0;
}
else {
wtiff->ready = wtiff->input;
g_object_ref( wtiff->ready );
}
VIPS_UNREF( input );
input = x;
}
wtiff->ready = input;
return( 0 );
}
@ -1122,7 +1160,8 @@ wtiff_new( VipsImage *input, const char *filename,
int level,
gboolean lossless,
VipsForeignDzDepth depth,
gboolean subifd )
gboolean subifd,
gboolean premultiply )
{
Wtiff *wtiff;
@ -1155,6 +1194,7 @@ wtiff_new( VipsImage *input, const char *filename,
wtiff->lossless = lossless;
wtiff->depth = depth;
wtiff->subifd = subifd;
wtiff->premultiply = premultiply;
wtiff->toilet_roll = FALSE;
wtiff->page_height = vips_image_get_page_height( input );
wtiff->page_number = 0;
@ -2176,7 +2216,8 @@ vips__tiff_write( VipsImage *input, const char *filename,
int level,
gboolean lossless,
VipsForeignDzDepth depth,
gboolean subifd )
gboolean subifd,
gboolean premultiply )
{
Wtiff *wtiff;
@ -2191,7 +2232,7 @@ vips__tiff_write( VipsImage *input, const char *filename,
tile, tile_width, tile_height, pyramid, bitdepth,
miniswhite, resunit, xres, yres, bigtiff, rgbjpeg,
properties, strip, region_shrink, level, lossless, depth,
subifd )) )
subifd, premultiply )) )
return( -1 );
if( wtiff_write_image( wtiff ) ) {
@ -2222,7 +2263,8 @@ vips__tiff_write_buf( VipsImage *input,
int level,
gboolean lossless,
VipsForeignDzDepth depth,
gboolean subifd )
gboolean subifd,
gboolean premultiply )
{
Wtiff *wtiff;
@ -2233,7 +2275,7 @@ vips__tiff_write_buf( VipsImage *input,
tile, tile_width, tile_height, pyramid, bitdepth,
miniswhite, resunit, xres, yres, bigtiff, rgbjpeg,
properties, strip, region_shrink, level, lossless, depth,
subifd )) )
subifd, premultiply )) )
return( -1 );
wtiff->obuf = obuf;

View File

@ -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, JP2K_FILE
WEBP_ANIMATED_FILE, JP2K_FILE, RGBA_FILE
class TestForeign:
tempdir = None
@ -27,6 +27,7 @@ class TestForeign:
cls.tempdir = tempfile.mkdtemp()
cls.colour = pyvips.Image.jpegload(JPEG_FILE)
cls.rgba = pyvips.Image.new_from_file(RGBA_FILE)
cls.mono = cls.colour.extract_band(1).copy()
# we remove the ICC profile: the RGB one will no longer be appropriate
cls.mono.remove("icc-profile-data")
@ -387,15 +388,14 @@ class TestForeign:
self.save_load("%s.tif", self.mono)
self.save_load("%s.tif", self.colour)
self.save_load("%s.tif", self.cmyk)
self.save_load("%s.tif", self.rgba)
self.save_load("%s.tif", self.onebit)
self.save_load_file(".tif", "[bitdepth=1]", self.onebit)
self.save_load_file(".tif", "[miniswhite]", self.onebit)
self.save_load_file(".tif", "[bitdepth=1,miniswhite]", self.onebit)
self.save_load_file(".tif",
"[profile={0}]".format(SRGB_FILE),
self.colour)
self.save_load_file(".tif", f"[profile={SRGB_FILE}]", self.colour)
self.save_load_file(".tif", "[tile]", self.colour)
self.save_load_file(".tif", "[tile,pyramid]", self.colour)
self.save_load_file(".tif", "[tile,pyramid,subifd]", self.colour)
@ -510,6 +510,12 @@ class TestForeign:
buf2 = f.read()
assert len(buf) == len(buf2)
filename = temp_filename(self.tempdir, '.tif')
self.rgba.write_to_file(filename, premultiply=True)
a = pyvips.Image.new_from_file(filename)
b = self.rgba.premultiply().cast("uchar").unpremultiply().cast("uchar")
assert a.avg() == b.avg()
a = pyvips.Image.new_from_buffer(buf, "", page=2)
b = pyvips.Image.new_from_buffer(buf2, "", page=2)
assert a.width == b.width