image_size 2.0.2 → 3.0.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.
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