This commit is contained in:
eugene 2025-09-10 20:08:33 +03:00
parent 62fffc86e9
commit ad939717ad
8 changed files with 153 additions and 56 deletions

120
README.md
View File

@ -1,7 +1,7 @@
# Telegram sticker converter # Telegram sticker converter
## System requirements ## Required system binaries
- ffmpeg - ffmpeg 7.1.1
## Build requirements ## Build requirements
- Go 1.24 - Go 1.24
@ -12,22 +12,104 @@
- WEBM - WEBM
## Supported transformations ## Supported transformations
### TGS
- TGS → PNG (first framge, all frames, N frame, frames range)
- TGS → JPEG (first framge, all frames, N frame, frames range)
- TGS → WEBP (first framge, all frames, N frame, frames range)
- TGS → Lottie.JSON
- TGS → GIF
- TGS → WEBM
- TGS → MP4
### WEBP | Input | Output formats |
- WEBP → PNG | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
- WEBP &rarr; JPEG | **TGS** | PNG (first frame, all frames, N frame, frame range) <br> JPEG (first frame, all frames, N frame, frame range) <br> WEBP (first frame, all frames, N frame, frame range) <br> Lottie JSON <br> GIF <br> WEBM <br> MP4 |
| **WEBP** | PNG <br> JPEG |
| **WEBM** | MP4 <br> GIF <br> PNG (first frame, all frames, N frame, frame range) <br> JPEG (first frame, all frames, N frame, frame range) <br> WEBP (first frame, all frames, N frame, frame range) |
### WEBM
- WEBM &rarr; MP4 ## Examples
- WEBM &rarr; GIF ### Extract frame from TGS
- WEBM &rarr; PNG (first framge, all frames, N frame, frames range) ```go
- WEBM &rarr; JPEG (first framge, all frames, N frame, frames range) package main
- WEBM &rarr; WEBP (first framge, all frames, N frame, frames range) import (
"context"
"os"
converter "github.com/yazmeyaa/telegram_sticker_converter"
"github.com/yazmeyaa/telegram_sticker_converter/tgs"
)
func main() {
conv := tgs.NewConverter()
r, err := os.Open("./sticker.tgs")
defer r.Close()
w, err := os.Create("./sticker.png")
defer w.Close()
opts := converter.TGSTransformOptions{
Format: converter.FormatPNG,
Frame: converter.FrameN,
FrameIndex: 10,
ResizeWidth: 1024,
ResizeHeight: 1024,
}
if err := conv.Transform(context.Background(), r, w, opts); err != nil {
panic(err)
}
}
```
### Convert TGS to video
```go
package main
import (
"context"
"os"
converter "github.com/yazmeyaa/telegram_sticker_converter"
"github.com/yazmeyaa/telegram_sticker_converter/tgs"
)
func main() {
conv := tgs.NewConverter()
r, err := os.Open("./sticker.tgs")
defer r.Close()
w, err := os.Create("./sticker.mp4")
defer w.Close()
opts := converter.TGSTransformOptions{
Format: converter.FormatMP4,
ResizeWidth: 1024,
ResizeHeight: 1024,
}
if err := conv.Transform(context.Background(), r, w, opts); err != nil {
panic(err)
}
}
```
### Convert TGS to frames array (ZIP)
```go
package main
import (
"context"
"os"
converter "github.com/yazmeyaa/telegram_sticker_converter"
"github.com/yazmeyaa/telegram_sticker_converter/tgs"
)
func main() {
conv := tgs.NewConverter()
r, err := os.Open("./sticker.tgs")
defer r.Close()
w, err := os.Create("./sticker.zip")
defer w.Close()
opts := converter.TGSTransformOptions{
Format: converter.FormatPNG,
Frame: converter.FrameAll,
ResizeWidth: 1024,
ResizeHeight: 1024,
}
if err := conv.Transform(context.Background(), r, w, opts); err != nil {
panic(err)
}
}
```

View File

