From 7237e5cf9ce59ad55532d336c459ea534b7940ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 2 Jan 2023 21:40:39 +0530 Subject: [PATCH] Get GIF basically working --- tools/cmd/icat/native.go | 52 ++++++++++++++++++++++++--- tools/cmd/icat/process_images.go | 17 +++++---- tools/cmd/icat/transmit.go | 61 ++++++++++++++++++++++++++------ 3 files changed, 109 insertions(+), 21 deletions(-) diff --git a/tools/cmd/icat/native.go b/tools/cmd/icat/native.go index 781d8d8b6..e496881e4 100644 --- a/tools/cmd/icat/native.go +++ b/tools/cmd/icat/native.go @@ -5,7 +5,9 @@ package icat import ( "fmt" "image" + "image/gif" "kitty/tools/tui/graphics" + "kitty/tools/utils" "kitty/tools/utils/images" "kitty/tools/utils/shm" @@ -14,7 +16,7 @@ import ( var _ = fmt.Print -func add_frame(imgd *image_data, img image.Image, is_opaque bool) { +func add_frame(imgd *image_data, img image.Image, is_opaque bool) *image_frame { if flip { img = imaging.FlipV(img) } @@ -22,7 +24,7 @@ func add_frame(imgd *image_data, img image.Image, is_opaque bool) { img = imaging.FlipH(img) } b := img.Bounds() - f := image_frame{width: b.Dx(), height: b.Dy()} + f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames), left: b.Min.X, top: b.Min.Y} dest_rect := image.Rect(0, 0, f.width, f.height) var final_img image.Image @@ -53,6 +55,7 @@ func add_frame(imgd *image_data, img image.Image, is_opaque bool) { } images.PasteCenter(final_img, img, remove_alpha) imgd.frames = append(imgd.frames, &f) + return &f } func load_one_frame_image(imgd *image_data, src *opened_input) (img image.Image, is_opaque bool, err error) { @@ -78,10 +81,49 @@ func load_one_frame_image(imgd *image_data, src *opened_input) (img image.Image, return } +func add_gif_frames(imgd *image_data, gf *gif.GIF) error { + max_gap := utils.Max(0, gf.Delay...) + min_gap := 0 + if max_gap <= 0 { + min_gap = 1 + } + + min_gap *= 1 + anchor_frame := 1 + for i, img := range gf.Image { + frame := add_frame(imgd, img, img.Opaque()) + frame.delay_ms = utils.Max(min_gap, gf.Delay[i]) * 10 + if frame.delay_ms == 0 { + frame.delay_ms = -1 + } + if i > 0 { + switch gf.Disposal[i] { + 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 + } + } + } + return nil +} + func render_image_with_go(imgd *image_data, src *opened_input) (err error) { - switch imgd.format_uppercase { - case "GIF": - return fmt.Errorf("TODO: implement GIF decoding") + switch { + case imgd.format_uppercase == "GIF" && opts.Loop != 0: + gif_frames, err := gif.DecodeAll(src.file) + src.Rewind() + if err != nil { + return fmt.Errorf("Failed to decode GIF file with error: %w", err) + } + err = add_gif_frames(imgd, gif_frames) + if err != nil { + return err + } default: img, is_opaque, err := load_one_frame_image(imgd, src) if err != nil { diff --git a/tools/cmd/icat/process_images.go b/tools/cmd/icat/process_images.go index cfe6a5d86..9cd0d014c 100644 --- a/tools/cmd/icat/process_images.go +++ b/tools/cmd/icat/process_images.go @@ -6,6 +6,7 @@ import ( "bytes" "fmt" "image" + "image/color" "io" "io/fs" "net/http" @@ -138,12 +139,16 @@ func (self *opened_input) Release() { } type image_frame struct { - filename string - shm shm.MMap - in_memory_bytes []byte - filename_is_temporary bool - width, height int - transmission_format graphics.GRT_f + filename string + shm shm.MMap + in_memory_bytes []byte + filename_is_temporary bool + width, height, left, top int + transmission_format graphics.GRT_f + compose_onto int + number int + disposal_background color.NRGBA + delay_ms int } type image_data struct { diff --git a/tools/cmd/icat/transmit.go b/tools/cmd/icat/transmit.go index 0f053b8d6..caf9f9cdf 100644 --- a/tools/cmd/icat/transmit.go +++ b/tools/cmd/icat/transmit.go @@ -21,19 +21,31 @@ var _ = fmt.Print func gc_for_image(imgd *image_data, frame_num int, frame *image_frame) *graphics.GraphicsCommand { gc := graphics.GraphicsCommand{} - gc.SetAction(graphics.GRT_action_transmit_and_display) gc.SetDataWidth(uint64(frame.width)).SetDataHeight(uint64(frame.height)) gc.SetQuiet(graphics.GRT_quiet_silent) - if frame_num == 0 && imgd.cell_x_offset > 0 { - gc.SetXOffset(uint64(imgd.cell_x_offset)) - } - if z_index != 0 { - gc.SetZIndex(z_index) - } + gc.SetFormat(frame.transmission_format) if imgd.image_number != 0 { gc.SetImageNumber(imgd.image_number) } - gc.SetFormat(frame.transmission_format) + if frame_num == 0 { + gc.SetAction(graphics.GRT_action_transmit_and_display) + if imgd.cell_x_offset > 0 { + gc.SetXOffset(uint64(imgd.cell_x_offset)) + } + if z_index != 0 { + gc.SetZIndex(z_index) + } + } else { + gc.SetAction(graphics.GRT_action_frame) + gc.SetGap(int32(frame.delay_ms)) + if frame.compose_onto > 0 { + gc.SetOverlaidFrame(uint64(frame.compose_onto)) + } else { + bg := (uint32(frame.disposal_background.R) << 24) | (uint32(frame.disposal_background.G) << 16) | (uint32(frame.disposal_background.B) << 8) | uint32(frame.disposal_background.A) + gc.SetBackgroundColor(bg) + } + gc.SetLeftEdge(uint64(frame.left)).SetTopEdge(uint64(frame.top)) + } return &gc } @@ -222,7 +234,9 @@ func transmit_image(imgd *image_data) { f = transmit_stream } if len(imgd.frames) > 1 { - imgd.image_number = rand.Uint32() + for imgd.image_number == 0 { + imgd.image_number = rand.Uint32() + } } place_cursor(imgd) lp.QueueWriteString("\r") @@ -232,11 +246,38 @@ func transmit_image(imgd *image_data) { if imgd.move_to.x > 0 { lp.MoveCursorTo(imgd.move_to.x, imgd.move_to.y) } + frame_control_cmd := graphics.GraphicsCommand{} + frame_control_cmd.SetAction(graphics.GRT_action_animate).SetImageNumber(imgd.image_number) + for frame_num, frame := range imgd.frames { err := f(imgd, frame_num, frame) if err != nil { - print_error("Failed to transmit %s with error: %v", imgd.source_name, err) + print_error("\rFailed to transmit %s with error: %v", imgd.source_name, err) + return } + switch frame_num { + case 0: + // set gap for the first frame and number of loops for the animation + c := frame_control_cmd + c.SetTargetFrame(uint64(frame.number)) + c.SetGap(int32(frame.delay_ms)) + switch { + case opts.Loop < 0: + c.SetNumberOfLoops(1) + case opts.Loop > 0: + c.SetNumberOfLoops(uint64(opts.Loop) + 1) + } + c.WriteWithPayloadToLoop(lp, nil) + case 1: + c := frame_control_cmd + c.SetAnimationControl(2) // set animation to loading mode + c.WriteWithPayloadToLoop(lp, nil) + } + } + if len(imgd.frames) > 1 { + c := frame_control_cmd + c.SetAnimationControl(3) // set animation to normal mode + c.WriteWithPayloadToLoop(lp, nil) } if imgd.move_to.x == 0 { lp.Println() // ensure cursor is on new line