diff --git a/.editorconfig b/.editorconfig index 000dc0a..b570cb1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*] -indent_style = tabs +indent_style = tab indent_size = 2 end_of_line = lf charset = utf-8 diff --git a/fixtures/exif/Landscape_1_out.jpg b/fixtures/exif/Landscape_1_out.jpg index da6c37f..dce95e3 100644 Binary files a/fixtures/exif/Landscape_1_out.jpg and b/fixtures/exif/Landscape_1_out.jpg differ diff --git a/fixtures/exif/Landscape_2_out.jpg b/fixtures/exif/Landscape_2_out.jpg index cbe146e..c68364e 100644 Binary files a/fixtures/exif/Landscape_2_out.jpg and b/fixtures/exif/Landscape_2_out.jpg differ diff --git a/fixtures/exif/Landscape_3_out.jpg b/fixtures/exif/Landscape_3_out.jpg index 6a72bbb..3032ba8 100644 Binary files a/fixtures/exif/Landscape_3_out.jpg and b/fixtures/exif/Landscape_3_out.jpg differ diff --git a/fixtures/exif/Landscape_4_out.jpg b/fixtures/exif/Landscape_4_out.jpg index 12e6f84..cf7fa3a 100644 Binary files a/fixtures/exif/Landscape_4_out.jpg and b/fixtures/exif/Landscape_4_out.jpg differ diff --git a/fixtures/exif/Landscape_5_out.jpg b/fixtures/exif/Landscape_5_out.jpg index 5e571c1..8cbfb74 100644 Binary files a/fixtures/exif/Landscape_5_out.jpg and b/fixtures/exif/Landscape_5_out.jpg differ diff --git a/fixtures/exif/Landscape_6_out.jpg b/fixtures/exif/Landscape_6_out.jpg index 467bd11..4ef5ac0 100644 Binary files a/fixtures/exif/Landscape_6_out.jpg and b/fixtures/exif/Landscape_6_out.jpg differ diff --git a/fixtures/exif/Landscape_7_out.jpg b/fixtures/exif/Landscape_7_out.jpg index cee60e5..363ab61 100644 Binary files a/fixtures/exif/Landscape_7_out.jpg and b/fixtures/exif/Landscape_7_out.jpg differ diff --git a/fixtures/exif/Landscape_8_out.jpg b/fixtures/exif/Landscape_8_out.jpg index ff106a9..81d4e7c 100644 Binary files a/fixtures/exif/Landscape_8_out.jpg and b/fixtures/exif/Landscape_8_out.jpg differ diff --git a/fixtures/exif/Portrait_1_out.jpg b/fixtures/exif/Portrait_1_out.jpg index 7c39a34..593b0b8 100644 Binary files a/fixtures/exif/Portrait_1_out.jpg and b/fixtures/exif/Portrait_1_out.jpg differ diff --git a/fixtures/exif/Portrait_2_out.jpg b/fixtures/exif/Portrait_2_out.jpg index e06b994..00c1137 100644 Binary files a/fixtures/exif/Portrait_2_out.jpg and b/fixtures/exif/Portrait_2_out.jpg differ diff --git a/fixtures/exif/Portrait_3_out.jpg b/fixtures/exif/Portrait_3_out.jpg index 2277bd1..f45c5d0 100644 Binary files a/fixtures/exif/Portrait_3_out.jpg and b/fixtures/exif/Portrait_3_out.jpg differ diff --git a/fixtures/exif/Portrait_4_out.jpg b/fixtures/exif/Portrait_4_out.jpg index 1e398f2..592776b 100644 Binary files a/fixtures/exif/Portrait_4_out.jpg and b/fixtures/exif/Portrait_4_out.jpg differ diff --git a/fixtures/exif/Portrait_5_out.jpg b/fixtures/exif/Portrait_5_out.jpg index d5f3241..703e731 100644 Binary files a/fixtures/exif/Portrait_5_out.jpg and b/fixtures/exif/Portrait_5_out.jpg differ diff --git a/fixtures/exif/Portrait_6_out.jpg b/fixtures/exif/Portrait_6_out.jpg index c223077..fedb8fd 100644 Binary files a/fixtures/exif/Portrait_6_out.jpg and b/fixtures/exif/Portrait_6_out.jpg differ diff --git a/fixtures/exif/Portrait_7_out.jpg b/fixtures/exif/Portrait_7_out.jpg index e2f6acf..4536d01 100644 Binary files a/fixtures/exif/Portrait_7_out.jpg and b/fixtures/exif/Portrait_7_out.jpg differ diff --git a/fixtures/exif/Portrait_8_out.jpg b/fixtures/exif/Portrait_8_out.jpg index ebe87f3..6b464f7 100644 Binary files a/fixtures/exif/Portrait_8_out.jpg and b/fixtures/exif/Portrait_8_out.jpg differ diff --git a/resize.go b/resize.go index e60f08f..6be8cad 100644 --- a/resize.go +++ b/resize.go @@ -1,561 +1,16 @@ -package bimg +// +build go17 -/* -#cgo pkg-config: vips -#include "vips/vips.h" -*/ -import "C" +package bimg import ( - "errors" - "math" + "runtime" ) // Resize is used to transform a given image as byte buffer // with the passed options. func Resize(buf []byte, o Options) ([]byte, error) { - defer C.vips_thread_shutdown() - - image, imageType, err := loadImage(buf) - if err != nil { - return nil, err - } - - // Clone and define default options - o = applyDefaults(o, imageType) - - if !IsTypeSupported(o.Type) { - return nil, errors.New("Unsupported image output type") - } - - debug("Options: %#v", o) - - // Auto rotate image based on EXIF orientation header - image, rotated, err := rotateAndFlipImage(image, o) - if err != nil { - return nil, err - } - - // If JPEG image, retrieve the buffer - if rotated && imageType == JPEG && !o.NoAutoRotate { - buf, err = getImageBuffer(image) - if err != nil { - return nil, err - } - } - - inWidth := int(image.Xsize) - inHeight := int(image.Ysize) - - // Infer the required operation based on the in/out image sizes for a coherent transformation - normalizeOperation(&o, inWidth, inHeight) - - // image calculations - factor := imageCalculations(&o, inWidth, inHeight) - shrink := calculateShrink(factor, o.Interpolator) - residual := calculateResidual(factor, shrink) - - // Do not enlarge the output if the input width or height - // are already less than the required dimensions - if !o.Enlarge && !o.Force { - if inWidth < o.Width && inHeight < o.Height { - factor = 1.0 - shrink = 1 - residual = 0 - o.Width = inWidth - o.Height = inHeight - } - } - - // Try to use libjpeg shrink-on-load - if imageType == JPEG && shrink >= 2 { - tmpImage, factor, err := shrinkJpegImage(buf, image, factor, shrink) - if err != nil { - return nil, err - } - - image = tmpImage - factor = math.Max(factor, 1.0) - shrink = int(math.Floor(factor)) - residual = float64(shrink) / factor - } - - // Zoom image, if necessary - image, err = zoomImage(image, o.Zoom) - if err != nil { - return nil, err - } - - // Transform image, if necessary - if shouldTransformImage(o, inWidth, inHeight) { - image, err = transformImage(image, o, shrink, residual) - if err != nil { - return nil, err - } - } - - // Apply effects, if necessary - if shouldApplyEffects(o) { - image, err = applyEffects(image, o) - if err != nil { - return nil, err - } - } - - // Add watermark, if necessary - image, err = watermarkImageWithText(image, o.Watermark) - if err != nil { - return nil, err - } - - // Add watermark, if necessary - image, err = watermarkImageWithAnotherImage(image, o.WatermarkImage) - if err != nil { - return nil, err - } - - // Flatten image on a background, if necessary - image, err = imageFlatten(image, imageType, o) - if err != nil { - return nil, err - } - - return saveImage(image, o) -} - -func loadImage(buf []byte) (*C.VipsImage, ImageType, error) { - if len(buf) == 0 { - return nil, JPEG, errors.New("Image buffer is empty") - } - - image, imageType, err := vipsRead(buf) - if err != nil { - return nil, JPEG, err - } - - return image, imageType, nil -} - -func applyDefaults(o Options, imageType ImageType) Options { - if o.Quality == 0 { - o.Quality = Quality - } - if o.Compression == 0 { - o.Compression = 6 - } - if o.Type == 0 { - o.Type = imageType - } - if o.Interpretation == 0 { - o.Interpretation = InterpretationSRGB - } - return o -} - -func saveImage(image *C.VipsImage, o Options) ([]byte, error) { - saveOptions := vipsSaveOptions{ - Quality: o.Quality, - Type: o.Type, - Compression: o.Compression, - Interlace: o.Interlace, - NoProfile: o.NoProfile, - Interpretation: o.Interpretation, - OutputICC: o.OutputICC, - } - // Finally get the resultant buffer - return vipsSave(image, saveOptions) -} - -func normalizeOperation(o *Options, inWidth, inHeight int) { - if !o.Force && !o.Crop && !o.Embed && !o.Enlarge && o.Rotate == 0 && (o.Width > 0 || o.Height > 0) { - o.Force = true - } -} - -func shouldTransformImage(o Options, inWidth, inHeight int) bool { - return o.Force || (o.Width > 0 && o.Width != inWidth) || - (o.Height > 0 && o.Height != inHeight) || o.AreaWidth > 0 || o.AreaHeight > 0 -} - -func shouldApplyEffects(o Options) bool { - return o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 || o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 -} - -func transformImage(image *C.VipsImage, o Options, shrink int, residual float64) (*C.VipsImage, error) { - var err error - // Use vips_shrink with the integral reduction - if shrink > 1 { - image, residual, err = shrinkImage(image, o, residual, shrink) - if err != nil { - return nil, err - } - } - - residualx, residualy := residual, residual - if o.Force { - residualx = float64(o.Width) / float64(image.Xsize) - residualy = float64(o.Height) / float64(image.Ysize) - } - - if o.Force || residual != 0 { - image, err = vipsAffine(image, residualx, residualy, o.Interpolator) - if err != nil { - return nil, err - } - } - - if o.Force { - o.Crop = false - o.Embed = false - } - - image, err = extractOrEmbedImage(image, o) - if err != nil { - return nil, err - } - - debug("Transform: shrink=%v, residual=%v, interpolator=%v", - shrink, residual, o.Interpolator.String()) - - return image, nil -} - -func applyEffects(image *C.VipsImage, o Options) (*C.VipsImage, error) { - var err error - - if o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 { - image, err = vipsGaussianBlur(image, o.GaussianBlur) - if err != nil { - return nil, err - } - } - - if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 { - image, err = vipsSharpen(image, o.Sharpen) - if err != nil { - return nil, err - } - } - - debug("Effects: gaussSigma=%v, gaussMinAmpl=%v, sharpenRadius=%v", - o.GaussianBlur.Sigma, o.GaussianBlur.MinAmpl, o.Sharpen.Radius) - - return image, nil -} - -func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { - var err error - inWidth := int(image.Xsize) - inHeight := int(image.Ysize) - - switch { - case o.Gravity == GravitySmart, o.SmartCrop: - image, err = vipsSmartCrop(image, o.Width, o.Height) - break - case o.Crop: - width := int(math.Min(float64(inWidth), float64(o.Width))) - height := int(math.Min(float64(inHeight), float64(o.Height))) - left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) - left, top = int(math.Max(float64(left), 0)), int(math.Max(float64(top), 0)) - image, err = vipsExtract(image, left, top, width, height) - break - case o.Embed: - left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 - image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) - break - - case o.Top != 0 || o.Left != 0 || o.AreaWidth != 0 || o.AreaHeight != 0: - if o.AreaWidth == 0 { - o.AreaHeight = o.Width - } - if o.AreaHeight == 0 { - o.AreaHeight = o.Height - } - if o.AreaWidth == 0 || o.AreaHeight == 0 { - return nil, errors.New("Extract area width/height params are required") - } - image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) - break - } - - return image, err -} - -func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, bool, error) { - var err error - var rotated bool - var direction Direction = -1 - - if o.NoAutoRotate == false { - rotation, flip := calculateRotationAndFlip(image, o.Rotate) - if flip { - o.Flip = flip - } - if rotation > 0 && o.Rotate == 0 { - o.Rotate = rotation - } - } - - if o.Rotate > 0 { - rotated = true - image, err = vipsRotate(image, getAngle(o.Rotate)) - } - - if o.Flip { - direction = Horizontal - } else if o.Flop { - direction = Vertical - } - - if direction != -1 { - rotated = true - image, err = vipsFlip(image, direction) - } - - return image, rotated, err -} - -func watermarkImageWithText(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { - if w.Text == "" { - return image, nil - } - - // Defaults - if w.Font == "" { - w.Font = WatermarkFont - } - if w.Width == 0 { - w.Width = int(math.Floor(float64(image.Xsize / 6))) - } - if w.DPI == 0 { - w.DPI = 150 - } - if w.Margin == 0 { - w.Margin = w.Width - } - if w.Opacity == 0 { - w.Opacity = 0.25 - } else if w.Opacity > 1 { - w.Opacity = 1 - } - - image, err := vipsWatermark(image, w) - if err != nil { - return nil, err - } - - return image, nil -} - -func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.VipsImage, error) { - - if len(w.Buf) == 0 { - return image, nil - } - - if w.Opacity == 0.0 { - w.Opacity = 1.0 - } - - image, err := vipsDrawWatermark(image, w) - - if err != nil { - return nil, err - } - - return image, nil -} - -func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) { - // Only PNG images are supported for now - if imageType != PNG || o.Background == ColorBlack { - return image, nil - } - return vipsFlattenBackground(image, o.Background) -} - -func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { - if zoom == 0 { - return image, nil - } - return vipsZoom(image, zoom+1) -} - -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) - if err != nil { - return nil, 0, err - } - - // Recalculate residual float based on dimensions of required vs shrunk images - residualx := float64(o.Width) / float64(image.Xsize) - residualy := float64(o.Height) / float64(image.Ysize) - - if o.Crop { - residual = math.Max(residualx, residualy) - } else { - residual = math.Min(residualx, residualy) - } - - return image, residual, nil -} - -func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) { - var image *C.VipsImage - var err error - shrinkOnLoad := 1 - - // Recalculate integral shrink and double residual - switch { - case shrink >= 8: - factor = factor / 8 - shrinkOnLoad = 8 - case shrink >= 4: - factor = factor / 4 - shrinkOnLoad = 4 - case shrink >= 2: - factor = factor / 2 - shrinkOnLoad = 2 - } - - // Reload input using shrink-on-load - if shrinkOnLoad > 1 { - image, err = vipsShrinkJpeg(buf, input, shrinkOnLoad) - } - - return image, factor, err -} - -func imageCalculations(o *Options, inWidth, inHeight int) float64 { - factor := 1.0 - xfactor := float64(inWidth) / float64(o.Width) - yfactor := float64(inHeight) / float64(o.Height) - - switch { - // Fixed width and height - case o.Width > 0 && o.Height > 0: - if o.Crop { - factor = math.Min(xfactor, yfactor) - } else { - factor = math.Max(xfactor, yfactor) - } - // Fixed width, auto height - case o.Width > 0: - if o.Crop { - o.Height = inHeight - } else { - factor = xfactor - o.Height = roundFloat(float64(inHeight) / factor) - } - // Fixed height, auto width - case o.Height > 0: - if o.Crop { - o.Width = inWidth - } else { - factor = yfactor - o.Width = roundFloat(float64(inWidth) / factor) - } - // Identity transform - default: - o.Width = inWidth - o.Height = inHeight - break - } - - return factor -} - -func roundFloat(f float64) int { - if f < 0 { - return int(math.Ceil(f - 0.5)) - } - return int(math.Floor(f + 0.5)) -} - -func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity) (int, int) { - left, top := 0, 0 - - switch gravity { - case GravityNorth: - left = (inWidth - outWidth + 1) / 2 - case GravityEast: - left = inWidth - outWidth - top = (inHeight - outHeight + 1) / 2 - case GravitySouth: - left = (inWidth - outWidth + 1) / 2 - top = inHeight - outHeight - case GravityWest: - top = (inHeight - outHeight + 1) / 2 - default: - left = (inWidth - outWidth + 1) / 2 - top = (inHeight - outHeight + 1) / 2 - } - - return left, top -} - -func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) { - rotate := D0 - flip := false - - if angle > 0 { - return rotate, flip - } - - switch vipsExifOrientation(image) { - case 6: - rotate = D90 - break - case 3: - rotate = D180 - break - case 8: - rotate = D270 - break - case 2: - flip = true - break // flip 1 - case 7: - flip = true - rotate = D270 - break // flip 6 - case 4: - flip = true - rotate = D180 - break // flip 3 - case 5: - flip = true - rotate = D90 - break // flip 8 - } - - return rotate, flip -} - -func calculateShrink(factor float64, i Interpolator) int { - var shrink float64 - - // Calculate integral box shrink - windowSize := vipsWindowSize(i.String()) - if factor >= 2 && windowSize > 3 { - // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic - shrink = float64(math.Floor(factor * 3.0 / windowSize)) - } else { - shrink = math.Floor(factor) - } - - return int(math.Max(shrink, 1)) -} - -func calculateResidual(factor float64, shrink int) float64 { - return float64(shrink) / factor -} - -func getAngle(angle Angle) Angle { - divisor := angle % 90 - if divisor != 0 { - angle = angle - divisor - } - return Angle(math.Min(float64(angle), 270)) + // Required in order to prevent premature garbage collection. See: + // https://github.com/h2non/bimg/pull/162 + defer runtime.KeepAlive(buf) + return resizer(buf, o) } diff --git a/resize_legacy.go b/resize_legacy.go new file mode 100644 index 0000000..381450c --- /dev/null +++ b/resize_legacy.go @@ -0,0 +1,10 @@ +// +build !go17 + +package bimg + +// Resize is used to transform a given image as byte buffer +// with the passed options. +// Used as proxy to resizer() +func Resize(buf []byte, o Options) ([]byte, error) { + return resizer(buf, o) +} diff --git a/resizer.go b/resizer.go new file mode 100644 index 0000000..cce7b17 --- /dev/null +++ b/resizer.go @@ -0,0 +1,561 @@ +package bimg + +/* +#cgo pkg-config: vips +#include "vips/vips.h" +*/ +import "C" + +import ( + "errors" + "math" +) + +// resizer is used to transform a given image as byte buffer +// with the passed options. +func resizer(buf []byte, o Options) ([]byte, error) { + defer C.vips_thread_shutdown() + + image, imageType, err := loadImage(buf) + if err != nil { + return nil, err + } + + // Clone and define default options + o = applyDefaults(o, imageType) + + if !IsTypeSupported(o.Type) { + return nil, errors.New("Unsupported image output type") + } + + debug("Options: %#v", o) + + // Auto rotate image based on EXIF orientation header + image, rotated, err := rotateAndFlipImage(image, o) + if err != nil { + return nil, err + } + + // If JPEG image, retrieve the buffer + if rotated && imageType == JPEG && !o.NoAutoRotate { + buf, err = getImageBuffer(image) + if err != nil { + return nil, err + } + } + + inWidth := int(image.Xsize) + inHeight := int(image.Ysize) + + // Infer the required operation based on the in/out image sizes for a coherent transformation + normalizeOperation(&o, inWidth, inHeight) + + // image calculations + factor := imageCalculations(&o, inWidth, inHeight) + shrink := calculateShrink(factor, o.Interpolator) + residual := calculateResidual(factor, shrink) + + // Do not enlarge the output if the input width or height + // are already less than the required dimensions + if !o.Enlarge && !o.Force { + if inWidth < o.Width && inHeight < o.Height { + factor = 1.0 + shrink = 1 + residual = 0 + o.Width = inWidth + o.Height = inHeight + } + } + + // Try to use libjpeg shrink-on-load + if imageType == JPEG && shrink >= 2 { + tmpImage, factor, err := shrinkJpegImage(buf, image, factor, shrink) + if err != nil { + return nil, err + } + + image = tmpImage + factor = math.Max(factor, 1.0) + shrink = int(math.Floor(factor)) + residual = float64(shrink) / factor + } + + // Zoom image, if necessary + image, err = zoomImage(image, o.Zoom) + if err != nil { + return nil, err + } + + // Transform image, if necessary + if shouldTransformImage(o, inWidth, inHeight) { + image, err = transformImage(image, o, shrink, residual) + if err != nil { + return nil, err + } + } + + // Apply effects, if necessary + if shouldApplyEffects(o) { + image, err = applyEffects(image, o) + if err != nil { + return nil, err + } + } + + // Add watermark, if necessary + image, err = watermarkImageWithText(image, o.Watermark) + if err != nil { + return nil, err + } + + // Add watermark, if necessary + image, err = watermarkImageWithAnotherImage(image, o.WatermarkImage) + if err != nil { + return nil, err + } + + // Flatten image on a background, if necessary + image, err = imageFlatten(image, imageType, o) + if err != nil { + return nil, err + } + + return saveImage(image, o) +} + +func loadImage(buf []byte) (*C.VipsImage, ImageType, error) { + if len(buf) == 0 { + return nil, JPEG, errors.New("Image buffer is empty") + } + + image, imageType, err := vipsRead(buf) + if err != nil { + return nil, JPEG, err + } + + return image, imageType, nil +} + +func applyDefaults(o Options, imageType ImageType) Options { + if o.Quality == 0 { + o.Quality = Quality + } + if o.Compression == 0 { + o.Compression = 6 + } + if o.Type == 0 { + o.Type = imageType + } + if o.Interpretation == 0 { + o.Interpretation = InterpretationSRGB + } + return o +} + +func saveImage(image *C.VipsImage, o Options) ([]byte, error) { + saveOptions := vipsSaveOptions{ + Quality: o.Quality, + Type: o.Type, + Compression: o.Compression, + Interlace: o.Interlace, + NoProfile: o.NoProfile, + Interpretation: o.Interpretation, + OutputICC: o.OutputICC, + } + // Finally get the resultant buffer + return vipsSave(image, saveOptions) +} + +func normalizeOperation(o *Options, inWidth, inHeight int) { + if !o.Force && !o.Crop && !o.Embed && !o.Enlarge && o.Rotate == 0 && (o.Width > 0 || o.Height > 0) { + o.Force = true + } +} + +func shouldTransformImage(o Options, inWidth, inHeight int) bool { + return o.Force || (o.Width > 0 && o.Width != inWidth) || + (o.Height > 0 && o.Height != inHeight) || o.AreaWidth > 0 || o.AreaHeight > 0 +} + +func shouldApplyEffects(o Options) bool { + return o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 || o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 +} + +func transformImage(image *C.VipsImage, o Options, shrink int, residual float64) (*C.VipsImage, error) { + var err error + // Use vips_shrink with the integral reduction + if shrink > 1 { + image, residual, err = shrinkImage(image, o, residual, shrink) + if err != nil { + return nil, err + } + } + + residualx, residualy := residual, residual + if o.Force { + residualx = float64(o.Width) / float64(image.Xsize) + residualy = float64(o.Height) / float64(image.Ysize) + } + + if o.Force || residual != 0 { + image, err = vipsAffine(image, residualx, residualy, o.Interpolator) + if err != nil { + return nil, err + } + } + + if o.Force { + o.Crop = false + o.Embed = false + } + + image, err = extractOrEmbedImage(image, o) + if err != nil { + return nil, err + } + + debug("Transform: shrink=%v, residual=%v, interpolator=%v", + shrink, residual, o.Interpolator.String()) + + return image, nil +} + +func applyEffects(image *C.VipsImage, o Options) (*C.VipsImage, error) { + var err error + + if o.GaussianBlur.Sigma > 0 || o.GaussianBlur.MinAmpl > 0 { + image, err = vipsGaussianBlur(image, o.GaussianBlur) + if err != nil { + return nil, err + } + } + + if o.Sharpen.Radius > 0 && o.Sharpen.Y2 > 0 || o.Sharpen.Y3 > 0 { + image, err = vipsSharpen(image, o.Sharpen) + if err != nil { + return nil, err + } + } + + debug("Effects: gaussSigma=%v, gaussMinAmpl=%v, sharpenRadius=%v", + o.GaussianBlur.Sigma, o.GaussianBlur.MinAmpl, o.Sharpen.Radius) + + return image, nil +} + +func extractOrEmbedImage(image *C.VipsImage, o Options) (*C.VipsImage, error) { + var err error + inWidth := int(image.Xsize) + inHeight := int(image.Ysize) + + switch { + case o.Gravity == GravitySmart, o.SmartCrop: + image, err = vipsSmartCrop(image, o.Width, o.Height) + break + case o.Crop: + width := int(math.Min(float64(inWidth), float64(o.Width))) + height := int(math.Min(float64(inHeight), float64(o.Height))) + left, top := calculateCrop(inWidth, inHeight, o.Width, o.Height, o.Gravity) + left, top = int(math.Max(float64(left), 0)), int(math.Max(float64(top), 0)) + image, err = vipsExtract(image, left, top, width, height) + break + case o.Embed: + left, top := (o.Width-inWidth)/2, (o.Height-inHeight)/2 + image, err = vipsEmbed(image, left, top, o.Width, o.Height, o.Extend, o.Background) + break + + case o.Top != 0 || o.Left != 0 || o.AreaWidth != 0 || o.AreaHeight != 0: + if o.AreaWidth == 0 { + o.AreaHeight = o.Width + } + if o.AreaHeight == 0 { + o.AreaHeight = o.Height + } + if o.AreaWidth == 0 || o.AreaHeight == 0 { + return nil, errors.New("Extract area width/height params are required") + } + image, err = vipsExtract(image, o.Left, o.Top, o.AreaWidth, o.AreaHeight) + break + } + + return image, err +} + +func rotateAndFlipImage(image *C.VipsImage, o Options) (*C.VipsImage, bool, error) { + var err error + var rotated bool + var direction Direction = -1 + + if o.NoAutoRotate == false { + rotation, flip := calculateRotationAndFlip(image, o.Rotate) + if flip { + o.Flip = flip + } + if rotation > 0 && o.Rotate == 0 { + o.Rotate = rotation + } + } + + if o.Rotate > 0 { + rotated = true + image, err = vipsRotate(image, getAngle(o.Rotate)) + } + + if o.Flip { + direction = Horizontal + } else if o.Flop { + direction = Vertical + } + + if direction != -1 { + rotated = true + image, err = vipsFlip(image, direction) + } + + return image, rotated, err +} + +func watermarkImageWithText(image *C.VipsImage, w Watermark) (*C.VipsImage, error) { + if w.Text == "" { + return image, nil + } + + // Defaults + if w.Font == "" { + w.Font = WatermarkFont + } + if w.Width == 0 { + w.Width = int(math.Floor(float64(image.Xsize / 6))) + } + if w.DPI == 0 { + w.DPI = 150 + } + if w.Margin == 0 { + w.Margin = w.Width + } + if w.Opacity == 0 { + w.Opacity = 0.25 + } else if w.Opacity > 1 { + w.Opacity = 1 + } + + image, err := vipsWatermark(image, w) + if err != nil { + return nil, err + } + + return image, nil +} + +func watermarkImageWithAnotherImage(image *C.VipsImage, w WatermarkImage) (*C.VipsImage, error) { + + if len(w.Buf) == 0 { + return image, nil + } + + if w.Opacity == 0.0 { + w.Opacity = 1.0 + } + + image, err := vipsDrawWatermark(image, w) + + if err != nil { + return nil, err + } + + return image, nil +} + +func imageFlatten(image *C.VipsImage, imageType ImageType, o Options) (*C.VipsImage, error) { + // Only PNG images are supported for now + if imageType != PNG || o.Background == ColorBlack { + return image, nil + } + return vipsFlattenBackground(image, o.Background) +} + +func zoomImage(image *C.VipsImage, zoom int) (*C.VipsImage, error) { + if zoom == 0 { + return image, nil + } + return vipsZoom(image, zoom+1) +} + +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) + if err != nil { + return nil, 0, err + } + + // Recalculate residual float based on dimensions of required vs shrunk images + residualx := float64(o.Width) / float64(image.Xsize) + residualy := float64(o.Height) / float64(image.Ysize) + + if o.Crop { + residual = math.Max(residualx, residualy) + } else { + residual = math.Min(residualx, residualy) + } + + return image, residual, nil +} + +func shrinkJpegImage(buf []byte, input *C.VipsImage, factor float64, shrink int) (*C.VipsImage, float64, error) { + var image *C.VipsImage + var err error + shrinkOnLoad := 1 + + // Recalculate integral shrink and double residual + switch { + case shrink >= 8: + factor = factor / 8 + shrinkOnLoad = 8 + case shrink >= 4: + factor = factor / 4 + shrinkOnLoad = 4 + case shrink >= 2: + factor = factor / 2 + shrinkOnLoad = 2 + } + + // Reload input using shrink-on-load + if shrinkOnLoad > 1 { + image, err = vipsShrinkJpeg(buf, input, shrinkOnLoad) + } + + return image, factor, err +} + +func imageCalculations(o *Options, inWidth, inHeight int) float64 { + factor := 1.0 + xfactor := float64(inWidth) / float64(o.Width) + yfactor := float64(inHeight) / float64(o.Height) + + switch { + // Fixed width and height + case o.Width > 0 && o.Height > 0: + if o.Crop { + factor = math.Min(xfactor, yfactor) + } else { + factor = math.Max(xfactor, yfactor) + } + // Fixed width, auto height + case o.Width > 0: + if o.Crop { + o.Height = inHeight + } else { + factor = xfactor + o.Height = roundFloat(float64(inHeight) / factor) + } + // Fixed height, auto width + case o.Height > 0: + if o.Crop { + o.Width = inWidth + } else { + factor = yfactor + o.Width = roundFloat(float64(inWidth) / factor) + } + // Identity transform + default: + o.Width = inWidth + o.Height = inHeight + break + } + + return factor +} + +func roundFloat(f float64) int { + if f < 0 { + return int(math.Ceil(f - 0.5)) + } + return int(math.Floor(f + 0.5)) +} + +func calculateCrop(inWidth, inHeight, outWidth, outHeight int, gravity Gravity) (int, int) { + left, top := 0, 0 + + switch gravity { + case GravityNorth: + left = (inWidth - outWidth + 1) / 2 + case GravityEast: + left = inWidth - outWidth + top = (inHeight - outHeight + 1) / 2 + case GravitySouth: + left = (inWidth - outWidth + 1) / 2 + top = inHeight - outHeight + case GravityWest: + top = (inHeight - outHeight + 1) / 2 + default: + left = (inWidth - outWidth + 1) / 2 + top = (inHeight - outHeight + 1) / 2 + } + + return left, top +} + +func calculateRotationAndFlip(image *C.VipsImage, angle Angle) (Angle, bool) { + rotate := D0 + flip := false + + if angle > 0 { + return rotate, flip + } + + switch vipsExifOrientation(image) { + case 6: + rotate = D90 + break + case 3: + rotate = D180 + break + case 8: + rotate = D270 + break + case 2: + flip = true + break // flip 1 + case 7: + flip = true + rotate = D270 + break // flip 6 + case 4: + flip = true + rotate = D180 + break // flip 3 + case 5: + flip = true + rotate = D90 + break // flip 8 + } + + return rotate, flip +} + +func calculateShrink(factor float64, i Interpolator) int { + var shrink float64 + + // Calculate integral box shrink + windowSize := vipsWindowSize(i.String()) + if factor >= 2 && windowSize > 3 { + // Shrink less, affine more with interpolators that use at least 4x4 pixel window, e.g. bicubic + shrink = float64(math.Floor(factor * 3.0 / windowSize)) + } else { + shrink = math.Floor(factor) + } + + return int(math.Max(shrink, 1)) +} + +func calculateResidual(factor float64, shrink int) float64 { + return float64(shrink) / factor +} + +func getAngle(angle Angle) Angle { + divisor := angle % 90 + if divisor != 0 { + angle = angle - divisor + } + return Angle(math.Min(float64(angle), 270)) +} diff --git a/type_test.go b/type_test.go index 9c83b0f..fa65513 100644 --- a/type_test.go +++ b/type_test.go @@ -26,9 +26,11 @@ func TestDeterminateImageType(t *testing.T) { buf, _ := ioutil.ReadAll(img) defer img.Close() - if DetermineImageType(buf) != file.expected { - t.Fatal("Image type is not valid") - } + if VipsIsTypeSupported(file.expected) { + if DetermineImageType(buf) != file.expected { + t.Fatalf("Image type is not valid: %s != %s", file.name, ImageTypes[file.expected]) + } + } } } @@ -51,8 +53,8 @@ func TestDeterminateImageTypeName(t *testing.T) { buf, _ := ioutil.ReadAll(img) defer img.Close() - if DetermineImageTypeName(buf) != file.expected { - t.Fatal("Image type is not valid") + if DetermineImageTypeName(buf) != file.expected { + t.Fatalf("Image type is not valid: %s != %s", file.name, file.expected) } } }