diff --git a/README.md b/README.md index 82ed22a..042480d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # Telegram sticker converter +## System requirements +- ffmpeg + +## Build requirements +- Go 1.24 + ## Supported formats - TGS -- WEBP **[WIP]** -- WEBM +- WEBP +- WEBM ## Supported transformations ### TGS @@ -16,12 +22,13 @@ - TGS → MP4 ### WEBP -- WEBP → PNG **[WIP]** -- WEBP → JPEG **[WIP]** +- WEBP → PNG +- WEBP → JPEG ### WEBM - WEBM → MP4 -- WEBM → GIF +- 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 +- WEBM → WEBP (first framge, all frames, N frame, frames range) +- \ No newline at end of file diff --git a/internal/converter/tgs.go b/internal/converter/tgs.go index b6ed622..a382d15 100644 --- a/internal/converter/tgs.go +++ b/internal/converter/tgs.go @@ -2,34 +2,9 @@ package converter import ( "context" - "errors" "io" ) -type OutputFormat string -type FrameSelector int - -const ( - FrameFirst FrameSelector = iota - FrameAll - FrameN - FrameRange -) - -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 TGSTransformOptions struct { Format OutputFormat Frame FrameSelector diff --git a/internal/converter/types.go b/internal/converter/types.go new file mode 100644 index 0000000..ca083d5 --- /dev/null +++ b/internal/converter/types.go @@ -0,0 +1,27 @@ +package converter + +import "errors" + +type OutputFormat string +type FrameSelector int + +const ( + FrameFirst FrameSelector = iota + FrameAll + FrameN + FrameRange +) + +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" +) diff --git a/internal/converter/webp.go b/internal/converter/webp.go new file mode 100644 index 0000000..639f231 --- /dev/null +++ b/internal/converter/webp.go @@ -0,0 +1,14 @@ +package converter + +import ( + "context" + "io" +) + +type WebpTransformOptions struct { + Format OutputFormat +} + +type WebpConverterService interface { + Transform(ctx context.Context, in io.Reader, out io.Writer, opts WebpTransformOptions) error +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index 294b326..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,17 +0,0 @@ -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/webm/ffmpeg.go b/internal/webm/ffmpeg.go new file mode 100644 index 0000000..58bee5d --- /dev/null +++ b/internal/webm/ffmpeg.go @@ -0,0 +1,129 @@ +package webm + +import ( + "fmt" + + ffmpeg_go "github.com/u2takey/ffmpeg-go" + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" +) + +var ( + PresetMP4 = ffmpeg_go.KwArgs{ + "vcodec": "libx264", + "format": "mp4", + "pix_fmt": "yuv420p", + "movflags": "frag_keyframe+empty_moov", + "preset": "ultrafast", + "tune": "zerolatency", + } + + PresetPNG = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "png", + "vsync": "0", + } + + PresetJPEG = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "mjpeg", + "vsync": "0", + } + + PresetWEBP = ffmpeg_go.KwArgs{ + "f": "image2pipe", + "c:v": "libwebp", + } + PresetGIF = ffmpeg_go.KwArgs{"f": "gif"} +) + +func buildPreset(opts converter.WEBMTransformOptions) (ffmpeg_go.KwArgs, error) { + switch opts.Format { + case converter.FormatGIF: + return PresetGIF, nil + case converter.FormatMP4: + return PresetMP4, nil + case converter.FormatPNG: + switch opts.Frame { + case converter.FrameAll: + return PresetPNG, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vframes": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetPNG, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + case converter.FormatJPEG: + switch opts.Frame { + case converter.FrameAll: + return PresetJPEG, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "frames": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetJPEG, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + case converter.FormatWEBP: + switch opts.Frame { + case converter.FrameAll: + return PresetWEBP, nil + case converter.FrameFirst: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "frames": "1", + }, + }), nil + case converter.FrameN: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "vframes": "1", + "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), + }, + }), nil + case converter.FrameRange: + return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ + PresetWEBP, + { + "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), + }, + }), nil + } + } + + return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat +} diff --git a/internal/webm/service.go b/internal/webm/service.go index be36183..aee82a3 100644 --- a/internal/webm/service.go +++ b/internal/webm/service.go @@ -18,131 +18,6 @@ func NewService() *webmService { } func (ws webmService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { - return ws.process(ctx, in, out, opts) -} - -var ( - PresetMP4 = ffmpeg_go.KwArgs{ - "vcodec": "libx264", - "format": "mp4", - "pix_fmt": "yuv420p", - "movflags": "frag_keyframe+empty_moov", - "preset": "ultrafast", - "tune": "zerolatency", - } - - PresetPNG = ffmpeg_go.KwArgs{ - "f": "image2pipe", - "c:v": "png", - "vsync": "0", - } - - PresetJPEG = ffmpeg_go.KwArgs{ - "f": "image2pipe", - "c:v": "mjpeg", - "vsync": "0", - } - - PresetWEBP = ffmpeg_go.KwArgs{ - "f": "image2pipe", - "c:v": "libwebp", - } - PresetGIF = ffmpeg_go.KwArgs{"f": "gif"} -) - -func buildPreset(opts converter.WEBMTransformOptions) (ffmpeg_go.KwArgs, error) { - switch opts.Format { - case converter.FormatGIF: - return PresetGIF, nil - case converter.FormatMP4: - return PresetMP4, nil - case converter.FormatPNG: - switch opts.Frame { - case converter.FrameAll: - return PresetPNG, nil - case converter.FrameFirst: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetPNG, - { - "vframes": "1", - }, - }), nil - case converter.FrameN: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetPNG, - { - "vframes": "1", - "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), - }, - }), nil - case converter.FrameRange: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetPNG, - { - "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), - }, - }), nil - } - case converter.FormatJPEG: - switch opts.Frame { - case converter.FrameAll: - return PresetJPEG, nil - case converter.FrameFirst: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetJPEG, - { - "frames": "1", - }, - }), nil - case converter.FrameN: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetJPEG, - { - "vframes": "1", - "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), - }, - }), nil - case converter.FrameRange: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetJPEG, - { - "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), - }, - }), nil - } - case converter.FormatWEBP: - switch opts.Frame { - case converter.FrameAll: - return PresetWEBP, nil - case converter.FrameFirst: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetWEBP, - { - "frames": "1", - }, - }), nil - case converter.FrameN: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetWEBP, - { - "vframes": "1", - "vf": fmt.Sprintf("select=eq(n\\,%d)", opts.FrameIndex), - }, - }), nil - case converter.FrameRange: - return ffmpeg_go.MergeKwArgs([]ffmpeg_go.KwArgs{ - PresetWEBP, - { - "vf": fmt.Sprintf("select=between(n\\,%d\\,%d)", opts.FrameIndex, opts.FrameIndex+opts.FrameOffset), - }, - }), nil - } - } - - return ffmpeg_go.KwArgs{}, converter.ErrUnknownFormat -} - -func (ws webmService) process(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error { rIn, wIn := io.Pipe() rOut, wOut := io.Pipe() diff --git a/internal/wepb/service.go b/internal/wepb/service.go new file mode 100644 index 0000000..2966aa9 --- /dev/null +++ b/internal/wepb/service.go @@ -0,0 +1,36 @@ +package wepb + +import ( + "context" + "image/jpeg" + "image/png" + "io" + + "github.com/yazmeyaa/telegram_sticker_converter/internal/converter" + "golang.org/x/image/webp" +) + +type webpConverterService struct{} + +// Transform implements converter.WebpConverterService. +func (w webpConverterService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WebpTransformOptions) error { + i, err := webp.Decode(in) + if err != nil { + return err + } + + if opts.Format == converter.FormatPNG { + return png.Encode(out, i) + } + if opts.Format == converter.FormatJPEG { + return jpeg.Encode(out, i, nil) + } + + return converter.ErrUnknownFormat +} + +var _ converter.WebpConverterService = webpConverterService{} + +func NewService() *webpConverterService { + return &webpConverterService{} +}