mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-11 14:39:09 -06:00
Bumps [github.com/kovidgoyal/imaging](https://github.com/kovidgoyal/imaging) from 1.7.2 to 1.8.17. - [Release notes](https://github.com/kovidgoyal/imaging/releases) - [Changelog](https://github.com/kovidgoyal/imaging/blob/master/.goreleaser.yaml) - [Commits](https://github.com/kovidgoyal/imaging/compare/v1.7.2...v1.8.17) --- updated-dependencies: - dependency-name: github.com/kovidgoyal/imaging dependency-version: 1.8.17 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
484 lines
14 KiB
Go
484 lines
14 KiB
Go
package imaging
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/gif"
|
|
"image/png"
|
|
"io"
|
|
"io/fs"
|
|
"math"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/kovidgoyal/imaging/apng"
|
|
"github.com/kovidgoyal/imaging/prism/meta"
|
|
"github.com/kovidgoyal/imaging/prism/meta/gifmeta"
|
|
"github.com/kovidgoyal/imaging/webp"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type Frame struct {
|
|
Number uint // a 1-based frame number
|
|
TopLeft image.Point // location of top-left of this frame w.r.t top left of first frame
|
|
Image image.Image `json:"-"` // the actual pixel data
|
|
Delay time.Duration // the time for which this frame should be visible
|
|
ComposeOnto uint // the frame number of the frame this frame should be composed onto. 0 means compose onto blank
|
|
Replace bool // Do a simple pixel replacement rather than a full alpha blend when compositing this frame
|
|
}
|
|
|
|
type Image struct {
|
|
Frames []*Frame // the actual frames of image data. The first frame is guaranteed to be the size of the image.
|
|
Metadata *meta.Data // image metadata
|
|
LoopCount uint // 0 means loop forever, 1 means loop once, ...
|
|
DefaultImage image.Image `json:"-"` // a "default image" for an animation that is not part of the actual animation
|
|
}
|
|
|
|
func (self *Image) populate_from_apng(p *apng.APNG) {
|
|
self.LoopCount = p.LoopCount
|
|
prev_disposal := apng.DISPOSE_OP_BACKGROUND
|
|
var prev_compose_onto uint
|
|
for _, f := range p.Frames {
|
|
if f.IsDefault {
|
|
self.DefaultImage = f.Image
|
|
continue
|
|
}
|
|
frame := Frame{Number: uint(len(self.Frames) + 1), Image: NormalizeOrigin(f.Image),
|
|
TopLeft: image.Point{X: f.XOffset, Y: f.YOffset},
|
|
Replace: f.BlendOp == apng.BLEND_OP_SOURCE,
|
|
Delay: time.Duration(float64(time.Second) * f.GetDelay())}
|
|
switch prev_disposal {
|
|
case apng.DISPOSE_OP_NONE:
|
|
frame.ComposeOnto = frame.Number - 1
|
|
case apng.DISPOSE_OP_PREVIOUS:
|
|
frame.ComposeOnto = uint(prev_compose_onto)
|
|
}
|
|
prev_disposal, prev_compose_onto = int(f.DisposeOp), frame.ComposeOnto
|
|
self.Frames = append(self.Frames, &frame)
|
|
}
|
|
}
|
|
|
|
func (f *Frame) Bounds() image.Rectangle {
|
|
return f.Image.Bounds().Add(f.TopLeft)
|
|
}
|
|
|
|
func (f *Frame) ColorModel() color.Model {
|
|
return f.Image.ColorModel()
|
|
}
|
|
|
|
func (f *Frame) At(x, y int) color.Color {
|
|
return f.Image.At(x-f.TopLeft.X, y-f.TopLeft.Y)
|
|
}
|
|
|
|
func (f *Frame) Dx() int { return f.Image.Bounds().Dx() }
|
|
func (f *Frame) Dy() int { return f.Image.Bounds().Dy() }
|
|
|
|
type canvas_t = image.NRGBA
|
|
|
|
var new_canvas = image.NewNRGBA
|
|
|
|
func (self *Image) populate_from_webp(p *webp.AnimatedWEBP) {
|
|
// See https://developers.google.com/speed/webp/docs/riff_container#animation
|
|
self.LoopCount = uint(p.Header.LoopCount)
|
|
bgcol := p.Header.BackgroundColor
|
|
// For some reason web viewers treat bgcol as full transparent. Sigh.
|
|
bgcol = image.Transparent
|
|
bg := image.NewUniform(bgcol)
|
|
_, _, _, a := bg.RGBA()
|
|
bg_is_fully_transparent := a == 0
|
|
w, h := int(self.Metadata.PixelWidth), int(self.Metadata.PixelHeight)
|
|
var dispose_prev bool
|
|
for i, f := range p.Frames {
|
|
frame := Frame{
|
|
Number: uint(len(self.Frames) + 1), Image: NormalizeOrigin(f.Frame),
|
|
TopLeft: image.Point{X: 2 * int(f.Header.FrameX), Y: 2 * int(f.Header.FrameY)},
|
|
Replace: !f.Header.AlphaBlend,
|
|
Delay: time.Millisecond * time.Duration(f.Header.FrameDuration),
|
|
}
|
|
// we want the first frame to have the same size as the canvas, which
|
|
// is not always true in WebP
|
|
if i == 0 && (frame.Dx() < w || frame.Dy() < h || frame.TopLeft != image.Point{}) {
|
|
img := new_canvas(image.Rect(0, 0, w, h))
|
|
dest := image.Rectangle{frame.TopLeft, frame.TopLeft.Add(image.Pt(frame.Bounds().Dx(), frame.Bounds().Dy()))}
|
|
if !bg_is_fully_transparent {
|
|
draw.Draw(img, img.Bounds(), bg, image.Point{}, draw.Src)
|
|
draw.Draw(img, dest, frame.Image, image.Point{}, draw.Over)
|
|
} else {
|
|
draw.Draw(img, dest, frame.Image, image.Point{}, draw.Src)
|
|
}
|
|
frame.Image = img
|
|
frame.TopLeft = image.Point{}
|
|
}
|
|
|
|
frame.ComposeOnto = frame.Number - 1
|
|
if dispose_prev {
|
|
// According to the spec dispose only affects the area of the
|
|
// frame, filling it with the background color on disposal, so
|
|
// add an extra frame that clears the prev frame's region and then
|
|
// draw the current frame as gapless frame.
|
|
prev_frame := self.Frames[len(self.Frames)-1]
|
|
b := prev_frame.Image.Bounds()
|
|
if bg_is_fully_transparent && (prev_frame.TopLeft == image.Point{}) && b.Dx() >= w && b.Dy() >= h {
|
|
// prev frame covered entire canvas and background is clear so
|
|
// just clear canvas
|
|
frame.ComposeOnto = 0
|
|
} else {
|
|
img := image.NewNRGBA(b)
|
|
draw.Draw(img, b, bg, image.Point{}, draw.Src)
|
|
if b == frame.Image.Bounds() && prev_frame.TopLeft == frame.TopLeft {
|
|
// prev frame and this frame overlap exactly, so just compose
|
|
// directly without needing an extra frame
|
|
draw.Draw(img, b, frame.Image, image.Point{}, draw.Over)
|
|
frame.Replace = true
|
|
frame.Image = img
|
|
} else {
|
|
// insert gapless frame to dispose previous frame
|
|
nf := Frame{
|
|
Number: frame.Number, Image: img, TopLeft: prev_frame.TopLeft, Replace: true,
|
|
ComposeOnto: prev_frame.Number,
|
|
}
|
|
self.Frames = append(self.Frames, &nf)
|
|
frame.Number++
|
|
frame.ComposeOnto = nf.Number
|
|
}
|
|
}
|
|
}
|
|
|
|
dispose_prev = f.Header.DisposalBitSet
|
|
self.Frames = append(self.Frames, &frame)
|
|
}
|
|
}
|
|
|
|
func (self *Image) populate_from_gif(g *gif.GIF) {
|
|
min_gap := gifmeta.CalcMinimumGap(g.Delay)
|
|
prev_disposal := uint8(gif.DisposalBackground)
|
|
var prev_compose_onto uint
|
|
for i, img := range g.Image {
|
|
b := img.Bounds()
|
|
frame := Frame{
|
|
Number: uint(len(self.Frames) + 1), Image: NormalizeOrigin(img), TopLeft: b.Min,
|
|
Delay: gifmeta.CalculateFrameDelay(g.Delay[i], min_gap),
|
|
}
|
|
switch prev_disposal {
|
|
case gif.DisposalNone, 0:
|
|
frame.ComposeOnto = frame.Number - 1
|
|
case gif.DisposalPrevious:
|
|
frame.ComposeOnto = prev_compose_onto
|
|
case gif.DisposalBackground:
|
|
// this is in contravention of the GIF spec but browsers and
|
|
// gif2apng both do this, so follow them. Test image for this
|
|
// is apple.gif
|
|
frame.ComposeOnto = frame.Number - 1
|
|
}
|
|
prev_disposal, prev_compose_onto = g.Disposal[i], frame.ComposeOnto
|
|
self.Frames = append(self.Frames, &frame)
|
|
}
|
|
switch {
|
|
case g.LoopCount == 0:
|
|
self.LoopCount = 0
|
|
case g.LoopCount < 0:
|
|
self.LoopCount = 1
|
|
default:
|
|
self.LoopCount = uint(g.LoopCount) + 1
|
|
}
|
|
}
|
|
|
|
// Create a clone of this image, all data is copied
|
|
func (self *Image) Clone() *Image {
|
|
ans := *self
|
|
if ans.DefaultImage != nil {
|
|
ans.DefaultImage = ClonePreservingType(ans.DefaultImage)
|
|
}
|
|
if ans.Metadata != nil {
|
|
ans.Metadata = ans.Metadata.Clone()
|
|
}
|
|
ans.Frames = make([]*Frame, len(self.Frames))
|
|
for i, f := range self.Frames {
|
|
nf := *f
|
|
nf.Image = ClonePreservingType(f.Image)
|
|
ans.Frames[i] = &nf
|
|
}
|
|
return &ans
|
|
}
|
|
|
|
// Coalesce all animation frames so that each frame is a snapshot of the
|
|
// animation at that instant.
|
|
func (self *Image) Coalesce() {
|
|
if len(self.Frames) == 1 {
|
|
return
|
|
}
|
|
canvas_rect := self.Frames[0].Bounds()
|
|
var canvas *canvas_t
|
|
for i, f := range self.Frames {
|
|
if i == 0 || f.ComposeOnto == 0 {
|
|
canvas = new_canvas(canvas_rect)
|
|
} else {
|
|
canvas = ClonePreservingType(self.Frames[f.ComposeOnto-1].Image).(*canvas_t)
|
|
}
|
|
op := draw.Over
|
|
if f.Replace {
|
|
op = draw.Src
|
|
}
|
|
draw.Draw(canvas, f.Bounds(), f.Image, image.Point{}, op)
|
|
f.Image = canvas
|
|
f.TopLeft = image.Point{}
|
|
f.ComposeOnto = 0
|
|
f.Replace = true
|
|
}
|
|
}
|
|
|
|
// converts a time.Duration to a numerator and denominator of type uint16.
|
|
// It finds the best rational approximation of the duration in seconds.
|
|
func as_fraction(d time.Duration) (num, den uint16) {
|
|
if d <= 0 {
|
|
return 0, 1
|
|
}
|
|
|
|
// Convert duration to seconds as a float64
|
|
val := d.Seconds()
|
|
|
|
// Use continued fractions to find the best rational approximation.
|
|
// We look for the convergent that is closest to the original value
|
|
// while keeping the numerator and denominator within uint16 bounds.
|
|
|
|
bestNum, bestDen := uint16(0), uint16(1)
|
|
bestError := math.Abs(val)
|
|
|
|
var h, k [3]int64
|
|
h[0], k[0] = 0, 1
|
|
h[1], k[1] = 1, 0
|
|
|
|
f := val
|
|
|
|
for i := 2; i < 100; i++ { // Limit iterations to prevent infinite loops
|
|
a := int64(f)
|
|
|
|
// Calculate next convergent
|
|
h[2] = a*h[1] + h[0]
|
|
k[2] = a*k[1] + k[0]
|
|
|
|
if h[2] > math.MaxUint16 || k[2] > math.MaxUint16 {
|
|
// This convergent is out of bounds, so the previous one was the best we could do.
|
|
break
|
|
}
|
|
|
|
numConv := uint16(h[2])
|
|
denConv := uint16(k[2])
|
|
|
|
currentVal := float64(numConv) / float64(denConv)
|
|
currentError := math.Abs(val - currentVal)
|
|
|
|
if currentError < bestError {
|
|
bestError = currentError
|
|
bestNum = numConv
|
|
bestDen = denConv
|
|
}
|
|
|
|
// Check if we have a perfect approximation
|
|
if f-float64(a) == 0.0 {
|
|
break
|
|
}
|
|
|
|
f = 1.0 / (f - float64(a))
|
|
|
|
h[0], h[1] = h[1], h[2]
|
|
k[0], k[1] = k[1], k[2]
|
|
}
|
|
|
|
return bestNum, bestDen
|
|
}
|
|
|
|
func (self *Image) as_apng() (ans apng.APNG) {
|
|
ans.LoopCount = self.LoopCount
|
|
if self.DefaultImage != nil {
|
|
ans.Frames = append(ans.Frames, apng.Frame{Image: self.DefaultImage, IsDefault: true})
|
|
}
|
|
for i, f := range self.Frames {
|
|
d := apng.Frame{
|
|
DisposeOp: apng.DISPOSE_OP_BACKGROUND, BlendOp: apng.BLEND_OP_OVER, XOffset: f.TopLeft.X, YOffset: f.TopLeft.Y, Image: f.Image,
|
|
}
|
|
if !f.Replace {
|
|
d.BlendOp = apng.BLEND_OP_SOURCE
|
|
}
|
|
d.DelayNumerator, d.DelayDenominator = as_fraction(f.Delay)
|
|
if i+1 < len(self.Frames) {
|
|
nf := self.Frames[i+1]
|
|
switch nf.ComposeOnto {
|
|
case f.Number:
|
|
d.DisposeOp = apng.DISPOSE_OP_NONE
|
|
case 0:
|
|
d.DisposeOp = apng.DISPOSE_OP_BACKGROUND
|
|
case f.ComposeOnto:
|
|
d.DisposeOp = apng.DISPOSE_OP_PREVIOUS
|
|
}
|
|
}
|
|
ans.Frames = append(ans.Frames, d)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Encode this image into a PNG
|
|
func (self *Image) EncodeAsPNG(w io.Writer) error {
|
|
if len(self.Frames) < 2 {
|
|
img := self.DefaultImage
|
|
if img == nil {
|
|
img = self.Frames[0].Image
|
|
}
|
|
return png.Encode(w, img)
|
|
}
|
|
// Unfortunately apng.Encode() is buggy or I am getting my dispose op
|
|
// mapping wrong, so coalesce first
|
|
img := self.Clone()
|
|
img.Coalesce()
|
|
return apng.Encode(w, img.as_apng())
|
|
}
|
|
|
|
// Save this image as PNG
|
|
func (self *Image) SaveAsPNG(path string, mode fs.FileMode) error {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
return self.EncodeAsPNG(f)
|
|
}
|
|
|
|
// Flip all frames horizontally
|
|
func (self *Image) FlipH() {
|
|
for _, f := range self.Frames {
|
|
f.Image = FlipH(f.Image)
|
|
}
|
|
}
|
|
|
|
// Flip all frames vertically
|
|
func (self *Image) FlipV() {
|
|
for _, f := range self.Frames {
|
|
f.Image = FlipV(f.Image)
|
|
}
|
|
}
|
|
|
|
type rotation struct {
|
|
angle_rads, cos, sin, center_x, center_y float64
|
|
}
|
|
|
|
func new_rotation(angle_deg float64, canvas_rect image.Rectangle) *rotation {
|
|
a := angle_deg * (math.Pi / 180.)
|
|
return &rotation{a, math.Cos(a), math.Sin(a), float64(canvas_rect.Dx()) / 2, float64(canvas_rect.Dy()) / 2}
|
|
}
|
|
|
|
func (r *rotation) apply(p image.Point) image.Point {
|
|
if (p == image.Point{}) {
|
|
return p
|
|
}
|
|
x := float64(p.X) - r.center_x
|
|
y := float64(p.Y) - r.center_y
|
|
rx := x*r.cos - y*r.sin
|
|
ry := x*r.sin + y*r.cos
|
|
return image.Pt(int(rx+r.center_x), int(ry+r.center_y))
|
|
}
|
|
|
|
func (self *Image) Bounds() image.Rectangle {
|
|
if self.DefaultImage != nil {
|
|
return self.DefaultImage.Bounds()
|
|
}
|
|
if len(self.Frames) > 0 {
|
|
return self.Frames[0].Bounds()
|
|
}
|
|
return image.Rect(0, 0, int(self.Metadata.PixelWidth), int(self.Metadata.PixelHeight))
|
|
}
|
|
|
|
// Transpose all frames (flip and rotate 90)
|
|
func (self *Image) Transpose() {
|
|
r := new_rotation(90, self.Bounds())
|
|
for _, f := range self.Frames {
|
|
f.Image = Transpose(f.Image)
|
|
f.TopLeft = r.apply(f.TopLeft)
|
|
}
|
|
if self.DefaultImage != nil {
|
|
self.DefaultImage = Transpose(self.DefaultImage)
|
|
}
|
|
self.Metadata.PixelWidth, self.Metadata.PixelHeight = self.Metadata.PixelHeight, self.Metadata.PixelWidth
|
|
}
|
|
|
|
// Transverse all frames (flip and rotate 90)
|
|
func (self *Image) Transverse() {
|
|
r := new_rotation(90, self.Bounds())
|
|
for _, f := range self.Frames {
|
|
f.Image = Transverse(f.Image)
|
|
f.TopLeft = r.apply(f.TopLeft)
|
|
}
|
|
if self.DefaultImage != nil {
|
|
self.DefaultImage = Transverse(self.DefaultImage)
|
|
}
|
|
self.Metadata.PixelWidth, self.Metadata.PixelHeight = self.Metadata.PixelHeight, self.Metadata.PixelWidth
|
|
}
|
|
|
|
// Rotate all frames by 90 counter clockwise
|
|
func (self *Image) Rotate90() {
|
|
r := new_rotation(90, self.Bounds())
|
|
for _, f := range self.Frames {
|
|
f.Image = Rotate90(f.Image)
|
|
f.TopLeft = r.apply(f.TopLeft)
|
|
}
|
|
if self.DefaultImage != nil {
|
|
self.DefaultImage = Rotate90(self.DefaultImage)
|
|
}
|
|
self.Metadata.PixelWidth, self.Metadata.PixelHeight = self.Metadata.PixelHeight, self.Metadata.PixelWidth
|
|
}
|
|
|
|
// Rotate all frames by 180 counter clockwise
|
|
func (self *Image) Rotate180() {
|
|
r := new_rotation(180, self.Bounds())
|
|
for _, f := range self.Frames {
|
|
f.Image = Rotate180(f.Image)
|
|
f.TopLeft = r.apply(f.TopLeft)
|
|
}
|
|
if self.DefaultImage != nil {
|
|
self.DefaultImage = Rotate180(self.DefaultImage)
|
|
}
|
|
}
|
|
|
|
// Rotate all frames by 270 counter clockwise
|
|
func (self *Image) Rotate270() {
|
|
r := new_rotation(270, self.Bounds())
|
|
for _, f := range self.Frames {
|
|
f.Image = Rotate270(f.Image)
|
|
f.TopLeft = r.apply(f.TopLeft)
|
|
}
|
|
self.Metadata.PixelWidth, self.Metadata.PixelHeight = self.Metadata.PixelHeight, self.Metadata.PixelWidth
|
|
if self.DefaultImage != nil {
|
|
self.DefaultImage = Rotate270(self.DefaultImage)
|
|
}
|
|
}
|
|
|
|
// Resize all frames to the specified size
|
|
func (self *Image) Resize(width, height int, filter ResampleFilter) {
|
|
old_width, old_height := self.Bounds().Dx(), self.Bounds().Dy()
|
|
sx := float64(width) / float64(old_width)
|
|
sy := float64(height) / float64(old_height)
|
|
scaledx := func(x int) int { return int(float64(x) * sx) }
|
|
scaledy := func(y int) int { return int(float64(y) * sy) }
|
|
for i, f := range self.Frames {
|
|
if i == 0 {
|
|
f.Image = ResizeWithOpacity(f.Image, width, height, filter, IsOpaque(f.Image))
|
|
} else {
|
|
f.Image = ResizeWithOpacity(f.Image, scaledx(f.Image.Bounds().Dx()), scaledy(f.Image.Bounds().Dy()), filter, IsOpaque(f.Image))
|
|
f.TopLeft = image.Pt(scaledx(f.TopLeft.X), scaledy(f.TopLeft.Y))
|
|
}
|
|
}
|
|
self.Metadata.PixelWidth, self.Metadata.PixelHeight = uint32(width), uint32(height)
|
|
}
|
|
|
|
// Paste all frames onto the specified background color (OVER alpha blend)
|
|
func (img *Image) PasteOntoBackground(bg color.Color) {
|
|
if img.DefaultImage != nil {
|
|
img.DefaultImage = PasteOntoBackground(img.DefaultImage, bg)
|
|
}
|
|
for _, f := range img.Frames {
|
|
f.Image = PasteOntoBackground(f.Image, bg)
|
|
}
|
|
}
|