Implement WEBP transformations

This commit is contained in:
eugene 2025-09-08 18:18:03 +03:00
parent 150ee347cd
commit 7ddda58558
8 changed files with 219 additions and 173 deletions

View File

@ -1,8 +1,14 @@
# Telegram sticker converter
## System requirements
- ffmpeg
## Build requirements
- Go 1.24
## Supported formats
- TGS
- WEBP **[WIP]**
- WEBP
- WEBM
## Supported transformations
@ -16,8 +22,8 @@
- TGS → MP4
### WEBP
- WEBP → PNG **[WIP]**
- WEBP → JPEG **[WIP]**
- WEBP → PNG
- WEBP → JPEG
### WEBM
- WEBM → MP4
@ -25,3 +31,4 @@
- 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)
-

View File

@ -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

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

129
internal/webm/ffmpeg.go Normal file
View File

@ -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
}

View File

@ -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()

36
internal/wepb/service.go Normal file
View File

@ -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{}
}