tb 0.1 → 0.2
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.
- data/README +156 -5
- data/bin/tb +2 -1110
- data/lib/tb.rb +4 -2
- data/lib/tb/catreader.rb +131 -0
- data/lib/tb/cmd_cat.rb +65 -0
- data/lib/tb/cmd_consecutive.rb +79 -0
- data/lib/tb/cmd_crop.rb +105 -0
- data/lib/tb/cmd_cross.rb +119 -0
- data/lib/tb/cmd_csv.rb +42 -0
- data/lib/tb/cmd_cut.rb +77 -0
- data/lib/tb/cmd_grep.rb +76 -0
- data/lib/tb/cmd_group.rb +82 -0
- data/lib/tb/cmd_gsub.rb +77 -0
- data/lib/tb/cmd_help.rb +98 -0
- data/lib/tb/cmd_join.rb +81 -0
- data/lib/tb/cmd_json.rb +60 -0
- data/lib/tb/cmd_ls.rb +273 -0
- data/lib/tb/cmd_mheader.rb +77 -0
- data/lib/tb/cmd_newfield.rb +59 -0
- data/lib/tb/cmd_pnm.rb +43 -0
- data/lib/tb/cmd_pp.rb +70 -0
- data/lib/tb/cmd_rename.rb +58 -0
- data/lib/tb/cmd_shape.rb +67 -0
- data/lib/tb/cmd_sort.rb +58 -0
- data/lib/tb/cmd_svn_log.rb +158 -0
- data/lib/tb/cmd_tsv.rb +43 -0
- data/lib/tb/cmd_yaml.rb +47 -0
- data/lib/tb/cmdmain.rb +45 -0
- data/lib/tb/cmdtop.rb +58 -0
- data/lib/tb/cmdutil.rb +327 -0
- data/lib/tb/csv.rb +30 -6
- data/lib/tb/fieldset.rb +39 -41
- data/lib/tb/pager.rb +132 -0
- data/lib/tb/pnm.rb +357 -0
- data/lib/tb/reader.rb +18 -128
- data/lib/tb/record.rb +3 -3
- data/lib/tb/ropen.rb +70 -0
- data/lib/tb/{pathfinder.rb → search.rb} +69 -34
- data/lib/tb/tsv.rb +29 -1
- data/sample/colors.ppm +0 -0
- data/sample/gradation.pgm +0 -0
- data/sample/langs.csv +46 -0
- data/sample/tbplot +293 -0
- data/test-all-cov.rb +65 -0
- data/test-all.rb +5 -0
- data/test/test_basic.rb +99 -2
- data/test/test_catreader.rb +27 -0
- data/test/test_cmd_cat.rb +118 -0
- data/test/test_cmd_consecutive.rb +90 -0
- data/test/test_cmd_crop.rb +101 -0
- data/test/test_cmd_cross.rb +113 -0
- data/test/test_cmd_csv.rb +129 -0
- data/test/test_cmd_cut.rb +100 -0
- data/test/test_cmd_grep.rb +89 -0
- data/test/test_cmd_group.rb +181 -0
- data/test/test_cmd_gsub.rb +103 -0
- data/test/test_cmd_help.rb +190 -0
- data/test/test_cmd_join.rb +197 -0
- data/test/test_cmd_json.rb +75 -0
- data/test/test_cmd_ls.rb +203 -0
- data/test/test_cmd_mheader.rb +86 -0
- data/test/test_cmd_newfield.rb +63 -0
- data/test/test_cmd_pnm.rb +35 -0
- data/test/test_cmd_pp.rb +62 -0
- data/test/test_cmd_rename.rb +91 -0
- data/test/test_cmd_shape.rb +50 -0
- data/test/test_cmd_sort.rb +105 -0
- data/test/test_cmd_tsv.rb +67 -0
- data/test/test_cmd_yaml.rb +55 -0
- data/test/test_cmdtty.rb +154 -0
- data/test/test_cmdutil.rb +43 -0
- data/test/test_csv.rb +10 -0
- data/test/test_fieldset.rb +42 -0
- data/test/test_pager.rb +142 -0
- data/test/test_pnm.rb +374 -0
- data/test/test_reader.rb +147 -0
- data/test/test_record.rb +49 -0
- data/test/test_search.rb +575 -0
- data/test/test_tsv.rb +7 -0
- metadata +108 -5
- data/lib/tb/qtsv.rb +0 -93
data/lib/tb/csv.rb
CHANGED
@@ -27,13 +27,38 @@
|
|
27
27
|
require 'csv'
|
28
28
|
|
29
29
|
class Tb
|
30
|
+
def Tb.load_csv(filename, *header_fields, &block)
|
31
|
+
Tb.parse_csv(File.read(filename), *header_fields, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def Tb.parse_csv(csv, *header_fields)
|
35
|
+
aa = []
|
36
|
+
csv_stream_input(csv) {|ary|
|
37
|
+
aa << ary
|
38
|
+
}
|
39
|
+
aa = yield aa if block_given?
|
40
|
+
if header_fields.empty?
|
41
|
+
reader = Tb::Reader.new(aa)
|
42
|
+
arys = []
|
43
|
+
reader.each {|ary|
|
44
|
+
arys << ary
|
45
|
+
}
|
46
|
+
header = reader.header
|
47
|
+
else
|
48
|
+
header = header_fields
|
49
|
+
arys = aa
|
50
|
+
end
|
51
|
+
t = Tb.new(header)
|
52
|
+
arys.each {|ary|
|
53
|
+
ary << nil while ary.length < header.length
|
54
|
+
t.insert_values header, ary
|
55
|
+
}
|
56
|
+
t
|
57
|
+
end
|
58
|
+
|
30
59
|
def Tb.csv_stream_input(csv, &b)
|
31
60
|
csvreader = CSVReader.new(csv)
|
32
|
-
|
33
|
-
csvreader.each(&b)
|
34
|
-
ensure
|
35
|
-
csvreader.close
|
36
|
-
end
|
61
|
+
csvreader.each(&b)
|
37
62
|
nil
|
38
63
|
end
|
39
64
|
|
@@ -79,7 +104,6 @@ class Tb
|
|
79
104
|
end
|
80
105
|
|
81
106
|
def close
|
82
|
-
@csv.close
|
83
107
|
end
|
84
108
|
end
|
85
109
|
|
data/lib/tb/fieldset.rb
CHANGED
@@ -27,12 +27,44 @@
|
|
27
27
|
class Tb::FieldSet
|
28
28
|
def initialize(*fs)
|
29
29
|
@header = []
|
30
|
-
|
30
|
+
@field2index = {}
|
31
|
+
fs.each {|f| add_field(f) }
|
31
32
|
end
|
32
33
|
attr_reader :header
|
33
34
|
|
35
|
+
def add_field(hint)
|
36
|
+
hint = '1' if hint.nil? || hint == ''
|
37
|
+
while @field2index[hint]
|
38
|
+
case hint
|
39
|
+
when /\A[1-9][0-9]*\z/
|
40
|
+
hint = (hint.to_i + 1).to_s
|
41
|
+
when /\([1-9][0-9]*\)\z/
|
42
|
+
hint = hint.sub(/\(([1-9][0-9]*)\)\z/) { "(#{$1.to_i + 1})" }
|
43
|
+
else
|
44
|
+
hint = "#{hint}(2)"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@field2index[hint] = @header.length
|
48
|
+
@header << hint
|
49
|
+
hint
|
50
|
+
end
|
51
|
+
private :add_field
|
52
|
+
|
53
|
+
def index_from_field_ex(f)
|
54
|
+
i = @field2index[f]
|
55
|
+
return i if !i.nil?
|
56
|
+
if /\A[1-9][0-9]*\z/ !~ f
|
57
|
+
raise ArgumentError, "unexpected field name: #{f.inspect}"
|
58
|
+
end
|
59
|
+
while true
|
60
|
+
if add_field(nil) == f
|
61
|
+
return @header.length-1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
34
66
|
def index_from_field(f)
|
35
|
-
i =
|
67
|
+
i = @field2index[f]
|
36
68
|
if i.nil?
|
37
69
|
raise ArgumentError, "unexpected field name: #{f.inspect}"
|
38
70
|
end
|
@@ -40,17 +72,16 @@ class Tb::FieldSet
|
|
40
72
|
end
|
41
73
|
|
42
74
|
def field_from_index_ex(i)
|
43
|
-
if
|
44
|
-
|
45
|
-
|
46
|
-
else
|
47
|
-
field_from_index(i)
|
75
|
+
raise ArgumentError, "negative index: #{i}" if i < 0
|
76
|
+
until i < @header.length
|
77
|
+
add_field(nil)
|
48
78
|
end
|
79
|
+
@header[i]
|
49
80
|
end
|
50
81
|
|
51
82
|
def field_from_index(i)
|
52
83
|
raise ArgumentError, "negative index: #{i}" if i < 0
|
53
|
-
f =
|
84
|
+
f = @header[i]
|
54
85
|
if f.nil?
|
55
86
|
raise ArgumentError, "index too big: #{i}"
|
56
87
|
end
|
@@ -60,37 +91,4 @@ class Tb::FieldSet
|
|
60
91
|
def length
|
61
92
|
@header.length
|
62
93
|
end
|
63
|
-
|
64
|
-
def extend_length(len)
|
65
|
-
fs = [""] * (len - self.length)
|
66
|
-
add_fields(*fs)
|
67
|
-
end
|
68
|
-
|
69
|
-
def add_fields(*fs)
|
70
|
-
h = {}
|
71
|
-
max = {}
|
72
|
-
@header.each {|f|
|
73
|
-
h[f] = true
|
74
|
-
if /\((\d+)\)\z/ =~ f
|
75
|
-
prefix = $`
|
76
|
-
n = $1.to_i
|
77
|
-
max[prefix] = n if !max[prefix] || max[prefix] < n
|
78
|
-
end
|
79
|
-
}
|
80
|
-
fs2 = []
|
81
|
-
fs.each {|f|
|
82
|
-
f ||= ''
|
83
|
-
if !h[f]
|
84
|
-
f2 = f
|
85
|
-
else
|
86
|
-
max[f] = 1 if !max[f]
|
87
|
-
max[f] += 1
|
88
|
-
f2 = "#{f}(#{max[f]})"
|
89
|
-
end
|
90
|
-
fs2 << f2
|
91
|
-
h[f2] = true
|
92
|
-
}
|
93
|
-
@header.concat fs2
|
94
|
-
fs2
|
95
|
-
end
|
96
94
|
end
|
data/lib/tb/pager.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
begin
|
2
|
+
require 'io/console'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
class Tb::Pager
|
7
|
+
def self.open
|
8
|
+
pager = self.new
|
9
|
+
begin
|
10
|
+
yield pager
|
11
|
+
ensure
|
12
|
+
pager.close
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
if STDOUT.tty?
|
18
|
+
@io = nil
|
19
|
+
@buf = ''
|
20
|
+
else
|
21
|
+
@io = STDOUT
|
22
|
+
@buf = nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def <<(obj)
|
27
|
+
write obj.to_s
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def print(*args)
|
32
|
+
s = ''
|
33
|
+
args.map {|a| s << a.to_s }
|
34
|
+
write s
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def printf(format, *args)
|
39
|
+
write sprintf(format, *args)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def putc(ch)
|
44
|
+
if Integer === ch
|
45
|
+
write [ch].pack("C")
|
46
|
+
else
|
47
|
+
write ch.to_s
|
48
|
+
end
|
49
|
+
ch
|
50
|
+
end
|
51
|
+
|
52
|
+
def puts(*objs)
|
53
|
+
if objs.empty?
|
54
|
+
write "\n"
|
55
|
+
else
|
56
|
+
objs.each {|o|
|
57
|
+
o = o.to_s
|
58
|
+
write o
|
59
|
+
write "\n" if /\n\z/ !~ o
|
60
|
+
}
|
61
|
+
end
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def write_nonblock(str)
|
66
|
+
write str.to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def expand_tab(str, tabstop=8)
|
70
|
+
col = 0
|
71
|
+
str.gsub(/(\t+)|[^\t]+/) {
|
72
|
+
if $1
|
73
|
+
' ' * (($1.length * tabstop) - (col + 1) % tabstop)
|
74
|
+
else
|
75
|
+
$&
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
DEFAULT_LINES = 24
|
81
|
+
DEFAULT_COLUMNS = 80
|
82
|
+
|
83
|
+
def winsize
|
84
|
+
if STDOUT.respond_to? :winsize
|
85
|
+
lines, columns = STDOUT.winsize
|
86
|
+
return [lines, columns] if lines != 0 && columns != 0
|
87
|
+
end
|
88
|
+
[DEFAULT_LINES, DEFAULT_COLUMNS]
|
89
|
+
end
|
90
|
+
|
91
|
+
def single_screen?(str)
|
92
|
+
lines, columns = winsize
|
93
|
+
n = 0
|
94
|
+
str.each_line {|line|
|
95
|
+
line = expand_tab(line).chomp
|
96
|
+
cols = line.length # xxx: 1 column/character assumed.
|
97
|
+
cols = 1 if cols == 0
|
98
|
+
m = (cols + columns - 1) / columns # termcap am capability is assumed.
|
99
|
+
n += m
|
100
|
+
}
|
101
|
+
n <= lines-1
|
102
|
+
end
|
103
|
+
|
104
|
+
def write(str)
|
105
|
+
str = str.to_s
|
106
|
+
if !@io
|
107
|
+
@buf << str
|
108
|
+
if !single_screen?(@buf)
|
109
|
+
@io = IO.popen(ENV['PAGER'] || 'more', 'w')
|
110
|
+
@io << @buf
|
111
|
+
@buf = nil
|
112
|
+
end
|
113
|
+
else
|
114
|
+
@io << str
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def flush
|
119
|
+
@io.flush if @io
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
def close
|
124
|
+
if !@io
|
125
|
+
STDOUT.print @buf
|
126
|
+
else
|
127
|
+
# don't need to ouput @buf because @buf is nil.
|
128
|
+
@io.close if @io != STDOUT
|
129
|
+
end
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
data/lib/tb/pnm.rb
ADDED
@@ -0,0 +1,357 @@
|
|
1
|
+
# lib/tb/pnm.rb - tools for (very small) PNM images.
|
2
|
+
#
|
3
|
+
# Copyright (C) 2010-2011 Tanaka Akira <akr@fsij.org>
|
4
|
+
#
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
7
|
+
#
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
# list of conditions and the following disclaimer.
|
10
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
# 3. The name of the author may not be used to endorse or promote products
|
14
|
+
# derived from this software without specific prior written permission.
|
15
|
+
#
|
16
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
17
|
+
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
18
|
+
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
19
|
+
# EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
20
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
21
|
+
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
24
|
+
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
25
|
+
# OF SUCH DAMAGE.
|
26
|
+
|
27
|
+
class Tb
|
28
|
+
# :call-seq:
|
29
|
+
# Tb.load_pnm(pnm_filename) -> tb
|
30
|
+
#
|
31
|
+
def Tb.load_pnm(pnm_filename)
|
32
|
+
pnm_content = File.open(pnm_filename, "rb") {|f| f.read }
|
33
|
+
Tb.parse_pnm(pnm_content)
|
34
|
+
end
|
35
|
+
|
36
|
+
# :call-seq:
|
37
|
+
# Tb.parse_pnm(pnm_content) -> tb
|
38
|
+
#
|
39
|
+
def Tb.parse_pnm(pnm_content)
|
40
|
+
reader = PNMReader.new(pnm_content)
|
41
|
+
header = reader.shift
|
42
|
+
t = Tb.new(header)
|
43
|
+
reader.each {|ary|
|
44
|
+
t.insert_values header, ary
|
45
|
+
}
|
46
|
+
t
|
47
|
+
end
|
48
|
+
|
49
|
+
# :call-seq:
|
50
|
+
# Tb.pnm_stream_input(pnm_io) {|ary| ... }
|
51
|
+
#
|
52
|
+
def Tb.pnm_stream_input(pnm_io)
|
53
|
+
pnm_io.binmode
|
54
|
+
content = pnm_io.read
|
55
|
+
PNMReader.new(content)
|
56
|
+
end
|
57
|
+
|
58
|
+
# practical only for (very) small images.
|
59
|
+
class PNMReader
|
60
|
+
WSP = /(?:[ \t\r\n]|\#[^\r\n]*[\r\n])+/
|
61
|
+
|
62
|
+
def initialize(pnm_content)
|
63
|
+
pnm_content.force_encoding("ASCII-8BIT") if pnm_content.respond_to? :force_encoding
|
64
|
+
if /\A(P[63])(#{WSP})(\d+)(#{WSP})(\d+)(#{WSP})(\d+)[ \t\r\n]/on =~ pnm_content
|
65
|
+
magic, wsp1, w, wsp2, h, wsp3, max, raster = $1, $2, $3.to_i, $4, $5.to_i, $6, $7.to_i, $'
|
66
|
+
pixel_component = %w[R G B]
|
67
|
+
elsif /\A(P[52])(#{WSP})(\d+)(#{WSP})(\d+)(#{WSP})(\d+)[ \t\r\n]/on =~ pnm_content
|
68
|
+
magic, wsp1, w, wsp2, h, wsp3, max, raster = $1, $2, $3.to_i, $4, $5.to_i, $6, $7.to_i, $'
|
69
|
+
pixel_component = %w[V]
|
70
|
+
elsif /\A(P[41])(#{WSP})(\d+)(#{WSP})(\d+)[ \t\r\n]/on =~ pnm_content
|
71
|
+
magic, wsp1, w, wsp2, h, raster = $1, $2, $3.to_i, $4, $5.to_i, $'
|
72
|
+
wsp3 = nil
|
73
|
+
max = 1
|
74
|
+
pixel_component = %w[V]
|
75
|
+
else
|
76
|
+
raise ArgumentError, "not PNM format"
|
77
|
+
end
|
78
|
+
raise ArgumentError, "unsupported max value: #{max}" if 65535 < max
|
79
|
+
|
80
|
+
@ary = [
|
81
|
+
['type', 'x', 'y', 'component', 'value'],
|
82
|
+
['meta', nil, nil, 'pnm_type', magic],
|
83
|
+
['meta', nil, nil, 'width', w],
|
84
|
+
['meta', nil, nil, 'height', h],
|
85
|
+
['meta', nil, nil, 'max', max]
|
86
|
+
]
|
87
|
+
|
88
|
+
[wsp1, wsp2, wsp3].each {|wsp|
|
89
|
+
next if !wsp
|
90
|
+
wsp.scan(/\#([^\r\n]*)[\r\n]/) { @ary << ['meta', nil, nil, 'comment', $1] }
|
91
|
+
}
|
92
|
+
|
93
|
+
if /P[65]/ =~ magic # raw (binary) PPM/PGM
|
94
|
+
if max < 0x100
|
95
|
+
each_pixel_component = method(:raw_ppm_pgm_1byte_each_pixel_component)
|
96
|
+
else
|
97
|
+
each_pixel_component = method(:raw_ppm_pgm_2byte_each_pixel_component)
|
98
|
+
end
|
99
|
+
elsif /P4/ =~ magic # raw (binary) PBM
|
100
|
+
each_pixel_component = make_raw_pbm_each_pixel_component(w)
|
101
|
+
elsif /P[32]/ =~ magic # plain (ascii) PPM/PGM
|
102
|
+
each_pixel_component = method(:plain_ppm_pgm_each_pixel_component)
|
103
|
+
elsif /P1/ =~ magic # plain (ascii) PBM
|
104
|
+
each_pixel_component = method(:plain_pbm_each_pixel_component)
|
105
|
+
end
|
106
|
+
n = w * h * pixel_component.length
|
107
|
+
i = 0
|
108
|
+
each_pixel_component.call(raster) {|value|
|
109
|
+
break if i == n
|
110
|
+
y, x = (i / pixel_component.length).divmod(w)
|
111
|
+
c = pixel_component[i % pixel_component.length]
|
112
|
+
@ary << ['pixel', x, y, c, value.to_f / max]
|
113
|
+
i += 1
|
114
|
+
}
|
115
|
+
if i != n
|
116
|
+
raise ArgumentError, "PNM raster data too short."
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def raw_ppm_pgm_1byte_each_pixel_component(raster, &b)
|
121
|
+
raster.each_byte(&b)
|
122
|
+
end
|
123
|
+
|
124
|
+
def raw_ppm_pgm_2byte_each_pixel_component(raster)
|
125
|
+
raster.enum_for(:each_byte).each_slice(2) {|byte1, byte2|
|
126
|
+
word = byte1 * 0x100 + byte2
|
127
|
+
yield word
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def plain_ppm_pgm_each_pixel_component(raster)
|
132
|
+
raster.scan(/\d+/) { yield $&.to_i }
|
133
|
+
end
|
134
|
+
|
135
|
+
def plain_pbm_each_pixel_component(raster)
|
136
|
+
raster.scan(/[01]/) { yield 1 - $&.to_i }
|
137
|
+
end
|
138
|
+
|
139
|
+
def make_raw_pbm_each_pixel_component(width)
|
140
|
+
iter = Object.new
|
141
|
+
iter.instance_variable_set(:@width, width)
|
142
|
+
def iter.call(raster)
|
143
|
+
numbytes = (@width + 7) / 8
|
144
|
+
y = 0
|
145
|
+
while true
|
146
|
+
return if raster.size <= y * numbytes
|
147
|
+
line = raster[y * numbytes, numbytes]
|
148
|
+
x = 0
|
149
|
+
while x < @width
|
150
|
+
i, j = x.divmod(8)
|
151
|
+
return if line.size <= i
|
152
|
+
byte = line[x/8].ord
|
153
|
+
yield 1 - ((byte >> (7-j)) & 1)
|
154
|
+
x += 1
|
155
|
+
end
|
156
|
+
y += 1
|
157
|
+
end
|
158
|
+
end
|
159
|
+
iter
|
160
|
+
end
|
161
|
+
|
162
|
+
def shift
|
163
|
+
@ary.shift
|
164
|
+
end
|
165
|
+
|
166
|
+
def each
|
167
|
+
while ary = self.shift
|
168
|
+
yield ary
|
169
|
+
end
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
|
173
|
+
def to_a
|
174
|
+
result= []
|
175
|
+
each {|ary| result << ary }
|
176
|
+
result
|
177
|
+
end
|
178
|
+
|
179
|
+
def close
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# :call-seq:
|
184
|
+
# generate_pnm(out='')
|
185
|
+
#
|
186
|
+
def generate_pnm(out='')
|
187
|
+
undefined_field = ['x', 'y', 'component', 'value'] - self.list_fields
|
188
|
+
if !undefined_field.empty?
|
189
|
+
raise ArgumentError, "field not defined: #{undefined_field.inspect[1...-1]}"
|
190
|
+
end
|
191
|
+
pnm_type = nil
|
192
|
+
width = height = nil
|
193
|
+
comments = []
|
194
|
+
max_value = nil
|
195
|
+
max_x = max_y = 0
|
196
|
+
values = { 0.0 => true, 1.0 => true }
|
197
|
+
components = {}
|
198
|
+
self.each {|rec|
|
199
|
+
case rec['component']
|
200
|
+
when 'pnm_type'
|
201
|
+
pnm_type = rec['value']
|
202
|
+
when 'width'
|
203
|
+
width = rec['value'].to_i
|
204
|
+
when 'height'
|
205
|
+
height = rec['value'].to_i
|
206
|
+
when 'max'
|
207
|
+
max_value = rec['value'].to_i
|
208
|
+
when 'comment'
|
209
|
+
comments << rec['value']
|
210
|
+
when 'R', 'G', 'B', 'V'
|
211
|
+
components[rec['component']] = true
|
212
|
+
x = rec['x'].to_i
|
213
|
+
y = rec['y'].to_i
|
214
|
+
max_x = x if max_x < x
|
215
|
+
max_y = y if max_y < y
|
216
|
+
values[rec['value'].to_f] = true
|
217
|
+
end
|
218
|
+
|
219
|
+
}
|
220
|
+
if !(components.keys - %w[V]).empty? &&
|
221
|
+
!(components.keys - %w[R G B]).empty?
|
222
|
+
raise ArgumentError, "inconsistent color component: #{components.keys.sort.inspect[1...-1]}"
|
223
|
+
end
|
224
|
+
case pnm_type
|
225
|
+
when 'P1', 'P4' then raise ArgumentError, "unexpected compoenent for PBM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
226
|
+
when 'P2', 'P5' then raise ArgumentError, "unexpected compoenent for PGM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
227
|
+
when 'P3', 'P6' then raise ArgumentError, "unexpected compoenent for PPM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[R G B]).empty?
|
228
|
+
end
|
229
|
+
comments.each {|c|
|
230
|
+
if /[\r\n]/ =~ c
|
231
|
+
raise ArgumentError, "comment cannot contain a newline: #{c.inspect}"
|
232
|
+
end
|
233
|
+
}
|
234
|
+
if !width
|
235
|
+
width = max_x + 1
|
236
|
+
end
|
237
|
+
if !height
|
238
|
+
height = max_y + 1
|
239
|
+
end
|
240
|
+
if !max_value
|
241
|
+
min_interval = 1.0
|
242
|
+
values.keys.sort.each_cons(2) {|v1, v2|
|
243
|
+
d = v2-v1
|
244
|
+
min_interval = d if d < min_interval
|
245
|
+
}
|
246
|
+
if min_interval < 0.0039 # 1/255 = 0.00392156862745098...
|
247
|
+
max_value = 0xffff
|
248
|
+
elsif min_interval < 1.0 || !(components.keys & %w[R G B]).empty?
|
249
|
+
max_value = 0xff
|
250
|
+
else
|
251
|
+
max_value = 1
|
252
|
+
end
|
253
|
+
end
|
254
|
+
if pnm_type
|
255
|
+
if !pnm_type.kind_of?(String) || /\AP[123456]\z/ !~ pnm_type
|
256
|
+
raise ArgumentError, "unexpected PNM type: #{pnm_type.inspect}"
|
257
|
+
end
|
258
|
+
else
|
259
|
+
if (components.keys - ['V']).empty?
|
260
|
+
if max_value == 1
|
261
|
+
pnm_type = 'P4' # PBM
|
262
|
+
else
|
263
|
+
pnm_type = 'P5' # PGM
|
264
|
+
end
|
265
|
+
else
|
266
|
+
pnm_type = 'P6' # PPM
|
267
|
+
end
|
268
|
+
end
|
269
|
+
header = "#{pnm_type}\n"
|
270
|
+
comments.each {|c| header << '#' << c << "\n" }
|
271
|
+
header << "#{width} #{height}\n"
|
272
|
+
header << "#{max_value}\n" if /P[2536]/ =~ pnm_type
|
273
|
+
if /P[14]/ =~ pnm_type # PBM
|
274
|
+
max_value = 1
|
275
|
+
end
|
276
|
+
bytes_per_component = bytes_per_line = component_fmt = component_template = nil
|
277
|
+
case pnm_type
|
278
|
+
when 'P1' then bytes_per_component = 1; raster = '1' * (width * height)
|
279
|
+
when 'P4' then bytes_per_line = (width + 7) / 8; raster = ["1"*width].pack("B*") * height
|
280
|
+
when 'P2' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (width * height)
|
281
|
+
when 'P5' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * width * height)
|
282
|
+
when 'P3' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (3 * width * height)
|
283
|
+
when 'P6' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * 3 * width * height)
|
284
|
+
else
|
285
|
+
raise
|
286
|
+
end
|
287
|
+
raster.force_encoding("ASCII-8BIT") if raster.respond_to? :force_encoding
|
288
|
+
self.each {|rec|
|
289
|
+
c = rec['component']
|
290
|
+
next if /\A[RGBV]\z/ !~ c
|
291
|
+
x = rec['x'].to_i
|
292
|
+
y = rec['y'].to_i
|
293
|
+
next if x < 0 || width <= x
|
294
|
+
next if y < 0 || height <= y
|
295
|
+
v = rec['value'].to_f
|
296
|
+
if v < 0
|
297
|
+
v = 0
|
298
|
+
elsif 1 < v
|
299
|
+
v = 1
|
300
|
+
end
|
301
|
+
case pnm_type
|
302
|
+
when 'P1'
|
303
|
+
v = v < 0.5 ? '1' : '0'
|
304
|
+
raster[y * width + x] = v
|
305
|
+
when 'P4'
|
306
|
+
xhi, xlo = x.divmod(8)
|
307
|
+
i = y * bytes_per_line + xhi
|
308
|
+
byte = raster[i].ord
|
309
|
+
if v < 0.5
|
310
|
+
byte |= 0x80 >> xlo
|
311
|
+
else
|
312
|
+
byte &= 0xff7f >> xlo
|
313
|
+
end
|
314
|
+
raster[i] = [byte].pack("C")
|
315
|
+
when 'P2'
|
316
|
+
v = (v * max_value).round
|
317
|
+
raster[(y * width + x) * bytes_per_component, bytes_per_component] = component_fmt % v
|
318
|
+
when 'P5'
|
319
|
+
v = (v * max_value).round
|
320
|
+
raster[(y * width + x) * bytes_per_component, bytes_per_component] = [v].pack(component_template)
|
321
|
+
when 'P3'
|
322
|
+
v = (v * max_value).round
|
323
|
+
i = (y * width + x) * 3
|
324
|
+
if c == 'G' then i += 1
|
325
|
+
elsif c == 'B' then i += 2
|
326
|
+
end
|
327
|
+
raster[i * bytes_per_component, bytes_per_component] = component_fmt % v
|
328
|
+
when 'P6'
|
329
|
+
v = (v * max_value).round
|
330
|
+
i = (y * width + x) * 3
|
331
|
+
if c == 'G' then i += 1
|
332
|
+
elsif c == 'B' then i += 2
|
333
|
+
end
|
334
|
+
raster[i * bytes_per_component, bytes_per_component] = [v].pack(component_template)
|
335
|
+
else
|
336
|
+
raise
|
337
|
+
end
|
338
|
+
}
|
339
|
+
if pnm_type == 'P1'
|
340
|
+
raster.gsub!(/[01]{#{width}}/, "\\&\n")
|
341
|
+
if 70 < width
|
342
|
+
raster.gsub!(/[01]{70}/, "\\&\n")
|
343
|
+
end
|
344
|
+
raster << "\n" if /\n\z/ !~ raster
|
345
|
+
elsif /P[23]/ =~ pnm_type
|
346
|
+
components_per_line = /P2/ =~ pnm_type ? width : 3 * width
|
347
|
+
raster.gsub!(/ +/, ' ')
|
348
|
+
raster.gsub!(/( \d+){#{components_per_line}}/, "\\&\n")
|
349
|
+
raster.gsub!(/(\A|\n) +/, '\1')
|
350
|
+
raster.gsub!(/.{71,}\n/) {
|
351
|
+
$&.gsub(/(.{1,69})[ \n]/, "\\1\n")
|
352
|
+
}
|
353
|
+
raster << "\n" if /\n\z/ !~ raster
|
354
|
+
end
|
355
|
+
out << (header+raster)
|
356
|
+
end
|
357
|
+
end
|