dimensions 1.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.
- 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
|
+
|