format_parser 0.29.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8efef8742cfefdcde0aa05ab32c726004325c2210b19e25ca2cf866ad786074
4
- data.tar.gz: bbd59f6f046a35cff93ea4e20a5168c2912007b06b3fabe7a5b23f7a0ed70c34
3
+ metadata.gz: e49394510e191185e3be1f3560ca182a103b9446225a1163294f2db6a912ae76
4
+ data.tar.gz: 4f52d761857020a0957711528ad4d10ee885c74745897fa14a5e69292abf0cc2
5
5
  SHA512:
6
- metadata.gz: b4a7d965be6ff0cb0abf8cdec5f4d31259aada155ae465c1b31d5eba718f9d0071ee8b7a6b2061e6024add45d32b901c0f95a37373727a59fd4a6562041f52e9
7
- data.tar.gz: d314cb6876185ddedcef01c67205cf0635c2d8aebd0871dfd7834f0f54c15c38c31db7cc730c6aea8f8a72aff85949fa6937d970dfb320120a5b8c843f003753
6
+ metadata.gz: 829bd488d021cb0db5ce2b718aa661bdf3d4f86a55e55808c8279367e955d35b2bdccdd2bdf805ff736cef6d49f8c0f425c1ec65c0d7f07ada2db684581d4c1f
7
+ data.tar.gz: ed76f04c157a865de03df0d614d0d353b53e1e9c9b0a164ac6b2a6612a358643ae210b00fd92b91fec6cefd8c8c71e6ff559e63df3343b13ce161175425dbd3a
@@ -16,9 +16,6 @@ jobs:
16
16
  - 2.7
17
17
  - 2.6
18
18
  - 2.5
19
- - 2.4
20
- - 2.3
21
- - 2.2
22
19
  - jruby
23
20
  steps:
24
21
  - name: Checkout
@@ -65,12 +62,11 @@ jobs:
65
62
  - 2.7
66
63
  - 2.6
67
64
  - 2.5
68
- - 2.4
69
- - 2.3
70
- - 2.2
71
65
  - jruby
72
66
  experimental: [false]
73
67
  include:
68
+ - ruby: 3.1
69
+ experimental: true
74
70
  - ruby: 3.0
75
71
  experimental: true
76
72
  steps:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.1.0
2
+ * Add support for `frame_rate` in moov_parser
3
+
4
+ ## 1.0.0
5
+ * Dropping support for Ruby 2.2.X, 2.3.X and 2.4.X
6
+ * MP3: Fix negative length reads in edge cases by bumping `id3tag` version to `v0.14.2`
7
+
8
+ ## 0.29.1
9
+ * Fix handling of 200 responses with `parse_http` as well as handling of very small responses which do not need range access
10
+
1
11
  ## 0.29.0
2
12
  * Add option `headers:` to `FormatParser.parse_http`
3
13
 
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'ruby-debug-ide'
4
+ gem 'debase'
5
+ gem 'solargraph', group: :development
6
+
3
7
  # Gem dependencies specified in the gemspec
4
8
  gemspec
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_dependency 'ks', '~> 0.0'
34
34
  spec.add_dependency 'exifr', '~> 1', '>= 1.3.8'
35
- spec.add_dependency 'id3tag', '~> 0.14'
35
+ spec.add_dependency 'id3tag', '~> 0.14', '>= 0.14.2'
36
36
  spec.add_dependency 'faraday', '~> 0.13'
37
37
  spec.add_dependency 'faraday_middleware', '~> 0.14'
38
38
  spec.add_dependency 'measurometer', '~> 1'
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '0.29.0'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -72,6 +72,7 @@ class FormatParser::MOOVParser::Decoder
72
72
 
73
73
  trak_atoms.find do |trak_atom|
74
74
  hdlr_atom = find_first_atom_by_path([trak_atom], 'trak', 'mdia', 'hdlr')
75
+ next if hdlr_atom.nil?
75
76
  hdlr_atom.atom_fields[:component_type] == 'mhlr' && hdlr_atom.atom_fields[:component_subtype] == 'vide'
