package imageext import ( "bytes" "fmt" "github.com/disintegration/imaging" "gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/mathext" "image" "image/color" "image/draw" "image/jpeg" "image/png" "io" "math" ) type ImageFit string //@enum:type const ( ImageFitStretch ImageFit = "STRETCH" ImageFitCover ImageFit = "COVER" ImageFitContainCenter ImageFit = "CONTAIN_CENTER" ImageFitContainTopLeft ImageFit = "CONTAIN_TOPLEFT" ImageFitContainTopRight ImageFit = "CONTAIN_TOPRIGHT" ImageFitContainBottomLeft ImageFit = "CONTAIN_BOTTOMLEFT" ImageFitContainBottomRight ImageFit = "CONTAIN_BOTTOMRIGHT" ) type ImageCrop struct { // all crop values are percentages! CropX float64 `bson:"cropX" json:"cropX"` CropY float64 `bson:"cropY" json:"cropY"` CropWidth float64 `bson:"cropWidth" json:"cropWidth"` CropHeight float64 `bson:"cropHeight" json:"cropHeight"` } type ImageCompresson string //@enum:type const ( CompressionPNGNone ImageCompresson = "PNG_NONE" CompressionPNGSpeed ImageCompresson = "PNG_SPEED" CompressionPNGBest ImageCompresson = "PNG_BEST" CompressionJPEG100 ImageCompresson = "JPEG_100" CompressionJPEG90 ImageCompresson = "JPEG_090" CompressionJPEG80 ImageCompresson = "JPEG_080" CompressionJPEG70 ImageCompresson = "JPEG_070" CompressionJPEG60 ImageCompresson = "JPEG_060" CompressionJPEG50 ImageCompresson = "JPEG_050" CompressionJPEG25 ImageCompresson = "JPEG_025" CompressionJPEG10 ImageCompresson = "JPEG_010" CompressionJPEG1 ImageCompresson = "JPEG_001" ) func CropImage(img image.Image, px float64, py float64, pw float64, ph float64) (image.Image, error) { type subImager interface { SubImage(r image.Rectangle) image.Image } x := int(float64(img.Bounds().Dx()) * px) y := int(float64(img.Bounds().Dy()) * py) w := int(float64(img.Bounds().Dx()) * pw) h := int(float64(img.Bounds().Dy()) * ph) if simg, ok := img.(subImager); ok { return simg.SubImage(image.Rect(x, y, x+w, y+h)), nil } else { bfr1 := bytes.Buffer{} err := png.Encode(&bfr1, img) if err != nil { return nil, exerr.Wrap(err, "").Build() } imgPNG, err := png.Decode(&bfr1) if err != nil { return nil, exerr.Wrap(err, "").Build() } return imgPNG.(subImager).SubImage(image.Rect(x, y, w+w, y+h)), nil } } func EncodeImage(img image.Image, compression ImageCompresson) (bytes.Buffer, string, error) { var err error bfr := bytes.Buffer{} switch compression { case CompressionPNGNone: enc := &png.Encoder{CompressionLevel: png.NoCompression} err = enc.Encode(&bfr, img) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/png", nil case CompressionPNGSpeed: enc := &png.Encoder{CompressionLevel: png.BestSpeed} err = enc.Encode(&bfr, img) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/png", nil case CompressionPNGBest: enc := &png.Encoder{CompressionLevel: png.BestCompression} err = enc.Encode(&bfr, img) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/png", nil case CompressionJPEG100: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 100}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG90: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 90}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG80: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 80}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG70: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 70}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG60: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 60}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG50: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 50}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG25: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 25}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG10: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 10}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil case CompressionJPEG1: err = jpeg.Encode(&bfr, img, &jpeg.Options{Quality: 1}) if err != nil { return bytes.Buffer{}, "", exerr.Wrap(err, "").Build() } return bfr, "image/jpeg", nil default: return bytes.Buffer{}, "", exerr.New(exerr.TypeInternal, "unknown compression method: "+compression.String()).Build() } } func ObjectFitImage(img image.Image, bbw float64, bbh float64, fit ImageFit, fillColor color.Color) (image.Image, PercentageRectangle, error) { iw := img.Bounds().Size().X ih := img.Bounds().Size().Y // [iw, ih] is the size of the image // [bbw, bbh] is the target bounding box, // - it specifies the target ratio // - and the maximal target resolution facW := float64(iw) / bbw facH := float64(ih) / bbh // facW is the ratio between iw and bbw // - it is the factor by which the bounding box must be multiplied to reach the image size (in the x-axis) // // (same is true for facH, but for the height and y-axis) if fit == ImageFitCover { // image-fit:cover completely fills the target-bounding-box, it potentially cuts parts of the image away // we use the smaller (!) value of facW and facH, because we want to have the smallest possible destination rect (due to file size) // and because the image is made to completely fill the bounding-box, the smaller factor (= teh dimension the image is stretched more) is relevant // but we cap `fac` at 1 (can be larger than 1) // a value >1 would mean the final image resolution is biger than the bounding box, which we do not want. // if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1 // which means we will simply use the bounding box as destination rect (and scale the image down) fac := mathext.Clamp(mathext.Min(facW, facH), 0.0, 1.0) // we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio) w := int(math.Round(bbw * fac)) h := int(math.Round(bbh * fac)) img = imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos) newImg := image.NewRGBA(image.Rect(0, 0, w, h)) draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src) draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over) return newImg, PercentageRectangle{0, 0, 1, 1}, nil } if fit == ImageFitContainCenter || fit == ImageFitContainTopLeft || fit == ImageFitContainTopRight || fit == ImageFitContainBottomLeft || fit == ImageFitContainBottomRight { // image-fit:contain fills the target-bounding-box with the image, there is potentially empty-space, it potentially cuts parts of the image away // we use the bigger (!) value of facW and facH, // because the image is made to fit the bounding-box, the bigger factor (= the dimension the image is stretched less) is relevant // but we cap `fac` at 1 (can be larger than 1) // a value >1 would mean the final image resolution is biger than the bounding box, which we do not want. // if the initial image (iw, ih) is already bigger than the bounding box (bbw, bbh), facW and facH are always >1 and fac will be 1 // which means we will simply use the bounding box as destination rect (and scale the image down) facOut := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0) // we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio) // [ow|oh] ==> size of output image (same ratio as bounding box [bbw|bbh]) ow := int(math.Round(bbw * facOut)) oh := int(math.Round(bbh * facOut)) facScale := mathext.Min(float64(ow)/float64(iw), float64(oh)/float64(ih)) // [dw|dh] ==> size of destination rect (where to draw source in output image) (same ratio as input image [iw|ih]) dw := int(math.Round(float64(iw) * facScale)) dh := int(math.Round(float64(ih) * facScale)) img = imaging.Resize(img, dw, dh, imaging.Lanczos) var destBounds image.Rectangle if fit == ImageFitContainCenter { destBounds = image.Rect((ow-dw)/2, (oh-dh)/2, (ow-dw)/2+dw, (oh-dh)/2+dh) } else if fit == ImageFitContainTopLeft { destBounds = image.Rect(0, 0, dw, dh) } else if fit == ImageFitContainTopRight { destBounds = image.Rect(ow-dw, 0, ow, dh) } else if fit == ImageFitContainBottomLeft { destBounds = image.Rect(0, oh-dh, dw, oh) } else if fit == ImageFitContainBottomRight { destBounds = image.Rect(ow-dw, oh-dh, ow, oh) } newImg := image.NewRGBA(image.Rect(0, 0, ow, oh)) draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src) draw.Draw(newImg, destBounds, img, image.Pt(0, 0), draw.Over) return newImg, calcRelativeRect(destBounds, newImg.Bounds()), nil } if fit == ImageFitStretch { // image-fit:stretch simply stretches the image to the bounding box // we use the bigger value of [facW;facH], to (potentially) scale the bounding box down before applying it // theoretically we could directly use [bbw, bbh] in the call to imaging.Resize, // but if the image is (a lot) smaller than the bouding box it is useful to scale it down to reduce final pdf filesize // we also cap fac at 1, because we never want the final rect to be bigger than the inputted bounding box (see comments at start of method) fac := mathext.Clamp(mathext.Max(facW, facH), 0.0, 1.0) // we scale the bounding box by fac (both dimension the same amount, to keep the bounding-box ratio) w := int(math.Round(bbw * fac)) h := int(math.Round(bbh * fac)) img = imaging.Resize(img, w, h, imaging.Lanczos) newImg := image.NewRGBA(image.Rect(0, 0, w, h)) draw.Draw(newImg, newImg.Bounds(), &image.Uniform{C: fillColor}, image.Pt(0, 0), draw.Src) draw.Draw(newImg, newImg.Bounds(), img, image.Pt(0, 0), draw.Over) return newImg, PercentageRectangle{0, 0, 1, 1}, nil } return nil, PercentageRectangle{}, exerr.New(exerr.TypeInternal, fmt.Sprintf("unknown image-fit: '%s'", fit)).Build() } func VerifyAndDecodeImage(data io.Reader, mime string) (image.Image, error) { if mime == "image/jpeg" { img, err := jpeg.Decode(data) if err != nil { return nil, exerr.Wrap(err, "failed to decode blob as jpeg").WithType(exerr.TypeInvalidImage).Build() } return img, nil } if mime == "image/png" { img, err := png.Decode(data) if err != nil { return nil, exerr.Wrap(err, "failed to decode blob as png").WithType(exerr.TypeInvalidImage).Build() } return img, nil } return nil, exerr.New(exerr.TypeInvalidMimeType, fmt.Sprintf("unknown/invalid image mimetype: '%s'", mime)).Build() }