dimensions 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +89 -0
- data/lib/dimensions.rb +50 -0
- data/lib/dimensions/exif_scanner.rb +61 -0
- data/lib/dimensions/io.rb +31 -0
- data/lib/dimensions/jpeg_scanner.rb +86 -0
- data/lib/dimensions/reader.rb +84 -0
- data/lib/dimensions/scanner.rb +46 -0
- data/lib/dimensions/version.rb +3 -0
- metadata +76 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Sam Stephenson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
Dimensions
|
2
|
+
==========
|
3
|
+
|
4
|
+
Dimensions is a pure Ruby library for reading the width, height and
|
5
|
+
rotation angle of GIF, PNG and JPEG images.
|
6
|
+
|
7
|
+
Use the `dimensions`, `width` and `height` methods on the `Dimensions`
|
8
|
+
module to measure image files:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
require 'dimensions'
|
12
|
+
|
13
|
+
Dimensions.dimensions("upload_bird.jpg") # => [300, 225]
|
14
|
+
Dimensions.width("upload_bird.jpg") # => 300
|
15
|
+
Dimensions.height("upload_bird.jpg") # => 225
|
16
|
+
```
|
17
|
+
|
18
|
+
Many cameras and smartphones set the EXIF orientation attribute for
|
19
|
+
photos taken in portrait or landscape mode. The Dimensions library
|
20
|
+
reads this attribute and swaps the `width` and `height` values
|
21
|
+
automatically to accurately reflect how the image looks when
|
22
|
+
rotated. You can use the `angle` method to check a JPEG image's
|
23
|
+
orientation in degrees:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
Dimensions.angle("upload_bird.jpg") # => 0
|
27
|
+
Dimensions.angle("rotated.jpg") # => 90
|
28
|
+
```
|
29
|
+
|
30
|
+
### Reading dimensions from a stream
|
31
|
+
|
32
|
+
Pass an IO object to the `Dimensions` method to extend it with the
|
33
|
+
`Dimensions::IO` module and transparently detect its dimensions and
|
34
|
+
orientation as it is read. Once the IO has been sufficiently read, its
|
35
|
+
`width`, `height` and `angle` methods will return non-nil values
|
36
|
+
(assuming its contents are an image).
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'dimensions'
|
40
|
+
require 'json'
|
41
|
+
require 'securerandom'
|
42
|
+
|
43
|
+
module Uploader
|
44
|
+
def self.call(env)
|
45
|
+
body = Dimensions(env["rack.input"])
|
46
|
+
|
47
|
+
# Handle the upload
|
48
|
+
token = SecureRandom.hex(20)
|
49
|
+
path = File.join(UPLOAD_PATH, token)
|
50
|
+
File.open(path, "wb") { |file| file.write body.read }
|
51
|
+
|
52
|
+
# Return the width and height as JSON
|
53
|
+
[
|
54
|
+
200,
|
55
|
+
{"Content-Type" => "application/json"},
|
56
|
+
[JSON.dump(
|
57
|
+
"token" => token,
|
58
|
+
"width" => body.width,
|
59
|
+
"height" => body.height
|
60
|
+
)]
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
### License
|
67
|
+
|
68
|
+
(The MIT License)
|
69
|
+
|
70
|
+
Copyright © 2011 Sam Stephenson
|
71
|
+
|
72
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
73
|
+
a copy of this software and associated documentation files (the
|
74
|
+
"Software"), to deal in the Software without restriction, including
|
75
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
76
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
77
|
+
permit persons to whom the Software is furnished to do so, subject to
|
78
|
+
the following conditions:
|
79
|
+
|
80
|
+
The above copyright notice and this permission notice shall be
|
81
|
+
included in all copies or substantial portions of the Software.
|
82
|
+
|
83
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
84
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
85
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
86
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
87
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
88
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
89
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/dimensions.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'dimensions/exif_scanner'
|
2
|
+
require 'dimensions/io'
|
3
|
+
require 'dimensions/jpeg_scanner'
|
4
|
+
require 'dimensions/reader'
|
5
|
+
require 'dimensions/scanner'
|
6
|
+
require 'dimensions/version'
|
7
|
+
|
8
|
+
# Extends an IO object with the `Dimensions::IO` module, which adds
|
9
|
+
# `dimensions`, `width`, `height` and `angle` methods. The methods
|
10
|
+
# will return non-nil values once the IO has been sufficiently read,
|
11
|
+
# assuming its contents are an image.
|
12
|
+
def Dimensions(io)
|
13
|
+
io.extend(Dimensions::IO)
|
14
|
+
end
|
15
|
+
|
16
|
+
module Dimensions
|
17
|
+
class << self
|
18
|
+
# Returns an array of [width, height] representing the dimensions
|
19
|
+
# of the image at the given path.
|
20
|
+
def dimensions(path)
|
21
|
+
io_for(path).dimensions
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the width of the image at the given path.
|
25
|
+
def width(path)
|
26
|
+
io_for(path).width
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the height of the image at the given path.
|
30
|
+
def height(path)
|
31
|
+
io_for(path).height
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the rotation angle of the JPEG image at the given
|
35
|
+
# path. If the JPEG is rotated 90 or 270 degrees (as is often the
|
36
|
+
# case with photos from smartphones, for example) its width and
|
37
|
+
# height will be swapped to accurately reflect the rotation.
|
38
|
+
def angle(path)
|
39
|
+
io_for(path).angle
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def io_for(path)
|
44
|
+
Dimensions(File.open(path, "rb")).tap do |io|
|
45
|
+
io.read
|
46
|
+
io.close
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'dimensions/scanner'
|
2
|
+
|
3
|
+
module Dimensions
|
4
|
+
class ExifScanner < Scanner
|
5
|
+
ORIENTATIONS = [
|
6
|
+
:top_left,
|
7
|
+
:top_right,
|
8
|
+
:bottom_right,
|
9
|
+
:bottom_left,
|
10
|
+
:left_top,
|
11
|
+
:right_top,
|
12
|
+
:right_bottom,
|
13
|
+
:left_bottom
|
14
|
+
]
|
15
|
+
|
16
|
+
attr_reader :orientation
|
17
|
+
|
18
|
+
def initialize(data)
|
19
|
+
@orientation = nil
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
def scan
|
24
|
+
scan_header
|
25
|
+
|
26
|
+
offset = read_long + 6
|
27
|
+
skip_to(offset)
|
28
|
+
|
29
|
+
# Note: This only checks the first IFD for orientation entries,
|
30
|
+
# which seems to work fine in my (limited) testing but might not
|
31
|
+
# play out in practice.
|
32
|
+
entry_count = read_short
|
33
|
+
entry_count.times do |i|
|
34
|
+
skip_to(offset + 2 + (12 * i))
|
35
|
+
tag = read_short
|
36
|
+
|
37
|
+
if tag == 0x0112 # orientation
|
38
|
+
advance(6)
|
39
|
+
@orientation = ORIENTATIONS[read_short - 1]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@orientation
|
44
|
+
end
|
45
|
+
|
46
|
+
def scan_header
|
47
|
+
advance(6)
|
48
|
+
scan_endianness
|
49
|
+
scan_tag_mark
|
50
|
+
end
|
51
|
+
|
52
|
+
def scan_endianness
|
53
|
+
tag = [read_char, read_char]
|
54
|
+
@big = tag == [0x4D, 0x4D]
|
55
|
+
end
|
56
|
+
|
57
|
+
def scan_tag_mark
|
58
|
+
raise ScanError unless read_short == 0x002A
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'dimensions/reader'
|
2
|
+
|
3
|
+
module Dimensions
|
4
|
+
module IO
|
5
|
+
def self.extended(io)
|
6
|
+
io.instance_variable_set(:@reader, Reader.new)
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(*args)
|
10
|
+
super.tap do |data|
|
11
|
+
@reader << data if data
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def dimensions
|
16
|
+
[width, height] if width && height
|
17
|
+
end
|
18
|
+
|
19
|
+
def width
|
20
|
+
@reader.width
|
21
|
+
end
|
22
|
+
|
23
|
+
def height
|
24
|
+
@reader.height
|
25
|
+
end
|
26
|
+
|
27
|
+
def angle
|
28
|
+
@reader.angle
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'dimensions/exif_scanner'
|
2
|
+
|
3
|
+
module Dimensions
|
4
|
+
class JpegScanner < Scanner
|
5
|
+
SOF_MARKERS = [0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF]
|
6
|
+
EOI_MARKER = 0xD9 # end of image
|
7
|
+
SOS_MARKER = 0xDA # start of stream
|
8
|
+
APP1_MARKER = 0xE1 # maybe EXIF
|
9
|
+
|
10
|
+
attr_reader :width, :height, :angle
|
11
|
+
|
12
|
+
def initialize(data)
|
13
|
+
@width = nil
|
14
|
+
@height = nil
|
15
|
+
@angle = 0
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def scan
|
20
|
+
advance(2)
|
21
|
+
|
22
|
+
while marker = read_next_marker
|
23
|
+
case marker
|
24
|
+
when *SOF_MARKERS
|
25
|
+
scan_start_of_frame
|
26
|
+
when EOI_MARKER, SOS_MARKER
|
27
|
+
break
|
28
|
+
when APP1_MARKER
|
29
|
+
scan_app1_frame
|
30
|
+
else
|
31
|
+
skip_frame
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
width && height
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_next_marker
|
39
|
+
c = read_char while c != 0xFF
|
40
|
+
c = read_char while c == 0xFF
|
41
|
+
c
|
42
|
+
end
|
43
|
+
|
44
|
+
def scan_start_of_frame
|
45
|
+
length = read_short
|
46
|
+
depth = read_char
|
47
|
+
height = read_short
|
48
|
+
width = read_short
|
49
|
+
size = read_char
|
50
|
+
|
51
|
+
if length == (size * 3) + 8
|
52
|
+
@width, @height = width, height
|
53
|
+
else
|
54
|
+
raise ScanError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def scan_app1_frame
|
59
|
+
frame = read_frame
|
60
|
+
if frame[0..5] == "Exif\000\000"
|
61
|
+
scanner = ExifScanner.new(frame)
|
62
|
+
if scanner.scan
|
63
|
+
case scanner.orientation
|
64
|
+
when :bottom_right
|
65
|
+
@angle = 180
|
66
|
+
when :left_top, :right_top
|
67
|
+
@angle = 90
|
68
|
+
when :right_bottom, :left_bottom
|
69
|
+
@angle = 270
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue ExifScanner::ScanError
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_frame
|
77
|
+
length = read_short - 2
|
78
|
+
read_data(length)
|
79
|
+
end
|
80
|
+
|
81
|
+
def skip_frame
|
82
|
+
length = read_short - 2
|
83
|
+
advance(length)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'dimensions/jpeg_scanner'
|
2
|
+
|
3
|
+
module Dimensions
|
4
|
+
class Reader
|
5
|
+
GIF_HEADER = [0x47, 0x49, 0x46, 0x38]
|
6
|
+
PNG_HEADER = [0x89, 0x50, 0x4E, 0x47]
|
7
|
+
JPEG_HEADER = [0xFF, 0xD8, 0xFF]
|
8
|
+
|
9
|
+
attr_reader :type, :width, :height, :angle
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@process = :determine_type
|
13
|
+
@type = nil
|
14
|
+
@width = nil
|
15
|
+
@height = nil
|
16
|
+
@angle = nil
|
17
|
+
@size = 0
|
18
|
+
@data = ""
|
19
|
+
@data.force_encoding("BINARY") if @data.respond_to?(:force_encoding)
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(data)
|
23
|
+
if @process
|
24
|
+
@data << data
|
25
|
+
@size = @data.length
|
26
|
+
process
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def process(process = @process)
|
31
|
+
send(@process) if @process = process
|
32
|
+
end
|
33
|
+
|
34
|
+
def determine_type
|
35
|
+
if @size >= 4
|
36
|
+
bytes = @data.unpack("C4")
|
37
|
+
|
38
|
+
if match_header(GIF_HEADER, bytes)
|
39
|
+
@type = :gif
|
40
|
+
elsif match_header(PNG_HEADER, bytes)
|
41
|
+
@type = :png
|
42
|
+
elsif match_header(JPEG_HEADER, bytes)
|
43
|
+
@type = :jpeg
|
44
|
+
end
|
45
|
+
|
46
|
+
process @type ? :"extract_#{type}_dimensions" : nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_gif_dimensions
|
51
|
+
if @size >= 10
|
52
|
+
@width, @height = @data.unpack("x6v2")
|
53
|
+
process nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def extract_png_dimensions
|
58
|
+
if @size >= 24
|
59
|
+
@width, @height = @data.unpack("x16N2")
|
60
|
+
process nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_jpeg_dimensions
|
65
|
+
scanner = JpegScanner.new(@data)
|
66
|
+
if scanner.scan
|
67
|
+
@width = scanner.width
|
68
|
+
@height = scanner.height
|
69
|
+
@angle = scanner.angle
|
70
|
+
|
71
|
+
if @angle == 90 || @angle == 270
|
72
|
+
@width, @height = @height, @width
|
73
|
+
end
|
74
|
+
|
75
|
+
process nil
|
76
|
+
end
|
77
|
+
rescue JpegScanner::ScanError
|
78
|
+
end
|
79
|
+
|
80
|
+
def match_header(header, bytes)
|
81
|
+
bytes[0, header.length] == header
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dimensions
|
2
|
+
class Scanner
|
3
|
+
class ScanError < ::StandardError; end
|
4
|
+
|
5
|
+
def initialize(data)
|
6
|
+
@data = data.dup
|
7
|
+
@data.force_encoding("BINARY") if @data.respond_to?(:force_encoding)
|
8
|
+
@size = @data.length
|
9
|
+
@pos = 0
|
10
|
+
@big = true # endianness
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_char
|
14
|
+
read(1, "C")
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_short
|
18
|
+
read(2, @big ? "n" : "v")
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_long
|
22
|
+
read(4, @big ? "N" : "V")
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(size, format)
|
26
|
+
data = read_data(size)
|
27
|
+
data.unpack(format)[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
def read_data(size)
|
31
|
+
data = @data[@pos, size]
|
32
|
+
advance(size)
|
33
|
+
data
|
34
|
+
end
|
35
|
+
|
36
|
+
def advance(length)
|
37
|
+
@pos += length
|
38
|
+
raise ScanError if @pos > @size
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_to(pos)
|
42
|
+
@pos = pos
|
43
|
+
raise ScanError if @pos > @size
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dimensions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sam Stephenson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-01 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: A pure Ruby library for measuring the dimensions and rotation angles of GIF, PNG and JPEG images.
|
23
|
+
email:
|
24
|
+
- sstephenson@gmail.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- README.md
|
33
|
+
- LICENSE
|
34
|
+
- lib/dimensions/exif_scanner.rb
|
35
|
+
- lib/dimensions/io.rb
|
36
|
+
- lib/dimensions/jpeg_scanner.rb
|
37
|
+
- lib/dimensions/reader.rb
|
38
|
+
- lib/dimensions/scanner.rb
|
39
|
+
- lib/dimensions/version.rb
|
40
|
+
- lib/dimensions.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: https://github.com/sstephenson/dimensions
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.5.0
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Pure Ruby dimension measurement for GIF, PNG and JPEG images
|
75
|
+
test_files: []
|
76
|
+
|