convert2ascii 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +114 -0
- data/Rakefile +19 -0
- data/TODO.md +8 -0
- data/exe/image2ascii +48 -0
- data/exe/video2ascii +127 -0
- data/lib/convert2ascii/check_package.rb +167 -0
- data/lib/convert2ascii/image2ascii.rb +132 -0
- data/lib/convert2ascii/multi-tasker.rb +36 -0
- data/lib/convert2ascii/terminal-player.rb +184 -0
- data/lib/convert2ascii/terminal.rb +40 -0
- data/lib/convert2ascii/version.rb +3 -0
- data/lib/convert2ascii/video2ascii.rb +199 -0
- data/lib/convert2ascii.rb +3 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 37a56a08059d638a7c1e406248484e8fb3ca0fd1d494c8676ac5cc1119e15eeb
|
4
|
+
data.tar.gz: 3cbf2abb2d13a8a1d1091128852584d35d073ac8b1d7a056dc856916d913582d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e1dcecc188abc4c1a5916199f91fba45fa1ee70d6fe25427a27b502e5c41fa57fe3c3dedf4cd0a7f3402ac36f132e44f87874ada185d0dc374d78eb29fd9e389
|
7
|
+
data.tar.gz: aedcc98597e9ef05af5c26435384cb72741b2e97cca92df0e1e2b44e9314f0f2a465f5a070503db29a5bc16bb0cc97933ab4ab7a9b9d21f44179f9df923bc6f9
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Mark24
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Convert2ASCII
|
2
|
+
|
3
|
+
Convert Image/Video to ASCII art.
|
4
|
+
|
5
|
+
|
6
|
+
## Test pass
|
7
|
+
|
8
|
+
* MacOS 15.2 ✅
|
9
|
+
* Ubuntu 24.04 ✅
|
10
|
+
* Windows 11 ❌
|
11
|
+
|
12
|
+
|
13
|
+
## Example
|
14
|
+
|
15
|
+

|
16
|
+
|
17
|
+

|
18
|
+
|
19
|
+

