terminal_image 0.3.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e81b808e453e3af8ffe4eaeae50732f0daa05ee2cf7920536b7259b7c71e2235
4
- data.tar.gz: b7f755c2e7fc80447d4f3ec97ddc1da3d3514a29db6cbaae9568a263873e7467
3
+ metadata.gz: 59f2f7f04d128c42a96335aa00e6c443b66514ff4cb5497d167e65942813a2bc
4
+ data.tar.gz: e6e79383209adbcc08acc3ea2145ec2c90909e749ce85acdca20b7f0b85c48ad
5
5
  SHA512:
6
- metadata.gz: b6c16294048ae920e6ed965a57672f0a761779d4f0938e36adaaf19be9404ba1224bc48808ac91a4d1373c86d828d2b6d2a28dc8182e7ef574255ebd3a14ad3b
7
- data.tar.gz: d6bf49d4f983ec45bd66761f351bba1f78eabbd4ce2176ab0933c0743fd36ceebc9031c602f07aa0377f7b37d7a50c2906179c14ff6a522a8bf91173b54c2bd3
6
+ metadata.gz: 0d004649f6006d31030d608aacf427488cfbfd84434a872dbcb9ac2b02f745dad4fa11dc118aee623d3ac92a15895b9d6737e1e6353b768f868f9f193031e955
7
+ data.tar.gz: 3decf6ffdaf489af2e8316ce28750299ac01947f147a0f77a98e57cadfc96bcdc798ce8e1dcec8c2f150aac268259156a3d310476204e4783a48205849ebfba2
@@ -0,0 +1 @@
1
+ * @unhappychoice
@@ -0,0 +1,33 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "12:00"
8
+ timezone: Asia/Tokyo
9
+ open-pull-requests-limit: 20
10
+ reviewers:
11
+ - unhappychoice
12
+ allow:
13
+ - dependency-type: direct
14
+ - dependency-type: indirect
15
+ ignore:
16
+ - dependency-name: parallel
17
+ update-types: ["version-update:semver-major"]
18
+ - dependency-name: codecov
19
+ versions:
20
+ - ">= 0.2.14.a, < 0.2.15"
21
+ - dependency-name: rubocop
22
+ versions:
23
+ - 1.12.1
24
+ - dependency-name: listen
25
+ versions:
26
+ - 3.5.0
27
+ - dependency-name: codecov
28
+ versions:
29
+ - 0.5.0
30
+ - dependency-name: regexp_parser
31
+ versions:
32
+ - 2.1.0
33
+ - 2.1.1
@@ -0,0 +1,45 @@
1
+ name: release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: Release version
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - major
12
+ - minor
13
+ - patch
14
+ jobs:
15
+ push:
16
+ name: Push gem to RubyGems.org
17
+ runs-on: ubuntu-24.04
18
+ environment:
19
+ name: release
20
+ permissions:
21
+ id-token: write
22
+ contents: write
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - name: Set up Ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ bundler-cache: true
29
+ ruby-version: .ruby-version
30
+ - name: Setup Git
31
+ run: |
32
+ git config --global user.email "unhappychoice@gmail.com"
33
+ git config --global user.name "Yuji Ueki"
34
+ - name: Install gem-release
35
+ run: gem install gem-release
36
+ - name: Bump version
37
+ run: |
38
+ git status
39
+ gem bump --version ${{inputs.version}} --message ':tada: Bump %{name} to %{version}'
40
+ git status
41
+ bundle config set frozen false
42
+ bundle install
43
+ git add .
44
+ git commit --amend --no-edit
45
+ - uses: rubygems/release-gem@v1
@@ -0,0 +1,52 @@
1
+ name: test
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ lint:
7
+ runs-on: ubuntu-24.04
8
+ steps:
9
+ - uses: actions/checkout@master
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: .ruby-version
14
+ - name: Install bundler
15
+ run: gem install bundler
16
+ - name: Install dependencies
17
+ run: bundle install --jobs 4
18
+ - name: Run test
19
+ run: bundle exec rubocop lib
20
+ test:
21
+ runs-on: ubuntu-24.04
22
+ services:
23
+ redis:
24
+ image: redis
25
+ options: >-
26
+ --health-cmd "redis-cli ping"
27
+ --health-interval 10s
28
+ --health-timeout 5s
29
+ --health-retries 5
30
+ ports:
31
+ - 6379:6379
32
+ strategy:
33
+ matrix:
34
+ ruby: ['3.4.4', '3.3.8']
35
+ steps:
36
+ - uses: actions/checkout@master
37
+ - name: Set up Rub
38
+ uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: ${{ matrix.ruby }}
41
+ - name: Install libsixel
42
+ run: sudo apt-get install libsixel-bin
43
+ - name: Install bundler
44
+ run: gem install bundler
45
+ - name: Install dependencies
46
+ run: bundle install --jobs 4
47
+ - name: Run test
48
+ env:
49
+ COVERAGE: true
50
+ CODECOV_TOKEN: d3d0fd4f-db2c-439b-9331-8e1d992813ad
51
+ AWS_REGION: ap-northeast-1
52
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,57 @@
1
+ # Created by https://www.gitignore.io/api/ruby
2
+ # Edit at https://www.gitignore.io/?templates=ruby
3
+
4
+ ### Ruby ###
5
+ *.gem
6
+ *.rbc
7
+ /.config
8
+ /coverage/
9
+ /InstalledFiles
10
+ /pkg/
11
+ /spec/reports/
12
+ /spec/examples.txt
13
+ /test/tmp/
14
+ /test/version_tmp/
15
+ /tmp/
16
+
17
+ # Used by dotenv library to load environment variables.
18
+ # .env
19
+
20
+ ## Specific to RubyMotion:
21
+ .dat*
22
+ .repl_history
23
+ build/
24
+ *.bridgesupport
25
+ build-iPhoneOS/
26
+ build-iPhoneSimulator/
27
+
28
+ ## Specific to RubyMotion (use of CocoaPods):
29
+ #
30
+ # We recommend against adding the Pods directory to your .gitignore. However
31
+ # you should judge for yourself, the pros and cons are mentioned at:
32
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33
+ #
34
+ # vendor/Pods/
35
+
36
+ ## Documentation cache and generated files:
37
+ /.yardoc/
38
+ /_yardoc/
39
+ /doc/
40
+ /rdoc/
41
+
42
+ ## Environment normalization:
43
+ /.bundle/
44
+ /vendor/bundle
45
+ /lib/bundler/man/
46
+ vendor/bundle
47
+
48
+ # for a library or gem, you might want to ignore these files since the code is
49
+ # intended to run in multiple environments; otherwise, check them in:
50
+ # Gemfile.lock
51
+ # .ruby-version
52
+ # .ruby-gemset
53
+
54
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
55
+ .rvmrc
56
+
57
+ # End of https://www.gitignore.io/api/ruby
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --require spec_helper
data/.rubocop.yml CHANGED
@@ -1,5 +1,9 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.3
4
+
1
5
  Style/Documentation:
