mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
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:
4
go.mod
4
go.mod
@@ -13,13 +13,13 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||||
github.com/kovidgoyal/go-parallel v1.1.1
|
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/seancfoley/ipaddress-go v1.7.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.10
|
github.com/shirou/gopsutil/v4 v4.25.10
|
||||||
github.com/zeebo/xxh3 v1.0.2
|
github.com/zeebo/xxh3 v1.0.2
|
||||||
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
|
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
|
||||||
golang.org/x/image v0.32.0
|
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
|
golang.org/x/text v0.30.0
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
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/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 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
|
||||||
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
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.8 h1:PohlAOYuokFtmt6sjhgA90YAUKhuuL3i0dhd5gepp4g=
|
||||||
github.com/kovidgoyal/imaging v1.8.5/go.mod h1:fHutWjAiIZ3t7+YRVzuPkICFFzHTTkxgx2jSeQXKjE0=
|
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 h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
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=
|
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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -15,52 +15,15 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/imaging"
|
||||||
"github.com/kovidgoyal/kitty/tools/tty"
|
"github.com/kovidgoyal/kitty/tools/tty"
|
||||||
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = fmt.Print
|
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 {
|
type input_arg struct {
|
||||||
arg string
|
arg string
|
||||||
value string
|
value string
|
||||||
@@ -120,54 +83,14 @@ func process_dirs(args ...string) (results []input_arg, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type opened_input struct {
|
type opened_input struct {
|
||||||
file io.ReadSeekCloser
|
file io.Reader
|
||||||
name_to_unlink string
|
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 {
|
type image_frame struct {
|
||||||
filename string
|
filename string
|
||||||
shm shm.MMap
|
|
||||||
in_memory_bytes []byte
|
in_memory_bytes []byte
|
||||||
filename_is_temporary bool
|
|
||||||
width, height, left, top int
|
width, height, left, top int
|
||||||
transmission_format graphics.GRT_f
|
transmission_format graphics.GRT_f
|
||||||
compose_onto int
|
compose_onto int
|
||||||
@@ -180,8 +103,7 @@ type image_data struct {
|
|||||||
canvas_width, canvas_height int
|
canvas_width, canvas_height int
|
||||||
format_uppercase string
|
format_uppercase string
|
||||||
available_width, available_height int
|
available_width, available_height int
|
||||||
needs_scaling, needs_conversion bool
|
needs_scaling bool
|
||||||
scaled_frac struct{ x, y float64 }
|
|
||||||
frames []*image_frame
|
frames []*image_frame
|
||||||
image_number uint32
|
image_number uint32
|
||||||
image_id 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_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) {
|
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) {
|
func make_output_from_input(imgd *image_data, f *opened_input) {
|
||||||
bb, ok := f.file.(*BytesBuf)
|
|
||||||
frame := image_frame{}
|
frame := image_frame{}
|
||||||
imgd.frames = append(imgd.frames, &frame)
|
imgd.frames = append(imgd.frames, &frame)
|
||||||
frame.width = imgd.canvas_width
|
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))
|
panic(fmt.Sprintf("Unknown transmission format: %s", imgd.format_uppercase))
|
||||||
}
|
}
|
||||||
frame.transmission_format = graphics.GRT_format_png
|
frame.transmission_format = graphics.GRT_format_png
|
||||||
if ok {
|
if f.bytes != nil {
|
||||||
frame.in_memory_bytes = bb.data
|
frame.in_memory_bytes = f.bytes
|
||||||
|
} else if f.path != "" {
|
||||||
|
frame.filename = f.path
|
||||||
} else {
|
} else {
|
||||||
frame.filename = f.file.(*os.File).Name()
|
var err error
|
||||||
if f.name_to_unlink != "" {
|
if frame.in_memory_bytes, err = io.ReadAll(f.file); err != nil {
|
||||||
frame.filename_is_temporary = true
|
panic(err)
|
||||||
f.name_to_unlink = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func process_arg(arg input_arg) {
|
||||||
var f opened_input
|
var f opened_input
|
||||||
if arg.is_http_url {
|
if arg.is_http_url {
|
||||||
@@ -271,14 +245,16 @@ func process_arg(arg input_arg) {
|
|||||||
report_error(arg.value, "Could not download", err)
|
report_error(arg.value, "Could not download", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.file = &BytesBuf{data: dest.Bytes()}
|
f.bytes = dest.Bytes()
|
||||||
|
f.file = bytes.NewReader(f.bytes)
|
||||||
} else if arg.value == "" {
|
} else if arg.value == "" {
|
||||||
stdin, err := io.ReadAll(os.Stdin)
|
stdin, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report_error("<stdin>", "Could not read from", err)
|
report_error("<stdin>", "Could not read from", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.file = &BytesBuf{data: stdin}
|
f.bytes = stdin
|
||||||
|
f.file = bytes.NewReader(f.bytes)
|
||||||
} else {
|
} else {
|
||||||
q, err := os.Open(arg.value)
|
q, err := os.Open(arg.value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -286,56 +262,70 @@ func process_arg(arg input_arg) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.file = q
|
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}
|
imgd := image_data{source_name: arg.value}
|
||||||
if opts.Engine == "auto" || opts.Engine == "builtin" {
|
dopts = append(dopts, imaging.ResizeCallback(func(w, h int) (int, int) {
|
||||||
c, format, err = image.DecodeConfig(f.file)
|
imgd.canvas_width, imgd.canvas_height = w, h
|
||||||
f.Rewind()
|
|
||||||
can_use_go = err == nil
|
|
||||||
}
|
|
||||||
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)
|
set_basic_metadata(&imgd)
|
||||||
if !imgd.needs_conversion {
|
if scale_image(&imgd) {
|
||||||
make_output_from_input(&imgd, &f)
|
needs_conversion = true
|
||||||
send_output(&imgd)
|
w, h = imgd.canvas_width, imgd.canvas_height
|
||||||
return
|
}
|
||||||
|
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...)
|
||||||
}
|
}
|
||||||
err = render_image_with_go(&imgd, &f)
|
|
||||||
if err != nil {
|
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)
|
report_error(arg.value, "Could not render image to RGB", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = nil
|
if !keep_going.Load() {
|
||||||
}
|
|
||||||
report_error(arg.value, "could not render", err)
|
|
||||||
return
|
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 {
|
} else {
|
||||||
err = render_image_with_magick(&imgd, &f)
|
for _, f := range img.Frames {
|
||||||
if err != nil {
|
frame := add_frame(&imgd, f.Img, f.Left, f.Top)
|
||||||
report_error(arg.value, "ImageMagick failed", err)
|
frame.number, frame.compose_onto = int(f.Number), int(f.Compose_onto)
|
||||||
return
|
frame.replace = f.Replace
|
||||||
|
frame.delay_ms = int(f.Delay_ms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !keep_going.Load() {
|
if !keep_going.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
send_output(&imgd)
|
send_output(&imgd)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func run_worker() {
|
func run_worker() {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/kovidgoyal/kitty"
|
"github.com/kovidgoyal/kitty"
|
||||||
"io"
|
"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)
|
return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
data_size, _ = f.Seek(0, io.SeekEnd)
|
if data_size, err = f.Seek(0, io.SeekEnd); err != nil {
|
||||||
_, _ = f.Seek(0, io.SeekStart)
|
return fmt.Errorf("Failed to seek in image data output file: %s with error: %w", frame.filename, err)
|
||||||
mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
|
}
|
||||||
if err != nil {
|
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)
|
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
|
||||||
}
|
}
|
||||||
dest := mmap.Slice()
|
if _, err = io.ReadFull(f, mmap.Slice()); err != nil {
|
||||||
for len(dest) > 0 {
|
mmap.Close()
|
||||||
n, err := f.Read(dest)
|
mmap.Unlink()
|
||||||
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)
|
return fmt.Errorf("Failed to read data from image output data file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if frame.shm == nil {
|
|
||||||
data_size = int64(len(frame.in_memory_bytes))
|
data_size = int64(len(frame.in_memory_bytes))
|
||||||
mmap, err = shm.CreateTemp("icat-*", uint64(data_size))
|
if mmap, err = shm.CreateTemp("icat-*", uint64(data_size)); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
|
return fmt.Errorf("Failed to create a SHM file for transmission: %w", err)
|
||||||
}
|
}
|
||||||
copy(mmap.Slice(), frame.in_memory_bytes)
|
copy(mmap.Slice(), frame.in_memory_bytes)
|
||||||
} else {
|
|
||||||
mmap = frame.shm
|
|
||||||
frame.shm = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
defer mmap.Close() // terminal is responsible for unlink
|
||||||
gc := gc_for_image(imgd, frame_num, frame)
|
gc := gc_for_image(imgd, frame_num, frame)
|
||||||
gc.SetTransmission(graphics.GRT_transmission_sharedmem)
|
gc.SetTransmission(graphics.GRT_transmission_sharedmem)
|
||||||
gc.SetDataSize(uint64(data_size))
|
gc.SetDataSize(uint64(data_size))
|
||||||
err = gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
|
err = gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
|
||||||
mmap.Close()
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +125,6 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
|
|||||||
fname := ""
|
fname := ""
|
||||||
var data_size int
|
var data_size int
|
||||||
if frame.in_memory_bytes == nil {
|
if frame.in_memory_bytes == nil {
|
||||||
is_temp = frame.filename_is_temporary
|
|
||||||
fname, err = filepath.Abs(frame.filename)
|
fname, err = filepath.Abs(frame.filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err)
|
return fmt.Errorf("Failed to convert image data output file: %s to absolute path with error: %w", frame.filename, err)
|
||||||
@@ -145,11 +132,6 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
|
|||||||
frame.filename = "" // so it isn't deleted in cleanup
|
frame.filename = "" // so it isn't deleted in cleanup
|
||||||
} else {
|
} else {
|
||||||
is_temp = true
|
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()
|
f, err := images.CreateTempInRAM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
|
return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err)
|
||||||
@@ -158,17 +140,13 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
|
|||||||
_, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
|
_, err = bytes.NewBuffer(frame.in_memory_bytes).WriteTo(f)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
os.Remove(f.Name())
|
||||||
return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
|
return fmt.Errorf("Failed to write image data to temp file for transmission: %w", err)
|
||||||
}
|
}
|
||||||
fname = f.Name()
|
fname = f.Name()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
gc := gc_for_image(imgd, frame_num, frame)
|
gc := gc_for_image(imgd, frame_num, frame)
|
||||||
if is_temp {
|
gc.SetTransmission(utils.IfElse(is_temp, graphics.GRT_transmission_tempfile, graphics.GRT_transmission_file))
|
||||||
gc.SetTransmission(graphics.GRT_transmission_tempfile)
|
|
||||||
} else {
|
|
||||||
gc.SetTransmission(graphics.GRT_transmission_file)
|
|
||||||
}
|
|
||||||
if data_size > 0 {
|
if data_size > 0 {
|
||||||
gc.SetDataSize(uint64(data_size))
|
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) {
|
func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err error) {
|
||||||
data := frame.in_memory_bytes
|
data := frame.in_memory_bytes
|
||||||
if data == nil {
|
if data == nil {
|
||||||
f, err := os.Open(frame.filename)
|
data, err = os.ReadFile(frame.filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to open image data output file: %s with error: %w", frame.filename, err)
|
return fmt.Errorf("Failed to read 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gc := gc_for_image(imgd, frame_num, frame)
|
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 {
|
if seen_image_ids == nil {
|
||||||
seen_image_ids = utils.NewSet[uint32](32)
|
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
|
var f func(*image_data, int, *image_frame) error
|
||||||
if opts.TransferMode != "detect" {
|
if opts.TransferMode != "detect" {
|
||||||
switch opts.TransferMode {
|
switch opts.TransferMode {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kovidgoyal/imaging"
|
||||||
"github.com/kovidgoyal/kitty/tools/cli"
|
"github.com/kovidgoyal/kitty/tools/cli"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"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) {
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
q := strings.ToLower(format)
|
||||||
if q == "rgba" {
|
if q == "rgba" {
|
||||||
return encode_rgba(output, img)
|
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) {
|
func develop_serialize(input_data io.ReadSeeker) (err error) {
|
||||||
img, err := OpenNativeImageFromReader(input_data)
|
img, _, err := OpenImageFromReader(input_data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if h, err = strconv.Atoi(hs); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
img, err := OpenNativeImageFromReader(input_data)
|
img, _, err := OpenImageFromReader(input_data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,14 @@
|
|||||||
package images
|
package images
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/gif"
|
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/kovidgoyal/imaging/nrgb"
|
"github.com/kovidgoyal/imaging/nrgb"
|
||||||
"github.com/kovidgoyal/imaging/prism/meta/gifmeta"
|
|
||||||
"github.com/kovidgoyal/kitty/tools/utils"
|
"github.com/kovidgoyal/kitty/tools/utils"
|
||||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
"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) {
|
func (self *ImageFrame) DataAsSHM(pattern string) (ans shm.MMap, err error) {
|
||||||
bytes_per_pixel := 4
|
d := self.Data()
|
||||||
if self.Is_opaque {
|
if ans, err = shm.CreateTemp(pattern, uint64(len(d))); err != nil {
|
||||||
bytes_per_pixel = 3
|
|
||||||
}
|
|
||||||
ans, err = shm.CreateTemp(pattern, uint64(self.Width*self.Height*bytes_per_pixel))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
switch img := self.Img.(type) {
|
copy(ans.Slice(), d)
|
||||||
case *imaging.NRGB:
|
|
||||||
if bytes_per_pixel == 3 {
|
|
||||||
copy(ans.Slice(), img.Pix)
|
|
||||||
return
|
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)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *ImageFrame) Data() (ans []byte) {
|
func (self *ImageFrame) Data() (ans []byte) {
|
||||||
bytes_per_pixel := 4
|
_, ans = imaging.AsRGBData8(self.Img)
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,15 +192,14 @@ func MakeTempDir(template string) (ans string, err error) {
|
|||||||
return os.MkdirTemp("", template)
|
return os.MkdirTemp("", template)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native {{{
|
func NewImageData(ic *imaging.Image) (ans *ImageData) {
|
||||||
func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
b := ic.Bounds()
|
||||||
ic, _, err := imaging.DecodeAll(f)
|
ans = &ImageData{
|
||||||
if err != nil {
|
Width: b.Dx(), Height: b.Dy(),
|
||||||
return nil, err
|
}
|
||||||
|
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 {
|
for _, f := range ic.Frames {
|
||||||
fr := ImageFrame{
|
fr := ImageFrame{
|
||||||
@@ -281,413 +215,18 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// }}}
|
func OpenImageFromPath(path string, opts ...imaging.DecodeOption) (ans *ImageData, err error) {
|
||||||
|
ic, err := imaging.OpenAll(path, opts...)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
return 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
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 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
return NewImageData(ic), nil
|
||||||
return ans, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderOptions struct {
|
func OpenImageFromReader(r io.Reader, opts ...imaging.DecodeOption) (ans *ImageData, s io.Reader, err error) {
|
||||||
RemoveAlpha *imaging.NRGBColor
|
ic, s, err := imaging.DecodeAll(r, opts...)
|
||||||
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 {
|
if err != nil {
|
||||||
for _, f := range fmap {
|
return nil, nil, err
|
||||||
os.Remove(f)
|
|
||||||
}
|
}
|
||||||
}
|
return NewImageData(ic), s, nil
|
||||||
}()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
var _ = fmt.Print
|
var _ = fmt.Print
|
||||||
|
|
||||||
func TestImageSerialize(t *testing.T) {
|
func TestImageSerialize(t *testing.T) {
|
||||||
img, err := OpenNativeImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData))
|
img, _, err := OpenImageFromReader(bytes.NewReader(kitty.KittyLogoAsPNGData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,9 +33,7 @@ func ShmUnlink(name string) error {
|
|||||||
if runtime.GOOS == "openbsd" {
|
if runtime.GOOS == "openbsd" {
|
||||||
return os.Remove(openbsd_shm_path(name))
|
return os.Remove(openbsd_shm_path(name))
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(name, "/") {
|
name = strings.TrimPrefix(name, "/")
|
||||||
name = name[1:]
|
|
||||||
}
|
|
||||||
return os.Remove(filepath.Join(SHM_DIR, name))
|
return os.Remove(filepath.Join(SHM_DIR, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user