@ -5,39 +5,53 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"strings"
converter "github.com/yazmeyaa/telegram_sticker_converter" converter "github.com/yazmeyaa/telegram_sticker_converter"
"github.com/yazmeyaa/telegram_sticker_converter/tgs" "github.com/yazmeyaa/telegram_sticker_converter/tgs"
) )
func main() { func main() {
filePath := flag.String("file", "", "path to input .tgs file") input := flag.String("input", "stream:stdin", "path to input .tgs file")
outPath := flag.String("out", "", "path to output file") outPath := flag.String("output", "", "path to output file")
format := flag.String("format", "png", "output format (png|jpeg|webp|gif|webm|mp4|lottie)") format := flag.String("format", "png", "output format (png|jpeg|webp|gif|webm|mp4|lottie)")
frame := flag.String("frame", "all", "frame selector (first|all|n)") frame := flag.String("frame", "all", "frame selector (first|all|n)")
frameIndex := flag.Int("frame-index", 0, "frame index (used only with frame=n)") frameIndex := flag.Int("frame-index", 0, "frame index (used only with frame=n)")
quality := flag.Int("quality", 90, "output quality (0-100)") quality := flag.Int("quality", 100, "output quality (0-100)")
resizeW := flag.Int("resize-width", 0, "resize width (0 = keep original)") resizeW := flag.Int("resize-width", 0, "resize width (0 = keep original)")
resizeH := flag.Int("resize-height", 0, "resize height (0 = keep original)") resizeH := flag.Int("resize-height", 0, "resize height (0 = keep original)")
flag.Parse() flag.Parse()
if *filePath == "" { inputParts := strings.Split(*input, ":")
fmt.Fprintln(os.Stderr, "error: -file is required") if len(inputParts) < 2 {
fmt.Fprintf(os.Stderr, "wrong input signature. valid is \"{type}:{input}\"\nExample: \"stream:stdin\"; \"file:input.tgs\"\n")
os.Exit(1) os.Exit(1)
} }
if *outPath == "" { inputType := inputParts[0]
fmt.Fprintln(os.Stderr, "error: -out is required") var in *os.File
os.Exit(1) if inputType == "file" {
} input, err := os.Open(*input)
in, err := os.Open(*filePath)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error opening file: %v\n", err) fmt.Fprintf(os.Stderr, "error opening file: %v\n", err)
os.Exit(1) os.Exit(1)
} }
in = input
}
if inputType == "stream" {
in = os.Stdin
}
if in == nil {
fmt.Fprintf(os.Stderr, "Unexpected input type\n")
os.Exit(1)
}
defer in.Close() defer in.Close()
if *outPath == "" {
fmt.Fprintln(os.Stderr, "error: -output is required")
os.Exit(1)
}
out, err := os.Create(*outPath) out, err := os.Create(*outPath)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error creating output: %v\n", err) fmt.Fprintf(os.Stderr, "error creating output: %v\n", err)
@ -67,8 +81,9 @@ func main() {
ResizeHeight: *resizeH, ResizeHeight: *resizeH,
} }
service := converter.TGSConverterService(tgs.NewService()) converter := tgs.NewConverter()
if err := service.Transform(context.Background(), in, out, opts); err != nil {
if err := converter.Transform(context.Background(), in, out, opts); err != nil {
fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) fmt.Fprintf(os.Stderr, "transform failed: %v\n", err)
os.Exit(1) os.Exit(1)
} }

View File

@ -65,8 +65,8 @@ func main() {
Height: *resizeH, Height: *resizeH,
} }
service := converter.WEBMConverter(webm.NewService()) converter := converter.WEBMConverter(webm.NewConverter())
if err := service.Transform(context.Background(), in, out, opts); err != nil { if err := converter.Transform(context.Background(), in, out, opts); err != nil {
fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) fmt.Fprintf(os.Stderr, "transform failed: %v\n", err)
os.Exit(1) os.Exit(1)
} }

2
tgs.go
View File

@ -15,6 +15,6 @@ type TGSTransformOptions struct {
ResizeHeight int ResizeHeight int
} }
type TGSConverterService interface { type TGSConverter interface {
Transform(ctx context.Context, data io.Reader, out io.Writer, opts TGSTransformOptions) error Transform(ctx context.Context, data io.Reader, out io.Writer, opts TGSTransformOptions) error
} }

View File

