format_parser 0.10.0 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4253990f4ed6f05476ba982726932b0298efee7da4a5267980eb176f12d6e68
4
- data.tar.gz: 74683875ccfeeed68fc7cdf13af16a7694b81ada533f9138165c26ff6a3e0cfa
3
+ metadata.gz: ab01944664740856d704875f0a74d1b0540379c8e351176e38d3b5bf119e813f
4
+ data.tar.gz: b0736ffd074eb3fb49586d52799dd687c72f0a09b6e2d4109b1cbd26a8eac293
5
5
  SHA512:
6
- metadata.gz: 19701f96beaeee111eaa2d9381b833a13a0954ea1e6deab9dc7095021ae95c809bf4a96abe04b2cbbe10d9d21902b3bdb5520e162151894931365f2bc188aef8
7
- data.tar.gz: bf7b0fe93336654c06d58a852f88c89725974759ed30da89575b3559be5c4658ef60c3def92b50f1a4afaca3659996b0f79a9b245e7a0e9f1161b0fc05feb68e
6
+ metadata.gz: 570e9fcef6a08ad4e800c84d8452d985900f8442d9071477e6cd465b533677f7c39b6ae51c8f94d0ab0e90305ce790c395c3ce54ecda46800921b3311adc23b8
7
+ data.tar.gz: 0662cc268f0fc1fc61ac97896811e505f1ae251a2a15fe9027de2414107adb1414b6ce4f3a1111be1215e8e48323330ce8b08b98e521bf7d9ec4a26c8d29f01d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.11.0
2
+ * Add `Image#display_width_px` and `Image#display_height_px` for EXIF/aspect corrected display dimensions, and provide
3
+ those values from a few parsers already. Also make full EXIF data available for JPEG/TIFF in `intrinsics[:exif]`
4
+ * Adds `limits_config` option to `FormatParser.parse()` for tweaking buffers and read limits externally
5
+
1
6
  ## 0.10.0
2
7
  * Adds the `format_parser_inspect` binary for parsing a file from the commandline
3
8
  and returning results in JSON
data/README.md CHANGED
@@ -41,8 +41,8 @@ Pass an IO object that responds to `read` and `seek` to `FormatParser` and the f
41
41
  match = FormatParser.parse(File.open("myimage.jpg", "rb"))
42
42
  match.nature #=> :image
43
43
  match.format #=> :jpg
44
- match.width_px #=> 320
45
- match.height_px #=> 240
44
+ match.display_width_px #=> 320
45
+ match.display_height_px #=> 240
46
46
  match.orientation #=> :top_left
