lamium/image/image.go

365 lines
11 KiB
Go

// 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
}