diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 419553a8..f2050ac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,9 +82,7 @@ jobs: if: runner.os == 'macOS' run: | brew install meson ninja fftw fontconfig glib libexif libgsf little-cms2 orc pango - brew install cfitsio libheif libimagequant libjpeg-turbo libmatio librsvg libspng libtiff openexr openjpeg openslide poppler webp - brew tap lovell/cgif-packaging https://github.com/lovell/cgif-packaging.git - brew install --build-bottle lovell/cgif-packaging/cgif + brew install cfitsio libheif libimagequant libjpeg-turbo libmatio librsvg libspng libtiff openexr openjpeg openslide poppler webp cgif - name: Install Clang 13 if: runner.os == 'Linux' && matrix.build.cc == 'clang-13' diff --git a/configure.ac b/configure.ac index 2aa99947..2c4a956e 100644 --- a/configure.ac +++ b/configure.ac @@ -1493,8 +1493,8 @@ AC_ARG_WITH([cgif], AS_HELP_STRING([--without-cgif], [build without cgif (default: test)])) if test x"$quantisation_package" != x"" && test x"$with_cgif" != x"no"; then - PKG_CHECK_MODULES(CGIF, cgif, - [AC_DEFINE(HAVE_CGIF,1,[define if you have cgif installed.]) + PKG_CHECK_MODULES(CGIF, cgif >= 0.2.0, + [AC_DEFINE(HAVE_CGIF,1,[define if you have cgif >= 0.2.0 installed.]) with_cgif=yes PACKAGES_USED="$PACKAGES_USED cgif" ], diff --git a/libvips/foreign/cgifsave.c b/libvips/foreign/cgifsave.c index 4b8ac9d9..59f855a2 100644 --- a/libvips/foreign/cgifsave.c +++ b/libvips/foreign/cgifsave.c @@ -45,6 +45,7 @@ #include #include #include +#include #include @@ -61,6 +62,7 @@ typedef struct _VipsForeignSaveCgif { double dither; int effort; int bitdepth; + double maxerror; VipsTarget *target; /* Derived write params. @@ -100,6 +102,10 @@ typedef struct _VipsForeignSaveCgif { */ VipsPel *index; + /* frame_bytes head (needed for transparency trick). + */ + VipsPel *frame_bytes_head; + /* The frame as written by libcgif. */ CGIF *cgif_context; @@ -138,6 +144,7 @@ vips_foreign_save_cgif_dispose( GObject *gobject ) VIPS_FREE( cgif->palette_rgb ); VIPS_FREE( cgif->index ); + VIPS_FREE( cgif->frame_bytes_head ); VIPS_FREEF( cgif_close, cgif->cgif_context ); @@ -153,6 +160,25 @@ static int vips__cgif_write( void *target, const uint8_t *buffer, (const void *) buffer, (size_t) length ); } +/* Compare pixels in a lossy way (allow a slight colour difference). + In combination with the GIF transparency optimization this leads to + less difference between frames and therefore improves the compression ratio. + */ +static gboolean +cgif_pixels_are_equal(const VipsPel *cur, const VipsPel *bef, double maxerror) +{ + if( bef[3] != 0xFF ) + /* Done. Cannot compare with alpha channel in frame before. + */ + return FALSE; + /* Test Euclidean distance between the two points. + */ + const int dR = cur[0] - bef[0]; + const int dG = cur[1] - bef[1]; + const int dB = cur[2] - bef[2]; + return( sqrt( dR * dR + dG * dG + dB * dB ) <= maxerror ); +} + /* We have a complete frame --- write! */ static int @@ -274,8 +300,6 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif ) if( !cgif->cgif_context ) { cgif->cgif_config.pGlobalPalette = cgif->palette_rgb; cgif->cgif_config.attrFlags = CGIF_ATTR_IS_ANIMATED; - cgif->cgif_config.attrFlags |= - cgif->has_transparency ? CGIF_ATTR_HAS_TRANSPARENCY : 0; cgif->cgif_config.width = frame_rect->width; cgif->cgif_config.height = frame_rect->height; cgif->cgif_config.numGlobalPaletteEntries = cgif->lp->count; @@ -286,12 +310,6 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif ) cgif->cgif_context = cgif_newgif( &cgif->cgif_config ); } - /* Reset global transparency flag. - */ - cgif->cgif_config.attrFlags = - (cgif->cgif_config.attrFlags & ~CGIF_ATTR_HAS_TRANSPARENCY) | - (cgif->has_transparency ? CGIF_ATTR_HAS_TRANSPARENCY : 0); - /* Write frame to cgif. */ memset( &frame_config, 0, sizeof( CGIF_FrameConfig ) ); @@ -303,6 +321,50 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif ) frame_config.genFlags = CGIF_FRAME_GEN_USE_TRANSPARENCY | CGIF_FRAME_GEN_USE_DIFF_WINDOW; + frame_config.attrFlags = 0; + + /* Switch per-frame alpha channel on. + * Index 0 is used for pixels with alpha channel. + */ + if( cgif->has_transparency ) { + frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_ALPHA; + frame_config.transIndex = 0; + } + + /* Do the transparency trick (only possible when no alpha channel present) + */ + if( cgif->frame_bytes_head ) { + VipsPel *cur, *bef; + + cur = frame_bytes; + bef = cgif->frame_bytes_head; + if( !cgif->has_transparency ) { + const uint8_t trans_index = cgif->lp->count; + const double maxerror = cgif->maxerror; + + int i; + + for( i = 0; i < n_pels; i++ ) { + if( cgif_pixels_are_equal( cur, bef, maxerror ) ) + cgif->index[i] = trans_index; + else { + bef[0] = cur[0]; + bef[1] = cur[1]; + bef[2] = cur[2]; + bef[3] = cur[3]; + } + cur += 4; + bef += 4; + } + frame_config.attrFlags |= CGIF_FRAME_ATTR_HAS_SET_TRANS; + frame_config.transIndex = trans_index; + } else { + /* Transparency trick not possible (alpha channel present). + * Update head. + */ + memcpy( bef, cur, 4 * n_pels); + } + } if( cgif->delay && page_index < cgif->delay_length ) @@ -312,13 +374,21 @@ vips_foreign_save_cgif_write_frame( VipsForeignSaveCgif *cgif ) /* Attach a local palette, if we need one. */ if( cgif->cgif_config.attrFlags & CGIF_ATTR_NO_GLOBAL_TABLE ) { - frame_config.attrFlags = CGIF_FRAME_ATTR_USE_LOCAL_TABLE; + frame_config.attrFlags |= CGIF_FRAME_ATTR_USE_LOCAL_TABLE; frame_config.pLocalPalette = cgif->palette_rgb; frame_config.numLocalPaletteEntries = cgif->lp->count; } cgif_addframe( cgif->cgif_context, &frame_config ); + if( !cgif->frame_bytes_head ) { + /* Keep head frame_bytes in memory for transparency trick + * which avoids the size explosion (#2576). + */ + cgif->frame_bytes_head = g_malloc( 4 * n_pels ); + memcpy( cgif->frame_bytes_head, frame_bytes, 4 * n_pels ); + } + return( 0 ); } @@ -529,6 +599,12 @@ vips_foreign_save_cgif_class_init( VipsForeignSaveCgifClass *class ) G_STRUCT_OFFSET( VipsForeignSaveCgif, bitdepth ), 1, 8, 8 ); + VIPS_ARG_DOUBLE( class, "maxerror", 13, + _( "Max. error for lossy transparency setting"), + _( "Maximum pixel error to allow when identifying areas as identical (inter frame)" ), + VIPS_ARGUMENT_OPTIONAL_INPUT, + G_STRUCT_OFFSET( VipsForeignSaveCgif, maxerror ), + 0, 32, 0.0 ); } static void @@ -537,6 +613,7 @@ vips_foreign_save_cgif_init( VipsForeignSaveCgif *gif ) gif->dither = 1.0; gif->effort = 7; gif->bitdepth = 8; + gif->maxerror = 0.0; } typedef struct _VipsForeignSaveCgifTarget { diff --git a/meson.build b/meson.build index 749f978f..5cddf647 100644 --- a/meson.build +++ b/meson.build @@ -234,7 +234,7 @@ endif cgif_dep = disabler() if quantisation_package.found() - cgif_dep = dependency('cgif', required: get_option('cgif')) + cgif_dep = dependency('cgif', version: '>=0.2.0', required: get_option('cgif')) if cgif_dep.found() libvips_deps += cgif_dep cfg_var.set('HAVE_CGIF', '1')