refactor
This commit is contained in:
parent
62fffc86e9
commit
ad939717ad
120
README.md
120
README.md
|
|
@ -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 → 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 → MP4
|
## Examples
|
||||||
- WEBM → GIF
|
### Extract frame from TGS
|
||||||
- WEBM → PNG (first framge, all frames, N frame, frames range)
|
```go
|
||||||
- WEBM → JPEG (first framge, all frames, N frame, frames range)
|
package main
|
||||||
- WEBM → 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
2
tgs.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
2
webp.go
2
webp.go
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue