Merge pull request #186 from h2non/fix/#162-resize-garbage-collection

fix(#162): garbage collection fix. split Resize() implementation for Go runtime
master
Tomás Aparicio 8 years ago committed by GitHub
commit d7007cecd3

@ -1,7 +1,7 @@
root = true root = true
[*] [*]
indent_style = tabs indent_style = tab
indent_size = 2 indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8

@ -1,4 +1,30 @@
## v1.0.11 / 2017-09-10
* feat(#189): allow strip image metadata via bimg.Options.StripMetadata = bool
* fix(resize): code format issue
* refactor(resize): add Go version comment
* refactor(tests): fix minor code formatting issues
* fix(#162): garbage collection fix. split Resize() implementation for Go runtime specific
* feat(travis): add go 1.9
* Merge pull request #183 from greut/autorotate
* Proper handling of the EXIF cases.
* Merge pull request #184 from greut/libvips858
* Merge branch 'master' into libvips858
* Merge pull request #185 from greut/libvips860
* Add libvips 8.6 pre-release
* Update to libvips 8.5.8
* fix(resize): runtime.KeepAlive is only Go
* fix(#159): prevent buf to be freed by the GC before resize function exits
* Merge pull request #171 from greut/fix-170
* Check the length before jumping into buffer.
* Merge pull request #168 from Traum-Ferienwohnungen/icc_transform
* Add option to convert embedded ICC profiles
* Merge pull request #166 from danjou-a/patch-1
* Fix Resize verification value
* Merge pull request #165 from greut/libvips846
* Testing using libvips8.4.6 from Github.
## v1.0.10 / 2017-06-25 ## v1.0.10 / 2017-06-25
* Merge pull request #164 from greut/length * Merge pull request #164 from greut/length

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 117 KiB

@ -204,6 +204,7 @@ type Options struct {
NoAutoRotate bool NoAutoRotate bool
NoProfile bool NoProfile bool
Interlace bool Interlace bool
StripMetadata bool
Extend Extend Extend Extend
Rotate Angle Rotate Angle
Background Color Background Color

@ -1,561 +1,16 @@
package bimg // +build go17
/* package bimg
#cgo pkg-config: vips
#include "vips/vips.h"
*/
import "C"
import ( import (
"errors" "runtime"
"math"
) )
// Resize is used to transform a given image as byte buffer // Resize is used to transform a given image as byte buffer
// with the passed options. // with the passed options.
func Resize(buf []byte, o Options) ([]byte, error) { func Resize(buf []byte, o Options) ([]byte, error) {
defer C.vips_thread_shutdown() // Required in order to prevent premature garbage collection. See:
// https://github.com/h2non/bimg/pull/162
image, imageType, err := loadImage(buf) defer runtime.KeepAlive(buf)
if err != nil { return resizer(buf, o)
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))
} }

@ -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() only in Go <= 1.6 versions
func Resize(buf []byte, o Options) ([]byte, error) {
return resizer(buf, o)
}

@ -0,0 +1,562 @@
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,
StripMetadata: o.StripMetadata,
}
// 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))
}

@ -26,8 +26,10 @@ func TestDeterminateImageType(t *testing.T) {
buf, _ := ioutil.ReadAll(img) buf, _ := ioutil.ReadAll(img)
defer img.Close() defer img.Close()
if DetermineImageType(buf) != file.expected { if VipsIsTypeSupported(file.expected) {
t.Fatal("Image type is not valid") if DetermineImageType(buf) != file.expected {
t.Fatalf("Image type is not valid: %s != %s", file.name, ImageTypes[file.expected])
}
} }
} }
} }
@ -52,7 +54,7 @@ func TestDeterminateImageTypeName(t *testing.T) {
defer img.Close() defer img.Close()
if DetermineImageTypeName(buf) != file.expected { if DetermineImageTypeName(buf) != file.expected {
t.Fatal("Image type is not valid") t.Fatalf("Image type is not valid: %s != %s", file.name, file.expected)
} }
} }
} }

@ -1,4 +1,4 @@
package bimg package bimg
// Version represents the current package semantic version. // Version represents the current package semantic version.
const Version = "1.0.10" const Version = "1.0.11"

