mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-06-27 21:39:22 +00:00
Convert .tgs with go libraries (and cgo) (telegram) (#1569)
This commit adds support for go/cgo tgs conversion when building with the -tags `cgo` The default binaries are still "pure" go and uses the old way of converting. * Move lottie_convert.py conversion code to its own file * Add optional libtgsconverter * Update vendor * Apply suggestions from code review * Update bridge/helper/libtgsconverter.go Co-authored-by: Wim <wim@42.be>
This commit is contained in:
24
vendor/github.com/Benau/tgsconverter/LICENSE
generated
vendored
Normal file
24
vendor/github.com/Benau/tgsconverter/LICENSE
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2021, (see AUTHORS)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
51
vendor/github.com/Benau/tgsconverter/libtgsconverter/apng.go
generated
vendored
Normal file
51
vendor/github.com/Benau/tgsconverter/libtgsconverter/apng.go
generated
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "bytes"
|
||||
import "image"
|
||||
|
||||
import "github.com/kettek/apng"
|
||||
import "github.com/av-elier/go-decimal-to-rational"
|
||||
|
||||
type toapng struct {
|
||||
apng apng.APNG
|
||||
prev_frame *image.RGBA
|
||||
}
|
||||
|
||||
func(to_apng *toapng) init(w uint, h uint, options ConverterOptions) {
|
||||
}
|
||||
|
||||
func(to_apng *toapng) SupportsAnimation() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (to_apng *toapng) AddFrame(image *image.RGBA, fps uint) error {
|
||||
if to_apng.prev_frame != nil && sameImage(to_apng.prev_frame, image) {
|
||||
var idx = len(to_apng.apng.Frames) - 1
|
||||
var prev_fps = float64(to_apng.apng.Frames[idx].DelayNumerator) / float64(to_apng.apng.Frames[idx].DelayDenominator)
|
||||
prev_fps += 1.0 / float64(fps)
|
||||
rat := dectofrac.NewRatP(prev_fps, 0.001)
|
||||
to_apng.apng.Frames[idx].DelayNumerator = uint16(rat.Num().Int64())
|
||||
to_apng.apng.Frames[idx].DelayDenominator = uint16(rat.Denom().Int64())
|
||||
return nil
|
||||
}
|
||||
f := apng.Frame{}
|
||||
f.Image = image
|
||||
f.DelayNumerator = 1
|
||||
f.DelayDenominator = uint16(fps)
|
||||
f.DisposeOp = apng.DISPOSE_OP_BACKGROUND
|
||||
f.BlendOp = apng.BLEND_OP_SOURCE
|
||||
f.IsDefault = false
|
||||
to_apng.apng.Frames = append(to_apng.apng.Frames, f)
|
||||
to_apng.prev_frame = image
|
||||
return nil
|
||||
}
|
||||
|
||||
func (to_apng *toapng) Result() []byte {
|
||||
var data []byte
|
||||
w := bytes.NewBuffer(data)
|
||||
err := apng.Encode(w, to_apng.apng)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return w.Bytes()
|
||||
}
|
81
vendor/github.com/Benau/tgsconverter/libtgsconverter/gif.go
generated
vendored
Normal file
81
vendor/github.com/Benau/tgsconverter/libtgsconverter/gif.go
generated
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "bytes"
|
||||
|
||||
import "image"
|
||||
import "image/color"
|
||||
import "image/gif"
|
||||
|
||||
type togif struct {
|
||||
gif gif.GIF
|
||||
images []image.Image
|
||||
prev_frame *image.RGBA
|
||||
}
|
||||
|
||||
func(to_gif *togif) init(w uint, h uint, options ConverterOptions) {
|
||||
to_gif.gif.Config.Width = int(w)
|
||||
to_gif.gif.Config.Height = int(h)
|
||||
}
|
||||
|
||||
func(to_gif *togif) SupportsAnimation() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (to_gif *togif) AddFrame(image *image.RGBA, fps uint) error {
|
||||
var fps_int = int(1.0 / float32(fps) * 100.)
|
||||
if to_gif.prev_frame != nil && sameImage(to_gif.prev_frame, image) {
|
||||
to_gif.gif.Delay[len(to_gif.gif.Delay) - 1] += fps_int
|
||||
return nil
|
||||
}
|
||||
to_gif.gif.Image = append(to_gif.gif.Image, nil)
|
||||
to_gif.gif.Delay = append(to_gif.gif.Delay, fps_int)
|
||||
to_gif.gif.Disposal = append(to_gif.gif.Disposal, gif.DisposalBackground)
|
||||
to_gif.images = append(to_gif.images, image)
|
||||
to_gif.prev_frame = image
|
||||
return nil
|
||||
}
|
||||
|
||||
func (to_gif *togif) Result() []byte {
|
||||
q := medianCutQuantizer{mode, nil, false}
|
||||
p := q.quantizeMultiple(make([]color.Color, 0, 256), to_gif.images)
|
||||
// Add transparent entry finally
|
||||
var trans_idx uint8 = 0
|
||||
if q.reserveTransparent {
|
||||
trans_idx = uint8(len(p))
|
||||
}
|
||||
var id_map = make(map[uint32]uint8)
|
||||
for i, img := range to_gif.images {
|
||||
pi := image.NewPaletted(img.Bounds(), p)
|
||||
for y := 0; y < img.Bounds().Dy(); y++ {
|
||||
for x := 0; x < img.Bounds().Dx(); x++ {
|
||||
c := img.At(x, y)
|
||||
cr, cg, cb, ca := c.RGBA()
|
||||
cid := (cr >> 8) << 16 | cg | (cb >> 8)
|
||||
if q.reserveTransparent && ca == 0 {
|
||||
pi.Pix[pi.PixOffset(x, y)] = trans_idx
|
||||
} else if val, ok := id_map[cid]; ok {
|
||||
pi.Pix[pi.PixOffset(x, y)] = val
|
||||
} else {
|
||||
val := uint8(p.Index(c))
|
||||
pi.Pix[pi.PixOffset(x, y)] = val
|
||||
id_map[cid] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
to_gif.gif.Image[i] = pi
|
||||
}
|
||||
if q.reserveTransparent {
|
||||
p = append(p, color.RGBA{0, 0, 0, 0})
|
||||
}
|
||||
for _, img := range to_gif.gif.Image {
|
||||
img.Palette = p
|
||||
}
|
||||
to_gif.gif.Config.ColorModel = p
|
||||
var data []byte
|
||||
w := bytes.NewBuffer(data)
|
||||
err := gif.EncodeAll(w, &to_gif.gif)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return w.Bytes()
|
||||
}
|
40
vendor/github.com/Benau/tgsconverter/libtgsconverter/imagewriter.go
generated
vendored
Normal file
40
vendor/github.com/Benau/tgsconverter/libtgsconverter/imagewriter.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "image"
|
||||
|
||||
type imageWriter interface {
|
||||
init(w uint, h uint, options ConverterOptions)
|
||||
SupportsAnimation() bool
|
||||
AddFrame(image *image.RGBA, fps uint) error
|
||||
Result() []byte
|
||||
}
|
||||
|
||||
func sameImage(a *image.RGBA, b *image.RGBA) bool {
|
||||
if len(a.Pix) != len(b.Pix) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a.Pix {
|
||||
if v != b.Pix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newImageWriter(extension string, w uint, h uint, options ConverterOptions) imageWriter {
|
||||
var writer imageWriter
|
||||
switch extension {
|
||||
case "apng":
|
||||
writer = &toapng{}
|
||||
case "gif":
|
||||
writer = &togif{}
|
||||
case "png":
|
||||
writer = &topng{}
|
||||
case "webp":
|
||||
writer = &towebp{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
writer.init(w, h, options)
|
||||
return writer
|
||||
}
|
160
vendor/github.com/Benau/tgsconverter/libtgsconverter/lib.go
generated
vendored
Normal file
160
vendor/github.com/Benau/tgsconverter/libtgsconverter/lib.go
generated
vendored
Normal file
@ -0,0 +1,160 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "bytes"
|
||||
import "errors"
|
||||
import "compress/gzip"
|
||||
import "image"
|
||||
import "io/ioutil"
|
||||
|
||||
import "github.com/Benau/go_rlottie"
|
||||
|
||||
type ConverterOptions interface {
|
||||
SetExtension(ext string)
|
||||
SetFPS(fps uint)
|
||||
SetScale(scale float32)
|
||||
SetWebpQuality(webp_quality float32)
|
||||
GetExtension() string
|
||||
GetFPS() uint
|
||||
GetScale() float32
|
||||
GetWebpQuality() float32
|
||||
}
|
||||
|
||||
type converter_options struct {
|
||||
// apng, gif, png or webp
|
||||
extension string
|
||||
// Frame per second of output image (if you specify apng, gif or webp)
|
||||
fps uint
|
||||
// Scale of image result
|
||||
scale float32
|
||||
// Webp encoder quality (0 to 100)
|
||||
webpQuality float32
|
||||
}
|
||||
|
||||
func(opt *converter_options) SetExtension(ext string) {
|
||||
opt.extension = ext
|
||||
}
|
||||
|
||||
func(opt *converter_options) SetFPS(fps uint) {
|
||||
opt.fps = fps
|
||||
}
|
||||
|
||||
func(opt *converter_options) SetScale(scale float32) {
|
||||
opt.scale = scale
|
||||
}
|
||||
|
||||
func(opt *converter_options) SetWebpQuality(webp_quality float32) {
|
||||
opt.webpQuality = webp_quality
|
||||
}
|
||||
|
||||
func(opt *converter_options) GetExtension() string {
|
||||
return opt.extension
|
||||
}
|
||||
|
||||
func(opt *converter_options) GetFPS() uint {
|
||||
return opt.fps
|
||||
}
|
||||
|
||||
func(opt *converter_options) GetScale() float32 {
|
||||
return opt.scale
|
||||
}
|
||||
|
||||
func(opt *converter_options) GetWebpQuality() float32 {
|
||||
return opt.webpQuality
|
||||
}
|
||||
|
||||
func NewConverterOptions() ConverterOptions {
|
||||
return &converter_options{"png", 30, 1.0, 75}
|
||||
}
|
||||
|
||||
func imageFromBuffer(p []byte, w uint, h uint) *image.RGBA {
|
||||
// rlottie use ARGB32_Premultiplied
|
||||
for i := 0; i < len(p); i += 4 {
|
||||
p[i + 0], p[i + 2] = p[i + 2], p[i + 0]
|
||||
}
|
||||
m := image.NewRGBA(image.Rect(0, 0, int(w), int(h)))
|
||||
m.Pix = p
|
||||
m.Stride = int(w) * 4
|
||||
return m
|
||||
}
|
||||
|
||||
var disabled_cache = false
|
||||
func ImportFromData(data []byte, options ConverterOptions) ([]byte, error) {
|
||||
if !disabled_cache {
|
||||
disabled_cache = true
|
||||
go_rlottie.LottieConfigureModelCacheSize(0)
|
||||
}
|
||||
z, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to create gzip reader:" + err.Error())
|
||||
}
|
||||
uncompressed, err := ioutil.ReadAll(z)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to read gzip archive")
|
||||
}
|
||||
z.Close()
|
||||
|
||||
animation := go_rlottie.LottieAnimationFromData(string(uncompressed[:]), "", "")
|
||||
if animation == nil {
|
||||
return nil, errors.New("Failed to import lottie animation data")
|
||||
}
|
||||
|
||||
w, h := go_rlottie.LottieAnimationGetSize(animation)
|
||||
w = uint(float32(w) * options.GetScale())
|
||||
h = uint(float32(h) * options.GetScale())
|
||||
|
||||
frame_rate := go_rlottie.LottieAnimationGetFramerate(animation)
|
||||
frame_count := go_rlottie.LottieAnimationGetTotalframe(animation)
|
||||
duration := float32(frame_count) / float32(frame_rate)
|
||||
var desired_framerate = float32(options.GetFPS())
|
||||
// Most (Gif) player doesn't support ~60fps (found in most tgs)
|
||||
if desired_framerate > 50. {
|
||||
desired_framerate = 50.
|
||||
}
|
||||
step := 1.0 / desired_framerate
|
||||
|
||||
writer := newImageWriter(options.GetExtension(), w, h, options)
|
||||
if writer == nil {
|
||||
return nil, errors.New("Failed create imagewriter")
|
||||
}
|
||||
|
||||
var i float32
|
||||
for i = 0.; i < duration; i += step {
|
||||
frame := go_rlottie.LottieAnimationGetFrameAtPos(animation, i / duration)
|
||||
buf := make([]byte, w * h * 4)
|
||||
go_rlottie.LottieAnimationRender(animation, frame, buf, w, h, w * 4)
|
||||
m := imageFromBuffer(buf, w, h)
|
||||
err := writer.AddFrame(m, uint(desired_framerate))
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to add frame:" + err.Error())
|
||||
}
|
||||
if !writer.SupportsAnimation() {
|
||||
break
|
||||
}
|
||||
}
|
||||
go_rlottie.LottieAnimationDestroy(animation)
|
||||
return writer.Result(), nil
|
||||
}
|
||||
|
||||
func ImportFromFile(path string, options ConverterOptions) ([]byte, error) {
|
||||
tgs, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, errors.New("Error when opening file:" + err.Error())
|
||||
}
|
||||
return ImportFromData(tgs, options)
|
||||
}
|
||||
|
||||
func SupportsExtension(extension string) (bool) {
|
||||
switch extension {
|
||||
case "apng":
|
||||
fallthrough
|
||||
case "gif":
|
||||
fallthrough
|
||||
case "png":
|
||||
fallthrough
|
||||
case "webp":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
30
vendor/github.com/Benau/tgsconverter/libtgsconverter/png.go
generated
vendored
Normal file
30
vendor/github.com/Benau/tgsconverter/libtgsconverter/png.go
generated
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "bytes"
|
||||
import "image"
|
||||
import "image/png"
|
||||
|
||||
type topng struct {
|
||||
result []byte
|
||||
}
|
||||
|
||||
func(to_png *topng) init(w uint, h uint, options ConverterOptions) {
|
||||
}
|
||||
|
||||
func(to_png *topng) SupportsAnimation() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (to_png *topng) AddFrame(image *image.RGBA, fps uint) error {
|
||||
var data []byte
|
||||
w := bytes.NewBuffer(data)
|
||||
if err := png.Encode(w, image); err != nil {
|
||||
return err
|
||||
}
|
||||
to_png.result = w.Bytes()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (to_png *topng) Result() []byte {
|
||||
return to_png.result
|
||||
}
|
119
vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_bucket.go
generated
vendored
Normal file
119
vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_bucket.go
generated
vendored
Normal file
@ -0,0 +1,119 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "image/color"
|
||||
|
||||
type colorAxis uint8
|
||||
|
||||
// Color axis constants
|
||||
const (
|
||||
red colorAxis = iota
|
||||
green
|
||||
blue
|
||||
)
|
||||
|
||||
type colorPriority struct {
|
||||
p uint32
|
||||
color.RGBA
|
||||
}
|
||||
|
||||
func (c colorPriority) axis(span colorAxis) uint8 {
|
||||
switch span {
|
||||
case red:
|
||||
return c.R
|
||||
case green:
|
||||
return c.G
|
||||
default:
|
||||
return c.B
|
||||
}
|
||||
}
|
||||
|
||||
type colorBucket []colorPriority
|
||||
|
||||
func (cb colorBucket) partition() (colorBucket, colorBucket) {
|
||||
mean, span := cb.span()
|
||||
left, right := 0, len(cb)-1
|
||||
for left < right {
|
||||
cb[left], cb[right] = cb[right], cb[left]
|
||||
for cb[left].axis(span) < mean && left < right {
|
||||
left++
|
||||
}
|
||||
for cb[right].axis(span) >= mean && left < right {
|
||||
right--
|
||||
}
|
||||
}
|
||||
if left == 0 {
|
||||
return cb[:1], cb[1:]
|
||||
}
|
||||
if left == len(cb)-1 {
|
||||
return cb[:len(cb)-1], cb[len(cb)-1:]
|
||||
}
|
||||
return cb[:left], cb[left:]
|
||||
}
|
||||
|
||||
func (cb colorBucket) mean() color.RGBA {
|
||||
var r, g, b uint64
|
||||
var p uint64
|
||||
for _, c := range cb {
|
||||
p += uint64(c.p)
|
||||
r += uint64(c.R) * uint64(c.p)
|
||||
g += uint64(c.G) * uint64(c.p)
|
||||
b += uint64(c.B) * uint64(c.p)
|
||||
}
|
||||
return color.RGBA{uint8(r / p), uint8(g / p), uint8(b / p), 255}
|
||||
}
|
||||
|
||||
type constraint struct {
|
||||
min uint8
|
||||
max uint8
|
||||
vals [256]uint64
|
||||
}
|
||||
|
||||
func (c *constraint) update(index uint8, p uint32) {
|
||||
if index < c.min {
|
||||
c.min = index
|
||||
}
|
||||
if index > c.max {
|
||||
c.max = index
|
||||
}
|
||||
c.vals[index] += uint64(p)
|
||||
}
|
||||
|
||||
func (c *constraint) span() uint8 {
|
||||
return c.max - c.min
|
||||
}
|
||||
|
||||
func (cb colorBucket) span() (uint8, colorAxis) {
|
||||
var R, G, B constraint
|
||||
R.min = 255
|
||||
G.min = 255
|
||||
B.min = 255
|
||||
var p uint64
|
||||
for _, c := range cb {
|
||||
R.update(c.R, c.p)
|
||||
G.update(c.G, c.p)
|
||||
B.update(c.B, c.p)
|
||||
p += uint64(c.p)
|
||||
}
|
||||
var toCount *constraint
|
||||
var span colorAxis
|
||||
if R.span() > G.span() && R.span() > B.span() {
|
||||
span = red
|
||||
toCount = &R
|
||||
} else if G.span() > B.span() {
|
||||
span = green
|
||||
toCount = &G
|
||||
} else {
|
||||
span = blue
|
||||
toCount = &B
|
||||
}
|
||||
var counted uint64
|
||||
var i int
|
||||
var c uint64
|
||||
for i, c = range toCount.vals {
|
||||
if counted > p/2 || counted+c == p {
|
||||
break
|
||||
}
|
||||
counted += c
|
||||
}
|
||||
return uint8(i), span
|
||||
}
|
209
vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_mediancut.go
generated
vendored
Normal file
209
vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_mediancut.go
generated
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
package libtgsconverter
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type bucketPool struct {
|
||||
sync.Pool
|
||||
maxCap int
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (p *bucketPool) getBucket(c int) colorBucket {
|
||||
p.m.Lock()
|
||||
if p.maxCap > c {
|
||||
p.maxCap = p.maxCap * 99 / 100
|
||||
}
|
||||
if p.maxCap < c {
|
||||
p.maxCap = c
|
||||
}
|
||||
maxCap := p.maxCap
|
||||
p.m.Unlock()
|
||||
val := p.Pool.Get()
|
||||
if val == nil || cap(val.(colorBucket)) < c {
|
||||
return make(colorBucket, maxCap)[0:c]
|
||||
}
|
||||
slice := val.(colorBucket)
|
||||
slice = slice[0:c]
|
||||
for i := range slice {
|
||||
slice[i] = colorPriority{}
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
var bpool bucketPool
|
||||
|
||||
// aggregationType specifies the type of aggregation to be done
|
||||
type aggregationType uint8
|
||||
|
||||
const (
|
||||
// Mode - pick the highest priority value
|
||||
mode aggregationType = iota
|
||||
// Mean - weighted average all values
|
||||
mean
|
||||
)
|
||||
|
||||
// medianCutQuantizer implements the go draw.Quantizer interface using the Median Cut method
|
||||
type medianCutQuantizer struct {
|
||||
// The type of aggregation to be used to find final colors
|
||||
aggregation aggregationType
|
||||
// The weighting function to use on each pixel
|
||||
weighting func(image.Image, int, int) uint32
|
||||
// Whether need to add a transparent entry after conversion
|
||||
reserveTransparent bool
|
||||
}
|
||||
|
||||
//bucketize takes a bucket and performs median cut on it to obtain the target number of grouped buckets
|
||||
func bucketize(colors colorBucket, num int) (buckets []colorBucket) {
|
||||
if len(colors) == 0 || num == 0 {
|
||||
return nil
|
||||
}
|
||||
bucket := colors
|
||||
buckets = make([]colorBucket, 1, num*2)
|
||||
buckets[0] = bucket
|
||||
|
||||
for len(buckets) < num && len(buckets) < len(colors) { // Limit to palette capacity or number of colors
|
||||
bucket, buckets = buckets[0], buckets[1:]
|
||||
if len(bucket) < 2 {
|
||||
buckets = append(buckets, bucket)
|
||||
continue
|
||||
} else if len(bucket) == 2 {
|
||||
buckets = append(buckets, bucket[:1], bucket[1:])
|
||||
continue
|
||||
}
|
||||
|
||||
left, right := bucket.partition()
|
||||
buckets = append(buckets, left, right)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// palettize finds a single color to represent a set of color buckets
|
||||
func (q* medianCutQuantizer) palettize(p color.Palette, buckets []colorBucket) color.Palette {
|
||||
for _, bucket := range buckets {
|
||||
switch q.aggregation {
|
||||
case mean:
|
||||
mean := bucket.mean()
|
||||
p = append(p, mean)
|
||||
case mode:
|
||||
var best colorPriority
|
||||
for _, c := range bucket {
|
||||
if c.p > best.p {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
p = append(p, best.RGBA)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// quantizeSlice expands the provided bucket and then palettizes the result
|
||||
func (q* medianCutQuantizer) quantizeSlice(p color.Palette, colors []colorPriority) color.Palette {
|
||||
numColors := cap(p) - len(p)
|
||||
reserveTransparent := q.reserveTransparent
|
||||
if reserveTransparent {
|
||||
numColors--
|
||||
}
|
||||
buckets := bucketize(colors, numColors)
|
||||
p = q.palettize(p, buckets)
|
||||
return p
|
||||
}
|
||||
|
||||
func colorAt(m image.Image, x int, y int) color.RGBA {
|
||||
switch i := m.(type) {
|
||||
case *image.YCbCr:
|
||||
yi := i.YOffset(x, y)
|
||||
ci := i.COffset(x, y)
|
||||
c := color.YCbCr{
|
||||
i.Y[yi],
|
||||
i.Cb[ci],
|
||||
i.Cr[ci],
|
||||
}
|
||||
return color.RGBA{c.Y, c.Cb, c.Cr, 255}
|
||||
case *image.RGBA:
|
||||
ci := i.PixOffset(x, y)
|
||||
return color.RGBA{i.Pix[ci+0], i.Pix[ci+1], i.Pix[ci+2], i.Pix[ci+3]}
|
||||
default:
|
||||
return color.RGBAModel.Convert(i.At(x, y)).(color.RGBA)
|
||||
}
|
||||
}
|
||||
|
||||
// buildBucketMultiple creates a prioritized color slice with all the colors in
|
||||
// the images.
|
||||
func (q* medianCutQuantizer) buildBucketMultiple(ms []image.Image) (bucket colorBucket) {
|
||||
if len(ms) < 1 {
|
||||
return colorBucket{}
|
||||
}
|
||||
|
||||
bounds := ms[0].Bounds()
|
||||
size := (bounds.Max.X - bounds.Min.X) * (bounds.Max.Y - bounds.Min.Y) * 2
|
||||
sparseBucket := bpool.getBucket(size)
|
||||
|
||||
for _, m := range ms {
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
priority := uint32(1)
|
||||
if q.weighting != nil {
|
||||
priority = q.weighting(m, x, y)
|
||||
}
|
||||
c := colorAt(m, x, y)
|
||||
if c.A == 0 {
|
||||
if !q.reserveTransparent {
|
||||
q.reserveTransparent = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if priority != 0 {
|
||||
index := int(c.R)<<16 | int(c.G)<<8 | int(c.B)
|
||||
for i := 1; ; i++ {
|
||||
p := &sparseBucket[index%size]
|
||||
if p.p == 0 || p.RGBA == c {
|
||||
*p = colorPriority{p.p + priority, c}
|
||||
break
|
||||
}
|
||||
index += 1 + i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bucket = sparseBucket[:0]
|
||||
switch ms[0].(type) {
|
||||
case *image.YCbCr:
|
||||
for _, p := range sparseBucket {
|
||||
if p.p != 0 {
|
||||
r, g, b := color.YCbCrToRGB(p.R, p.G, p.B)
|
||||
bucket = append(bucket, colorPriority{p.p, color.RGBA{r, g, b, p.A}})
|
||||
}
|
||||
}
|
||||
default:
|
||||
for _, p := range sparseBucket {
|
||||
if p.p != 0 {
|
||||
bucket = append(bucket, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Quantize quantizes an image to a palette and returns the palette
|
||||
func (q* medianCutQuantizer) quantize(p color.Palette, m image.Image) color.Palette {
|
||||
// Package quantize offers an implementation of the draw.Quantize interface using an optimized Median Cut method,
|
||||
// including advanced functionality for fine-grained control of color priority
|
||||
bucket := q.buildBucketMultiple([]image.Image{m})
|
||||
defer bpool.Put(bucket)
|
||||
return q.quantizeSlice(p, bucket)
|
||||
}
|
||||
|
||||
// QuantizeMultiple quantizes several images at once to a palette and returns
|
||||
// the palette
|
||||
func (q* medianCutQuantizer) quantizeMultiple(p color.Palette, m []image.Image) color.Palette {
|
||||
bucket := q.buildBucketMultiple(m)
|
||||
defer bpool.Put(bucket)
|
||||
return q.quantizeSlice(p, bucket)
|
||||
}
|
39
vendor/github.com/Benau/tgsconverter/libtgsconverter/webp.go
generated
vendored
Normal file
39
vendor/github.com/Benau/tgsconverter/libtgsconverter/webp.go
generated
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
package libtgsconverter
|
||||
|
||||
import "bytes"
|
||||
import "image"
|
||||
|
||||
import "github.com/sizeofint/webpanimation"
|
||||
|
||||
type towebp struct {
|
||||
timestamp int
|
||||
webpanim *webpanimation.WebpAnimation
|
||||
config webpanimation.WebPConfig
|
||||
}
|
||||
|
||||
func(to_webp *towebp) init(w uint, h uint, options ConverterOptions) {
|
||||
to_webp.timestamp = 0
|
||||
to_webp.webpanim = webpanimation.NewWebpAnimation(int(w), int(h), 0)
|
||||
to_webp.config = webpanimation.NewWebpConfig()
|
||||
to_webp.config.SetQuality(options.GetWebpQuality())
|
||||
}
|
||||
|
||||
func(to_webp *towebp) SupportsAnimation() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (to_webp *towebp) AddFrame(image *image.RGBA, fps uint) error {
|
||||
err := to_webp.webpanim.AddFrame(image, to_webp.timestamp, to_webp.config)
|
||||
to_webp.timestamp += int((1.0 / float32(fps)) * 1000.)
|
||||
return err
|
||||
}
|
||||
|
||||
func (to_webp *towebp) Result() []byte {
|
||||
var buf bytes.Buffer
|
||||
err := to_webp.webpanim.Encode(&buf)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
to_webp.webpanim.ReleaseMemory()
|
||||
return buf.Bytes()
|
||||
}
|
Reference in New Issue
Block a user