@ -17,7 +17,7 @@ import (
converter "github.com/yazmeyaa/telegram_sticker_converter" converter "github.com/yazmeyaa/telegram_sticker_converter"
) )
func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TGSTransformOptions) error { func (t tgsConverterImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TGSTransformOptions) error {
data, err := io.ReadAll(in) data, err := io.ReadAll(in)
if err != nil { if err != nil {
return err return err
@ -57,7 +57,7 @@ func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writ
return converter.ErrUnknownFormat return converter.ErrUnknownFormat
} }
func (t tgsServiceImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error { func (t tgsConverterImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error {
width := uint(opts.ResizeWidth) width := uint(opts.ResizeWidth)
height := uint(opts.ResizeHeight) height := uint(opts.ResizeHeight)
@ -111,7 +111,7 @@ var (
} }
) )
func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error { func (t tgsConverterImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TGSTransformOptions) error {
totalFrames := rlottie.LottieAnimationGetTotalframe(anim) totalFrames := rlottie.LottieAnimationGetTotalframe(anim)
frameRate := rlottie.LottieAnimationGetFramerate(anim) frameRate := rlottie.LottieAnimationGetFramerate(anim)
var preset ffmpeg_go.KwArgs var preset ffmpeg_go.KwArgs
@ -169,7 +169,7 @@ func BGRAtoRGBA(buf []byte) {
} }
} }
func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error { func (t tgsConverterImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error {
BGRAtoRGBA(frameBuffer) BGRAtoRGBA(frameBuffer)
img := &image.RGBA{ img := &image.RGBA{
Pix: frameBuffer, Pix: frameBuffer,
@ -191,7 +191,7 @@ func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte,
return converter.ErrUnknownFormat return converter.ErrUnknownFormat
} }
func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error { func (t tgsConverterImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TGSTransformOptions) error {
totalFrames := rlottie.LottieAnimationGetTotalframe(anim) totalFrames := rlottie.LottieAnimationGetTotalframe(anim)
archive := zip.NewWriter(out) archive := zip.NewWriter(out)
defer archive.Close() defer archive.Close()
@ -222,7 +222,7 @@ func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_A
return nil return nil
} }
func (t tgsServiceImpl) processFrame(ctx context.Context, anim rlottie.Lottie_Animation, frameN uint, sx uint, sy uint, out []byte) error { func (t tgsConverterImpl) processFrame(ctx context.Context, anim rlottie.Lottie_Animation, frameN uint, sx uint, sy uint, out []byte) error {
expectedSize := int(sx * sy * 4) expectedSize := int(sx * sy * 4)
rlottie.LottieAnimationRender(anim, frameN, out, sx, sy, sx*4) rlottie.LottieAnimationRender(anim, frameN, out, sx, sy, sx*4)
if len(out) < expectedSize { if len(out) < expectedSize {
@ -231,8 +231,8 @@ func (t tgsServiceImpl) processFrame(ctx context.Context, anim rlottie.Lottie_An
return nil return nil
} }
type tgsServiceImpl struct{} type tgsConverterImpl struct{}
func NewService() *tgsServiceImpl { func NewConverter() *tgsConverterImpl {
return &tgsServiceImpl{} return &tgsConverterImpl{}
} }

View File

@ -11,13 +11,13 @@ import (
converter "github.com/yazmeyaa/telegram_sticker_converter" converter "github.com/yazmeyaa/telegram_sticker_converter"
) )
type webmService struct{} type webmConverter struct{}
func NewService() *webmService { func NewConverter() *webmConverter {
return &webmService{} return &webmConverter{}
} }
func (ws webmService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { func (ws webmConverter) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error {
rIn, wIn := io.Pipe() rIn, wIn := io.Pipe()
rOut, wOut := io.Pipe() rOut, wOut := io.Pipe()

View File

@ -9,6 +9,6 @@ type WebpTransformOptions struct {
Format OutputFormat Format OutputFormat
} }
type WebpConverterService interface { type WebpConverter interface {
Transform(ctx context.Context, in io.Reader, out io.Writer, opts WebpTransformOptions) error Transform(ctx context.Context, in io.Reader, out io.Writer, opts WebpTransformOptions) error
} }

View File

@ -10,10 +10,10 @@ import (
"golang.org/x/image/webp" "golang.org/x/image/webp"
) )
type webpConverterService struct{} type webpConverter struct{}
// Transform implements converter.WebpConverterService. // Transform implements converter.WebpConverter.
func (w webpConverterService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WebpTransformOptions) error { func (w webpConverter) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WebpTransformOptions) error {
i, err := webp.Decode(in) i, err := webp.Decode(in)
if err != nil { if err != nil {
return err return err
@ -29,8 +29,8 @@ func (w webpConverterService) Transform(ctx context.Context, in io.Reader, out i
return converter.ErrUnknownFormat return converter.ErrUnknownFormat
} }
var _ converter.WebpConverterService = webpConverterService{} var _ converter.WebpConverter = webpConverter{}
func NewService() *webpConverterService { func NewConverter() *webpConverter {
return &webpConverterService{} return &webpConverter{}
} }