pixelchart 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: adb12fcdae7b3b9b31e255e10e95a6eb060cf7e9ef08fa900b3ec141468660d9
4
+ data.tar.gz: 485112259502b71d2c83db71d3e90cf2937bdfcba7d3faac24488ecc7d89eb0b
5
+ SHA512:
6
+ metadata.gz: 55509fb90bc7113e81f135f630eec895106a9bb903549b24d6d1dab9229d8c229d023840eec93fa3aa57bd1590d8c19216e1e640ebb987cd230994edd5cf7ddf
7
+ data.tar.gz: bccbb03b7030e8ca398255f1dd8e91f7d3ede15551449460f071dbdff36d20d34f768536f527de0fabb4aef142ce8a87a5316b37c78843c56de4692e8bcb1d67
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019-2020 Alexandre ZANNI
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.
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Ruby internal
5
+ require 'pp'
6
+ # Project internal
7
+ require 'pixelchart'
8
+ # External
9
+ require 'docopt'
10
+ require 'paint'
11
+
12
+ doc = <<~DOCOPT
13
+ PixelChart - Map binary data into a beautiful chart
14
+
15
+ Usage:
16
+ pixelchart draw <input> <output> (--width <pixel> --height <pixel>) [--colors <colors>] [--backend <name>] [--scale <ratio>] [--no-color] [--debug]
17
+ pixelchart calc (--area <size>|<input>) [--no-color] [--debug]
18
+ pixelchart -H | --help
19
+ pixelchart -V | --version
20
+
21
+ Options:
22
+ <input> Input file containing values
23
+ <output> Output image (filename)
24
+ -w <pixel>, --width <pixel> Width of the output image (in number pixel)
25
+ -h <pixel>, --height <pixel> Height of the output image (in number pixel)
26
+ -c <colors>, --colors <colors> Colors of the image (in RGB or random)
27
+ -b <name>, --backend <name> Image processing backend (rmagick or ruby-vips)
28
+ -s <ratio>, --scale <ratio> Scale ratio or dimensions
29
+ --area <size> Area, number of values, total number of pixel
30
+ --no-color Disable colorized output
31
+ --debug Display arguments
32
+ -H, --help Show this screen
33
+ -V, --version Show version
34
+
35
+ Examples:
36
+ pixelchart draw test.csv test.png -w 100 -h 100
37
+ pixelchart draw test.csv test.png -w 100 -h 100 -c 'random|125,125,125' -b rubyvips
38
+ pixelchart calc test.csv
39
+ pixelchart calc --area 10000 --no-color
40
+ DOCOPT
41
+
42
+ begin
43
+ args = Docopt.docopt(doc, version: PixelChart::VERSION)
44
+ pp args if args['--debug']
45
+ Paint.mode = 0 if args['--no-color']
46
+ if args['draw']
47
+ data = PixelChart.load_file(args['<input>'])
48
+ width = args['--width'].to_i
49
+ height = args['--height'].to_i
50
+ filename = args['<output>']
51
+ opts = {}
52
+ if args['--colors']
53
+ colors = args['--colors'].split('|')
54
+ colors.each_with_index do |col, i|
55
+ colors[i] = if col == 'random'
56
+ col.to_sym
57
+ else
58
+ colors[i] = col.split(',').map(&:to_i)
59
+ end
60
+ end
61
+ opts[:colors] = colors
62
+ end
63
+ opts[:backend] = args['--backend'].to_sym if args['--backend']
64
+ if args['--scale']
65
+ opts[:scale] = if /,/.match?(args['--scale']) # dimensions
66
+ args['--scale'].split(',').map(&:to_i)
67
+ else # ratio
68
+ args['--scale'].to_f
69
+ end
70
+ end
71
+ pc = PixelChart.new(data, width, height, opts)
72
+ pc.draw(filename, opts)
73
+ puts Paint['[+]', :green] + ' Image saved'
74
+ elsif args['calc']
75
+ dimensions = nil
76
+ if args['--area']
77
+ dimensions = PixelChart.dimensions(args['--area'].to_i)
78
+ elsif args['<input>']
79
+ data = PixelChart.load_file(args['<input>'])
80
+ dimensions = PixelChart.dimensions(data.size)
81
+ end
82
+ puts 'Possible dimensions: width x height or height x width'
83
+ dimensions.each do |xy|
84
+ puts Paint[xy[0], :magenta] + ' x ' + Paint[xy[1], :magenta]
85
+ end
86
+ end
87
+ rescue Docopt::Exit => e
88
+ puts e.message
89
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ruby internal
4
+ require 'set'
5
+ # Project internal
6
+ require 'pixelchart/version'
7
+ # External
8
+ require 'rmagick'
9
+ require 'vips'
10
+
11
+ # PixelChart class
12
+ class PixelChart
13
+ # Constants
14
+ include Version
15
+
16
+ # A new instance of PixelChart
17
+ # @param data [Array<Boolean>] An array containing values
18
+ # (0, 1) used to generate the image.
19
+ # @param width [Integer] Desired width of the image.
20
+ # @param height [Integer] Desired height of the image.
21
+ # @option opts [Array<Color>] :colors Must be an array containing 2 values.
22
+ # Each value is either the +:random+ symbol to get a random color or a
23
+ # color given as RGB, so 3 integers in an array.
24
+ # @option opts [Scale] :scale Scale the image to dimensions or with a ratio.
25
+ # Ratio must be a positive (non-nul) float or integer, and dimensions must
26
+ # be a set of width and height separated by a comma.
27
+ def initialize(data, width, height, opts = {})
28
+ @width = check_int(width)
29
+ @height = check_int(height)
30
+ @data = check_data(data)
31
+ opts[:colors] ||= [[255, 255, 255], [0, 0, 0]] # white, black
32
+ check_colors(opts[:colors])
33
+ opts[:colors].each_with_index do |color, i|
34
+ opts[:colors][i] = (0..2).map { |_x| rand(256) } if color == :random
35
+ end
36
+ @colors = opts[:colors]
37
+ opts[:scale] ||= nil
38
+ @scale = opts[:scale]
39
+ end
40
+
41
+ # Generate and save the image
42
+ # @param filename [String] Name of the output image.
43
+ # @param opts [Hash] options for image processing
44
+ # @option opts [Symbol] :backend Image processing backend, +:rmagick+ for
45
+ # Rmagick/Imagemagick or +:rubyvips+ for ruby-vips/libvips.
46
+ def draw(filename, opts = {})
47
+ opts[:backend] ||= :rmagick
48
+ backend = check_backend(opts[:backend])
49
+ if backend == :rmagick
50
+ draw_rmagick(filename)
51
+ elsif backend == :rubyvips
52
+ draw_rubyvips(filename)
53
+ end
54
+ end
55
+
56
+ # Calculate the possible dimensions (width, height) for a given area
57
+ # @param area [Integer] number of values, total of pixels
58
+ # @return [Array<Array<Integer>>] Array of possible dimensions
59
+ def self.dimensions(area)
60
+ dim = []
61
+ (1..Math.sqrt(area).to_i).each do |x|
62
+ y, rem = area.divmod(x)
63
+ dim.push([x, y]) if rem.zero?
64
+ end
65
+ dim
66
+ end
67
+
68
+ # Calculate the possible dimensions (width, height) for the data of the object
69
+ # @return [Array<Array<Integer>>] Array of possible dimensions
70
+ def dimensions
71
+ dimensions(@data.size)
72
+ end
73
+
74
+ # Read the input file to extract the values
75
+ # @param filename [String] Name of the input file containing data. Values must
76
+ # be 0, 1, true, false and separated by commas.
77
+ # @return [Array<Integer>] Array of 0, 1. Can be directly passed to initialize
78
+ def self.load_file(filename)
79
+ content = File.read(filename).gsub(/\s+/, '')
80
+ data = content.split(',')
81
+ data.each_with_index do |x, i|
82
+ data[i] = 0 if x == '0' || x.downcase == 'false'
83
+ data[i] = 1 if x == '1' || x.downcase == 'true'
84
+ end
85
+ data
86
+ end
87
+
88
+ private
89
+
90
+ # Generate and save the image with Rmagick
91
+ # @param filename [String] Name of the output image.
92
+ def draw_rmagick(filename)
93
+ img = Magick::Image.new(@width, @height)
94
+ i = 0
95
+ (0...@height).each do |y|
96
+ (0...@width).each do |x|
97
+ if [1, true].include?(@data[i]) # true color
98
+ img.pixel_color(x, y, "rgb(#{@colors[1].join(',')}")
99
+ else # false color
100
+ img.pixel_color(x, y, "rgb(#{@colors[0].join(',')})")
101
+ end
102
+ i += 1
103
+ end
104
+ end
105
+ unless @scale.nil?
106
+ if @scale.is_a?(Numeric)
107
+ if @scale.zero?
108
+ raise(ArgumentError, 'Scale must be a ratio or a set of width and height')
109
+ end
110
+
111
+ img.scale!(@scale)
112
+ elsif @scale.is_a?(Array)
113
+ img.scale!(@scale[0], @scale[1])
114
+ else
115
+ raise(ArgumentError, 'Scale must be a ratio or a set of width and height')
116
+ end
117
+ end
118
+ img.write(filename)
119
+ end
120
+
121
+ # Generate and save the image ruby-vips
122
+ # @param filename [String] Name of the output image.
123
+ def draw_rubyvips(filename)
124
+ # true -> 1, false -> 0
125
+ @data.map! { |x| x == true ? 1 : x }
126
+ @data.map! { |x| x == false ? 0 : x }
127
+ # make a 2D image from the data array
128
+ im = Vips::Image.new_from_array(@data.each_slice(@width).to_a)
129
+ im = (im == 1).ifthenelse(@colors[1], @colors[0], blend: true)
130
+ if @scale.is_a?(Numeric)
131
+ opts = { kernel: :nearest }
132
+ im = im.resize(@scale, **opts)
133
+ end
134
+ im.write_to_file(filename, compression: 9, palette: true, colours: 2, Q: 0)
135
+ end
136
+
137
+ # Check if the argument is an integer else raise an error
138
+ # @param data [Object] The object to check
139
+ # @return [Object] Untouched input object
140
+ def check_int(data)
141
+ unless data.is_a?(Integer)
142
+ raise(ArgumentError, 'Argument is not an integer')
143
+ end
144
+
145
+ data
146
+ end
147
+
148
+ # Check if the data is in the format expected by #initialize else raise an
149
+ # error
150
+ # @param data [Object] The object to check
151
+ # @return [Object] Untouched input object
152
+ def check_data(data)
153
+ raise(ArgumentError, 'Argument is not an array') unless data.is_a?(Array)
154
+
155
+ possible_values = PixelChart.dimensions(data.size)
156
+ possible_sets = possible_values.map(&:to_set)
157
+
158
+ unless possible_sets.include?(Set[@width, @height])
159
+ raise(ArgumentError, 'The number of values does not match with the possible dimensions')
160
+ end
161
+
162
+ data
163
+ end
164
+
165
+ # Check if the colors are in the format expected by #initialize else raise an
166
+ # error
167
+ # @param data [Object] The object to check
168
+ # @return [Object] Untouched input object
169
+ def check_colors(data)
170
+ raise(ArgumentError, 'Argument is not an array') unless data.is_a?(Array)
171
+
172
+ data.each do |item|
173
+ if item == :random
174
+ # nothing
175
+ elsif item.is_a?(Array)
176
+ raise(ArgumentError, 'RGB must contains 3 values') unless item.size == 3
177
+
178
+ item.each do |i|
179
+ unless i.is_a?(Integer)
180
+ raise(ArgumentError, 'RGB values are not integers')
181
+ end
182
+ unless (0..255).include?(i)
183
+ raise(ArgumentError, 'RGB values must be between 0 and 255')
184
+ end
185
+ end
186
+ else
187
+ raise(ArgumentError, 'Colors are not a RGB array or :random')
188
+ end
189
+ end
190
+ data
191
+ end
192
+
193
+ # Check if the backend option is in the format expected by #draw else raise
194
+ # an error
195
+ # @param data [Object] The object to check
196
+ # @return [Object] Untouched input object
197
+ def check_backend(data)
198
+ accepted_values = %i[rmagick rubyvips]
199
+ unless accepted_values.include?(data)
200
+ raise(ArgumentError, "Backend must be #{accepted_values.join(' or ')}")
201
+ end
202
+
203
+ data
204
+ end
205
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Version
4
+ VERSION = '1.0.0'
5
+ end
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pixelchart
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexandre ZANNI
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: docopt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: paint
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rmagick
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-vips
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: commonmarker
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.21'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.21'
97
+ - !ruby/object:Gem::Dependency
98
+ name: github-markup
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.12'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.12'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '13.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '13.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: redcarpet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.5'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.5'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rubocop
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.80'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.80'
167
+ - !ruby/object:Gem::Dependency
168
+ name: yard
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.9'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.9'
181
+ description: Create a binary pixel map
182
+ email: alexandre.zanni@engineer.com
183
+ executables:
184
+ - pixelchart
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - LICENSE.txt
189
+ - bin/pixelchart
190
+ - lib/pixelchart.rb
191
+ - lib/pixelchart/version.rb
192
+ homepage:
193
+ licenses:
194
+ - MIT
195
+ metadata: {}
196
+ post_install_message:
197
+ rdoc_options: []
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - "~>"
203
+ - !ruby/object:Gem::Version
204
+ version: '2.4'
205
+ required_rubygems_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ requirements: []
211
+ rubygems_version: 3.1.2
212
+ signing_key:
213
+ specification_version: 4
214
+ summary: Create a binary pixel map
215
+ test_files: []