365 lines
11 KiB
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
|
|
}
|