76
77
  end
77
78
  end
@@ -111,6 +112,24 @@ class FormatParser::MOOVParser::Decoder
111
112
  }
112
113
  end
113
114
 
115
+ def parse_stts_atom(io, _)
116
+ version = read_byte_value(io)
117
+ is_v1 = version == 1
118
+ stts = {
119
+ version: version,
120
+ flags: read_bytes(io, 3),
121
+ number_of_entries: is_v1 ? read_64bit_uint(io) : read_32bit_uint(io),
122
+ entries: []
123
+ }
124
+ stts[:number_of_entries].times {
125
+ stts[:entries] << {
126
+ sample_count: read_32bit_uint(io),
127
+ sample_duration: read_32bit_uint(io)
128
+ }
129
+ }
130
+ stts
131
+ end
132
+
114
133
  def parse_mdhd_atom(io, _)
115
134
  version = read_byte_value(io)
116
135
  is_v1 = version == 1
@@ -48,9 +48,9 @@ class FormatParser::MOOVParser
48
48
  width, height = parse_dimensions(decoder, atom_tree)
49
49
 
50
50
  # Try to find the "topmost" duration (respecting edits)
51
- if mdhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'mvhd')
52
- timescale = mdhd.field_value(:tscale)
53
- duration = mdhd.field_value(:duration)
51
+ if mvhd = decoder.find_first_atom_by_path(atom_tree, 'moov', 'mvhd')
52
+ timescale = mvhd.field_value(:tscale)
53
+ duration = mvhd.field_value(:duration)
54
54
  media_duration_s = duration / timescale.to_f
55
55
  end
56
56
 
@@ -64,13 +64,15 @@ class FormatParser::MOOVParser
64
64
  intrinsics: atom_tree,
65
65
  )
66
66
  else
67
+ frame_rate = parse_sample_atom(decoder, atom_tree)&.truncate(2)
67
68
  FormatParser::Video.new(
68
69
  format: format_from_moov_type(file_type),
69
70
  width_px: width,
70
71
  height_px: height,
72
+ frame_rate: frame_rate,
71
73
  media_duration_seconds: media_duration_s,
72
74
  content_type: MP4_MIXED_MIME_TYPE,
73
- intrinsics: atom_tree,
75
+ intrinsics: atom_tree
74
76
  )
75
77
  end
76
78
  end
@@ -115,5 +117,35 @@ class FormatParser::MOOVParser
115
117
  maybe_atom_size >= minimum_ftyp_atom_size && maybe_ftyp_atom_signature == 'ftyp'
116
118
  end
117
119
 
120
+ # Sample information is found in the 'time-to-sample' stts atom.
121
+ # The media atom mdhd is needed too in order to get the movie timescale
122
+ def parse_sample_atom(decoder, atom_tree)
123
+ video_trak_atom = decoder.find_video_trak_atom(atom_tree)
124
+
125
+ stts = if video_trak_atom
126
+ decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'mdia', 'minf', 'stbl', 'stts')
127
+ else
128
+ decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'mdia', 'minf', 'stbl', 'stts')
129
+ end
130
+
131
+ mdhd = if video_trak_atom
132
+ decoder.find_first_atom_by_path([video_trak_atom], 'trak', 'mdia', 'mdhd')
133
+ else
134
+ decoder.find_first_atom_by_path(atom_tree, 'moov', 'trak', 'mdia', 'mdhd')
135
+ end
136
+
137
+ if stts && mdhd
138
+ timescale = mdhd.atom_fields[:tscale]
139
+ sample_duration = stts.field_value(:entries).first[:sample_duration]
140
+ if timescale.nil? || timescale == 0 || sample_duration.nil? || sample_duration == 0
141
+ nil
142
+ else
143
+ timescale.to_f / sample_duration
144
+ end
145
+ else
146
+ nil
147
+ end
148
+ end
149
+
118
150
  FormatParser.register_parser new, natures: :video, formats: FTYP_MAP.values, priority: 1
119
151
  end
