image_size 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +69 -0
  3. data/.rubocop.yml +27 -4
  4. data/.rubocop_todo.yml +11 -8
  5. data/CHANGELOG.markdown +19 -0
  6. data/GPL +340 -0
  7. data/Gemfile +1 -3
  8. data/LICENSE.txt +56 -0
  9. data/README.markdown +22 -18
  10. data/image_size.gemspec +6 -5
  11. data/lib/image_size/chunky_reader.rb +44 -0
  12. data/lib/image_size/reader.rb +64 -0
  13. data/lib/image_size/seekable_io_reader.rb +28 -0
  14. data/lib/image_size/stream_io_reader.rb +22 -0
  15. data/lib/image_size/string_reader.rb +21 -0
  16. data/lib/image_size/uri_reader.rb +88 -0
  17. data/lib/image_size.rb +159 -85
  18. data/spec/image_size/chunky_reader_spec.rb +69 -0
  19. data/spec/image_size_spec.rb +45 -2
  20. data/spec/images/.gitattributes +1 -0
  21. data/spec/images/cur/32x256.cur +0 -0
  22. data/spec/images/ico/32x256.ico +0 -0
  23. data/spec/images/jp2/163x402.jp2 +0 -0
  24. data/spec/images/jp2/176x373.jpx +0 -0
  25. data/spec/images/jp2/224x293.j2c +0 -0
  26. data/spec/images/jpeg/436x429.jpeg +0 -0
  27. data/spec/images/jpeg/extraneous-bytes.436x429.jpeg +0 -0
  28. data/spec/images/mng/61x42.mng +0 -0
  29. data/spec/images/{apng → png}/192x110.apng +0 -0
  30. data/spec/images/pnm/22x25.pam +8 -0
  31. data/spec/images/pnm/22x25.pbm +0 -0
  32. data/spec/images/pnm/22x25.pgm +4 -0
  33. data/spec/images/pnm/22x25.ppm +4 -0
  34. data/spec/images/pnm/ascii.22x25.pbm +27 -0
  35. data/spec/images/pnm/ascii.22x25.pgm +28 -0
  36. data/spec/images/pnm/ascii.22x25.ppm +28 -0
  37. data/spec/images/svg/crlf.72x100.svg +3 -0
  38. data/spec/images/svg/long.72x100.svg +20 -0
  39. data/spec/images/svg/long.crlf.72x100.svg +20 -0
  40. data/spec/images/tiff/big-endian.68x49.tiff +0 -0
  41. data/spec/images/tiff/little-endian.40x68.tiff +0 -0
  42. data/spec/images/xbm/crlf.16x32.xbm +11 -0
  43. data/spec/images/xpm/crlf.24x32.xpm +40 -0
  44. metadata +86 -32
  45. data/.travis.yml +0 -24
  46. data/spec/images/cur/50x256.cur +0 -0
  47. data/spec/images/ico/256x27.ico +0 -0
  48. data/spec/images/jpeg/320x240.jpeg +0 -0
  49. data/spec/images/jpeg/extraneous-bytes.320x240.jpeg +0 -0
  50. data/spec/images/mng/612x132.mng +0 -0
  51. data/spec/images/pbm/85x55.pbm +0 -0
  52. data/spec/images/pgm/90x55.pgm +0 -5
  53. data/spec/images/tiff/48x64.tiff +0 -0
