From f50161874a72bf9729fd538b2f8a2ce6ccfa2f2d Mon Sep 17 00:00:00 2001 From: eugene Date: Fri, 5 Sep 2025 22:14:40 +0300 Subject: [PATCH] init commit --- .gitignore | 1 + README.md | 24 +++ cmd/telegram_bot/telegram_bot.go | 12 ++ cmd/tgs_file_convert/tgs_file_convert.go | 78 ++++++++ go.mod | 12 ++ go.sum | 60 ++++++ internal/converter/tgs.go | 43 ++++ internal/logger/logger.go | 17 ++ internal/tgs/service.go | 237 +++++++++++++++++++++++ 9 files changed, 484 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/telegram_bot/telegram_bot.go create mode 100644 cmd/tgs_file_convert/tgs_file_convert.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/converter/tgs.go create mode 100644 internal/logger/logger.go create mode 100644 internal/tgs/service.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..67e9177 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Telegram sticker converter + +## Supported formats +- TGS +- WEBP **[WIP]** +- WEBM **[WIP]** + +## Supported transformations +### TGS +- TGS → PNG (first framge, all frames, N frame) +- TGS → JPEG (first framge, all frames, N frame) +- TGS → WEBP (first framge, all frames, N frame) +- TGS → Lottie.JSON +- TGS → GIF +- TGS → WEBM +- TGS → MP4 + +### WEBP +- WEBP → PNG **[WIP]** +- WEBP → JPEG **[WIP]** + +### WEBM +- WEBM → MP4 **[WIP]** +- WEBM → GIF **[WIP]** diff --git a/cmd/telegram_bot/telegram_bot.go b/cmd/telegram_bot/telegram_bot.go new file mode 100644 index 0000000..f8224f6 --- /dev/null +++ b/cmd/telegram_bot/telegram_bot.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + "github.com/yazmeyaa/telegram_sticker_converter/internal/logger" +) + +func main() { + logger := logger.NewLogger(os.Stdout, "telegram_bot") + logger.Info("Starting telegram bot") +} diff --git a/cmd/tgs_file_convert/tgs_file_convert.go b/cmd/tgs_file_convert/tgs_file_convert.go new file mode 100644 index 0000000..9dce41f --- /dev/null +++ b/cmd/tgs_file_convert/tgs_file_convert.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" + "github.com/yazmeyaa/telegram_sticker_converter/internal/tgs" +) + +func main() { + filePath := flag.String("file", "", "path to input .tgs file") + outPath := flag.String("out", "", "path to output file") + format := flag.String("format", "png", "output format (png|jpeg|webp|gif|webm|mp4|lottie)") + frame := flag.String("frame", "all", "frame selector (first|all|n)") + frameIndex := flag.Int("frame-index", 0, "frame index (used only with frame=n)") + quality := flag.Int("quality", 90, "output quality (0-100)") + resizeW := flag.Int("resize-width", 0, "resize width (0 = keep original)") + resizeH := flag.Int("resize-height", 0, "resize height (0 = keep original)") + flag.Parse() + + if *filePath == "" { + fmt.Fprintln(os.Stderr, "error: -file is required") + os.Exit(1) + } + + if *outPath == "" { + fmt.Fprintln(os.Stderr, "error: -out is required") + os.Exit(1) + } + + in, err := os.Open(*filePath) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening file: %v\n", err) + os.Exit(1) + } + defer in.Close() + + out, err := os.Create(*outPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating output: %v\n", err) + os.Exit(1) + } + defer out.Close() + + var frameSel converter.FrameSelector + switch *frame { + case "first": + frameSel = converter.FrameFirst + case "all": + frameSel = converter.FrameAll + case "n": + frameSel = converter.FrameN + default: + fmt.Fprintf(os.Stderr, "unknown frame selector: %s\n", *frame) + os.Exit(1) + } + + opts := converter.TransformOptions{ + Format: converter.OutputFormat(*format), + Frame: frameSel, + FrameIndex: *frameIndex, + Qualtity: *quality, + ResizeWidth: *resizeW, + ResizeHeight: *resizeH, + } + fmt.Fprintf(os.Stdout, "opts: %+v\n", opts) + + service := converter.TGSConverterService(tgs.NewService()) + if err := service.Transform(context.Background(), in, out, opts); err != nil { + fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("success:", *outPath) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6389858 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/yazmeyaa/telegram_sticker_converter + +go 1.24.6 + +require ( + github.com/arugaz/go-rlottie v0.1.0 // indirect + github.com/aws/aws-sdk-go v1.55.8 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/u2takey/ffmpeg-go v0.5.0 // indirect + github.com/u2takey/go-utils v0.3.1 // indirect + golang.org/x/image v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cb78d54 --- /dev/null +++ b/go.sum @@ -0,0 +1,60 @@ +github.com/arugaz/go-rlottie v0.1.0 h1:/rQaoBoSEG2T+PW56ANvpkgOHdYYtGZpvxzjI6touZ8= +github.com/arugaz/go-rlottie v0.1.0/go.mod h1:m50xy50q5U9ngFIBJja9m09vFhvfw6cxkRiqIxjKeWQ= +github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU= +github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc= +github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= +github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= +gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= +golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/converter/tgs.go b/internal/converter/tgs.go new file mode 100644 index 0000000..7049be7 --- /dev/null +++ b/internal/converter/tgs.go @@ -0,0 +1,43 @@ +package converter + +import ( + "context" + "errors" + "io" +) + +type OutputFormat string +type FrameSelector int + +const ( + FrameFirst FrameSelector = iota + FrameAll + FrameN +) + +var ( + ErrUnknownFormat error = errors.New("unknown format") +) + +const ( + FormatPNG OutputFormat = "png" + FormatJPEG OutputFormat = "jpeg" + FormatWEBP OutputFormat = "webp" + FormatGIF OutputFormat = "gif" + FormatWEBM OutputFormat = "webm" + FormatMP4 OutputFormat = "mp4" + FormatLottie OutputFormat = "lottie" +) + +type TransformOptions struct { + Format OutputFormat + Frame FrameSelector + FrameIndex int + Qualtity int + ResizeWidth int + ResizeHeight int +} + +type TGSConverterService interface { + Transform(ctx context.Context, data io.Reader, out io.Writer, opts TransformOptions) error +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..294b326 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,17 @@ +package logger + +import ( + "io" + "log/slog" + "os" +) + +func NewLogger(w io.Writer, cmd string) *slog.Logger { + log := slog.New(slog.NewJSONHandler(w, nil)). + With(slog.Group( + "program_info", + slog.Int("pid", os.Getpid()), + )) + + return log +} diff --git a/internal/tgs/service.go b/internal/tgs/service.go new file mode 100644 index 0000000..a657721 --- /dev/null +++ b/internal/tgs/service.go @@ -0,0 +1,237 @@ +package tgs + +import ( + "archive/zip" + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "image" + "image/jpeg" + "image/png" + "io" + + "github.com/arugaz/go-rlottie" + ffmpeg_go "github.com/u2takey/ffmpeg-go" + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" +) + +func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TransformOptions) error { + data, err := io.ReadAll(in) + if err != nil { + return err + } + + gr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return err + } + defer gr.Close() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, gr); err != nil { + return err + } + if opts.Format == converter.FormatLottie { + _, err := out.Write(buf.Bytes()) + return err + } + + anim := rlottie.LottieAnimationFromData(buf.String(), "", "") + defer rlottie.LottieAnimationDestroy(anim) + + width, height := rlottie.LottieAnimationGetSize(anim) + if opts.ResizeWidth == 0 && opts.ResizeHeight == 0 { + opts.ResizeWidth = int(width) + opts.ResizeHeight = int(height) + } + + if opts.Format == converter.FormatGIF || opts.Format == converter.FormatWEBM || opts.Format == converter.FormatMP4 { + return t.processVideo(ctx, anim, out, opts) + } + if opts.Format == converter.FormatPNG || opts.Format == converter.FormatWEBP || opts.Format == converter.FormatJPEG { + return t.processFrames(ctx, anim, out, opts) + } + + return converter.ErrUnknownFormat +} + +func (t tgsServiceImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error { + width := uint(opts.ResizeWidth) + height := uint(opts.ResizeHeight) + + if width == 0 || height == 0 { + width, height = rlottie.LottieAnimationGetSize(anim) + } + + frameBuffer := make([]byte, width*height*4) + + if opts.Frame == converter.FrameFirst { + if err := t.processFrame(ctx, anim, 0, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer); err != nil { + return err + } + + return t.makeSingleImage(ctx, frameBuffer, out, opts) + } + + if opts.Frame == converter.FrameN { + if err := t.processFrame(ctx, anim, uint(opts.FrameIndex), uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer); err != nil { + return err + } + return t.makeSingleImage(ctx, frameBuffer, out, opts) + } + + if opts.Frame == converter.FrameAll { + return t.makeAllImages(ctx, anim, frameBuffer, out, opts) + } + + return nil +} + +var ( + PresetGif = ffmpeg_go.KwArgs{ + "f": "gif", + "pix_fmt": "rgb24", + } + PresetWebm = ffmpeg_go.KwArgs{ + "vcodec": "libvpx", + "format": "webm", + "pix_fmt": "yuv420p", + "b:v": "0", + "deadline": "realtime", + } + PresetMP4 = ffmpeg_go.KwArgs{ + "vcodec": "libx264", + "format": "mp4", + "pix_fmt": "yuv420p", + "movflags": "frag_keyframe+empty_moov", + "preset": "ultrafast", + "tune": "zerolatency", + } +) + +func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error { + totalFrames := rlottie.LottieAnimationGetTotalframe(anim) + frameRate := rlottie.LottieAnimationGetFramerate(anim) + var preset ffmpeg_go.KwArgs + + switch opts.Format { + case converter.FormatGIF: + preset = PresetGif + case converter.FormatWEBM: + preset = PresetWebm + case converter.FormatMP4: + preset = PresetMP4 + default: + return converter.ErrUnknownFormat + } + + frameBuffer := make([]byte, opts.ResizeWidth*opts.ResizeHeight*4) + r, w := io.Pipe() + + go func() { + for frameIdx := range totalFrames { + err := t.processFrame(ctx, anim, frameIdx, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer) + if err != nil { + w.CloseWithError(err) + return + } + + w.Write(frameBuffer) + } + w.Close() + }() + + err := ffmpeg_go. + Input("pipe:0", ffmpeg_go.KwArgs{ + "format": "rawvideo", + "pix_fmt": "bgra", + "s": fmt.Sprintf("%dx%d", opts.ResizeWidth, opts.ResizeHeight), + "r": frameRate, + }). + Output("pipe:1", preset). + WithInput(r). + WithOutput(out). + Run() + + if err != nil { + return err + } + + return nil +} + +func BGRAtoRGBA(buf []byte) { + for i := 0; i < len(buf); i += 4 { + buf[i], buf[i+2] = buf[i+2], buf[i] + } +} + +func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error { + BGRAtoRGBA(frameBuffer) + img := &image.RGBA{ + Pix: frameBuffer, + Stride: int(opts.ResizeWidth) * 4, + Rect: image.Rect(0, 0, opts.ResizeWidth, opts.ResizeHeight), + } + + if opts.Format == converter.FormatPNG { + return png.Encode(out, img) + } + + if opts.Format == converter.FormatJPEG { + return jpeg.Encode(out, img, nil) + } + + // TODO + // if opts.Format == converter.FormatWEBP { + // } + return converter.ErrUnknownFormat +} + +func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error { + totalFrames := rlottie.LottieAnimationGetTotalframe(anim) + archive := zip.NewWriter(out) + defer archive.Close() + + for i := range totalFrames { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + file, err := archive.Create(fmt.Sprintf("frame_%d.%s", i, opts.Format)) + if err != nil { + return err + } + + t.processFrame(ctx, anim, i, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer) + + if err := t.makeSingleImage(ctx, frameBuffer, file, opts); err != nil { + return err + } + } + + if err := archive.Close(); err != nil { + return err + } + + return nil +} + +func (t tgsServiceImpl) processFrame(ctx context.Context, anim rlottie.Lottie_Animation, frameN uint, sx uint, sy uint, out []byte) error { + expectedSize := int(sx * sy * 4) + rlottie.LottieAnimationRender(anim, frameN, out, sx, sy, sx*4) + if len(out) < expectedSize { + return errors.New("not valid buffer size") + } + return nil +} + +type tgsServiceImpl struct{} + +func NewService() *tgsServiceImpl { + return &tgsServiceImpl{} +}