From 1db6864ea09b9a24e6d63fd4794c8a5f260116c9 Mon Sep 17 00:00:00 2001 From: eugene Date: Sun, 7 Sep 2025 22:57:44 +0300 Subject: [PATCH] update: add webm support --- README.md | 10 +- cmd/webm_file_convert/webm_file_convert.go | 75 ++++++++++++++ internal/tgs/service.go | 1 + internal/webm/frame_scanner.go | 111 +++++++++++++++++++++ internal/webm/service.go | 58 +++++++++-- 5 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 cmd/webm_file_convert/webm_file_convert.go create mode 100644 internal/webm/frame_scanner.go diff --git a/README.md b/README.md index df5bf24..db92e4b 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ - WEBP → JPEG **[WIP]** ### WEBM -- WEBM → MP4 **[WIP]** -- WEBM → GIF **[WIP]** -- WEBM → PNG (first framge, all frames, N frame, frames range) **[WIP]** -- WEBM → JPEG (first framge, all frames, N frame, frames range) **[WIP]** -- WEBM → WEBP (first framge, all frames, N frame, frames range) **[WIP]** \ No newline at end of file +- 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 diff --git a/cmd/webm_file_convert/webm_file_convert.go b/cmd/webm_file_convert/webm_file_convert.go new file mode 100644 index 0000000..893832c --- /dev/null +++ b/cmd/webm_file_convert/webm_file_convert.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" + "github.com/yazmeyaa/telegram_sticker_converter/internal/webm" +) + +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.Uint("frame-index", 0, "frame index (used only with frame=n)") + resizeW := flag.Uint("resize-width", 0, "resize width (0 = keep original)") + resizeH := flag.Uint("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.WEBMTransformOptions{ + Format: converter.OutputFormat(*format), + Frame: frameSel, + FrameIndex: *frameIndex, + Width: *resizeW, + Height: *resizeH, + } + + service := converter.WEBMConverter(webm.NewService()) + if err := service.Transform(context.Background(), in, out, opts); err != nil { + fmt.Fprintf(os.Stderr, "transform failed: %v\n", err) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/internal/tgs/service.go b/internal/tgs/service.go index 6164488..cbddfb0 100644 --- a/internal/tgs/service.go +++ b/internal/tgs/service.go @@ -150,6 +150,7 @@ func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_An "s": fmt.Sprintf("%dx%d", opts.ResizeWidth, opts.ResizeHeight), "r": frameRate, }). + Silent(true). Output("pipe:1", preset). WithInput(r). WithOutput(out). diff --git a/internal/webm/frame_scanner.go b/internal/webm/frame_scanner.go new file mode 100644 index 0000000..2da5c45 --- /dev/null +++ b/internal/webm/frame_scanner.go @@ -0,0 +1,111 @@ +package webm + +import ( + "bytes" + "errors" + "io" + + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" +) + +type frameScanner struct { + r io.Reader + opts converter.WEBMTransformOptions + + buf []byte + frame []byte + eof bool +} + +func newScanner(r io.Reader, opts converter.WEBMTransformOptions) *frameScanner { + return &frameScanner{ + r: r, + opts: opts, + buf: make([]byte, 0, 64*1024), + } +} + +var ( + pngStart = []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + pngEnd = []byte{0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82} + + jpegStart = []byte{0xFF, 0xD8} + jpegEnd = []byte{0xFF, 0xD9} + + webpStart = []byte{'R', 'I', 'F', 'F'} + webpWebP = []byte{'W', 'E', 'B', 'P'} +) + +func (f *frameScanner) signatures() ([]byte, []byte) { + switch f.opts.Format { + case converter.FormatPNG: + return pngStart, pngEnd + case converter.FormatJPEG: + return jpegStart, jpegEnd + case converter.FormatWEBP: + return webpStart, nil + default: + return nil, nil + } +} + +func (f *frameScanner) Next() ([]byte, error) { + if f.eof { + return nil, io.EOF + } + + startSig, endSig := f.signatures() + if startSig == nil { + return nil, converter.ErrUnknownFormat + } + + for { + startIdx := bytes.Index(f.buf, startSig) + if startIdx >= 0 { + switch f.opts.Format { + case converter.FormatWEBP: + if len(f.buf[startIdx:]) < 12 { + break + } + if !bytes.Equal(f.buf[startIdx+8:startIdx+12], webpWebP) { + f.buf = f.buf[startIdx+4:] + continue + } + size := int(uint32(f.buf[startIdx+4]) | + uint32(f.buf[startIdx+5])<<8 | + uint32(f.buf[startIdx+6])<<16 | + uint32(f.buf[startIdx+7])<<24) + total := 8 + size + if len(f.buf[startIdx:]) < total { + break + } + frame := f.buf[startIdx : startIdx+total] + f.buf = append([]byte{}, f.buf[startIdx+total:]...) + return frame, nil + + default: + endIdx := bytes.Index(f.buf[startIdx+len(startSig):], endSig) + if endIdx >= 0 { + endIdx += startIdx + len(startSig) + frame := f.buf[startIdx : endIdx+len(endSig)] + f.buf = append([]byte{}, f.buf[endIdx+len(endSig):]...) + return frame, nil + } + } + } + + tmp := make([]byte, 8192) + n, err := f.r.Read(tmp) + if n > 0 { + f.buf = append(f.buf, tmp[:n]...) + continue + } + if errors.Is(err, io.EOF) { + f.eof = true + return nil, io.EOF + } + if err != nil { + return nil, err + } + } +} diff --git a/internal/webm/service.go b/internal/webm/service.go index 7f2af58..be36183 100644 --- a/internal/webm/service.go +++ b/internal/webm/service.go @@ -1,7 +1,9 @@ package webm import ( + "archive/zip" "context" + "errors" "fmt" "io" @@ -136,15 +138,17 @@ func buildPreset(opts converter.WEBMTransformOptions) (ffmpeg_go.KwArgs, error) }), nil } } + return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat } func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { - r, w := io.Pipe() + rIn, wIn := io.Pipe() + rOut, wOut := io.Pipe() go func() { - defer w.Close() - io.Copy(w, in) + defer wIn.Close() + _, _ = io.Copy(wIn, in) }() preset, err := buildPreset(opts) @@ -152,16 +156,54 @@ func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, return err } - err = ffmpeg_go. + stream := ffmpeg_go. Input("pipe:0", ffmpeg_go.KwArgs{ "f": "webm", }). + Silent(true). Output("pipe:1", preset). - WithInput(r). - WithOutput(out). - Run() + WithInput(rIn). + WithOutput(wOut) - if err != nil { + if (opts.Frame == converter.FrameAll || opts.Frame == converter.FrameRange) && + (opts.Format == converter.FormatWEBP || opts.Format == converter.FormatJPEG || opts.Format == converter.FormatPNG) { + + go func() { + defer wOut.Close() + _ = stream.Run() + }() + + zw := zip.NewWriter(out) + defer zw.Close() + + frameIdx := 0 + scanner := newScanner(rOut, opts) + + for { + frame, err := scanner.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return err + } + + f, err := zw.Create(fmt.Sprintf("frame_%d.%s", frameIdx, string(opts.Format))) + if err != nil { + return err + } + + if _, err := f.Write(frame); err != nil { + return err + } + frameIdx++ + } + + return nil + } + + defer wOut.Close() + if err := stream.WithOutput(out).Run(); err != nil { return err }