howood/imagereductor

View on GitHub
application/actor/imageoperator.go

Summary

Maintainability
C
1 day
Test Coverage
package actor

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "image"
    "image/color"
    "image/gif"
    "image/jpeg"
    "image/png"
    "io"
    "math"
    "reflect"

    "github.com/howood/imagereductor/domain/entity"
    "github.com/howood/imagereductor/domain/repository"
    log "github.com/howood/imagereductor/infrastructure/logger"
    "github.com/howood/imagereductor/library/utils"
    "github.com/rwcarlsen/goexif/exif"
    "golang.org/x/image/draw"
    "golang.org/x/image/math/f64"
)

const (
    // ImageRotateRight is rotate image 90 degree right
    ImageRotateRight = "right"
    // ImageRotateLeft is rotate image 90 degree left
    ImageRotateLeft = "left"
    // ImageRotateUpsidedown is rotate image upside down
    ImageRotateUpsidedown = "upsidedown"
    // ImageRotateAutoVertical is rotate image auto vertical
    ImageRotateAutoVertical = "autovertical"
    // ImageRotateAutoHorizontal is rotate image auto horizontal
    ImageRotateAutoHorizontal = "autohorizontal"
    // ImageRotateExifOrientation is rotate image by exif orientation
    ImageRotateExifOrientation = "exiforientation"
)

// ImageOperator struct
type ImageOperator struct {
    repository.ImageObjectRepository
}

// NewImageOperator creates a new ImageObjectRepository
func NewImageOperator(ctx context.Context, contenttype string, option ImageOperatorOption) *ImageOperator {
    objectOption := entity.ImageObjectOption(option)
    return &ImageOperator{
        &imageCreator{
            object: &entity.ImageObject{
                ContentType: contenttype,
            },
            option: &objectOption,
            ctx:    ctx,
        },
    }
}

// imageCreator struct
type imageCreator struct {
    object          *entity.ImageObject
    option          *entity.ImageObjectOption
    exifOrientation int
    ctx             context.Context
}

// ImageOperatorOption is Option of ImageOperator struct
type ImageOperatorOption entity.ImageObjectOption

type subImager interface {
    SubImage(r image.Rectangle) image.Image
}

// Decode images
func (im *imageCreator) Decode(src io.ReadSeeker) error {
    var err error
    im.object.Source, im.object.ImageName, err = image.Decode(src)
    if err == nil {
        im.decodeExifOrientation(src)
    }
    if err == nil {
        rectang := im.object.Source.Bounds()
        im.object.OriginX = rectang.Bounds().Dx()
        im.object.OriginY = rectang.Bounds().Dy()
        log.Debug(im.ctx, fmt.Sprintf("OriginX: %d / OriginY: %d", im.object.OriginX, im.object.OriginY))
    }
    return err
}

// Process images process resize and more
func (im *imageCreator) Process() error {
    if im.option.Gamma != 0 {
        im.object.Source = im.gamma(im.object.Source)
    }
    if im.option.Contrast != 0 {
        im.object.Source = im.contrast(im.object.Source)
    }
    if im.option.Brightness != 0 {
        im.object.Source = im.brightness(im.object.Source)
    }
    switch {
    case (im.option.Rotate != ""):
        err := im.rotate()
        if err != nil {
            return err
        }
        im.calcResizeXY()
        return im.resize()
    case !reflect.DeepEqual(im.option.Crop, [4]int{}):
        im.calcResizeXYWithCrop()
        return im.cropAndResize()
    default:
        im.calcResizeXY()
        return im.resize()
    }
}

// resize images
func (im *imageCreator) resize() error {
    rect := image.Rect(0, 0, im.object.DstX, im.object.DstY)
    im.object.Dst = im.scale(im.object.Source, rect, im.getDrawer())
    return nil
}