data/lib/remote_io.rb CHANGED
@@ -89,18 +89,32 @@ class FormatParser::RemoteIO
89
89
  response = conn.get(@uri, nil, range: 'bytes=%d-%d' % [range.begin, range.end])
90
90
 
91
91
  case response.status
92
- when 200, 206
92
+ when 200
93
+ # S3 returns 200 when you request a Range that is fully satisfied by the entire object,
94
+ # we take that into account here. Also, for very tiny responses (and also for empty responses)
95
+ # the responses are going to be 200 which does not mean we cannot proceed
96
+ # To have a good check for both of these conditions we need to know whether the ranges overlap fully
97
+ response_size = response.body.bytesize
98
+ requested_range_size = range.end - range.begin + 1
99
+ if response_size > requested_range_size
100
+ error_message = [
101
+ "We requested #{requested_range_size} bytes, but the server sent us more",
102
+ "(#{response_size} bytes) - it likely has no `Range:` support.",
103
+ "The error occurred when talking to #{@uri})"
104
+ ]
105
+ raise InvalidRequest.new(response.status, error_message.join("\n"))
106
+ end
107
+ [response_size, response.body]
108
+ when 206
93
109
  # Figure out of the server supports content ranges, if it doesn't we have no
94
110
  # business working with that server
95
111
  range_header = response.headers['Content-Range']
96
- raise InvalidRequest.new(response.status, "No range support at #{@uri}") unless range_header
112
+ raise InvalidRequest.new(response.status, "The server replied with 206 status but no Content-Range at #{@uri}") unless range_header
97
113
 
98
114
  # "Content-Range: bytes 0-0/307404381" is how the response header is structured
99
115
  size = range_header[/\/(\d+)$/, 1].to_i
100
116
 
101
- # S3 returns 200 when you request a Range that is fully satisfied by the entire object,
102
- # we take that into account here. For other servers, 206 is the expected response code.
103
- # Also, if we request a _larger_ range than what can be satisfied by the server,
117
+ # If we request a _larger_ range than what can be satisfied by the server,
104
118
  # the response is going to only contain what _can_ be sent and the status is also going
105
119
  # to be 206
106
120
  return [size, response.body]
data/lib/video.rb CHANGED
@@ -8,6 +8,8 @@ module FormatParser
8
8
 
9
9
  attr_accessor :height_px
10
10
 
11
+ attr_accessor :frame_rate
12
+
11
13
  # Type of the file (e.g :mp3)
12
14
  attr_accessor :format
13
15
 
@@ -53,19 +53,22 @@ end
53
53
  require 'minitest/autorun'
54
54
  require 'open-uri'
55
55
 
56
+ fixtures_dir = File.join(File.dirname(__FILE__), '../../fixtures')
57
+
56
58
  describe User do
57
59
  describe "profile_picture's metadatas" do
58
60
  it 'parse metadatas with format_parser' do
61
+ fixture_path = fixtures_dir + '/PNG/cat.png'
59
62
  user = User.create
60
63
  user.profile_picture.attach(
61
64
  filename: 'cat.png',
62
- io: URI.open('https://freesvg.org/img/1416155153.png')
65
+ io: File.open(fixture_path, 'rb')
63
66
  )
64
67
 
65
68
  user.profile_picture.analyze
66
69
 
67
- _(user.profile_picture.metadata[:width_px]).must_equal 500
68
- _(user.profile_picture.metadata[:height_px]).must_equal 296
70
+ _(user.profile_picture.metadata[:width_px]).must_equal 600
71
+ _(user.profile_picture.metadata[:height_px]).must_equal 600
69
72
  _(user.profile_picture.metadata[:color_mode]).must_equal 'rgba'
70
73
  end
71
74
  end
@@ -106,6 +106,7 @@ describe FormatParser::MOOVParser do
106
106
  expect(result.format).to eq(:mov)
107
107
  expect(result.width_px).to eq(160)
108
108
  expect(result.height_px).to eq(90)
109
+ expect(result.frame_rate).to eq(14.98)
109
110
  end
110
111
 
111
112
  it 'provides filename hints' do
