You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
5.2 KiB
203 lines
5.2 KiB
package main
|
|
|
|
import (
|
|
"os"
|
|
"errors"
|
|
"io/fs"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"image"
|
|
"log"
|
|
"regexp"
|
|
"image/png"
|
|
"github.com/disintegration/gift"
|
|
"lcthw.dev/go/jankifier/filters"
|
|
)
|
|
|
|
func LoadImage(filename string) image.Image {
|
|
reader, err := os.Open(filename)
|
|
if err != nil { log.Fatal(err) }
|
|
defer reader.Close()
|
|
|
|
img, _, err := image.Decode(reader)
|
|
if err != nil { log.Fatal(err) }
|
|
|
|
return img
|
|
}
|
|
|
|
func SaveImage(filename string, img image.Image) {
|
|
out, err := os.Create(filename)
|
|
if err != nil {
|
|
log.Fatalf("can't write file %s: %v", filename, err)
|
|
}
|
|
defer out.Close()
|
|
|
|
err = png.Encode(out, img)
|
|
if err != nil {
|
|
log.Fatalf("can't png encode %s: %v", filename, err)
|
|
}
|
|
}
|
|
|
|
func AdjustDim(src_dim int, setting_dim int) int {
|
|
// BUG: should use the aspect ratio
|
|
if setting_dim == 0 {
|
|
return src_dim
|
|
} else {
|
|
return setting_dim
|
|
}
|
|
}
|
|
|
|
func JankImage(settings Conversion, in_file string, out_file string) {
|
|
src := LoadImage(in_file)
|
|
src_bounds := src.Bounds()
|
|
var target_bounds image.Rectangle
|
|
|
|
target_bounds.Max.X = AdjustDim(src_bounds.Max.X, settings.Width)
|
|
target_bounds.Max.Y = AdjustDim(src_bounds.Max.Y, settings.Height)
|
|
|
|
fmt.Println("final size is: ", target_bounds.Max.X, target_bounds.Max.Y)
|
|
|
|
// BUG: use the shortest dimension for the pixelate, or add a framesize setting
|
|
resize := gift.Resize(src_bounds.Max.X / settings.PixelWidth, 0, gift.NearestNeighborResampling)
|
|
// posterize := filters.Posterize(uint16(settings.ColorDepth), settings.DitherType)
|
|
upscale := filters.Upscale(src_bounds, settings.PixelWidth)
|
|
final_size := gift.Resize(target_bounds.Max.X, target_bounds.Max.Y, gift.NearestNeighborResampling)
|
|
sharpen := gift.UnsharpMask(1, 1, 0)
|
|
|
|
var g *gift.GIFT
|
|
|
|
if target_bounds.Max.X != src_bounds.Max.X || target_bounds.Max.Y != src_bounds.Max.Y {
|
|
g = gift.New(resize, upscale, sharpen, final_size)
|
|
} else {
|
|
g = gift.New(resize, upscale, sharpen)
|
|
}
|
|
|
|
out_img := image.NewNRGBA(g.Bounds(target_bounds))
|
|
g.Draw(out_img, src)
|
|
|
|
SaveImage(out_file, out_img)
|
|
}
|
|
|
|
func UnfuckedPathSplit(path string) []string {
|
|
path = filepath.ToSlash(path)
|
|
// WARN: have to use strings.Split because fsnotify uses /, even on windows
|
|
return strings.Split(path, "/")[1:]
|
|
}
|
|
|
|
func SplitPathExt(path string) (string, string, bool) {
|
|
split_path := UnfuckedPathSplit(path)
|
|
source_name := strings.Join(split_path, "/") // Render wants / even on windows
|
|
|
|
ext := filepath.Ext(source_name)
|
|
source_name, found := strings.CutSuffix(source_name, ext)
|
|
return source_name, ext, found
|
|
}
|
|
|
|
func RePrefixPath(source string, path string, new_prefix string) string {
|
|
split_path := UnfuckedPathSplit(path)
|
|
split_source := UnfuckedPathSplit(source)
|
|
tail := split_path[len(split_source) - 1:]
|
|
|
|
prefixed_path := append([]string{new_prefix}, tail...)
|
|
|
|
res := filepath.Join(prefixed_path...)
|
|
return filepath.ToSlash(res)
|
|
}
|
|
|
|
func SamePath(a string, b string) bool {
|
|
return filepath.ToSlash(a) == filepath.ToSlash(b)
|
|
}
|
|
|
|
func MkdirPath(target_path string) error {
|
|
target_dir := filepath.Dir(target_path)
|
|
_, err := os.Stat(target_dir)
|
|
|
|
if os.IsNotExist(err) {
|
|
log.Println("MAKING: ", target_dir)
|
|
err = os.MkdirAll(target_dir, 0750)
|
|
|
|
if err != nil {
|
|
log.Fatal("making path to %s: %v", target_dir, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func Included(config Settings, path string) bool {
|
|
_, ext, found := SplitPathExt(path)
|
|
if !found { return false }
|
|
|
|
for _, include_ext := range config.Include {
|
|
if include_ext == ext {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func GetConversion(config Settings, path string) Conversion {
|
|
path = strings.ToLower(RePrefixPath(config.Source, path, ""))
|
|
|
|
for key, conversion := range config.Exceptions {
|
|
match, err := regexp.MatchString(key, path)
|
|
if err != nil { log.Fatalf("problem matching regex %s: %v", key, err) }
|
|
|
|
if match {
|
|
return conversion
|
|
}
|
|
}
|
|
|
|
return config.Base
|
|
}
|
|
|
|
func HasChanged(old_info fs.FileInfo, new_path string) bool {
|
|
new_info, err := os.Stat(new_path)
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return true
|
|
} else if err != nil {
|
|
log.Printf("!!!! can't stat target: %s: %v", new_path, err)
|
|
return false
|
|
}
|
|
|
|
return old_info.ModTime().After(new_info.ModTime())
|
|
}
|
|
|
|
func RenderImages(config Settings, force bool) error {
|
|
err := filepath.WalkDir(config.Source,
|
|
func(path string, d fs.DirEntry, err error) error {
|
|
path = filepath.ToSlash(path)
|
|
|
|
if !d.IsDir() && Included(config, path) {
|
|
// have to lowercase the target path to avoid windows issues
|
|
target := strings.ToLower(RePrefixPath(config.Source, path, config.Target))
|
|
|
|
err = MkdirPath(target)
|
|
if err != nil {
|
|
log.Fatalf("failed to make path %s: %v", path, err)
|
|
}
|
|
|
|
fmt.Println("FILE: ", path, "TARGET:", target)
|
|
convert := GetConversion(config, path)
|
|
|
|
path_info, err := d.Info()
|
|
if err != nil {
|
|
log.Fatalf("failed to stat %s: %v", path, err)
|
|
}
|
|
|
|
if force || HasChanged(path_info, target) {
|
|
fmt.Println("JANKIFY: ", path, "->", target)
|
|
JankImage(convert, path, target)
|
|
} else {
|
|
fmt.Println("UNCHANGED: ", path, "->", target)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return err
|
|
}
|
|
|