-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# gif2cast - Convert animated gif into asciicast session | ||
|
||
## What it does | ||
Basically does the opposite of [asciicast2gif](https://github.com/asciinema/asciicast2gif) - turning an animated gif into asciicast file, which also uploading to the [asciinema](http://asciinema.org) site. | ||
|
||
## Preview | ||
[![asciicast](https://asciinema.org/a/191865.png)](https://asciinema.org/a/191865) | ||
|
||
[Original file](https://en.wikipedia.org/wiki/GIF#/media/File:Rotating_earth_(large).gif) | ||
|
||
## Usage | ||
### Bundled tool | ||
``` | ||
> AC_APIKEY=abcdef-123445 gif2cast animated.gif | ||
or | ||
> gif2cast -o out.cast animated.gif | ||
``` | ||
### Library | ||
```go | ||
fp, err := os.Open(fileName) | ||
... | ||
gifImage, err := gif.DecodeAll(fp) | ||
... | ||
width := 80 | ||
height := 24 | ||
gc := gif2cast.NewGif2Cast(gifImage, 80, 24, "title") | ||
out, err := os.OpenFile(outFilename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) | ||
gc.Write(out) | ||
//or | ||
url, err := gc.Upload(APIKEY) | ||
if err != nil { | ||
fmt.Println("Cannot upload:", err) | ||
os.Exit(1) | ||
} | ||
fmt.Println(url) | ||
``` | ||
|
||
## Note | ||
|
||
This library is some hobby project I wrote over the weekend so it has a lot of rough edges, e.g. | ||
- No support on partial frame refresh, it takes every frame in the gif as a full refresh. | ||
- Does not support disposal methods | ||
- Does not support loop | ||
- Bundled tool does not find API automatically | ||
- Rudimentary optimization to reduce asciicast file size |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package asciicast | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"image" | ||
"io" | ||
"os" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type asciiCastHeader struct { | ||
Version int `json:"version"` | ||
Width int `json:"width"` | ||
Height int `json:"height"` | ||
Timestamp int64 `json:"timestamp"` | ||
Command string `json:"command"` | ||
Title string `json:"title"` | ||
Env map[string]string `json:"env"` | ||
} | ||
|
||
type VirtualScreen struct { | ||
header asciiCastHeader | ||
accumulatedDelay float32 | ||
textBuffer strings.Builder | ||
} | ||
|
||
const ( | ||
LogTimeFormat string = "20060102-150405" | ||
) | ||
|
||
// NewVirtualScreen creates new virtual screen struct, provide | ||
// it with console size and title that would in the asciicast | ||
// file header | ||
func NewVirtualScreen(w, h int, title string) *VirtualScreen { | ||
header := asciiCastHeader{ | ||
Version: 2, | ||
Width: w, | ||
Height: h, | ||
Timestamp: time.Now().Unix(), | ||
Command: "gif2cast", | ||
Title: title, | ||
Env: map[string]string{"TERM": "gif2cast", "ENV": "gif2cast"}, | ||
} | ||
return &VirtualScreen{ | ||
header: header, | ||
accumulatedDelay: 0, | ||
textBuffer: strings.Builder{}, | ||
} | ||
} | ||
|
||
func ansiString(frame *image.RGBA) string { | ||
strBuf := strings.Builder{} | ||
fmt.Fprintf(&strBuf, "\033[1;1H") | ||
for y := 0; y < frame.Rect.Dy(); y += 2 { | ||
for x := 0; x < frame.Rect.Dx(); x++ { | ||
i := frame.PixOffset(x, y) | ||
//48 is background, upper part of cell | ||
if i < 3 || i > 3 && (frame.Pix[i] != frame.Pix[i-3] || | ||
frame.Pix[i+1] != frame.Pix[i-2] || | ||
frame.Pix[i+2] != frame.Pix[i-1]) { | ||
fmt.Fprintf(&strBuf, "\033[48;2;%d;%d;%dm", frame.Pix[i], frame.Pix[i+1], frame.Pix[i+2]) | ||
} | ||
lPix := i + frame.Stride | ||
//38 is foreground, lower part | ||
if i < 3 || i > 3 && (frame.Pix[lPix] != frame.Pix[lPix-3] || | ||
frame.Pix[lPix+1] != frame.Pix[lPix-2] || | ||
frame.Pix[lPix+2] != frame.Pix[lPix-1]) { | ||
fmt.Fprintf(&strBuf, "\033[38;2;%d;%d;%dm", | ||
frame.Pix[i+frame.Stride], frame.Pix[i+frame.Stride+1], frame.Pix[i+frame.Stride+2]) | ||
} | ||
//u2584 is the fill lowerblock char | ||
fmt.Fprint(&strBuf, "\u2584") | ||
} | ||
fmt.Fprint(&strBuf, "\r\n") | ||
} | ||
return strBuf.String() | ||
} | ||
|
||
// WriteFrame write a image.RGBA into the string builder in asciicast format | ||
func (vs *VirtualScreen) WriteFrame(frame *image.RGBA, delay int) { | ||
escStr, _ := json.Marshal(ansiString(frame)) | ||
fmt.Fprintf(&vs.textBuffer, "[%f, \"o\", %s]\r\n", vs.accumulatedDelay, escStr) | ||
vs.accumulatedDelay += float32(delay) / 100 | ||
} | ||
|
||
// Write writes the asciicast file in buffer to the file | ||
func (vs *VirtualScreen) WriteToFile(filename string) error { | ||
fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) | ||
if err != nil { | ||
return err | ||
} | ||
defer fp.Close() | ||
return vs.Write(fp) | ||
} | ||
|
||
// Write writes the asciicast file in buffer to the writer | ||
func (vs *VirtualScreen) Write(w io.Writer) error { | ||
bheader, err := json.Marshal(vs.header) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = w.Write(bheader) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = w.Write([]byte("\r\n")) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = w.Write([]byte(vs.textBuffer.String())) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"image/gif" | ||
"os" | ||
|
||
"github.com/mkishere/gif2cast" | ||
) | ||
|
||
func main() { | ||
outFilename := "" | ||
flag.StringVar(&outFilename, "o", "", "Output file name") | ||
flag.Parse() | ||
|
||
if flag.Arg(0) == "" { | ||
fmt.Println("Input file expected") | ||
os.Exit(1) | ||
} | ||
inFilename := flag.Arg(0) | ||
fp, err := os.Open(inFilename) | ||
if err != nil { | ||
fmt.Println("Cannot open file:", err) | ||
os.Exit(1) | ||
} | ||
gifImage, err := gif.DecodeAll(fp) | ||
if err != nil { | ||
fmt.Println("Cannot open file:", err) | ||
os.Exit(1) | ||
} | ||
gc := gif2cast.NewGif2Cast(gifImage, 80, 24, flag.Arg(0)) | ||
if outFilename != "" { | ||
out, err := os.OpenFile(outFilename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666) | ||
if err != nil { | ||
fmt.Println("Cannot create output file:", err) | ||
os.Exit(1) | ||
} | ||
defer out.Close() | ||
gc.Write(out) | ||
} else { | ||
//TODO: Get APIkey | ||
str, err := gc.Upload(os.Getenv("AC_APIKEY")) | ||
if err != nil { | ||
fmt.Println("Cannot upload:", err) | ||
os.Exit(1) | ||
} | ||
fmt.Println(str) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package gif2cast | ||
|
||
import ( | ||
"bytes" | ||
"image" | ||
"image/gif" | ||
"io" | ||
"mime/multipart" | ||
"net/http" | ||
|
||
"github.com/mkishere/gif2cast/asciicast" | ||
"github.com/mkishere/gif2cast/imgutil" | ||
) | ||
|
||
const ( | ||
endpoint = "https://asciinema.org/api/asciicasts" | ||
) | ||
|
||
// Gif2Cast helps with image conversion(resize) and controlling the virtual screen | ||
type Gif2Cast struct { | ||
oriImg *gif.GIF | ||
cw, ch int | ||
rgbImg []*image.RGBA | ||
title string | ||
} | ||
|
||
// NewGif2Cast creates a new Gif2Cast object | ||
func NewGif2Cast(gif *gif.GIF, w, h int, title string) *Gif2Cast { | ||
return &Gif2Cast{ | ||
oriImg: gif, | ||
cw: w, | ||
ch: h, | ||
title: title, | ||
} | ||
} | ||
|
||
// Write writes file content to writer | ||
func (gc *Gif2Cast) Write(w io.Writer) (err error) { | ||
gc.rgbImg, err = imgutil.Resize(gc.oriImg, gc.cw, gc.ch*2) | ||
if err != nil { | ||
return err | ||
} | ||
vs := asciicast.NewVirtualScreen(gc.cw, gc.ch, gc.title) | ||
for i := range gc.rgbImg { | ||
vs.WriteFrame(gc.rgbImg[i], gc.oriImg.Delay[i]) | ||
} | ||
|
||
return vs.Write(w) | ||
} | ||
|
||
// Upload the written file to asciinema server | ||
func (gc *Gif2Cast) Upload(apiKey string) (string, error) { | ||
buf := &bytes.Buffer{} | ||
writer := multipart.NewWriter(buf) | ||
filePart, _ := writer.CreateFormFile("asciicast", "ascii.cast") | ||
_ = gc.Write(filePart) | ||
writer.Close() | ||
req, _ := http.NewRequest("POST", endpoint, buf) | ||
req.SetBasicAuth("gif2cast", apiKey) | ||
req.Header.Set("Content-Type", writer.FormDataContentType()) | ||
req.Header.Add("User-Agent", "gif2cast/1.0.0") | ||
client := &http.Client{} | ||
rsp, err := client.Do(req) | ||
if err != nil { | ||
return "", err | ||
} | ||
body := &bytes.Buffer{} | ||
_, err = body.ReadFrom(rsp.Body) | ||
rsp.Body.Close() | ||
return string(body.Bytes()), err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package imgutil | ||
|
||
import ( | ||
"image" | ||
"image/color" | ||
"image/gif" | ||
|
||
"github.com/bamiaux/rez" | ||
) | ||
|
||
// ConvertPalettedImgToRGBA converts a paletted image format (e.g. GIF) | ||
// into RGBA | ||
func ConvertPalettedImgToRGBA(src *image.Paletted) (dst *image.RGBA) { | ||
dst = &image.RGBA{ | ||
Pix: make([]uint8, 0, len(src.Pix)*4), | ||
Rect: src.Rect, | ||
Stride: src.Stride * 4, | ||
} | ||
|
||
for i := range src.Pix { | ||
c := src.Palette[src.Pix[i]].(color.RGBA) | ||
dst.Pix = append(dst.Pix, c.R, c.G, c.B, c.A) | ||
} | ||
return | ||
} | ||
|
||
// Resize takes a GIF image and resize it with given width and height | ||
// Output is RGBA format in a stack. | ||
func Resize(gifImg *gif.GIF, w, h int) (dst []*image.RGBA, err error) { | ||
dst = make([]*image.RGBA, 0, len(gifImg.Image)) | ||
|
||
converter, err := rez.NewConverter(&rez.ConverterConfig{ | ||
Input: rez.Descriptor{ | ||
Width: gifImg.Image[0].Rect.Dx(), | ||
Height: gifImg.Image[0].Rect.Dy(), | ||
Interlaced: false, | ||
Ratio: rez.Ratio444, | ||
Pack: 4, | ||
Planes: 1, | ||
}, | ||
Output: rez.Descriptor{ | ||
Width: w, | ||
Height: h, | ||
Interlaced: false, | ||
Ratio: rez.Ratio444, | ||
Pack: 4, | ||
Planes: 1, | ||
}, | ||
}, rez.NewBilinearFilter()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for i := range gifImg.Image { | ||
dstFrame := image.NewRGBA(image.Rect(0, 0, w, h)) | ||
err = converter.Convert(dstFrame, ConvertPalettedImgToRGBA(gifImg.Image[i])) | ||
if err != nil { | ||
return nil, err | ||
} | ||
dst = append(dst, dstFrame) | ||
} | ||
return dst, nil | ||
} |