@@ -122,6 +123,7 @@ describe FormatParser::MOOVParser do
122
123
  expect(result.format).to eq(:mov)
123
124
  expect(result.width_px).to eq(640)
124
125
  expect(result.height_px).to eq(360)
126
+ expect(result.frame_rate).to eq(30)
125
127
  end
126
128
 
127
129
  it 'does not raise error when a meta atom has size 0' do
@@ -19,18 +19,27 @@ describe 'Fetching data from HTTP remotes' do
19
19
  res.status = 302
20
20
  res.header['Location'] = req.path.sub('/redirect', '')
21
21
  end
22
+ @server.mount_proc '/empty' do |_req, res|
23
+ res.status = 200
24
+ res.body = ''
25
+ end
26
+ @server.mount_proc '/tiny' do |_req, res|
27
+ res.status = 200
28
+ res.body = File.read(fixtures_dir + '/test.gif')
29
+ end
30
+
22
31
  trap('INT') { @server.stop }
23
32
  @server_thread = Thread.new { @server.start }
24
33
  end
25
34
 
26
- it '#parse_http is called without any option' do
35
+ it 'works with .parse_http called without any options' do
27
36
  result = FormatParser.parse_http('http://localhost:9399/PNG/anim.png')
28
37
 
29
38
  expect(result.format).to eq(:png)
30
39
  expect(result.height_px).to eq(180)
31
40
  end
32
41
 
33
- it '#parse_http is called with hash options' do
42
+ it 'works with .parse_http called with additional options' do
34
43
  fake_result = double(nature: :audio, format: :aiff)
35
44
  expect_any_instance_of(FormatParser::AIFFParser).to receive(:call).and_return(fake_result)
36
45
  results = FormatParser.parse_http('http://localhost:9399/PNG/anim.png', results: :all)
@@ -39,6 +48,18 @@ describe 'Fetching data from HTTP remotes' do
39
48
  expect(results).to include(fake_result)
40
49
  end
41
50
 
51
+ it 'is able to cope with a 0-size resource which does not provide Content-Range' do
52
+ file_information = FormatParser.parse_http('http://localhost:9399/empty')
53
+
54
+ expect(file_information).to be_nil
55
+ end
56
+
57
+ it 'is able to cope with a tiny resource which fits into the first requested range completely' do
58
+ file_information = FormatParser.parse_http('http://localhost:9399/tiny')
59
+ expect(file_information).not_to be_nil
60
+ expect(file_information.nature).to eq(:image)
61
+ end
62
+
42
63
  it 'parses the animated PNG over HTTP' do
43
64
  file_information = FormatParser.parse_http('http://localhost:9399/PNG/anim.png')
44
65
  expect(file_information).not_to be_nil
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
8
8
  - Julik Tarkhanov
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-02-18 00:00:00.000000000 Z
12
+ date: 2022-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ks
@@ -52,6 +52,9 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.14'
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 0.14.2
55
58
  type: :runtime
56
59
  prerelease: false
57
60
  version_requirements: !ruby/object:Gem::Requirement
@@ -59,6 +62,9 @@ dependencies:
59
62
  - - "~>"
60
63
  - !ruby/object:Gem::Version
61
64
  version: '0.14'
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.14.2
62
68
  - !ruby/object:Gem::Dependency
63
69
  name: faraday
64
70
  requirement: !ruby/object:Gem::Requirement
@@ -294,7 +300,7 @@ licenses:
294
300
  - MIT (Hippocratic)
295
301
  metadata:
296
302
  allowed_push_host: https://rubygems.org
297
- post_install_message:
303
+ post_install_message:
298
304
  rdoc_options: []
299
305
  require_paths:
300
306
  - lib
@@ -309,8 +315,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
309
315
  - !ruby/object:Gem::Version
310
316
  version: '0'
311
317
  requirements: []
312
- rubygems_version: 3.0.3
313
- signing_key:
318
+ rubygems_version: 3.3.4
319
+ signing_key:
314
320
  specification_version: 4
315
321
  summary: A library for efficient parsing of file metadata
316
322
  test_files: []