spark_pr 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/.gitignore +11 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +21 -0
- data/README +55 -0
- data/Rakefile +10 -0
- data/lib/spark_pr.rb +4 -0
- data/lib/spark_pr/spark.rb +117 -0
- data/lib/spark_pr/spark_canvas.rb +115 -0
- data/lib/spark_pr/version.rb +3 -0
- data/spark_pr.gemspec +25 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2ae3c06cbc2f596478b629cbe0cb1633f69588ba
|
4
|
+
data.tar.gz: 74c01a3c6803ab55cd31525efe57ac7e698232d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b7409d7d0e046cb2a45b1396ea6339e4aaf56a231057df192a62680f0b022e399bcc73726bd3b53e992f18bc0aa069c5ebbcd3ba01000afb780325c50ba7ab43
|
7
|
+
data.tar.gz: 5085572f5d9a30797ddfb0abe294b26649403081516916379b46192e64ea88877b239e41eed635ffbc42bd3dbabe9036a5790e838fa507e0d4302828103198a9
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2005 Thomas Fuchs
|
2
|
+
http://mir.aculo.us/
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
spark_pr is a Ruby class to generate sparkline graphs with PNG or ASCII output.
|
2
|
+
|
3
|
+
It only depends on zlib and generates PNGs with pure Ruby code.
|
4
|
+
The line-graph outputs antialised lines.
|
5
|
+
|
6
|
+
Example for PNG output:
|
7
|
+
|
8
|
+
File.open( 'test.png', 'wb' ) do |png|
|
9
|
+
png << Spark.plot( [47, 43, 24, 47, 16, 28, 38, 57, 50, 76,
|
10
|
+
42, 20, 98, 34, 53, 1, 55, 74, 63, 38,
|
11
|
+
31, 98, 89],
|
12
|
+
:has_min => true, :has_max => true, 'has_last' => 'true',
|
13
|
+
'height' => '40', :step => 10, :normalize => 'logarithmic' )
|
14
|
+
end
|
15
|
+
|
16
|
+
Example ASCII output:
|
17
|
+
|
18
|
+
spark = Spark.new({:has_min => true, :has_max => true, :height => 14, :step => 4})
|
19
|
+
puts spark.smooth( [47, 43, 24, 47, 16, 28, 38, 57, 50, 76,
|
20
|
+
42, 1, 98, 34, 53, 97, 55, 74, 63, 38,
|
21
|
+
31, 98, 89] ).to_ascii
|
22
|
+
|
23
|
+
The PNG output can also be obtained in data: URI format convenient for web
|
24
|
+
use. If the Spark.data_uri is used, there is also a dependency on Base64,
|
25
|
+
which is a standard library just like Zlib
|
26
|
+
|
27
|
+
You can paste the output from:
|
28
|
+
|
29
|
+
Spark.data_uri( [47, 43, 24, 47, 16, 28, 38, 57, 50, 76,
|
30
|
+
42, 20, 98, 34, 53, 1, 55, 74, 63, 38,
|
31
|
+
31, 98, 89],
|
32
|
+
:has_min => true, :has_max => true, 'has_last' => 'true',
|
33
|
+
'height' => '40', :step => 10, :normalize => 'logarithmic' )
|
34
|
+
|
35
|
+
into your browser's address and see the PNG directly.
|
36
|
+
|
37
|
+
Class based usage:
|
38
|
+
|
39
|
+
spark = Spark.new(:has_min => true, :has_max => true, 'has_last' => 'true',
|
40
|
+
'height' => '40', :step => 10, :normalize => 'logarithmic' )
|
41
|
+
spark.smooth(data)
|
42
|
+
|
43
|
+
File.open( 'test.png', 'wb' ) do |png|
|
44
|
+
png << spark.png
|
45
|
+
end
|
46
|
+
|
47
|
+
img_src = spark.data_uri
|
48
|
+
|
49
|
+
|
50
|
+
The SparkCanvas class can also be used for other drawing operations,
|
51
|
+
it provides drawing canvas with alpha blending and some primitive graphics
|
52
|
+
operations and PNG output in just about 100 lines of Ruby code.
|
53
|
+
|
54
|
+
Pure Ruby sparklines are released under the terms of the MIT-LICENSE.
|
55
|
+
See the included MIT-LICENSE file for details.
|
data/Rakefile
ADDED
data/lib/spark_pr.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require_relative 'spark_canvas'
|
3
|
+
|
4
|
+
class Spark
|
5
|
+
attr_accessor :opts, :png, :canvas
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
# process options
|
9
|
+
o = options.inject({}) do |o, (key, value)|
|
10
|
+
o[key.to_sym] = value ; o
|
11
|
+
end
|
12
|
+
[:height, :width, :step].each do |k|
|
13
|
+
o[k] = o[k].to_i if o.has_key?(k)
|
14
|
+
end
|
15
|
+
[:has_min, :has_max, :has_last].each do |k|
|
16
|
+
o[k] = (o[k] ? true : false) if o.has_key?(k)
|
17
|
+
end
|
18
|
+
o[:normalize] ||= :linear
|
19
|
+
o[:normalize] = o[:normalize].to_sym
|
20
|
+
|
21
|
+
self.opts = o
|
22
|
+
end
|
23
|
+
|
24
|
+
# normalize arr to contain values between 0..1 inclusive
|
25
|
+
def normalize( arr, type = :linear )
|
26
|
+
arr = arr.map{|v| Math.log(v) } if type == :logarithmic
|
27
|
+
adj, fac = arr.min, arr.max-arr.min
|
28
|
+
arr.map {|v| (v-adj).quo(fac) rescue 0 }
|
29
|
+
end
|
30
|
+
|
31
|
+
def smooth( results )
|
32
|
+
o = {
|
33
|
+
:step => 2,
|
34
|
+
:height => 14,
|
35
|
+
:has_min => false,
|
36
|
+
:has_max => false
|
37
|
+
}.merge(opts)
|
38
|
+
|
39
|
+
o[:width] ||= (results.size-1)*o[:step] + 5
|
40
|
+
|
41
|
+
c = SparkCanvas.new(o[:width], o[:height])
|
42
|
+
|
43
|
+
results = normalize(results, o[:normalize])
|
44
|
+
fac = c.height-5
|
45
|
+
i = -o[:step]
|
46
|
+
coords = results.map do |r|
|
47
|
+
[(i += o[:step])+2, c.height - 3 - r*fac ]
|
48
|
+
end
|
49
|
+
|
50
|
+
c.color = opts[:line_color] || [0xB0, 0xB0, 0xB0, 0xFF]
|
51
|
+
c.polyline coords
|
52
|
+
|
53
|
+
if o[:has_min]
|
54
|
+
min_pt = coords[results.index(results.min)]
|
55
|
+
c.color = [0x80, 0x80, 0x00, 0x70]
|
56
|
+
c.rectangle(min_pt[0]-2, min_pt[1]-2, min_pt[0]+2, min_pt[1]+2)
|
57
|
+
end
|
58
|
+
|
59
|
+
if o[:has_max]
|
60
|
+
max_pt = coords[results.index(results.max)]
|
61
|
+
c.color = [0x00, 0x80, 0x00, 0x70]
|
62
|
+
c.rectangle(max_pt[0]-2, max_pt[1]-2, max_pt[0]+2, max_pt[1]+2)
|
63
|
+
end
|
64
|
+
|
65
|
+
if o[:has_last]
|
66
|
+
c.color = [0xFF, 0x00, 0x00, 0x70]
|
67
|
+
c.rectangle(coords.last[0]-2, coords.last[1]-2, coords.last[0]+2, coords.last[1]+2)
|
68
|
+
end
|
69
|
+
self.png = c.to_png
|
70
|
+
self.canvas = c
|
71
|
+
end
|
72
|
+
|
73
|
+
def discrete( results )
|
74
|
+
o = {
|
75
|
+
:height => 14,
|
76
|
+
:upper => 0.5,
|
77
|
+
:has_min => false,
|
78
|
+
:has_max => false
|
79
|
+
}.merge(opts)
|
80
|
+
|
81
|
+
o[:width] ||= results.size*2-1
|
82
|
+
|
83
|
+
c = SparkCanvas.new(o[:width], o[:height])
|
84
|
+
|
85
|
+
results = normalize(results, o[:normalize])
|
86
|
+
fac = c.height-4
|
87
|
+
|
88
|
+
i = -2
|
89
|
+
results.each do |r|
|
90
|
+
p = c.height - 4 - r*fac
|
91
|
+
c.color = r < o[:upper] ? [0x66,0x66,0x66,0xFF] : [0xFF,0x00,0x00,0xFF]
|
92
|
+
c.line(i+=2, p, i, p+3)
|
93
|
+
end
|
94
|
+
|
95
|
+
self.png = c.to_png
|
96
|
+
self.canvas = c
|
97
|
+
end
|
98
|
+
|
99
|
+
def data_uri
|
100
|
+
%{data:image/png;base64,#{Base64.encode64(png).gsub("\n",'')}}
|
101
|
+
end
|
102
|
+
|
103
|
+
# convenience methods
|
104
|
+
def self.plot( results, options = {} )
|
105
|
+
spark = Spark.new(options)
|
106
|
+
type = opts.delete(:type) || :smooth
|
107
|
+
spark.send(type, results, o).to_png
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.data_uri( results, options = {} )
|
111
|
+
spark = Spark.new(options)
|
112
|
+
type = spark.opts.delete(:type) || :smooth
|
113
|
+
spark.send(type, results )
|
114
|
+
spark.data_uri
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# pure ruby sparklines module, generates PNG or ASCII
|
2
|
+
# contact thomas@fesch.at for questions
|
3
|
+
#
|
4
|
+
# strives to be somewhat compatible with sparklines lib by
|
5
|
+
# {Dan Nugent}[mailto:nugend@gmail.com] and {Geoffrey Grosenbach}[mailto:boss@topfunky.com]
|
6
|
+
#
|
7
|
+
# png creation based on http://www.whytheluckystiff.net/bumpspark/
|
8
|
+
|
9
|
+
require 'zlib'
|
10
|
+
|
11
|
+
class SparkCanvas
|
12
|
+
|
13
|
+
attr_accessor :color
|
14
|
+
attr_reader :width, :height
|
15
|
+
|
16
|
+
def initialize(width,height)
|
17
|
+
@canvas = []
|
18
|
+
@height = height
|
19
|
+
@width = width
|
20
|
+
height.times{ @canvas << [[0xFF,0xFF,0xFF]]*width }
|
21
|
+
@color = [0,0,0,0xFF] #RGBA
|
22
|
+
end
|
23
|
+
|
24
|
+
# alpha blends two colors, using the alpha given by c2
|
25
|
+
def blend(c1, c2)
|
26
|
+
(0..2).map{ |i| (c1[i]*(0xFF-c2[3]) + c2[i]*c2[3]) >> 8 }
|
27
|
+
end
|
28
|
+
|
29
|
+
# calculate a new alpha given a 0-0xFF intensity
|
30
|
+
def intensity(c,i)
|
31
|
+
[c[0],c[1],c[2],(c[3]*i) >> 8]
|
32
|
+
end
|
33
|
+
|
34
|
+
# calculate perceptive grayscale value
|
35
|
+
def grayscale(c)
|
36
|
+
(c[0]*0.3 + c[1]*0.59 + c[2]*0.11).to_i
|
37
|
+
end
|
38
|
+
|
39
|
+
def point(x,y,color = nil)
|
40
|
+
return if x<0 or y<0 or x>@width-1 or y>@height-1
|
41
|
+
@canvas[y][x] = blend(@canvas[y][x], color || @color)
|
42
|
+
end
|
43
|
+
|
44
|
+
def rectangle(x0, y0, x1, y1)
|
45
|
+
x0, y0, x1, y1 = x0.to_i, y0.to_i, x1.to_i, y1.to_i
|
46
|
+
x0, x1 = x1, x0 if x0 > x1
|
47
|
+
y0, y1 = y1, y0 if y0 > y1
|
48
|
+
x0.upto(x1) { |x| y0.upto(y1) { |y| point x, y } }
|
49
|
+
end
|
50
|
+
|
51
|
+
# draw an antialiased line
|
52
|
+
# google for "wu antialiasing"
|
53
|
+
def line(x0, y0, x1, y1)
|
54
|
+
# clean params
|
55
|
+
x0, y0, x1, y1 = x0.to_i, y0.to_i, x1.to_i, y1.to_i
|
56
|
+
y0, y1, x0, x1 = y1, y0, x1, x0 if y0>y1
|
57
|
+
sx = (dx = x1-x0) < 0 ? -1 : 1 ; dx *= sx ; dy = y1-y0
|
58
|
+
|
59
|
+
# special cases
|
60
|
+
x0.step(x1,sx) { |x| point x, y0 } and return if dy.zero?
|
61
|
+
y0.upto(y1) { |y| point x0, y } and return if dx.zero?
|
62
|
+
x0.step(x1,sx) { |x| point x, y0; y0 += 1 } and return if dx==dy
|
63
|
+
|
64
|
+
# main loops
|
65
|
+
point x0, y0
|
66
|
+
|
67
|
+
e_acc = 0
|
68
|
+
if dy > dx
|
69
|
+
e = (dx << 16) / dy
|
70
|
+
y0.upto(y1-1) do
|
71
|
+
e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF
|
72
|
+
x0 += sx if (e_acc <= e_acc_temp)
|
73
|
+
point x0, (y0 += 1), intensity(@color,(w=0xFF-(e_acc >> 8)))
|
74
|
+
point x0+sx, y0, intensity(@color,(0xFF-w))
|
75
|
+
end
|
76
|
+
point x1, y1
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
e = (dy << 16) / dx
|
81
|
+
x0.step(x1-sx,sx) do
|
82
|
+
e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF
|
83
|
+
y0 += 1 if (e_acc <= e_acc_temp)
|
84
|
+
point (x0 += sx), y0, intensity(@color,(w=0xFF-(e_acc >> 8)))
|
85
|
+
point x0, y0+1, intensity(@color,(0xFF-w))
|
86
|
+
end
|
87
|
+
point x1, y1
|
88
|
+
end
|
89
|
+
|
90
|
+
def polyline(arr)
|
91
|
+
(0...arr.size-1).each{ |i| line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1]) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_png
|
95
|
+
header = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
|
96
|
+
raw_data = @canvas.map { |row| [0] + row }.flatten.pack("C*")
|
97
|
+
ihdr_data = [@canvas.first.length,@canvas.length,8,2,0,0,0].pack("NNCCCCC")
|
98
|
+
|
99
|
+
header +
|
100
|
+
build_png_chunk("IHDR", ihdr_data) +
|
101
|
+
build_png_chunk("tRNS", ([ 0xFF ]*6).pack("C6")) +
|
102
|
+
build_png_chunk("IDAT", Zlib::Deflate.deflate(raw_data)) +
|
103
|
+
build_png_chunk("IEND", "")
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_png_chunk(type,data)
|
107
|
+
to_check = type + data
|
108
|
+
[data.length].pack("N") + to_check + [Zlib.crc32(to_check)].pack("N")
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_ascii
|
112
|
+
chr = %w(M O # + ; - .) << ' '
|
113
|
+
@canvas.map{ |r| r.map { |pt| chr[grayscale(pt) >> 5] }.join << "\n" }.join
|
114
|
+
end
|
115
|
+
end
|
data/spark_pr.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'spark_pr/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "spark_pr"
|
8
|
+
spec.version = SparkPr::VERSION
|
9
|
+
spec.authors = ["Thomas Fuchs", "Rob Biedenharn", "Lukas Eklund"]
|
10
|
+
spec.email = ["leklund@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Pure Ruby sparkline graph generator with PNG or ASCII output}
|
13
|
+
spec.description = %q{spark_pr is a Ruby class to generate sparkline graphs with PNG or ASCII output. It only depends on zlib and generates PNGs with pure Ruby code. The line-graph outputs antialised lines.}
|
14
|
+
spec.homepage = "https://github.com/leklund/spark_pr"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spark_pr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Fuchs
|
8
|
+
- Rob Biedenharn
|
9
|
+
- Lukas Eklund
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2016-03-30 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: bundler
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - "~>"
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.10'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - "~>"
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '1.10'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rake
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - "~>"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '10.0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '10.0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: minitest
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
description: spark_pr is a Ruby class to generate sparkline graphs with PNG or ASCII
|
58
|
+
output. It only depends on zlib and generates PNGs with pure Ruby code. The line-graph
|
59
|
+
outputs antialised lines.
|
60
|
+
email:
|
61
|
+
- leklund@gmail.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- ".gitignore"
|
67
|
+
- Gemfile
|
68
|
+
- MIT-LICENSE
|
69
|
+
- README
|
70
|
+
- Rakefile
|
71
|
+
- lib/spark_pr.rb
|
72
|
+
- lib/spark_pr/spark.rb
|
73
|
+
- lib/spark_pr/spark_canvas.rb
|
74
|
+
- lib/spark_pr/version.rb
|
75
|
+
- spark_pr.gemspec
|
76
|
+
homepage: https://github.com/leklund/spark_pr
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.4.6
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Pure Ruby sparkline graph generator with PNG or ASCII output
|
100
|
+
test_files: []
|