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.
This commit is contained in:
Kovid Goyal
2025-11-10 11:34:56 +05:30
parent 6d4e6438f7
commit 1c8e8e9530
11 changed files with 181 additions and 989 deletions

View File

@@ -1,64 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

View File

@@ -1,169 +0,0 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

View File

@@ -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("<stdin>", "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() {

View File

@@ -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 {