|
20
|
+
|
21
|
+
## Prerequisites
|
22
|
+
|
23
|
+
* Ruby3+
|
24
|
+
* ImageMagick ([Download here](https://imagemagick.org/script/download.php))
|
25
|
+
* ffmpeg ([Download here](https://www.ffmpeg.org/))
|
26
|
+
|
27
|
+
|
28
|
+
## Executable command
|
29
|
+
|
30
|
+
### image2ascii
|
31
|
+
|
32
|
+
Make image to ascii art in your terminal.
|
33
|
+
|
34
|
+
```bash
|
35
|
+
image2ascii -h
|
36
|
+
Usage: image2ascii [options]
|
37
|
+
--version verison
|
38
|
+
-i, --image=URI image uri (required)
|
39
|
+
-w, --width=WIDTH image width (integer)
|
40
|
+
-s, --style=STYLE ascii style: ['color'| 'text']
|
41
|
+
-b, --block ascii color style use BLOCK or not [ true | false ]
|
42
|
+
```
|
43
|
+
|
44
|
+
### video2ascii
|
45
|
+
|
46
|
+
Make image to ascii art in your terminal.
|
47
|
+
|
48
|
+
```bash
|
49
|
+
Usage: video2ascii [options]
|
50
|
+
|
51
|
+
* default will generate and play without save.
|
52
|
+
* -p will just play ascii frames dir, and ignore -i, -o others options. --loop will play loop
|
53
|
+
* -i,-o will just generate and output frames and ignore others options
|
54
|
+
--version verison
|
55
|
+
-i, --input=URI video uri (required)
|
56
|
+
-w, --width=WIDTH video width (integer)
|
57
|
+
-s, --style=STYLE ascii style: ['color'| 'text']
|
58
|
+
-b, --block ascii color style use BLOCK or not [ true | false ]
|
59
|
+
-o, --ouput=OUTPUT save ascii frame to output dirname
|
60
|
+
-p, --play_dir=PLAY_DIRNAME input ascii frames dirname to play
|
61
|
+
--loop
|
62
|
+
```
|
63
|
+
|
64
|
+
|
65
|
+
## As a Gem
|
66
|
+
|
67
|
+
### Convert2Ascii::Image2Ascii
|
68
|
+
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
require 'convert2ascii/image2ascii'
|
72
|
+
|
73
|
+
# generate image
|
74
|
+
uri = "path/to/image"
|
75
|
+
ascii = Convert2Ascii::Image2Ascii.new(uri:, width: 50)
|
76
|
+
|
77
|
+
# generate image
|
78
|
+
ascii.generate
|
79
|
+
# display in your terminal
|
80
|
+
ascii.tty_print
|
81
|
+
|
82
|
+
|
83
|
+
# also chain call
|
84
|
+
ascii.generate.tty_print
|
85
|
+
|
86
|
+
```
|
87
|
+
|
88
|
+
|
89
|
+
### Convert2Ascii::Video2Ascii
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
require 'convert2ascii/video2ascii'
|
93
|
+
|
94
|
+
# generate video
|
95
|
+
uri = "path/to/video.mp4"
|
96
|
+
ascii = Convert2Ascii::Video2Ascii.new(uri:, width: 50)
|
97
|
+
# generate video
|
98
|
+
ascii.generate
|
99
|
+
# save frames
|
100
|
+
ascii.save(output_path)
|
101
|
+
|
102
|
+
# play in terminal
|
103
|
+
ascii.play
|
104
|
+
|
105
|
+
|
106
|
+
# chain call
|
107
|
+
ascii.generate.play
|
108
|
+
|
109
|
+
```
|
110
|
+
|
111
|
+
## Inspired by
|
112
|
+
|
113
|
+
* [michaelkofron/image2ascii](https://github.com/michaelkofron/image2ascii)
|
114
|
+
* [andrewcohen/video_to_ascii](https://github.com/andrewcohen/video_to_ascii)
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
desc "Run Test"
|
5
|
+
task :test do
|
6
|
+
tests = Dir.glob("./test/test_*.rb")
|
7
|
+
tests.each do |t|
|
8
|
+
system("ruby #{t}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Build Rdoc"
|
13
|
+
task :build_rdoc do
|
14
|
+
system("rdoc build")
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
task default: %i[]
|
data/TODO.md
ADDED
data/exe/image2ascii
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require_relative "../lib/convert2ascii/image2ascii"
|
4
|
+
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
OptionParser.new do |parser|
|
8
|
+
parser.banner = "Usage: image2ascii [options]"
|
9
|
+
|
10
|
+
parser.on("--version", "verison") do |v|
|
11
|
+
puts "convert2ascii/image2ascii: v#{::Convert2Ascii::VERSION}"
|
12
|
+
puts "author: Mark24"
|
13
|
+
puts "mail: mark.zhangyoung@gmail.com"
|
14
|
+
puts "project: https://github.com/mark24Code/convert2ascii"
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
parser.on("-iURI", "--image=URI", "image uri (required)") do |uri|
|
19
|
+
options[:uri] = uri
|
20
|
+
|
21
|
+
# check options
|
22
|
+
unless options[:uri]
|
23
|
+
puts "Error: --image option is required."
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
parser.on("-wWIDTH", "--width=WIDTH", Integer ,"image width (integer)") do |width|
|
29
|
+
options[:width] = width
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.on("-sSTYLE", "--style=STYLE", "ascii style: ['color'| 'text']") do |style|
|
33
|
+
options[:style] = style
|
34
|
+
|
35
|
+
styles = ["color", "text"]
|
36
|
+
# check options
|
37
|
+
unless styles.include? options[:style]
|
38
|
+
puts "Error: --style option must be [\"color\" | \"text\"]."
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.on("-b", "--block", "ascii color style use BLOCK or not [ true | false ] ") do |color_block|
|
44
|
+
options[:color_block] = color_block || false
|
45
|
+
end
|
46
|
+
end.parse!
|
47
|
+
|
48
|
+
Convert2Ascii::Image2Ascii.new(**options).generate.tty_print
|
data/exe/video2ascii
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require_relative "../lib/convert2ascii/video2ascii"
|
4
|
+
require_relative "../lib/convert2ascii/terminal-player"
|
5
|
+
|
6
|
+
|
7
|
+
def get_name_order(name_path)
|
8
|
+
if name_path
|
9
|
+
return File.basename(name_path, ".*").to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def play_frames(dist_dir, play_loop: false)
|
14
|
+
|
15
|
+
config = JSON.parse(File.read(File.join(dist_dir, "meta.json")))
|
16
|
+
step_duration = config["step_duration"]
|
17
|
+
audio_name = config["audio"]
|
18
|
+
audio = audio_name ? File.join(dist_dir, audio_name) : nil
|
19
|
+
|
20
|
+
frames_path = Dir.glob("#{dist_dir}/*.txt")
|
21
|
+
frames_path = frames_path.sort do |a, b|
|
22
|
+
get_name_order(a) <=> get_name_order(b)
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO
|
26
|
+
# 载入内存中可能会遇到过大的问题
|
27
|
+
frames = frames_path.map { |f| File.open(f).read }
|
28
|
+
|
29
|
+
payload = {
|
30
|
+
frames: frames,
|
31
|
+
audio: audio,
|
32
|
+
play_loop: play_loop,
|
33
|
+
step_duration: step_duration
|
34
|
+
}
|
35
|
+
Convert2Ascii::TerminalPlayer.new(**payload).play
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
options = {}
|
40
|
+
OptionParser.new do |parser|
|
41
|
+
parser.banner = <<DOC
|
42
|
+
Usage: video2ascii [options]
|
43
|
+
|
44
|
+
* default will generate and play without save.
|
45
|
+
* -p will just play ascii frames dir, and ignore -i, -o others options. --loop will play loop
|
46
|
+
* -i,-o will just generate and output frames and ignore others options
|
47
|
+
DOC
|
48
|
+
parser.on("--version", "verison") do |v|
|
49
|
+
puts "convert2ascii/video2ascii: v#{::Convert2Ascii::VERSION}"
|
50
|
+
puts "author: Mark24"
|
51
|
+
puts "mail: mark.zhangyoung@gmail.com"
|
52
|
+
puts "project: https://github.com/mark24Code/convert2ascii"
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
parser.on("-iURI", "--input=URI", "video uri (required)") do |uri|
|
57
|
+
options[:uri] = uri
|
58
|
+
|
59
|
+
# check options
|
60
|
+
unless options[:uri]
|
61
|
+
puts "Error: --video option is required."
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
parser.on("-wWIDTH", "--width=WIDTH", Integer ,"video width (integer)") do |width|
|
67
|
+
options[:width] = width
|
68
|
+
end
|
69
|
+
|
70
|
+
parser.on("-sSTYLE", "--style=STYLE", "ascii style: ['color'| 'text']") do |style|
|
71
|
+
options[:style] = style
|
72
|
+
|
73
|
+
styles = ["color", "text"]
|
74
|
+
# check options
|
75
|
+
unless styles.include? options[:style]
|
76
|
+
puts "Error: --style option must be [\"color\" | \"text\"]."
|
77
|
+
exit 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
parser.on("-b", "--block", "ascii color style use BLOCK or not [ true | false ] ") do |color_block|
|
82
|
+
options[:color_block] = color_block || false
|
83
|
+
end
|
84
|
+
|
85
|
+
parser.on("-o", "--ouput=OUTPUT", "save ascii frame to output dirname") do |output|
|
86
|
+
options[:output] = output
|
87
|
+
end
|
88
|
+
|
89
|
+
parser.on("-p", "--play_dir=PLAY_DIRNAME", "input ascii frames dirname to play") do |play_dir|
|
90
|
+
options[:play_dir] = play_dir
|
91
|
+
end
|
92
|
+
|
93
|
+
parser.on("--loop", "play loop") do |play_loop|
|
94
|
+
options[:play_loop] = play_loop
|
95
|
+
end
|
96
|
+
end.parse!
|
97
|
+
|
98
|
+
if options[:play_dir]
|
99
|
+
# play frames
|
100
|
+
# TODO
|
101
|
+
# 导出文件必须包含更多信息 step_duration
|
102
|
+
play_loop = false
|
103
|
+
|
104
|
+
if options[:play_loop]
|
105
|
+
play_loop = options[:play_loop]
|
106
|
+
end
|
107
|
+
play_frames(options[:play_dir], play_loop: play_loop)
|
108
|
+
exit 0
|
109
|
+
end
|
110
|
+
|
111
|
+
payload = {
|
112
|
+
uri: options[:uri],
|
113
|
+
width: options[:width],
|
114
|
+
style: options[:style],
|
115
|
+
color_block: options[:color_block],
|
116
|
+
}
|
117
|
+
|
118
|
+
if options[:uri] and options[:output]
|
119
|
+
output = options[:output]
|
120
|
+
|
121
|
+
Convert2Ascii::Video2Ascii.new(**payload).generate.save(output)
|
122
|
+
|
123
|
+
exit 0
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
Convert2Ascii::Video2Ascii.new(**payload).generate.play
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
|
3
|
+
module Convert2Ascii
|
4
|
+
module OS
|
5
|
+
|
6
|
+
module OS_ENUM
|
7
|
+
MacOS = :macos
|
8
|
+
Linux = :linux
|
9
|
+
Windows = :ms
|
10
|
+
Unknow = :unknow
|
11
|
+
end
|
12
|
+
|
13
|
+
def detect_os
|
14
|
+
if RUBY_PLATFORM =~ /linux/
|
15
|
+
return OS_ENUM::Linux
|
16
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
17
|
+
return OS_ENUM::MacOS
|
18
|
+
elsif RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
19
|
+
return OS_ENUM::Windows
|
20
|
+
else
|
21
|
+
return OS_ENUM::Unknow
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class CheckPackageError < StandardError
|
27
|
+
end
|
28
|
+
|
29
|
+
class CheckPackage
|
30
|
+
include OS
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@os_name = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def macos_check
|
37
|
+
end
|
38
|
+
|
39
|
+
def ms_check
|
40
|
+
end
|
41
|
+
|
42
|
+
def linux_check
|
43
|
+
end
|
44
|
+
|
45
|
+
def unknow_check
|
46
|
+
raise CheckPackageError, Rainbow("[Error] #{@os_name} not support!").red
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def macos_installed?(package_name)
|
51
|
+
output = `brew list #{package_name} 2>/dev/null`
|
52
|
+
return output != ""
|
53
|
+
end
|
54
|
+
|
55
|
+
def detect_linux_distribution
|
56
|
+
|
57
|
+
if !File.exist?('/etc/os-release')
|
58
|
+
raise CheckPackageError, Rainbow("[Error] can not detect_os! #{@os_name} ").red
|
59
|
+
end
|
60
|
+
|
61
|
+
os_release = File.read('/etc/os-release')
|
62
|
+
|
63
|
+
if os_release.include?('debian')
|
64
|
+
# debian/ubuntu
|
65
|
+
return'debian'
|
66
|
+
end
|
67
|
+
|
68
|
+
if os_release.include?('centos')
|
69
|
+
# centos
|
70
|
+
return'centos'
|
71
|
+
end
|
72
|
+
|
73
|
+
if os_release.include?('redhat')
|
74
|
+
# redhat
|
75
|
+
return'redhat'
|
76
|
+
end
|
77
|
+
|
78
|
+
if os_release.include?('arch')
|
79
|
+
# arch linux
|
80
|
+
return'arch'
|
81
|
+
end
|
82
|
+
|
83
|
+
raise CheckPackageError, Rainbow("[Error] #{@os_name} linux platform not support!").red
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def debian_installed?(package_name)
|
88
|
+
output = `dpkg -l | grep #{package_name}`
|
89
|
+
!output.strip.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def centos_installed?(package_name)
|
93
|
+
output = `rpm -qa | grep #{package_name}`
|
94
|
+
!output.strip.empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
def redhat_installed?(package_name)
|
98
|
+
output = `yum list installed | grep #{package_name}`
|
99
|
+
!output.strip.empty?
|
100
|
+
end
|
101
|
+
|
102
|
+
def arch_installed?(package_name)
|
103
|
+
output = `pacman -Q | grep #{package_name}`
|
104
|
+
!output.strip.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
def check
|
108
|
+
@os_name = detect_os
|
109
|
+
__send__ "#{@os_name}_check"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class CheckFFmpeg < CheckPackage
|
114
|
+
def initialize
|
115
|
+
super
|
116
|
+
@name = "ffmpeg"
|
117
|
+
@need_error = Rainbow("[Error] `#{@name}` is need!").red
|
118
|
+
@tips = Rainbow("[Tips ] For more details and install: https://www.ffmpeg.org/").green
|
119
|
+
end
|
120
|
+
|
121
|
+
def macos_check
|
122
|
+
if !macos_installed?(@name)
|
123
|
+
raise CheckPackageError, "\n#{@need_error}\n#{@tips}\n"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def linux_check
|
128
|
+
linux_platform_name = detect_linux_distribution
|
129
|
+
__send__ "#{linux_platform_name}_installed?", @name
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
class CheckImageMagick < CheckPackage
|
135
|
+
def initialize
|
136
|
+
super
|
137
|
+
@name = "imagemagick"
|
138
|
+
@need_error = Rainbow("[Error] `imagemagick` is need!").red
|
139
|
+
@tips = Rainbow("[Tips ] For more details and install guide: https://github.com/rmagick/rmagick").green
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
def macos_check
|
144
|
+
if !macos_installed?(@name)
|
145
|
+
raise CheckPackageError, "\n#{@need_error}\n#{@tips}\n"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def linux_check
|
150
|
+
# just Debian/Ubuntu
|
151
|
+
linux_pkg = {
|
152
|
+
"debian" => "libmagickwand-dev",
|
153
|
+
"redhat" => "ImageMagick-devel",
|
154
|
+
"arch" => "imagemagick",
|
155
|
+
}
|
156
|
+
|
157
|
+
linux_platform_name = detect_linux_distribution
|
158
|
+
pkg_name = linux_pkg.fetch(linux_platform_name, nil)
|
159
|
+
|
160
|
+
if !pkg_name
|
161
|
+
raise CheckPackageError, "\n#{@need_error}\n#{@tips}\n"
|
162
|
+
end
|
163
|
+
|
164
|
+
__send__ "#{linux_platform_name}_installed?", pkg_name
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require "io/console"
|
2
|
+
require "rmagick"
|
3
|
+
require "rainbow"
|
4
|
+
require "open-uri"
|
5
|
+
require_relative "./check_package"
|
6
|
+
require_relative './version'
|
7
|
+
|
8
|
+
module Convert2Ascii
|
9
|
+
class Image2AsciiError < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
class Image2Ascii
|
13
|
+
module STYLE_ENUM
|
14
|
+
Color = "color"
|
15
|
+
Text = "text"
|
16
|
+
end
|
17
|
+
|
18
|
+
module COLOR_ENUM
|
19
|
+
Full = "full"
|
20
|
+
Greyscale = "greyscale"
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
attr_reader :width, :ascii_string
|
25
|
+
attr_accessor :chars
|
26
|
+
|
27
|
+
def initialize(**args)
|
28
|
+
@uri = args[:uri]
|
29
|
+
@width = args[:width] || IO.console.winsize[1]
|
30
|
+
@style = args[:style] || STYLE_ENUM::Color # "color": color ansi , "text": plain text
|
31
|
+
@color = args[:color] || COLOR_ENUM::Full # full
|
32
|
+
@color_block = args[:color_block] || false
|
33
|
+
|
34
|
+
check_quantum_convert_factor
|
35
|
+
# divides quantum depth color space into usable rgb values
|
36
|
+
@quantum_convert_factor = Magick::MAGICKCORE_QUANTUM_DEPTH == 16 ? 257 : 1
|
37
|
+
|
38
|
+
@chars = ".'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
|
39
|
+
@ascii_string = ""
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_quantum_convert_factor
|
43
|
+
# quantum conversion factor for dealing with quantum depth color values
|
44
|
+
if Magick::MAGICKCORE_QUANTUM_DEPTH > 16
|
45
|
+
raise Image2AsciiError, "[Error] ImageMagick quantum depth is set to #{Magick::MAGICKCORE_QUANTUM_DEPTH}. It needs to be 16 or less"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate(**args)
|
50
|
+
@width = args[:width] || @width
|
51
|
+
@style = args[:style] || @style # "color": color ansi , "text": plain text
|
52
|
+
@color = args[:color] || @color # full
|
53
|
+
@color_block = args[:color_block] || @color_block
|
54
|
+
|
55
|
+
generate_string
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def tty_print
|
61
|
+
print @ascii_string
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def check_packages
|
67
|
+
CheckImageMagick.new.check
|
68
|
+
end
|
69
|
+
|
70
|
+
def generate_string
|
71
|
+
resource = URI.open(@uri)
|
72
|
+
img = Magick::ImageList.new
|
73
|
+
img.from_blob(resource.read)
|
74
|
+
|
75
|
+
img = correct_aspect_ratio(img, @width)
|
76
|
+
|
77
|
+
img.each_pixel do |pixel, col, row|
|
78
|
+
r, g, b, brightness = get_pixel_values(pixel)
|
79
|
+
char = select_character(brightness)
|
80
|
+
|
81
|
+
if @style == STYLE_ENUM::Text
|
82
|
+
@ascii_string << char
|
83
|
+
end
|
84
|
+
|
85
|
+
if @style == STYLE_ENUM::Color
|
86
|
+
chosen_color = get_chosen_color(@color, r, g, b)
|
87
|
+
|
88
|
+
if @color_block == true
|
89
|
+
@ascii_string << Rainbow(" ").background(*chosen_color)
|
90
|
+
else
|
91
|
+
@ascii_string << Rainbow(char).color(*chosen_color)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# add line wrap once desired width is reached
|
96
|
+
if (col % (@width - 1) == 0) and (col != 0)
|
97
|
+
@ascii_string << "\n"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def correct_aspect_ratio(img, width)
|
103
|
+
img = img.scale(width / img.columns.to_f)
|
104
|
+
img = img.scale(img.columns, img.rows / 2)
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_pixel_values(pixel)
|
108
|
+
r = pixel.red / @quantum_convert_factor
|
109
|
+
g = pixel.green / @quantum_convert_factor
|
110
|
+
b = pixel.blue / @quantum_convert_factor
|
111
|
+
# Brightness ref: https://en.wikipedia.org/wiki/Relative_luminance
|
112
|
+
brightness = (0.2126 * r + 0.7152 * g + 0.0722 * b)
|
113
|
+
|
114
|
+
return [r, g, b, brightness]
|
115
|
+
end
|
116
|
+
|
117
|
+
def select_character(brightness)
|
118
|
+
char_index = brightness / (255.0 / @chars.length)
|
119
|
+
@chars[char_index.floor]
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_chosen_color(color, r, g, b)
|
123
|
+
if color == COLOR_ENUM::Full
|
124
|
+
[r, g, b]
|
125
|
+
elsif color == COLOR_ENUM::Greyscale
|
126
|
+
[r, r, r]
|
127
|
+
else
|
128
|
+
color
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "async"
|
2
|
+
require "async/barrier"
|
3
|
+
require "async/semaphore"
|
4
|
+
require "etc"
|
5
|
+
|
6
|
+
module Convert2Ascii
|
7
|
+
class MultiTasker
|
8
|
+
def initialize(proc_tasks)
|
9
|
+
@proc_tasks = proc_tasks
|
10
|
+
@count = set_threads_count
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_threads_count
|
14
|
+
cpu_threads = (Etc.nprocessors || 1)
|
15
|
+
cpu_threads = cpu_threads > 4 ? cpu_threads - 1 : cpu_threads
|
16
|
+
cpu_threads
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
barrier = Async::Barrier.new
|
21
|
+
|
22
|
+
Sync do
|
23
|
+
# Only 10 tasks are created at a time:
|
24
|
+
semaphore = Async::Semaphore.new(@count, parent: barrier)
|
25
|
+
|
26
|
+
@proc_tasks.map do |proc_task|
|
27
|
+
semaphore.async do
|
28
|
+
proc_task.call
|
29
|
+
end
|
30
|
+
end.map(&:wait)
|
31
|
+
ensure
|
32
|
+
barrier.stop
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require "rainbow"
|
2
|
+
require_relative "./terminal"
|
3
|
+
|
4
|
+
module Convert2Ascii
|
5
|
+
class TerminalPlayer
|
6
|
+
|
7
|
+
SAFE_SLOW_DELTA = 0.9 # seconds
|
8
|
+
SAFE_FAST_DELTA = 0.2 # seconds
|
9
|
+
|
10
|
+
attr_accessor :play_loop, :step_duration, :debug
|
11
|
+
def initialize(**args)
|
12
|
+
@debug = false
|
13
|
+
@audio = args[:audio]
|
14
|
+
@frames = args[:frames]
|
15
|
+
@play_loop = args[:play_loop]
|
16
|
+
@step_duration = args[:step_duration]
|
17
|
+
|
18
|
+
@total_duration = @frames.length * @step_duration
|
19
|
+
@first_frame = true
|
20
|
+
@backspace_adjust = "\033[A" * (@frames.length + 1)
|
21
|
+
|
22
|
+
regist_hook
|
23
|
+
end
|
24
|
+
|
25
|
+
def play
|
26
|
+
begin
|
27
|
+
init_screen
|
28
|
+
render
|
29
|
+
rescue => error
|
30
|
+
raise error
|
31
|
+
ensure
|
32
|
+
clean_up
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def regist_hook
|
39
|
+
trap("INT") {
|
40
|
+
clean_up
|
41
|
+
exit 0
|
42
|
+
}
|
43
|
+
|
44
|
+
# TODO ??? 改变会消失
|
45
|
+
# # 窗口变化事件
|
46
|
+
# trap("SIGWINCH") {
|
47
|
+
# resize
|
48
|
+
# }
|
49
|
+
end
|
50
|
+
|
51
|
+
def resize
|
52
|
+
clear_screen
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear_screen
|
56
|
+
Terminal.clear_buffer
|
57
|
+
end
|
58
|
+
|
59
|
+
def init_screen
|
60
|
+
clear_screen
|
61
|
+
setup
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup
|
65
|
+
Terminal.open_buffer
|
66
|
+
Terminal.hide_cursor
|
67
|
+
Terminal.clear_screen
|
68
|
+
end
|
69
|
+
|
70
|
+
def clean_up
|
71
|
+
if !@debug
|
72
|
+
Terminal.close_buffer
|
73
|
+
Terminal.clear_screen
|
74
|
+
Terminal.show_cursor
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def debug_log(var_name)
|
79
|
+
puts Rainbow('-- debug ----').yellow
|
80
|
+
puts "class:"
|
81
|
+
p var_name.class
|
82
|
+
puts "value:"
|
83
|
+
p var_name
|
84
|
+
end
|
85
|
+
|
86
|
+
def full_screen(content)
|
87
|
+
|
88
|
+
if !content
|
89
|
+
return content
|
90
|
+
end
|
91
|
+
# 补齐高度,没内容也要追加 \n 刷新,因为会拖拽 窗体尺寸会变化
|
92
|
+
rows, _ = Terminal.winsize
|
93
|
+
# content: pure text with "\n"
|
94
|
+
content = content.split("\n")
|
95
|
+
|
96
|
+
rows_fill = []
|
97
|
+
delta = rows - content.length
|
98
|
+
|
99
|
+
if delta > 0
|
100
|
+
delta.times do
|
101
|
+
rows_fill << "\n"
|
102
|
+
end
|
103
|
+
elsif delta < 0
|
104
|
+
content = content[0..(rows - 1)]
|
105
|
+
end
|
106
|
+
|
107
|
+
content.join("\n")
|
108
|
+
end
|
109
|
+
|
110
|
+
def self_adaption_frame_play
|
111
|
+
# 当偏移在-90ms(音频滞后于视频)到+20ms(音频超前视频)之间时,人感觉不到视听质量的变化,这个区域可以认为是同步区域
|
112
|
+
# https://github.com/0voice/audio_video_streaming/blob/main/article/029-%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5%E7%AE%97%E6%B3%95.md
|
113
|
+
|
114
|
+
start_time = Time.now
|
115
|
+
if @audio
|
116
|
+
Thread.new do
|
117
|
+
start_time = Time.now # 以音频为准
|
118
|
+
play_cmd = "ffplay -nodisp "
|
119
|
+
if @play_loop
|
120
|
+
play_cmd << " -loop 0"
|
121
|
+
end
|
122
|
+
system("#{play_cmd} -i #{@audio} &> /dev/null")
|
123
|
+
if @debug
|
124
|
+
puts Rainbow("[info] audio time: #{Time.now - start_time} s").green
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
frame_index = 0
|
129
|
+
loop do
|
130
|
+
if frame_index <= @frames.length
|
131
|
+
content = @frames[frame_index]
|
132
|
+
if !@first_frame
|
133
|
+
$stdout.print @backspace_adjust
|
134
|
+
end
|
135
|
+
clear_screen
|
136
|
+
$stdout.print(full_screen(content))
|
137
|
+
@first_frame = false
|
138
|
+
sleep(@step_duration)
|
139
|
+
end
|
140
|
+
|
141
|
+
actual_time = Time.now - start_time
|
142
|
+
video_play_time = frame_index * @step_duration
|
143
|
+
|
144
|
+
offset_time = actual_time - video_play_time
|
145
|
+
if offset_time > SAFE_SLOW_DELTA
|
146
|
+
offset = (offset_time / @step_duration).floor
|
147
|
+
if @debug
|
148
|
+
puts Rainbow("[+] #{offset}").green
|
149
|
+
end
|
150
|
+
elsif offset_time < -1 * SAFE_FAST_DELTA
|
151
|
+
offset = 0
|
152
|
+
else
|
153
|
+
offset = 1
|
154
|
+
end
|
155
|
+
|
156
|
+
frame_index = (frame_index + offset) % @frames.length
|
157
|
+
|
158
|
+
real_time = Time.now - start_time
|
159
|
+
frame_time = (frame_index + 1) * @step_duration
|
160
|
+
|
161
|
+
if !@play_loop
|
162
|
+
if real_time > @total_duration
|
163
|
+
break
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
if @debug
|
168
|
+
puts ""
|
169
|
+
puts Rainbow("RealTime: #{real_time} s").green
|
170
|
+
puts Rainbow("FrameTime: #{frame_time} s").green
|
171
|
+
puts Rainbow("CurrentFrame: #{frame_index} / #{@frames.length} ").green
|
172
|
+
puts Rainbow("Real - Frame: #{real_time - frame_time} s").green
|
173
|
+
puts Rainbow("[+] #{offset}").green
|
174
|
+
puts Rainbow("@step_duration #{@step_duration}").green
|
175
|
+
puts Rainbow("@play_loop #{@play_loop}").green
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def render
|
181
|
+
self_adaption_frame_play
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "io/console"
|
2
|
+
|
3
|
+
# ANSI_escape_code
|
4
|
+
# Ref: https://xn--rpa.cc/irl/term.html
|
5
|
+
# https://en.wikipedia.org/wiki/ANSI_escape_code#CSIsection
|
6
|
+
module Convert2Ascii
|
7
|
+
class Terminal
|
8
|
+
class << self
|
9
|
+
def winsize
|
10
|
+
IO.console.winsize # [rows, columns]
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear_buffer
|
14
|
+
print "\x1b[3J"
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear_screen
|
18
|
+
print "\x1b[2J"
|
19
|
+
end
|
20
|
+
|
21
|
+
def hide_cursor
|
22
|
+
print "\x1b[?25l"
|
23
|
+
end
|
24
|
+
|
25
|
+
def show_cursor
|
26
|
+
print "\x1b[?25h"
|
27
|
+
end
|
28
|
+
|
29
|
+
def open_buffer
|
30
|
+
# 打开特殊缓存
|
31
|
+
print "\x1b[?1049h"
|
32
|
+
end
|
33
|
+
|
34
|
+
def close_buffer
|
35
|
+
# 关闭特殊缓存
|
36
|
+
print "\x1b[?1049l"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require "open-uri"
|
2
|
+
require "json"
|
3
|
+
require "tmpdir"
|
4
|
+
require "etc"
|
5
|
+
require "io/console"
|
6
|
+
require "rainbow"
|
7
|
+
require_relative "./image2ascii"
|
8
|
+
require_relative "./terminal-player"
|
9
|
+
require_relative "./multi-tasker"
|
10
|
+
require_relative "./check_package"
|
11
|
+
require_relative './version'
|
12
|
+
|
13
|
+
module Convert2Ascii
|
14
|
+
class Video2AsciiError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class Video2Ascii
|
18
|
+
DEFAULT_STEP_DURATION = 0.04
|
19
|
+
|
20
|
+
|
21
|
+
attr_accessor :uri, :width, :threads_count, :output, :step_duration
|
22
|
+
def initialize(**args)
|
23
|
+
@uri = args[:uri]
|
24
|
+
@step_duration = args[:step_duration] || DEFAULT_STEP_DURATION
|
25
|
+
@threads_count = set_threads_count
|
26
|
+
|
27
|
+
@tmpdir = nil
|
28
|
+
@output = Dir.pwd
|
29
|
+
|
30
|
+
# image2ascii attrs
|
31
|
+
@width = args[:width] || IO.console.winsize[1]
|
32
|
+
@style = args[:style] || Image2Ascii::STYLE_ENUM::Color # "color": color ansi , "text": plain text
|
33
|
+
@color = args[:color] || Image2Ascii::COLOR_ENUM::Full # full
|
34
|
+
@color_block = args[:color_block] || false
|
35
|
+
|
36
|
+
check_packages
|
37
|
+
regist_hooks
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate(**args)
|
41
|
+
@width = args[:width] || @width
|
42
|
+
@style = args[:style] || @style # "color": color ansi , "text": plain text
|
43
|
+
@color = args[:color] || @color # full
|
44
|
+
@color_block = args[:color_block] || @color_block
|
45
|
+
|
46
|
+
@tmpdir = Dir.mktmpdir
|
47
|
+
@audio = get_audio_from_video(@tmpdir)
|
48
|
+
screenshots_from_video(@tmpdir)
|
49
|
+
convert_all_images(@tmpdir)
|
50
|
+
@frames_path = order_frames_path
|
51
|
+
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def save(output_dir)
|
57
|
+
system("rm -rf #{@tmpdir}/*.jpg")
|
58
|
+
system("rm -rf #{output_dir} && mkdir #{output_dir}")
|
59
|
+
system("cp -r #{@tmpdir}/* #{output_dir}")
|
60
|
+
|
61
|
+
# save config
|
62
|
+
File.open("#{output_dir}/meta.json", "w") do |f|
|
63
|
+
config = {
|
64
|
+
step_duration: @step_duration,
|
65
|
+
audio: @audio ? File.basename(@audio) : nil,
|
66
|
+
frames_count: @frames_path.length
|
67
|
+
}
|
68
|
+
json_data = JSON.generate(config, pretty: true)
|
69
|
+
f.puts json_data
|
70
|
+
end
|
71
|
+
|
72
|
+
puts ""
|
73
|
+
puts Rainbow("[info] save success!").green
|
74
|
+
end
|
75
|
+
|
76
|
+
def order_frames_path
|
77
|
+
frames_path = Dir.glob("#{@tmpdir}/*.txt")
|
78
|
+
frames_path = frames_path.sort do |a, b|
|
79
|
+
get_name_order(a) <=> get_name_order(b)
|
80
|
+
end
|
81
|
+
|
82
|
+
@frames_path = frames_path
|
83
|
+
end
|
84
|
+
|
85
|
+
def play(**args)
|
86
|
+
play_loop = args[:play_loop] || false
|
87
|
+
step_duration = args[:step_duration] || @step_duration
|
88
|
+
frames = @frames_path.map { |f| File.open(f).read }
|
89
|
+
|
90
|
+
player_args = {
|
91
|
+
frames: ,
|
92
|
+
audio: @audio,
|
93
|
+
play_loop:,
|
94
|
+
step_duration:,
|
95
|
+
}
|
96
|
+
TerminalPlayer.new(**player_args).play
|
97
|
+
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def check_packages
|
104
|
+
CheckImageMagick.new.check
|
105
|
+
CheckFFmpeg.new.check
|
106
|
+
end
|
107
|
+
|
108
|
+
def regist_hooks
|
109
|
+
at_exit {
|
110
|
+
if @tmpdir
|
111
|
+
FileUtils.remove_entry @tmpdir
|
112
|
+
end
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_threads_count
|
117
|
+
cpu_threads = (Etc.nprocessors || 1)
|
118
|
+
cpu_threads = cpu_threads > 4 ? cpu_threads - 1 : cpu_threads
|
119
|
+
cpu_threads
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_audio_from_video(save_dir)
|
123
|
+
puts Rainbow("[info] parsing audio...").green
|
124
|
+
audio_save = "#{save_dir}/audio.mp3"
|
125
|
+
cmd = "ffmpeg -i '#{@uri}' -vn -nostats -loglevel 0 #{audio_save}"
|
126
|
+
result = system(cmd)
|
127
|
+
if !result
|
128
|
+
audio_save = nil
|
129
|
+
end
|
130
|
+
puts Rainbow("[info] parsing audio done.").green
|
131
|
+
|
132
|
+
return audio_save
|
133
|
+
end
|
134
|
+
|
135
|
+
def screenshots_from_video(save_dir)
|
136
|
+
# ffmpeg use multi threads
|
137
|
+
# Benchmark
|
138
|
+
# Apple M1 Max:
|
139
|
+
# * thread:1 2.64s
|
140
|
+
# * thread:9 0.96s
|
141
|
+
puts Rainbow("[info] video slicing...").green
|
142
|
+
image_path = save_dir
|
143
|
+
cmd = "ffmpeg -i '#{@uri}' -threads #{@threads_count} -vf fps=1/#{@step_duration} -nostats -loglevel 0 #{image_path}/%d.jpg"
|
144
|
+
result = system(cmd)
|
145
|
+
|
146
|
+
if !result
|
147
|
+
raise Video2AsciiError, Rainbow("\n[Error] exec `#{cmd}` fail!").red
|
148
|
+
end
|
149
|
+
puts Rainbow("[info] video slicing done.").green
|
150
|
+
|
151
|
+
return image_path
|
152
|
+
end
|
153
|
+
|
154
|
+
def convert_to_ascii(image)
|
155
|
+
config = {
|
156
|
+
width: @width,
|
157
|
+
style: @style,
|
158
|
+
color: @color,
|
159
|
+
color_block: @color_block
|
160
|
+
}
|
161
|
+
Image2Ascii.new(uri: image).generate(**config).ascii_string
|
162
|
+
end
|
163
|
+
|
164
|
+
def get_name_order(name_path)
|
165
|
+
if name_path
|
166
|
+
return File.basename(name_path, ".*").to_i
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def convert_all_images(save_dir)
|
171
|
+
images = Dir.glob("#{save_dir}/*.jpg")
|
172
|
+
|
173
|
+
processed_count = 0
|
174
|
+
tasks = []
|
175
|
+
time_start = Time.now
|
176
|
+
images.each_with_index do |image, i|
|
177
|
+
tasks << lambda {
|
178
|
+
begin
|
179
|
+
ascii_string = convert_to_ascii(image)
|
180
|
+
basename = File.basename(image, ".jpg")
|
181
|
+
File.open("#{@tmpdir}/#{basename}.txt", "w") do |f|
|
182
|
+
f.puts ascii_string
|
183
|
+
end
|
184
|
+
|
185
|
+
processed_count += 1
|
186
|
+
print(Rainbow("\rprocessing... #{sprintf("%.2f", (1.0 * processed_count / images.length) * 100)} % (time: #{sprintf("%.2f", Time.now - time_start)} s)").green)
|
187
|
+
rescue => error
|
188
|
+
puts "-------"
|
189
|
+
puts Rainbow("\n[Error] convert_to_ascii -> image: #{image} | i: #{i}").red
|
190
|
+
puts error
|
191
|
+
end
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
MultiTasker.new(tasks).run
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: convert2ascii
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark24
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-01-12 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rmagick
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 6.0.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 6.0.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rainbow
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.1.1
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.1.1
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: async
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.21'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.21'
|
54
|
+
description: This gem can help you convert image or video became ASCII art in your
|
55
|
+
terminal.
|
56
|
+
email:
|
57
|
+
- mark.zhangyoung@gmail.com
|
58
|
+
executables:
|
59
|
+
- image2ascii
|
60
|
+
- video2ascii
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- LICENSE.txt
|
65
|
+
- README.md
|
66
|
+
- Rakefile
|
67
|
+
- TODO.md
|
68
|
+
- exe/image2ascii
|
69
|
+
- exe/video2ascii
|
70
|
+
- lib/convert2ascii.rb
|
71
|
+
- lib/convert2ascii/check_package.rb
|
72
|
+
- lib/convert2ascii/image2ascii.rb
|
73
|
+
- lib/convert2ascii/multi-tasker.rb
|
74
|
+
- lib/convert2ascii/terminal-player.rb
|
75
|
+
- lib/convert2ascii/terminal.rb
|
76
|
+
- lib/convert2ascii/version.rb
|
77
|
+
- lib/convert2ascii/video2ascii.rb
|
78
|
+
homepage: https://github.com/Mark24Code/convert2ascii
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata:
|
82
|
+
homepage_uri: https://github.com/Mark24Code/convert2ascii
|
83
|
+
source_code_uri: https://github.com/Mark24Code/convert2ascii
|
84
|
+
changelog_uri: https://github.com/Mark24Code/convert2ascii
|
85
|
+
rdoc_options: []
|
86
|
+
require_paths:
|
87
|
+
- lib
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 3.1.0
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubygems_version: 3.6.2
|
100
|
+
specification_version: 4
|
101
|
+
summary: Convert image or video to ASCII art
|
102
|
+
test_files: []
|