4
0
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:
Benau
2021-08-25 04:32:50 +08:00
committed by GitHub
parent d4195deb3a
commit 53cafa9f3d
310 changed files with 121526 additions and 85 deletions

24
vendor/github.com/Benau/tgsconverter/LICENSE generated vendored Normal file
View 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.

View 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()
}

View 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()
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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()
}