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 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.
@@ -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.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Dimensions
2
+ VERSION = "1.0.0"
3
+ 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
+