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.
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