image_size 1.0.6 → 1.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.
@@ -1,7 +1,7 @@
1
1
  # image_size
2
2
 
3
3
  measure image size using pure Ruby
4
- formats: PCX, PSD, XPM, TIFF, XBM, PGM, PBM, PPM, BMP, JPEG, PNG, GIF, SWF
4
+ formats: `bmp`, `gif`, `jpeg`, `pbm`, `pcx`, `pgm`, `png`, `ppm`, `psd`, `swf`, `tiff`, `xbm`, `xpm`
5
5
 
6
6
  ## Download
7
7
 
@@ -11,13 +11,27 @@ The latest version of image\_size can be found at http://github.com/toy/image_si
11
11
 
12
12
  gem install image_size
13
13
 
14
- ## Simple Example
14
+ ## Examples
15
15
 
16
- ruby "image_size"
17
- ruby "open-uri"
16
+ require 'image_size'
18
17
 
19
- open("http://www.rubycgi.org/image/ruby_gtk_book_title.jpg", "rb") do |fh|
20
- p ImageSize.new(fh.read).size
18
+ p ImageSize.path('spec/test.jpg').size
19
+
20
+ open('spec/test.jpg', 'rb') do |fh|
21
+ p ImageSize.new(fh).size
22
+ end
23
+
24
+
25
+ require 'image_size'
26
+ require 'open-uri'
27
+
28
+ open('http://www.rubycgi.org/image/ruby_gtk_book_title.jpg', 'rb') do |fh|
29
+ p ImageSize.new(fh).size
30
+ end
31
+
32
+ open('http://www.rubycgi.org/image/ruby_gtk_book_title.jpg', 'rb') do |fh|
33
+ data = fh.read
34
+ p ImageSize.new(data).size
21
35
  end
22
36
 
23
37
  ## Licence
@@ -2,9 +2,9 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_size'
5
- s.version = '1.0.6'
5
+ s.version = '1.1.0'
6
6
  s.summary = %q{Measure image size using pure Ruby}
7
- s.description = %q{Measure following file dimensions: PCX, PSD, XPM, TIFF, XBM, PGM, PBM, PPM, BMP, JPEG, PNG, GIF, SWF}
7
+ s.description = %q{Measure following file dimensions: bmp, gif, jpeg, pbm, pcx, pgm, png, ppm, psd, swf, tiff, xbm, xpm}
8
8
  s.homepage = "http://github.com/toy/#{s.name}"
9
9
  s.authors = ['Keisuke Minami', 'Ivan Kuchin']
10
10
  s.license = 'MIT'
@@ -1,90 +1,105 @@
1
1
  require 'stringio'
2
+ require 'tempfile'
2
3
 
3
4
  class ImageSize
4
- attr_reader :format, :width, :height
5
+ class Size < Array
6
+ # join using 'x'
7
+ def to_s
8
+ join('x')
9
+ end
10
+ end
5
11
 
6
- # receive image & make size
7
- # argument is image String, StringIO or IO
8
- def initialize(data)
9
- data = data.dup
10
-
11
- case data
12
- when IO, StringIO
13
- img_top = data.read(1024)
14
- img_io = def_read_o(data)
15
- when String
16
- img_top = data[0, 1024]
17
- img_io = StringIO.open(data)
18
- img_io = def_read_o(img_io)
19
- else
20
- raise ArgumentError.new("expected instance of IO, StringIO or String, got #{data.class}")
12
+ class ImageReader # :nodoc:
13
+ def initialize(data_or_io)
14
+ @io = case data_or_io
15
+ when IO, StringIO, Tempfile
16
+ data_or_io.dup.tap(&:rewind)
17
+ when String
18
+ StringIO.new(data_or_io)
19
+ else
20
+ raise ArgumentError.new("expected instance of IO, StringIO, Tempfile or String, got #{data.class}")
21
+ end
22
+ @read = 0
23
+ @data = ''
21
24
  end
22
25
 
