mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-12 10:47:05 +02:00
172 lines
4.1 KiB
Go
172 lines
4.1 KiB
Go
package images
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"kitty/tools/cli"
|
|
"kitty/tools/utils"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
func convert_and_save_as_rgba_data(input_path, output_path string, perm os.FileMode) (err error) {
|
|
f, err := os.Open(input_path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
image_data, err := OpenNativeImageFromReader(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(image_data.Frames) == 0 {
|
|
return fmt.Errorf("Image at %s has no frames", input_path)
|
|
}
|
|
img := image_data.Frames[0].Img
|
|
var final_img *image.NRGBA
|
|
switch img.(type) {
|
|
case *image.NRGBA:
|
|
final_img = img.(*image.NRGBA)
|
|
default:
|
|
b := img.Bounds()
|
|
final_img = image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
|
ctx := Context{}
|
|
ctx.PasteCenter(final_img, img, nil)
|
|
}
|
|
b := final_img.Bounds()
|
|
header := make([]byte, 8)
|
|
var width = utils.Abs(b.Dx())
|
|
var height = utils.Abs(b.Dy())
|
|
binary.LittleEndian.PutUint32(header, uint32(width))
|
|
binary.LittleEndian.PutUint32(header[4:], uint32(height))
|
|
readers := []io.Reader{bytes.NewReader(header)}
|
|
stride := 4 * width
|
|
|
|
if final_img.Stride == stride {
|
|
readers = append(readers, bytes.NewReader(final_img.Pix))
|
|
} else {
|
|
p := final_img.Pix
|
|
for y := 0; y < b.Dy(); y++ {
|
|
readers = append(readers, bytes.NewReader(p[:min(stride, len(p))]))
|
|
p = p[final_img.Stride:]
|
|
}
|
|
}
|
|
return utils.AtomicUpdateFile(output_path, io.MultiReader(readers...), perm)
|
|
}
|
|
|
|
var now_implementation = time.Now
|
|
var chmtime_after_creation = false
|
|
|
|
func prune_cache(cdir string, max_entries int) error {
|
|
entries, err := os.ReadDir(cdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(entries) <= max_entries {
|
|
return nil
|
|
}
|
|
now := now_implementation()
|
|
epoch := time.Unix(0, 0)
|
|
entries = utils.SortWithKey(entries, func(a fs.DirEntry) time.Duration {
|
|
if info, err := a.Info(); err == nil {
|
|
return now.Sub(info.ModTime())
|
|
}
|
|
return now.Sub(epoch)
|
|
})
|
|
for _, x := range entries[max_entries:] {
|
|
if err = os.Remove(filepath.Join(cdir, x.Name())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func render_image(src_path, cdir string, max_cache_entries int) (output_path string, err error) {
|
|
if src_path, err = filepath.EvalSymlinks(src_path); err != nil {
|
|
return
|
|
}
|
|
if src_path, err = filepath.Abs(src_path); err != nil {
|
|
return
|
|
}
|
|
lock_file := filepath.Join(cdir, "lock")
|
|
lockf, err := os.OpenFile(lock_file, os.O_CREATE|os.O_RDWR, 0600)
|
|
defer lockf.Close()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = utils.LockFileExclusive(lockf); err != nil {
|
|
return "", fmt.Errorf("Failed to lock cache file %s with error: %w", lock_file, err)
|
|
}
|
|
defer func() {
|
|
utils.UnlockFile(lockf)
|
|
}()
|
|
output_path = filepath.Join(cdir, fmt.Sprintf("%x", sha256.Sum256([]byte(src_path)))) + ".rgba"
|
|
needs_update := true
|
|
input_info, err := os.Stat(src_path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
output_info, err := os.Stat(output_path)
|
|
if err == nil {
|
|
needs_update = input_info.Size() != output_info.Size() || input_info.ModTime().After(output_info.ModTime())
|
|
} else {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return
|
|
}
|
|
}
|
|
if needs_update {
|
|
if err = convert_and_save_as_rgba_data(src_path, output_path, 0600); err != nil {
|
|
return
|
|
}
|
|
if chmtime_after_creation {
|
|
n := now_implementation()
|
|
if err = os.Chtimes(output_path, n, n); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if err = prune_cache(cdir, max_cache_entries); err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
n := now_implementation()
|
|
if err = os.Chtimes(output_path, n, n); err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func RenderEntryPoint(root *cli.Command) {
|
|
root.AddSubCommand(&cli.Command{
|
|
Name: "__render_image__",
|
|
Hidden: true,
|
|
OnlyArgsAllowed: true,
|
|
Run: func(cmd *cli.Command, args []string) (rc int, err error) {
|
|
if len(args) != 1 {
|
|
return 1, fmt.Errorf("Usage: render input_image_path")
|
|
}
|
|
cdir := utils.CacheDir()
|
|
cdir = filepath.Join(cdir, "rgba")
|
|
if err = os.MkdirAll(cdir, 0755); err != nil {
|
|
return 1, err
|
|
}
|
|
if output_path, err := render_image(args[0], cdir, 32); err != nil {
|
|
return 1, err
|
|
} else {
|
|
fmt.Println(output_path)
|
|
}
|
|
return
|
|
},
|
|
})
|
|
}
|