tb 0.9 → 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 +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
|