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