// crop and resize images
func (im *imageCreator) cropAndResize() error {
    croprect := image.Rect(im.option.Crop[0], im.option.Crop[1], im.option.Crop[2], im.option.Crop[3])
    cropimg, err := im.subimage(im.object.Source, croprect)
    if err != nil {
        return err
    }
    scalerect := image.Rect(0, 0, im.object.DstX, im.object.DstY)
    im.object.Dst = im.scale(cropimg, scalerect, im.getDrawer())
    return nil
}

// rotate images
func (im *imageCreator) rotate() error {
    originX := im.object.OriginX
    originY := im.object.OriginY
    switch im.option.Rotate {
    case ImageRotateRight:
        rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
        im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(90.0, float64(im.object.OriginY), 0), im.getDrawer())
        im.object.OriginX = originY
        im.object.OriginY = originX
    case ImageRotateLeft:
        rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
        im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(270.0, 0, float64(im.object.OriginX)), im.getDrawer())
        im.object.OriginX = originY
        im.object.OriginY = originX
    case ImageRotateUpsidedown:
        rect := image.Rect(0, 0, im.object.OriginX, im.object.OriginY)
        im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(180.0, float64(im.object.OriginX), float64(im.object.OriginY)), im.getDrawer())
    case ImageRotateAutoVertical:
        if im.object.OriginX > im.object.OriginY {
            rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
            im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(90.0, float64(im.object.OriginY), 0), im.getDrawer())
            im.object.OriginX = originY
            im.object.OriginY = originX
        }
    case ImageRotateAutoHorizontal:
        if im.object.OriginY > im.object.OriginX {
            rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
            im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(270.0, 0, float64(im.object.OriginX)), im.getDrawer())
            im.object.OriginX = originY
            im.object.OriginY = originX
        }
    case ImageRotateExifOrientation:
        if im.exifOrientation == 3 {
            rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
            im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(180.0, 0, float64(im.object.OriginX)), im.getDrawer())
        }
        if im.exifOrientation == 6 {
            rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
            im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(90.0, 0, float64(im.object.OriginX)), im.getDrawer())
            im.object.OriginX = originY
            im.object.OriginY = originX
        }
        if im.exifOrientation == 8 {
            rect := image.Rect(0, 0, im.object.OriginY, im.object.OriginX)
            im.object.Source = im.transform(im.object.Source, rect, im.calcRotateAffine(270.0, 0, float64(im.object.OriginX)), im.getDrawer())
            im.object.OriginX = originY
            im.object.OriginY = originX
        }
    default:
        return fmt.Errorf("Invalid Rotate Parameter")
    }
    return nil
}

// ImageByte get image bytes
func (im *imageCreator) ImageByte() ([]byte, error) {
    buf := new(bytes.Buffer)
    var err error
    switch im.object.ContentType {
    case "image/jpeg":
        err = jpeg.Encode(buf, im.object.Dst, im.jpegOption())
    case "image/png":
        err = png.Encode(buf, im.object.Dst)
    case "image/gif":
        err = gif.Encode(buf, im.object.Dst, nil)
    default:
        err = errors.New("invalid format")
    }
    if err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

// scale image
func (im *imageCreator) scale(src image.Image, rect image.Rectangle, scaler draw.Scaler) image.Image {
    dst := image.NewNRGBA(rect)
    scaler.Scale(dst, rect, src, src.Bounds(), draw.Over, nil)
    return dst
}

// crop image
func (im *imageCreator) subimage(src image.Image, rect image.Rectangle) (image.Image, error) {
    simg, ok := src.(subImager)
    if !ok {
        return nil, fmt.Errorf("Image not support Crop")
    }
    return simg.SubImage(rect), nil
}

// rotate image
func (im *imageCreator) transform(src image.Image, rect image.Rectangle, t f64.Aff3, scaler draw.Transformer) image.Image {
    dst := image.NewNRGBA(rect)
    scaler.Transform(dst, t, src, src.Bounds(), draw.Over, nil)
    return dst
}

// change brightness
func (im *imageCreator) brightness(src image.Image) *image.NRGBA {
    lookup := make([]uint8, 256)
    percentage := math.Min(math.Max(float64(im.option.Brightness), -100.0), 100.0)
    for i := 0; i < 256; i++ {
        lookup[i] = uint8(utils.InRanged(float64(i)*(percentage/100.0), 0, 255))
    }
    return im.convertLuminance(src, lookup)
}

// change contrast
func (im *imageCreator) contrast(src image.Image) *image.NRGBA {
    lookup := make([]uint8, 256)
    percentage := math.Min(math.Max(float64(im.option.Contrast), -100.0), 100.0)
    v := (100.0 + percentage) / 100.0
    for i := 0; i < 256; i++ {
        lookup[i] = uint8(utils.InRanged(((((float64(i)/255)-0.5)*v)+0.5)*255, 0, 255))
    }
    return im.convertLuminance(src, lookup)
}

// change gamma
func (im *imageCreator) gamma(src image.Image) *image.NRGBA {
    lookup := make([]uint8, 256)
    e := 1.0 / math.Max(im.option.Gamma, 0.0001)
    for i := 0; i < 256; i++ {
        lookup[i] = uint8(utils.InRanged(math.Pow(float64(i)/255.0, e)*255.0, 0, 255))
    }
    return im.convertLuminance(src, lookup)
}

func (im *imageCreator) convertLuminance(src image.Image, lookup []uint8) *image.NRGBA {
    fnc := func(c color.RGBA) color.RGBA {
        return color.RGBA{lookup[c.R], lookup[c.G], lookup[c.B], c.A}
    }
    bounds := src.Bounds()
    dst := image.NewNRGBA(bounds)
    draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
    utils.ApplyParallel(0, dst.Bounds().Dy(), func(start, end int) {
        for y := start; y < end; y++ {
            for x := 0; x < dst.Bounds().Dx(); x++ {
                dstPos := y*dst.Stride + x*4
                dr := &dst.Pix[dstPos+0]
                dg := &dst.Pix[dstPos+1]
                db := &dst.Pix[dstPos+2]
                da := &dst.Pix[dstPos+3]
                c := color.RGBA{
                    R: *dr,
                    G: *dg,
                    B: *db,
                    A: *da,
                }
                c = fnc(c)
                *dr = c.R
                *dg = c.G
                *db = c.B
                *da = c.A
            }
        }
    })
    return dst
}

func (im *imageCreator) getDrawer() draw.Interpolator {
    switch im.option.Quality {
    case 1:
        return draw.NearestNeighbor
    case 2:
        return draw.ApproxBiLinear
    case 3:
        return draw.BiLinear
    case 4:
        return draw.CatmullRom
    default:
        return draw.CatmullRom
    }
}

func (im *imageCreator) calcResizeXY() {
    log.Debug(im.ctx, fmt.Sprintf("OptionX: %d / OptionY: %d", im.option.Width, im.option.Height))
    switch {
    case (im.option.Width == 0 && im.option.Height == 0):
        im.object.DstX = im.object.OriginX
        im.object.DstY = im.object.OriginY
    case (im.option.Width != 0 && im.option.Height == 0),
        (im.option.Width != 0 && im.option.Height != 0 && float64(im.object.OriginY)/float64(im.object.OriginX) <= float64(im.option.Height)/float64(im.option.Width)):
        im.calcResizeFitOptionWidth(im.object.OriginX, im.object.OriginY)
    case (im.option.Width == 0 && im.option.Height != 0),
        (im.option.Width != 0 && im.option.Height != 0 && float64(im.object.OriginY)/float64(im.object.OriginX) > float64(im.option.Height)/float64(im.option.Width)):
        im.calcResizeFitOptionHeight(im.object.OriginX, im.object.OriginY)
    }
    log.Debug(im.ctx, fmt.Sprintf("DstX: %d / DstY: %d", im.object.DstX, im.object.DstY))
}

func (im *imageCreator) calcResizeXYWithCrop() {
    log.Debug(im.ctx, fmt.Sprintf("OptionX: %d / OptionY: %d", im.option.Width, im.option.Height))
    log.Debug(im.ctx, fmt.Sprintf("Crop: %v", im.option.Crop))
    cropedX := int(math.Abs(float64(im.option.Crop[2] - im.option.Crop[0])))
    cropedY := int(math.Abs(float64(im.option.Crop[3] - im.option.Crop[1])))
    switch {
    case (im.option.Width == 0 && im.option.Height == 0):
        im.object.DstX = cropedX
        im.object.DstY = cropedY
    case (im.option.Width != 0 && im.option.Height == 0),
        (im.option.Width != 0 && im.option.Height != 0 && float64(cropedY)/float64(cropedX) <= float64(im.option.Height)/float64(im.option.Width)):
        im.calcResizeFitOptionWidth(cropedX, cropedY)
    case (im.option.Width == 0 && im.option.Height != 0),
        (im.option.Width != 0 && im.option.Height != 0 && float64(cropedY)/float64(cropedX) > float64(im.option.Height)/float64(im.option.Width)):
        im.calcResizeFitOptionHeight(cropedX, cropedY)
    }
    log.Debug(im.ctx, fmt.Sprintf("DstX: %d / DstY: %d", im.object.DstX, im.object.DstY))
}

func (im *imageCreator) calcResizeFitOptionWidth(originx, originy int) {
    im.object.DstX = im.option.Width
    im.object.DstY = originy
    if originx != 0 {
        im.object.DstY = int(float64(im.option.Width) * (float64(originy) / float64(originx)))
    }
}

func (im *imageCreator) calcResizeFitOptionHeight(originx, originy int) {
    im.object.DstX = originx
    if originy != 0 {
        im.object.DstX = int(float64(im.option.Height) * (float64(originx) / float64(originy)))
    }
    im.object.DstY = im.option.Height
}

func (im *imageCreator) calcRotateAffine(deg, moveleft, movedown float64) f64.Aff3 {
    log.Debug(im.ctx, fmt.Sprintf("deg: %v, moveleft: %v, movedown: %v", deg, moveleft, movedown))
    rad := deg * math.Pi / 180
    cos, sin := math.Cos(rad), math.Sin(rad)
    return f64.Aff3{
        +cos, -sin, moveleft,
        +sin, +cos, movedown,
    }
}

func (im *imageCreator) jpegOption() *jpeg.Options {
    switch im.option.Quality {
    case 1:
        return &jpeg.Options{Quality: 75}
    case 2:
        return &jpeg.Options{Quality: 85}
    case 3:
        return &jpeg.Options{Quality: 90}
    case 4:
        return &jpeg.Options{Quality: 100}
    default:
        return &jpeg.Options{Quality: 85}
    }
}

func (im *imageCreator) decodeExifOrientation(src io.ReadSeeker) {
    if _, err := src.Seek(0, io.SeekStart); err != nil {
        log.Debug(im.ctx, fmt.Sprintf("reader seek 0 error %v", err.Error()))
        return
    }
    decodedExif, err := exif.Decode(src)
    if err != nil {
        log.Debug(im.ctx, fmt.Sprintf("exif decode error %v", err.Error()))
        return
    }
    orientation, err := decodedExif.Get(exif.Orientation)
    if err != nil {
        log.Debug(im.ctx, fmt.Sprintf("exif orientation error %v", err.Error()))
        return
    }
    orientationvVal, err := orientation.Int(0)
    if err != nil {
        log.Debug(im.ctx, fmt.Sprintf("exif orientation int error %v", err.Error()))
        return
    }
    im.exifOrientation = orientationvVal
    log.Debug(im.ctx, fmt.Sprintf("exif orientation %v", im.exifOrientation))
}