2
6
  Enabled: false
3
7
 
4
- Metrics/LineLength:
8
+ Layout/LineLength:
5
9
  Max: 120
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.4
data/Gemfile CHANGED
@@ -4,3 +4,15 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in terminal_image.gemspec
6
6
  gemspec
7
+
8
+ group :development do
9
+ gem 'bundler', '~> 2.0'
10
+ gem 'codecov'
11
+ gem 'guard'
12
+ gem 'guard-rspec'
13
+ gem 'guard-rubocop'
14
+ gem 'rake', '~> 13.4'
15
+ gem 'rspec', '~> 3.13'
16
+ gem 'rubocop'
17
+ gem 'simplecov'
18
+ end
data/Gemfile.lock CHANGED
@@ -1,30 +1,35 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- terminal_image (0.3.0)
4
+ terminal_image (1.1.0)
5
+ base64 (~> 0.3)
6
+ chunky_png (~> 1.4)
5
7
  fastimage
6
8
 
7
9
  GEM
8
10
  remote: https://rubygems.org/
9
11
  specs:
10
- ast (2.4.0)
11
- codecov (0.1.14)
12
+ ast (2.4.3)
13
+ base64 (0.3.0)
14
+ chunky_png (1.4.0)
15
+ codecov (0.2.12)
12
16
  json
13
17
  simplecov
14
- url
15
- coderay (1.1.2)
16
- diff-lcs (1.3)
17
- docile (1.3.2)
18
- fastimage (2.1.7)
19
- ffi (1.11.1)
20
- formatador (0.2.5)
21
- guard (2.15.1)
18
+ coderay (1.1.3)
19
+ diff-lcs (1.6.2)
20
+ fastimage (2.4.3)
21
+ ffi (1.17.4-x86_64-darwin)
22
+ ffi (1.17.4-x86_64-linux-gnu)
23
+ formatador (1.2.3)
24
+ reline
25
+ guard (2.20.2)
22
26
  formatador (>= 0.2.4)