23
- if @format = check_format(img_top)
24
- @width, @height = self.send("measure_#{@format}", img_io)
26
+ def rewind
27
+ @io.rewind
25
28
  end
26
29
 
27
- if data.is_a?(String)
28
- img_io.close
30
+ CHUNK = 1024
31
+ def [](offset, length)
32
+ while offset + length > @read
33
+ @read += CHUNK
34
+ if data = @io.read(CHUNK)
35
+ @data << data
36
+ end
37
+ end
38
+ @data[offset, length]
29
39
  end
30
40
  end
31
41
 
32
- # get image width and height as an array which to_s method returns "#{width}x#{height}"
33
- def size
34
- if format
35
- size = [width, height]
36
- def size.to_s
37
- join('x')
38
- end
39
- size
42
+ # Given path to image finds its format, width and height
43
+ def self.path(path)
44
+ open(path, 'rb'){ |f| new(f) }
45
+ end
46
+
47
+ # Given image as IO, StringIO, Tempfile or String finds its format and dimensions
48
+ def initialize(data)
49
+ ir = ImageReader.new(data)
50
+ if @format = detect_format(ir)
51
+ @width, @height = self.send("size_of_#{@format}", ir)
40
52
  end
53
+ ir.rewind
41
54
  end
42
55
 
43
- alias :h :height
56
+ # Image format
57
+ attr_reader :format
58
+
59
+ # Image width
60
+ attr_reader :width
44
61
  alias :w :width
45
62
 
46
- private
63
+ # Image height
64
+ attr_reader :height
65
+ alias :h :height
47
66
 
48
- def def_read_o(io)
49
- io.seek(0, 0)
50
- # define Singleton-method definition to IO (byte, offset)
51
- def io.read_o(length = 1, offset = nil)
52
- self.seek(offset, 0) if offset
53
- ret = self.read(length)
54
- raise 'cannot read!!' unless ret
55
- ret
67
+ # get image width and height as an array which to_s method returns "#{width}x#{height}"
68
+ def size
69
+ if format
70
+ Size.new([width, height])
56
71
  end
57
- io
58
72
  end
59
73
 
60
- def check_format(img_top)
74
+ private
75
+
76
+ def detect_format(ir)
77
+ head = ir[0, 1024]
61
78
  case
62
- when img_top =~ /^GIF8[7,9]a/ then :gif
63
- when img_top[0, 8] == "\x89PNG\x0d\x0a\x1a\x0a" then :png
64
- when img_top[0, 2] == "\xFF\xD8" then :jpeg
65
- when img_top[0, 2] == 'BM' then :bmp
66
- when img_top =~ /^P[1-7]/ then :ppm
67
- when img_top =~ /\#define\s+\S+\s+\d+/ then :xbm
68
- when img_top[0, 4] == "MM\x00\x2a" then :tiff
69
- when img_top[0, 4] == "II\x2a\x00" then :tiff
70
- when img_top[0, 4] == "MM\x00\x2a" then :tiff
71
- when img_top[0, 4] == "II\x2a\x00" then :tiff
72
- when img_top =~ /\/\* XPM \*\// then :xpm
73
- when img_top[0, 4] == '8BPS' then :psd
74
- when img_top =~ /^[FC]WS/ then :swf
75
- when img_top[0, 1] == "\x0a" then :pcx
79
+ when head =~ /^GIF8[7,9]a/ then :gif
80
+ when head[0, 8] == "\211PNG\r\n\032\n" then :png
81
+ when head[0, 2] == "\377\330" then :jpeg
82
+ when head[0, 2] == 'BM' then :bmp
83
+ when head =~ /^P[1-7]/ then :ppm
84
+ when head =~ /\#define\s+\S+\s+\d+/ then :xbm
85
+ when head[0, 4] == "II*\000" then :tiff
86
+ when head[0, 4] == "MM\000*" then :tiff
87
+ when head =~ /\/\* XPM \*\// then :xpm
88
+ when head[0, 4] == '8BPS' then :psd
89
+ when head =~ /^[FC]WS/ then :swf
90
+ when head[0, 1] == "\n" then :pcx
76
91
  end