@ -55,6 +55,7 @@ type vipsSaveOptions struct {
Type ImageType Type ImageType
Interlace bool Interlace bool
NoProfile bool NoProfile bool
StripMetadata bool
OutputICC string // Absolute path to the output ICC profile OutputICC string // Absolute path to the output ICC profile
Interpretation Interpretation Interpretation Interpretation
} }
@ -420,6 +421,7 @@ func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) {
saveErr := C.int(0) saveErr := C.int(0)
interlace := C.int(boolToInt(o.Interlace)) interlace := C.int(boolToInt(o.Interlace))
quality := C.int(o.Quality) quality := C.int(o.Quality)
strip := C.int(boolToInt(o.Interlace))
if o.Type != 0 && !IsTypeSupportedSave(o.Type) { if o.Type != 0 && !IsTypeSupportedSave(o.Type) {
return nil, fmt.Errorf("VIPS cannot save to %#v", ImageTypes[o.Type]) return nil, fmt.Errorf("VIPS cannot save to %#v", ImageTypes[o.Type])
@ -427,13 +429,13 @@ func vipsSave(image *C.VipsImage, o vipsSaveOptions) ([]byte, error) {
var ptr unsafe.Pointer var ptr unsafe.Pointer
switch o.Type { switch o.Type {
case WEBP: case WEBP:
saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, 1, quality) saveErr = C.vips_webpsave_bridge(tmpImage, &ptr, &length, strip, quality)
case PNG: case PNG:
saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, 1, C.int(o.Compression), quality, interlace) saveErr = C.vips_pngsave_bridge(tmpImage, &ptr, &length, strip, C.int(o.Compression), quality, interlace)
case TIFF: case TIFF:
saveErr = C.vips_tiffsave_bridge(tmpImage, &ptr, &length) saveErr = C.vips_tiffsave_bridge(tmpImage, &ptr, &length)
default: default:
saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, 1, quality, interlace) saveErr = C.vips_jpegsave_bridge(tmpImage, &ptr, &length, strip, quality, interlace)
} }
if int(saveErr) != 0 { if int(saveErr) != 0 {

@ -269,7 +269,7 @@ vips_icc_transform_bridge (VipsImage *in, VipsImage **out, const char *output_ic
int int
vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) { vips_jpegsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality, int interlace) {
return vips_jpegsave_buffer(in, buf, len, return vips_jpegsave_buffer(in, buf, len,
"strip", strip, "strip", strip == 1 ? TRUE : FALSE,
"Q", quality, "Q", quality,
"optimize_coding", TRUE, "optimize_coding", TRUE,
"interlace", with_interlace(interlace), "interlace", with_interlace(interlace),
@ -281,7 +281,7 @@ int
vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compression, int quality, int interlace) { 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)) #if (VIPS_MAJOR_VERSION >= 8 || (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 42))
return vips_pngsave_buffer(in, buf, len, return vips_pngsave_buffer(in, buf, len,
"strip", FALSE, "strip", strip == 1 ? TRUE : FALSE,
"compression", compression, "compression", compression,
"interlace", with_interlace(interlace), "interlace", with_interlace(interlace),
"filter", VIPS_FOREIGN_PNG_FILTER_NONE, "filter", VIPS_FOREIGN_PNG_FILTER_NONE,
@ -289,7 +289,7 @@ vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compr
); );
#else #else
return vips_pngsave_buffer(in, buf, len, return vips_pngsave_buffer(in, buf, len,
"strip", FALSE, "strip", strip == 1 ? TRUE : FALSE,
"compression", compression, "compression", compression,
"interlace", with_interlace(interlace), "interlace", with_interlace(interlace),
NULL NULL
@ -300,7 +300,7 @@ vips_pngsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int compr
int int
vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) { vips_webpsave_bridge(VipsImage *in, void **buf, size_t *len, int strip, int quality) {
return vips_webpsave_buffer(in, buf, len, return vips_webpsave_buffer(in, buf, len,
"strip", strip, "strip", strip == 1 ? TRUE : FALSE,
"Q", quality, "Q", quality,
NULL NULL
); );

@ -33,7 +33,7 @@ func TestVipsSave(t *testing.T) {
for _, typ := range types { for _, typ := range types {
image, _, _ := vipsRead(readImage("test.jpg")) image, _, _ := vipsRead(readImage("test.jpg"))
options := vipsSaveOptions{Quality: 95, Type: typ} options := vipsSaveOptions{Quality: 95, Type: typ, StripMetadata: true}
buf, err := vipsSave(image, options) buf, err := vipsSave(image, options)
if err != nil { if err != nil {

Loading…
Cancel
Save