From 404a775f4b0776f7634b647b048b131150871a44 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 23 Mar 2023 23:01:08 +0530 Subject: [PATCH] Start work on image support for new diff kitten --- tools/cmd/icat/native.go | 15 +--- tools/utils/images/loading.go | 133 ++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 tools/utils/images/loading.go diff --git a/tools/cmd/icat/native.go b/tools/cmd/icat/native.go index 92e8fcf0f..1cf9f6605 100644 --- a/tools/cmd/icat/native.go +++ b/tools/cmd/icat/native.go @@ -136,18 +136,7 @@ func calc_min_gap(gaps []int) int { } func (frame *image_frame) set_disposal(anchor_frame int, disposal byte) int { - if frame.number > 1 { - switch disposal { - case gif.DisposalNone: - frame.compose_onto = frame.number - 1 - anchor_frame = frame.number - case gif.DisposalBackground: - // see https://github.com/golang/go/issues/20694 - anchor_frame = frame.number - case gif.DisposalPrevious: - frame.compose_onto = anchor_frame - } - } + anchor_frame, frame.compose_onto = images.SetGIFFrameDisposal(frame.number, anchor_frame, disposal) return anchor_frame } @@ -159,7 +148,7 @@ func (frame *image_frame) set_delay(gap, min_gap int) { } func add_gif_frames(ctx *images.Context, imgd *image_data, gf *gif.GIF) error { - min_gap := calc_min_gap(gf.Delay) + min_gap := images.CalcMinimumGIFGap(gf.Delay) scale_image(imgd) anchor_frame := 1 for i, paletted_img := range gf.Image { diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go new file mode 100644 index 000000000..310d33e3b --- /dev/null +++ b/tools/utils/images/loading.go @@ -0,0 +1,133 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +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 +}