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.
- checksums.yaml +4 -4
- data/.github/workflows/check.yml +69 -0
- data/.rubocop.yml +27 -4
- data/.rubocop_todo.yml +11 -8
- data/CHANGELOG.markdown +19 -0
- data/GPL +340 -0
- data/Gemfile +1 -3
- data/LICENSE.txt +56 -0
- data/README.markdown +22 -18
- data/image_size.gemspec +6 -5
- data/lib/image_size/chunky_reader.rb +44 -0
- data/lib/image_size/reader.rb +64 -0
- data/lib/image_size/seekable_io_reader.rb +28 -0
- data/lib/image_size/stream_io_reader.rb +22 -0
- data/lib/image_size/string_reader.rb +21 -0
- data/lib/image_size/uri_reader.rb +88 -0
- data/lib/image_size.rb +159 -85
- data/spec/image_size/chunky_reader_spec.rb +69 -0
- data/spec/image_size_spec.rb +45 -2
- data/spec/images/.gitattributes +1 -0
- data/spec/images/cur/32x256.cur +0 -0
- data/spec/images/ico/32x256.ico +0 -0
- data/spec/images/jp2/163x402.jp2 +0 -0
- data/spec/images/jp2/176x373.jpx +0 -0
- data/spec/images/jp2/224x293.j2c +0 -0
- data/spec/images/jpeg/436x429.jpeg +0 -0
- data/spec/images/jpeg/extraneous-bytes.436x429.jpeg +0 -0
- data/spec/images/mng/61x42.mng +0 -0
- data/spec/images/{apng → png}/192x110.apng +0 -0
- data/spec/images/pnm/22x25.pam +8 -0
- data/spec/images/pnm/22x25.pbm +0 -0
- data/spec/images/pnm/22x25.pgm +4 -0
- data/spec/images/pnm/22x25.ppm +4 -0
- data/spec/images/pnm/ascii.22x25.pbm +27 -0
- data/spec/images/pnm/ascii.22x25.pgm +28 -0
- data/spec/images/pnm/ascii.22x25.ppm +28 -0
- data/spec/images/svg/crlf.72x100.svg +3 -0
- data/spec/images/svg/long.72x100.svg +20 -0
- data/spec/images/svg/long.crlf.72x100.svg +20 -0
- data/spec/images/tiff/big-endian.68x49.tiff +0 -0
- data/spec/images/tiff/little-endian.40x68.tiff +0 -0
- data/spec/images/xbm/crlf.16x32.xbm +11 -0
- data/spec/images/xpm/crlf.24x32.xpm +40 -0
- metadata +86 -32
- data/.travis.yml +0 -24
- data/spec/images/cur/50x256.cur +0 -0
- data/spec/images/ico/256x27.ico +0 -0
- data/spec/images/jpeg/320x240.jpeg +0 -0
- data/spec/images/jpeg/extraneous-bytes.320x240.jpeg +0 -0
- data/spec/images/mng/612x132.mng +0 -0
- data/spec/images/pbm/85x55.pbm +0 -0
- data/spec/images/pgm/90x55.pgm +0 -5
- 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
|
2
|
-
[![Build Status](https://img.shields.io/
|
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
|
-
|
7
|
-
|
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/
|
26
|
+
image_size = ImageSize.path('spec/images/jpeg/436x429.jpeg')
|
27
27
|
|
28
28
|
image_size.format #=> :jpec
|
29
|
-
image_size.width #=>
|
30
|
-
image_size.height #=>
|
31
|
-
image_size.w #=>
|
32
|
-
image_size.h #=>
|
33
|
-
image_size.size #=> [
|
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/
|
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/
|
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/
|
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
|
-
|
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 = '
|
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,
|
8
|
-
s.homepage = "
|
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.
|
26
|
-
s.add_development_dependency 'rubocop', '~> 0
|
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
|