// imagery provides functions to read, edit and export images package imagery import "seekia/internal/encoding" import "seekia/internal/helpers" import "seekia/internal/appValues" import "seekia/internal/localFilesystem" import "github.com/disintegration/gift" import "github.com/srwiley/oksvg" import "github.com/srwiley/rasterx" import chaiWebp "github.com/chai2010/webp" import goJpeg "image/jpeg" import goWebp "golang.org/x/image/webp" import goPng "image/png" import goFilepath "path/filepath" import "image" import "image/color" import "image/draw" import "strings" import "bytes" import "errors" // Image can be either jpeg, jpg, webp or png //Outputs: // -bool: File found // -bool: Able to read image // -image.Image: golang image // -error func ReadImageFile(filepath string) (bool, bool, image.Image, error){ exists, fileBytes, err := localFilesystem.GetFileContents(filepath) if (err != nil) { return false, false, nil, err } if (exists == false){ return false, false, nil, nil } filename := goFilepath.Base(filepath) filenameLowercase := strings.ToLower(filename) isJPEG := strings.HasSuffix(filenameLowercase, "jpeg") isJPG := strings.HasSuffix(filenameLowercase, "jpg") if (isJPEG == true || isJPG == true){ imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes) if (err == nil) { return true, true, imageObject, nil } } isPNG := strings.HasSuffix(filenameLowercase, "png") if (isPNG == true){ imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes) if (err == nil) { return true, true, imageObject, nil } } isWEBP := strings.HasSuffix(filenameLowercase, "webp") if (isWEBP == true){ imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes) if (err == nil){ return true, true, imageObject, nil } } // File extention is unknown, or it does not correspond to the image file's true format // We will try every method we haven't already tried. if (isJPEG == false && isJPG == false){ imageObject, err := ConvertJPEGImageFileBytesToGolangImage(fileBytes) if (err == nil) { return true, true, imageObject, nil } } if (isPNG == false){ imageObject, err := ConvertPNGImageFileBytesToGolangImage(fileBytes) if (err == nil) { return true, true, imageObject, nil } } if (isWEBP == false){ imageObject, err := ConvertWEBPImageFileBytesToGolangImage(fileBytes) if (err == nil){ return true, true, imageObject, nil } } return true, false, nil, nil } func ConvertJPEGImageFileBytesToGolangImage(input []byte)(image.Image, error){ bytesBuffer := bytes.NewBuffer(input) imageObject, err := goJpeg.Decode(bytesBuffer) if (err != nil) { return nil, err } return imageObject, nil } func ConvertPNGImageFileBytesToGolangImage(input []byte)(image.Image, error){ bytesBuffer := bytes.NewBuffer(input) imageObject, err := goPng.Decode(bytesBuffer) if (err != nil) { return nil, err } return imageObject, nil } func ConvertWEBPImageFileBytesToGolangImage(input []byte)(image.Image, error){ bytesBuffer := bytes.NewBuffer(input) imageObject, err := goWebp.Decode(bytesBuffer) if (err != nil) { return nil, err } return imageObject, nil } // This function only works for square images func ConvertSVGImageFileBytesToGolangImage(inputBytes []byte)(image.Image, error){ //TODO: Use a different svg package, because oksvg cannot render many complex openmoji icons fileReader := bytes.NewReader(inputBytes) svgIconObject, err := oksvg.ReadIconStream(fileReader, oksvg.StrictErrorMode) if (err != nil) { return nil, err } svgWidth := int(svgIconObject.ViewBox.W) svgHeight := int(svgIconObject.ViewBox.H) svgIconObject.SetTarget(0, 0, 400, 400) imageObject := image.NewRGBA(image.Rect(0, 0, 400, 400)) scannerGV := rasterx.NewScannerGV(svgWidth, svgHeight, imageObject, imageObject.Bounds()) raster := rasterx.NewDasher(400, 400, scannerGV) svgIconObject.Draw(raster, 1) return imageObject, nil } // This function creates a 1 pixel image of a provided color func GetColorSquare(colorCode string)(image.Image, error){ colorObject, err := GetColorObjectFromColorCode(colorCode) if (err != nil){ return nil, errors.New("GetColorSquare called with invalid color code: " + colorCode) } imageRectangle := image.Rect(0, 0, 1, 1) imageObject := image.NewRGBA(imageRectangle) imageObject.Set(0, 0, colorObject) return imageObject, nil } // Example color codes: // -Black: "000000" // -White: "ffffff" // -Blue: "0000ff" // -Red: "ff0000" func GetColorObjectFromColorCode(colorCode string)(color.Color, error){ colorCodeBytes, err := encoding.DecodeHexStringToBytes(colorCode) if (err != nil) { return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode) } if (len(colorCodeBytes) != 3){ return nil, errors.New("GetColorObjectFromColorCode called with invalid color code: " + colorCode) } colorRed := colorCodeBytes[0] colorGreen := colorCodeBytes[1] colorBlue := colorCodeBytes[2] // Color Alpha (opacity) // It is always ff, which represents 100% opacity colorAlpha := uint8(0xff) colorObject := color.RGBA{colorRed, colorGreen, colorBlue, colorAlpha} return colorObject, nil } // This converts a profile/message webp base64 image string to a viewable, cropped image // The cropping adds transparent bars so it will conform to a reasonable ratio // The images are encoded into profiles/messages without the bars to save space func ConvertWEBPBase64StringToCroppedDownsizedImageObject(base64Input string)(image.Image, error){ imageObject, err := ConvertWebpBase64StringToImageObject(base64Input) if (err != nil) { return nil, err } croppedImage, err := cropGolangImageToMaximumRatio(imageObject) if (err != nil) { return nil, err } maximumSideLength := appValues.GetStandardImageMaximumSideLength() downsizedImage, err := DownsizeGolangImage(croppedImage, maximumSideLength) if (err != nil){ return nil, err } return downsizedImage, nil } func ConvertWebpBase64StringToImageObject(base64Input string)(image.Image, error){ imageBytes, err := encoding.DecodeBase64StringToBytes(base64Input) if (err != nil) { return nil, err } imageObject, err := ConvertWEBPImageFileBytesToGolangImage(imageBytes) if (err != nil) { return nil, err } return imageObject, nil } func GetImageWidthAndHeightPixels(inputImage image.Image)(int, int, error){ if (inputImage == nil) { return 0, 0, errors.New("GetImageWidthAndHeightPixels called with nil image.") } minX := inputImage.Bounds().Min.X maxX := inputImage.Bounds().Max.X minY := inputImage.Bounds().Min.Y maxY := inputImage.Bounds().Max.Y width := maxX-minX height := maxY-minY if (width <= 0 || height <= 0) { return 0, 0, errors.New("Failed to derive image width and height.") } return width, height, nil } // This will downscale an image to the maximum side length // If the image is already small enough, it will do nothing // It preserves aspect ratio func DownsizeGolangImage(inputImage image.Image, maximumSideLength int)(image.Image, error){ inputImageWidth, inputImageHeight, err := GetImageWidthAndHeightPixels(inputImage) if (err != nil) { return nil, err } if (inputImageWidth <= maximumSideLength && inputImageHeight <= maximumSideLength) { return inputImage, nil } resizedImage, err := ResizeGolangImage(inputImage, maximumSideLength) if (err != nil) { return nil, err } return resizedImage, nil } //This function can upscale or downscale an image. It preserves aspect ratio. func ResizeGolangImage(inputImage image.Image, maximumSideLength int) (image.Image, error){ if (inputImage == nil) { return nil, errors.New("ResizeGolangImage called with nil image.") } imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(inputImage) if (err != nil) { return nil, err } if (imageWidth == 0 || imageHeight == 0){ return nil, errors.New("ResizeGolangImage called with input image of zero width and length.") } if (maximumSideLength <= 0){ return nil, errors.New("ResizeGolangImage called with invalid maximumSideLength") } //Outputs: // -int: New width // -int: New height // -error getResizedImageWidthAndHeight := func()(int, int, error){ if (imageWidth == imageHeight){ return maximumSideLength, maximumSideLength, nil } // oldWidth/oldHeight = newWidth/newHeight // We set either newWidth or newHeight to maximumSideLength // Then we solve for either newHeight or newWidth if (imageWidth > imageHeight) { newHeight := float64(maximumSideLength) * (float64(imageHeight)/float64(imageWidth)) newHeightInt, err := helpers.FloorFloat64ToInt(newHeight) if (err != nil) { return 0, 0, err } return maximumSideLength, newHeightInt, nil } newWidth := float64(maximumSideLength) * (float64(imageWidth)/float64(imageHeight)) newWidthInt, err := helpers.FloorFloat64ToInt(newWidth) if (err != nil) { return 0, 0, err } return newWidthInt, maximumSideLength, nil } newWidth, newHeight, err := getResizedImageWidthAndHeight() if (err != nil) { return nil, err } rectangle := image.Rect(0, 0, newWidth, newHeight) resizedImage := image.NewRGBA(rectangle) giftResizeFilterList := gift.New(gift.Resize(newWidth, newHeight, gift.LanczosResampling)) giftResizeFilterList.Draw(resizedImage, inputImage) return resizedImage, nil } // This function crops image to maximum allowed aspect ratio. // It adds transparent space to top/bottom or left/right of image. // (Smaller Side)/(Larger side) must be > 0.7 func cropGolangImageToMaximumRatio(inputImage image.Image)(image.Image, error){ if (inputImage == nil) { return nil, errors.New("cropGolangImageToMaximumRatio called with nil image.") } widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage) if (err != nil) { return nil, err } shorterSide := min(widthPixels, heightPixels) longerSide := max(widthPixels, heightPixels) currentImageRatio := float64(shorterSide)/float64(longerSide) if (currentImageRatio > 0.7) { //Image does not exceed maximum allowed ratio, image does not need cropping. return inputImage, nil } // We must increase the length of the smaller side so that the image has a ratio of .7 // smaller/larger = .7 // smaller = (.7)*(larger) getNewWidthAndHeight := func()(int, int, error){ minimumRatio := float64(0.7) if (widthPixels > heightPixels){ newHeightFloat := float64(widthPixels) * minimumRatio newHeight, err := helpers.CeilFloat64ToInt(newHeightFloat) if (err != nil) { return 0, 0, err } return widthPixels, newHeight, nil } newWidthFloat := float64(heightPixels) * minimumRatio newWidth, err := helpers.CeilFloat64ToInt(newWidthFloat) if (err != nil) { return 0, 0, err } return newWidth, heightPixels, nil } newWidthPixels, newHeightPixels, err := getNewWidthAndHeight() if (err != nil) { return nil, err } newLongerSideLength := max(newWidthPixels, newHeightPixels) resizedImage, err := ResizeGolangImage(inputImage, newLongerSideLength) if (err != nil) { return nil, err } resizedImageWidth, resizedImageHeight, err := GetImageWidthAndHeightPixels(resizedImage) if (err != nil) { return nil, err } croppedImageRectangle := image.Rect(0, 0, newWidthPixels, newHeightPixels) croppedImage := image.NewRGBA(croppedImageRectangle) // We find the coordinates of the top left corner of the image we are placing within our new image xAxisPlacementPoint := -((newWidthPixels - resizedImageWidth)/2) yAxisPlacementPoint := -((newHeightPixels - resizedImageHeight)/2) croppedImagePlacementPoint := image.Point{ X: xAxisPlacementPoint, Y: yAxisPlacementPoint, } draw.Draw(croppedImage, croppedImageRectangle, resizedImage, croppedImagePlacementPoint, draw.Over) return croppedImage, nil } // Performs downsizing and compression to maximum standard size func ConvertImageObjectToStandardWebpBase64String(inputImage image.Image)(string, error){ if (inputImage == nil) { return "", errors.New("ConvertImageObjectToStandardWebpBase64String called with nil image.") } maximumSideLength := appValues.GetStandardImageMaximumSideLength() sideLength := maximumSideLength for sideLength > 1 { resizedImage, err := ResizeGolangImage(inputImage, sideLength) if (err != nil) { return "", err } imageMaximumBytes := appValues.GetStandardImageMaximumBytes() imageQuality := float32(100) for { bytesBuffer := new(bytes.Buffer) webpOptions := &chaiWebp.Options{ Lossless: false, Quality: imageQuality, } err := chaiWebp.Encode(bytesBuffer, resizedImage, webpOptions) if (err != nil) { return "", err } if (&bytesBuffer == nil) { return "", errors.New("Nil image buffer after chaiWebp.Encode()") } imageBytes := bytesBuffer.Bytes() if (len(imageBytes) == 0){ return "", errors.New("Empty image buffer after chaiWebp.Encode()") } imageSize := len(imageBytes) if (imageSize < imageMaximumBytes){ imageBase64String := encoding.EncodeBytesToBase64String(imageBytes) return imageBase64String, nil } if (imageQuality > 10){ imageQuality -= 10 } else if (imageQuality > 4){ imageQuality -= 1 } else { // imageQuality is <= 4 // We cannot compress the image while retaining good quality at these dimensions // We will downsize the image by 100 pixels and try again sideLength -= 100 break } } } return "", errors.New("Failed to create standard webp image.") } func PixelateGolangImage(inputImage image.Image, amount0to100 int)(image.Image, error){ if (inputImage == nil) { return nil, errors.New("PixelateGolangImage called with nil image.") } if (amount0to100 < 0 || amount0to100 > 100) { return nil, errors.New("PixelateGolangImage called with input not between 0 and 100.") } if (amount0to100 == 0) { return inputImage, nil } widthPixels, heightPixels, err := GetImageWidthAndHeightPixels(inputImage) if (err != nil) { return nil, err } longerSideLength := max(widthPixels, heightPixels) pixelationAmountInt, err := helpers.ScaleNumberProportionally(true, amount0to100, 0, 100, 0, longerSideLength/5) if (err != nil) { return nil, err } rectangle := image.Rect(0, 0, widthPixels, heightPixels) pixelatedImage := image.NewRGBA(rectangle) giftPixelateFilterList := gift.New(gift.Pixelate(pixelationAmountInt)) giftPixelateFilterList.Draw(pixelatedImage, inputImage) return pixelatedImage, nil } // This will verify an image conforms to Seekia's image size and ratio requirements // All images in profiles and messages must conform to these requirements //Outputs: // -bool: Image is valid // -error func VerifyStandardImageBytes(inputBytes []byte)(bool, error){ imageMaximumBytes := appValues.GetStandardImageMaximumBytes() if (len(inputBytes) > imageMaximumBytes){ return false, nil } imageObject, err := ConvertWEBPImageFileBytesToGolangImage(inputBytes) if (err != nil) { return false, nil } imageWidth, imageHeight, err := GetImageWidthAndHeightPixels(imageObject) if (err != nil) { return false, errors.New("ConvertWEBPImageFileBytesToGolangImage returning image with invalid width and height: " + err.Error()) } maximumAllowedSideLength := appValues.GetStandardImageMaximumSideLength() if (imageWidth > maximumAllowedSideLength || imageHeight > maximumAllowedSideLength){ return false, nil } return true, nil }