77
92
  end
78
93
 
79
- def measure_gif(img_io)
80
- img_io.read_o(6)
81
- img_io.read_o(4).unpack('vv')
94
+ def size_of_gif(ir)
95
+ ir[6, 4].unpack('vv')
82
96
  end
83
97
 
84
- def measure_png(img_io)
85
- img_io.read_o(12)
86
- raise 'This file is not PNG.' unless img_io.read_o(4) == 'IHDR'
87
- img_io.read_o(8).unpack('NN')
98
+ def size_of_png(ir)
99
+ unless ir[12, 4] == 'IHDR'
100
+ raise 'IHDR not in place for PNG'
101
+ end
102
+ ir[16, 8].unpack('NN')
88
103
  end
89
104
 
90
105
  JpegCodeCheck = [
@@ -92,129 +107,98 @@ private
92
107
  "\xc5", "\xc6", "\xc7",
93
108
  "\xc9", "\xca", "\xcb",
94
109
  "\xcd", "\xce", "\xcf",
95
- ]
96
- def measure_jpeg(img_io)
97
- c_marker = "\xFF" # Section marker.
98
- img_io.read_o(2)
110
+ ] # :nodoc:
111
+ def size_of_jpeg(ir)
112
+ section_marker = "\xFF"
113
+ offset = 2
99
114
  loop do
100
- marker, code, length = img_io.read_o(4).unpack('aan')
101
- raise 'JPEG marker not found!' if marker != c_marker
115
+ marker, code, length = ir[offset, 4].unpack('aan')
116
+ offset += 4
117
+ raise 'JPEG marker not found' if marker != section_marker
102
118
 
103
119
  if JpegCodeCheck.include?(code)
104
- height, width = img_io.read_o(5).unpack('xnn')
105
- return [width, height]
120
+ return ir[offset + 1, 4].unpack('nn').reverse
106
121
  end
107
- img_io.read_o(length - 2)
122
+ offset += length - 2
108
123
  end
109
124
  end
110
125
 
111
- def measure_bmp(img_io)
112
- img_io.read_o(26).unpack('x18VV');
126
+ def size_of_bmp(ir)
127
+ ir[18, 8].unpack('VV')
113
128
  end
114
129
 
