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 +4 -4
- data/.github/workflows/main.yml +2 -6
- data/CHANGELOG.md +10 -0
- data/Gemfile +4 -0
- data/format_parser.gemspec +1 -1
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/moov_parser/decoder.rb +19 -0
- data/lib/parsers/moov_parser.rb +36 -4
- data/lib/remote_io.rb +19 -5
- data/lib/video.rb +2 -0
- data/spec/integration/active_storage/rails_app.rb +6 -3
- data/spec/parsers/moov_parser_spec.rb +2 -0
- data/spec/remote_fetching_spec.rb +23 -2
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e49394510e191185e3be1f3560ca182a103b9446225a1163294f2db6a912ae76
|
4
|
+
data.tar.gz: 4f52d761857020a0957711528ad4d10ee885c74745897fa14a5e69292abf0cc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 829bd488d021cb0db5ce2b718aa661bdf3d4f86a55e55808c8279367e955d35b2bdccdd2bdf805ff736cef6d49f8c0f425c1ec65c0d7f07ada2db684581d4c1f
|
7
|
+
data.tar.gz: ed76f04c157a865de03df0d614d0d353b53e1e9c9b0a164ac6b2a6612a358643ae210b00fd92b91fec6cefd8c8c71e6ff559e63df3343b13ce161175425dbd3a
|
data/.github/workflows/main.yml
CHANGED
@@ -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
data/format_parser.gemspec
CHANGED
@@ -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'
|
@@ -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
|
data/lib/parsers/moov_parser.rb
CHANGED
@@ -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
|
52
|
-
timescale =
|
53
|
-
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
|
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, "
|
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
|
-
#
|
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
@@ -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:
|
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
|
68
|
-
_(user.profile_picture.metadata[:height_px]).must_equal
|
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 '
|
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 '
|
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:
|
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:
|
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.
|
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: []
|