23
27
  listen (>= 2.7, < 4.0)
28
+ logger (~> 1.6)
24
29
  lumberjack (>= 1.0.12, < 2.0)
25
30
  nenv (~> 0.1)
26
31
  notiffany (~> 0.0)
27
- pry (>= 0.9.12)
32
+ pry (>= 0.13.0)
28
33
  shellany (~> 0.0)
29
34
  thor (>= 0.18.1)
30
35
  guard-compat (1.2.1)
@@ -32,66 +37,80 @@ GEM
32
37
  guard (~> 2.1)
33
38
  guard-compat (~> 1.1)
34
39
  rspec (>= 2.99.0, < 4.0)
35
- guard-rubocop (1.3.0)
40
+ guard-rubocop (1.5.0)
36
41
  guard (~> 2.0)
37
- rubocop (~> 0.20)
38
- jaro_winkler (1.5.3)
39
- json (2.2.0)
40
- listen (3.1.5)
41
- rb-fsevent (~> 0.9, >= 0.9.4)
42
- rb-inotify (~> 0.9, >= 0.9.7)
43
- ruby_dep (~> 1.2)
44
- lumberjack (1.0.13)
45
- method_source (0.9.2)
42
+ rubocop (< 2.0)
43
+ io-console (0.9.5)
44
+ json (3.1.0)
45
+ language_server-protocol (3.17.0.6)
46
+ lint_roller (1.1.0)
47
+ listen (3.10.1)
48
+ logger
49
+ rb-fsevent (~> 0.10, >= 0.10.3)
50
+ rb-inotify (~> 0.9, >= 0.9.10)
51
+ logger (1.7.0)
52
+ lumberjack (1.4.2)
53
+ method_source (1.1.0)
46
54
  nenv (0.3.0)
47
55
  notiffany (0.1.3)
48
56
  nenv (~> 0.1)
49
57
  shellany (~> 0.0)
50
- parallel (1.17.0)
51
- parser (2.6.4.1)
52
- ast (~> 2.4.0)
53
- pry (0.12.2)
54
- coderay (~> 1.1.0)
55
- method_source (~> 0.9.0)
56
- rainbow (3.0.0)
57
- rake (10.5.0)
58
- rb-fsevent (0.10.3)
59
- rb-inotify (0.10.0)
58
+ parallel (2.3.0)
59
+ parser (3.3.13.0)
60
+ ast (~> 2.4.1)
61
+ racc
62
+ prism (1.9.0)
63
+ pry (0.16.1)
64
+ coderay (~> 1.1)
65
+ method_source (~> 1.0)
66
+ reline (>= 0.6.0)
67
+ racc (1.8.1)
68
+ rainbow (3.1.1)
69
+ rake (13.4.2)
70
+ rb-fsevent (0.11.2)
71
+ rb-inotify (0.11.1)
60
72
  ffi (~> 1.0)
61
- rspec (3.8.0)
62
- rspec-core (~> 3.8.0)
63
- rspec-expectations (~> 3.8.0)
64
- rspec-mocks (~> 3.8.0)
65
- rspec-core (3.8.2)
66
- rspec-support (~> 3.8.0)
67
- rspec-expectations (3.8.4)
73
+ regexp_parser (2.13.1)
74
+ reline (0.7.0)
75
+ io-console (~> 0.5)
76
+ rspec (3.13.2)
77
+ rspec-core (~> 3.13.0)
78
+ rspec-expectations (~> 3.13.0)
79
+ rspec-mocks (~> 3.13.0)
80
+ rspec-core (3.13.6)
81
+ rspec-support (~> 3.13.0)
82
+ rspec-expectations (3.13.5)
68
83
  diff-lcs (>= 1.2.0, < 2.0)
69
- rspec-support (~> 3.8.0)
70
- rspec-mocks (3.8.1)
84
+ rspec-support (~> 3.13.0)
85
+ rspec-mocks (3.13.8)
71
86
  diff-lcs (>= 1.2.0, < 2.0)
72
- rspec-support (~> 3.8.0)
73
- rspec-support (3.8.2)
74
- rubocop (0.74.0)
75
- jaro_winkler (~> 1.5.1)
76
- parallel (~> 1.10)
77
- parser (>= 2.6)
87
+ rspec-support (~> 3.13.0)
88
+ rspec-support (3.13.7)
89
+ rubocop (1.92.2)
90
+ json (>= 2.3)
91
+ language_server-protocol (~> 3.17.0.2)
92
+ lint_roller (~> 1.1.0)
93
+ parallel (>= 1.10)
94
+ parser (>= 3.3.0.2)
78
95
  rainbow (>= 2.2.2, < 4.0)
