diff --git a/README.md b/README.md index 45887584..ab4d04cb 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ If you're using `gopkg.in`, you can still rely in the `v0` without worrying abou - Format conversion (with additional quality/compression settings) - EXIF metadata (size, alpha channel, profile, orientation...) - Trim (libvips 8.6+) +- Composite (libvips 8.6+) ## Prerequisites diff --git a/image.go b/image.go index 093d5a5a..28c96959 100644 --- a/image.go +++ b/image.go @@ -185,6 +185,13 @@ func (i *Image) Trim() ([]byte, error) { return i.Process(options) } +// Composite blends two images together working from the bottom upwards, with +// the blend mode at each step being set by the corresponding BlendMode +func (i *Image) Composite(l *Image, mode BlendMode) ([]byte, error) { + options := Options{Composite: true, BlendMode: mode, CompositeLayers: []*Image{l}} + return i.Process(options) +} + // Process processes the image based on the given transformation options, // talking with libvips bindings accordingly and returning the resultant // image buffer. diff --git a/image_test.go b/image_test.go index 5af0431d..f6d35fa0 100644 --- a/image_test.go +++ b/image_test.go @@ -520,6 +520,19 @@ func TestImageTrimParameters(t *testing.T) { Write("testdata/parameter_trim.png", buf) } +func TestImageComposite(t *testing.T) { + if !(VipsMajorVersion >= 8 && VipsMinorVersion >= 6) { + t.Skipf("Skipping this test, libvips doesn't meet version requirement %s >= 8.6", VipsVersion) + } + + overlay := initImage("transparent.png") + buf, err := initImage("test.jpg").Composite(overlay, BlendModeAdd) + if err != nil { + t.Errorf("Cannot process the image: %#v", err) + } + Write("testdata/test_composite_out.jpg", buf) +} + func TestImageLength(t *testing.T) { i := initImage("test.jpg") diff --git a/metadata.go b/metadata.go index 77eac8cd..667885fb 100644 --- a/metadata.go +++ b/metadata.go @@ -2,7 +2,7 @@ package bimg /* #cgo pkg-config: vips -#include "vips/vips.h" +#include "vips.h" */ import "C" diff --git a/options.go b/options.go index 17a1cb4c..ebc3f402 100644 --- a/options.go +++ b/options.go @@ -2,7 +2,7 @@ package bimg /* #cgo pkg-config: vips -#include "vips/vips.h" +#include "vips.h" */ import "C" @@ -139,6 +139,63 @@ const ( ExtendLast Extend = C.VIPS_EXTEND_LAST ) +// BlendMode represents the blend mode used when compositing. +// See: https://jcupitt.github.io/libvips/API/current/libvips-conversion.html#VipsBlendMode +type BlendMode int + +const ( + // BlendModeClear where the second object is drawn, the first is removed + BlendModeClear BlendMode = C.VIPS_BLEND_MODE_CLEAR + // BlendModeSource the second object is drawn as if nothing were below + BlendModeSource BlendMode = C.VIPS_BLEND_MODE_SOURCE + // BlendModeOver the image shows what you would expect if you held two semi-transparent slides on top of each other + BlendModeOver BlendMode = C.VIPS_BLEND_MODE_OVER + // BlendModeIn the first object is removed completely, the second is only drawn where the first was + BlendModeIn BlendMode = C.VIPS_BLEND_MODE_IN + // BlendModeOut the second is drawn only where the first isn't + BlendModeOut BlendMode = C.VIPS_BLEND_MODE_OUT + // BlendModeAtop this leaves the first object mostly intact, but mixes both objects in the overlapping area + BlendModeAtop BlendMode = C.VIPS_BLEND_MODE_ATOP + // BlendModeDest leaves the first object untouched, the second is discarded completely + BlendModeDest BlendMode = C.VIPS_BLEND_MODE_DEST + // BlendModeDestOver like OVER, but swaps the arguments + BlendModeDestOver BlendMode = C.VIPS_BLEND_MODE_DEST_OVER + // BlendModeDestIn like IN, but swaps the arguments + BlendModeDestIn BlendMode = C.VIPS_BLEND_MODE_DEST_IN + // BlendModeDestOut like OUT, but swaps the arguments + BlendModeDestOut BlendMode = C.VIPS_BLEND_MODE_DEST_OUT + // BlendModeDestAtop like ATOP, but swaps the arguments + BlendModeDestAtop BlendMode = C.VIPS_BLEND_MODE_DEST_ATOP + // BlendModeXOR something like a difference operator + BlendModeXOR BlendMode = C.VIPS_BLEND_MODE_XOR + // BlendModeAdd a bit like adding the two images + BlendModeAdd BlendMode = C.VIPS_BLEND_MODE_ADD + // BlendModeSaturate a bit like the darker of the two + BlendModeSaturate BlendMode = C.VIPS_BLEND_MODE_SATURATE + // BlendModeMultiply at least as dark as the darker of the two inputs + BlendModeMultiply BlendMode = C.VIPS_BLEND_MODE_MULTIPLY + // BlendModeScreen at least as light as the lighter of the inputs + BlendModeScreen BlendMode = C.VIPS_BLEND_MODE_SCREEN + // BlendModeOverlay multiplies or screens colors, depending on the lightness + BlendModeOverlay BlendMode = C.VIPS_BLEND_MODE_OVERLAY + // BlendModeDarken the darker of each component + BlendModeDarken BlendMode = C.VIPS_BLEND_MODE_DARKEN + // BlendModeLighten the lighter of each component + BlendModeLighten BlendMode = C.VIPS_BLEND_MODE_LIGHTEN + // BlendModeColorDodge brighten first by a factor second + BlendModeColorDodge BlendMode = C.VIPS_BLEND_MODE_COLOUR_DODGE + // BlendModeColorBurn darken first by a factor of second + BlendModeColorBurn BlendMode = C.VIPS_BLEND_MODE_COLOUR_BURN + // BlendModeHardLight multiply or screen, depending on lightness + BlendModeHardLight BlendMode = C.VIPS_BLEND_MODE_HARD_LIGHT + // BlendModeSoftLight darken or lighten, depending on lightness + BlendModeSoftLight BlendMode = C.VIPS_BLEND_MODE_SOFT_LIGHT + // BlendModeDifference difference of the two + BlendModeDifference BlendMode = C.VIPS_BLEND_MODE_DIFFERENCE + // BlendModeExclusion somewhat like DIFFERENCE, but lower-contrast + BlendModeExclusion BlendMode = C.VIPS_BLEND_MODE_EXCLUSION +) + // WatermarkFont defines the default watermark font to be used. var WatermarkFont = "sans 10" @@ -188,39 +245,42 @@ type Sharpen struct { // Options represents the supported image transformation options. type Options struct { - Height int - Width int - AreaHeight int - AreaWidth int - Top int - Left int - Quality int - Compression int - Zoom int - Crop bool - SmartCrop bool // Deprecated, use: bimg.Options.Gravity = bimg.GravitySmart - Enlarge bool - Embed bool - Flip bool - Flop bool - Force bool - NoAutoRotate bool - NoProfile bool - Interlace bool - StripMetadata bool - Trim bool - Lossless bool - Extend Extend - Rotate Angle - Background Color - Gravity Gravity - Watermark Watermark - WatermarkImage WatermarkImage - Type ImageType - Interpolator Interpolator - Interpretation Interpretation - GaussianBlur GaussianBlur - Sharpen Sharpen - Threshold float64 - OutputICC string + Height int + Width int + AreaHeight int + AreaWidth int + Top int + Left int + Quality int + Compression int + Zoom int + Crop bool + SmartCrop bool // Deprecated, use: bimg.Options.Gravity = bimg.GravitySmart + Enlarge bool + Embed bool + Flip bool + Flop bool + Force bool + NoAutoRotate bool + NoProfile bool + Interlace bool + StripMetadata bool + Trim bool + Lossless bool + Composite bool + Extend Extend + Rotate Angle + Background Color + Gravity Gravity + Watermark Watermark + WatermarkImage WatermarkImage + Type ImageType + Interpolator Interpolator + Interpretation Interpretation + GaussianBlur GaussianBlur + Sharpen Sharpen + BlendMode BlendMode + Threshold float64 + OutputICC string + CompositeLayers []*Image } diff --git a/resizer.go b/resizer.go index 98a95ae7..5dc60c5e 100644 --- a/resizer.go +++ b/resizer.go @@ -2,7 +2,7 @@ package bimg /* #cgo pkg-config: vips -#include "vips/vips.h" +#include "vips.h" */ import "C" @@ -87,6 +87,11 @@ func resizer(buf []byte, o Options) ([]byte, error) { return nil, err } + image, err = compositeImage(image, o.CompositeLayers, o.BlendMode) + if err != nil { + return nil, err + } + // Transform image, if necessary if shouldTransformImage(o, inWidth, inHeight) { image, err = transformImage(image, o, shrink, residual) @@ -385,6 +390,23 @@ func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { return vipsZoom(image, zoom+1) } +func compositeImage(image *C.VipsImage, layers []*Image, mode BlendMode) (*C.VipsImage, error) { + if mode == 0 { + return image, nil + } + + inputs := make([]*C.VipsImage, 0, len(layers)+1) + inputs = append(inputs, image) + for _, l := range layers { + layer, _, err := loadImage(l.buffer) + if err != nil { + return nil, err + } + inputs = append(inputs, layer) + } + return vipsComposite(inputs, mode) +} + func shrinkImage(image *C.VipsImage, o Options, residual float64, shrink int) (*C.VipsImage, float64, error) { // Use vips_shrink with the integral reduction image, err := vipsShrink(image, shrink) diff --git a/vips.c b/vips.c new file mode 100644 index 00000000..1396ba6a --- /dev/null +++ b/vips.c @@ -0,0 +1,498 @@ +#include "vips.h" + +/** + * This method is here to handle the weird initialization of the vips lib. + * libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41, + * or calls vips_init() in version >= 7.41. + * + * Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x, + * as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo + * is unable to determine the return type of vips_init(), making the build impossible. + * In order to correctly build bimg, for version < 7.41, we should undef vips_init and + * creates a vips_init() method that calls VIPS_INIT(). + */ + +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) +#undef vips_init +int +vips_init(const char *argv0) +{ + return VIPS_INIT(argv0); +} +#endif + +void +vips_enable_cache_set_trace() { + vips_cache_set_trace(TRUE); +} + +int +vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { + return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); +} + +int +vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { + return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL); +} + +int +vips_webpload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { + return vips_webpload_buffer(buf, len, out, "shrink", shrink, NULL); +} + +int +vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) { + return vips_flip(in, out, direction, NULL); +} + +int +vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { + return vips_shrink(in, out, xshrink, yshrink, NULL); +} + +int +vips_reduce_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { + return vips_reduce(in, out, xshrink, yshrink, NULL); +} + +int +vips_type_find_bridge(int t) { + if (t == GIF) { + return vips_type_find("VipsOperation", "gifload"); + } + if (t == PDF) { + return vips_type_find("VipsOperation", "pdfload"); + } + if (t == TIFF) { + return vips_type_find("VipsOperation", "tiffload"); + } + if (t == SVG) { + return vips_type_find("VipsOperation", "svgload"); + } + if (t == WEBP) { + return vips_type_find("VipsOperation", "webpload"); + } + if (t == PNG) { + return vips_type_find("VipsOperation", "pngload"); + } + if (t == JPEG) { + return vips_type_find("VipsOperation", "jpegload"); + } + if (t == MAGICK) { + return vips_type_find("VipsOperation", "magickload"); + } + return 0; +} + +int +vips_type_find_save_bridge(int t) { + if (t == TIFF) { + return vips_type_find("VipsOperation", "tiffsave_buffer"); + } + if (t == WEBP) { + return vips_type_find("VipsOperation", "webpsave_buffer"); + } + if (t == PNG) { + return vips_type_find("VipsOperation", "pngsave_buffer"); + } + if (t == JPEG) { + return vips_type_find("VipsOperation", "jpegsave_buffer"); + } + return 0; +} + +int +vips_rotate(VipsImage *in, VipsImage **out, int angle) { + int rotate = VIPS_ANGLE_D0; + + angle %= 360; + + if (angle == 45) { + rotate = VIPS_ANGLE45_D45; + } else if (angle == 90) { + rotate = VIPS_ANGLE_D90; + } else if (angle == 135) { + rotate = VIPS_ANGLE45_D135; + } else if (angle == 180) { + rotate = VIPS_ANGLE_D180; + } else if (angle == 225) { + rotate = VIPS_ANGLE45_D225; + } else if (angle == 270) { + rotate = VIPS_ANGLE_D270; + } else if (angle == 315) { + rotate = VIPS_ANGLE45_D315; + } else { + angle = 0; + } + + if (angle > 0 && angle % 90 != 0) { + return vips_rot45(in, out, "angle", rotate, NULL); + } else { + return vips_rot(in, out, rotate, NULL); + } +} + +int +vips_exif_orientation(VipsImage *image) { + int orientation = 0; + const char *exif; + if ( + vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && + !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) + ) { + orientation = atoi(&exif[0]); + } + return orientation; +} + +int +interpolator_window_size(char const *name) { + VipsInterpolate *interpolator = vips_interpolate_new(name); + int window_size = vips_interpolate_get_window_size(interpolator); + g_object_unref(interpolator); + return window_size; +} + +const char * +vips_enum_nick_bridge(VipsImage *image) { + return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); +} + +int +vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) { + return vips_zoom(in, out, xfac, yfac, NULL); +} + +int +vips_composite_bridge(VipsImage **in, VipsImage **out, int n, int mode) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 6) + return vips_composite(in, out, n, &mode, NULL); +#else + return 0; +#endif +} + +int +vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend, double r, double g, double b) { + if (extend == VIPS_EXTEND_BACKGROUND) { + double background[3] = {r, g, b}; + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + return vips_embed(in, out, left, top, width, height, "extend", extend, "background", vipsBackground, NULL); + } + return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); +} + +int +vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) { + return vips_extract_area(in, out, left, top, width, height, NULL); +} + +int +vips_colourspace_issupported_bridge(VipsImage *in) { + return vips_colourspace_issupported(in) ? 1 : 0; +} + +VipsInterpretation +vips_image_guess_interpretation_bridge(VipsImage *in) { + return vips_image_guess_interpretation(in); +} + +int +vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) { + return vips_colourspace(in, out, space, NULL); +} + +int +vips_icc_transform_bridge (VipsImage *in, VipsImage **out, const char *output_icc_profile) { + // `output_icc_profile` represents the absolute path to the output ICC profile file + return vips_icc_transform(in, out, output_icc_profile, "embedded", TRUE, NULL); +} + +int +vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) { + return vips_jpegsave_buffer(in, buf, len, + "strip", INT_TO_GBOOLEAN(strip), + "Q", quality, + "optimize_coding", TRUE, + "interlace", INT_TO_GBOOLEAN(interlace), + NULL + ); +} + +int +vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { +#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)) + return vips_pngsave_buffer(in, buf, len, + "strip", INT_TO_GBOOLEAN(strip), + "compression", compression, + "interlace", INT_TO_GBOOLEAN(interlace), + "filter", VIPS_FOREIGN_PNG_FILTER_NONE, + NULL + ); +#else + return vips_pngsave_buffer(in, buf, len, + "strip", INT_TO_GBOOLEAN(strip), + "compression", compression, + "interlace", INT_TO_GBOOLEAN(interlace), + NULL + ); +#endif +} + +int +vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless) { + return vips_webpsave_buffer(in, buf, len, + "strip", INT_TO_GBOOLEAN(strip), + "Q", quality, + "lossless", INT_TO_GBOOLEAN(lossless), + NULL + ); +} + +int +vips_tiffsave_bridge(VipsImage *in, void **buf, size_t *len) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) + return vips_tiffsave_buffer(in, buf, len, NULL); +#else + return 0; +#endif +} + +int +vips_is_16bit (VipsInterpretation interpretation) { + return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16; +} + +int +vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double r, double g, double b) { + if (vips_is_16bit(in->Type)) { + r = 65535 * r / 255; + g = 65535 * g / 255; + b = 65535 * b / 255; + } + + double background[3] = {r, g, b}; + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + + return vips_flatten(in, out, + "background", vipsBackground, + "max_alpha", vips_is_16bit(in->Type) ? 65535.0 : 255.0, + NULL + ); +} + +int +vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { + int code = 1; + + if (imageType == JPEG) { + code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == PNG) { + code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == WEBP) { + code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == TIFF) { + code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#if (VIPS_MAJOR_VERSION >= 8) +#if (VIPS_MINOR_VERSION >= 3) + } else if (imageType == GIF) { + code = vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == PDF) { + code = vips_pdfload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); + } else if (imageType == SVG) { + code = vips_svgload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#endif + } else if (imageType == MAGICK) { + code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); +#endif + } + + return code; +} + +int +vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) { + VipsImage *cache = vips_image_new(); + + if ( + vips_replicate(in, &cache, + 1 + orig->Xsize / in->Xsize, + 1 + orig->Ysize / in->Ysize, NULL) || + vips_crop(cache, out, 0, 0, orig->Xsize, orig->Ysize, NULL) + ) { + g_object_unref(cache); + return 1; + } + + g_object_unref(cache); + return 0; +} + +int +vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) { + double ones[3] = { 1, 1, 1 }; + + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + t[0] = in; + + // Make the mask. + if ( + vips_text(&t[1], to->Text, + "width", o->Width, + "dpi", o->DPI, + "font", to->Font, + NULL) || + vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || + vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || + vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL) + ) { + g_object_unref(base); + return 1; + } + + // Replicate if necessary + if (o->NoReplicate != 1) { + VipsImage *cache = vips_image_new(); + if (vips_watermark_replicate(t[0], t[4], &cache)) { + g_object_unref(cache); + g_object_unref(base); + return 1; + } + g_object_unref(t[4]); + t[4] = cache; + } + + // Make the constant image to paint the text with. + if ( + vips_black(&t[5], 1, 1, NULL) || + vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) || + vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) || + vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL) + ) { + g_object_unref(base); + return 1; + } + + // Blend the mask and text and write to output. + if (vips_ifthenelse(t[4], t[9], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} + +int +vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_gaussblur(in, out, (int) sigma, NULL); +#else + return vips_gaussblur(in, out, sigma, NULL, "min_ampl", min_ampl, NULL); +#endif +} + +int +vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2) { +#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) + return vips_sharpen(in, out, radius, x1, y2, y3, m1, m2, NULL); +#else + return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); +#endif +} + +int +vips_add_band(VipsImage *in, VipsImage **out, double c) { +#if (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 2)) + return vips_bandjoin_const1(in, out, c, NULL); +#else + VipsImage *base = vips_image_new(); + if ( + vips_black(&base, in->Xsize, in->Ysize, NULL) || + vips_linear1(base, &base, 1, c, NULL)) { + g_object_unref(base); + return 1; + } + g_object_unref(base); + return vips_bandjoin2(in, base, out, c, NULL); +#endif +} + +int +vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o) { + VipsImage *base = vips_image_new(); + VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); + + // add in and sub for unreffing and later use + t[0] = in; + t[1] = sub; + + if (has_alpha_channel(in) == 0) { + vips_add_band(in, &t[0], 255.0); + // in is no longer in the array and won't be unreffed, so add it at the end + t[8] = in; + } + + if (has_alpha_channel(sub) == 0) { + vips_add_band(sub, &t[1], 255.0); + // sub is no longer in the array and won't be unreffed, so add it at the end + t[9] = sub; + } + + // Place watermark image in the right place and size it to the size of the + // image that should be watermarked + if ( + vips_embed(t[1], &t[2], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { + g_object_unref(base); + return 1; + } + + // Create a mask image based on the alpha band from the watermark image + // and place it in the right position + if ( + vips_extract_band(t[1], &t[3], t[1]->Bands - 1, "n", 1, NULL) || + vips_linear1(t[3], &t[4], o->Opacity, 0.0, NULL) || + vips_cast(t[4], &t[5], VIPS_FORMAT_UCHAR, NULL) || + vips_copy(t[5], &t[6], "interpretation", t[0]->Type, NULL) || + vips_embed(t[6], &t[7], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { + g_object_unref(base); + return 1; + } + + // Blend the mask and watermark image and write to output. + if (vips_ifthenelse(t[7], t[2], t[0], out, "blend", TRUE, NULL)) { + g_object_unref(base); + return 1; + } + + g_object_unref(base); + return 0; +} + +int +vips_smartcrop_bridge(VipsImage *in, VipsImage **out, int width, int height) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) + return vips_smartcrop(in, out, width, height, NULL); +#else + return 0; +#endif +} + +int vips_find_trim_bridge(VipsImage *in, int *top, int *left, int *width, int *height, double r, double g, double b, double threshold) { +#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 6) + if (vips_is_16bit(in->Type)) { + r = 65535 * r / 255; + g = 65535 * g / 255; + b = 65535 * b / 255; + } + + double background[3] = {r, g, b}; + VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); + return vips_find_trim(in, top, left, width, height, "background", vipsBackground, "threshold", threshold, NULL); +#else + return 0; +#endif +} diff --git a/vips.go b/vips.go index fb179013..f911bd74 100644 --- a/vips.go +++ b/vips.go @@ -569,6 +569,17 @@ func vipsReduce(input *C.VipsImage, xshrink float64, yshrink float64) (*C.VipsIm return image, nil } +func vipsComposite(inputs []*C.VipsImage, mode BlendMode) (*C.VipsImage, error) { + var image *C.VipsImage + + err := C.vips_composite_bridge(&inputs[0], &image, C.int(len(inputs)), C.int(mode)) + if err != 0 { + return nil, catchVipsError() + } + + return image, nil +} + func vipsEmbed(input *C.VipsImage, left, top, width, height int, extend Extend, background Color) (*C.VipsImage, error) { var image *C.VipsImage diff --git a/vips.h b/vips.h index 29a2e638..329ce3d0 100644 --- a/vips.h +++ b/vips.h @@ -22,6 +22,36 @@ #define INT_TO_GBOOLEAN(bool) (bool > 0 ? TRUE : FALSE) +#if (VIPS_MAJOR_VERSION <= 8 && VIPS_MINOR_VERSION < 6) +typedef enum { + VIPS_BLEND_MODE_CLEAR, + VIPS_BLEND_MODE_SOURCE, + VIPS_BLEND_MODE_OVER, + VIPS_BLEND_MODE_IN, + VIPS_BLEND_MODE_OUT, + VIPS_BLEND_MODE_ATOP, + VIPS_BLEND_MODE_DEST, + VIPS_BLEND_MODE_DEST_OVER, + VIPS_BLEND_MODE_DEST_IN, + VIPS_BLEND_MODE_DEST_OUT, + VIPS_BLEND_MODE_DEST_ATOP, + VIPS_BLEND_MODE_XOR, + VIPS_BLEND_MODE_ADD, + VIPS_BLEND_MODE_SATURATE, + VIPS_BLEND_MODE_MULTIPLY, + VIPS_BLEND_MODE_SCREEN, + VIPS_BLEND_MODE_OVERLAY, + VIPS_BLEND_MODE_DARKEN, + VIPS_BLEND_MODE_LIGHTEN, + VIPS_BLEND_MODE_COLOUR_DODGE, + VIPS_BLEND_MODE_COLOUR_BURN, + VIPS_BLEND_MODE_HARD_LIGHT, + VIPS_BLEND_MODE_SOFT_LIGHT, + VIPS_BLEND_MODE_DIFFERENCE, + VIPS_BLEND_MODE_EXCLUSION, + VIPS_BLEND_MODE_LAST +} VipsBlendMode; +#endif enum types { UNKNOWN = 0, @@ -74,490 +104,116 @@ has_alpha_channel(VipsImage *image) { ) ? 1 : 0; } -/** - * This method is here to handle the weird initialization of the vips lib. - * libvips use a macro VIPS_INIT() that call vips__init() in version < 7.41, - * or calls vips_init() in version >= 7.41. - * - * Anyway, it's not possible to build bimg on Debian Jessie with libvips 7.40.x, - * as vips_init() is a macro to VIPS_INIT(), which is also a macro, hence, cgo - * is unable to determine the return type of vips_init(), making the build impossible. - * In order to correctly build bimg, for version < 7.41, we should undef vips_init and - * creates a vips_init() method that calls VIPS_INIT(). - */ - #if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) #undef vips_init int -vips_init(const char *argv0) -{ - return VIPS_INIT(argv0); -} +vips_init(const char *argv0); #endif void -vips_enable_cache_set_trace() { - vips_cache_set_trace(TRUE); -} +vips_enable_cache_set_trace(); int -vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator) { - return vips_affine(in, out, a, b, c, d, "interpolate", interpolator, NULL); -} +vips_affine_interpolator(VipsImage *in, VipsImage **out, double a, double b, double c, double d, VipsInterpolate *interpolator); int -vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { - return vips_jpegload_buffer(buf, len, out, "shrink", shrink, NULL); -} +vips_jpegload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink); int -vips_webpload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink) { - return vips_webpload_buffer(buf, len, out, "shrink", shrink, NULL); -} +vips_webpload_buffer_shrink(void *buf, size_t len, VipsImage **out, int shrink); int -vips_flip_bridge(VipsImage *in, VipsImage **out, int direction) { - return vips_flip(in, out, direction, NULL); -} +vips_flip_bridge(VipsImage *in, VipsImage **out, int direction); int -vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { - return vips_shrink(in, out, xshrink, yshrink, NULL); -} +vips_shrink_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink); int -vips_reduce_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink) { - return vips_reduce(in, out, xshrink, yshrink, NULL); -} +vips_reduce_bridge(VipsImage *in, VipsImage **out, double xshrink, double yshrink); int -vips_type_find_bridge(int t) { - if (t == GIF) { - return vips_type_find("VipsOperation", "gifload"); - } - if (t == PDF) { - return vips_type_find("VipsOperation", "pdfload"); - } - if (t == TIFF) { - return vips_type_find("VipsOperation", "tiffload"); - } - if (t == SVG) { - return vips_type_find("VipsOperation", "svgload"); - } - if (t == WEBP) { - return vips_type_find("VipsOperation", "webpload"); - } - if (t == PNG) { - return vips_type_find("VipsOperation", "pngload"); - } - if (t == JPEG) { - return vips_type_find("VipsOperation", "jpegload"); - } - if (t == MAGICK) { - return vips_type_find("VipsOperation", "magickload"); - } - return 0; -} +vips_type_find_bridge(int t); int -vips_type_find_save_bridge(int t) { - if (t == TIFF) { - return vips_type_find("VipsOperation", "tiffsave_buffer"); - } - if (t == WEBP) { - return vips_type_find("VipsOperation", "webpsave_buffer"); - } - if (t == PNG) { - return vips_type_find("VipsOperation", "pngsave_buffer"); - } - if (t == JPEG) { - return vips_type_find("VipsOperation", "jpegsave_buffer"); - } - return 0; -} +vips_type_find_save_bridge(int t); int -vips_rotate(VipsImage *in, VipsImage **out, int angle) { - int rotate = VIPS_ANGLE_D0; - - angle %= 360; - - if (angle == 45) { - rotate = VIPS_ANGLE45_D45; - } else if (angle == 90) { - rotate = VIPS_ANGLE_D90; - } else if (angle == 135) { - rotate = VIPS_ANGLE45_D135; - } else if (angle == 180) { - rotate = VIPS_ANGLE_D180; - } else if (angle == 225) { - rotate = VIPS_ANGLE45_D225; - } else if (angle == 270) { - rotate = VIPS_ANGLE_D270; - } else if (angle == 315) { - rotate = VIPS_ANGLE45_D315; - } else { - angle = 0; - } - - if (angle > 0 && angle % 90 != 0) { - return vips_rot45(in, out, "angle", rotate, NULL); - } else { - return vips_rot(in, out, rotate, NULL); - } -} +vips_rotate(VipsImage *in, VipsImage **out, int angle); int -vips_exif_orientation(VipsImage *image) { - int orientation = 0; - const char *exif; - if ( - vips_image_get_typeof(image, EXIF_IFD0_ORIENTATION) != 0 && - !vips_image_get_string(image, EXIF_IFD0_ORIENTATION, &exif) - ) { - orientation = atoi(&exif[0]); - } - return orientation; -} +vips_exif_orientation(VipsImage *image); int -interpolator_window_size(char const *name) { - VipsInterpolate *interpolator = vips_interpolate_new(name); - int window_size = vips_interpolate_get_window_size(interpolator); - g_object_unref(interpolator); - return window_size; -} +interpolator_window_size(char const *name); const char * -vips_enum_nick_bridge(VipsImage *image) { - return vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); -} +vips_enum_nick_bridge(VipsImage *image); int -vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac) { - return vips_zoom(in, out, xfac, yfac, NULL); -} +vips_zoom_bridge(VipsImage *in, VipsImage **out, int xfac, int yfac); int -vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend, double r, double g, double b) { - if (extend == VIPS_EXTEND_BACKGROUND) { - double background[3] = {r, g, b}; - VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); - return vips_embed(in, out, left, top, width, height, "extend", extend, "background", vipsBackground, NULL); - } - return vips_embed(in, out, left, top, width, height, "extend", extend, NULL); -} +vips_composite_bridge(VipsImage **in, VipsImage **out, int n, int mode); int -vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height) { - return vips_extract_area(in, out, left, top, width, height, NULL); -} +vips_embed_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height, int extend, double r, double g, double b); int -vips_colourspace_issupported_bridge(VipsImage *in) { - return vips_colourspace_issupported(in) ? 1 : 0; -} +vips_extract_area_bridge(VipsImage *in, VipsImage **out, int left, int top, int width, int height); + +int +vips_colourspace_issupported_bridge(VipsImage *in); VipsInterpretation -vips_image_guess_interpretation_bridge(VipsImage *in) { - return vips_image_guess_interpretation(in); -} +vips_image_guess_interpretation_bridge(VipsImage *in); int -vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space) { - return vips_colourspace(in, out, space, NULL); -} +vips_colourspace_bridge(VipsImage *in, VipsImage **out, VipsInterpretation space); int -vips_icc_transform_bridge (VipsImage *in, VipsImage **out, const char *output_icc_profile) { - // `output_icc_profile` represents the absolute path to the output ICC profile file - return vips_icc_transform(in, out, output_icc_profile, "embedded", TRUE, NULL); -} +vips_icc_transform_bridge (VipsImage *in, VipsImage **out, const char *output_icc_profile); int -vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) { - return vips_jpegsave_buffer(in, buf, len, - "strip", INT_TO_GBOOLEAN(strip), - "Q", quality, - "optimize_coding", TRUE, - "interlace", INT_TO_GBOOLEAN(interlace), - NULL - ); -} +vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace); int -vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { -#if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42)) - return vips_pngsave_buffer(in, buf, len, - "strip", INT_TO_GBOOLEAN(strip), - "compression", compression, - "interlace", INT_TO_GBOOLEAN(interlace), - "filter", VIPS_FOREIGN_PNG_FILTER_NONE, - NULL - ); -#else - return vips_pngsave_buffer(in, buf, len, - "strip", INT_TO_GBOOLEAN(strip), - "compression", compression, - "interlace", INT_TO_GBOOLEAN(interlace), - NULL - ); -#endif -} +vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace); int -vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless) { - return vips_webpsave_buffer(in, buf, len, - "strip", INT_TO_GBOOLEAN(strip), - "Q", quality, - "lossless", INT_TO_GBOOLEAN(lossless), - NULL - ); -} +vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int lossless); int -vips_tiffsave_bridge(VipsImage *in, void **buf, size_t *len) { -#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) - return vips_tiffsave_buffer(in, buf, len, NULL); -#else - return 0; -#endif -} +vips_tiffsave_bridge(VipsImage *in, void **buf, size_t *len); int -vips_is_16bit (VipsInterpretation interpretation) { - return interpretation == VIPS_INTERPRETATION_RGB16 || interpretation == VIPS_INTERPRETATION_GREY16; -} +vips_is_16bit (VipsInterpretation interpretation); int -vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double r, double g, double b) { - if (vips_is_16bit(in->Type)) { - r = 65535 * r / 255; - g = 65535 * g / 255; - b = 65535 * b / 255; - } - - double background[3] = {r, g, b}; - VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); - - return vips_flatten(in, out, - "background", vipsBackground, - "max_alpha", vips_is_16bit(in->Type) ? 65535.0 : 255.0, - NULL - ); -} +vips_flatten_background_brigde(VipsImage *in, VipsImage **out, double r, double g, double b); int -vips_init_image (void *buf, size_t len, int imageType, VipsImage **out) { - int code = 1; - - if (imageType == JPEG) { - code = vips_jpegload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); - } else if (imageType == PNG) { - code = vips_pngload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); - } else if (imageType == WEBP) { - code = vips_webpload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); - } else if (imageType == TIFF) { - code = vips_tiffload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); -#if (VIPS_MAJOR_VERSION >= 8) -#if (VIPS_MINOR_VERSION >= 3) - } else if (imageType == GIF) { - code = vips_gifload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); - } else if (imageType == PDF) { - code = vips_pdfload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); - } else if (imageType == SVG) { - code = vips_svgload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); -#endif - } else if (imageType == MAGICK) { - code = vips_magickload_buffer(buf, len, out, "access", VIPS_ACCESS_RANDOM, NULL); -#endif - } - - return code; -} +vips_init_image (void *buf, size_t len, int imageType, VipsImage **out); int -vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out) { - VipsImage *cache = vips_image_new(); - - if ( - vips_replicate(in, &cache, - 1 + orig->Xsize / in->Xsize, - 1 + orig->Ysize / in->Ysize, NULL) || - vips_crop(cache, out, 0, 0, orig->Xsize, orig->Ysize, NULL) - ) { - g_object_unref(cache); - return 1; - } - - g_object_unref(cache); - return 0; -} +vips_watermark_replicate (VipsImage *orig, VipsImage *in, VipsImage **out); int -vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o) { - double ones[3] = { 1, 1, 1 }; - - VipsImage *base = vips_image_new(); - VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); - t[0] = in; - - // Make the mask. - if ( - vips_text(&t[1], to->Text, - "width", o->Width, - "dpi", o->DPI, - "font", to->Font, - NULL) || - vips_linear1(t[1], &t[2], o->Opacity, 0.0, NULL) || - vips_cast(t[2], &t[3], VIPS_FORMAT_UCHAR, NULL) || - vips_embed(t[3], &t[4], 100, 100, t[3]->Xsize + o->Margin, t[3]->Ysize + o->Margin, NULL) - ) { - g_object_unref(base); - return 1; - } - - // Replicate if necessary - if (o->NoReplicate != 1) { - VipsImage *cache = vips_image_new(); - if (vips_watermark_replicate(t[0], t[4], &cache)) { - g_object_unref(cache); - g_object_unref(base); - return 1; - } - g_object_unref(t[4]); - t[4] = cache; - } - - // Make the constant image to paint the text with. - if ( - vips_black(&t[5], 1, 1, NULL) || - vips_linear(t[5], &t[6], ones, o->Background, 3, NULL) || - vips_cast(t[6], &t[7], VIPS_FORMAT_UCHAR, NULL) || - vips_copy(t[7], &t[8], "interpretation", t[0]->Type, NULL) || - vips_embed(t[8], &t[9], 0, 0, t[0]->Xsize, t[0]->Ysize, "extend", VIPS_EXTEND_COPY, NULL) - ) { - g_object_unref(base); - return 1; - } - - // Blend the mask and text and write to output. - if (vips_ifthenelse(t[4], t[9], t[0], out, "blend", TRUE, NULL)) { - g_object_unref(base); - return 1; - } - - g_object_unref(base); - return 0; -} +vips_watermark(VipsImage *in, VipsImage **out, WatermarkTextOptions *to, WatermarkOptions *o); int -vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) { -#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) - return vips_gaussblur(in, out, (int) sigma, NULL); -#else - return vips_gaussblur(in, out, sigma, NULL, "min_ampl", min_ampl, NULL); -#endif -} +vips_gaussblur_bridge(VipsImage *in, VipsImage **out, double sigma, double min_ampl) ; int -vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2) { -#if (VIPS_MAJOR_VERSION == 7 && VIPS_MINOR_VERSION < 41) - return vips_sharpen(in, out, radius, x1, y2, y3, m1, m2, NULL); -#else - return vips_sharpen(in, out, "radius", radius, "x1", x1, "y2", y2, "y3", y3, "m1", m1, "m2", m2, NULL); -#endif -} +vips_sharpen_bridge(VipsImage *in, VipsImage **out, int radius, double x1, double y2, double y3, double m1, double m2); int -vips_add_band(VipsImage *in, VipsImage **out, double c) { -#if (VIPS_MAJOR_VERSION > 8 || (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 2)) - return vips_bandjoin_const1(in, out, c, NULL); -#else - VipsImage *base = vips_image_new(); - if ( - vips_black(&base, in->Xsize, in->Ysize, NULL) || - vips_linear1(base, &base, 1, c, NULL)) { - g_object_unref(base); - return 1; - } - g_object_unref(base); - return vips_bandjoin2(in, base, out, c, NULL); -#endif -} +vips_add_band(VipsImage *in, VipsImage **out, double c); int -vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o) { - VipsImage *base = vips_image_new(); - VipsImage **t = (VipsImage **) vips_object_local_array(VIPS_OBJECT(base), 10); - - // add in and sub for unreffing and later use - t[0] = in; - t[1] = sub; - - if (has_alpha_channel(in) == 0) { - vips_add_band(in, &t[0], 255.0); - // in is no longer in the array and won't be unreffed, so add it at the end - t[8] = in; - } - - if (has_alpha_channel(sub) == 0) { - vips_add_band(sub, &t[1], 255.0); - // sub is no longer in the array and won't be unreffed, so add it at the end - t[9] = sub; - } - - // Place watermark image in the right place and size it to the size of the - // image that should be watermarked - if ( - vips_embed(t[1], &t[2], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { - g_object_unref(base); - return 1; - } - - // Create a mask image based on the alpha band from the watermark image - // and place it in the right position - if ( - vips_extract_band(t[1], &t[3], t[1]->Bands - 1, "n", 1, NULL) || - vips_linear1(t[3], &t[4], o->Opacity, 0.0, NULL) || - vips_cast(t[4], &t[5], VIPS_FORMAT_UCHAR, NULL) || - vips_copy(t[5], &t[6], "interpretation", t[0]->Type, NULL) || - vips_embed(t[6], &t[7], o->Left, o->Top, t[0]->Xsize, t[0]->Ysize, NULL)) { - g_object_unref(base); - return 1; - } - - // Blend the mask and watermark image and write to output. - if (vips_ifthenelse(t[7], t[2], t[0], out, "blend", TRUE, NULL)) { - g_object_unref(base); - return 1; - } - - g_object_unref(base); - return 0; -} +vips_watermark_image(VipsImage *in, VipsImage *sub, VipsImage **out, WatermarkImageOptions *o); int -vips_smartcrop_bridge(VipsImage *in, VipsImage **out, int width, int height) { -#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 5) - return vips_smartcrop(in, out, width, height, NULL); -#else - return 0; -#endif -} +vips_smartcrop_bridge(VipsImage *in, VipsImage **out, int width, int height); -int vips_find_trim_bridge(VipsImage *in, int *top, int *left, int *width, int *height, double r, double g, double b, double threshold) { -#if (VIPS_MAJOR_VERSION >= 8 && VIPS_MINOR_VERSION >= 6) - if (vips_is_16bit(in->Type)) { - r = 65535 * r / 255; - g = 65535 * g / 255; - b = 65535 * b / 255; - } - - double background[3] = {r, g, b}; - VipsArrayDouble *vipsBackground = vips_array_double_new(background, 3); - return vips_find_trim(in, top, left, width, height, "background", vipsBackground, "threshold", threshold, NULL); -#else - return 0; -#endif -} +int +vips_find_trim_bridge(VipsImage *in, int *top, int *left, int *width, int *height, double r, double g, double b, double threshold);