tb 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README +156 -5
  2. data/bin/tb +2 -1110
  3. data/lib/tb.rb +4 -2
  4. data/lib/tb/catreader.rb +131 -0
  5. data/lib/tb/cmd_cat.rb +65 -0
  6. data/lib/tb/cmd_consecutive.rb +79 -0
  7. data/lib/tb/cmd_crop.rb +105 -0
  8. data/lib/tb/cmd_cross.rb +119 -0
  9. data/lib/tb/cmd_csv.rb +42 -0
  10. data/lib/tb/cmd_cut.rb +77 -0
  11. data/lib/tb/cmd_grep.rb +76 -0
  12. data/lib/tb/cmd_group.rb +82 -0
  13. data/lib/tb/cmd_gsub.rb +77 -0
  14. data/lib/tb/cmd_help.rb +98 -0
  15. data/lib/tb/cmd_join.rb +81 -0
  16. data/lib/tb/cmd_json.rb +60 -0
  17. data/lib/tb/cmd_ls.rb +273 -0
  18. data/lib/tb/cmd_mheader.rb +77 -0
  19. data/lib/tb/cmd_newfield.rb +59 -0
  20. data/lib/tb/cmd_pnm.rb +43 -0
  21. data/lib/tb/cmd_pp.rb +70 -0
  22. data/lib/tb/cmd_rename.rb +58 -0
  23. data/lib/tb/cmd_shape.rb +67 -0
  24. data/lib/tb/cmd_sort.rb +58 -0
  25. data/lib/tb/cmd_svn_log.rb +158 -0
  26. data/lib/tb/cmd_tsv.rb +43 -0
  27. data/lib/tb/cmd_yaml.rb +47 -0
  28. data/lib/tb/cmdmain.rb +45 -0
  29. data/lib/tb/cmdtop.rb +58 -0
  30. data/lib/tb/cmdutil.rb +327 -0
  31. data/lib/tb/csv.rb +30 -6
  32. data/lib/tb/fieldset.rb +39 -41
  33. data/lib/tb/pager.rb +132 -0
  34. data/lib/tb/pnm.rb +357 -0
  35. data/lib/tb/reader.rb +18 -128
  36. data/lib/tb/record.rb +3 -3
  37. data/lib/tb/ropen.rb +70 -0
  38. data/lib/tb/{pathfinder.rb → search.rb} +69 -34
  39. data/lib/tb/tsv.rb +29 -1
  40. data/sample/colors.ppm +0 -0
  41. data/sample/gradation.pgm +0 -0
  42. data/sample/langs.csv +46 -0
  43. data/sample/tbplot +293 -0
  44. data/test-all-cov.rb +65 -0
  45. data/test-all.rb +5 -0
  46. data/test/test_basic.rb +99 -2
  47. data/test/test_catreader.rb +27 -0
  48. data/test/test_cmd_cat.rb +118 -0
  49. data/test/test_cmd_consecutive.rb +90 -0
  50. data/test/test_cmd_crop.rb +101 -0
  51. data/test/test_cmd_cross.rb +113 -0
  52. data/test/test_cmd_csv.rb +129 -0
  53. data/test/test_cmd_cut.rb +100 -0
  54. data/test/test_cmd_grep.rb +89 -0
  55. data/test/test_cmd_group.rb +181 -0
  56. data/test/test_cmd_gsub.rb +103 -0
  57. data/test/test_cmd_help.rb +190 -0
  58. data/test/test_cmd_join.rb +197 -0
  59. data/test/test_cmd_json.rb +75 -0
  60. data/test/test_cmd_ls.rb +203 -0
  61. data/test/test_cmd_mheader.rb +86 -0
  62. data/test/test_cmd_newfield.rb +63 -0
  63. data/test/test_cmd_pnm.rb +35 -0
  64. data/test/test_cmd_pp.rb +62 -0
  65. data/test/test_cmd_rename.rb +91 -0
  66. data/test/test_cmd_shape.rb +50 -0
  67. data/test/test_cmd_sort.rb +105 -0
  68. data/test/test_cmd_tsv.rb +67 -0
  69. data/test/test_cmd_yaml.rb +55 -0
  70. data/test/test_cmdtty.rb +154 -0
  71. data/test/test_cmdutil.rb +43 -0
  72. data/test/test_csv.rb +10 -0
  73. data/test/test_fieldset.rb +42 -0
  74. data/test/test_pager.rb +142 -0
  75. data/test/test_pnm.rb +374 -0
  76. data/test/test_reader.rb +147 -0
  77. data/test/test_record.rb +49 -0
  78. data/test/test_search.rb +575 -0
  79. data/test/test_tsv.rb +7 -0
  80. metadata +108 -5
  81. data/lib/tb/qtsv.rb +0 -93
@@ -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
- begin
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
 
@@ -27,12 +27,44 @@
27
27
  class Tb::FieldSet
28
28
  def initialize(*fs)
29
29
  @header = []
30
- add_fields(*fs) if !fs.empty?
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 = self.header.index(f)
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 self.length <= i
44
- fs2 = extend_length(i+1)
45
- fs2.last
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 = self.header[i]
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
@@ -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
@@ -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