diff --git a/README.md b/README.md index 83fd15e..10c4029 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Telegram sticker converter -## System requirements -- ffmpeg +## Required system binaries +- ffmpeg 7.1.1 ## Build requirements - Go 1.24 @@ -12,22 +12,104 @@ - WEBM ## 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 -- WEBP → PNG -- WEBP → JPEG +| Input | Output formats | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **TGS** | PNG (first frame, all frames, N frame, frame range)
JPEG (first frame, all frames, N frame, frame range)
WEBP (first frame, all frames, N frame, frame range)
Lottie JSON
GIF
WEBM
MP4 | +| **WEBP** | PNG
JPEG | +| **WEBM** | MP4
GIF
PNG (first frame, all frames, N frame, frame range)
JPEG (first frame, all frames, N frame, frame range)
WEBP (first frame, all frames, N frame, frame range) | -### WEBM -- WEBM → MP4 -- WEBM → GIF -- WEBM → PNG (first framge, all frames, N frame, frames range) -- WEBM → JPEG (first framge, all frames, N frame, frames range) -- WEBM → WEBP (first framge, all frames, N frame, frames range) \ No newline at end of file + +## Examples +### Extract frame from TGS +```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.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) + } +} +``` \ No newline at end of file diff --git a/cmd/tgs_file_convert/tgs_file_convert.go b/cmd/tgs_file_convert/tgs_file_convert.go index 45eb087..caa7669 100644 --- a/cmd/tgs_file_convert/tgs_file_convert.go +++ b/cmd/tgs_file_convert/tgs_file_convert.go @@ -5,39 +5,53 @@ import ( "flag" "fmt" "os" + "strings" converter "github.com/yazmeyaa/telegram_sticker_converter" "github.com/yazmeyaa/telegram_sticker_converter/tgs" ) func main() { - filePath := flag.String("file", "", "path to input .tgs file") - outPath := flag.String("out", "", "path to output file") + input := flag.String("input", "stream:stdin", "path to input .tgs file") + outPath := flag.String("output", "", "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)") + quality := flag.Int("quality", 100, "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") + inputParts := strings.Split(*input, ":") + 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) } - if *outPath == "" { - fmt.Fprintln(os.Stderr, "error: -out is required") - os.Exit(1) + inputType := inputParts[0] + var in *os.File + if inputType == "file" { + input, err := os.Open(*input) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening file: %v\n", err) + os.Exit(1) + } + in = input } - - in, err := os.Open(*filePath) - if err != nil { - fmt.Fprintf(os.Stderr, "error opening file: %v\n", err) + if inputType == "stream" { + in = os.Stdin + } + if in == nil { + fmt.Fprintf(os.Stderr, "Unexpected input type\n") os.Exit(1) } defer in.Close() + if *outPath == "" { + fmt.Fprintln(os.Stderr, "error: -output is required") + os.Exit(1) + } + out, err := os.Create(*outPath) if err != nil { fmt.Fprintf(os.Stderr, "error creating output: %v\n", err) @@ -67,8 +81,9 @@ func main() { ResizeHeight: *resizeH, } - service := converter.TGSConverterService(tgs.NewService()) - if err := service.Transform(context.Background(), in, out, opts); err != nil { + converter := tgs.NewConverter() + + if err := converter.Transform(context.Background(), in, out, opts); err != nil { fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) os.Exit(1) } diff --git a/cmd/webm_file_convert/webm_file_convert.go b/cmd/webm_file_convert/webm_file_convert.go index 77f11da..e3daa5b 100644 --- a/cmd/webm_file_convert/webm_file_convert.go +++ b/cmd/webm_file_convert/webm_file_convert.go @@ -65,8 +65,8 @@ func main() { Height: *resizeH, } - service := converter.WEBMConverter(webm.NewService()) - if err := service.Transform(context.Background(), in, out, opts); err != nil { + converter := converter.WEBMConverter(webm.NewConverter()) + if err := converter.Transform(context.Background(), in, out, opts); err != nil { fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) os.Exit(1) } diff --git a/tgs.go b/tgs.go index a382d15..f3f2529 100644 --- a/tgs.go +++ b/tgs.go @@ -15,6 +15,6 @@ type TGSTransformOptions struct { ResizeHeight int } -type TGSConverterService interface { +type TGSConverter interface { Transform(ctx context.Context, data io.Reader, out io.Writer, opts TGSTransformOptions) error } diff --git a/tgs/service.go b/tgs/service.go index 2d3cf29..7f902d8 100644 --- a/tgs/service.go +++ b/tgs/service.go @@ -17,7 +17,7 @@ import ( 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) if err != nil { return err @@ -57,7 +57,7 @@ func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writ 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) 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) frameRate := rlottie.LottieAnimationGetFramerate(anim) 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) img := &image.RGBA{ Pix: frameBuffer, @@ -191,7 +191,7 @@ func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, 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) archive := zip.NewWriter(out) defer archive.Close() @@ -222,7 +222,7 @@ func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_A 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) rlottie.LottieAnimationRender(anim, frameN, out, sx, sy, sx*4) if len(out) < expectedSize { @@ -231,8 +231,8 @@ func (t tgsServiceImpl) processFrame(ctx context.Context, anim rlottie.Lottie_An return nil } -type tgsServiceImpl struct{} +type tgsConverterImpl struct{} -func NewService() *tgsServiceImpl { - return &tgsServiceImpl{} +func NewConverter() *tgsConverterImpl { + return &tgsConverterImpl{} } diff --git a/webm/service.go b/webm/service.go index 199889f..2603c6f 100644 --- a/webm/service.go +++ b/webm/service.go @@ -11,13 +11,13 @@ import ( converter "github.com/yazmeyaa/telegram_sticker_converter" ) -type webmService struct{} +type webmConverter struct{} -func NewService() *webmService { - return &webmService{} +func NewConverter() *webmConverter { + 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() rOut, wOut := io.Pipe() diff --git a/webp.go b/webp.go index 639f231..2fd81d2 100644 --- a/webp.go +++ b/webp.go @@ -9,6 +9,6 @@ type WebpTransformOptions struct { Format OutputFormat } -type WebpConverterService interface { +type WebpConverter interface { Transform(ctx context.Context, in io.Reader, out io.Writer, opts WebpTransformOptions) error } diff --git a/webp/service.go b/webp/service.go index 6fc321f..16002a4 100644 --- a/webp/service.go +++ b/webp/service.go @@ -10,10 +10,10 @@ import ( "golang.org/x/image/webp" ) -type webpConverterService struct{} +type webpConverter struct{} -// Transform implements converter.WebpConverterService. -func (w webpConverterService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WebpTransformOptions) error { +// Transform implements converter.WebpConverter. +func (w webpConverter) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WebpTransformOptions) error { i, err := webp.Decode(in) if err != nil { return err @@ -29,8 +29,8 @@ func (w webpConverterService) Transform(ctx context.Context, in io.Reader, out i return converter.ErrUnknownFormat } -var _ converter.WebpConverterService = webpConverterService{} +var _ converter.WebpConverter = webpConverter{} -func NewService() *webpConverterService { - return &webpConverterService{} +func NewConverter() *webpConverter { + return &webpConverter{} }