image_size 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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