Implement WEBP transformations
This commit is contained in:
parent
150ee347cd
commit
7ddda58558
19
README.md
19
README.md
|
|
@ -1,9 +1,15 @@
|
||||||
# Telegram sticker converter
|
# Telegram sticker converter
|
||||||
|
|
||||||
|
## System requirements
|
||||||
|
- ffmpeg
|
||||||
|
|
||||||
|
## Build requirements
|
||||||
|
- Go 1.24
|
||||||
|
|
||||||
## Supported formats
|
## Supported formats
|
||||||
- TGS
|
- TGS
|
||||||
- WEBP **[WIP]**
|
- WEBP
|
||||||
- WEBM
|
- WEBM
|
||||||
|
|
||||||
## Supported transformations
|
## Supported transformations
|
||||||
### TGS
|
### TGS
|
||||||
|
|
@ -16,12 +22,13 @@
|
||||||
- TGS → MP4
|
- TGS → MP4
|
||||||
|
|
||||||
### WEBP
|
### WEBP
|
||||||
- WEBP → PNG **[WIP]**
|
- WEBP → PNG
|
||||||
- WEBP → JPEG **[WIP]**
|
- WEBP → JPEG
|
||||||
|
|
||||||
### WEBM
|
### WEBM
|
||||||
- WEBM → MP4
|
- WEBM → MP4
|
||||||
- WEBM → GIF
|
- WEBM → GIF
|
||||||
- WEBM → PNG (first framge, all frames, N frame, frames range)
|
- WEBM → PNG (first framge, all frames, N frame, frames range)
|
||||||
- WEBM → JPEG (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)
|
- WEBM → WEBP (first framge, all frames, N frame, frames range)
|
||||||
|
-
|
||||||
|
|
@ -2,34 +2,9 @@ package converter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"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 {
|
type TGSTransformOptions struct {
|
||||||
Format OutputFormat
|
Format OutputFormat
|
||||||
Frame FrameSelector
|
Frame FrameSelector
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -18,131 +18,6 @@ func NewService() *webmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws webmService) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.WEBMTransformOptions) error {
|
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()
|
rIn, wIn := io.Pipe()
|
||||||
rOut, wOut := io.Pipe()
|
rOut, wOut := io.Pipe()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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{}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue