tb 0.9 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README +13 -11
- data/lib/tb.rb +14 -6
- data/lib/tb/catreader.rb +2 -2
- data/lib/tb/cmd_consecutive.rb +6 -2
- data/lib/tb/cmd_crop.rb +22 -3
- data/lib/tb/cmd_cross.rb +24 -0
- data/lib/tb/cmd_cut.rb +20 -10
- data/lib/tb/cmd_git.rb +20 -7
- data/lib/tb/cmd_group.rb +32 -0
- data/lib/tb/cmd_gsub.rb +21 -0
- data/lib/tb/cmd_join.rb +28 -0
- data/lib/tb/cmd_ls.rb +9 -0
- data/lib/tb/cmd_melt.rb +15 -0
- data/lib/tb/cmd_mheader.rb +15 -0
- data/lib/tb/cmd_nest.rb +27 -6
- data/lib/tb/cmd_newfield.rb +19 -2
- data/lib/tb/cmd_rename.rb +20 -0
- data/lib/tb/{cmd_grep.rb → cmd_search.rb} +37 -23
- data/lib/tb/cmd_shape.rb +69 -25
- data/lib/tb/cmd_sort.rb +20 -0
- data/lib/tb/cmd_tar.rb +38 -0
- data/lib/tb/cmd_to_json.rb +2 -2
- data/lib/tb/cmd_to_ltsv.rb +3 -3
- data/lib/tb/cmd_to_pnm.rb +3 -3
- data/lib/tb/cmd_to_tsv.rb +3 -3
- data/lib/tb/cmd_to_yaml.rb +3 -3
- data/lib/tb/cmd_unmelt.rb +15 -0
- data/lib/tb/cmd_unnest.rb +31 -7
- data/lib/tb/cmdmain.rb +2 -0
- data/lib/tb/cmdtop.rb +1 -1
- data/lib/tb/cmdutil.rb +9 -62
- data/lib/tb/csv.rb +21 -79
- data/lib/tb/enumerable.rb +42 -68
- data/lib/tb/enumerator.rb +15 -7
- data/lib/tb/{fieldset.rb → hashreader.rb} +37 -56
- data/lib/tb/hashwriter.rb +54 -0
- data/lib/tb/headerreader.rb +108 -0
- data/lib/tb/headerwriter.rb +116 -0
- data/lib/tb/json.rb +17 -15
- data/lib/tb/ltsv.rb +35 -96
- data/lib/tb/ndjson.rb +63 -0
- data/lib/tb/numericreader.rb +66 -0
- data/lib/tb/numericwriter.rb +61 -0
- data/lib/tb/pnm.rb +206 -200
- data/lib/tb/ropen.rb +54 -59
- data/lib/tb/tsv.rb +39 -71
- data/sample/excel2csv +24 -25
- data/sample/poi-xls2csv.rb +13 -14
- data/tb.gemspec +154 -0
- data/test/test_cmd_cat.rb +28 -6
- data/test/test_cmd_consecutive.rb +8 -3
- data/test/test_cmd_cut.rb +14 -4
- data/test/test_cmd_git_log.rb +50 -50
- data/test/test_cmd_grep.rb +6 -6
- data/test/test_cmd_gsub.rb +7 -2
- data/test/test_cmd_ls.rb +70 -62
- data/test/test_cmd_shape.rb +43 -6
- data/test/test_cmd_svn_log.rb +26 -27
- data/test/test_cmd_to_csv.rb +10 -5
- data/test/test_cmd_to_json.rb +16 -0
- data/test/test_cmd_to_ltsv.rb +2 -2
- data/test/test_cmd_to_pp.rb +7 -2
- data/test/test_csv.rb +74 -62
- data/test/test_ex_enumerable.rb +0 -1
- data/test/test_fileenumerator.rb +3 -3
- data/test/test_headercsv.rb +43 -0
- data/test/test_json.rb +2 -2
- data/test/test_ltsv.rb +22 -17
- data/test/test_ndjson.rb +62 -0
- data/test/test_numericcsv.rb +36 -0
- data/test/test_pnm.rb +69 -70
- data/test/test_reader.rb +27 -124
- data/test/test_tbenum.rb +18 -18
- data/test/test_tsv.rb +21 -32
- data/test/util_tbtest.rb +12 -0
- metadata +41 -19
- data/lib/tb/basic.rb +0 -1070
- data/lib/tb/reader.rb +0 -106
- data/lib/tb/record.rb +0 -158
- data/test/test_basic.rb +0 -403
- data/test/test_fieldset.rb +0 -42
- data/test/test_record.rb +0 -61
data/lib/tb/json.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (C) 2012 Tanaka Akira <akr@fsij.org>
|
1
|
+
# Copyright (C) 2012-2014 Tanaka Akira <akr@fsij.org>
|
2
2
|
#
|
3
3
|
# Redistribution and use in source and binary forms, with or without
|
4
4
|
# modification, are permitted provided that the following conditions
|
@@ -28,23 +28,25 @@
|
|
28
28
|
|
29
29
|
require 'json'
|
30
30
|
|
31
|
-
|
32
|
-
class JSONReader
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@ary = JSON.parse(string)
|
31
|
+
module Tb
|
32
|
+
class JSONReader < Tb::HashReader
|
33
|
+
def initialize(io)
|
34
|
+
ary = JSON.parse(io.read)
|
35
|
+
super lambda { ary.shift }
|
37
36
|
end
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
class JSONWriter < Tb::HashWriter
|
40
|
+
def initialize(io)
|
41
|
+
io << "[\n"
|
42
|
+
sep = ""
|
43
|
+
super lambda {|hash|
|
44
|
+
io << sep << JSON.pretty_generate(hash)
|
45
|
+
sep = ",\n"
|
46
|
+
},
|
47
|
+
lambda {
|
48
|
+
io << "\n]\n"
|
43
49
|
}
|
44
50
|
end
|
45
|
-
|
46
|
-
def each(&block)
|
47
|
-
header_and_each(nil, &block)
|
48
|
-
end
|
49
51
|
end
|
50
52
|
end
|
data/lib/tb/ltsv.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# lib/tb/ltsv.rb - LTSV related fetures for table library
|
2
2
|
#
|
3
|
-
# Copyright (C) 2013 Tanaka Akira <akr@fsij.org>
|
3
|
+
# Copyright (C) 2013-2014 Tanaka Akira <akr@fsij.org>
|
4
4
|
#
|
5
5
|
# Redistribution and use in source and binary forms, with or without
|
6
6
|
# modification, are permitted provided that the following conditions
|
@@ -30,38 +30,7 @@
|
|
30
30
|
|
31
31
|
require 'stringio'
|
32
32
|
|
33
|
-
|
34
|
-
def Tb.load_ltsv(filename, &block)
|
35
|
-
Tb.parse_ltsv(File.read(filename), &block)
|
36
|
-
end
|
37
|
-
|
38
|
-
def Tb.parse_ltsv(ltsv)
|
39
|
-
assoc_list = []
|
40
|
-
ltsv_stream_input(ltsv) {|assoc|
|
41
|
-
assoc_list << assoc
|
42
|
-
}
|
43
|
-
fields_hash = {}
|
44
|
-
assoc_list.each {|assoc|
|
45
|
-
assoc.each {|key, val|
|
46
|
-
fields_hash[key] ||= fields_hash.size
|
47
|
-
}
|
48
|
-
}
|
49
|
-
header = fields_hash.keys
|
50
|
-
t = Tb.new(header)
|
51
|
-
assoc_list.each {|assoc|
|
52
|
-
t.insert Hash[assoc]
|
53
|
-
}
|
54
|
-
t
|
55
|
-
end
|
56
|
-
|
57
|
-
def Tb.ltsv_stream_input(ltsv)
|
58
|
-
ltsvreader = LTSVReader.new(ltsv)
|
59
|
-
while assoc = ltsvreader.shift
|
60
|
-
yield assoc
|
61
|
-
end
|
62
|
-
nil
|
63
|
-
end
|
64
|
-
|
33
|
+
module Tb
|
65
34
|
def Tb.ltsv_escape_key(str)
|
66
35
|
if /[\0-\x1f":\\\x7f]/ =~ str
|
67
36
|
'"' +
|
@@ -138,70 +107,19 @@ class Tb
|
|
138
107
|
end
|
139
108
|
end
|
140
109
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
def shift
|
153
|
-
line = @input.gets
|
154
|
-
return nil if !line
|
155
|
-
line = line.chomp("\n")
|
156
|
-
line = line.chomp("\r")
|
157
|
-
ary = line.split(/\t/, -1)
|
158
|
-
assoc = ary.map {|str|
|
159
|
-
/:/ =~ str
|
160
|
-
key = $`
|
161
|
-
val = $'
|
162
|
-
key = Tb.ltsv_unescape_string(key)
|
163
|
-
val = Tb.ltsv_unescape_string(val)
|
164
|
-
[key, val]
|
165
|
-
}
|
166
|
-
assoc
|
167
|
-
end
|
168
|
-
|
169
|
-
def header_and_each(header_proc)
|
170
|
-
header_proc.call(nil) if header_proc
|
171
|
-
while assoc = self.shift
|
172
|
-
yield Hash[assoc]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def each(&block)
|
177
|
-
header_and_each(nil, &block)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def Tb.ltsv_stream_output(out)
|
182
|
-
gen = Object.new
|
183
|
-
gen.instance_variable_set(:@out, out)
|
184
|
-
def gen.<<(assoc)
|
185
|
-
@out << Tb.ltsv_assoc_join(assoc) << "\n"
|
186
|
-
end
|
187
|
-
yield gen
|
188
|
-
end
|
189
|
-
|
190
|
-
# :call-seq:
|
191
|
-
# generate_ltsv(out='') {|recordids| modified_recordids }
|
192
|
-
# generate_ltsv(out='')
|
193
|
-
#
|
194
|
-
def generate_ltsv(out='', fields=nil, &block)
|
195
|
-
recordids = list_recordids
|
196
|
-
if block_given?
|
197
|
-
recordids = yield(recordids)
|
198
|
-
end
|
199
|
-
Tb.ltsv_stream_output(out) {|gen|
|
200
|
-
recordids.each {|recordid|
|
201
|
-
gen << get_record(recordid)
|
202
|
-
}
|
110
|
+
def Tb.ltsv_split_line(line)
|
111
|
+
line = line.chomp("\n")
|
112
|
+
line = line.chomp("\r")
|
113
|
+
ary = line.split(/\t/, -1)
|
114
|
+
assoc = ary.map {|str|
|
115
|
+
/:/ =~ str
|
116
|
+
key = $`
|
117
|
+
val = $'
|
118
|
+
key = Tb.ltsv_unescape_string(key)
|
119
|
+
val = Tb.ltsv_unescape_string(val)
|
120
|
+
[key, val]
|
203
121
|
}
|
204
|
-
|
122
|
+
assoc
|
205
123
|
end
|
206
124
|
|
207
125
|
def Tb.ltsv_assoc_join(assoc)
|
@@ -209,4 +127,25 @@ class Tb
|
|
209
127
|
Tb.ltsv_escape_key(key) + ':' + Tb.ltsv_escape_value(val)
|
210
128
|
}.join("\t")
|
211
129
|
end
|
130
|
+
|
131
|
+
class LTSVReader < Tb::HashReader
|
132
|
+
def initialize(io)
|
133
|
+
super lambda {
|
134
|
+
line = io.gets
|
135
|
+
if line
|
136
|
+
Hash[Tb.ltsv_split_line(line)]
|
137
|
+
else
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class LTSVWriter < Tb::HashWriter
|
145
|
+
def initialize(io)
|
146
|
+
super lambda {|hash|
|
147
|
+
io << (Tb.ltsv_assoc_join(hash) + "\n")
|
148
|
+
}
|
149
|
+
end
|
150
|
+
end
|
212
151
|
end
|
data/lib/tb/ndjson.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Copyright (C) 2014 Tanaka Akira <akr@fsij.org>
|
2
|
+
#
|
3
|
+
# Redistribution and use in source and binary forms, with or without
|
4
|
+
# modification, are permitted provided that the following conditions
|
5
|
+
# are met:
|
6
|
+
#
|
7
|
+
# 1. Redistributions of source code must retain the above copyright
|
8
|
+
# notice, this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above
|
10
|
+
# copyright notice, this list of conditions and the following
|
11
|
+
# disclaimer in the documentation and/or other materials provided
|
12
|
+
# with the distribution.
|
13
|
+
# 3. The name of the author may not be used to endorse or promote
|
14
|
+
# products derived from this software without specific prior
|
15
|
+
# written permission.
|
16
|
+
#
|
17
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
18
|
+
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
19
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
20
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
21
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
22
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
23
|
+
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
24
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
25
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
26
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
27
|
+
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
|
+
|
29
|
+
require 'json'
|
30
|
+
|
31
|
+
# NDJSON is Newline Delimited JSON
|
32
|
+
# http://ndjson.org/
|
33
|
+
#
|
34
|
+
# Tb::NDJSONReader accepts empty lines.
|
35
|
+
|
36
|
+
module Tb
|
37
|
+
class NDJSONReader < Tb::HashReader
|
38
|
+
# io.gets should returns a string.
|
39
|
+
def initialize(io)
|
40
|
+
super lambda {
|
41
|
+
while true
|
42
|
+
line = io.gets
|
43
|
+
if line.nil? || /\S/ =~ line
|
44
|
+
break
|
45
|
+
end
|
46
|
+
end
|
47
|
+
if line
|
48
|
+
JSON.parse(line)
|
49
|
+
else
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class NDJSONWriter < Tb::HashWriter
|
57
|
+
def initialize(io)
|
58
|
+
super lambda {|hash|
|
59
|
+
io << (JSON.generate(hash) + "\n")
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# lib/tb/numericreaderm.rb - reader mixin for table without header
|
2
|
+
#
|
3
|
+
# Copyright (C) 2014 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
|
7
|
+
# are met:
|
8
|
+
#
|
9
|
+
# 1. Redistributions of source code must retain the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer.
|
11
|
+
# 2. Redistributions in binary form must reproduce the above
|
12
|
+
# copyright notice, this list of conditions and the following
|
13
|
+
# disclaimer in the documentation and/or other materials provided
|
14
|
+
# with the distribution.
|
15
|
+
# 3. The name of the author may not be used to endorse or promote
|
16
|
+
# products derived from this software without specific prior
|
17
|
+
# written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
20
|
+
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
23
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
25
|
+
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
27
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
28
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
29
|
+
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
class Tb::NumericReader
|
32
|
+
include Tb::EnumerableWithEach
|
33
|
+
|
34
|
+
def initialize(get_array)
|
35
|
+
@get_array = get_array
|
36
|
+
end
|
37
|
+
|
38
|
+
def header_known?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_named_header
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_hash
|
47
|
+
ary = @get_array.call
|
48
|
+
if !ary
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
hash = {}
|
52
|
+
ary.each_with_index {|v, i|
|
53
|
+
field = (i+1).to_s
|
54
|
+
hash[field] = v
|
55
|
+
}
|
56
|
+
hash
|
57
|
+
end
|
58
|
+
|
59
|
+
def header_and_each(header_proc)
|
60
|
+
header_proc.call(get_named_header) if header_proc
|
61
|
+
while hash = get_hash
|
62
|
+
yield hash
|
63
|
+
end
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# lib/tb/numericwriterm.rb - writer mixin for table without header
|
2
|
+
#
|
3
|
+
# Copyright (C) 2014 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
|
7
|
+
# are met:
|
8
|
+
#
|
9
|
+
# 1. Redistributions of source code must retain the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer.
|
11
|
+
# 2. Redistributions in binary form must reproduce the above
|
12
|
+
# copyright notice, this list of conditions and the following
|
13
|
+
# disclaimer in the documentation and/or other materials provided
|
14
|
+
# with the distribution.
|
15
|
+
# 3. The name of the author may not be used to endorse or promote
|
16
|
+
# products derived from this software without specific prior
|
17
|
+
# written permission.
|
18
|
+
#
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
20
|
+
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
23
|
+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
25
|
+
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
27
|
+
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
28
|
+
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
29
|
+
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
31
|
+
require 'tempfile'
|
32
|
+
|
33
|
+
class Tb::NumericWriter
|
34
|
+
def initialize(put_array, put_finish=nil)
|
35
|
+
@put_array = put_array
|
36
|
+
@put_finish = put_finish
|
37
|
+
end
|
38
|
+
|
39
|
+
def header_required?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
def header_generator=(gen)
|
44
|
+
end
|
45
|
+
|
46
|
+
def put_hash(hash)
|
47
|
+
ary = []
|
48
|
+
hash.each {|k, v|
|
49
|
+
if /\A[1-9][0-9]*\z/ !~ k
|
50
|
+
raise ArgumentError, "numeric field name expected: #{k.inspect}"
|
51
|
+
end
|
52
|
+
ary[k.to_i-1] = v
|
53
|
+
}
|
54
|
+
@put_array.call ary
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def finish
|
59
|
+
@put_finish.call if @put_finish
|
60
|
+
end
|
61
|
+
end
|
data/lib/tb/pnm.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# lib/tb/pnm.rb - tools for (very small) PNM images.
|
2
2
|
#
|
3
|
-
# Copyright (C) 2010-
|
3
|
+
# Copyright (C) 2010-2014 Tanaka Akira <akr@fsij.org>
|
4
4
|
#
|
5
5
|
# Redistribution and use in source and binary forms, with or without
|
6
6
|
# modification, are permitted provided that the following conditions
|
@@ -28,43 +28,47 @@
|
|
28
28
|
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
29
29
|
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
30
|
|
31
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
module Tb
|
32
|
+
# practical only for (very) small images.
|
33
|
+
class PNMReader < HashReader
|
34
|
+
def initialize(pnm_io)
|
35
|
+
pnm_io.binmode
|
36
|
+
content = pnm_io.read
|
37
|
+
initialize0(content)
|
38
|
+
header = @ary.shift
|
39
|
+
super lambda {
|
40
|
+
a = @ary.shift
|
41
|
+
if a
|
42
|
+
h = {}
|
43
|
+
a.each_with_index {|v, i|
|
44
|
+
h[header[i]] = v if !v.nil?
|
45
|
+
}
|
46
|
+
h
|
47
|
+
else
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
39
52
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
def Tb.parse_pnm(pnm_content)
|
44
|
-
reader = PNMReader.new(pnm_content)
|
45
|
-
header = reader.shift
|
46
|
-
t = Tb.new(header)
|
47
|
-
reader.each {|ary|
|
48
|
-
t.insert_values header, ary
|
49
|
-
}
|
50
|
-
t
|
51
|
-
end
|
53
|
+
def header_known?
|
54
|
+
true
|
55
|
+
end
|
52
56
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
def Tb.pnm_stream_input(pnm_io)
|
57
|
-
pnm_io.binmode
|
58
|
-
content = pnm_io.read
|
59
|
-
PNMReader.new(content)
|
60
|
-
end
|
57
|
+
def get_named_header
|
58
|
+
['type', 'x', 'y', 'component', 'value']
|
59
|
+
end
|
61
60
|
|
62
|
-
# practical only for (very) small images.
|
63
|
-
class PNMReader
|
64
61
|
WSP = /(?:[ \t\r\n]|\#[^\r\n]*[\r\n])+/
|
65
62
|
|
66
|
-
def
|
67
|
-
|
63
|
+
def initialize0(pnm_content)
|
64
|
+
if pnm_content.respond_to? :to_str
|
65
|
+
pnm_content.force_encoding("ASCII-8BIT") if pnm_content.respond_to? :force_encoding
|
66
|
+
else
|
67
|
+
# IO
|
68
|
+
pnm_content.binmode
|
69
|
+
pnm_content = pnm_content.read
|
70
|
+
end
|
71
|
+
|
68
72
|
if /\A(P[63])(#{WSP})(\d+)(#{WSP})(\d+)(#{WSP})(\d+)[ \t\r\n]/on =~ pnm_content
|
69
73
|
magic, wsp1, w, wsp2, h, wsp3, max, raster = $1, $2, $3.to_i, $4, $5.to_i, $6, $7.to_i, $'
|
70
74
|
pixel_component = %w[R G B]
|
@@ -162,197 +166,199 @@ class Tb
|
|
162
166
|
end
|
163
167
|
iter
|
164
168
|
end
|
169
|
+
end
|
165
170
|
|
166
|
-
|
167
|
-
|
171
|
+
class PNMWriter
|
172
|
+
def initialize(io)
|
173
|
+
@io = io
|
174
|
+
@ary = []
|
175
|
+
@fields = {}
|
168
176
|
end
|
169
177
|
|
170
|
-
def
|
171
|
-
|
172
|
-
yield ary
|
173
|
-
end
|
174
|
-
nil
|
178
|
+
def header_required?
|
179
|
+
false
|
175
180
|
end
|
176
181
|
|
177
|
-
def
|
178
|
-
result= []
|
179
|
-
each {|ary| result << ary }
|
180
|
-
result
|
182
|
+
def header_generator=(gen)
|
181
183
|
end
|
182
|
-
end
|
183
184
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
if !undefined_field.empty?
|
190
|
-
raise ArgumentError, "field not defined: #{undefined_field.inspect[1...-1]}"
|
185
|
+
def put_hash(pairs)
|
186
|
+
@ary << pairs
|
187
|
+
pairs.each {|k, v|
|
188
|
+
@fields[k] = true
|
189
|
+
}
|
191
190
|
end
|
192
|
-
pnm_type = nil
|
193
|
-
width = height = nil
|
194
|
-
comments = []
|
195
|
-
max_value = nil
|
196
|
-
max_x = max_y = 0
|
197
|
-
values = { 0.0 => true, 1.0 => true }
|
198
|
-
components = {}
|
199
|
-
self.each {|rec|
|
200
|
-
case rec['component']
|
201
|
-
when 'pnm_type'
|
202
|
-
pnm_type = rec['value']
|
203
|
-
when 'width'
|
204
|
-
width = rec['value'].to_i
|
205
|
-
when 'height'
|
206
|
-
height = rec['value'].to_i
|
207
|
-
when 'max'
|
208
|
-
max_value = rec['value'].to_i
|
209
|
-
when 'comment'
|
210
|
-
comments << rec['value']
|
211
|
-
when 'R', 'G', 'B', 'V'
|
212
|
-
components[rec['component']] = true
|
213
|
-
x = rec['x'].to_i
|
214
|
-
y = rec['y'].to_i
|
215
|
-
max_x = x if max_x < x
|
216
|
-
max_y = y if max_y < y
|
217
|
-
values[rec['value'].to_f] = true
|
218
|
-
end
|
219
191
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
end
|
225
|
-
case pnm_type
|
226
|
-
when 'P1', 'P4' then raise ArgumentError, "unexpected compoenent for PBM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
227
|
-
when 'P2', 'P5' then raise ArgumentError, "unexpected compoenent for PGM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
228
|
-
when 'P3', 'P6' then raise ArgumentError, "unexpected compoenent for PPM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[R G B]).empty?
|
229
|
-
end
|
230
|
-
comments.each {|c|
|
231
|
-
if /[\r\n]/ =~ c
|
232
|
-
raise ArgumentError, "comment cannot contain a newline: #{c.inspect}"
|
192
|
+
def finish
|
193
|
+
undefined_field = ['x', 'y', 'component', 'value'] - @fields.keys
|
194
|
+
if !undefined_field.empty?
|
195
|
+
raise ArgumentError, "field not defined: #{undefined_field.inspect[1...-1]}"
|
233
196
|
end
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
197
|
+
pnm_type = nil
|
198
|
+
width = height = nil
|
199
|
+
comments = []
|
200
|
+
max_value = nil
|
201
|
+
max_x = max_y = 0
|
202
|
+
values = { 0.0 => true, 1.0 => true }
|
203
|
+
components = {}
|
204
|
+
@ary.each {|rec|
|
205
|
+
case rec['component']
|
206
|
+
when 'pnm_type'
|
207
|
+
pnm_type = rec['value']
|
208
|
+
when 'width'
|
209
|
+
width = rec['value'].to_i
|
210
|
+
when 'height'
|
211
|
+
height = rec['value'].to_i
|
212
|
+
when 'max'
|
213
|
+
max_value = rec['value'].to_i
|
214
|
+
when 'comment'
|
215
|
+
comments << rec['value']
|
216
|
+
when 'R', 'G', 'B', 'V'
|
217
|
+
components[rec['component']] = true
|
218
|
+
x = rec['x'].to_i
|
219
|
+
y = rec['y'].to_i
|
220
|
+
max_x = x if max_x < x
|
221
|
+
max_y = y if max_y < y
|
222
|
+
values[rec['value'].to_f] = true
|
223
|
+
end
|
246
224
|
}
|
247
|
-
if
|
248
|
-
|
249
|
-
|
250
|
-
max_value = 0xff
|
251
|
-
else
|
252
|
-
max_value = 1
|
225
|
+
if !(components.keys - %w[V]).empty? &&
|
226
|
+
!(components.keys - %w[R G B]).empty?
|
227
|
+
raise ArgumentError, "inconsistent color component: #{components.keys.sort.inspect[1...-1]}"
|
253
228
|
end
|
254
|
-
|
255
|
-
|
256
|
-
if !
|
257
|
-
|
229
|
+
case pnm_type
|
230
|
+
when 'P1', 'P4' then raise ArgumentError, "unexpected compoenent for PBM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
231
|
+
when 'P2', 'P5' then raise ArgumentError, "unexpected compoenent for PGM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
|
232
|
+
when 'P3', 'P6' then raise ArgumentError, "unexpected compoenent for PPM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[R G B]).empty?
|
258
233
|
end
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
pnm_type = 'P4' # PBM
|
263
|
-
else
|
264
|
-
pnm_type = 'P5' # PGM
|
234
|
+
comments.each {|c|
|
235
|
+
if /[\r\n]/ =~ c
|
236
|
+
raise ArgumentError, "comment cannot contain a newline: #{c.inspect}"
|
265
237
|
end
|
266
|
-
|
267
|
-
|
238
|
+
}
|
239
|
+
if !width
|
240
|
+
width = max_x + 1
|
268
241
|
end
|
269
|
-
|
270
|
-
|
271
|
-
comments.each {|c| header << '#' << c << "\n" }
|
272
|
-
header << "#{width} #{height}\n"
|
273
|
-
header << "#{max_value}\n" if /P[2536]/ =~ pnm_type
|
274
|
-
if /P[14]/ =~ pnm_type # PBM
|
275
|
-
max_value = 1
|
276
|
-
end
|
277
|
-
bytes_per_component = bytes_per_line = component_fmt = component_template = nil
|
278
|
-
case pnm_type
|
279
|
-
when 'P1' then bytes_per_component = 1; raster = '1' * (width * height)
|
280
|
-
when 'P4' then bytes_per_line = (width + 7) / 8; raster = ["1"*width].pack("B*") * height
|
281
|
-
when 'P2' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (width * height)
|
282
|
-
when 'P5' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * width * height)
|
283
|
-
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)
|
284
|
-
when 'P6' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * 3 * width * height)
|
285
|
-
else
|
286
|
-
raise
|
287
|
-
end
|
288
|
-
raster.force_encoding("ASCII-8BIT") if raster.respond_to? :force_encoding
|
289
|
-
self.each {|rec|
|
290
|
-
c = rec['component']
|
291
|
-
next if /\A[RGBV]\z/ !~ c
|
292
|
-
x = rec['x'].to_i
|
293
|
-
y = rec['y'].to_i
|
294
|
-
next if x < 0 || width <= x
|
295
|
-
next if y < 0 || height <= y
|
296
|
-
v = rec['value'].to_f
|
297
|
-
if v < 0
|
298
|
-
v = 0
|
299
|
-
elsif 1 < v
|
300
|
-
v = 1
|
242
|
+
if !height
|
243
|
+
height = max_y + 1
|
301
244
|
end
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
245
|
+
if !max_value
|
246
|
+
min_interval = 1.0
|
247
|
+
values.keys.sort.each_cons(2) {|v1, v2|
|
248
|
+
d = v2-v1
|
249
|
+
min_interval = d if d < min_interval
|
250
|
+
}
|
251
|
+
if min_interval < 0.0039 # 1/255 = 0.00392156862745098...
|
252
|
+
max_value = 0xffff
|
253
|
+
elsif min_interval < 1.0 || !(components.keys & %w[R G B]).empty?
|
254
|
+
max_value = 0xff
|
312
255
|
else
|
313
|
-
|
256
|
+
max_value = 1
|
314
257
|
end
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
when 'P5'
|
320
|
-
v = (v * max_value).round
|
321
|
-
raster[(y * width + x) * bytes_per_component, bytes_per_component] = [v].pack(component_template)
|
322
|
-
when 'P3'
|
323
|
-
v = (v * max_value).round
|
324
|
-
i = (y * width + x) * 3
|
325
|
-
if c == 'G' then i += 1
|
326
|
-
elsif c == 'B' then i += 2
|
258
|
+
end
|
259
|
+
if pnm_type
|
260
|
+
if !pnm_type.kind_of?(String) || /\AP[123456]\z/ !~ pnm_type
|
261
|
+
raise ArgumentError, "unexpected PNM type: #{pnm_type.inspect}"
|
327
262
|
end
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
263
|
+
else
|
264
|
+
if (components.keys - ['V']).empty?
|
265
|
+
if max_value == 1
|
266
|
+
pnm_type = 'P4' # PBM
|
267
|
+
else
|
268
|
+
pnm_type = 'P5' # PGM
|
269
|
+
end
|
270
|
+
else
|
271
|
+
pnm_type = 'P6' # PPM
|
334
272
|
end
|
335
|
-
|
273
|
+
end
|
274
|
+
header = "#{pnm_type}\n"
|
275
|
+
comments.each {|c| header << '#' << c << "\n" }
|
276
|
+
header << "#{width} #{height}\n"
|
277
|
+
header << "#{max_value}\n" if /P[2536]/ =~ pnm_type
|
278
|
+
if /P[14]/ =~ pnm_type # PBM
|
279
|
+
max_value = 1
|
280
|
+
end
|
281
|
+
bytes_per_component = bytes_per_line = component_fmt = component_template = nil
|
282
|
+
case pnm_type
|
283
|
+
when 'P1' then bytes_per_component = 1; raster = '1' * (width * height)
|
284
|
+
when 'P4' then bytes_per_line = (width + 7) / 8; raster = ["1"*width].pack("B*") * height
|
285
|
+
when 'P2' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (width * height)
|
286
|
+
when 'P5' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * width * height)
|
287
|
+
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)
|
288
|
+
when 'P6' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * 3 * width * height)
|
336
289
|
else
|
337
290
|
raise
|
338
291
|
end
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
292
|
+
raster.force_encoding("ASCII-8BIT") if raster.respond_to? :force_encoding
|
293
|
+
@ary.each {|rec|
|
294
|
+
c = rec['component']
|
295
|
+
next if /\A[RGBV]\z/ !~ c
|
296
|
+
x = rec['x'].to_i
|
297
|
+
y = rec['y'].to_i
|
298
|
+
next if x < 0 || width <= x
|
299
|
+
next if y < 0 || height <= y
|
300
|
+
v = rec['value'].to_f
|
301
|
+
if v < 0
|
302
|
+
v = 0
|
303
|
+
elsif 1 < v
|
304
|
+
v = 1
|
305
|
+
end
|
306
|
+
case pnm_type
|
307
|
+
when 'P1'
|
308
|
+
v = v < 0.5 ? '1' : '0'
|
309
|
+
raster[y * width + x] = v
|
310
|
+
when 'P4'
|
311
|
+
xhi, xlo = x.divmod(8)
|
312
|
+
i = y * bytes_per_line + xhi
|
313
|
+
byte = raster[i].ord
|
314
|
+
if v < 0.5
|
315
|
+
byte |= 0x80 >> xlo
|
316
|
+
else
|
317
|
+
byte &= 0xff7f >> xlo
|
318
|
+
end
|
319
|
+
raster[i] = [byte].pack("C")
|
320
|
+
when 'P2'
|
321
|
+
v = (v * max_value).round
|
322
|
+
raster[(y * width + x) * bytes_per_component, bytes_per_component] = component_fmt % v
|
323
|
+
when 'P5'
|
324
|
+
v = (v * max_value).round
|
325
|
+
raster[(y * width + x) * bytes_per_component, bytes_per_component] = [v].pack(component_template)
|
326
|
+
when 'P3'
|
327
|
+
v = (v * max_value).round
|
328
|
+
i = (y * width + x) * 3
|
329
|
+
if c == 'G' then i += 1
|
330
|
+
elsif c == 'B' then i += 2
|
331
|
+
end
|
332
|
+
raster[i * bytes_per_component, bytes_per_component] = component_fmt % v
|
333
|
+
when 'P6'
|
334
|
+
v = (v * max_value).round
|
335
|
+
i = (y * width + x) * 3
|
336
|
+
if c == 'G' then i += 1
|
337
|
+
elsif c == 'B' then i += 2
|
338
|
+
end
|
339
|
+
raster[i * bytes_per_component, bytes_per_component] = [v].pack(component_template)
|
340
|
+
else
|
341
|
+
raise
|
342
|
+
end
|
353
343
|
}
|
354
|
-
|
344
|
+
if pnm_type == 'P1'
|
345
|
+
raster.gsub!(/[01]{#{width}}/, "\\&\n")
|
346
|
+
if 70 < width
|
347
|
+
raster.gsub!(/[01]{70}/, "\\&\n")
|
348
|
+
end
|
349
|
+
raster << "\n" if /\n\z/ !~ raster
|
350
|
+
elsif /P[23]/ =~ pnm_type
|
351
|
+
components_per_line = /P2/ =~ pnm_type ? width : 3 * width
|
352
|
+
raster.gsub!(/ +/, ' ')
|
353
|
+
raster.gsub!(/( \d+){#{components_per_line}}/, "\\&\n")
|
354
|
+
raster.gsub!(/(\A|\n) +/, '\1')
|
355
|
+
raster.gsub!(/.{71,}\n/) {
|
356
|
+
$&.gsub(/(.{1,69})[ \n]/, "\\1\n")
|
357
|
+
}
|
358
|
+
raster << "\n" if /\n\z/ !~ raster
|
359
|
+
end
|
360
|
+
@io << (header+raster)
|
355
361
|
end
|
356
|
-
out << (header+raster)
|
357
362
|
end
|
363
|
+
|
358
364
|
end
|