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 }