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
|
||||
|
||||
## 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)
|
||||
- WEBM → WEBP (first framge, all frames, N frame, frames range)
|
||||
-
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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