mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
Update to the latest version of imaging
Gives us a bunch of new features and allows us to move a bunch of code into imaging
This commit is contained in:
@@ -144,6 +144,8 @@ Detailed list of changes
|
||||
|
||||
- ssh kitten: Fix a bug where automatic login was not working (:iss:`9187`)
|
||||
|
||||
- icat kitten: Add support for APNG, netPBM, ICC color profiles and CCIP metadata to the builtin engine
|
||||
|
||||
0.44.0 [2025-11-03]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
6
go.mod
6
go.mod
@@ -12,9 +12,8 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
|
||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032
|
||||
github.com/kovidgoyal/go-parallel v1.0.1
|
||||
github.com/kovidgoyal/imaging v1.7.2
|
||||
github.com/kovidgoyal/go-parallel v1.1.1
|
||||
github.com/kovidgoyal/imaging v1.8.2
|
||||
github.com/seancfoley/ipaddress-go v1.7.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.10
|
||||
github.com/zeebo/xxh3 v1.0.2
|
||||
@@ -34,6 +33,7 @@ require (
|
||||
require (
|
||||
github.com/ebitengine/purego v0.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
|
||||
12
go.sum
12
go.sum
@@ -24,16 +24,16 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253 h1:ar6YqPcuumkcWgAJHkmda6Q35V3OnpxeTej4iU/QFLA=
|
||||
github.com/kettek/apng v0.0.0-20250827064933-2bb5f5fcf253/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
|
||||
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1/go.mod h1:RbNG3Q1g6GUy1/WzWVx+S24m7VKyvl57vV+cr2hpt50=
|
||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032 h1:TEV9lpo2a6fP1byiDsoJe2fXpvrj2itae41xMM+bEAg=
|
||||
github.com/kovidgoyal/exiffix v0.0.0-20250919160812-dbef770c2032/go.mod h1:VU38Nlbvb0lbyS5YkopCZMS5HuJ5QLVJBxRWyzq79q4=
|
||||
github.com/kovidgoyal/go-parallel v1.0.1 h1:nYUjN+EdpbmQjTg3N5eTUInuXTB3/1oD2vHdaMfuHoI=
|
||||
github.com/kovidgoyal/go-parallel v1.0.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
|
||||
github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c=
|
||||
github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU=
|
||||
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.2 h1:AYBaYSAI7W6p2CfQnAqVEB6AeiOVMJ0uUEuhuM+zuWo=
|
||||
github.com/kovidgoyal/imaging v1.8.2/go.mod h1:fHutWjAiIZ3t7+YRVzuPkICFFzHTTkxgx2jSeQXKjE0=
|
||||
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=
|
||||
|
||||
@@ -210,6 +210,9 @@ func (self *GraphicsHandler) transmit(lp *loop.Loop, img *images.ImageData, m *i
|
||||
default:
|
||||
gc.SetAction(graphics.GRT_action_frame)
|
||||
gc.SetGap(int32(frame.Delay_ms))
|
||||
if frame.Replace {
|
||||
gc.SetCompositionMode(graphics.Overwrite)
|
||||
}
|
||||
if frame.Compose_onto > 0 {
|
||||
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ package icat
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
|
||||
"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"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/shm"
|
||||
|
||||
"github.com/kovidgoyal/exiffix"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
@@ -33,19 +31,19 @@ func resize_frame(imgd *image_data, img image.Image) (image.Image, image.Rectang
|
||||
|
||||
const shm_template = "kitty-icat-*"
|
||||
|
||||
func add_frame(ctx *images.Context, imgd *image_data, img image.Image) *image_frame {
|
||||
func add_frame(ctx *images.Context, imgd *image_data, img image.Image, left, top int) *image_frame {
|
||||
is_opaque := false
|
||||
if imgd.format_uppercase == "JPEG" {
|
||||
// special cased because EXIF orientation could have already changed this image to an NRGBA making IsOpaque() very slow
|
||||
is_opaque = true
|
||||
} else {
|
||||
is_opaque = images.IsOpaque(img)
|
||||
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: b.Min.X, top: b.Min.Y}
|
||||
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
|
||||
@@ -55,7 +53,7 @@ func add_frame(ctx *images.Context, imgd *image_data, img image.Image) *image_fr
|
||||
bytes_per_pixel = 3
|
||||
m, err := shm.CreateTemp(shm_template, uint64(f.width*f.height*bytes_per_pixel))
|
||||
if err != nil {
|
||||
rgb = imaging.NewNRGB(dest_rect)
|
||||
rgb = nrgb.NewNRGB(dest_rect)
|
||||
} else {
|
||||
rgb = &imaging.NRGB{Pix: m.Slice(), Stride: bytes_per_pixel * f.width, Rect: dest_rect}
|
||||
f.shm = m
|
||||
@@ -111,47 +109,21 @@ func scale_image(imgd *image_data) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func load_one_frame_image(imgd *image_data, src *opened_input) (img image.Image, err error) {
|
||||
img, _, err = exiffix.Decode(src.file)
|
||||
src.Rewind()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// reset the sizes as we read EXIF tags here which could have rotated the image
|
||||
imgd.canvas_width = img.Bounds().Dx()
|
||||
imgd.canvas_height = img.Bounds().Dy()
|
||||
set_basic_metadata(imgd)
|
||||
scale_image(imgd)
|
||||
return
|
||||
}
|
||||
|
||||
var debugprintln = tty.DebugPrintln
|
||||
var _ = debugprintln
|
||||
|
||||
func (frame *image_frame) set_disposal(anchor_frame int, disposal byte) int {
|
||||
anchor_frame, frame.compose_onto = images.SetGIFFrameDisposal(frame.number, anchor_frame, disposal)
|
||||
return anchor_frame
|
||||
}
|
||||
|
||||
func (frame *image_frame) set_delay(gap, min_gap int) {
|
||||
frame.delay_ms = utils.Max(min_gap, gap) * 10
|
||||
if frame.delay_ms == 0 {
|
||||
frame.delay_ms = -1
|
||||
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.X, f.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 add_gif_frames(ctx *images.Context, imgd *image_data, gf *gif.GIF) error {
|
||||
min_gap := images.CalcMinimumGIFGap(gf.Delay)
|
||||
scale_image(imgd)
|
||||
anchor_frame := 1
|
||||
for i, paletted_img := range gf.Image {
|
||||
frame := add_frame(ctx, imgd, paletted_img)
|
||||
frame.set_delay(gf.Delay[i], min_gap)
|
||||
anchor_frame = frame.set_disposal(anchor_frame, gf.Disposal[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func render_image_with_go(imgd *image_data, src *opened_input) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -159,23 +131,19 @@ func render_image_with_go(imgd *image_data, src *opened_input) (err error) {
|
||||
}
|
||||
}()
|
||||
ctx := images.Context{}
|
||||
switch {
|
||||
case imgd.format_uppercase == "GIF" && opts.Loop != 0:
|
||||
gif_frames, err := gif.DecodeAll(src.file)
|
||||
src.Rewind()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to decode GIF file with error: %w", err)
|
||||
}
|
||||
err = add_gif_frames(&ctx, imgd, gif_frames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
img, err := load_one_frame_image(imgd, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
add_frame(&ctx, imgd, img)
|
||||
imgs, _, err := imaging.DecodeAll(src.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if imgs == nil {
|
||||
return fmt.Errorf("unknown image format")
|
||||
}
|
||||
// Loading could auto orient and therefore change width/height, so
|
||||
// re-calculate
|
||||
imgd.canvas_width = int(imgs.Metadata.PixelWidth)
|
||||
imgd.canvas_height = int(imgs.Metadata.PixelHeight)
|
||||
set_basic_metadata(imgd)
|
||||
scale_image(imgd)
|
||||
add_frames(&ctx, imgd, imgs)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
@@ -171,8 +170,8 @@ type image_frame struct {
|
||||
width, height, left, top int
|
||||
transmission_format graphics.GRT_f
|
||||
compose_onto int
|
||||
replace bool
|
||||
number int
|
||||
disposal_background color.NRGBA
|
||||
delay_ms int
|
||||
}
|
||||
|
||||
|
||||
@@ -74,11 +74,9 @@ func gc_for_image(imgd *image_data, frame_num int, frame *image_frame) *graphics
|
||||
} else {
|
||||
gc.SetAction(graphics.GRT_action_frame)
|
||||
gc.SetGap(int32(frame.delay_ms))
|
||||
gc.SetCompositionMode(utils.IfElse(frame.replace, graphics.Overwrite, graphics.AlphaBlend))
|
||||
if frame.compose_onto > 0 {
|
||||
gc.SetOverlaidFrame(uint64(frame.compose_onto))
|
||||
} else {
|
||||
bg := (uint32(frame.disposal_background.R) << 24) | (uint32(frame.disposal_background.G) << 16) | (uint32(frame.disposal_background.B) << 8) | uint32(frame.disposal_background.A)
|
||||
gc.SetBackgroundColor(bg)
|
||||
}
|
||||
gc.SetLeftEdge(uint64(frame.left)).SetTopEdge(uint64(frame.top))
|
||||
}
|
||||
|
||||
@@ -403,6 +403,9 @@ func (self *ImageCollection) transmit_rendering(lp *loop.Loop, r *rendering) {
|
||||
if frame.Compose_onto > 0 {
|
||||
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
|
||||
}
|
||||
if frame.Replace {
|
||||
gc.SetCompositionMode(Overwrite)
|
||||
}
|
||||
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
|
||||
}
|
||||
transmit(lp, r.image_id, self.temp_file_map, frame, gc)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"io"
|
||||
@@ -20,10 +19,11 @@ import (
|
||||
"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"
|
||||
|
||||
"github.com/kovidgoyal/exiffix"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
@@ -50,15 +50,17 @@ type ImageFrame struct {
|
||||
Number int // 1-based number
|
||||
Compose_onto int // number of frame to compose onto
|
||||
Delay_ms int32 // negative for gapless frame, zero ignored, positive is number of ms
|
||||
Replace bool // do a replace rather than an alpha blend
|
||||
Is_opaque bool
|
||||
Img image.Image
|
||||
}
|
||||
|
||||
type SerializableImageFrame struct {
|
||||
Width, Height, Left, Top int
|
||||
Number int // 1-based number
|
||||
Compose_onto int // number of frame to compose onto
|
||||
Delay_ms int // negative for gapless frame, zero ignored, positive is number of ms
|
||||
Number int // 1-based number
|
||||
Compose_onto int // number of frame to compose onto
|
||||
Delay_ms int // negative for gapless frame, zero ignored, positive is number of ms
|
||||
Replace bool // do a replace rather than an alpha blend
|
||||
Is_opaque bool
|
||||
Size int
|
||||
}
|
||||
@@ -71,7 +73,7 @@ func (s *ImageFrame) Serialize() SerializableImageFrame {
|
||||
return SerializableImageFrame{
|
||||
Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top,
|
||||
Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int(s.Delay_ms),
|
||||
Is_opaque: s.Is_opaque,
|
||||
Is_opaque: s.Is_opaque, Replace: s.Replace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +133,7 @@ func (self *ImageFrame) Data() (ans []byte) {
|
||||
var final_img image.Image
|
||||
switch bytes_per_pixel {
|
||||
case 3:
|
||||
rgb := imaging.NewNRGB(dest_rect)
|
||||
rgb := nrgb.NewNRGB(dest_rect)
|
||||
final_img = rgb
|
||||
ans = rgb.Pix
|
||||
case 4:
|
||||
@@ -148,14 +150,14 @@ func ImageFrameFromSerialized(s SerializableImageFrame, data []byte) (aa *ImageF
|
||||
ans := ImageFrame{
|
||||
Width: s.Width, Height: s.Height, Left: s.Left, Top: s.Top,
|
||||
Number: s.Number, Compose_onto: s.Compose_onto, Delay_ms: int32(s.Delay_ms),
|
||||
Is_opaque: s.Is_opaque,
|
||||
Is_opaque: s.Is_opaque, Replace: s.Replace,
|
||||
}
|
||||
bytes_per_pixel := utils.IfElse(s.Is_opaque, 3, 4)
|
||||
if expected := bytes_per_pixel * s.Width * s.Height; len(data) != expected {
|
||||
return nil, fmt.Errorf("serialized image data has size: %d != %d", len(data), expected)
|
||||
}
|
||||
if s.Is_opaque {
|
||||
ans.Img, err = imaging.NewNRGBWithContiguousRGBPixels(data, s.Left, s.Top, s.Width, s.Height)
|
||||
ans.Img, err = nrgb.NewNRGBWithContiguousRGBPixels(data, s.Left, s.Top, s.Width, s.Height)
|
||||
} else {
|
||||
ans.Img, err = NewNRGBAWithContiguousRGBAPixels(data, s.Left, s.Top, s.Width, s.Height)
|
||||
}
|
||||
@@ -242,37 +244,6 @@ func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
|
||||
return &ans
|
||||
}
|
||||
|
||||
func CalcMinimumGIFGap(gaps []int) int {
|
||||
// Some broken GIF images have all zero gaps, browsers with their usual
|
||||
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
|
||||
// Browsers actually force a 100ms gap at any zero gap frame, but that
|
||||
// just means it is impossible to deliberately use zero gap frames for
|
||||
// sophisticated blending, so we dont do that.
|
||||
max_gap := utils.Max(0, gaps...)
|
||||
min_gap := 0
|
||||
if max_gap <= 0 {
|
||||
min_gap = 10
|
||||
}
|
||||
return min_gap
|
||||
}
|
||||
|
||||
func SetGIFFrameDisposal(number, anchor_frame int, disposal byte) (int, int) {
|
||||
compose_onto := 0
|
||||
if number > 1 {
|
||||
switch disposal {
|
||||
case gif.DisposalNone:
|
||||
compose_onto = number - 1
|
||||
anchor_frame = number
|
||||
case gif.DisposalBackground:
|
||||
// see https://github.com/golang/go/issues/20694
|
||||
anchor_frame = number
|
||||
case gif.DisposalPrevious:
|
||||
compose_onto = anchor_frame
|
||||
}
|
||||
}
|
||||
return anchor_frame, compose_onto
|
||||
}
|
||||
|
||||
func MakeTempDir(template string) (ans string, err error) {
|
||||
if template == "" {
|
||||
template = "kitty-img-*"
|
||||
@@ -287,51 +258,25 @@ func MakeTempDir(template string) (ans string, err error) {
|
||||
}
|
||||
|
||||
// Native {{{
|
||||
func (frame *ImageFrame) set_delay(min_gap, delay int) {
|
||||
frame.Delay_ms = int32(max(min_gap, delay) * 10)
|
||||
if frame.Delay_ms == 0 {
|
||||
frame.Delay_ms = -1 // gapless frame in the graphics protocol
|
||||
}
|
||||
}
|
||||
|
||||
func open_native_gif(f io.Reader, ans *ImageData) error {
|
||||
gif_frames, err := gif.DecodeAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
min_gap := CalcMinimumGIFGap(gif_frames.Delay)
|
||||
anchor_frame := 1
|
||||
for i, paletted_img := range gif_frames.Image {
|
||||
b := paletted_img.Bounds()
|
||||
frame := ImageFrame{Img: paletted_img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy(), Number: len(ans.Frames) + 1, Is_opaque: paletted_img.Opaque()}
|
||||
frame.set_delay(min_gap, gif_frames.Delay[i])
|
||||
anchor_frame, frame.Compose_onto = SetGIFFrameDisposal(frame.Number, anchor_frame, gif_frames.Disposal[i])
|
||||
ans.Frames = append(ans.Frames, &frame)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
||||
c, fmt, err := image.DecodeConfig(f)
|
||||
ic, _, err := imaging.DecodeAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = f.Seek(0, io.SeekStart)
|
||||
ans = &ImageData{Width: c.Width, Height: c.Height, Format_uppercase: strings.ToUpper(fmt)}
|
||||
|
||||
if ans.Format_uppercase == "GIF" {
|
||||
err = open_native_gif(f, ans)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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{
|
||||
Img: f.Image, Left: f.X, Top: f.Y, Width: f.Image.Bounds().Dx(), Height: f.Image.Bounds().Dy(),
|
||||
Compose_onto: int(f.ComposeOnto), Number: int(f.Number), Delay_ms: int32(f.Delay.Milliseconds()),
|
||||
Replace: f.Replace, Is_opaque: imaging.IsOpaque(f.Image),
|
||||
}
|
||||
} else {
|
||||
img, _, err := exiffix.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if fr.Delay_ms <= 0 {
|
||||
fr.Delay_ms = -1 // -1 is gapless in graphics protocol
|
||||
}
|
||||
b := img.Bounds()
|
||||
ans.Frames = []*ImageFrame{{Img: img, Left: b.Min.X, Top: b.Min.Y, Width: b.Dx(), Height: b.Dy()}}
|
||||
ans.Frames[0].Is_opaque = c.ColorModel == color.YCbCrModel || c.ColorModel == color.GrayModel || c.ColorModel == color.Gray16Model || c.ColorModel == color.CMYKModel || ans.Format_uppercase == "JPEG" || ans.Format_uppercase == "JPG" || IsOpaque(img)
|
||||
ans.Frames = append(ans.Frames, &fr)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -601,7 +546,9 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
|
||||
for i, frame := range frames {
|
||||
gaps[i] = frame.Gap
|
||||
}
|
||||
min_gap := CalcMinimumGIFGap(gaps)
|
||||
// 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, ".")
|
||||
@@ -653,7 +600,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
|
||||
frame := ImageFrame{
|
||||
Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque,
|
||||
}
|
||||
frame.set_delay(min_gap, identify_data.Gap)
|
||||
frame.Delay_ms = int32(max(min_gap, identify_data.Gap) * 10)
|
||||
err = check_resize(&frame, df.Name())
|
||||
if err != nil {
|
||||
return
|
||||
@@ -665,9 +612,10 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) (
|
||||
return
|
||||
}
|
||||
slices.SortFunc(ans, func(a, b *ImageFrame) int { return a.Number - b.Number })
|
||||
anchor_frame := 1
|
||||
anchor_frame := uint(1)
|
||||
for i, frame := range ans {
|
||||
anchor_frame, frame.Compose_onto = SetGIFFrameDisposal(frame.Number, anchor_frame, byte(frames[i].Disposal))
|
||||
af, co := gifmeta.SetGIFFrameDisposal(uint(frame.Number), anchor_frame, byte(frames[i].Disposal))
|
||||
anchor_frame, frame.Compose_onto = af, int(co)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package images
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
func paletted_is_opaque(p *image.Paletted) bool {
|
||||
if len(p.Palette) > 256 {
|
||||
return p.Opaque()
|
||||
}
|
||||
var is_alpha [256]bool
|
||||
has_alpha := false
|
||||
for i, c := range p.Palette {
|
||||
_, _, _, a := c.RGBA()
|
||||
if a != 0xffff {
|
||||
is_alpha[i] = true
|
||||
has_alpha = true
|
||||
}
|
||||
}
|
||||
if !has_alpha {
|
||||
return true
|
||||
}
|
||||
i0, i1 := 0, p.Rect.Dx()
|
||||
for y := p.Rect.Min.Y; y < p.Rect.Max.Y; y++ {
|
||||
for _, c := range p.Pix[i0:i1] {
|
||||
if is_alpha[c] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
i0 += p.Stride
|
||||
i1 += p.Stride
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsOpaque(img image.Image) bool {
|
||||
switch i := img.(type) {
|
||||
case *image.RGBA:
|
||||
return i.Opaque()
|
||||
case *image.RGBA64:
|
||||
return i.Opaque()
|
||||
case *image.NRGBA:
|
||||
return i.Opaque()
|
||||
case *image.NRGBA64:
|
||||
return i.Opaque()
|
||||
case *image.Alpha:
|
||||
return i.Opaque()
|
||||
case *image.Alpha16:
|
||||
return i.Opaque()
|
||||
case *image.Gray:
|
||||
return i.Opaque()
|
||||
case *image.Gray16:
|
||||
return i.Opaque()
|
||||
case *image.CMYK:
|
||||
return i.Opaque()
|
||||
case *image.Paletted:
|
||||
return paletted_is_opaque(i)
|
||||
case *image.Uniform:
|
||||
return i.Opaque()
|
||||
case *image.YCbCr:
|
||||
return i.Opaque()
|
||||
case *imaging.NRGB:
|
||||
return i.Opaque()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"image"
|
||||
|
||||
"github.com/kovidgoyal/imaging"
|
||||
"github.com/kovidgoyal/imaging/nrgb"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
@@ -17,6 +18,6 @@ func (self *Context) paste_nrgb_onto_opaque(background *imaging.NRGB, img image.
|
||||
bg = *bgcol
|
||||
|
||||
}
|
||||
src := imaging.NewNRGBScanner(img, bg)
|
||||
src := nrgb.NewNRGBScanner(img, bg)
|
||||
self.run_paste(src, background, pos, func(dst []byte) {})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user