diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb136ea --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pprof diff --git a/README.md b/README.md index 1ab8e68..c26bfb9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ braille unicode. | ![image](https://user-images.githubusercontent.com/94043894/223675190-ecbd20a6-cf49-40a0-a36d-d7bf6b0a75ff.png) | | reverse when your background is too light. | -| ![image](https://user-images.githubusercontent.com/94043894/223677662-d27bc50a-3364-461f-bad4-ba7f0c4b8df9.png) | +| ![image](https://user-images.githubusercontent.com/94043894/236626257-7fb68cf0-89e7-4230-885f-f6f62b95490b.gif) | | :-------------------------------------------------------------------------------------------------------------: | | gif, not much use though. 💩 | @@ -31,16 +31,16 @@ braille unicode. ### 🍰 How2use -`bobibo /path/to/image.png [-option]` +`bobibo [-option] /path/to/image.png ` options: -- `-r` enable reverse the character color. -- `-g` enable gif mode, print every frame of gif image. +- `-v` enable reverse the character color. +- `-g` enable gif mode(test), print every frame of gif image. - `-s value` set the scale for images(value default 0.5, (0, +)). -- `-t value` set the threshold of binarization(value default generate by OTSU, [0, 255]). +- `-t value` set the threshold of binarization(value default generate by OTSU, [-1, 255]). -> use `bobibo help` to print options. +> use `bobibo -h` to print options. > use `bobibo version` to print version. ### ⚙️ Contribute diff --git a/bobibo.go b/bobibo.go index 74bdad8..d576e17 100644 --- a/bobibo.go +++ b/bobibo.go @@ -1,6 +1,7 @@ package bobibo import ( + "errors" "io" "github.com/orzation/bobibo/img" @@ -10,54 +11,102 @@ import ( type Params struct { Image io.Reader Gif bool - Reverse bool + Inverse bool Scale float64 Threshold int } -type Option func(p *Params) +type Option func(p *Params) error -func BoBiBo(ima io.Reader, isGif, ifReverse bool, scale float64, threshold int, opts ...Option) (<-chan []string, error) { +func ScaleOpt(scale float64) Option { + return func(p *Params) error { + if scale <= 0 { + return errors.New("The Value of scale must be within (0, +).") + } + p.Scale = scale + return nil + } +} + +func ThresholdOpt(thre int) Option { + return func(p *Params) error { + if thre < -1 || thre > 255 { + return errors.New("The Value of threshold must be within [-1, 255].") + } + p.Threshold = thre + return nil + } +} + +type Art struct { + Content []string + Delay int +} + +func BoBiBo(ima io.Reader, isGif, isInverse bool, opts ...Option) (<-chan Art, error) { params := &Params{ - Image: ima, - Gif: isGif, - Reverse: ifReverse, - Scale: scale, - Threshold: threshold, + Image: ima, + Gif: isGif, + Inverse: isInverse, } for _, opt := range opts { - opt(params) + if err := opt(params); err != nil { + return nil, err + } } + inStream := make(chan img.Img) - mix := u.Multiply(img.ArtotBin(params.Reverse), + mix := u.Multiply(img.ArtotBin(params.Inverse), u.Multiply(img.BinotImg(params.Threshold), u.Multiply(img.TurnGray, - img.Resize(params.Scale)))) + img.Resize(params.Scale), + ))) outStream := mix(inStream) - err := putStream(inStream, params) + delays, err := putStream(inStream, params) + wrap := wrapOut(delays) if err != nil { return nil, err } - return outStream, nil + return wrap(outStream), nil +} + +var wrapOut = func(delays []int) func(<-chan []string) <-chan Art { + flag := true + if delays == nil || len(delays) == 0 { + flag = false + } + return u.GenChanFunc(func(out <-chan []string, wrapOut chan<- Art) { + cnt := 0 + for o := range out { + if flag { + wrapOut <- Art{Content: o, Delay: delays[cnt]} + } else { + wrapOut <- Art{Content: o, Delay: 0} + } + cnt++ + } + }) } -func putStream(in chan<- img.Img, params *Params) error { +func putStream(in chan<- img.Img, params *Params) ([]int, error) { + var delays []int if params.Gif { - p, err := img.LoadAGif(params.Image) + p, dls, err := img.LoadAGif(params.Image) if err != nil { - return err + return nil, err } + delays = dls go inStream(in, p...) } else { i, err := img.LoadAImage(params.Image) if err != nil { - return err + return nil, err } go inStream(in, i) } - return nil + return delays, nil } func inStream[T img.Img](in chan<- img.Img, ims ...T) { diff --git a/bobibo_test.go b/bobibo_test.go index 9af5f01..fd1b09b 100644 --- a/bobibo_test.go +++ b/bobibo_test.go @@ -7,18 +7,45 @@ import ( ) func TestBobibo(t *testing.T) { - f, err := os.Open("./test.jpg") + f, err := os.Open("./test.gif") + defer f.Close() if err != nil { t.Error(err) } - c, err2 := BoBiBo(f, false, false, 1.0, -1) + c, err2 := BoBiBo(f, false, false, ScaleOpt(0.25), ThresholdOpt(-1)) if err2 != nil { panic(err2) } for e := range c { - for _, v := range e { + for _, v := range e.Content { fmt.Println(v) } } +} +func BenchmarkBobibo(b *testing.B) { + f, err := os.Open("./test.gif") + if err != nil { + b.Error(err) + } + defer f.Close() + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + f.Seek(0, 0) + b.StartTimer() + arts, err := BoBiBo(f, true, false, ScaleOpt(1), ThresholdOpt(-1)) + if err != nil { + b.Error(err) + } + for { + select { + case _, ok := <-arts: + if !ok { + goto loopOut + } + } + } + loopOut: + } } diff --git a/cli/cli.go b/cli/cli.go index e9511a8..302ca5e 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,88 +1,68 @@ package main import ( + "flag" "fmt" "os" - "strconv" "github.com/orzation/bobibo" ) var ( version string + + gif bool + inverse bool + scale float64 + threshold int ) +func init() { + flag.BoolVar(&gif, "g", false, "enable gif mode.") + flag.BoolVar(&inverse, "v", false, "inverse the colors.") + flag.Float64Var(&scale, "s", 0.5, "scale the size of arts. range: (0, +).") + flag.IntVar(&threshold, "t", -1, "set the threshold of binarization. range: [-1, 255], -1 means gen by OTSU.") +} + func main() { - if len(os.Args) <= 1 { - fmt.Println("Please input a path of image.") - fmt.Println("Or using help to print options.") - fmt.Println("Or using version to print version.") - fmt.Println(":P") - os.Exit(1) - } - path := os.Args[1] - if path == "help" { - fmt.Println("Options:") - fmt.Println(" -r reverse the char color.") - fmt.Println(" -g enable gif analyzation, default: disable.") - fmt.Println(" -s [d](0, +) set the scale of art. [default: 0.5]") - fmt.Println(" -t [d][0, 255] set the threshold of binarization. [default: gen by ostu]") - os.Exit(0) - } else if path == "version" { - fmt.Printf("BoBiBo %s :P\n", version) - os.Exit(0) - } - f, err := os.Open(path) - defer f.Close() + flag.Parse() + args := flag.Args() - if err != nil { - fmt.Println(err.Error()) + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "Usage: bobibo [OPTION]... PARTERNS [FILE]...") + fmt.Fprintln(os.Stderr, "Try 'bobibo --help' for more information.") os.Exit(1) } - var gif, rever bool - var scale float64 = 0.5 - var threshold = -1 - for i, v := range os.Args[2:] { - switch v { - case "-g": - gif = true - case "-r": - rever = true - case "-s": - f, err := strconv.ParseFloat(os.Args[i+3], 64) - if err != nil { - fmt.Println("The range of scale must at (0, +).") - os.Exit(1) - } - if f == 0 { - fmt.Println("The range of scale must at (0, +).") - os.Exit(1) - } - scale = f - case "-t": - i, err := strconv.ParseInt(os.Args[i+3], 10, 64) - if err != nil { - fmt.Println("The range of threshold must at [0, 255].") - os.Exit(1) - } - if i < 0 || i > 255 { - fmt.Println("The range of threshold must at [0, 255].") - os.Exit(1) - } - threshold = int(i) + opt := args[0] + var imgFile *os.File + + switch opt { + case "version": + fmt.Printf("BoBiBo %s :P\n", version) + return + default: + f, err := os.Open(opt) + if err != nil { + fmt.Fprintln(os.Stderr, "Open image error: ", err.Error()) } + imgFile = f } - - out, err := bobibo.BoBiBo(f, gif, rever, scale, threshold) + defer imgFile.Close() + arts, err := bobibo.BoBiBo( + imgFile, gif, inverse, + bobibo.ScaleOpt(scale), + bobibo.ThresholdOpt(threshold)) if err != nil { - fmt.Println(err.Error()) + fmt.Fprintln(os.Stderr, "Bobibo error: ", err.Error()) + imgFile.Close() os.Exit(1) } - for e := range out { - for _, v := range e { - fmt.Printf("\r%s\n", v) - } + err = printArts(arts, gif) + if err != nil { + fmt.Fprintln(os.Stderr, "Print error: ", err.Error()) + imgFile.Close() + os.Exit(1) } } diff --git a/cli/makefile b/cli/makefile index f15fd4a..ca39686 100644 --- a/cli/makefile +++ b/cli/makefile @@ -1,4 +1,4 @@ -VERSION=V1.1.0 +VERSION=V1.2.0 EXE=bobibo DESTDIR := @@ -8,7 +8,6 @@ default: build .PHONY: build build: cli.go go build -ldflags="-X 'main.version=$(VERSION)' -s -w" -o $(EXE) - upx $(EXE) # upx not found ? install it or remove this line. @echo Build Success !!! .PHONY: install diff --git a/cli/makefile_cross b/cli/makefile_cross index 08617a3..f0056d6 100644 --- a/cli/makefile_cross +++ b/cli/makefile_cross @@ -1,8 +1,8 @@ -OS = linux +OS = windows ARCH = amd64 EXE = bobibo_$(OS)_$(ARCH) IS_STATIC = 0 -VERSION=V1.1.0 +VERSION=V1.2.0 all: build build: cli.go diff --git a/cli/print.go b/cli/print.go new file mode 100644 index 0000000..ef210a5 --- /dev/null +++ b/cli/print.go @@ -0,0 +1,89 @@ +package main + +import ( + "errors" + "fmt" + "os" + "os/signal" + "time" + + b "github.com/orzation/bobibo" + "golang.org/x/term" +) + +func printArts(arts <-chan b.Art, gifMode bool) error { + if !gifMode { + for art := range arts { + for _, v := range art.Content { + fmt.Printf("\r%s\n", v) + } + } + return nil + } + + artsBuffer := make([]b.Art, len(arts)) + for a := range arts { + artsBuffer = append(artsBuffer, a) + } + + fd := int(os.Stdout.Fd()) + errChan := make(chan error, 2) + interrupt := make(chan os.Signal) + signal.Notify(interrupt, os.Interrupt) + + hideCursor() + defer showCursor() + + go func() { + for { + tw, th, err := term.GetSize(fd) + if err != nil { + errChan <- err + return + } + + clearAll() + for _, art := range artsBuffer { + sw, sh := len([]rune(art.Content[0])), len(art.Content) + posX, posY := (tw-sw)>>1, (th-sh)>>1 + if posX < 0 || posY < 0 || posX > tw || posY > th { + errChan <- errors.New("Image size is too large, please zoom out and try again.") + return + } + for offset, line := range art.Content { + moveCursor(posY+offset, posX) + clearLine() + fmt.Printf("%s", line) + } + time.Sleep(time.Microsecond * time.Duration(art.Delay*10000)) + } + } + }() + + select { + case <-interrupt: + return nil + case err := <-errChan: + return err + } +} + +func moveCursor(y, x int) { + fmt.Printf("\033[%d;%dH", y, x) +} + +func clearAll() { + fmt.Print("\033[2J") +} + +func clearLine() { + fmt.Print("\033[2K") +} + +func hideCursor() { + fmt.Print("\033[?25l") +} + +func showCursor() { + fmt.Print("\033[?25h") +} diff --git a/go.mod b/go.mod index c711c73..849ef6a 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/orzation/bobibo go 1.20 -require golang.org/x/image v0.6.0 +require ( + golang.org/x/image v0.6.0 + golang.org/x/term v0.5.0 +) + +require golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index 64dc9f5..33e5c36 100644 --- a/go.sum +++ b/go.sum @@ -17,9 +17,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/img/img.go b/img/img.go index 8b3d44f..c1d3de9 100644 --- a/img/img.go +++ b/img/img.go @@ -17,7 +17,7 @@ import ( type Img = image.Image type Pale = *image.Paletted -// we use braille char to draw pic. +// use braille chars to draw arts. var brailleMap = []rune("⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤" + "⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶" + "⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾" + @@ -36,12 +36,12 @@ func LoadAImage(f io.Reader) (Img, error) { } // loading a gif, return arrays of image. -func LoadAGif(f io.Reader) ([]Pale, error) { +func LoadAGif(f io.Reader) ([]Pale, []int, error) { g, err := gif.DecodeAll(f) if err != nil { - return nil, err + return nil, nil, err } - return g.Image, nil + return g.Image, g.Delay, nil } // resizing the image with scale value, it won't change the ratio. @@ -69,7 +69,7 @@ var TurnGray = u.GenChanFunc(func(in <-chan Img, out chan<- Img) { }) // turning image to 2d binary matrix. -// your threshold to adjust the binarization. +// use threshold to adjust the binarization. var BinotImg = func(threshold int) func(<-chan Img) <-chan [][]bool { return u.GenChanFunc(func(in <-chan Img, out chan<- [][]bool) { for im := range in { @@ -112,13 +112,13 @@ func otsu(im Img) uint8 { grayPro[i] *= 1.0 / float32(pixelSum) u += float32(i) * grayPro[i] } - var wk, uk, gmax float32 + var w1, u1, gmax float32 for i := 0; i < grayScale; i++ { - wk += grayPro[i] - uk += float32(i) * grayPro[i] + w1 += grayPro[i] + u1 += float32(i) * grayPro[i] - tmp := uk - u*wk - sigma := tmp * tmp / (wk * (1 - wk)) + tmp := u1 - u*w1 + sigma := tmp * tmp / (w1 * (1 - w1)) if sigma >= gmax { threshold = uint8(i) gmax = sigma diff --git a/test.gif b/test.gif new file mode 100644 index 0000000..1697341 Binary files /dev/null and b/test.gif differ