data/README.markdown CHANGED
@@ -1,10 +1,10 @@
1
- [![Gem Version](https://img.shields.io/gem/v/image_size.svg?style=flat)](https://rubygems.org/gems/image_size)
2
- [![Build Status](https://img.shields.io/travis/toy/image_size/master.svg?style=flat)](https://travis-ci.org/toy/image_size)
1
+ [![Gem Version](https://img.shields.io/gem/v/image_size?logo=rubygems)](https://rubygems.org/gems/image_size)
2
+ [![Build Status](https://img.shields.io/github/workflow/status/toy/image_size/check/master?logo=github)](https://github.com/toy/image_size/actions/workflows/check.yml)
3
3
 
4
4
  # image_size
5
5
 
6
- measure image size using pure Ruby
7
- formats: `apng`, `bmp`, `cur`, `gif`, `jpeg`, `ico`, `mng`, `pbm`, `pcx`, `pgm`, `png`, `ppm`, `psd`, `swf`, `tiff`, `xbm`, `xpm`, `webp`
6
+ Measure image size using pure Ruby.
7
+ Formats: `apng`, `bmp`, `cur`, `gif`, `ico`, `j2c`, `jp2`, `jpeg`, `jpx`, `mng`, `pam`, `pbm`, `pcx`, `pgm`, `png`, `ppm`, `psd`, `svg`, `swf`, `tiff`, `webp`, `xbm`, `xpm`.
8
8
 
9
9
  ## Installation
10
10
 
@@ -23,20 +23,26 @@ gem 'image_size', '~> 2.0'
23
23
  ## Usage
24
24
 
25
25
  ```ruby
26
- image_size = ImageSize.path('spec/test.jpg')
26
+ image_size = ImageSize.path('spec/images/jpeg/436x429.jpeg')
27
27
 
28
28
  image_size.format #=> :jpec
29
- image_size.width #=> 320
30
- image_size.height #=> 240
31
- image_size.w #=> 320
32
- image_size.h #=> 240
33
- image_size.size #=> [320, 240]
29
+ image_size.width #=> 436
30
+ image_size.height #=> 429
31
+ image_size.w #=> 436
32
+ image_size.h #=> 429
33
+ image_size.size #=> [436, 429]
34
+ image_size.size.to_s #=> "436x429"
35
+ "#{image_size.size}" #=> "436x429"
36
+ image_size.size.width #=> 436
37
+ image_size.size.height #=> 429
38
+ image_size.size.w #=> 436
39
+ image_size.size.h #=> 429
34
40
  ```
35
41
 
36
42
  Or using `IO` object:
37
43
 
38
44
  ```ruby
39
- image_size = File.open('spec/test.jpg', 'rb'){ |fh| ImageSize.new(fh) }
45
+ image_size = File.open('spec/images/jpeg/436x429.jpeg', 'rb'){ |fh| ImageSize.new(fh) }
40
46
  ```
41
47
 
42
48
  Any object responding to `read` and `eof?`:
@@ -68,14 +74,14 @@ So rewind if needed before passing to `ImageSize` and/or rewind after passing to
68
74
  ```ruby
69
75
  require 'image_size'
70
76
 
71
- File.open('spec/test.jpg', 'rb') do |fh|
77
+ File.open('spec/images/jpeg/436x429.jpeg', 'rb') do |fh|
72
78
  image_size = ImageSize.new(fh)
73
79
 
74
80
  fh.rewind
75
81
  data = fh.read
76
82
  end
77
83
 
78
- File.open('spec/test.jpg', 'rb') do |fh|
84
+ File.open('spec/images/jpeg/436x429.jpeg', 'rb') do |fh|
79
85
  data = fh.read
80
86
  fh.rewind
81
87
 
@@ -85,9 +91,7 @@ end
85
91
 
86
92
  ## Licence
87
93
 
88
- This code is free to use under the terms of the Ruby's licence.
94
+ This code is free to use under the terms of the [Ruby's licence](LICENSE.txt).
89
95
 
90
- ## Contact
91
-
92
- Original author: "Keisuke Minami": mailto:keisuke@rccn.com
93
- Further development by Ivan Kuchin https://github.com/toy/image_size
96
+ Original author: Keisuke Minami <keisuke@rccn.com>.\
97
+ Further development 2010-2021 Ivan Kuchin https://github.com/toy/image_size
data/image_size.gemspec CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_size'
5
- s.version = '2.0.2'
5
+ s.version = '3.0.0'
6
6
  s.summary = %q{Measure image size using pure Ruby}
7
- s.description = %q{Measure following file dimensions: apng, bmp, cur, gif, jpeg, ico, mng, pbm, pcx, pgm, png, ppm, psd, swf, tiff, xbm, xpm, webp}
8
- s.homepage = "http://github.com/toy/#{s.name}"
7
+ s.description = %q{Measure following file dimensions: apng, bmp, cur, gif, ico, j2c, jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf, tiff, webp, xbm, xpm}
8
+ s.homepage = "https://github.com/toy/#{s.name}"
9
9
  s.authors = ['Keisuke Minami', 'Ivan Kuchin']
10
10
  s.license = 'Ruby'
11
11
 
@@ -22,7 +22,8 @@ Gem::Specification.new do |s|
22
22
  s.require_paths = %w[lib]
23
23
 
24
24
  s.add_development_dependency 'rspec', '~> 3.0'
25
- if RUBY_VERSION >= '2.2'
26
- s.add_development_dependency 'rubocop', '~> 0.59'
25
+ if RUBY_VERSION >= '2.4'
26
+ s.add_development_dependency 'rubocop', '~> 1.0'
27
+ s.add_development_dependency 'rubocop-rspec', '~> 2.0'
27
28
  end
28
29
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size/reader'
4
+
5
+ class ImageSize
6
+ module ChunkyReader # :nodoc:
7
+ include Reader
8
+
9
+ # Size of a chunk in which to read
10
+ def chunk_size
11
+ 4096
12
+ end
13
+
14
+ # Including class should define method chunk that accepts the chunk number
15
+ # and returns a string of chunk_size length or shorter for last chunk, or
16
+ # nil for further chunks.
17
+ # Determines required chunks, takes parts of them to construct desired
18
+ # substring, behaves same as str[start, length] except start can't be
19
+ # negative.
20
+ def [](offset, length)
21
+ raise ArgumentError, "expected offset not to be negative, got #{offset}" if offset < 0
22
+ return if length < 0
23
+
24
+ first = offset / chunk_size
25
+ return unless (first_chunk = chunk(first))
26
+
27
+ last = (offset + length - 1) / chunk_size
28
+
29
+ if first >= last
30
+ first_chunk[offset - (first * chunk_size), length]
31
+ else
32
+ return unless (first_piece = first_chunk[offset - (first * chunk_size), chunk_size])
33
+
34
+ chunks = (first.succ...last).map{ |i| chunk(i) }.unshift(first_piece)
35
+
36
+ if (last_chunk = chunk(last))
37
+ chunks.push(last_chunk[0, offset + length - (last * chunk_size)])
38
+ end
39
+
40
+ chunks.join
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'stringio'
5
+
6
+ class ImageSize
7
+ module Reader # :nodoc:
8
+ class << self
9
+ def open(input)
10
+ case
11
+ when input.is_a?(String)
12
+ yield StringReader.new(input)
13
+ when input.is_a?(StringIO)
14
+ yield StringReader.new(input.string)
15
+ when input.respond_to?(:read) && input.respond_to?(:eof?)
16
+ yield for_io(input)
17
+ when input.is_a?(Pathname)
18
+ input.open('rb'){ |f| yield for_io(f) }
19
+ else
20
+ raise ArgumentError, "expected data as String or an object responding to read and eof?, got #{input.class}"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def for_io(io)
27
+ if io.respond_to?(:stat) && !io.stat.file?
28
+ StreamIOReader.new(io)
29
+ else
30
+ begin
31
+ io.seek(0, IO::SEEK_CUR)
32
+ SeekableIOReader.new(io)
33
+ rescue Errno::ESPIPE, Errno::EINVAL
34
+ StreamIOReader.new(io)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def fetch(offset, length)
41
+ chunk = self[offset, length]
42
+
43
+ unless chunk && chunk.length == length
44
+ raise FormatError, "Expected #{length} bytes at offset #{offset}, got #{chunk.inspect}"
45
+ end
46
+
47
+ chunk
48
+ end
49
+
50
+ def unpack(offset, length, format)
51
+ fetch(offset, length).unpack(format)
52
+ end
53
+
54
+ if ''.respond_to?(:unpack1)
55
+ def unpack1(offset, length, format)
56
+ fetch(offset, length).unpack1(format)
57
+ end
58
+ else
59
+ def unpack1(offset, length, format)
60
+ fetch(offset, length).unpack(format)[0]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size/chunky_reader'
4
+
5
+ class ImageSize
6
+ class SeekableIOReader # :nodoc:
7
+ include ChunkyReader
8
+
9
+ def initialize(io)
10
+ @io = io
11
+ @pos = 0
12
+ @chunks = {}
13
+ end
14
+
15
+ private
16
+
17
+ def chunk(i)
18
+ unless @chunks.key?(i)
19
+ @io.seek((chunk_size * i) - @pos, IO::SEEK_CUR)
20
+ data = @io.read(chunk_size)
21
+ @pos += data.length
22
+ @chunks[i] = data
23
+ end
24
+
25
+ @chunks[i]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size/chunky_reader'
4
+
5
+ class ImageSize
6
+ class StreamIOReader # :nodoc:
7
+ include ChunkyReader
8
+
9
+ def initialize(io)
10
+ @io = io
11
+ @chunks = []
12
+ end
13
+
14
+ private
15
+
16
+ def chunk(i)
17
+ @chunks << @io.read(chunk_size) while i >= @chunks.length && !@io.eof?
18
+
19
+ @chunks[i]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size/reader'
4
+
5
+ class ImageSize
6
+ class StringReader # :nodoc:
7
+ include Reader
8
+
9
+ def initialize(string)
10
+ @string = if string.respond_to?(:encoding) && string.encoding.name != 'ASCII-8BIT'
11
+ string.dup.force_encoding('ASCII-8BIT')
12
+ else
13
+ string
14
+ end
15
+ end
16
+
17
+ def [](offset, length)
18
+ @string[offset, length]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_size/reader'
4
+ require 'image_size/chunky_reader'
5
+
6
+ require 'net/https'
7
+ require 'uri'
8
+
9
+ # This is a hacky experiment and not part of public API
10
+ #
11
+ # It adds ability to fetch size of image from http server while downloading only
12
+ # needed chunks if the server recognises Range header
13
+ class ImageSize
14
+ class URIReader # :nodoc:
15
+ include ChunkyReader
16
+
17
+ def initialize(uri, redirects = 5)
18
+ if !@http || @http.address != uri.host || @http.port != uri.port
19
+ @http.finish if @http
20
+ @http = Net::HTTP.new(uri.host, uri.port)
21
+ @http.use_ssl = true if uri.scheme == 'https'
22
+ @http.start
23
+ end
24
+
25
+ @request_uri = uri.request_uri
26
+ response = request_chunk(0)
27
+
28
+ case response
29
+ when Net::HTTPRedirection
30
+ raise "Too many redirects: #{response['location']}" unless redirects > 0
31
+
32
+ initialize(uri + response['location'], redirects - 1)
33
+ when Net::HTTPOK
34
+ @body = response.body
35
+ when Net::HTTPPartialContent
36
+ @chunks = { 0 => response.body }
37
+ else
38
+ raise "Unexpected response: #{response}"
39
+ end
40
+ end
41
+
42
+ def [](offset, length)
43
+ if @body
44
+ @body[offset, length]
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def chunk(i)
51
+ unless @chunks.key?(i)
52
+ response = request_chunk(i)
53
+ case response
54
+ when Net::HTTPPartialContent
55
+ @chunks[i] = response.body
56
+ else
57
+ raise "Unexpected response: #{response}"
58
+ end
59
+ end
60
+
61
+ @chunks[i]
62
+ end
63
+
64
+ private
65
+
66
+ def request_chunk(i)
67
+ @http.get(@request_uri, 'Range' => "bytes=#{chunk_size * i}-#{(chunk_size * (i + 1)) - 1}")
68
+ end
69
+ end
70
+
71
+ module Reader # :nodoc:
72
+ class << self
73
+ def open_with_uri(input, &block)
74
+ if input.is_a?(URI)
75
+ yield URIReader.new(input)
76
+ else
77
+ open_without_uri(input, &block)
78
+ end
79
+ end
80
+ alias_method :open_without_uri, :open
81
+ alias_method :open, :open_with_uri
82
+ end
83
+ end
84
+
85
+ def self.url(url)
86
+ new(url.is_a?(URI) ? url : URI(url))
87
+ end
88
+ end