pnm 0.4.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +25 -20
- data/Rakefile +11 -10
- data/benchmark/bm_converter.rb +21 -17
- data/lib/pnm.rb +54 -54
- data/lib/pnm/converter.rb +44 -47
- data/lib/pnm/exceptions.rb +2 -0
- data/lib/pnm/image.rb +81 -54
- data/lib/pnm/parser.rb +48 -39
- data/lib/pnm/version.rb +4 -2
- data/pnm.gemspec +23 -19
- data/test/backports.rb +19 -0
- data/test/test_converter.rb +85 -80
- data/test/test_exceptions.rb +124 -120
- data/test/test_image.rb +198 -122
- data/test/test_parser.rb +57 -53
- data/test/test_pnm.rb +58 -54
- metadata +17 -16
data/lib/pnm/exceptions.rb
CHANGED
data/lib/pnm/image.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PNM
|
2
4
|
|
3
5
|
# Abstract base class for +PBM+, +PGM+, and +PPM+ images.
|
@@ -36,20 +38,20 @@ module PNM
|
|
36
38
|
# This method should be called as PNM.create.
|
37
39
|
# See there for a description of pixel data formats
|
38
40
|
# and available options.
|
39
|
-
def self.create(pixels,
|
41
|
+
def self.create(pixels, type: nil, maxgray: nil, comment: nil)
|
40
42
|
assert_valid_array(pixels)
|
41
|
-
assert_valid_maxgray(
|
42
|
-
assert_valid_comment(
|
43
|
+
assert_valid_maxgray(maxgray)
|
44
|
+
assert_valid_comment(comment)
|
43
45
|
|
44
|
-
type = sanitize_and_assert_valid_type(
|
45
|
-
type ||= detect_type(pixels,
|
46
|
+
type = sanitize_and_assert_valid_type(type)
|
47
|
+
type ||= detect_type(pixels, maxgray)
|
46
48
|
|
47
49
|
# except for type detection, the maxgray option must be ignored for PBM
|
48
|
-
if type == :pbm
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
maxgray = if type == :pbm
|
51
|
+
nil
|
52
|
+
else
|
53
|
+
maxgray
|
54
|
+
end
|
53
55
|
|
54
56
|
image_class = case type
|
55
57
|
when :pbm
|
@@ -60,7 +62,7 @@ module PNM
|
|
60
62
|
PPMImage
|
61
63
|
end
|
62
64
|
|
63
|
-
image_class.new(pixels, maxgray,
|
65
|
+
image_class.new(pixels, maxgray, comment)
|
64
66
|
end
|
65
67
|
|
66
68
|
class << self
|
@@ -80,22 +82,31 @@ module PNM
|
|
80
82
|
assert_pixel_value_range
|
81
83
|
|
82
84
|
post_initialize
|
85
|
+
|
86
|
+
@pixels.freeze
|
87
|
+
@comment.freeze
|
83
88
|
end
|
84
89
|
|
85
|
-
# Writes the image to +file+ (a filename or an IO object)
|
86
|
-
#
|
87
|
-
#
|
90
|
+
# Writes the image to +file+ (a filename or an IO object).
|
91
|
+
#
|
92
|
+
# When +add_extension+ is set to +true+ (default: +false+)
|
93
|
+
# the appropriate file extension is added to the provided filename
|
94
|
+
# (+.pbm+, +.pgm+, or +.ppm+).
|
95
|
+
#
|
96
|
+
# The encoding can be set using the +encoding+ keyword argument,
|
97
|
+
# valid options are +:binary+ (default) and +:ascii+.
|
88
98
|
#
|
89
99
|
# Returns the number of bytes written.
|
90
|
-
def write(file,
|
100
|
+
def write(file, add_extension: false, encoding: :binary)
|
91
101
|
content = if encoding == :ascii
|
92
102
|
to_ascii
|
93
103
|
elsif encoding == :binary
|
94
104
|
to_binary
|
95
105
|
end
|
96
106
|
|
97
|
-
if file.
|
98
|
-
|
107
|
+
if file.is_a?(String)
|
108
|
+
filename = add_extension ? "#{file}.#{type}" : file
|
109
|
+
File.binwrite(filename, content)
|
99
110
|
else
|
100
111
|
file.binmode
|
101
112
|
file.write content
|
@@ -107,35 +118,44 @@ module PNM
|
|
107
118
|
"#{type.to_s.upcase} #{width}x#{height} #{type_string}"
|
108
119
|
end
|
109
120
|
|
110
|
-
alias
|
121
|
+
alias to_s info
|
111
122
|
|
112
123
|
# Returns a string representation for debugging.
|
113
124
|
def inspect
|
114
125
|
# implemented by subclasses
|
115
126
|
end
|
116
127
|
|
117
|
-
|
128
|
+
# Equality --- Two images are considered equal if they have
|
129
|
+
# the same pixel values, type, maxgray, and comments.
|
130
|
+
def ==(other)
|
131
|
+
return true if other.equal?(self)
|
132
|
+
return false unless other.instance_of?(self.class)
|
133
|
+
|
134
|
+
type == other.type && maxgray == other.maxgray && comment == other.comment && pixels == other.pixels
|
135
|
+
end
|
118
136
|
|
119
137
|
def self.assert_valid_array(pixels) # :nodoc:
|
120
138
|
assert_array_dimensions(pixels)
|
121
139
|
assert_pixel_types(pixels)
|
122
140
|
end
|
141
|
+
private_class_method :assert_valid_array
|
123
142
|
|
124
143
|
def self.assert_array_dimensions(pixels) # :nodoc:
|
125
144
|
msg = "invalid pixel data: Array expected"
|
126
|
-
raise PNM::ArgumentError, msg unless Array
|
145
|
+
raise PNM::ArgumentError, msg unless pixels.is_a?(Array)
|
127
146
|
|
128
147
|
msg = "invalid pixel array"
|
129
|
-
|
130
148
|
raise PNM::DataError, msg unless pixels.map(&:class).uniq == [Array]
|
149
|
+
|
131
150
|
width = pixels.first.size
|
132
|
-
raise PNM::DataError, msg if width
|
151
|
+
raise PNM::DataError, msg if width.zero?
|
133
152
|
raise PNM::DataError, msg unless pixels.map(&:size).uniq == [width]
|
134
153
|
end
|
154
|
+
private_class_method :assert_array_dimensions
|
135
155
|
|
136
156
|
def self.assert_pixel_types(pixels) # :nodoc:
|
137
157
|
pixel_values = pixels.flatten(1)
|
138
|
-
is_color =
|
158
|
+
is_color = pixel_values.first.is_a?(Array)
|
139
159
|
|
140
160
|
if is_color
|
141
161
|
pixel_values.each {|pixel| assert_valid_color_pixel(pixel) }
|
@@ -143,43 +163,47 @@ module PNM
|
|
143
163
|
pixel_values.each {|pixel| assert_valid_pixel(pixel) }
|
144
164
|
end
|
145
165
|
end
|
166
|
+
private_class_method :assert_pixel_types
|
146
167
|
|
147
168
|
def self.assert_valid_pixel(pixel) # :nodoc:
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
169
|
+
return if pixel.is_a?(Integer)
|
170
|
+
|
171
|
+
msg = "invalid pixel value: Integer expected - #{pixel.inspect}"
|
172
|
+
raise PNM::DataError, msg
|
152
173
|
end
|
174
|
+
private_class_method :assert_valid_pixel
|
153
175
|
|
154
176
|
def self.assert_valid_color_pixel(pixel) # :nodoc:
|
155
|
-
|
156
|
-
msg = "invalid pixel value: "
|
157
|
-
msg << "Array of 3 Fixnums expected - #{pixel.inspect}"
|
177
|
+
return if pixel.is_a?(Array) && pixel.size == 3 && pixel.all? {|p| p.is_a?(Integer) }
|
158
178
|
|
159
|
-
|
160
|
-
|
179
|
+
msg = "invalid pixel value: ".dup
|
180
|
+
msg << "Array of 3 Integers expected - #{pixel.inspect}"
|
181
|
+
raise PNM::DataError, msg
|
161
182
|
end
|
183
|
+
private_class_method :assert_valid_color_pixel
|
162
184
|
|
163
185
|
def self.assert_valid_maxgray(maxgray) # :nodoc:
|
164
186
|
return unless maxgray
|
187
|
+
return if maxgray.is_a?(Integer) && maxgray > 0 && maxgray <= 255
|
165
188
|
|
166
|
-
|
167
|
-
|
168
|
-
end
|
189
|
+
msg = "invalid maxgray value - #{maxgray.inspect}"
|
190
|
+
raise PNM::ArgumentError, msg
|
169
191
|
end
|
192
|
+
private_class_method :assert_valid_maxgray
|
170
193
|
|
171
194
|
def self.assert_valid_comment(comment) # :nodoc:
|
172
195
|
return unless comment
|
196
|
+
return if comment.is_a?(String)
|
173
197
|
|
174
|
-
|
175
|
-
|
176
|
-
end
|
198
|
+
msg = "invalid comment value - #{comment.inspect}"
|
199
|
+
raise PNM::ArgumentError, msg
|
177
200
|
end
|
201
|
+
private_class_method :assert_valid_comment
|
178
202
|
|
179
203
|
def self.sanitize_and_assert_valid_type(type) # :nodoc:
|
180
204
|
return unless type
|
181
205
|
|
182
|
-
type = type.to_sym if type.
|
206
|
+
type = type.to_sym if type.is_a?(String)
|
183
207
|
|
184
208
|
unless [:pbm, :pgm, :ppm].include?(type)
|
185
209
|
msg = "invalid image type - #{type.inspect}"
|
@@ -188,9 +212,10 @@ module PNM
|
|
188
212
|
|
189
213
|
type
|
190
214
|
end
|
215
|
+
private_class_method :sanitize_and_assert_valid_type
|
191
216
|
|
192
217
|
def self.detect_type(pixels, maxgray) # :nodoc:
|
193
|
-
if pixels.first.first.
|
218
|
+
if pixels.first.first.is_a?(Array)
|
194
219
|
:ppm
|
195
220
|
elsif (maxgray && maxgray > 1) || pixels.flatten.max > 1
|
196
221
|
:pgm
|
@@ -198,25 +223,27 @@ module PNM
|
|
198
223
|
:pbm
|
199
224
|
end
|
200
225
|
end
|
226
|
+
private_class_method :detect_type
|
227
|
+
|
228
|
+
private
|
201
229
|
|
202
230
|
def assert_grayscale_data # :nodoc:
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
231
|
+
return unless color_pixels?
|
232
|
+
|
233
|
+
msg = "specified type does not match RGB data - #{type.inspect}"
|
234
|
+
raise PNM::DataError, msg
|
207
235
|
end
|
208
236
|
|
209
237
|
def assert_pixel_value_range # :nodoc:
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
end
|
238
|
+
msg = "invalid data: value(s) greater than maxgray"
|
239
|
+
raise PNM::DataError, msg unless pixels.flatten.max <= maxgray
|
240
|
+
|
241
|
+
msg = "invalid data: value(s) less than zero"
|
242
|
+
raise PNM::DataError, msg unless pixels.flatten.min >= 0
|
216
243
|
end
|
217
244
|
|
218
245
|
def header_without_maxgray(encoding) # :nodoc:
|
219
|
-
header =
|
246
|
+
header = "#{PNM.magic_number[type][encoding]}\n".dup
|
220
247
|
comment_lines.each do |line|
|
221
248
|
header << (line.empty? ? "#\n" : "# #{line}\n")
|
222
249
|
end
|
@@ -231,7 +258,7 @@ module PNM
|
|
231
258
|
|
232
259
|
def comment_lines # :nodoc:
|
233
260
|
return [] unless comment
|
234
|
-
return [
|
261
|
+
return [""] if comment.empty?
|
235
262
|
|
236
263
|
keep_trailing_null_fields = -1 # magic value for split limit
|
237
264
|
comment.split(/\n/, keep_trailing_null_fields)
|
@@ -250,11 +277,11 @@ module PNM
|
|
250
277
|
end
|
251
278
|
|
252
279
|
def color_pixels? # :nodoc:
|
253
|
-
|
280
|
+
pixels.first.first.is_a?(Array)
|
254
281
|
end
|
255
282
|
|
256
283
|
def inspect_string_with_maxgray # :nodoc:
|
257
|
-
|
284
|
+
"#<%s:0x%x %s, maxgray=%d>" % [self.class.name, object_id, info, maxgray]
|
258
285
|
end
|
259
286
|
|
260
287
|
def inspect_string_without_maxgray # :nodoc:
|
data/lib/pnm/parser.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module PNM
|
2
4
|
|
3
5
|
# @private
|
@@ -20,14 +22,14 @@ module PNM
|
|
20
22
|
content = content.dup
|
21
23
|
|
22
24
|
magic_number = nil
|
23
|
-
tokens
|
25
|
+
tokens = []
|
24
26
|
comments = []
|
25
27
|
|
26
28
|
until magic_number
|
27
29
|
token = next_token!(content)
|
28
30
|
|
29
|
-
if token.start_with?(
|
30
|
-
comments << token.gsub(/# */,
|
31
|
+
if token.start_with?("#")
|
32
|
+
comments << token.gsub(/# */, "")
|
31
33
|
else
|
32
34
|
magic_number = token
|
33
35
|
end
|
@@ -36,11 +38,11 @@ module PNM
|
|
36
38
|
assert_valid_magic_number(magic_number)
|
37
39
|
|
38
40
|
while tokens.size < token_number[magic_number]
|
39
|
-
content.gsub!(/\A[ \t\r\n]+/,
|
41
|
+
content.gsub!(/\A[ \t\r\n]+/, "")
|
40
42
|
token = next_token!(content)
|
41
43
|
|
42
|
-
if token.start_with?(
|
43
|
-
comments << token.gsub(/# */,
|
44
|
+
if token.start_with?("#")
|
45
|
+
comments << token.gsub(/# */, "")
|
44
46
|
else
|
45
47
|
tokens << token
|
46
48
|
end
|
@@ -48,23 +50,23 @@ module PNM
|
|
48
50
|
|
49
51
|
width, height, maxgray = tokens
|
50
52
|
|
51
|
-
assert_integer(width,
|
52
|
-
assert_integer(height,
|
53
|
+
assert_integer(width, "width")
|
54
|
+
assert_integer(height, "height")
|
53
55
|
assert_integer(maxgray, "maxgray") if maxgray
|
54
56
|
|
55
|
-
width
|
56
|
-
height
|
57
|
+
width = width.to_i
|
58
|
+
height = height.to_i
|
57
59
|
maxgray = maxgray.to_i if maxgray
|
58
60
|
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
assert_positive(width, "width")
|
62
|
+
assert_positive(height, "height")
|
63
|
+
assert_in_maxgray_range(maxgray) if maxgray
|
62
64
|
|
63
65
|
result = {
|
64
|
-
:
|
65
|
-
:
|
66
|
-
:
|
67
|
-
:
|
66
|
+
magic_number: magic_number,
|
67
|
+
width: width,
|
68
|
+
height: height,
|
69
|
+
data: content
|
68
70
|
}
|
69
71
|
result[:maxgray] = maxgray if maxgray
|
70
72
|
result[:comments] = comments unless comments.empty?
|
@@ -74,24 +76,24 @@ module PNM
|
|
74
76
|
|
75
77
|
def self.token_number
|
76
78
|
{
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
79
|
+
"P1" => 2,
|
80
|
+
"P2" => 3,
|
81
|
+
"P3" => 3,
|
82
|
+
"P4" => 2,
|
83
|
+
"P5" => 3,
|
84
|
+
"P6" => 3
|
83
85
|
}
|
84
86
|
end
|
85
87
|
|
86
88
|
def self.next_token!(content)
|
87
|
-
delimiter = if content.start_with?(
|
89
|
+
delimiter = if content.start_with?("#")
|
88
90
|
"\n"
|
89
91
|
else
|
90
|
-
|
92
|
+
/[ \t\r\n]|(?=#)/
|
91
93
|
end
|
92
94
|
|
93
95
|
token, rest = content.split(delimiter, 2)
|
94
|
-
raise PNM::ParserError,
|
96
|
+
raise PNM::ParserError, "not enough tokens in file" unless rest
|
95
97
|
|
96
98
|
content.replace(rest)
|
97
99
|
|
@@ -99,24 +101,31 @@ module PNM
|
|
99
101
|
end
|
100
102
|
|
101
103
|
def self.assert_valid_magic_number(magic_number)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
return if %w[P1 P2 P3 P4 P5 P6].include?(magic_number)
|
105
|
+
|
106
|
+
msg = "unknown magic number - `#{magic_number}'"
|
107
|
+
raise PNM::ParserError, msg
|
106
108
|
end
|
107
109
|
|
108
110
|
def self.assert_integer(value_string, value_name)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
111
|
+
return if value_string =~ /\A[0-9]+\Z/
|
112
|
+
|
113
|
+
msg = "#{value_name} must be an integer - `#{value_string}'"
|
114
|
+
raise PNM::ParserError, msg
|
113
115
|
end
|
114
116
|
|
115
|
-
def self.
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
117
|
+
def self.assert_positive(value, name)
|
118
|
+
return if value > 0
|
119
|
+
|
120
|
+
msg = "#{name} must be greater than 0 - `#{value}'"
|
121
|
+
raise PNM::ParserError, msg
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.assert_in_maxgray_range(value)
|
125
|
+
return if value > 0 && value <= 255
|
126
|
+
|
127
|
+
msg = "invalid maxgray value - `#{value}'"
|
128
|
+
raise PNM::ParserError, msg
|
120
129
|
end
|
121
130
|
end
|
122
131
|
end
|
data/lib/pnm/version.rb
CHANGED
data/pnm.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "./lib/pnm"
|
2
4
|
|
3
5
|
version = PNM::VERSION
|
4
6
|
date = PNM::DATE
|
@@ -6,39 +8,41 @@ homepage = PNM::HOMEPAGE
|
|
6
8
|
tagline = PNM::TAGLINE
|
7
9
|
|
8
10
|
Gem::Specification.new do |s|
|
9
|
-
s.name =
|
11
|
+
s.name = "pnm"
|
10
12
|
s.version = version
|
11
13
|
s.date = date
|
12
14
|
|
13
|
-
s.description =
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
s.description = "PNM is a pure Ruby library for creating, reading, " \
|
16
|
+
"and writing of PNM image files (Portable Anymap): " \
|
17
|
+
"PBM (Portable Bitmap), " \
|
18
|
+
"PGM (Portable Graymap), and " \
|
19
|
+
"PPM (Portable Pixmap)."
|
18
20
|
s.summary = "PNM - #{tagline}"
|
19
21
|
|
20
|
-
s.authors = [
|
21
|
-
s.email =
|
22
|
+
s.authors = ["Marcus Stollsteimer"]
|
23
|
+
s.email = "sto.mar@web.de"
|
22
24
|
s.homepage = homepage
|
23
25
|
|
24
|
-
s.license =
|
26
|
+
s.license = "GPL-3.0"
|
25
27
|
|
26
|
-
s.required_ruby_version =
|
28
|
+
s.required_ruby_version = ">= 2.0.0"
|
27
29
|
|
28
|
-
s.add_development_dependency
|
29
|
-
s.add_development_dependency
|
30
|
+
s.add_development_dependency "minitest", ">= 5.0"
|
31
|
+
s.add_development_dependency "rake", ">= 10.0"
|
30
32
|
|
31
|
-
s.
|
33
|
+
s.require_paths = ["lib"]
|
32
34
|
|
33
|
-
s.test_files = Dir.glob(
|
35
|
+
s.test_files = Dir.glob("test/**/test_*.rb")
|
34
36
|
|
35
|
-
s.files =
|
37
|
+
s.files =
|
38
|
+
%w[
|
36
39
|
README.md
|
37
40
|
Rakefile
|
38
41
|
pnm.gemspec
|
39
42
|
.yardopts
|
40
|
-
|
41
|
-
Dir.glob(
|
43
|
+
] +
|
44
|
+
Dir.glob("{benchmark,lib,test}/**/*")
|
42
45
|
|
43
|
-
s.
|
46
|
+
s.extra_rdoc_files = ["README.md"]
|
47
|
+
s.rdoc_options = ["--charset=UTF-8", "--main=README.md"]
|
44
48
|
end
|