123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- 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 ||
- o.Trim
- }
- 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 {
- if residualx < 1 && residualy < 1 {
- image, err = vipsReduce(image, 1/residualx, 1/residualy)
- } else {
- 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.Trim:
- left, top, width, height, err := vipsTrim(image)
- if err == nil {
- image, err = vipsExtract(image, left, top, width, height)
- }
- 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))
- }
|