init commit
This commit is contained in:
commit
f50161874a
|
|
@ -0,0 +1 @@
|
||||||
|
build
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Telegram sticker converter
|
||||||
|
|
||||||
|
## Supported formats
|
||||||
|
- TGS
|
||||||
|
- WEBP **[WIP]**
|
||||||
|
- WEBM **[WIP]**
|
||||||
|
|
||||||
|
## Supported transformations
|
||||||
|
### TGS
|
||||||
|
- TGS → PNG (first framge, all frames, N frame)
|
||||||
|
- TGS → JPEG (first framge, all frames, N frame)
|
||||||
|
- TGS → WEBP (first framge, all frames, N frame)
|
||||||
|
- TGS → Lottie.JSON
|
||||||
|
- TGS → GIF
|
||||||
|
- TGS → WEBM
|
||||||
|
- TGS → MP4
|
||||||
|
|
||||||
|
### WEBP
|
||||||
|
- WEBP → PNG **[WIP]**
|
||||||
|
- WEBP → JPEG **[WIP]**
|
||||||
|
|
||||||
|
### WEBM
|
||||||
|
- WEBM → MP4 **[WIP]**
|
||||||
|
- WEBM → GIF **[WIP]**
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yazmeyaa/telegram_sticker_converter/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := logger.NewLogger(os.Stdout, "telegram_bot")
|
||||||
|
logger.Info("Starting telegram bot")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/yazmeyaa/telegram_sticker_converter/internal/converter"
|
||||||
|
"github.com/yazmeyaa/telegram_sticker_converter/internal/tgs"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.Int("frame-index", 0, "frame index (used only with frame=n)")
|
||||||
|
quality := flag.Int("quality", 90, "output quality (0-100)")
|
||||||
|
resizeW := flag.Int("resize-width", 0, "resize width (0 = keep original)")
|
||||||
|
resizeH := flag.Int("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.TransformOptions{
|
||||||
|
Format: converter.OutputFormat(*format),
|
||||||
|
Frame: frameSel,
|
||||||
|
FrameIndex: *frameIndex,
|
||||||
|
Qualtity: *quality,
|
||||||
|
ResizeWidth: *resizeW,
|
||||||
|
ResizeHeight: *resizeH,
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stdout, "opts: %+v\n", opts)
|
||||||
|
|
||||||
|
service := converter.TGSConverterService(tgs.NewService())
|
||||||
|
if err := service.Transform(context.Background(), in, out, opts); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "transform failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("success:", *outPath)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
module github.com/yazmeyaa/telegram_sticker_converter
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/arugaz/go-rlottie v0.1.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go v1.55.8 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0 // indirect
|
||||||
|
github.com/u2takey/go-utils v0.3.1 // indirect
|
||||||
|
golang.org/x/image v0.30.0 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
github.com/arugaz/go-rlottie v0.1.0 h1:/rQaoBoSEG2T+PW56ANvpkgOHdYYtGZpvxzjI6touZ8=
|
||||||
|
github.com/arugaz/go-rlottie v0.1.0/go.mod h1:m50xy50q5U9ngFIBJja9m09vFhvfw6cxkRiqIxjKeWQ=
|
||||||
|
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
|
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
|
||||||
|
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||||
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0 h1:r7d86XuL7uLWJ5mzSeQ03uvjfIhiJYvsRAJFCW4uklU=
|
||||||
|
github.com/u2takey/ffmpeg-go v0.5.0/go.mod h1:ruZWkvC1FEiUNjmROowOAps3ZcWxEiOpFoHCvk97kGc=
|
||||||
|
github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys=
|
||||||
|
github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs=
|
||||||
|
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||||
|
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutputFormat string
|
||||||
|
type FrameSelector int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FrameFirst FrameSelector = iota
|
||||||
|
FrameAll
|
||||||
|
FrameN
|
||||||
|
)
|
||||||
|
|
||||||
|
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 TransformOptions struct {
|
||||||
|
Format OutputFormat
|
||||||
|
Frame FrameSelector
|
||||||
|
FrameIndex int
|
||||||
|
Qualtity int
|
||||||
|
ResizeWidth int
|
||||||
|
ResizeHeight int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TGSConverterService interface {
|
||||||
|
Transform(ctx context.Context, data io.Reader, out io.Writer, opts TransformOptions) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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,237 @@
|
||||||
|
package tgs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/arugaz/go-rlottie"
|
||||||
|
ffmpeg_go "github.com/u2takey/ffmpeg-go"
|
||||||
|
"github.com/yazmeyaa/telegram_sticker_converter/internal/converter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) Transform(ctx context.Context, in io.Reader, out io.Writer, opts converter.TransformOptions) error {
|
||||||
|
data, err := io.ReadAll(in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gr, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer gr.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, gr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if opts.Format == converter.FormatLottie {
|
||||||
|
_, err := out.Write(buf.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
anim := rlottie.LottieAnimationFromData(buf.String(), "", "")
|
||||||
|
defer rlottie.LottieAnimationDestroy(anim)
|
||||||
|
|
||||||
|
width, height := rlottie.LottieAnimationGetSize(anim)
|
||||||
|
if opts.ResizeWidth == 0 && opts.ResizeHeight == 0 {
|
||||||
|
opts.ResizeWidth = int(width)
|
||||||
|
opts.ResizeHeight = int(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Format == converter.FormatGIF || opts.Format == converter.FormatWEBM || opts.Format == converter.FormatMP4 {
|
||||||
|
return t.processVideo(ctx, anim, out, opts)
|
||||||
|
}
|
||||||
|
if opts.Format == converter.FormatPNG || opts.Format == converter.FormatWEBP || opts.Format == converter.FormatJPEG {
|
||||||
|
return t.processFrames(ctx, anim, out, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return converter.ErrUnknownFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) processFrames(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error {
|
||||||
|
width := uint(opts.ResizeWidth)
|
||||||
|
height := uint(opts.ResizeHeight)
|
||||||
|
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
width, height = rlottie.LottieAnimationGetSize(anim)
|
||||||
|
}
|
||||||
|
|
||||||
|
frameBuffer := make([]byte, width*height*4)
|
||||||
|
|
||||||
|
if opts.Frame == converter.FrameFirst {
|
||||||
|
if err := t.processFrame(ctx, anim, 0, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.makeSingleImage(ctx, frameBuffer, out, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Frame == converter.FrameN {
|
||||||
|
if err := t.processFrame(ctx, anim, uint(opts.FrameIndex), uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.makeSingleImage(ctx, frameBuffer, out, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Frame == converter.FrameAll {
|
||||||
|
return t.makeAllImages(ctx, anim, frameBuffer, out, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
PresetGif = ffmpeg_go.KwArgs{
|
||||||
|
"f": "gif",
|
||||||
|
"pix_fmt": "rgb24",
|
||||||
|
}
|
||||||
|
PresetWebm = ffmpeg_go.KwArgs{
|
||||||
|
"vcodec": "libvpx",
|
||||||
|
"format": "webm",
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"b:v": "0",
|
||||||
|
"deadline": "realtime",
|
||||||
|
}
|
||||||
|
PresetMP4 = ffmpeg_go.KwArgs{
|
||||||
|
"vcodec": "libx264",
|
||||||
|
"format": "mp4",
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"movflags": "frag_keyframe+empty_moov",
|
||||||
|
"preset": "ultrafast",
|
||||||
|
"tune": "zerolatency",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) processVideo(ctx context.Context, anim rlottie.Lottie_Animation, out io.Writer, opts converter.TransformOptions) error {
|
||||||
|
totalFrames := rlottie.LottieAnimationGetTotalframe(anim)
|
||||||
|
frameRate := rlottie.LottieAnimationGetFramerate(anim)
|
||||||
|
var preset ffmpeg_go.KwArgs
|
||||||
|
|
||||||
|
switch opts.Format {
|
||||||
|
case converter.FormatGIF:
|
||||||
|
preset = PresetGif
|
||||||
|
case converter.FormatWEBM:
|
||||||
|
preset = PresetWebm
|
||||||
|
case converter.FormatMP4:
|
||||||
|
preset = PresetMP4
|
||||||
|
default:
|
||||||
|
return converter.ErrUnknownFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
frameBuffer := make([]byte, opts.ResizeWidth*opts.ResizeHeight*4)
|
||||||
|
r, w := io.Pipe()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for frameIdx := range totalFrames {
|
||||||
|
err := t.processFrame(ctx, anim, frameIdx, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer)
|
||||||
|
if err != nil {
|
||||||
|
w.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(frameBuffer)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := ffmpeg_go.
|
||||||
|
Input("pipe:0", ffmpeg_go.KwArgs{
|
||||||
|
"format": "rawvideo",
|
||||||
|
"pix_fmt": "bgra",
|
||||||
|
"s": fmt.Sprintf("%dx%d", opts.ResizeWidth, opts.ResizeHeight),
|
||||||
|
"r": frameRate,
|
||||||
|
}).
|
||||||
|
Output("pipe:1", preset).
|
||||||
|
WithInput(r).
|
||||||
|
WithOutput(out).
|
||||||
|
Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BGRAtoRGBA(buf []byte) {
|
||||||
|
for i := 0; i < len(buf); i += 4 {
|
||||||
|
buf[i], buf[i+2] = buf[i+2], buf[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) makeSingleImage(ctx context.Context, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error {
|
||||||
|
BGRAtoRGBA(frameBuffer)
|
||||||
|
img := &image.RGBA{
|
||||||
|
Pix: frameBuffer,
|
||||||
|
Stride: int(opts.ResizeWidth) * 4,
|
||||||
|
Rect: image.Rect(0, 0, opts.ResizeWidth, opts.ResizeHeight),
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Format == converter.FormatPNG {
|
||||||
|
return png.Encode(out, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Format == converter.FormatJPEG {
|
||||||
|
return jpeg.Encode(out, img, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// if opts.Format == converter.FormatWEBP {
|
||||||
|
// }
|
||||||
|
return converter.ErrUnknownFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) makeAllImages(ctx context.Context, anim rlottie.Lottie_Animation, frameBuffer []byte, out io.Writer, opts converter.TransformOptions) error {
|
||||||
|
totalFrames := rlottie.LottieAnimationGetTotalframe(anim)
|
||||||
|
archive := zip.NewWriter(out)
|
||||||
|
defer archive.Close()
|
||||||
|
|
||||||
|
for i := range totalFrames {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := archive.Create(fmt.Sprintf("frame_%d.%s", i, opts.Format))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.processFrame(ctx, anim, i, uint(opts.ResizeWidth), uint(opts.ResizeHeight), frameBuffer)
|
||||||
|
|
||||||
|
if err := t.makeSingleImage(ctx, frameBuffer, file, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := archive.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t tgsServiceImpl) processFrame(ctx context.Context, anim rlottie.Lottie_Animation, frameN uint, sx uint, sy uint, out []byte) error {
|
||||||
|
expectedSize := int(sx * sy * 4)
|
||||||
|
rlottie.LottieAnimationRender(anim, frameN, out, sx, sy, sx*4)
|
||||||
|
if len(out) < expectedSize {
|
||||||
|
return errors.New("not valid buffer size")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tgsServiceImpl struct{}
|
||||||
|
|
||||||
|
func NewService() *tgsServiceImpl {
|
||||||
|
return &tgsServiceImpl{}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue