kitty/tools/utils/images/loading.go
2023-03-27 07:53:57 +05:30

134 lines
3.7 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package images
import (
"fmt"
"image"
"image/color"
"image/gif"
"io"
"os"
"strings"
"kitty/tools/utils"
"github.com/disintegration/imaging"
)
var _ = fmt.Print
type ImageFrame struct {
Width, Height, Left, Top int
Number int // 1-based number
Compose_onto int // number of frame to compose onto
Delay_ms int32 // negative for gapless frame, zero ignored, positive is number of ms
Is_opaque bool
Img image.Image
}
type ImageData struct {
Width, Height int
Format_uppercase string
Frames []*ImageFrame
}
func CalcMinimumGIFGap(gaps []int) int {
// Some broken GIF images have all zero gaps, browsers with their usual
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
// Browsers actually force a 100ms gap at any zero gap frame, but that
// just means it is impossible to deliberately use zero gap frames for
// sophisticated blending, so we dont do that.
max_gap := utils.Max(0, gaps...)
min_gap := 0
if max_gap <= 0 {
min_gap = 10
}
return min_gap
}
func SetGIFFrameDisposal(number, anchor_frame int, disposal byte) (int, int) {
compose_onto := 0
if number > 1 {
switch disposal {
case gif.DisposalNone:
compose_onto = number - 1
anchor_frame = number
case gif.DisposalBackground:
// see https://github.com/golang/go/issues/20694
anchor_frame = number
case gif.DisposalPrevious:
compose_onto = anchor_frame
}
}
return anchor_frame, compose_onto
}
func open_native_gif(f io.Reader, ans *ImageData) error {
gif_frames, err := gif.DecodeAll(f)
if err != nil {
return err
}
min_gap := CalcMinimumGIFGap(gif_frames.Delay)
anchor_frame := 1
for i, paletted_img := range gif_frames.Image {
b := paletted_img.Bounds()
frame := ImageFrame{Img: paletted_img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy(), Number: len(ans.Frames) + 1, Is_opaque: paletted_img.Opaque()}
frame.Delay_ms = int32(utils.Max(min_gap, gif_frames.Delay[i]) * 10)
if frame.Delay_ms == 0 {
frame.Delay_ms = -1 // gapless frame
}
anchor_frame, frame.Compose_onto = SetGIFFrameDisposal(frame.Number, anchor_frame, gif_frames.Disposal[i])
ans.Frames = append(ans.Frames, &frame)
}
return nil
}
func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
c, fmt, err := image.DecodeConfig(f)
if err != nil {
return nil, err
}
f.Seek(0, os.SEEK_SET)
ans = &ImageData{Width: c.Width, Height: c.Height, Format_uppercase: strings.ToUpper(fmt)}
if ans.Format_uppercase == "GIF" {
err = open_native_gif(f, ans)
if err != nil {
return nil, err
}
} else {
img, err := imaging.Decode(f, imaging.AutoOrientation(true))
if err != nil {
return nil, err
}
b := img.Bounds()
ans.Frames = []*ImageFrame{{Img: img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy()}}
ans.Frames[0].Is_opaque = c.ColorModel == color.YCbCrModel || c.ColorModel == color.GrayModel || c.ColorModel == color.Gray16Model || c.ColorModel == color.CMYKModel || ans.Format_uppercase == "JPEG" || ans.Format_uppercase == "JPG" || IsOpaque(img)
}
return
}
func OpenMagickImageFromPath(path string) (ans *ImageData, err error) {
// TODO: Implement this
return
}
func OpenImageFromPath(path string) (ans *ImageData, err error) {
mt := utils.GuessMimeType(path)
if DecodableImageTypes[mt] {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
ans, err = OpenNativeImageFromReader(f)
if err != nil {
return nil, fmt.Errorf("Failed to load image at %#v with error: %w", path, err)
}
} else {
return OpenMagickImageFromPath(path)
}
return
}