96
+ regexp_parser (>= 2.11.3, < 3.0)
97
+ rubocop-ast (>= 1.49.0, < 2.0)
79
98
  ruby-progressbar (~> 1.7)
80
- unicode-display_width (>= 1.4.0, < 1.7)
81
- ruby-progressbar (1.10.1)
82
- ruby_dep (1.5.0)
99
+ unicode-display_width (>= 2.4.0, < 4.0)
100
+ rubocop-ast (1.50.0)
101
+ parser (>= 3.3.7.2)
102
+ prism (~> 1.7)
103
+ ruby-progressbar (1.13.0)
83
104
  shellany (0.0.1)
84
- simplecov (0.17.0)
85
- docile (~> 1.1)
86
- json (>= 1.8, < 3)
87
- simplecov-html (~> 0.10.0)
88
- simplecov-html (0.10.2)
89
- thor (0.20.3)
90
- unicode-display_width (1.6.0)
91
- url (0.3.2)
105
+ simplecov (1.3.2)
106
+ thor (1.5.0)
107
+ unicode-display_width (3.3.0)
108
+ unicode-emoji (~> 4.3)
109
+ unicode-emoji (4.3.0)
92
110
 
93
111
  PLATFORMS
94
- ruby
112
+ x86_64-darwin-24
113
+ x86_64-linux
95
114
 
96
115
  DEPENDENCIES
97
116
  bundler (~> 2.0)
@@ -99,11 +118,11 @@ DEPENDENCIES
99
118
  guard
100
119
  guard-rspec
101
120
  guard-rubocop
102
- rake (~> 10.0)
103
- rspec (~> 3.0)
121
+ rake (~> 13.4)
122
+ rspec (~> 3.13)
104
123
  rubocop
105
124
  simplecov
106
125
  terminal_image!
107
126
 
108
127
  BUNDLED WITH
109
- 2.0.2
128
+ 2.4.14
data/Guardfile CHANGED
@@ -17,7 +17,7 @@
17
17
  #
18
18
  # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
19
19
 
20
- # Note: The cmd option is now required due to the increasing number of ways
20
+ # NOTE: The cmd option is now required due to the increasing number of ways
21
21
  # rspec may be run, below are examples of the most common uses.
22
22
  # * bundler: 'bundle exec rspec'
23
23
  # * bundler binstubs: 'bin/rspec'
