format_parser 0.29.0 → 1.1.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: 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: []