115
- def measure_ppm(img_io)
116
- header = img_io.read_o(1024)
130
+ def size_of_ppm(ir)
131
+ header = ir[0, 1024]
117
132
  header.gsub!(/^\#[^\n\r]*/m, '')
118
133
  header =~ /^(P[1-6])\s+?(\d+)\s+?(\d+)/m
119
134
  case $1
120
135
  when 'P1', 'P4' then @format = :pbm
121
136
  when 'P2', 'P5' then @format = :pgm
122
137
  end
123
-
124
138
  [$2.to_i, $3.to_i]
125
139
  end
126
140
 
127
- alias :measure_pgm :measure_ppm
128
- alias :measure_pbm :measure_ppm
129
-
130
- def measure_xbm(img_io)
131
- img_io.read_o(1024) =~ /^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi
132
-
141
+ def size_of_xbm(ir)
142
+ ir[0, 1024] =~ /^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi
133
143
  [$1.to_i, $2.to_i]
134
144
  end
135
145
 
136
- def measure_xpm(img_io)
137
- width = height = nil
138
- while(line = img_io.read_o(1024))
139
- if line =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m
140
- width = $1.to_i; height = $2.to_i
141
- break
146
+ def size_of_xpm(ir)
147
+ length = 1024
148
+ until (data = ir[0, length]) =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m
149
+ if data.length != length
150
+ raise 'XPM size not found'
142
151
  end
152
+ length += 1024
143
153
  end
144
-
145
- [width, height]
154
+ [$1.to_i, $2.to_i]
146
155
  end
147
156
 
148
- def measure_psd(img_io)
149
- img_io.read_o(26).unpack('x14NN')
157
+ def size_of_psd(ir)
158
+ ir[14, 8].unpack('NN')
150
159
  end
151
160
 
152
- def measure_tiff(img_io)
153
- endian = (img_io.read_o(4) =~ /II\x2a\x00/o) ? 'v' : 'n' # 'v' little-endian 'n' default to big-endian
161
+ def size_of_tiff(ir)
162
+ endian2b = (ir[0, 4] == "II*\000") ? 'v' : 'n'
163
+ endian4b = endian2b.upcase
164
+ packspec = [nil, 'C', nil, endian2b, endian4b, nil, 'c', nil, endian2b, endian4b]
154
165
 
155
- packspec = [
156
- nil, # nothing (shouldn't happen)
157
- 'C', # BYTE (8-bit unsigned integer)
158
- nil, # ASCII
159
- endian, # SHORT (16-bit unsigned integer)
160
- endian.upcase, # LONG (32-bit unsigned integer)
161
- nil, # RATIONAL
162
- 'c', # SBYTE (8-bit signed integer)
163
- nil, # UNDEFINED
164
- endian, # SSHORT (16-bit unsigned integer)
165
- endian.upcase, # SLONG (32-bit unsigned integer)
166
- ]
167
-
168
- offset = img_io.read_o(4).unpack(endian.upcase)[0] # Get offset to IFD
169
-
170
- ifd = img_io.read_o(2, offset)
171
- num_dirent = ifd.unpack(endian)[0] # Make it useful
166
+ offset = ir[4, 4].unpack(endian4b)[0]
167
+ num_dirent = ir[offset, 2].unpack(endian2b)[0]
172
168
  offset += 2
173
- num_dirent = offset + (num_dirent * 12); # Calc. maximum offset of IFD
169
+ num_dirent = offset + (num_dirent * 12)
174
170
 
175
- ifd = width = height = nil
176
- while(width.nil? || height.nil?)
177
- ifd = img_io.read_o(12, offset) # Get first directory entry
178
- break if (ifd.nil? || (offset > num_dirent))
171
+ width = height = nil
172
+ until width && height
173
+ ifd = ir[offset, 12]
174
+ raise 'Reached end of directory entries in TIFF' if ifd.nil? || offset > num_dirent
175
+ tag, type = ifd.unpack(endian2b * 2)
179
176
  offset += 12
180
- tag = ifd.unpack(endian)[0] # ...and decode its tag
181
- type = ifd[2, 2].unpack(endian)[0] # ...and the data type
182
-
183
- # Check the type for sanity.
184
- next if (type > packspec.size + 0) || (packspec[type].nil?)
185
- if tag == 0x0100 # Decode the value
186
- width = ifd[8, 4].unpack(packspec[type])[0]
187
- elsif tag == 0x0101 # Decode the value
188
- height = ifd[8, 4].unpack(packspec[type])[0]
177
+
178
+ unless packspec[type].nil?
179
+ value = ifd[8, 4].unpack(packspec[type])[0]
180
+ case tag
181
+ when 0x0100
182
+ width = value
183
+ when 0x0101
184
+ height = value
185
+ end
189
186
  end
190
187
  end
191
-
192
188
  [width, height]
193
189
  end
194
190
 
195
- def measure_pcx(img_io)
196
- header = img_io.read_o(128)
197
- head_part = header.unpack('C4S4')
198
-
199
- [head_part[6] - head_part[4] + 1, head_part[7] - head_part[5] + 1]
191
+ def size_of_pcx(ir)
192
+ parts = ir[4, 8].unpack('S4')
193
+ [parts[2] - parts[0] + 1, parts[3] - parts[1] + 1]
200
194
  end
201
195
 
202
- def measure_swf(img_io)
203
- header = img_io.read_o(9)
204
-
205
- sig1 = header[0, 1]
206
- sig2 = header[1, 1]
207
- sig3 = header[2, 1]
208
-
209
- bit_length = Integer("0b#{header.unpack('@8B5').first}")
210
- header << img_io.read_o(bit_length * 4 / 8 + 1)
211
- str = header.unpack("@8B#{5 + bit_length * 4}")[0]
212
- last = 5
213
- x_min = Integer("0b#{str[last, bit_length]}")
214
- x_max = Integer("0b#{str[(last += bit_length), bit_length]}")
215
- y_min = Integer("0b#{str[(last += bit_length), bit_length]}")
216
- y_max = Integer("0b#{str[(last += bit_length), bit_length]}")
217
-
196
+ def size_of_swf(ir)
197
+ value_bit_length = ir[8, 1].unpack('B5').first.to_i(2)
198
+ bit_length = 5 + value_bit_length * 4
199
+ rect_bits = ir[8, bit_length / 8 + 1].unpack("B#{bit_length}").first
200
+ values = rect_bits.unpack('@5' + "a#{value_bit_length}" * 4).map{ |bits| bits.to_i(2) }
201
+ x_min, x_max, y_min, y_max = values
218
202
  [(x_max - x_min) / 20, (y_max - y_min) / 20]
219
203
  end
220
204
  end
@@ -20,7 +20,7 @@ describe ImageSize do
20
20
  ].each do |name, format, width, height|