47
47
  ```
48
48
 
@@ -122,6 +122,9 @@ Unless specified otherwise in this section the fixture files are MIT licensed an
122
122
  ### FDX
123
123
  - fixture.fdx was created by one of the project maintainers and is MIT licensed
124
124
 
125
+ ### DPX
126
+ - DPX files were created by one of the project maintainers and may be used with the library for the purposes of testing
127
+
125
128
  ### MOOV
126
129
  - bmff.mp4 is borrowed from the [bmff](https://github.com/zuku/bmff) project
127
130
  - Test_Circular MOV files were created by one of the project maintainers and are MIT licensed
@@ -6,7 +6,7 @@ require 'optparse'
6
6
 
7
7
  options = {results: :first}
8
8
  OptionParser.new do |opts|
9
- opts.banner = 'Usage: format_parser_inspect --first my_file.jpg'
9
+ opts.banner = 'Usage: format_parser_inspect --all my_file.jpg my_other_file.png'
10
10
  opts.on('-a', '--all', 'Return all results instead of just the first one') do |_v|
11
11
  options[:results] = :all
12
12
  end
data/lib/format_parser.rb CHANGED
@@ -98,23 +98,17 @@ module FormatParser
98
98
  # is ambiguous. The default is `:first` which returns the first matching result. Other
99
99
  # possible values are `:all` to get all possible results and an Integer to return
100
100
  # at most N results.
101
+ # @param limits_config[ReadLimitsConfig] the configuration object for various read/cache limits. The default
102
+ # one should be good for most cases.
101
103
  # @return [Array<Result>, Result, nil] either an Array of results, a single parsing result or `nil`if
102
104
  # no useful metadata could be recovered from the file
103
- def self.parse(io, natures: @parsers_per_nature.keys, formats: @parsers_per_format.keys, results: :first)
104
- # We need to apply various limits so that parsers do not over-read, do not cause too many HTTP
105
- # requests to be dispatched and so on. These should be _balanced_ with one another- for example,
106
- # we cannot tell a parser that it is limited to reading 1024 bytes while at the same time
107
- # limiting the size of the cache pages it may slurp in to less than that amount, since
108
- # it can quickly become frustrating. The limits configurator computes these limits
109
- # for us, in a fairly balanced way, based on one setting.
110
- limit_config = FormatParser::ReadLimitsConfig.new(MAX_BYTES_READ_PER_PARSER)
111
-
105
+ def self.parse(io, natures: @parsers_per_nature.keys, formats: @parsers_per_format.keys, results: :first, limits_config: default_limits_config)
112
106
  # Limit the number of cached _pages_ we may fetch. This allows us to limit the number
113
107
  # of page faults (page cache misses) a parser may incur
114
- read_limiter_under_cache = FormatParser::ReadLimiter.new(io, max_reads: limit_config.max_pagefaults_per_parser)
108
+ read_limiter_under_cache = FormatParser::ReadLimiter.new(io, max_reads: limits_config.max_pagefaults_per_parser)
115
109
 
116
110
  # Then configure a layer of caching on top of that
117
- cached_io = Care::IOWrapper.new(read_limiter_under_cache, page_size: limit_config.cache_page_size)
111
+ cached_io = Care::IOWrapper.new(read_limiter_under_cache, page_size: limits_config.cache_page_size)
118
112
 
119
113
  # How many results has the user asked for? Used to determinate whether an array
120
114
  # is returned or not.
@@ -133,7 +127,12 @@ module FormatParser
133
127
  parsers = parsers_for(natures, formats)
134
128
 
135
129
  # Limit how many operations the parser can perform
136
- limited_io = ReadLimiter.new(cached_io, max_bytes: limit_config.max_read_bytes_per_parser, max_reads: limit_config.max_reads_per_parser, max_seeks: limit_config.max_seeks_per_parser)
130
+ limited_io = ReadLimiter.new(
131
+ cached_io,
132
+ max_bytes: limits_config.max_read_bytes_per_parser,
133
+ max_reads: limits_config.max_reads_per_parser,
134
+ max_seeks: limits_config.max_seeks_per_parser
135
+ )
137
136
 
138
137
  results = parsers.lazy.map do |parser|
139
138
  # Reset all the read limits, per parser
@@ -157,6 +156,21 @@ module FormatParser
157
156
  cached_io.clear if cached_io
158
157
  end
159
158
 
159
+ # We need to apply various limits so that parsers do not over-read, do not cause too many HTTP
160
+ # requests to be dispatched and so on. These should be _balanced_ with one another- for example,
161
+ # we cannot tell a parser that it is limited to reading 1024 bytes while at the same time
162
+ # limiting the size of the cache pages it may slurp in to less than that amount, since
163
+ # it can quickly become frustrating. The limits configurator computes these limits
164
+ # for us, in a fairly balanced way, based on one setting.
165
+ #
166
+ # This method returns a ReadLimitsConfig object preset from the `MAX_BYTES_READ_PER_PARSER`
167
+ # default.
168
+ #
169
+ # @return [ReadLimitsConfig]
170
+ def self.default_limits_config
171
+ FormatParser::ReadLimitsConfig.new(MAX_BYTES_READ_PER_PARSER)
172
+ end
173
+
160
174
  def self.execute_parser_and_capture_expected_exceptions(parser, limited_io)
161
175
  parser_name_for_instrumentation = parser.class.to_s.split('::').last
162
176
  Measurometer.instrument('format_parser.parser.%s' % parser_name_for_instrumentation) do
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '0.10.0'
2
+ VERSION = '0.11.0'
3
3
  end
data/lib/image.rb CHANGED
@@ -15,6 +15,24 @@ module FormatParser
15
15
  # Number of pixels vertically in the pixel buffer
16
16
  attr_accessor :height_px
17
17
 
18
+ # Image width when displayed and taking into account things like camera
19
+ # orientation tags and non-square pixels/anamorphicity. The dimensions
20
+ # used for display are always computed by _squashing_, not by _stretching_.
21
+ #
22
+ # If the display width/height are not specified, the sizes of the
23
+ # pixel buffer will get returned instead (values of the `width_px`/`height_px`
24
+ # attributes)
25
+ attr_accessor :display_width_px
26
+
27
+ # Image height when displayed and taking into account things like camera
28
+ # orientation tags and non-square pixels/anamorphicity. The dimensions
29
+ # used for display are always computed by _squashing_, not by _stretching_.
30
+ #
31
+ # If the display width/height are not specified, the sizes of the
32
+ # pixel buffer will get returned instead (values of the `width_px`/`height_px`
33
+ # attributes)
34
+ attr_accessor :display_height_px
35
+
18
36
  # Whether the file has multiple frames (relevant for image files and video)
19
37
  attr_accessor :has_multiple_frames
20
38
 
@@ -49,6 +67,8 @@ module FormatParser
49
67
  # Only permits assignments via defined accessors
50
68
  def initialize(**attributes)
51
69
  attributes.map { |(k, v)| public_send("#{k}=", v) }
70
+ @display_width_px ||= @width_px
71
+ @display_height_px ||= @height_px
52
72
  end
53
73
 
54
74
  def nature
@@ -1,143 +1,57 @@
1
+ require 'delegate'
2
+
1
3
  class FormatParser::DPXParser
2
4
  include FormatParser::IOUtils
5
+ require_relative 'dpx_parser/dpx_structs'
6
+ BE_MAGIC = 'SDPX'
7
+ LE_MAGIC = BE_MAGIC.reverse
3
8
 
4
- FILE_INFO = [
5
- # :x4, # magic bytes SDPX, we read them anyway so not in the pattern
6
- :x4, # u32 :image_offset, :desc => 'Offset to image data in bytes', :req => true
7
- :x8, # char :version, 8, :desc => 'Version of header format', :req => true
8
- :x4, # u32 :file_size, :desc => "Total image size in bytes", :req => true
9
- :x4, # u32 :ditto_key, :desc => 'Whether the basic headers stay the same through the sequence (1 means they do)'
10
- :x4, # u32 :generic_size, :desc => 'Generic header length'
11
- :x4, # u32 :industry_size, :desc => 'Industry header length'
12
- :x4, # u32 :user_size, :desc => 'User header length'
13
- :x100, # char :filename, 100, :desc => 'Original filename'
14
- :x24, # char :timestamp, 24, :desc => 'Creation timestamp'
15
- :x100, # char :creator, 100, :desc => 'Creator application'
16
- :x200, # char :project, 200, :desc => 'Project name'
17
- :x200, # char :copyright, 200, :desc => 'Copyright'
18
- :x4, # u32 :encrypt_key, :desc => 'Encryption key'
19
- :x104, # blanking :reserve, 104
20
- ].join
21
-
22
- FILM_INFO = [
23
- :x2, # char :id, 2, :desc => 'Film mfg. ID code (2 digits from film edge code)'
24
- :x2, # char :type, 2, :desc => 'Film type (2 digits from film edge code)'
25
- :x2, # char :offset, 2, :desc => 'Offset in perfs (2 digits from film edge code)'
26
- :x6, # char :prefix, 6, :desc => 'Prefix (6 digits from film edge code'
27
- :x4, # char :count, 4, :desc => 'Count (4 digits from film edge code)'
28
- :x32, # char :format, 32, :desc => 'Format (e.g. Academy)'
29
- :x4, # u32 :frame_position, :desc => 'Frame position in sequence'
30
- :x4, # u32 :sequence_extent, :desc => 'Sequence length'
31
- :x4, # u32 :held_count, :desc => 'For how many frames the frame is held'
32
- :x4, # r32 :frame_rate, :desc => 'Frame rate'
33
- :x4, # r32 :shutter_angle, :desc => 'Shutter angle'
34
- :x4, # char :frame_id, 32, :desc => 'Frame identification (keyframe)'
35
- :x4, # char :slate, 100, :desc => 'Slate information'
36
- :x4, # blanking :reserve, 56
37
- ].join
38
-
39
- IMAGE_ELEMENT = [
40
- :x4, # u32 :data_sign, :desc => 'Data sign (0=unsigned, 1=signed). Core is unsigned', :req => true
41
- #
42
- :x4, # u32 :low_data, :desc => 'Reference low data code value'
43
- :x4, # r32 :low_quantity, :desc => 'Reference low quantity represented'
44
- :x4, # u32 :high_data, :desc => 'Reference high data code value (1023 for 10bit per channel)'
45
- :x4, # r32 :high_quantity, :desc => 'Reference high quantity represented'
46
- #
47
- :x1, # u8 :descriptor, :desc => 'Descriptor for this image element (ie Video or Film), by enum', :req => true
48
- # TODO - colirimetry information might be handy to recover,
49
- # as well as "bit size per element" (how many bits _per component_ we have) -
50
- # this will be different for, say, 8-bit DPX files versus 10-bit etc.
51
- :x1, # u8 :transfer, :desc => 'Transfer function (ie Linear), by enum', :req => true
52
- :x1, # u8 :colorimetric, :desc => 'Colorimetric (ie YcbCr), by enum', :req => true
53
- :x1, # u8 :bit_size, :desc => 'Bit size for element (ie 10)', :req => true
54
- #
55
- :x2, # u16 :packing, :desc => 'Packing (0=Packed into 32-bit words, 1=Filled to 32-bit words))', :req => true
56
- :x2, # u16 :encoding, :desc => "Encoding (0=None, 1=RLE)", :req => true
57
- :x4, # u32 :data_offset, :desc => 'Offset to data for this image element', :req => true
58
- :x4, # u32 :end_of_line_padding, :desc => "End-of-line padding for this image element"
59
- :x4, # u32 :end_of_image_padding, :desc => "End-of-line padding for this image element"
60
- :x32, # char :description, 32
61
- ].join
9
+ class ByteOrderHintIO < SimpleDelegator
10
+ def initialize(io, is_little_endian)
11
+ super(io)
12
+ @little_endian = is_little_endian
13
+ end
62
14
 
63
- IMAGE_INFO = [
64
- :x2, # u16 :orientation, OrientationInfo, :desc => 'Orientation descriptor', :req => true
65
- :n1, # u16 :number_elements, :desc => 'How many elements to scan', :req => true
66
- :N1, # u32 :pixels_per_line, :desc => 'Pixels per horizontal line', :req => true
67
- :N1, # u32 :lines_per_element, :desc => 'Line count', :req => true
68
- IMAGE_ELEMENT * 8, # 8 IMAGE_ELEMENT structures
69
- :x52, # blanking :reserve, 52
70
- ].join
15
+ def le?
16
+ @little_endian
17
+ end
18
+ end
71
19
 
72
- ORIENTATION_INFO = [
73
- :x4, # u32 :x_offset
74
- :x4, # u32 :y_offset
75
- #
76
- :x4, # r32 :x_center
77
- :x4, # r32 :y_center
78
- #
79
- :x4, # u32 :x_size, :desc => 'Original X size'
80
- :x4, # u32 :y_size, :desc => 'Original Y size'
81
- #
82
- :x100, # char :filename, 100, :desc => "Source image filename"
83
- :x24, # char :timestamp, 24, :desc => "Source image/tape timestamp"
84
- :x32, # char :device, 32, :desc => "Input device or tape"
85
- :x32, # char :serial, 32, :desc => "Input device serial number"
86
- #
87
- :x4, # array :border, :u16, 4, :desc => 'Border validity: XL, XR, YT, YB'
88
- :x4,
89
- :x4,
90
- :x4,
20
+ private_constant :ByteOrderHintIO
91
21
 
92
- # TODO: - the aspect ratio might be handy to recover since it
93
- # will be used in DPX files in, say, anamorphic (non-square pixels)
94
- :x4, # array :aspect_ratio , :u32, 2, :desc => "Aspect (H:V)"
95
- :x4,
96
- #
97
- :x28, # blanking :reserve, 28
98
- ].join
22
+ def call(io)
23
+ io = FormatParser::IOConstraint.new(io)
24
+ magic = safe_read(io, 4)
25
+ return unless [BE_MAGIC, LE_MAGIC].include?(magic)
99
26
 
100
- DPX_INFO = [
101
- FILE_INFO,
102
- IMAGE_INFO,
103
- ORIENTATION_INFO,
104
- ].join
27
+ io.seek(0)
105
28
 
106
- DPX_INFO_LE = DPX_INFO.tr('n', 'v').tr('N', 'V')
29
+ dpx_structure = DPX.read_and_unpack(ByteOrderHintIO.new(io, magic == LE_MAGIC))
107
30
 
108
- SIZEOF = ->(pattern) {
109
- bytes_per_element = {
110
- 'v' => 2, # 16bit uints
111
- 'n' => 2,
112
- 'V' => 4, # 32bit uints
113
- 'N' => 4,
114
- 'C' => 1,
115
- 'x' => 1,
116
- }
117
- pattern.scan(/[^\d]\d+/).map do |pattern|
118
- unpack_code = pattern[0]
119
- num_repetitions = pattern[1..-1].to_i
120
- bytes_per_element.fetch(unpack_code) * num_repetitions
121
- end.inject(&:+)
122
- }
31
+ w = dpx_structure.fetch(:image).fetch(:pixels_per_line)
32
+ h = dpx_structure.fetch(:image).fetch(:lines_per_element)
123
33
 
124
- BE_MAGIC = 'SDPX'
125
- LE_MAGIC = BE_MAGIC.reverse
126
- HEADER_SIZE = SIZEOF[DPX_INFO] # Does not include the initial 4 bytes
34
+ pixel_aspect_w = dpx_structure.fetch(:orientation).fetch(:horizontal_pixel_aspect)
35
+ pixel_aspect_h = dpx_structure.fetch(:orientation).fetch(:vertical_pixel_aspect)
36
+ pixel_aspect = pixel_aspect_w / pixel_aspect_h.to_f
127
37
 
128
- def call(io)
129
- io = FormatParser::IOConstraint.new(io)
130
- magic = io.read(4)
38
+ image_aspect = w / h.to_f * pixel_aspect
131
39
 
132
- return unless [BE_MAGIC, LE_MAGIC].include?(magic)
40
+ display_w = w
41
+ display_h = h
42
+ if image_aspect > 1
43
+ display_h = (display_w / image_aspect).round
44
+ else
45
+ display_w = (display_h * image_aspect).round
46
+ end
133
47
 
134
- unpack_pattern = DPX_INFO
135
- unpack_pattern = DPX_INFO_LE if magic == LE_MAGIC
136
- _num_elements, pixels_per_line, num_lines, *_ = safe_read(io, HEADER_SIZE).unpack(unpack_pattern)
137
48
  FormatParser::Image.new(
138
49
  format: :dpx,
139
- width_px: pixels_per_line,
140
- height_px: num_lines
50
+ width_px: w,
51
+ height_px: h,
52
+ display_width_px: display_w,
53
+ display_height_px: display_h,
54
+ intrinsics: dpx_structure,
141
55
  )
142
56
  end
143
57
 
@@ -0,0 +1,229 @@
1
+ class FormatParser::DPXParser
2
+ # A teeny-tiny rewording of depix (https://rubygems.org/gems/depix)
3
+ class Binstr
4
+ TO_LITTLE_ENDIAN = {
5
+ 'N' => 'V',
6
+ 'n' => 'v',
7
+ }
8
+
9
+ class Capture < Struct.new(:pattern, :bytes)
10
+ include FormatParser::IOUtils
11
+ def read_and_unpack(io)
12
+ platform_byte_order_pattern = io.le? ? TO_LITTLE_ENDIAN.fetch(pattern, pattern) : pattern
13
+ safe_read(io, bytes).unpack(platform_byte_order_pattern).first
14
+ end
15
+ end
16
+
17
+ def self.fields
18
+ @fields ||= []
19
+ @fields
20
+ end
21
+
22
+ def self.char(field_name, length, **_kwargs)
23
+ fields << [field_name, Capture.new('Z%d' % length, length)]
24
+ end
25
+
26
+ def self.u8(field_name, **_kwargs)
27
+ fields << [field_name, Capture.new('c', 1)]
28
+ end
29
+
30
+ def self.u16(field_name, **_kwargs)
31
+ fields << [field_name, Capture.new('n', 2)]
32
+ end
33
+
34
+ def self.u32(field_name, **_kwargs)
35
+ fields << [field_name, Capture.new('N', 4)]
36
+ end
37
+
38
+ def self.r32(field_name, **_kwargs)
39
+ fields << [field_name, Capture.new('e', 4)]
40
+ end
41
+
42
+ def self.blanking(field_name, length, **_kwargs)
43
+ fields << [field_name, Capture.new('x%d' % length, length)]
44
+ end
45
+
46
+ def self.array(field_name, nested_struct_descriptor_or_symbol, n_items, **_kwargs)
47
+ if nested_struct_descriptor_or_symbol.is_a?(Symbol)
48
+ n_items.times do |i|
49
+ public_send(nested_struct_descriptor_or_symbol, '%s_%d' % [field_name, i])
50
+ end
51
+ else
52
+ n_items.times do |i|
53
+ fields << ['%s_%d' % [field_name, i], nested_struct_descriptor_or_symbol]
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.inner(field_name, nested_struct_descriptor, **_kwargs)
59
+ fields << [field_name, nested_struct_descriptor]
60
+ end
61
+
62
+ def self.cleanup(v)
63
+ case v
64
+ when String
65
+ v.scrub
66
+ when Float
67
+ v.nan? ? nil : v
68
+ else
69
+ v
70
+ end
71
+ end
72
+
73
+ def self.read_and_unpack(io)
74
+ fields.each_with_object({}) do |(field_name, capture), h|
75
+ maybe_value = cleanup(capture.read_and_unpack(io))
76
+ h[field_name] = maybe_value unless maybe_value.nil?
77
+ end
78
+ end
79
+ end
80
+
81
+ class FileInfo < Binstr
82
+ char :magic, 4, desc: 'Endianness (SDPX is big endian)', req: true
83
+ u32 :image_offset, desc: 'Offset to image data in bytes', req: true
84
+ char :version, 8, desc: 'Version of header format', req: true
85
+
86
+ u32 :file_size, desc: 'Total image size in bytes', req: true
87
+ u32 :ditto_key, desc: 'Whether the basic headers stay the same through the sequence (1 means they do)'
88
+ u32 :generic_size, desc: 'Generic header length'
89
+ u32 :industry_size, desc: 'Industry header length'
90
+ u32 :user_size, desc: 'User header length'
91
+
92
+ char :filename, 100, desc: 'Original filename'
93
+ char :timestamp, 24, desc: 'Creation timestamp'
94
+ char :creator, 100, desc: 'Creator application'
95
+ char :project, 200, desc: 'Project name'
96
+ char :copyright, 200, desc: 'Copyright'
97
+
98
+ u32 :encrypt_key, desc: 'Encryption key'
99
+ blanking :reserve, 104
100
+ end
101
+
102
+ class FilmInfo < Binstr
103
+ char :id, 2, desc: 'Film mfg. ID code (2 digits from film edge code)'
104
+ char :type, 2, desc: 'Film type (2 digits from film edge code)'
105
+ char :offset, 2, desc: 'Offset in perfs (2 digits from film edge code)'
106
+ char :prefix, 6, desc: 'Prefix (6 digits from film edge code'
107
+ char :count, 4, desc: 'Count (4 digits from film edge code)'
108
+ char :format, 32, desc: 'Format (e.g. Academy)'
109
+
110
+ u32 :frame_position, desc: 'Frame position in sequence'
111
+ u32 :sequence_extent, desc: 'Sequence length'
112
+ u32 :held_count, desc: 'For how many frames the frame is held'
113
+
114
+ r32 :frame_rate, desc: 'Frame rate'
115
+ r32 :shutter_angle, desc: 'Shutter angle'
116
+
117
+ char :frame_id, 32, desc: 'Frame identification (keyframe)'
118
+ char :slate, 100, desc: 'Slate information'
119
+ blanking :reserve, 56
120
+ end
121
+
122
+ class ImageElement < Binstr
123
+ u32 :data_sign, desc: 'Data sign (0=unsigned, 1=signed). Core is unsigned', req: true
124
+
125
+ u32 :low_data, desc: 'Reference low data code value'
126
+ r32 :low_quantity, desc: 'Reference low quantity represented'
127
+ u32 :high_data, desc: 'Reference high data code value (1023 for 10bit per channel)'
128
+ r32 :high_quantity, desc: 'Reference high quantity represented'
129
+
130
+ u8 :descriptor, desc: 'Descriptor for this image element (ie Video or Film), by enum', req: true
131
+ u8 :transfer, desc: 'Transfer function (ie Linear), by enum', req: true
132
+ u8 :colorimetric, desc: 'Colorimetric (ie YcbCr), by enum', req: true
133
+ u8 :bit_size, desc: 'Bit size for element (ie 10)', req: true
134
+
135
+ u16 :packing, desc: 'Packing (0=Packed into 32-bit words, 1=Filled to 32-bit words))', req: true
136
+ u16 :encoding, desc: 'Encoding (0=None, 1=RLE)', req: true
137
+ u32 :data_offset, desc: 'Offset to data for this image element', req: true
138
+ u32 :end_of_line_padding, desc: 'End-of-line padding for this image element'
139
+ u32 :end_of_image_padding, desc: 'End-of-line padding for this image element'
140
+ char :description, 32
141
+ end
142
+
143
+ class OrientationInfo < Binstr
144
+ u32 :x_offset
145
+ u32 :y_offset
146
+
147
+ r32 :x_center
148
+ r32 :y_center
149
+
150
+ u32 :x_size, desc: 'Original X size'
151
+ u32 :y_size, desc: 'Original Y size'
152
+
153
+ char :filename, 100, desc: 'Source image filename'
154
+ char :timestamp, 24, desc: 'Source image/tape timestamp'
155
+ char :device, 32, desc: 'Input device or tape'
156
+ char :serial, 32, desc: 'Input device serial number'
157
+
158
+ array :border, :u16, 4, desc: 'Border validity: XL, XR, YT, YB'
159
+ u32 :horizontal_pixel_aspect, desc: 'Aspect (H)'
160
+ u32 :vertical_pixel_aspect, desc: 'Aspect (V)'
161
+
162
+ blanking :reserve, 28
163
+ end
164
+
165
+ class TelevisionInfo < Binstr
166
+ u32 :time_code, desc: 'Timecode, formatted as HH:MM:SS:FF in the 4 higher bits of each 8bit group'
167
+ u32 :user_bits, desc: 'Timecode UBITs'
168
+ u8 :interlace, desc: 'Interlace (0 = noninterlaced; 1 = 2:1 interlace'
169
+
170
+ u8 :field_number, desc: 'Field number'
171
+ u8 :video_signal, desc: 'Video signal (by enum)'
172
+ u8 :padding, desc: 'Zero (for byte alignment)'
173
+
174
+ r32 :horizontal_sample_rate, desc: 'Horizontal sampling Hz'
175
+ r32 :vertical_sample_rate, desc: 'Vertical sampling Hz'
176
+ r32 :frame_rate, desc: 'Frame rate'
177
+ r32 :time_offset, desc: 'From sync pulse to first pixel'
178
+ r32 :gamma, desc: 'Gamma'
179
+ r32 :black_level, desc: 'Black pedestal code value'
180
+ r32 :black_gain, desc: 'Black gain code value'
181
+ r32 :break_point, desc: 'Break point (?)'
182
+ r32 :white_level, desc: 'White level'
183
+ r32 :integration_times, desc: 'Integration times (S)'
184
+ blanking :reserve, 4 # As long as a real
185
+ end
186
+
187
+ class UserInfo < Binstr
188
+ char :id, 32, desc: 'Name of the user data tag'
189
+ u32 :user_data_ptr
190
+ end
191
+
192
+ class ImageInfo < Binstr
193
+ u16 :orientation, desc: 'To which orientation descriptor this relates', req: true
194
+ u16 :number_elements, desc: 'How many elements to scan', req: true
195
+
196
+ u32 :pixels_per_line, desc: 'Pixels per horizontal line', req: true
197
+ u32 :lines_per_element, desc: 'Line count', req: true
198
+
199
+ array :image_elements, ImageElement, 8, desc: 'Image elements'
200
+
201
+ blanking :reserve, 52
202
+
203
+ # Only expose the elements present
204
+ def image_elements #:nodoc:
205
+ @image_elements[0...number_elements]
206
+ end
207
+ end
208
+
209
+ # This is the main structure represinting headers of one DPX file
210
+ class DPX < Binstr
211
+ inner :file, FileInfo, desc: 'File information', req: true
212
+ inner :image, ImageInfo, desc: 'Image information', req: true
213
+ inner :orientation, OrientationInfo, desc: 'Orientation', req: true
214
+ inner :film, FilmInfo, desc: 'Film industry info', req: true
215
+ inner :television, TelevisionInfo, desc: 'TV industry info', req: true
216
+ blanking :user, 32 + 4, desc: 'User info', req: true
217
+
218
+ def self.read_and_unpack(io)
219
+ super.tap do |h|
220
+ num_elems = h[:image][:number_elements]
221
+ num_elems.upto(8) do |n|
222
+ h[:image].delete("image_elements_#{n}")
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ private_constant :Binstr, :FileInfo, :FilmInfo, :ImageElement, :OrientationInfo, :TelevisionInfo, :UserInfo, :ImageInfo
229
+ end
@@ -1,8 +1,31 @@
1
1
  require 'exifr/tiff'
2
2
  require 'delegate'
3
3
 
4
- class FormatParser::EXIFParser
5
- include FormatParser::IOUtils
4
+ module FormatParser::EXIFParser
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
+ ROTATED_ORIENTATIONS = ORIENTATIONS - [
16
+ :bottom_left,
17
+ :bottom_right,
18
+ :top_left,
19
+ :top_right
20
+ ]
21
+ module MethodsMethodFix
22
+ # Fix a little bug in EXIFR which breaks delegators
23
+ # https://github.com/remvee/exifr/pull/55
24
+ def methods(*)
25
+ super() # no args
26
+ end
27
+ end
28
+ EXIFR::TIFF.prepend(MethodsMethodFix)
6
29
 
7
30
  # EXIFR kindly requests the presence of a few more methods than what our IOConstraint
8
31
  # is willing to provide, but they can be derived from the available ones
@@ -30,47 +53,26 @@ class FormatParser::EXIFParser
30
53
  alias_method :getbyte, :readbyte
31
54
  end
32
55
 
33
- # Squash exifr's invalid date warning since we do not use that data.
34
- logger = Logger.new(nil)
35
- EXIFR.logger = logger
36
-
37
- attr_accessor :exif_data, :orientation, :width, :height
38
-
39
- ORIENTATIONS = [
40
- :top_left,
41
- :top_right,
42
- :bottom_right,
43
- :bottom_left,
44
- :left_top,
45
- :right_top,
46
- :right_bottom,
47
- :left_bottom
48
- ]
56
+ class EXIFResult < SimpleDelegator
57
+ def rotated?
58
+ ROTATED_ORIENTATIONS.include?(orientation)
59
+ end
49
60
 
50
- def initialize(io_blob_with_exif_data)
51
- @exif_io = IOExt.new(io_blob_with_exif_data)
52
- @exif_data = nil
53
- @orientation = nil
54
- @height = nil
55
- @width = nil
56
- end
61
+ def to_json(*maybe_coder)
62
+ __getobj__.to_hash.to_json(*maybe_coder)
63
+ end
57
64
 
58
- def scan_image_tiff
59
- raw_exif_data = EXIFR::TIFF.new(@exif_io)
60
- # For things that we don't yet have a parser for
61
- # we make the raw exif result available
62
- @exif_data = raw_exif_data
63
- @orientation = orientation_parser(raw_exif_data)
64
- @width = @exif_data.width
65
- @height = @exif_data.height
65
+ def orientation
66
+ value = __getobj__.orientation.to_i
67
+ ORIENTATIONS.fetch(value - 1)
68
+ end
66
69
  end
67
70
 
68
- def orientation_parser(raw_exif_data)
69
- value = raw_exif_data.orientation.to_i
70
- @orientation = ORIENTATIONS[value - 1] if valid_orientation?(value)
71
- end
71
+ # Squash exifr's invalid date warning since we do not use that data.
72
+ EXIFR.logger = Logger.new(nil)
72
73
 
73
- def valid_orientation?(value)
74
- (1..ORIENTATIONS.length).include?(value)
74
+ def exif_from_tiff_io(constrained_io)
75
+ raw_exif_data = EXIFR::TIFF.new(IOExt.new(constrained_io))
76
+ raw_exif_data ? EXIFResult.new(raw_exif_data) : nil
75
77
  end
76
78
  end
@@ -1,5 +1,6 @@
1
1
  class FormatParser::JPEGParser
2
2
  include FormatParser::IOUtils
3
+ include FormatParser::EXIFParser
3
4
 
4
5
  class InvalidStructure < StandardError
5
6
  end
@@ -16,8 +17,6 @@ class FormatParser::JPEGParser
16
17
  @buf = FormatParser::IOConstraint.new(io)
17
18
  @width = nil
18
19
  @height = nil
19
- @orientation = nil
20
- @intrinsics = {}
21
20
  scan
22
21
  end
23
22
 
@@ -66,13 +65,17 @@ class FormatParser::JPEGParser
66
65
 
67
66
  # Return at the earliest possible opportunity
68
67
  if @width && @height
69
- return FormatParser::Image.new(
68
+ result = FormatParser::Image.new(
70
69
  format: :jpg,
71
70
  width_px: @width,
72
71
  height_px: @height,
73
- orientation: @orientation,
74
- intrinsics: @intrinsics,
72
+ display_width_px: @exif_data&.rotated? ? @height : @width,
73
+ display_height_px: @exif_data&.rotated? ? @width : @height,
74
+ orientation: @exif_data&.orientation,
75
+ intrinsics: {exif: @exif_data},
75
76
  )
77
+
78
+ return result
76
79
  end
77
80
 
78
81
  nil # We could not parse anything
@@ -138,27 +141,8 @@ class FormatParser::JPEGParser
138
141
  exif_data = safe_read(@buf, app1_frame_content_length - EXIF_MAGIC_STRING.bytesize)
139
142
 
140
143
  FormatParser::Measurometer.add_distribution_value('format_parser.JPEGParser.bytes_sent_to_exif_parser', exif_data.bytesize)
141
- scanner = FormatParser::EXIFParser.new(StringIO.new(exif_data))
142
- scanner.scan_image_tiff
143
-
144
- @exif_output = scanner.exif_data
145
- @orientation = scanner.orientation unless scanner.orientation.nil?
146
- @intrinsics[:exif_pixel_x_dimension] = @exif_output.pixel_x_dimension
147
- @intrinsics[:exif_pixel_y_dimension] = @exif_output.pixel_y_dimension
148
- # Save these two for later, when we decide to provide display width /
149
- # display height in addition to pixel buffer width / height. These two
150
- # are _different concepts_. Imagine you have an image shot with a camera
151
- # in portrait orientation, and the camera has an anamorphic lens. That
152
- # anamorpohic lens is a smart lens, and therefore transmits pixel aspect
153
- # ratio to the camera, and the camera encodes that aspect ratio into the
154
- # image metadata. If we want to know what size our _pixel buffer_ will be,
155
- # and how to _read_ the pixel data (stride/interleaving) - we need the
156
- # pixel buffer dimensions. If we want to know what aspect and dimensions
157
- # our file is going to have _once displayed_ and _once pixels have been
158
- # brought to the right orientation_ we need to work with **display dimensions**
159
- # which can be remarkably different from the pixel buffer dimensions.
160
- @exif_width = scanner.width
161
- @exif_height = scanner.height
144
+
145
+ @exif_data = exif_from_tiff_io(StringIO.new(exif_data))
162
146
  rescue EXIFR::MalformedTIFF
163
147
  # Not a JPEG or the Exif headers contain invalid data, or
164
148
  # an APP1 marker was detected in a file that is not a JPEG
@@ -1,5 +1,6 @@
1
1
  class FormatParser::TIFFParser
2
2
  include FormatParser::IOUtils
3
+ include FormatParser::EXIFParser
3
4
 
4
5
  MAGIC_LE = [0x49, 0x49, 0x2A, 0x0].pack('C4')
5
6
  MAGIC_BE = [0x4D, 0x4D, 0x0, 0x2A].pack('C4')
@@ -14,16 +15,20 @@ class FormatParser::TIFFParser
14
15
  # The TIFF scanner in EXIFR is plenty good enough,
15
16
  # so why don't we use it? It does all the right skips
16
17
  # in all the right places.
17
- scanner = FormatParser::EXIFParser.new(io)
18
- scanner.scan_image_tiff
19
- return unless scanner.exif_data
18
+ exif_data = exif_from_tiff_io(io)
19
+ return unless exif_data
20
+
21
+ w = exif_data.image_width
22
+ h = exif_data.image_length
20
23
 
21
24
  FormatParser::Image.new(
22
25
  format: :tif,
23
- width_px: scanner.exif_data.image_width,
24
- height_px: scanner.exif_data.image_length,
25
- # might be nil if EXIF metadata wasn't found
26
- orientation: scanner.orientation
26
+ width_px: w,
27
+ height_px: h,
28
+ display_width_px: exif_data.rotated? ? h : w,
29
+ display_height_px: exif_data.rotated? ? w : h,
30
+ orientation: exif_data.orientation,
31
+ intrinsics: {exif: exif_data},
27
32
  )
28
33
  rescue EXIFR::MalformedTIFF
29
34
  nil
@@ -1,3 +1,9 @@
1
+ # We need to apply various limits so that parsers do not over-read, do not cause too many HTTP
2
+ # requests to be dispatched and so on. These should be _balanced_ with one another- for example,
3
+ # we cannot tell a parser that it is limited to reading 1024 bytes while at the same time
4
+ # limiting the size of the cache pages it may slurp in to less than that amount, since
5
+ # it can quickly become frustrating. ReadLimitsConfig computes these limits
6
+ # for us, in a fairly balanced way, based on one setting.
1
7
  class FormatParser::ReadLimitsConfig
2
8
  MAX_PAGE_FAULTS = 16
3
9
 
@@ -42,6 +42,21 @@ describe FormatParser::AttributesJSON do
42
42
  expect(readback[:some_infinity]).to be_nil
43
43
  end
44
44
 
45
+ it 'converts NaN to nil' do
46
+ anon_class = Class.new do
47
+ include FormatParser::AttributesJSON
48
+ attr_accessor :some_nan
49
+ def some_nan
50
+ (1.0 / 0.0).to_f
51
+ end
52
+ end
53
+ instance = anon_class.new
54
+ output = JSON.dump(instance)
55
+ readback = JSON.parse(output, symbolize_names: true)
56
+ expect(readback).to have_key(:some_nan)
57
+ expect(readback[:some_nan]).to be_nil
58
+ end
59
+
45
60
  it 'provides a default implementation of to_json as well' do
46
61
  anon_class = Class.new do
47
62
  include FormatParser::AttributesJSON
@@ -1,17 +1,31 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe FormatParser do
4
- it 'returns nil when trying to parse an empty IO' do
5
- d = StringIO.new('')
6
- expect(FormatParser.parse(d)).to be_nil
7
- end
4
+ describe '.parse' do
5
+ it 'returns nil when trying to parse an empty IO' do
6
+ d = StringIO.new('')
7
+ expect(FormatParser.parse(d)).to be_nil
8
+ end
8
9
 
9
- it 'returns nil when parsing an IO no parser can make sense of' do
10
- d = StringIO.new(Random.new.bytes(1))
11
- expect(FormatParser.parse(d)).to be_nil
12
- end
10
+ it 'returns nil when parsing an IO no parser can make sense of' do
11
+ d = StringIO.new(Random.new.bytes(1))
12
+ expect(FormatParser.parse(d)).to be_nil
13
+ end
14
+
15
+ it 'uses the passed ReadLimitsConfig and applies limits in it' do
16
+ conf = FormatParser::ReadLimitsConfig.new(16)
17
+ d = StringIO.new(Random.new.bytes(64 * 1024))
18
+ expect(FormatParser.parse(d, limits_config: conf)).to be_nil
19
+ end
20
+
21
+ it 'parses our fixtures without raising any errors' do
22
+ Dir.glob(fixtures_dir + '/**/*.*').sort.each do |fixture_path|
23
+ File.open(fixture_path, 'rb') do |file|
24
+ FormatParser.parse(file)
25
+ end
26
+ end
27
+ end
13
28
 
14
- describe 'with fuzzing' do
15
29
  it "returns either a valid result or a nil for all fuzzed inputs at seed #{RSpec.configuration.seed}" do
16
30
  r = Random.new(RSpec.configuration.seed)
17
31
  1024.times do
@@ -19,54 +33,54 @@ describe FormatParser do
19
33
  FormatParser.parse(random_blob) # If there is an error in one of the parsers the example will raise too
20
34
  end
21
35
  end
22
- end
23
36
 
24
- it 'fails gracefully when a parser module reads more and more causing page faults and prevents too many reads on the source' do
25
- exploit = ->(io) {
26
- loop {
27
- skip = 16 * 1024
28
- io.read(1)
29
- io.seek(io.pos + skip)
37
+ it 'fails gracefully when a parser module reads more and more causing page faults and prevents too many reads on the source' do
38
+ exploit = ->(io) {
39
+ loop {
40
+ skip = 16 * 1024
41
+ io.read(1)
42
+ io.seek(io.pos + skip)
43
+ }
30
44
  }
31
- }
32
- FormatParser.register_parser exploit, natures: :document, formats: :exploit
45
+ FormatParser.register_parser exploit, natures: :document, formats: :exploit
33
46
 
34
- sample_io = StringIO.new(Random.new.bytes(1024 * 1024 * 8))
35
- allow(sample_io).to receive(:read).and_call_original
47
+ sample_io = StringIO.new(Random.new.bytes(1024 * 1024 * 8))
48
+ allow(sample_io).to receive(:read).and_call_original
36
49
 
37
- result = FormatParser.parse(sample_io, formats: [:exploit])
50
+ result = FormatParser.parse(sample_io, formats: [:exploit])
38
51
 
39
- expect(sample_io).to have_received(:read).at_most(16).times
40
- expect(result).to be_nil
52
+ expect(sample_io).to have_received(:read).at_most(16).times
53
+ expect(result).to be_nil
41
54
 
42
- FormatParser.deregister_parser(exploit)
43
- end
55
+ FormatParser.deregister_parser(exploit)
56
+ end
44
57
 
45
- describe 'multiple values return' do
46
- let(:blob) { StringIO.new(Random.new.bytes(512 * 1024)) }
47
- let(:audio) { FormatParser::Audio.new(format: :aiff, num_audio_channels: 1) }
48
- let(:image) { FormatParser::Image.new(format: :dpx, width_px: 1, height_px: 1) }
58
+ describe 'when multiple results are requested' do
59
+ let(:blob) { StringIO.new(Random.new.bytes(512 * 1024)) }
60
+ let(:audio) { FormatParser::Audio.new(format: :aiff, num_audio_channels: 1) }
61
+ let(:image) { FormatParser::Image.new(format: :dpx, width_px: 1, height_px: 1) }
49
62
 
50
- context '.parse called with options' do
51
- before do
52
- expect_any_instance_of(FormatParser::AIFFParser).to receive(:call).and_return(audio)
53
- expect_any_instance_of(FormatParser::DPXParser).to receive(:call).and_return(image)
54
- end
55
-
56
- subject { FormatParser.parse(blob, results: :all) }
63
+ context 'with :result=> :all (multiple results)' do
64
+ before do
65
+ expect_any_instance_of(FormatParser::AIFFParser).to receive(:call).and_return(audio)
66
+ expect_any_instance_of(FormatParser::DPXParser).to receive(:call).and_return(image)
67
+ end
57
68
 
58
- it { is_expected.to include(image) }
59
- it { is_expected.to include(audio) }
60
- end
69
+ subject { FormatParser.parse(blob, results: :all) }
61
70
 
62
- context '.parse called without hash options' do
63
- before do
64
- expect_any_instance_of(FormatParser::DPXParser).to receive(:call).and_return(image)
71
+ it { is_expected.to include(image) }
72
+ it { is_expected.to include(audio) }
65
73
  end
66
74
 
67
- subject { FormatParser.parse(blob) }
75
+ context 'without :result=> :all (first result)' do
76
+ before do
77
+ expect_any_instance_of(FormatParser::DPXParser).to receive(:call).and_return(image)
78
+ end
79
+
80
+ subject { FormatParser.parse(blob) }
68
81
 
69
- it { is_expected.to eq(image) }
82
+ it { is_expected.to eq(image) }
83
+ end
70
84
  end
71
85
  end
72
86
 
@@ -76,19 +90,15 @@ describe FormatParser do
76
90
  result = FormatParser.parse_file_at(path)
77
91
  expect(result.nature).to eq(:audio)
78
92
  end
79
- end
80
93
 
81
- describe 'when parsing fixtures' do
82
- Dir.glob(fixtures_dir + '/**/*.*').sort.each do |fixture_path|
83
- it "parses #{fixture_path} without raising any errors" do
84
- File.open(fixture_path, 'rb') do |file|
85
- FormatParser.parse(file)
86
- end
87
- end
94
+ it 'passes keyword arguments to parse()' do
95
+ path = fixtures_dir + '/WAV/c_M1F1-Alaw-AFsp.wav'
96
+ expect(FormatParser).to receive(:parse).with(an_instance_of(File), foo: :bar)
97
+ FormatParser.parse_file_at(path, foo: :bar)
88
98
  end
89
99
  end
90
100
 
91
- describe 'parsers_for' do
101
+ describe '.parsers_for' do
92
102
  it 'raises on an invalid request' do
93
103
  expect {
94
104
  FormatParser.parsers_for([:image], [:fdx])
@@ -111,7 +121,7 @@ describe FormatParser do
111
121
  end
112
122
  end
113
123
 
114
- describe 'parser registration and deregistration with the module' do
124
+ describe '.register_parser and .deregister_parser' do
115
125
  it 'registers a parser for a certain nature and format' do
116
126
  some_parser = ->(_io) { 'I parse EXRs! Whee!' }
117
127
 
@@ -130,4 +140,10 @@ describe FormatParser do
130
140
  }.to raise_error(/No parsers provide/)
131
141
  end
132
142
  end
143
+
144
+ describe '.default_limits_config' do
145
+ it 'returns a ReadLimitsConfig object' do
146
+ expect(FormatParser.default_limits_config).to be_kind_of(FormatParser::ReadLimitsConfig)
147
+ end
148
+ end
133
149
  end
@@ -1,29 +1,40 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe FormatParser::DPXParser do
4
- describe 'with Depix example files' do
5
- Dir.glob(fixtures_dir + '/dpx/*.*').each do |dpx_path|
6
- it "is able to parse #{File.basename(dpx_path)}" do
7
- parsed = subject.call(File.open(dpx_path, 'rb'))
8
-
9
- expect(parsed).not_to be_nil
10
- expect(parsed.nature).to eq(:image)
11
- expect(parsed.format).to eq(:dpx)
12
-
13
- # If we have an error in the struct offsets these values are likely to become
14
- # the maximum value of a 4-byte uint, which is way higher
15
- expect(parsed.width_px).to be_kind_of(Integer)
16
- expect(parsed.width_px).to be_between(0, 2048)
17
- expect(parsed.height_px).to be_kind_of(Integer)
18
- expect(parsed.height_px).to be_between(0, 4000)
19
- end
20
- end
4
+ Dir.glob(fixtures_dir + '/dpx/*.*').each do |dpx_path|
5
+ it "is able to parse #{File.basename(dpx_path)}" do
6
+ parsed = subject.call(File.open(dpx_path, 'rb'))
7
+
8
+ expect(parsed).not_to be_nil
9
+ expect(parsed.nature).to eq(:image)
10
+ expect(parsed.format).to eq(:dpx)
21
11
 
22
- it 'correctly reads pixel dimensions' do
23
- fi = File.open(fixtures_dir + '/dpx/026_FROM_HERO_TAPE_5-3-1_MOV.0029.dpx', 'rb')
24
- parsed = subject.call(fi)
25
- expect(parsed.width_px).to eq(1920)
26
- expect(parsed.height_px).to eq(1080)
12
+ # If we have an error in the struct offsets these values are likely to become
13
+ # the maximum value of a 4-byte uint, which is way higher
14
+ expect(parsed.width_px).to be_kind_of(Integer)
15
+ expect(parsed.width_px).to be_between(0, 2048)
16
+ expect(parsed.height_px).to be_kind_of(Integer)
17
+ expect(parsed.height_px).to be_between(0, 4000)
27
18
  end
28
19
  end
20
+
21
+ it 'correctly reads display dimensions corrected for the pixel aspect from the DPX header' do
22
+ fi = File.open(fixtures_dir + '/dpx/aspect_237_example.dpx', 'rb')
23
+ parsed = subject.call(fi)
24
+
25
+ expect(parsed.width_px).to eq(1920)
26
+ expect(parsed.height_px).to eq(1080)
27
+
28
+ expect(parsed.display_width_px).to eq(1920)
29
+ expect(parsed.display_height_px).to eq(810)
30
+
31
+ expect(parsed.display_width_px / parsed.display_height_px.to_f).to be_within(0.01).of(2.37)
32
+ end
33
+
34
+ it 'does not explode on invalid inputs' do
35
+ invalid = StringIO.new('SDPX' + (' ' * 64))
36
+ expect {
37
+ subject.call(invalid)
38
+ }.to raise_error(FormatParser::IOUtils::InvalidRead)
39
+ end
29
40
  end
@@ -1,17 +1,19 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe FormatParser::EXIFParser do
4
+ let(:subject) do
5
+ Object.new.tap { |o| o.extend FormatParser::EXIFParser }
6
+ end
7
+
4
8
  describe 'is able to correctly parse orientation for all the TIFF EXIF examples from FastImage' do
5
9
  Dir.glob(fixtures_dir + '/exif-orientation-testimages/tiff-*/*.tif').each do |tiff_path|
6
10
  filename = File.basename(tiff_path)
7
11
  it "is able to parse #{filename}" do
8
- parser = FormatParser::EXIFParser.new(File.open(tiff_path, 'rb'))
9
- parser.scan_image_tiff
10
- expect(parser).not_to be_nil
11
-
12
- expect(parser.orientation).to be_kind_of(Symbol)
12
+ result = subject.exif_from_tiff_io(File.open(tiff_path, 'rb'))
13
+ expect(result).not_to be_nil
14
+ expect(result.orientation).to be_kind_of(Symbol)
13
15
  # Filenames in this dir correspond with the orientation of the file
14
- expect(filename.include?(parser.orientation.to_s)).to be true
16
+ expect(filename).to include(result.orientation.to_s)
15
17
  end
16
18
  end
17
19
  end
@@ -26,6 +26,8 @@ describe FormatParser::JPEGParser do
26
26
  expect(parsed.orientation).to be_kind_of(Symbol)
27
27
  expect(parsed.width_px).to be > 0
28
28
  expect(parsed.height_px).to be > 0
29
+ expect(parsed.display_width_px).to eq(1240)
30
+ expect(parsed.display_height_px).to eq(1754)
29
31
  end
30
32
 
31
33
  bottom_left_path = fixtures_dir + '/exif-orientation-testimages/jpg/bottom_left.jpg'
@@ -33,20 +35,30 @@ describe FormatParser::JPEGParser do
33
35
  expect(parsed.orientation).to eq(:bottom_left)
34
36
  expect(parsed.width_px).to eq(1240)
35
37
  expect(parsed.height_px).to eq(1754)
38
+ expect(parsed.display_width_px).to eq(1240)
39
+ expect(parsed.display_height_px).to eq(1754)
40
+ expect(parsed.intrinsics[:exif]).not_to be_nil
36
41
 
37
42
  top_right_path = fixtures_dir + '/exif-orientation-testimages/jpg/right_bottom.jpg'
38
43
  parsed = subject.call(File.open(top_right_path, 'rb'))
39
44
  expect(parsed.orientation).to eq(:right_bottom)
40
45
  expect(parsed.width_px).to eq(1754)
41
46
  expect(parsed.height_px).to eq(1240)
47
+ expect(parsed.display_width_px).to eq(1240)
48
+ expect(parsed.display_height_px).to eq(1754)
49
+ expect(parsed.intrinsics[:exif]).not_to be_nil
42
50
  end
43
51
 
44
52
  it 'gives true pixel dimensions priority over pixel dimensions in EXIF tags' do
45
53
  jpeg_path = fixtures_dir + '/JPEG/divergent_pixel_dimensions_exif.jpg'
46
54
  result = subject.call(File.open(jpeg_path, 'rb'))
55
+
47
56
  expect(result.width_px).to eq(1920)
48
57
  expect(result.height_px).to eq(1280)
49
- expect(result.intrinsics).to eq(exif_pixel_x_dimension: 8214, exif_pixel_y_dimension: 5476)
58
+
59
+ exif = result.intrinsics.fetch(:exif)
60
+ expect(exif.pixel_x_dimension).to eq(8214)
61
+ expect(exif.pixel_y_dimension).to eq(5476)
50
62
  end
51
63
 
52
64
  it 'reads an example with many APP1 markers at the beginning of which none are EXIF' do
@@ -44,6 +44,7 @@ describe FormatParser::TIFFParser do
44
44
  expect(parsed).not_to be_nil
45
45
  expect(parsed.width_px).to eq(320)
46
46
  expect(parsed.height_px).to eq(240)
47
+ expect(parsed.intrinsics[:exif]).not_to be_nil
47
48
  end
48
49
 
49
50
  describe 'correctly extracts dimensions from various TIFF flavors of the same file' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2018-04-23 00:00:00.000000000 Z
12
+ date: 2018-04-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ks
@@ -177,6 +177,7 @@ files:
177
177
  - lib/parsers/bmp_parser.rb
178
178
  - lib/parsers/cr2_parser.rb
179
179
  - lib/parsers/dpx_parser.rb
180
+ - lib/parsers/dpx_parser/dpx_structs.rb
180
181
  - lib/parsers/exif_parser.rb
181
182
  - lib/parsers/fdx_parser.rb
182
183
  - lib/parsers/flac_parser.rb