From 1c8e8e9530bc0d11e7ab1a3d730384cb23c91afe Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 10 Nov 2025 11:34:56 +0530 Subject: [PATCH] Switch over to the new imaging backend for icat Greatly simplifies a whole bunch of code. The new backend takes care of falling back to ImageMagick efficiently itself. --- go.mod | 4 +- go.sum | 8 +- kittens/icat/magick.go | 64 ---- kittens/icat/native.go | 169 --------- kittens/icat/process_images.go | 250 +++++++------- kittens/icat/transmit.go | 101 ++---- tools/utils/images/convert.go | 11 +- tools/utils/images/loading.go | 499 +-------------------------- tools/utils/images/serialize_test.go | 2 +- tools/utils/images/transforms.go | 58 ---- tools/utils/shm/shm_fs.go | 4 +- 11 files changed, 181 insertions(+), 989 deletions(-) delete mode 100644 kittens/icat/magick.go delete mode 100644 kittens/icat/native.go delete mode 100644 tools/utils/images/transforms.go diff --git a/go.mod b/go.mod index 1b0be8d82..d5494d8a1 100644 --- a/go.mod +++ b/go.mod @@ -13,13 +13,13 @@ require ( github.com/google/uuid v1.6.0 github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 github.com/kovidgoyal/go-parallel v1.1.1 - github.com/kovidgoyal/imaging v1.8.5 + github.com/kovidgoyal/imaging v1.8.8 github.com/seancfoley/ipaddress-go v1.7.1 github.com/shirou/gopsutil/v4 v4.25.10 github.com/zeebo/xxh3 v1.0.2 golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b golang.org/x/image v0.32.0 - golang.org/x/sys v0.37.0 + golang.org/x/sys v0.38.0 golang.org/x/text v0.30.0 howett.net/plist v1.0.1 ) diff --git a/go.sum b/go.sum index 1bab6a0a5..01257683f 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BL github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50= github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok= github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= -github.com/kovidgoyal/imaging v1.8.5 h1:GZrBlpRbuSbpomWhERCl5ZmQq5Q6nMR8gnHL9R3uomM= -github.com/kovidgoyal/imaging v1.8.5/go.mod h1:fHutWjAiIZ3t7+YRVzuPkICFFzHTTkxgx2jSeQXKjE0= +github.com/kovidgoyal/imaging v1.8.8 h1:PohlAOYuokFtmt6sjhgA90YAUKhuuL3i0dhd5gepp4g= +github.com/kovidgoyal/imaging v1.8.8/go.mod h1:GAbZkbyB86PSfosof5EnS2o6N15yUk9Vy2r61EWy1Wg= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -67,8 +67,8 @@ golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/kittens/icat/magick.go b/kittens/icat/magick.go deleted file mode 100644 index 2fcf10eed..000000000 --- a/kittens/icat/magick.go +++ /dev/null @@ -1,64 +0,0 @@ -// License: GPLv3 Copyright: 2023, Kovid Goyal, - -package icat - -import ( - "fmt" - - "github.com/kovidgoyal/go-parallel" - "github.com/kovidgoyal/kitty/tools/tui/graphics" - "github.com/kovidgoyal/kitty/tools/utils/images" -) - -var _ = fmt.Print - -func render(path, original_file_path string, ro *images.RenderOptions, frames []images.IdentifyRecord) (ans []*image_frame, err error) { - ro.TempfilenameTemplate = shm_template - image_frames, filenames, err := images.RenderWithMagick(path, original_file_path, ro, frames) - if err == nil { - ans = make([]*image_frame, len(image_frames)) - for i, x := range image_frames { - ans[i] = &image_frame{ - filename: filenames[x.Number], filename_is_temporary: true, - number: x.Number, width: x.Width, height: x.Height, left: x.Left, top: x.Top, - transmission_format: graphics.GRT_format_rgba, delay_ms: int(x.Delay_ms), compose_onto: x.Compose_onto, - } - if x.Is_opaque { - ans[i].transmission_format = graphics.GRT_format_rgb - } - } - } - return ans, err -} - -func render_image_with_magick(imgd *image_data, src *opened_input) (err error) { - defer func() { - if r := recover(); r != nil { - err = parallel.Format_stacktrace_on_panic(r, 1) - } - }() - err = src.PutOnFilesystem() - if err != nil { - return err - } - frames, err := images.IdentifyWithMagick(src.FileSystemName(), imgd.source_name) - if err != nil { - return err - } - imgd.format_uppercase = frames[0].Fmt_uppercase - imgd.canvas_width, imgd.canvas_height = frames[0].Canvas.Width, frames[0].Canvas.Height - set_basic_metadata(imgd) - if !imgd.needs_conversion { - make_output_from_input(imgd, src) - return nil - } - ro := images.RenderOptions{RemoveAlpha: remove_alpha, Flip: flip, Flop: flop} - if scale_image(imgd) { - ro.ResizeTo.X, ro.ResizeTo.Y = imgd.canvas_width, imgd.canvas_height - } - imgd.frames, err = render(src.FileSystemName(), imgd.source_name, &ro, frames) - if err != nil { - return err - } - return nil -} diff --git a/kittens/icat/native.go b/kittens/icat/native.go deleted file mode 100644 index 3c711db13..000000000 --- a/kittens/icat/native.go +++ /dev/null @@ -1,169 +0,0 @@ -// License: GPLv3 Copyright: 2023, Kovid Goyal, - -package icat - -import ( - "fmt" - "image" - - "github.com/kovidgoyal/go-parallel" - "github.com/kovidgoyal/imaging/nrgb" - "github.com/kovidgoyal/kitty/tools/tty" - "github.com/kovidgoyal/kitty/tools/tui/graphics" - "github.com/kovidgoyal/kitty/tools/utils/images" - "github.com/kovidgoyal/kitty/tools/utils/shm" - - "github.com/kovidgoyal/imaging" -) - -var _ = fmt.Print - -func resize_frame(imgd *image_data, img image.Image) (image.Image, image.Rectangle) { - b := img.Bounds() - left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy() - new_width := int(imgd.scaled_frac.x * float64(width)) - new_height := int(imgd.scaled_frac.y * float64(height)) - img = imaging.Resize(img, new_width, new_height, imaging.Lanczos) - newleft := int(imgd.scaled_frac.x * float64(left)) - newtop := int(imgd.scaled_frac.y * float64(top)) - return img, image.Rect(newleft, newtop, newleft+new_width, newtop+new_height) -} - -const shm_template = "kitty-icat-*" - -func add_frame(ctx *images.Context, imgd *image_data, img image.Image, left, top int) *image_frame { - is_opaque := imaging.IsOpaque(img) - b := img.Bounds() - if imgd.scaled_frac.x != 0 { - img, b = resize_frame(imgd, img) - } - f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: left, top: top} - dest_rect := image.Rect(0, 0, f.width, f.height) - var final_img image.Image - bytes_per_pixel := 4 - - if is_opaque || remove_alpha != nil { - var rgb *imaging.NRGB - bytes_per_pixel = 3 - m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel)) - if err != nil { - rgb = nrgb.NewNRGB(dest_rect) - } else { - rgb = &imaging.NRGB{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect} - f.shm = m - } - f.transmission_format = graphics.GRT_format_rgb - f.in_memory_bytes = rgb.Pix - final_img = rgb - } else { - var rgba *image.NRGBA - m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel)) - if err != nil { - rgba = image.NewNRGBA(dest_rect) - } else { - rgba = &image.NRGBA{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect} - f.shm = m - } - f.transmission_format = graphics.GRT_format_rgba - f.in_memory_bytes = rgba.Pix - final_img = rgba - } - ctx.PasteCenter(final_img, img, remove_alpha) - imgd.frames = append(imgd.frames, &f) - if flip { - ctx.FlipPixelsV(bytes_per_pixel, f.width, f.height, f.in_memory_bytes) - if f.height < imgd.canvas_height { - f.top = (2*imgd.canvas_height - f.height - f.top) % imgd.canvas_height - } - } - if flop { - ctx.FlipPixelsH(bytes_per_pixel, f.width, f.height, f.in_memory_bytes) - if f.width < imgd.canvas_width { - f.left = (2*imgd.canvas_width - f.width - f.left) % imgd.canvas_width - } - } - return &f -} - -func scale_up(width, height, maxWidth, maxHeight int) (newWidth, newHeight int) { - if width == 0 || height == 0 { - return 0, 0 - } - - // Calculate the ratio to scale the width and the ratio to scale the height. - // We use floating-point division for precision. - widthRatio := float64(maxWidth) / float64(width) - heightRatio := float64(maxHeight) / float64(height) - - // To preserve the aspect ratio and fit within the limits, we must use the - // smaller of the two scaling ratios. - var ratio float64 - if widthRatio < heightRatio { - ratio = widthRatio - } else { - ratio = heightRatio - } - - // Calculate the new dimensions and convert them back to uints. - newWidth = int(float64(width) * ratio) - newHeight = int(float64(height) * ratio) - - return newWidth, newHeight -} - -func scale_image(imgd *image_data) bool { - if imgd.needs_scaling { - width, height := imgd.canvas_width, imgd.canvas_height - if opts.ScaleUp && (imgd.canvas_width < imgd.available_width || imgd.canvas_height < imgd.available_height) && (imgd.available_height != inf || imgd.available_width != inf) { - imgd.canvas_width, imgd.canvas_height = scale_up(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height) - } - neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height) - imgd.needs_scaling = false - imgd.scaled_frac.x = float64(neww) / float64(width) - imgd.scaled_frac.y = float64(newh) / float64(height) - imgd.canvas_width = int(imgd.scaled_frac.x * float64(width)) - imgd.canvas_height = int(imgd.scaled_frac.y * float64(height)) - return true - } - return false -} - -var debugprintln = tty.DebugPrintln -var _ = debugprintln - -func add_frames(ctx *images.Context, imgd *image_data, gf *imaging.Image) { - for _, f := range gf.Frames { - frame := add_frame(ctx, imgd, f.Image, f.TopLeft.X, f.TopLeft.Y) - frame.number, frame.compose_onto = int(f.Number), int(f.ComposeOnto) - frame.replace = f.Replace - frame.delay_ms = int(f.Delay.Milliseconds()) - if frame.delay_ms <= 0 { - frame.delay_ms = -1 // -1 is gapless in graphics protocol - } - } -} - -func render_image_with_go(imgd *image_data, src *opened_input) (err error) { - defer func() { - if r := recover(); r != nil { - err = parallel.Format_stacktrace_on_panic(r, 1) - } - }() - ctx := images.Context{} - imgs, _, err := imaging.DecodeAll(src.file) - if err != nil { - return err - } - if imgs == nil { - return fmt.Errorf("unknown image format") - } - imgd.format_uppercase = imgs.Metadata.Format.String() - // Loading could auto orient and therefore change width/height, so - // re-calculate - b := imgs.Bounds() - imgd.canvas_width, imgd.canvas_height = b.Dx(), b.Dy() - set_basic_metadata(imgd) - scale_image(imgd) - add_frames(&ctx, imgd, imgs) - return nil -} diff --git a/kittens/icat/process_images.go b/kittens/icat/process_images.go index 4f5834bc1..f89883e49 100644 --- a/kittens/icat/process_images.go +++ b/kittens/icat/process_images.go @@ -15,52 +15,15 @@ import ( "path/filepath" "strings" + "github.com/kovidgoyal/imaging" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tui/graphics" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/images" - "github.com/kovidgoyal/kitty/tools/utils/shm" ) var _ = fmt.Print -type BytesBuf struct { - data []byte - pos int64 -} - -func (self *BytesBuf) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekStart: - self.pos = offset - case io.SeekCurrent: - self.pos += offset - case io.SeekEnd: - self.pos = int64(len(self.data)) + offset - default: - return self.pos, fmt.Errorf("Unknown value for whence: %#v", whence) - } - self.pos = utils.Max(0, utils.Min(self.pos, int64(len(self.data)))) - return self.pos, nil -} - -func (self *BytesBuf) Read(p []byte) (n int, err error) { - nb := utils.Min(int64(len(p)), int64(len(self.data))-self.pos) - if nb == 0 { - err = io.EOF - } else { - n = copy(p, self.data[self.pos:self.pos+nb]) - self.pos += nb - } - return -} - -func (self *BytesBuf) Close() error { - self.data = nil - self.pos = 0 - return nil -} - type input_arg struct { arg string value string @@ -120,54 +83,14 @@ func process_dirs(args ...string) (results []input_arg, err error) { } type opened_input struct { - file io.ReadSeekCloser - name_to_unlink string + file io.Reader + bytes []byte + path string } -func (self *opened_input) Rewind() { - if self.file != nil { - _, _ = self.file.Seek(0, io.SeekStart) - } -} - -func (self *opened_input) Release() { - if self.file != nil { - self.file.Close() - self.file = nil - } - if self.name_to_unlink != "" { - os.Remove(self.name_to_unlink) - self.name_to_unlink = "" - } -} - -func (self *opened_input) PutOnFilesystem() (err error) { - if self.name_to_unlink != "" { - return - } - f, err := images.CreateTempInRAM() - if err != nil { - return fmt.Errorf("Failed to create a temporary file to store input data with error: %w", err) - } - self.Rewind() - _, err = io.Copy(f, self.file) - if err != nil { - f.Close() - return fmt.Errorf("Failed to copy input data to temporary file with error: %w", err) - } - self.Release() - self.file = f - self.name_to_unlink = f.Name() - return -} - -func (self *opened_input) FileSystemName() string { return self.name_to_unlink } - type image_frame struct { 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 @@ -180,8 +103,7 @@ type image_data struct { canvas_width, canvas_height int format_uppercase string available_width, available_height int - needs_scaling, needs_conversion bool - scaled_frac struct{ x, y float64 } + needs_scaling bool frames []*image_frame image_number uint32 image_id uint32 @@ -222,7 +144,6 @@ func set_basic_metadata(imgd *image_data) { } } imgd.needs_scaling = imgd.canvas_width > imgd.available_width || imgd.canvas_height > imgd.available_height || opts.ScaleUp - imgd.needs_conversion = imgd.needs_scaling || remove_alpha != nil || flip || flop || imgd.format_uppercase != "PNG" } func report_error(source_name, msg string, err error) { @@ -231,7 +152,6 @@ func report_error(source_name, msg string, err error) { } func make_output_from_input(imgd *image_data, f *opened_input) { - bb, ok := f.file.(*BytesBuf) frame := image_frame{} imgd.frames = append(imgd.frames, &frame) frame.width = imgd.canvas_width @@ -240,17 +160,71 @@ func make_output_from_input(imgd *image_data, f *opened_input) { panic(fmt.Sprintf("Unknown transmission format: %s", imgd.format_uppercase)) } frame.transmission_format = graphics.GRT_format_png - if ok { - frame.in_memory_bytes = bb.data + if f.bytes != nil { + frame.in_memory_bytes = f.bytes + } else if f.path != "" { + frame.filename = f.path } else { - frame.filename = f.file.(*os.File).Name() - if f.name_to_unlink != "" { - frame.filename_is_temporary = true - f.name_to_unlink = "" + var err error + if frame.in_memory_bytes, err = io.ReadAll(f.file); err != nil { + panic(err) } } } +func scale_up(width, height, maxWidth, maxHeight int) (newWidth, newHeight int) { + if width == 0 || height == 0 { + return 0, 0 + } + // Calculate the ratio to scale the width and the ratio to scale the height. + // We use floating-point division for precision. + widthRatio := float64(maxWidth) / float64(width) + heightRatio := float64(maxHeight) / float64(height) + + // To preserve the aspect ratio and fit within the limits, we must use the + // smaller of the two scaling ratios. + var ratio float64 + if widthRatio < heightRatio { + ratio = widthRatio + } else { + ratio = heightRatio + } + + // Calculate the new dimensions and convert them back to uints. + newWidth = int(float64(width) * ratio) + newHeight = int(float64(height) * ratio) + + return newWidth, newHeight +} + +func scale_image(imgd *image_data) bool { + if imgd.needs_scaling { + width, height := imgd.canvas_width, imgd.canvas_height + if opts.ScaleUp && (imgd.canvas_width < imgd.available_width || imgd.canvas_height < imgd.available_height) && (imgd.available_height != inf || imgd.available_width != inf) { + imgd.canvas_width, imgd.canvas_height = scale_up(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height) + } + neww, newh := images.FitImage(imgd.canvas_width, imgd.canvas_height, imgd.available_width, imgd.available_height) + imgd.needs_scaling = false + x := float64(neww) / float64(width) + y := float64(newh) / float64(height) + imgd.canvas_width = int(x * float64(width)) + imgd.canvas_height = int(y * float64(height)) + return true + } + return false +} + +func add_frame(imgd *image_data, img image.Image, left, top int) *image_frame { + const shm_template = "kitty-icat-*" + num_channels, pix := imaging.AsRGBData8(img) + b := img.Bounds() + f := image_frame{width: b.Dx(), height: b.Dy(), number: len(imgd.frames) + 1, left: left, top: top} + f.transmission_format = utils.IfElse(num_channels == 3, graphics.GRT_format_rgb, graphics.GRT_format_rgba) + f.in_memory_bytes = pix + imgd.frames = append(imgd.frames, &f) + return &f +} + func process_arg(arg input_arg) { var f opened_input if arg.is_http_url { @@ -271,14 +245,16 @@ func process_arg(arg input_arg) { report_error(arg.value, "Could not download", err) return } - f.file = &BytesBuf{data: dest.Bytes()} + f.bytes = dest.Bytes() + f.file = bytes.NewReader(f.bytes) } else if arg.value == "" { stdin, err := io.ReadAll(os.Stdin) if err != nil { report_error("", "Could not read from", err) return } - f.file = &BytesBuf{data: stdin} + f.bytes = stdin + f.file = bytes.NewReader(f.bytes) } else { q, err := os.Open(arg.value) if err != nil { @@ -286,56 +262,70 @@ func process_arg(arg input_arg) { return } f.file = q + f.path = q.Name() + defer q.Close() + } + + var img *images.ImageData + var dopts []imaging.DecodeOption + needs_conversion := false + if flip { + dopts = append(dopts, imaging.Transform(imaging.FlipVTransform)) + needs_conversion = true + } + if flop { + dopts = append(dopts, imaging.Transform(imaging.FlipHTransform)) + needs_conversion = true + } + if remove_alpha != nil { + dopts = append(dopts, imaging.Background(*remove_alpha)) + needs_conversion = true + } + switch opts.Engine { + case "native", "builtin": + dopts = append(dopts, imaging.Backends(imaging.GO_IMAGE)) + case "magick": + dopts = append(dopts, imaging.Backends(imaging.MAGICK_IMAGE)) } - defer f.Release() - can_use_go := false - var c image.Config - var format string - var err error imgd := image_data{source_name: arg.value} - if opts.Engine == "auto" || opts.Engine == "builtin" { - c, format, err = image.DecodeConfig(f.file) - f.Rewind() - can_use_go = err == nil + dopts = append(dopts, imaging.ResizeCallback(func(w, h int) (int, int) { + imgd.canvas_width, imgd.canvas_height = w, h + set_basic_metadata(&imgd) + if scale_image(&imgd) { + needs_conversion = true + w, h = imgd.canvas_width, imgd.canvas_height + } + return w, h + })) + var err error + if f.path != "" { + img, err = images.OpenImageFromPath(f.path, dopts...) + } else { + img, f.file, err = images.OpenImageFromReader(f.file, dopts...) + } + if err != nil { + report_error(arg.value, "Could not render image to RGB", err) + return } if !keep_going.Load() { return } - if can_use_go { - imgd.canvas_width = c.Width - imgd.canvas_height = c.Height - imgd.format_uppercase = strings.ToUpper(format) - set_basic_metadata(&imgd) - if !imgd.needs_conversion { - make_output_from_input(&imgd, &f) - send_output(&imgd) - return - } - err = render_image_with_go(&imgd, &f) - if err != nil { - if opts.Engine != "builtin" { - merr := render_image_with_magick(&imgd, &f) - if merr != nil { - report_error(arg.value, "Could not render image to RGB", err) - return - } - err = nil - } - report_error(arg.value, "could not render", err) - return - } + imgd.format_uppercase = img.Format_uppercase + imgd.canvas_width, imgd.canvas_height = img.Width, img.Height + if !needs_conversion && imgd.format_uppercase == "PNG" && len(img.Frames) == 1 { + make_output_from_input(&imgd, &f) } else { - err = render_image_with_magick(&imgd, &f) - if err != nil { - report_error(arg.value, "ImageMagick failed", err) - return + for _, f := range img.Frames { + frame := add_frame(&imgd, f.Img, f.Left, f.Top) + frame.number, frame.compose_onto = int(f.Number), int(f.Compose_onto) + frame.replace = f.Replace + frame.delay_ms = int(f.Delay_ms) } } if !keep_going.Load() { return } send_output(&imgd) - } func run_worker() { diff --git a/kittens/icat/transmit.go b/kittens/icat/transmit.go index a5c4a3003..c6f92a81f 100644 --- a/kittens/icat/transmit.go +++ b/kittens/icat/transmit.go @@ -6,7 +6,6 @@ import ( "bytes" "crypto/rand" "encoding/binary" - "errors" "fmt" "github.com/kovidgoyal/kitty" "io" @@ -92,43 +91,32 @@ func transmit_shm(imgd *image_data, frame_num int, frame *image_frame) (err erro return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err) } defer f.Close() - data_size, _ = f.Seek(0, io.SeekEnd) - _, _ = f.Seek(0, io.SeekStart) - mmap, err = shm.CreateTemp("icat-*", uint64(data_size)) - if err != nil { + if data_size, err = f.Seek(0, io.SeekEnd); err != nil { + return fmt.Errorf("Failed to seek in image data output file: %s with error: %w", frame.filename, err) + } + if _, err = f.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("Failed to seek in image data output file: %s with error: %w", frame.filename, err) + } + if mmap, err = shm.CreateTemp("icat-*", uint64(data_size)); err != nil { return fmt.Errorf("Failed to create a SHM file for transmission: %w", err) } - dest := mmap.Slice() - for len(dest) > 0 { - n, err := f.Read(dest) - dest = dest[n:] - if err != nil { - if errors.Is(err, io.EOF) { - break - } - _ = mmap.Unlink() - return fmt.Errorf("Failed to read data from image output data file: %w", err) - } + if _, err = io.ReadFull(f, mmap.Slice()); err != nil { + mmap.Close() + mmap.Unlink() + return fmt.Errorf("Failed to read data from image output data file: %w", err) } } else { - if frame.shm == nil { - data_size = int64(len(frame.in_memory_bytes)) - mmap, err = shm.CreateTemp("icat-*", uint64(data_size)) - if err != nil { - return fmt.Errorf("Failed to create a SHM file for transmission: %w", err) - } - copy(mmap.Slice(), frame.in_memory_bytes) - } else { - mmap = frame.shm - frame.shm = nil + data_size = int64(len(frame.in_memory_bytes)) + if mmap, err = shm.CreateTemp("icat-*", uint64(data_size)); err != nil { + return fmt.Errorf("Failed to create a SHM file for transmission: %w", err) } + copy(mmap.Slice(), frame.in_memory_bytes) } + defer mmap.Close() // terminal is responsible for unlink gc := gc_for_image(imgd, frame_num, frame) gc.SetTransmission(graphics.GRT_transmission_sharedmem) gc.SetDataSize(uint64(data_size)) err = gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name())) - mmap.Close() - return } @@ -137,7 +125,6 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err fname := "" var data_size int if frame.in_memory_bytes == nil { - is_temp = frame.filename_is_temporary fname, err = filepath.Abs(frame.filename) if err != nil { return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err) @@ -145,30 +132,21 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err frame.filename = "" // so it isn't deleted in cleanup } else { is_temp = true - if frame.shm != nil && frame.shm.FileSystemName() != "" { - fname = frame.shm.FileSystemName() - frame.shm.Close() - frame.shm = nil - } else { - f, err := images.CreateTempInRAM() - if err != nil { - return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err) - } - data_size = len(frame.in_memory_bytes) - _, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f) - f.Close() - if err != nil { - return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err) - } - fname = f.Name() + f, err := images.CreateTempInRAM() + if err != nil { + return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err) } + data_size = len(frame.in_memory_bytes) + _, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f) + f.Close() + if err != nil { + os.Remove(f.Name()) + return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err) + } + fname = f.Name() } gc := gc_for_image(imgd, frame_num, frame) - if is_temp { - gc.SetTransmission(graphics.GRT_transmission_tempfile) - } else { - gc.SetTransmission(graphics.GRT_transmission_file) - } + gc.SetTransmission(utils.IfElse(is_temp, graphics.GRT_transmission_tempfile, graphics.GRT_transmission_file)) if data_size > 0 { gc.SetDataSize(uint64(data_size)) } @@ -178,14 +156,9 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err error) { data := frame.in_memory_bytes if data == nil { - f, err := os.Open(frame.filename) + data, err = os.ReadFile(frame.filename) if err != nil { - return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err) - } - data, err = io.ReadAll(f) - f.Close() - if err != nil { - return fmt.Errorf("Failed to read data from image output data file: %w", err) + return fmt.Errorf("Failed to read image data output file: %s with error: %w", frame.filename, err) } } gc := gc_for_image(imgd, frame_num, frame) @@ -282,20 +255,6 @@ func transmit_image(imgd *image_data, no_trailing_newline bool) { if seen_image_ids == nil { seen_image_ids = utils.NewSet[uint32](32) } - defer func() { - for _, frame := range imgd.frames { - if frame.filename_is_temporary && frame.filename != "" { - os.Remove(frame.filename) - frame.filename = "" - } - if frame.shm != nil { - _ = frame.shm.Unlink() - frame.shm.Close() - frame.shm = nil - } - frame.in_memory_bytes = nil - } - }() var f func(*image_data, int, *image_frame) error if opts.TransferMode != "detect" { switch opts.TransferMode { diff --git a/tools/utils/images/convert.go b/tools/utils/images/convert.go index cf24692d1..ecfb68e21 100644 --- a/tools/utils/images/convert.go +++ b/tools/utils/images/convert.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "github.com/kovidgoyal/imaging" "github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/utils" ) @@ -51,14 +52,10 @@ func encode_rgba(output io.Writer, img image.Image) (err error) { } func convert_image(input io.ReadSeeker, output io.Writer, format string) (err error) { - image_data, err := OpenNativeImageFromReader(input) + img, err := imaging.Decode(input) if err != nil { return err } - if len(image_data.Frames) == 0 { - return fmt.Errorf("Image has no frames") - } - img := image_data.Frames[0].Img q := strings.ToLower(format) if q == "rgba" { return encode_rgba(output, img) @@ -92,7 +89,7 @@ func images_equal(img, rimg *ImageData) (err error) { } func develop_serialize(input_data io.ReadSeeker) (err error) { - img, err := OpenNativeImageFromReader(input_data) + img, _, err := OpenImageFromReader(input_data) if err != nil { return err } @@ -113,7 +110,7 @@ func develop_resize(spec string, input_data io.ReadSeeker) (err error) { if h, err = strconv.Atoi(hs); err != nil { return } - img, err := OpenNativeImageFromReader(input_data) + img, _, err := OpenImageFromReader(input_data) if err != nil { return err } diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go index d57a39d53..73afb204a 100644 --- a/tools/utils/images/loading.go +++ b/tools/utils/images/loading.go @@ -3,24 +3,14 @@ package images import ( - "bytes" - "encoding/json" - "errors" "fmt" "image" - "image/gif" "image/png" "io" "os" - "os/exec" - "path/filepath" - "slices" - "strconv" "strings" - "sync" "github.com/kovidgoyal/imaging/nrgb" - "github.com/kovidgoyal/imaging/prism/meta/gifmeta" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/shm" @@ -78,71 +68,16 @@ func (s *ImageFrame) Serialize() SerializableImageFrame { } func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) { - bytes_per_pixel := 4 - if self.Is_opaque { - bytes_per_pixel = 3 - } - ans, err = shm.CreateTemp(pattern, uint64(self.Width*self.Height*bytes_per_pixel)) - if err != nil { + d := self.Data() + if ans, err = shm.CreateTemp(pattern, uint64(len(d))); err != nil { return nil, err } - switch img := self.Img.(type) { - case *imaging.NRGB: - if bytes_per_pixel == 3 { - copy(ans.Slice(), img.Pix) - return - } - case *image.NRGBA: - if bytes_per_pixel == 4 { - copy(ans.Slice(), img.Pix) - return - } - } - dest_rect := image.Rect(0, 0, self.Width, self.Height) - var final_img image.Image - switch bytes_per_pixel { - case 3: - rgb := &imaging.NRGB{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect} - final_img = rgb - case 4: - rgba := &image.NRGBA{Pix: ans.Slice(), Stride: bytes_per_pixel * self.Width, Rect: dest_rect} - final_img = rgba - } - ctx := Context{} - ctx.PasteCenter(final_img, self.Img, nil) + copy(ans.Slice(), d) return - } func (self *ImageFrame) Data() (ans []byte) { - bytes_per_pixel := 4 - if self.Is_opaque { - bytes_per_pixel = 3 - } - switch img := self.Img.(type) { - case *imaging.NRGB: - if bytes_per_pixel == 3 { - return img.Pix - } - case *image.NRGBA: - if bytes_per_pixel == 4 { - return img.Pix - } - } - dest_rect := image.Rect(0, 0, self.Width, self.Height) - var final_img image.Image - switch bytes_per_pixel { - case 3: - rgb := nrgb.NewNRGB(dest_rect) - final_img = rgb - ans = rgb.Pix - case 4: - rgba := image.NewNRGBA(dest_rect) - final_img = rgba - ans = rgba.Pix - } - ctx := Context{} - ctx.PasteCenter(final_img, self.Img, nil) + _, ans = imaging.AsRGBData8(self.Img) return } @@ -257,15 +192,14 @@ func MakeTempDir(template string) (ans string, err error) { return os.MkdirTemp("", template) } -// Native {{{ -func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) { - ic, _, err := imaging.DecodeAll(f) - if err != nil { - return nil, err +func NewImageData(ic *imaging.Image) (ans *ImageData) { + b := ic.Bounds() + ans = &ImageData{ + Width: b.Dx(), Height: b.Dy(), + } + if ic.Metadata != nil { + ans.Format_uppercase = strings.ToUpper(ic.Metadata.Format.String()) } - _, _ = f.Seek(0, io.SeekStart) - - ans = &ImageData{Width: int(ic.Metadata.PixelWidth), Height: int(ic.Metadata.PixelHeight), Format_uppercase: strings.ToUpper(ic.Metadata.Format.String())} for _, f := range ic.Frames { fr := ImageFrame{ @@ -281,413 +215,18 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) { return } -// }}} - -// ImageMagick {{{ -var MagickExe = sync.OnceValue(func() string { - return utils.FindExe("magick") -}) - -func check_resize(frame *ImageFrame, filename string) error { - // ImageMagick sometimes generates RGBA images smaller than the specified - // size. See https://github.com/kovidgoyal/kitty/issues/276 for examples - s, err := os.Stat(filename) +func OpenImageFromPath(path string, opts ...imaging.DecodeOption) (ans *ImageData, err error) { + ic, err := imaging.OpenAll(path, opts...) if err != nil { - return err + return nil, err } - sz := int(s.Size()) - bytes_per_pixel := 4 - if frame.Is_opaque { - bytes_per_pixel = 3 - } - expected_size := bytes_per_pixel * frame.Width * frame.Height - if sz < expected_size { - if bytes_per_pixel == 4 && sz == 3*frame.Width*frame.Height { - frame.Is_opaque = true - return nil - } - missing := expected_size - sz - if missing%(bytes_per_pixel*frame.Width) != 0 { - return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d frame-number: %d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel, frame.Number) - } - frame.Height -= missing / (bytes_per_pixel * frame.Width) - } - return nil + return NewImageData(ic), nil } -func RunMagick(path string, cmd []string) ([]byte, error) { - if MagickExe() != "magick" { - cmd = append([]string{MagickExe()}, cmd...) - } - c := exec.Command(cmd[0], cmd[1:]...) - output, err := c.Output() +func OpenImageFromReader(r io.Reader, opts ...imaging.DecodeOption) (ans *ImageData, s io.Reader, err error) { + ic, s, err := imaging.DecodeAll(r, opts...) if err != nil { - var exit_err *exec.ExitError - if errors.As(err, &exit_err) { - return nil, fmt.Errorf("Running the command: %s\nFailed with error:\n%s", strings.Join(cmd, " "), string(exit_err.Stderr)) - } - return nil, fmt.Errorf("Could not find the program: %#v. Is ImageMagick installed and in your PATH?", cmd[0]) + return nil, nil, err } - return output, nil -} - -type IdentifyOutput struct { - Fmt, Canvas, Transparency, Gap, Index, Size, Dpi, Dispose, Orientation string -} - -type IdentifyRecord struct { - Fmt_uppercase string - Gap int - Canvas struct{ Width, Height, Left, Top int } - Width, Height int - Dpi struct{ X, Y float64 } - Index int - Is_opaque bool - Needs_blend bool - Disposal int - Dimensions_swapped bool -} - -func parse_identify_record(ans *IdentifyRecord, raw *IdentifyOutput) (err error) { - ans.Fmt_uppercase = strings.ToUpper(raw.Fmt) - if raw.Gap != "" { - ans.Gap, err = strconv.Atoi(raw.Gap) - if err != nil { - return fmt.Errorf("Invalid gap value in identify output: %s", raw.Gap) - } - ans.Gap = max(0, ans.Gap) - } - area, pos, found := strings.Cut(raw.Canvas, "+") - ok := false - if found { - w, h, found := strings.Cut(area, "x") - if found { - ans.Canvas.Width, err = strconv.Atoi(w) - if err == nil { - ans.Canvas.Height, err = strconv.Atoi(h) - if err == nil { - x, y, found := strings.Cut(pos, "+") - if found { - ans.Canvas.Left, err = strconv.Atoi(x) - if err == nil { - if ans.Canvas.Top, err = strconv.Atoi(y); err == nil { - ok = true - } - } - } - } - } - } - } - if !ok { - return fmt.Errorf("Invalid canvas value in identify output: %s", raw.Canvas) - } - w, h, found := strings.Cut(raw.Size, "x") - ok = false - if found { - ans.Width, err = strconv.Atoi(w) - if err == nil { - if ans.Height, err = strconv.Atoi(h); err == nil { - ok = true - } - } - } - if !ok { - return fmt.Errorf("Invalid size value in identify output: %s", raw.Size) - } - x, y, found := strings.Cut(raw.Dpi, "x") - ok = false - if found { - ans.Dpi.X, err = strconv.ParseFloat(x, 64) - if err == nil { - if ans.Dpi.Y, err = strconv.ParseFloat(y, 64); err == nil { - ok = true - } - } - } - if !ok { - return fmt.Errorf("Invalid dpi value in identify output: %s", raw.Dpi) - } - ans.Index, err = strconv.Atoi(raw.Index) - if err != nil { - return fmt.Errorf("Invalid index value in identify output: %s", raw.Index) - } - q := strings.ToLower(raw.Transparency) - if q == "blend" || q == "true" { - ans.Is_opaque = false - } else { - ans.Is_opaque = true - } - ans.Needs_blend = q == "blend" - switch strings.ToLower(raw.Dispose) { - case "none", "undefined": - ans.Disposal = gif.DisposalNone - case "background": - ans.Disposal = gif.DisposalBackground - case "previous": - ans.Disposal = gif.DisposalPrevious - default: - return fmt.Errorf("Invalid value for dispose: %s", raw.Dispose) - } - switch raw.Orientation { - case "5", "6", "7", "8": - ans.Dimensions_swapped = true - } - if ans.Dimensions_swapped { - ans.Canvas.Width, ans.Canvas.Height = ans.Canvas.Height, ans.Canvas.Width - ans.Width, ans.Height = ans.Height, ans.Width - } - - return -} - -func IdentifyWithMagick(path, original_file_path string) (ans []IdentifyRecord, err error) { - cmd := []string{"identify"} - q := `{"fmt":"%m","canvas":"%g","transparency":"%A","gap":"%T","index":"%p","size":"%wx%h",` + - `"dpi":"%xx%y","dispose":"%D","orientation":"%[EXIF:Orientation]"},` - ext := filepath.Ext(original_file_path) - ipath := path - if strings.ToLower(ext) == ".apng" { - ipath = "APNG:" + path - } - cmd = append(cmd, "-format", q, "--", ipath) - output, err := RunMagick(path, cmd) - if err != nil { - return nil, fmt.Errorf("Failed to identify image at path: %s with error: %w", path, err) - } - output = bytes.TrimRight(bytes.TrimSpace(output), ",") - raw_json := make([]byte, 0, len(output)+2) - raw_json = append(raw_json, '[') - raw_json = append(raw_json, output...) - raw_json = append(raw_json, ']') - var records []IdentifyOutput - err = json.Unmarshal(raw_json, &records) - if err != nil { - return nil, fmt.Errorf("The ImageMagick identify program returned malformed output for the image at path: %s, with error: %w", path, err) - } - ans = make([]IdentifyRecord, len(records)) - for i, rec := range records { - err = parse_identify_record(&ans[i], &rec) - if err != nil { - return nil, err - } - } - return ans, nil -} - -type RenderOptions struct { - RemoveAlpha *imaging.NRGBColor - Flip, Flop bool - ResizeTo image.Point - OnlyFirstFrame bool - TempfilenameTemplate string -} - -func RenderWithMagick(path, original_file_path string, ro *RenderOptions, frames []IdentifyRecord) (ans []*ImageFrame, fmap map[int]string, err error) { - cmd := []string{"convert"} - ans = make([]*ImageFrame, 0, len(frames)) - fmap = make(map[int]string, len(frames)) - ext := filepath.Ext(original_file_path) - ipath := path - if strings.ToLower(ext) == ".apng" { - ipath = "APNG:" + path - } - - defer func() { - if err != nil { - for _, f := range fmap { - os.Remove(f) - } - } - }() - - if ro.RemoveAlpha != nil { - cmd = append(cmd, "-background", ro.RemoveAlpha.AsSharp(), "-alpha", "remove") - } else { - cmd = append(cmd, "-background", "none") - } - if ro.Flip { - cmd = append(cmd, "-flip") - } - if ro.Flop { - cmd = append(cmd, "-flop") - } - cpath := ipath - if ro.OnlyFirstFrame { - cpath += "[0]" - } - has_multiple_frames := len(frames) > 1 - get_multiple_frames := has_multiple_frames && !ro.OnlyFirstFrame - cmd = append(cmd, "--", cpath, "-auto-orient") - if ro.ResizeTo.X > 0 { - rcmd := []string{"-resize", fmt.Sprintf("%dx%d!", ro.ResizeTo.X, ro.ResizeTo.Y)} - if get_multiple_frames { - cmd = append(cmd, "-coalesce") - cmd = append(cmd, rcmd...) - cmd = append(cmd, "-deconstruct") - } else { - cmd = append(cmd, rcmd...) - } - } - cmd = append(cmd, "-depth", "8", "-set", "filename:f", "%w-%h-%g-%p") - if get_multiple_frames { - cmd = append(cmd, "+adjoin") - } - tdir, err := MakeTempDir(ro.TempfilenameTemplate) - if err != nil { - err = fmt.Errorf("Failed to create temporary directory to hold ImageMagick output with error: %w", err) - return - } - defer os.RemoveAll(tdir) - mode := "rgba" - if frames[0].Is_opaque { - mode = "rgb" - } - cmd = append(cmd, filepath.Join(tdir, "im-%[filename:f]."+mode)) - _, err = RunMagick(path, cmd) - if err != nil { - return - } - entries, err := os.ReadDir(tdir) - if err != nil { - err = fmt.Errorf("Failed to read temp dir used to store ImageMagick output with error: %w", err) - return - } - base_dir := filepath.Dir(tdir) - gaps := make([]int, len(frames)) - for i, frame := range frames { - gaps[i] = frame.Gap - } - // although ImageMagick *might* be already taking care of this adjustment, - // I dont know for sure, so do it anyway. - min_gap := gifmeta.CalcMinimumGap(gaps) - for _, entry := range entries { - fname := entry.Name() - p, _, _ := strings.Cut(fname, ".") - parts := strings.Split(p, "-") - if len(parts) < 5 { - continue - } - index, cerr := strconv.Atoi(parts[len(parts)-1]) - if cerr != nil || index < 0 || index >= len(frames) { - continue - } - width, cerr := strconv.Atoi(parts[1]) - if cerr != nil { - continue - } - height, cerr := strconv.Atoi(parts[2]) - if cerr != nil { - continue - } - _, pos, found := strings.Cut(parts[3], "+") - if !found { - continue - } - px, py, found := strings.Cut(pos, "+") - if !found { - continue - } - x, cerr := strconv.Atoi(px) - if cerr != nil { - continue - } - y, cerr := strconv.Atoi(py) - if cerr != nil { - continue - } - identify_data := frames[index] - df, cerr := os.CreateTemp(base_dir, TempTemplate+"."+mode) - if cerr != nil { - err = fmt.Errorf("Failed to create a temporary file in %s with error: %w", base_dir, cerr) - return - } - err = os.Rename(filepath.Join(tdir, fname), df.Name()) - if err != nil { - err = fmt.Errorf("Failed to rename a temporary file in %s with error: %w", tdir, err) - return - } - df.Close() - fmap[index+1] = df.Name() - frame := ImageFrame{ - Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque, - } - frame.Delay_ms = int32(max(min_gap, identify_data.Gap) * 10) - err = check_resize(&frame, df.Name()) - if err != nil { - return - } - ans = append(ans, &frame) - } - if len(ans) < len(frames) { - err = fmt.Errorf("Failed to render %d out of %d frames", len(frames)-len(ans), len(frames)) - return - } - slices.SortFunc(ans, func(a, b *ImageFrame) int { return a.Number - b.Number }) - prev_disposal := gif.DisposalBackground - prev_compose_onto := 0 - for i, frame := range ans { - switch prev_disposal { - case gif.DisposalNone: - frame.Compose_onto = frame.Number - 1 - case gif.DisposalPrevious: - frame.Compose_onto = prev_compose_onto - } - prev_disposal, prev_compose_onto = frames[i].Disposal, frame.Compose_onto - } - return -} - -func OpenImageFromPathWithMagick(path string) (ans *ImageData, err error) { - identify_records, err := IdentifyWithMagick(path, path) - if err != nil { - return nil, fmt.Errorf("Failed to identify image at %#v with error: %w", path, err) - } - frames, filenames, err := RenderWithMagick(path, path, &RenderOptions{}, identify_records) - if err != nil { - return nil, fmt.Errorf("Failed to render image at %#v with error: %w", path, err) - } - defer func() { - for _, f := range filenames { - os.Remove(f) - } - }() - - for _, frame := range frames { - filename := filenames[frame.Number] - data, err := os.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("Failed to read temp file for image %#v at %#v with error: %w", path, filename, err) - } - dest_rect := image.Rect(0, 0, frame.Width, frame.Height) - if frame.Is_opaque { - frame.Img = &imaging.NRGB{Pix: data, Stride: frame.Width * 3, Rect: dest_rect} - } else { - frame.Img = &image.NRGBA{Pix: data, Stride: frame.Width * 4, Rect: dest_rect} - } - } - ans = &ImageData{ - Width: frames[0].Width, Height: frames[0].Height, Format_uppercase: identify_records[0].Fmt_uppercase, Frames: frames, - } - return ans, nil -} - -// }}} - -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 OpenImageFromPathWithMagick(path) - } - } else { - return OpenImageFromPathWithMagick(path) - } - return + return NewImageData(ic), s, nil } diff --git a/tools/utils/images/serialize_test.go b/tools/utils/images/serialize_test.go index 286008889..5a278115d 100644 --- a/tools/utils/images/serialize_test.go +++ b/tools/utils/images/serialize_test.go @@ -12,7 +12,7 @@ import ( var _ = fmt.Print func TestImageSerialize(t *testing.T) { - img, err := OpenNativeImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData)) + img, _, err := OpenImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData)) if err != nil { t.Fatal(err) } diff --git a/tools/utils/images/transforms.go b/tools/utils/images/transforms.go deleted file mode 100644 index 8bf751414..000000000 --- a/tools/utils/images/transforms.go +++ /dev/null @@ -1,58 +0,0 @@ -// License: GPLv3 Copyright: 2023, Kovid Goyal, - -package images - -import ( - "fmt" -) - -var _ = fmt.Print - -func reverse_row(bytes_per_pixel int, pix []uint8) { - if len(pix) <= bytes_per_pixel { - return - } - i := 0 - j := len(pix) - bytes_per_pixel - for i < j { - pi := pix[i : i+bytes_per_pixel : i+bytes_per_pixel] - pj := pix[j : j+bytes_per_pixel : j+bytes_per_pixel] - for x := range bytes_per_pixel { - pi[x], pj[x] = pj[x], pi[x] - } - i += bytes_per_pixel - j -= bytes_per_pixel - } -} - -func (self *Context) FlipPixelsH(bytes_per_pixel, width, height int, pix []uint8) { - stride := bytes_per_pixel * width - if err := self.SafeParallel(0, height, func(ys <-chan int) { - for y := range ys { - i := y * stride - reverse_row(bytes_per_pixel, pix[i:i+stride]) - } - }); err != nil { - panic(err) - } -} - -func (self *Context) FlipPixelsV(bytes_per_pixel, width, height int, pix []uint8) { - stride := bytes_per_pixel * width - num := height / 2 - if err := self.SafeParallel(0, num, func(ys <-chan int) { - for y := range ys { - upper := y - lower := height - 1 - y - a := upper * stride - b := lower * stride - as := pix[a : a+stride : a+stride] - bs := pix[b : b+stride : b+stride] - for i := range as { - as[i], bs[i] = bs[i], as[i] - } - } - }); err != nil { - panic(err) - } -} diff --git a/tools/utils/shm/shm_fs.go b/tools/utils/shm/shm_fs.go index 73512a89b..a0d92abe8 100644 --- a/tools/utils/shm/shm_fs.go +++ b/tools/utils/shm/shm_fs.go @@ -33,9 +33,7 @@ func ShmUnlink(name string) error { if runtime.GOOS == "openbsd" { return os.Remove(openbsd_shm_path(name)) } - if strings.HasPrefix(name, "/") { - name = name[1:] - } + name = strings.TrimPrefix(name, "/") return os.Remove(filepath.Join(SHM_DIR, name)) }