21
21
  path = File.join(File.dirname(__FILE__), name)
22
22
 
23
- it "should get format and dimensions for #{name} given io" do
23
+ it "should get format and dimensions for #{name} given IO" do
24
24
  File.open(path, 'rb') do |fh|
25
25
  is = ImageSize.new(fh)
26
26
  [is.format, is.width, is.height].should == [format, width, height]
@@ -40,5 +40,29 @@ describe ImageSize do
40
40
  [is.format, is.width, is.height].should == [format, width, height]
41
41
  end
42
42
  end
43
+
44
+ it "should get format and dimensions for #{name} given Tempfile" do
45
+ file_data = File.open(path, 'rb') { |fh| fh.read }
46
+ Tempfile.open(name) do |tf|
47
+ tf.write(file_data)
48
+ tf.rewind
49
+ is = ImageSize.new(tf)
50
+ [is.format, is.width, is.height].should == [format, width, height]
51
+ end
52
+ end
53
+
54
+ it "should get format and dimensions for #{name} given IO when run twice" do
55
+ File.open(path, 'rb') do |fh|
56
+ is = ImageSize.new(fh)
57
+ [is.format, is.width, is.height].should == [format, width, height]
58
+ is = ImageSize.new(fh)
59
+ [is.format, is.width, is.height].should == [format, width, height]
60
+ end
61
+ end
62
+
63
+ it "should get format and dimensions for #{name} as path" do
64
+ is = ImageSize.path(path)
65
+ [is.format, is.width, is.height].should == [format, width, height]
66
+ end
43
67
  end
44
68
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_size
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 6
10
- version: 1.0.6
10
+ version: 1.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Keisuke Minami
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2012-02-08 00:00:00 Z
19
+ date: 2012-02-25 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: rspec
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: "0"
33
33
  type: :development
34
34
  version_requirements: *id001
35
- description: "Measure following file dimensions: PCX, PSD, XPM, TIFF, XBM, PGM, PBM, PPM, BMP, JPEG, PNG, GIF, SWF"
35
+ description: "Measure following file dimensions: bmp, gif, jpeg, pbm, pcx, pgm, png, ppm, psd, swf, tiff, xbm, xpm"
36
36
  email:
37
37
  executables: []
38
38
 
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
88
  requirements: []
89
89
 
90
90
  rubyforge_project: image_size
91
- rubygems_version: 1.8.15
91
+ rubygems_version: 1.8.16
92
92
  signing_key:
93
93
  specification_version: 3
94
94
  summary: Measure image size using pure Ruby