diff options
Diffstat (limited to 'vendor/github.com/Benau/tgsconverter/libtgsconverter')
8 files changed, 729 insertions, 0 deletions
diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/apng.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/apng.go new file mode 100644 index 00000000..78541533 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/apng.go @@ -0,0 +1,51 @@ +package libtgsconverter + +import "bytes" +import "image" + +import "github.com/kettek/apng" +import "github.com/av-elier/go-decimal-to-rational" + +type toapng struct { + apng apng.APNG + prev_frame *image.RGBA +} + +func(to_apng *toapng) init(w uint, h uint, options ConverterOptions) { +} + +func(to_apng *toapng) SupportsAnimation() bool { + return true +} + +func (to_apng *toapng) AddFrame(image *image.RGBA, fps uint) error { + if to_apng.prev_frame != nil && sameImage(to_apng.prev_frame, image) { + var idx = len(to_apng.apng.Frames) - 1 + var prev_fps = float64(to_apng.apng.Frames[idx].DelayNumerator) / float64(to_apng.apng.Frames[idx].DelayDenominator) + prev_fps += 1.0 / float64(fps) + rat := dectofrac.NewRatP(prev_fps, 0.001) + to_apng.apng.Frames[idx].DelayNumerator = uint16(rat.Num().Int64()) + to_apng.apng.Frames[idx].DelayDenominator = uint16(rat.Denom().Int64()) + return nil + } + f := apng.Frame{} + f.Image = image + f.DelayNumerator = 1 + f.DelayDenominator = uint16(fps) + f.DisposeOp = apng.DISPOSE_OP_BACKGROUND + f.BlendOp = apng.BLEND_OP_SOURCE + f.IsDefault = false + to_apng.apng.Frames = append(to_apng.apng.Frames, f) + to_apng.prev_frame = image + return nil +} + +func (to_apng *toapng) Result() []byte { + var data []byte + w := bytes.NewBuffer(data) + err := apng.Encode(w, to_apng.apng) + if err != nil { + return nil + } + return w.Bytes() +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/gif.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/gif.go new file mode 100644 index 00000000..c4f5c5d9 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/gif.go @@ -0,0 +1,81 @@ +package libtgsconverter + +import "bytes" + +import "image" +import "image/color" +import "image/gif" + +type togif struct { + gif gif.GIF + images []image.Image + prev_frame *image.RGBA +} + +func(to_gif *togif) init(w uint, h uint, options ConverterOptions) { + to_gif.gif.Config.Width = int(w) + to_gif.gif.Config.Height = int(h) +} + +func(to_gif *togif) SupportsAnimation() bool { + return true +} + +func (to_gif *togif) AddFrame(image *image.RGBA, fps uint) error { + var fps_int = int(1.0 / float32(fps) * 100.) + if to_gif.prev_frame != nil && sameImage(to_gif.prev_frame, image) { + to_gif.gif.Delay[len(to_gif.gif.Delay) - 1] += fps_int + return nil + } + to_gif.gif.Image = append(to_gif.gif.Image, nil) + to_gif.gif.Delay = append(to_gif.gif.Delay, fps_int) + to_gif.gif.Disposal = append(to_gif.gif.Disposal, gif.DisposalBackground) + to_gif.images = append(to_gif.images, image) + to_gif.prev_frame = image + return nil +} + +func (to_gif *togif) Result() []byte { + q := medianCutQuantizer{mode, nil, false} + p := q.quantizeMultiple(make([]color.Color, 0, 256), to_gif.images) + // Add transparent entry finally + var trans_idx uint8 = 0 + if q.reserveTransparent { + trans_idx = uint8(len(p)) + } + var id_map = make(map[uint32]uint8) + for i, img := range to_gif.images { + pi := image.NewPaletted(img.Bounds(), p) + for y := 0; y < img.Bounds().Dy(); y++ { + for x := 0; x < img.Bounds().Dx(); x++ { + c := img.At(x, y) + cr, cg, cb, ca := c.RGBA() + cid := (cr >> 8) << 16 | cg | (cb >> 8) + if q.reserveTransparent && ca == 0 { + pi.Pix[pi.PixOffset(x, y)] = trans_idx + } else if val, ok := id_map[cid]; ok { + pi.Pix[pi.PixOffset(x, y)] = val + } else { + val := uint8(p.Index(c)) + pi.Pix[pi.PixOffset(x, y)] = val + id_map[cid] = val + } + } + } + to_gif.gif.Image[i] = pi + } + if q.reserveTransparent { + p = append(p, color.RGBA{0, 0, 0, 0}) + } + for _, img := range to_gif.gif.Image { + img.Palette = p + } + to_gif.gif.Config.ColorModel = p + var data []byte + w := bytes.NewBuffer(data) + err := gif.EncodeAll(w, &to_gif.gif) + if err != nil { + return nil + } + return w.Bytes() +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/imagewriter.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/imagewriter.go new file mode 100644 index 00000000..9549e337 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/imagewriter.go @@ -0,0 +1,40 @@ +package libtgsconverter + +import "image" + +type imageWriter interface { + init(w uint, h uint, options ConverterOptions) + SupportsAnimation() bool + AddFrame(image *image.RGBA, fps uint) error + Result() []byte +} + +func sameImage(a *image.RGBA, b *image.RGBA) bool { + if len(a.Pix) != len(b.Pix) { + return false + } + for i, v := range a.Pix { + if v != b.Pix[i] { + return false + } + } + return true +} + +func newImageWriter(extension string, w uint, h uint, options ConverterOptions) imageWriter { + var writer imageWriter + switch extension { + case "apng": + writer = &toapng{} + case "gif": + writer = &togif{} + case "png": + writer = &topng{} + case "webp": + writer = &towebp{} + default: + return nil + } + writer.init(w, h, options) + return writer +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/lib.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/lib.go new file mode 100644 index 00000000..af6f8ab0 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/lib.go @@ -0,0 +1,160 @@ +package libtgsconverter + +import "bytes" +import "errors" +import "compress/gzip" +import "image" +import "io/ioutil" + +import "github.com/Benau/go_rlottie" + +type ConverterOptions interface { + SetExtension(ext string) + SetFPS(fps uint) + SetScale(scale float32) + SetWebpQuality(webp_quality float32) + GetExtension() string + GetFPS() uint + GetScale() float32 + GetWebpQuality() float32 +} + +type converter_options struct { + // apng, gif, png or webp + extension string + // Frame per second of output image (if you specify apng, gif or webp) + fps uint + // Scale of image result + scale float32 + // Webp encoder quality (0 to 100) + webpQuality float32 +} + +func(opt *converter_options) SetExtension(ext string) { + opt.extension = ext +} + +func(opt *converter_options) SetFPS(fps uint) { + opt.fps = fps +} + +func(opt *converter_options) SetScale(scale float32) { + opt.scale = scale +} + +func(opt *converter_options) SetWebpQuality(webp_quality float32) { + opt.webpQuality = webp_quality +} + +func(opt *converter_options) GetExtension() string { + return opt.extension +} + +func(opt *converter_options) GetFPS() uint { + return opt.fps +} + +func(opt *converter_options) GetScale() float32 { + return opt.scale +} + +func(opt *converter_options) GetWebpQuality() float32 { + return opt.webpQuality +} + +func NewConverterOptions() ConverterOptions { + return &converter_options{"png", 30, 1.0, 75} +} + +func imageFromBuffer(p []byte, w uint, h uint) *image.RGBA { + // rlottie use ARGB32_Premultiplied + for i := 0; i < len(p); i += 4 { + p[i + 0], p[i + 2] = p[i + 2], p[i + 0] + } + m := image.NewRGBA(image.Rect(0, 0, int(w), int(h))) + m.Pix = p + m.Stride = int(w) * 4 + return m +} + +var disabled_cache = false +func ImportFromData(data []byte, options ConverterOptions) ([]byte, error) { + if !disabled_cache { + disabled_cache = true + go_rlottie.LottieConfigureModelCacheSize(0) + } + z, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, errors.New("Failed to create gzip reader:" + err.Error()) + } + uncompressed, err := ioutil.ReadAll(z) + if err != nil { + return nil, errors.New("Failed to read gzip archive") + } + z.Close() + + animation := go_rlottie.LottieAnimationFromData(string(uncompressed[:]), "", "") + if animation == nil { + return nil, errors.New("Failed to import lottie animation data") + } + + w, h := go_rlottie.LottieAnimationGetSize(animation) + w = uint(float32(w) * options.GetScale()) + h = uint(float32(h) * options.GetScale()) + + frame_rate := go_rlottie.LottieAnimationGetFramerate(animation) + frame_count := go_rlottie.LottieAnimationGetTotalframe(animation) + duration := float32(frame_count) / float32(frame_rate) + var desired_framerate = float32(options.GetFPS()) + // Most (Gif) player doesn't support ~60fps (found in most tgs) + if desired_framerate > 50. { + desired_framerate = 50. + } + step := 1.0 / desired_framerate + + writer := newImageWriter(options.GetExtension(), w, h, options) + if writer == nil { + return nil, errors.New("Failed create imagewriter") + } + + var i float32 + for i = 0.; i < duration; i += step { + frame := go_rlottie.LottieAnimationGetFrameAtPos(animation, i / duration) + buf := make([]byte, w * h * 4) + go_rlottie.LottieAnimationRender(animation, frame, buf, w, h, w * 4) + m := imageFromBuffer(buf, w, h) + err := writer.AddFrame(m, uint(desired_framerate)) + if err != nil { + return nil, errors.New("Failed to add frame:" + err.Error()) + } + if !writer.SupportsAnimation() { + break + } + } + go_rlottie.LottieAnimationDestroy(animation) + return writer.Result(), nil +} + +func ImportFromFile(path string, options ConverterOptions) ([]byte, error) { + tgs, err := ioutil.ReadFile(path) + if err != nil { + return nil, errors.New("Error when opening file:" + err.Error()) + } + return ImportFromData(tgs, options) +} + +func SupportsExtension(extension string) (bool) { + switch extension { + case "apng": + fallthrough + case "gif": + fallthrough + case "png": + fallthrough + case "webp": + return true + default: + return false + } + return false +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/png.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/png.go new file mode 100644 index 00000000..cf492ea4 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/png.go @@ -0,0 +1,30 @@ +package libtgsconverter + +import "bytes" +import "image" +import "image/png" + +type topng struct { + result []byte +} + +func(to_png *topng) init(w uint, h uint, options ConverterOptions) { +} + +func(to_png *topng) SupportsAnimation() bool { + return false +} + +func (to_png *topng) AddFrame(image *image.RGBA, fps uint) error { + var data []byte + w := bytes.NewBuffer(data) + if err := png.Encode(w, image); err != nil { + return err + } + to_png.result = w.Bytes() + return nil +} + +func (to_png *topng) Result() []byte { + return to_png.result +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_bucket.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_bucket.go new file mode 100644 index 00000000..1f00c685 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_bucket.go @@ -0,0 +1,119 @@ +package libtgsconverter + +import "image/color" + +type colorAxis uint8 + +// Color axis constants +const ( + red colorAxis = iota + green + blue +) + +type colorPriority struct { + p uint32 + color.RGBA +} + +func (c colorPriority) axis(span colorAxis) uint8 { + switch span { + case red: + return c.R + case green: + return c.G + default: + return c.B + } +} + +type colorBucket []colorPriority + +func (cb colorBucket) partition() (colorBucket, colorBucket) { + mean, span := cb.span() + left, right := 0, len(cb)-1 + for left < right { + cb[left], cb[right] = cb[right], cb[left] + for cb[left].axis(span) < mean && left < right { + left++ + } + for cb[right].axis(span) >= mean && left < right { + right-- + } + } + if left == 0 { + return cb[:1], cb[1:] + } + if left == len(cb)-1 { + return cb[:len(cb)-1], cb[len(cb)-1:] + } + return cb[:left], cb[left:] +} + +func (cb colorBucket) mean() color.RGBA { + var r, g, b uint64 + var p uint64 + for _, c := range cb { + p += uint64(c.p) + r += uint64(c.R) * uint64(c.p) + g += uint64(c.G) * uint64(c.p) + b += uint64(c.B) * uint64(c.p) + } + return color.RGBA{uint8(r / p), uint8(g / p), uint8(b / p), 255} +} + +type constraint struct { + min uint8 + max uint8 + vals [256]uint64 +} + +func (c *constraint) update(index uint8, p uint32) { + if index < c.min { + c.min = index + } + if index > c.max { + c.max = index + } + c.vals[index] += uint64(p) +} + +func (c *constraint) span() uint8 { + return c.max - c.min +} + +func (cb colorBucket) span() (uint8, colorAxis) { + var R, G, B constraint + R.min = 255 + G.min = 255 + B.min = 255 + var p uint64 + for _, c := range cb { + R.update(c.R, c.p) + G.update(c.G, c.p) + B.update(c.B, c.p) + p += uint64(c.p) + } + var toCount *constraint + var span colorAxis + if R.span() > G.span() && R.span() > B.span() { + span = red + toCount = &R + } else if G.span() > B.span() { + span = green + toCount = &G + } else { + span = blue + toCount = &B + } + var counted uint64 + var i int + var c uint64 + for i, c = range toCount.vals { + if counted > p/2 || counted+c == p { + break + } + counted += c + } + return uint8(i), span +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_mediancut.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_mediancut.go new file mode 100644 index 00000000..850708b9 --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/quantize_mediancut.go @@ -0,0 +1,209 @@ +package libtgsconverter + +import ( + "image" + "image/color" + "sync" +) + +type bucketPool struct { + sync.Pool + maxCap int + m sync.Mutex +} + +func (p *bucketPool) getBucket(c int) colorBucket { + p.m.Lock() + if p.maxCap > c { + p.maxCap = p.maxCap * 99 / 100 + } + if p.maxCap < c { + p.maxCap = c + } + maxCap := p.maxCap + p.m.Unlock() + val := p.Pool.Get() + if val == nil || cap(val.(colorBucket)) < c { + return make(colorBucket, maxCap)[0:c] + } + slice := val.(colorBucket) + slice = slice[0:c] + for i := range slice { + slice[i] = colorPriority{} + } + return slice +} + +var bpool bucketPool + +// aggregationType specifies the type of aggregation to be done +type aggregationType uint8 + +const ( + // Mode - pick the highest priority value + mode aggregationType = iota + // Mean - weighted average all values + mean +) + +// medianCutQuantizer implements the go draw.Quantizer interface using the Median Cut method +type medianCutQuantizer struct { + // The type of aggregation to be used to find final colors + aggregation aggregationType + // The weighting function to use on each pixel + weighting func(image.Image, int, int) uint32 + // Whether need to add a transparent entry after conversion + reserveTransparent bool +} + +//bucketize takes a bucket and performs median cut on it to obtain the target number of grouped buckets +func bucketize(colors colorBucket, num int) (buckets []colorBucket) { + if len(colors) == 0 || num == 0 { + return nil + } + bucket := colors + buckets = make([]colorBucket, 1, num*2) + buckets[0] = bucket + + for len(buckets) < num && len(buckets) < len(colors) { // Limit to palette capacity or number of colors + bucket, buckets = buckets[0], buckets[1:] + if len(bucket) < 2 { + buckets = append(buckets, bucket) + continue + } else if len(bucket) == 2 { + buckets = append(buckets, bucket[:1], bucket[1:]) + continue + } + + left, right := bucket.partition() + buckets = append(buckets, left, right) + } + return +} + +// palettize finds a single color to represent a set of color buckets +func (q* medianCutQuantizer) palettize(p color.Palette, buckets []colorBucket) color.Palette { + for _, bucket := range buckets { + switch q.aggregation { + case mean: + mean := bucket.mean() + p = append(p, mean) + case mode: + var best colorPriority + for _, c := range bucket { + if c.p > best.p { + best = c + } + } + p = append(p, best.RGBA) + } + } + return p +} + +// quantizeSlice expands the provided bucket and then palettizes the result +func (q* medianCutQuantizer) quantizeSlice(p color.Palette, colors []colorPriority) color.Palette { + numColors := cap(p) - len(p) + reserveTransparent := q.reserveTransparent + if reserveTransparent { + numColors-- + } + buckets := bucketize(colors, numColors) + p = q.palettize(p, buckets) + return p +} + +func colorAt(m image.Image, x int, y int) color.RGBA { + switch i := m.(type) { + case *image.YCbCr: + yi := i.YOffset(x, y) + ci := i.COffset(x, y) + c := color.YCbCr{ + i.Y[yi], + i.Cb[ci], + i.Cr[ci], + } + return color.RGBA{c.Y, c.Cb, c.Cr, 255} + case *image.RGBA: + ci := i.PixOffset(x, y) + return color.RGBA{i.Pix[ci+0], i.Pix[ci+1], i.Pix[ci+2], i.Pix[ci+3]} + default: + return color.RGBAModel.Convert(i.At(x, y)).(color.RGBA) + } +} + +// buildBucketMultiple creates a prioritized color slice with all the colors in +// the images. +func (q* medianCutQuantizer) buildBucketMultiple(ms []image.Image) (bucket colorBucket) { + if len(ms) < 1 { + return colorBucket{} + } + + bounds := ms[0].Bounds() + size := (bounds.Max.X - bounds.Min.X) * (bounds.Max.Y - bounds.Min.Y) * 2 + sparseBucket := bpool.getBucket(size) + + for _, m := range ms { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + priority := uint32(1) + if q.weighting != nil { + priority = q.weighting(m, x, y) + } + c := colorAt(m, x, y) + if c.A == 0 { + if !q.reserveTransparent { + q.reserveTransparent = true + } + continue + } + if priority != 0 { + index := int(c.R)<<16 | int(c.G)<<8 | int(c.B) + for i := 1; ; i++ { + p := &sparseBucket[index%size] + if p.p == 0 || p.RGBA == c { + *p = colorPriority{p.p + priority, c} + break + } + index += 1 + i + } + } + } + } + } + + bucket = sparseBucket[:0] + switch ms[0].(type) { + case *image.YCbCr: + for _, p := range sparseBucket { + if p.p != 0 { + r, g, b := color.YCbCrToRGB(p.R, p.G, p.B) + bucket = append(bucket, colorPriority{p.p, color.RGBA{r, g, b, p.A}}) + } + } + default: + for _, p := range sparseBucket { + if p.p != 0 { + bucket = append(bucket, p) + } + } + } + return +} + +// Quantize quantizes an image to a palette and returns the palette +func (q* medianCutQuantizer) quantize(p color.Palette, m image.Image) color.Palette { + // Package quantize offers an implementation of the draw.Quantize interface using an optimized Median Cut method, + // including advanced functionality for fine-grained control of color priority + bucket := q.buildBucketMultiple([]image.Image{m}) + defer bpool.Put(bucket) + return q.quantizeSlice(p, bucket) +} + +// QuantizeMultiple quantizes several images at once to a palette and returns +// the palette +func (q* medianCutQuantizer) quantizeMultiple(p color.Palette, m []image.Image) color.Palette { + bucket := q.buildBucketMultiple(m) + defer bpool.Put(bucket) + return q.quantizeSlice(p, bucket) +} diff --git a/vendor/github.com/Benau/tgsconverter/libtgsconverter/webp.go b/vendor/github.com/Benau/tgsconverter/libtgsconverter/webp.go new file mode 100644 index 00000000..60e9887c --- /dev/null +++ b/vendor/github.com/Benau/tgsconverter/libtgsconverter/webp.go @@ -0,0 +1,39 @@ +package libtgsconverter + +import "bytes" +import "image" + +import "github.com/sizeofint/webpanimation" + +type towebp struct { + timestamp int + webpanim *webpanimation.WebpAnimation + config webpanimation.WebPConfig +} + +func(to_webp *towebp) init(w uint, h uint, options ConverterOptions) { + to_webp.timestamp = 0 + to_webp.webpanim = webpanimation.NewWebpAnimation(int(w), int(h), 0) + to_webp.config = webpanimation.NewWebpConfig() + to_webp.config.SetQuality(options.GetWebpQuality()) +} + +func(to_webp *towebp) SupportsAnimation() bool { + return true +} + +func (to_webp *towebp) AddFrame(image *image.RGBA, fps uint) error { + err := to_webp.webpanim.AddFrame(image, to_webp.timestamp, to_webp.config) + to_webp.timestamp += int((1.0 / float32(fps)) * 1000.) + return err +} + +func (to_webp *towebp) Result() []byte { + var buf bytes.Buffer + err := to_webp.webpanim.Encode(&buf) + if err != nil { + return nil + } + to_webp.webpanim.ReleaseMemory() + return buf.Bytes() +} |