// Package image uses the official Go image/draw library to provide more options // for image downscaling. It includes functions related to the supported image // formats. package image import ( "fmt" "image" "image/gif" "image/jpeg" "image/png" "io" "math" "os" "path/filepath" "slices" "strings" "golang.org/x/image/draw" util "lamium/util" ) var ( // Error messages. err = struct { imageNotCreated string imageNotLoaded string imageNotDecoded string imageNotEncoded string }{ imageNotCreated: "Error: new image not created.", imageNotLoaded: "Error: image could not be loaded:", imageNotDecoded: "Error: image could not be decoded:", imageNotEncoded: "Error: image could not be encoded:", } // Supported image extensions. imageExt = map[string][]string{ "gif": {".gif", ".GIF"}, "jpg": {".jpeg", ".jpg", ".JPEG", ".JPG"}, "png": {".png", ".PNG"}, } // Image information messages. msg = struct { debugSrcImg string debugSrcPath string debugNewImg string imgResize string }{ debugSrcImg: "Source image size:", debugSrcPath: "Image:", debugNewImg: "New image size:", imgResize: "Resizing", } ) // IsSupported checks an image's file extension returns true if it is one of the // supported extensions. func IsSupported(path string) bool { var exts []string for _, s := range imageExt { for _, v := range s { exts = append(exts, v) } } if slices.Contains(exts, filepath.Ext(path)) { return true } else { return false } } // CopyImages copies image files from srcPath to destPath, overwriting files at // the destination if overwrite is set to true. func CopyImages(srcPath, destPath string, overwrite bool) { files := util.GetFileList(srcPath, "name", false) for _, file := range files { destFile := filepath.Join(destPath, filepath.Base(file)) if (!util.FileExists(destFile) || (util.FileExists(destFile) && overwrite)) && IsSupported(file) { util.CopyFile(file, destFile) } } } // GetImageList returns a slice of strings of image files in srcPath. // // For sortOrder options, see util.GetFileList. func GetImageList(srcPath, sortOrder string, includeDirs bool) []string { files := util.GetFileList(srcPath, sortOrder, includeDirs) var list []string for _, file := range files { if IsSupported(file) { list = append(list, file) } } return list } // DecodeImage returns an image.Image object from an image path. func DecodeImage(path string) image.Image { var ( img image.Image decErr error ) srcFile, openErr := os.Open(filepath.FromSlash(path)) defer srcFile.Close() if !util.HasError(openErr, err.imageNotLoaded+" "+path) { if slices.Contains(imageExt["jpg"], filepath.Ext(path)) { img, decErr = jpeg.Decode(srcFile) } else if slices.Contains(imageExt["png"], filepath.Ext(path)) { img, decErr = png.Decode(srcFile) } else if slices.Contains(imageExt["gif"], filepath.Ext(path)) { img, decErr = gif.Decode(srcFile) } else { util.HasError(decErr, err.imageNotDecoded+" "+path) } } util.HasError(decErr, err.imageNotDecoded+" "+path) return img } // EncodeImage writes an image.Image object as image file path. func EncodeImage(path string, writer io.Writer, img image.Image) { var encErr error if slices.Contains(imageExt["jpg"], filepath.Ext(path)) { encErr = jpeg.Encode(writer, img, nil) } else if slices.Contains(imageExt["png"], filepath.Ext(path)) { encErr = png.Encode(writer, img) } else if slices.Contains(imageExt["gif"], filepath.Ext(path)) { encErr = gif.Encode(writer, img, nil) } else { util.HasError(encErr, err.imageNotEncoded+" "+path) } util.HasError(encErr, err.imageNotEncoded+" "+path) } // CalcImageSize returns the new width and height of image.Image srcImg // based on pct, clip and unifyMode. // // pct is percentage of the original size, an int value between 1-99 // inclusively. // // clip options: square, rect. // // unifyMode options with example scenarios if width is 200 and height is 100: // // maxSize: images are the maximum size at the longer side. // An image in landscape would be 200px in width and less in height. // An image in portrait would have a height of 200px, less in width. // A square image would have a width and height of 200px. // // minSize: images are the minimum size at the shorter side. // An image in landscape would be 100px in height and more in width. // An image in portrait would have a width of 100px, more in height. // A square image would have a width and height of 100px. // // width: images have the same width. // An image in landscape would be 200px wide, less in height. // Animage in portrait would be 200px wide, more in height // A square image would have a width and height of 200px. // // height: images have the same height. // An image in landscape would be 100px high and wider than 100px. // An image in portrait would be 100px high, and narrower than 100px. // A square image would have a width and height of 100px. // // none: images are sized at a percentage of the original image regardless of // set width and height. The current default is 50% of the original image // dimensions, e.g. a 600 x 800px source image yields a new 300 x 400px image. func CalcImageSize(srcImg image.Image, width, height, pct int, clip string, unifyMode string) (int, int) { // Constrain width and height to source dimensions. w, h := width, height if w > srcImg.Bounds().Dx() || w < 0 { w = srcImg.Bounds().Dx() } if h > srcImg.Bounds().Dy() || h < 0 { h = srcImg.Bounds().Dy() } // Get source aspect ratio. minSrcSize, maxSrcSize := util.MinMax(srcImg.Bounds().Dx(), srcImg.Bounds().Dy()) var ratio = float64(minSrcSize) / float64(maxSrcSize) switch unifyMode { case "maxSize": // Use the higher value of width/height as maximum size. _, maxSize := util.MinMax(w, h) if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() { w = maxSize h = int(math.Round(float64(w) * ratio)) } else if srcImg.Bounds().Dy() > srcImg.Bounds().Dx() { w = int(math.Round(float64(maxSize) * ratio)) h = maxSize } else { w, h = maxSize, maxSize } case "minSize": // Use the lower value of width/height as minimum size. minSize, _ := util.MinMax(w, h) if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() { w = int(math.Round(float64(srcImg.Bounds().Dx()) / (float64(srcImg.Bounds().Dy()) / float64(h)))) h = minSize } else if srcImg.Bounds().Dx() < srcImg.Bounds().Dy() { w = minSize h = int(math.Round(float64(srcImg.Bounds().Dy()) / (float64(srcImg.Bounds().Dx()) / float64(w)))) } else { w, h = minSize, minSize } case "width": // Use width as baseline for aspect ratio. if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() { h = int(math.Round(float64(srcImg.Bounds().Dy()) / (float64(srcImg.Bounds().Dx()) / float64(w)))) } else { h = w } case "height": // Use height as baseline for aspect ratio. if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() { w = int(math.Round(float64(srcImg.Bounds().Dx()) / (float64(srcImg.Bounds().Dy()) / float64(h)))) } else { w = h } case "none": // Derive values as a percentage of source image size. pc := pct if pct > 0 || pct < 100 { pc = 50 } if srcImg.Bounds().Dx() > srcImg.Bounds().Dy() { w = int(math.Round(float64(srcImg.Bounds().Dx()) * (float64(pc) / 100))) h = int(math.Round(float64(srcImg.Bounds().Dx()) * ratio * (float64(pc) / 100))) } else { w = int(math.Round(float64(srcImg.Bounds().Dy()) * ratio * (float64(pc) / 100))) h = int(math.Round(float64(srcImg.Bounds().Dy()) * (float64(pc) / 100))) } } if clip == "square" { switch unifyMode { case "minSize": w, _ = util.MinMax(w, h) h = w case "maxSize", "none": _, w = util.MinMax(w, h) h = w case "width": h = w case "height": w = h } } return w, h } // CalcClipSize returns the new width and height as well as an image.Rectangle // object for image.Image srcImg based on a set width and height and clip mode // (square or rect). func CalcClipSize(srcImg image.Image, width, height int, clip string) (int, int, image.Rectangle) { var w, h int var srcBounds image.Rectangle if clip == "square" { // Center the square on rectangular images. // Use max length for new image size if width and height are // different. _, w = util.MinMax(width, height) h = w // Get the shorter side. srcMin, _ := util.MinMax(srcImg.Bounds().Dx(), srcImg.Bounds().Dy()) x1, y1 := 0, 0 x2, y2 := srcMin, srcMin // Shift the start and end values on the longer side of the // source image's image.Rectangle. if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() && srcMin == srcImg.Bounds().Dx() { y1 = int(math.Round(float64(srcImg.Bounds().Dy()- srcMin) / float64(2))) y2 = int(srcMin + y1) } else if srcImg.Bounds().Dx() != srcImg.Bounds().Dy() && srcMin == srcImg.Bounds().Dy() { x1 = int(math.Round(float64(srcImg.Bounds().Dx()- srcMin) / float64(2))) x2 = int(srcMin + x1) } srcBounds = image.Rect(x1, y1, x2, y2) } else { w, h = width, height srcBounds = srcImg.Bounds() } return w, h, srcBounds } // ResizeImage creates a new downscaled image at destPath from source image // srcPath. // // For more details about the pct, clip, and unifyMode parameters, see // CalcImageSize. func ResizeImage(srcPath, destPath string, width, height, pct int, clip, unifyMode string, debug ...bool) { util.MakeDir(filepath.Dir(destPath), true) var destFile *os.File var createErr error if IsSupported(srcPath) { destFile, createErr = os.Create(filepath.FromSlash(destPath)) defer destFile.Close() } if !util.HasError(createErr, err.imageNotCreated) && IsSupported(srcPath) { w, h := width, height var srcBounds image.Rectangle srcImg := DecodeImage(srcPath) w, h = CalcImageSize(srcImg, w, h, pct, clip, unifyMode) w, h, srcBounds = CalcClipSize(srcImg, w, h, clip) destImg := image.NewRGBA(image.Rect(0, 0, w, h)) draw.BiLinear.Scale(destImg, destImg.Rect, srcImg, srcBounds, draw.Over, nil) EncodeImage(destPath, destFile, destImg) if slices.Contains(debug, true) { fmt.Println(msg.debugSrcPath, srcPath) fmt.Println(msg.debugSrcImg, srcImg.Bounds().Dx(), srcImg.Bounds().Dy()) fmt.Println(msg.debugNewImg, w, h) } } } // ResizeImages creates multiple downscaled images at directory destPath from // source images at directory srcPath. func ResizeImages(srcPath, destPath string, width, height, pct int, clip string, unifyMode string, overwrite bool, debug ...bool) { files := util.GetFileList(srcPath, "name", false) debugOn := false if slices.Contains(debug, true) { debugOn = true } for _, file := range files { destImg := filepath.Join(destPath, filepath.Base(file)) if !util.FileExists(destImg) || (util.FileExists(destImg) && overwrite) { ResizeImage(file, destImg, width, height, pct, clip, unifyMode, debugOn) } } } // GetImageTitle returns a title string based on the image filename in path. func GetImageTitle(path string) string { repChars := map[string]string{"-": " ", "_": " "} str := strings.Replace(util.MultiReplace(filepath.Base(path), repChars), filepath.Ext(path), "", 1) return str }