data/README.md CHANGED
@@ -8,7 +8,8 @@
8
8
  ![GitHub](https://img.shields.io/github/license/unhappychoice/terminal_image.svg)
9
9
 
10
10
  TerminalImage is a library to show images on terminals.
11
- Currently, this library supports [iTerm2](https://iterm2.com/index.html) and terminals which supports [libsixel](https://github.com/saitoha/libsixel).
11
+ Currently, this library supports [iTerm2](https://iterm2.com/index.html) and terminals with [libsixel](https://github.com/saitoha/libsixel) installed.
12
+ On other terminals, images fall back to 24-bit ANSI colors (see [Fallback](#fallback)).
12
13
 
13
14
  ![](./images/example.png)
14
15
 
@@ -40,6 +41,20 @@ string = TerminalImage.encode(File.open('your-image-path.png'))
40
41
  string = TerminalImage.encode_url('https://raw.githubusercontent.com/unhappychoice/terminal_image/master/images/sample.png')
41
42
  ```
42
43
 
44
+ ## Fallback
45
+
46
+ When the terminal is not iTerm2 and `img2sixel` is not installed, images are drawn with 24-bit ("truecolor") ANSI colors.
47
+ Each character cell is an upper half block (`▀`): the foreground color is one pixel and the background color is the pixel below it.
48
+ The image is scaled down to the terminal width (`IO.console`, then `$COLUMNS`, then 80 columns) and is never scaled up.
49
+ Transparent pixels keep the terminal's default background.
50
+
51
+ Supported formats:
52
+
53
+ - **PNG** is decoded with the pure-Ruby [chunky_png](https://github.com/wvanbergen/chunky_png) gem and needs nothing else.
54
+ - **Other formats** (JPEG, GIF, WebP, ...) are converted to PNG with the [ImageMagick](https://imagemagick.org/) CLI
55
+ (`magick`, or `convert` for ImageMagick 6) when it is installed. Only the first frame of an animated image is shown.
56
+ Without ImageMagick they raise `TerminalImage::UnsupportedFormat`, a subclass of `TerminalImage::UnsupportedTerminal`.
57
+
43
58
  ## Contributing
44
59
 
45
60
  Bug reports and pull requests are welcome on GitHub at https://github.com/unhappychoice/terminal_image. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -51,3 +66,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
51
66
  ## Code of Conduct
52
67
 
53
68
  Everyone interacting in the TerminalImage project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/unhappychoice/terminal_image/blob/master/CODE_OF_CONDUCT.md).
69
+
70
+ ## Author
71
+
72
+ [@unhappychoice](https://unhappychoice.com)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chunky_png'
4
+ require 'io/console'
5
+
6
+ module TerminalImage
7
+ # Renders a PNG with 24-bit ANSI colors, two pixel rows per character cell.
8
+ module AnsiEncoder
9
+ UPPER_HALF_BLOCK = '▀'
10
+ LOWER_HALF_BLOCK = '▄'
11
+ DEFAULT_BACKGROUND = "\e[49m"
12
+ RESET = "\e[0m"
13
+ DEFAULT_WIDTH = 80
14
+ MIN_VISIBLE_ALPHA = 128
15
+ MAX_PIXELS = 50_000_000
16
+
17
+ class << self
18
+ def encode(file, width: terminal_width)
19
+ ensure_within_pixel_limit(*png_dimensions(file))
20
+ file.rewind
21
+ image = fit_to_width(ChunkyPNG::Image.from_io(file), width)
22
+ row_pairs(image).map { |top, bottom| encode_line(top, bottom) }.join("\n")
23
+ end
24
+
25
+ def terminal_width
26
+ [console_width, ENV.fetch('COLUMNS', nil).to_i].compact.find(&:positive?) || DEFAULT_WIDTH
27
+ end
28
+
29
+ def ensure_within_pixel_limit(width, height)
30
+ return if width.to_i * height.to_i <= MAX_PIXELS
31
+
32
+ raise ImageTooLarge, "#{width}x#{height} exceeds the #{MAX_PIXELS} pixel limit of the ANSI fallback"
33
+ end
34
+
35
+ private
36
+
37
+ def png_dimensions(file)
38
+ file.rewind
39
+ file.read(24).to_s.b.unpack('@16N2').map(&:to_i)
40
+ end
41
+
42
+ def console_width
43
+ IO.console&.winsize&.last
44
+ rescue SystemCallError
45
+ nil
46
+ end
47
+
48
+ def fit_to_width(image, width)
49
+ return image if image.width <= width
50
+
51
+ height = [(image.height * width / image.width.to_f).round, 1].max
52
+ image.resample_nearest_neighbor(width, height)
53
+ end
54
+
55
+ def row_pairs(image)
56
+ (0...image.height).step(2).map do |y|
57
+ [image.row(y), y + 1 < image.height ? image.row(y + 1) : []]
58
+ end
59
+ end
60
+
61
+ def encode_line(top, bottom)
62
+ top.each_with_index.map { |color, x| encode_cell(color, bottom[x]) }.join + RESET
63
+ end
64
+
65
+ def encode_cell(top, bottom)
66
+ case [visible?(top), visible?(bottom)]
67
+ when [true, true] then "#{foreground(top)}#{background(bottom)}#{UPPER_HALF_BLOCK}"
68
+ when [true, false] then "#{foreground(top)}#{DEFAULT_BACKGROUND}#{UPPER_HALF_BLOCK}"
69
+ when [false, true] then "#{foreground(bottom)}#{DEFAULT_BACKGROUND}#{LOWER_HALF_BLOCK}"
70
+ else "#{DEFAULT_BACKGROUND} "
71
+ end
72
+ end
73
+
74
+ def visible?(color)
75
+ !color.nil? && ChunkyPNG::Color.a(color) >= MIN_VISIBLE_ALPHA
76
+ end
77
+
78
+ def foreground(color)
79
+ "\e[38;2;#{rgb(color)}m"
80
+ end
81
+
82
+ def background(color)
83
+ "\e[48;2;#{rgb(color)}m"
84
+ end
85
+
86
+ def rgb(color)
87
+ ChunkyPNG::Color.to_truecolor_bytes(color).join(';')
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'stringio'
5
+
6
+ module TerminalImage
7
+ # Converts images to PNG with the ImageMagick CLI, reading the first frame only.
8
+ module PngConverter
9
+ COMMANDS = %w[magick convert].freeze
10
+ RESOURCE_LIMITS = %w[-limit memory 256MiB -limit disk 1GiB].freeze
11
+
12
+ class << self
13
+ def convert(file, command:)
14
+ file.rewind
15
+ png, status = pipe([command, *RESOURCE_LIMITS, '-[0]', 'png:-'], file)
16
+ raise UnsupportedFormat, "#{command} could not convert the image to PNG" unless status.success?
17
+
18
+ StringIO.new(png)
19
+ end
20
+
21
+ def image_magick?(command)
22
+ output, status = Open3.capture2e(command, '-version')
23
+ status.success? && output.include?('ImageMagick')
24
+ rescue SystemCallError
25
+ false
26
+ end
27
+
28
+ private
29
+
30
+ def pipe(args, input)
31
+ Open3.popen2(*args, err: File::NULL) do |stdin, stdout, wait|
32
+ [stdin, stdout].each(&:binmode)
33
+ writer = Thread.new { stream(input, stdin) }
34
+ png = stdout.read
35
+ writer.join
36
+ [png, wait.value]
37
+ end
38
+ end
39
+
40
+ def stream(input, stdin)
41
+ IO.copy_stream(input, stdin)
42
+ rescue Errno::EPIPE
43
+ nil
44
+ ensure
45
+ stdin.close
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TerminalImage
4
- VERSION = '0.3.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'terminal_image/version'
4
+ require 'terminal_image/ansi_encoder'
5
+ require 'terminal_image/png_converter'
6
+ require 'base64'
4
7
  require 'fastimage'
5
8
  require 'open-uri'
6
9
  require 'tempfile'
7
10
 
8
11
  module TerminalImage
9
12
  class UnsupportedTerminal < StandardError; end
13
+ class UnsupportedFormat < UnsupportedTerminal; end
14
+ class ImageTooLarge < UnsupportedTerminal; end
10
15
 
11
16
  class << self
12
17
  def show_url(url)
13
18
  tempfile = Tempfile.create
14
19
  tempfile.binmode
15
- URI.open(url) { |o| tempfile.write o.read }
20
+ URI.parse(url).open { |o| tempfile.write o.read }
16
21
  TerminalImage.show(tempfile)
17
22
  end
18
23
 
@@ -23,18 +28,17 @@ module TerminalImage
23
28
  def encode_url(url)
24
29
  tempfile = Tempfile.create
25
30
  tempfile.binmode
26
- URI.open(url) { |o| tempfile.write o.read }
31
+ URI.parse(url).open { |o| tempfile.write o.read }
27
32
  TerminalImage.encode(tempfile)
28
33
  end
29
34
 
30
35
  def encode(file)
31
- if ENV['TERM_PROGRAM'] == 'iTerm.app'
36
+ if ENV.fetch('TERM_PROGRAM', nil) == 'iTerm.app'
32
37
  encode_for_iterm2(file)
33
38
  elsif which 'img2sixel'
34
39
  encode_for_libsixel(file)
35
40
  else
36
- puts 'Use iTerm2 or install libsixel according to https://github.com/saitoha/libsixel#install'
37
- raise UnsupportedTerminal, 'Unsupported terminal'
41
+ encode_for_ansi(file)
38
42
  end
39
43
  end
40
44
 
@@ -42,13 +46,36 @@ module TerminalImage
42
46
 
43
47
  def encode_for_iterm2(file)
44
48
  width, height = FastImage.size(file)
45
- "\033]1337;File=inline=1;width=#{width}px;height=#{height}px:#{Base64.strict_encode64(file.read)}\a\n"
49
+ "\033]1337;File=inline=1;width=#{width}px;height=#{height}px:#{Base64.strict_encode64(file.read)}\a"
46
50
  end
47
51
 
48
52
  def encode_for_libsixel(file)
49
53
  `img2sixel < #{file.path} 2>/dev/null`
50
54
  end
51
55
 
56
+ def encode_for_ansi(file)
57
+ AnsiEncoder.encode(png_source(file))
58
+ end
59
+
60
+ def png_source(file)
61
+ type = FastImage.type(file)
62
+ return file if type == :png
63
+
64
+ command = image_magick_command
65
+ raise UnsupportedFormat, unsupported_format_message(type) unless command
66
+
67
+ AnsiEncoder.ensure_within_pixel_limit(*FastImage.size(file))
68
+ PngConverter.convert(file, command: command)
69
+ end
70
+
71
+ def image_magick_command
72
+ PngConverter::COMMANDS.find { |cmd| which(cmd) && PngConverter.image_magick?(cmd) }
73
+ end
74
+
75
+ def unsupported_format_message(type)
76
+ "Install ImageMagick to show #{type || 'non-PNG'} images without iTerm2 or libsixel"
77
+ end
78
+
52
79
  def which(cmd)
53
80
  exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
54
81
  ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
@@ -5,19 +5,20 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'terminal_image/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = 'terminal_image'
9
- spec.version = TerminalImage::VERSION
10
- spec.authors = ['Yuji Ueki']
11
- spec.email = ['unhappychoice@gmail.com']
8
+ spec.name = 'terminal_image'
9
+ spec.version = TerminalImage::VERSION
10
+ spec.required_ruby_version = '>= 3.3'
11
+ spec.authors = ['Yuji Ueki']
12
+ spec.email = ['unhappychoice@gmail.com']
13
+ spec.summary = 'Display images on terminal'
14
+ spec.description = 'Displays images on terminal'
15
+ spec.homepage = 'https://github.com/unhappychoice/terminal_image'
16
+ spec.license = 'MIT'
12
17
 
13
- spec.summary = 'Display images on terminal'
14
- spec.description = 'Displays images on terminal'
15
- spec.homepage = 'https://github.com/unhappychoice/terminal_image'
16
- spec.license = 'MIT'
17
-
18
- spec.metadata['homepage_uri'] = spec.homepage
19
- spec.metadata['source_code_uri'] = 'https://github.com/unhappychoice/terminal_image'
20
- spec.metadata['changelog_uri'] = 'https://github.com/unhappychoice/terminal_image'
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/unhappychoice/terminal_image'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/unhappychoice/terminal_image'
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
22
 
22
23
  # Specify which files should be added to the gem when it is released.
23
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -28,15 +29,7 @@ Gem::Specification.new do |spec|
28
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
30
  spec.require_paths = ['lib']
30
31
 
32
+ spec.add_dependency 'base64', '~> 0.3'
33
+ spec.add_dependency 'chunky_png', '~> 1.4'
31
34
  spec.add_dependency 'fastimage'
32
-
33
- spec.add_development_dependency 'bundler', '~> 2.0'
34
- spec.add_development_dependency 'codecov'
35
- spec.add_development_dependency 'guard'
36
- spec.add_development_dependency 'guard-rspec'
37
- spec.add_development_dependency 'guard-rubocop'
38
- spec.add_development_dependency 'rake', '~> 10.0'
39
- spec.add_development_dependency 'rspec', '~> 3.0'
40
- spec.add_development_dependency 'rubocop'
41
- spec.add_development_dependency 'simplecov'
42
35
  end
metadata CHANGED
@@ -1,149 +1,50 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Ueki
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2019-09-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: fastimage
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
13
+ name: base64
29
14
  requirement: !ruby/object:Gem::Requirement
30
15
  requirements:
31
16
  - - "~>"
32
17
  - !ruby/object:Gem::Version
33
- version: '2.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.0'
41
- - !ruby/object:Gem::Dependency
42
- name: codecov
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: guard
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: guard-rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: guard-rubocop
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: rake
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '10.0'
104
- type: :development
18
+ version: '0.3'
19
+ type: :runtime
105
20
  prerelease: false
106
21
  version_requirements: !ruby/object:Gem::Requirement
107
22
  requirements:
108
23
  - - "~>"
109
24
  - !ruby/object:Gem::Version
110
- version: '10.0'
25
+ version: '0.3'
111
26
  - !ruby/object:Gem::Dependency
112
- name: rspec
27
+ name: chunky_png
113
28
  requirement: !ruby/object:Gem::Requirement
114
29
  requirements:
115
30
  - - "~>"
116
31
  - !ruby/object:Gem::Version
117
- version: '3.0'
118
- type: :development
32
+ version: '1.4'
33
+ type: :runtime
119
34
  prerelease: false
120
35
  version_requirements: !ruby/object:Gem::Requirement
121
36
  requirements:
122
37
  - - "~>"
123
38
  - !ruby/object:Gem::Version
124
- version: '3.0'
125
- - !ruby/object:Gem::Dependency
126
- name: rubocop
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
39
+ version: '1.4'
139
40
  - !ruby/object:Gem::Dependency
140
- name: simplecov
41
+ name: fastimage
141
42
  requirement: !ruby/object:Gem::Requirement
142
43
  requirements:
143
44
  - - ">="
144
45
  - !ruby/object:Gem::Version
145
46
  version: '0'
146
- type: :development
47
+ type: :runtime
147
48
  prerelease: false
148
49
  version_requirements: !ruby/object:Gem::Requirement
149
50
  requirements:
@@ -157,8 +58,14 @@ executables: []
157
58
  extensions: []
158
59
  extra_rdoc_files: []
159
60
  files:
160
- - ".circleci/config.yml"
61
+ - ".github/CODEOWNERS"
62
+ - ".github/dependabot.yml"
63
+ - ".github/workflows/release.yaml"
64
+ - ".github/workflows/test.yaml"
65
+ - ".gitignore"
66
+ - ".rspec"
161
67
  - ".rubocop.yml"
68
+ - ".ruby-version"
162
69
  - CODE_OF_CONDUCT.md
163
70
  - Gemfile
164
71
  - Gemfile.lock
@@ -171,6 +78,8 @@ files:
171
78
  - images/example.png
172
79
  - images/sample.png
173
80
  - lib/terminal_image.rb
81
+ - lib/terminal_image/ansi_encoder.rb
82
+ - lib/terminal_image/png_converter.rb
174
83
  - lib/terminal_image/version.rb
175
84
  - terminal_image.gemspec
176
85
  homepage: https://github.com/unhappychoice/terminal_image
@@ -180,7 +89,7 @@ metadata:
180
89
  homepage_uri: https://github.com/unhappychoice/terminal_image
181
90
  source_code_uri: https://github.com/unhappychoice/terminal_image
182
91
  changelog_uri: https://github.com/unhappychoice/terminal_image
183
- post_install_message:
92
+ rubygems_mfa_required: 'true'
184
93
  rdoc_options: []
185
94
  require_paths:
186
95
  - lib
@@ -188,15 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
97
  requirements:
189
98
  - - ">="
190
99
  - !ruby/object:Gem::Version
191
- version: '0'
100
+ version: '3.3'
192
101
  required_rubygems_version: !ruby/object:Gem::Requirement
193
102
  requirements:
194
103
  - - ">="
195
104
  - !ruby/object:Gem::Version
196
105
  version: '0'
197
106
  requirements: []
198
- rubygems_version: 3.0.3
199
- signing_key:
107
+ rubygems_version: 3.6.7
200
108
  specification_version: 4
201
109
  summary: Display images on terminal
202
110
  test_files: []
data/.circleci/config.yml DELETED
@@ -1,61 +0,0 @@
1
- version: 2.1
2
-
3
- update_bundler: &update_bundler
4
- run:
5
- name: update bundler
6
- command: gem update bundler
7
-
8
- bundle_install: &bundle_install
9
- run:
10
- name: bundle install
11
- command: bundle update --bundler && bundle install --path vendor/bundle --jobs 4
12
-
13
- restore_bundle_cache: &restore_bundle_cache
14
- restore_cache:
15
- key: cache-bundler-{{ checksum "Gemfile.lock" }}
16
-
17
- jobs:
18
- build:
19
- docker:
20
- - image: circleci/ruby
21
- steps:
22
- - checkout
23
- - *restore_bundle_cache
24
- - *update_bundler
25
- - *bundle_install
26
- - save_cache:
27
- key: cache-bundler-{{ checksum "Gemfile.lock" }}
28
- paths:
29
- - vendor/bundle
30
- rubocop:
31
- docker:
32
- - image: circleci/ruby
33
- steps:
34
- - checkout
35
- - *restore_bundle_cache
36
- - *update_bundler
37
- - *bundle_install
38
- - run: bundle exec rubocop
39
- rspec:
40
- docker:
41
- - image: circleci/ruby
42
- steps:
43
- - checkout
44
- - *restore_bundle_cache
45
- - *update_bundler
46
- - *bundle_install
47
- - run:
48
- environment:
49
- RAILS_ENV: test
50
- COVERAGE: true
51
- CODECOV_TOKEN: d9091f65-2e14-4029-86e6-f6ec9c59ecec
52
- command: bundle exec rspec
53
- workflows:
54
- version: 2.1
55
- rspec:
56
- jobs:
57
- - build
58
- - rubocop:
59
- requires: [build]
60
- - rspec:
61
- requires: [build]