4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-09-07 20:32:30 +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

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