file_data 5.0.0 → 5.2.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 +5 -5
- data/.gitignore +5 -1
- data/.rubocop.yml +2 -2
- data/Gemfile.lock +44 -1
- data/README.md +30 -2
- data/file_data.gemspec +1 -0
- data/lib/file_data/core_extensions/binary_extensions.rb +13 -0
- data/lib/file_data/file_types/file_info.rb +33 -0
- data/lib/file_data/file_types/jpeg.rb +28 -32
- data/lib/file_data/formats/exif/exif.rb +13 -1
- data/lib/file_data/formats/exif/exif_jpeg.rb +10 -4
- data/lib/file_data/formats/exif/exif_stream.rb +8 -12
- data/lib/file_data/formats/exif/exif_tag_reader.rb +2 -2
- data/lib/file_data/formats/mpeg4/box.rb +38 -0
- data/lib/file_data/formats/mpeg4/box_factory.rb +10 -0
- data/lib/file_data/formats/mpeg4/box_parsers/ilst_box.rb +25 -0
- data/lib/file_data/formats/mpeg4/box_parsers/ilst_data_box.rb +17 -0
- data/lib/file_data/formats/mpeg4/box_parsers/keys_box.rb +24 -0
- data/lib/file_data/formats/mpeg4/box_parsers/meta_box.rb +42 -0
- data/lib/file_data/formats/mpeg4/box_parsers/mvhd_box.rb +19 -0
- data/lib/file_data/formats/mpeg4/box_path.rb +26 -0
- data/lib/file_data/formats/mpeg4/boxes_reader.rb +19 -0
- data/lib/file_data/formats/mpeg4/mpeg4.rb +30 -0
- data/lib/file_data/helpers/sized_field.rb +11 -0
- data/lib/file_data/helpers/stream_view.rb +49 -0
- data/lib/file_data/version.rb +1 -1
- data/lib/file_data.rb +3 -0
- metadata +31 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 57a1747c7962bc51b2333ae0037e9736437190f0c98dce5d77810bc81df22152
|
|
4
|
+
data.tar.gz: 88f5ddc362b309d14c3c5e8f3c463191dc448fb48901b9afae5b942bf5595745
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89d98f94f872872ad3a59b84fdc42612d82db860ccac2769f8ebcce5b2b5715b44847dad125a3f6ca6a6811715866aebc2c0558550c4ff06f478119726937190
|
|
7
|
+
data.tar.gz: 54d2e8defca7b5dd1447c7bb0de238d4a70f662a99afa9f9b9d8647b0c8152a73b58cb536e45064cc96091be571e3bcafa5910a88e9dd1b5baa800a114b679f6
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -8,7 +8,7 @@ Metrics/ClassLength:
|
|
|
8
8
|
Max: 100
|
|
9
9
|
EndOfLine:
|
|
10
10
|
Enabled: false
|
|
11
|
-
|
|
11
|
+
Metrics/BlockLength:
|
|
12
12
|
Exclude:
|
|
13
13
|
- 'spec/**/*_spec.rb'
|
|
14
14
|
Style/BlockComments:
|
|
@@ -16,4 +16,4 @@ Style/BlockComments:
|
|
|
16
16
|
- 'spec/spec_helper.rb'
|
|
17
17
|
AllCops:
|
|
18
18
|
Exclude:
|
|
19
|
-
- 'file_data.gemspec'
|
|
19
|
+
- 'file_data.gemspec'
|
data/Gemfile.lock
CHANGED
|
@@ -6,17 +6,47 @@ PATH
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
8
8
|
specs:
|
|
9
|
+
ast (2.4.0)
|
|
10
|
+
backports (3.11.3)
|
|
11
|
+
binding_of_caller (0.8.0)
|
|
12
|
+
debug_inspector (>= 0.0.1)
|
|
13
|
+
coderay (1.1.2)
|
|
9
14
|
coveralls (0.8.19)
|
|
10
15
|
json (>= 1.8, < 3)
|
|
11
16
|
simplecov (~> 0.12.0)
|
|
12
17
|
term-ansicolor (~> 1.3)
|
|
13
18
|
thor (~> 0.19.1)
|
|
14
19
|
tins (~> 1.6)
|
|
20
|
+
debug_inspector (0.0.3)
|
|
21
|
+
deep-cover (0.6.2)
|
|
22
|
+
backports (>= 3.11.0)
|
|
23
|
+
binding_of_caller
|
|
24
|
+
bundler
|
|
25
|
+
highline
|
|
26
|
+
parser (~> 2.5.0)
|
|
27
|
+
pry
|
|
28
|
+
sass
|
|
29
|
+
slop (~> 4.0)
|
|
30
|
+
term-ansicolor
|
|
31
|
+
terminal-table
|
|
32
|
+
with_progress
|
|
15
33
|
diff-lcs (1.3)
|
|
16
34
|
docile (1.1.5)
|
|
17
35
|
fakefs (0.10.2)
|
|
36
|
+
ffi (1.9.25)
|
|
37
|
+
ffi (1.9.25-x64-mingw32)
|
|
38
|
+
highline (2.0.0)
|
|
18
39
|
json (2.0.3)
|
|
40
|
+
method_source (0.9.0)
|
|
41
|
+
parser (2.5.1.0)
|
|
42
|
+
ast (~> 2.4.0)
|
|
43
|
+
pry (0.11.3)
|
|
44
|
+
coderay (~> 1.1.0)
|
|
45
|
+
method_source (~> 0.9.0)
|
|
19
46
|
rake (10.5.0)
|
|
47
|
+
rb-fsevent (0.10.3)
|
|
48
|
+
rb-inotify (0.9.10)
|
|
49
|
+
ffi (>= 0.5.0, < 2)
|
|
20
50
|
rspec (3.5.0)
|
|
21
51
|
rspec-core (~> 3.5.0)
|
|
22
52
|
rspec-expectations (~> 3.5.0)
|
|
@@ -30,15 +60,27 @@ GEM
|
|
|
30
60
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
31
61
|
rspec-support (~> 3.5.0)
|
|
32
62
|
rspec-support (3.5.0)
|
|
63
|
+
ruby-progressbar (1.9.0)
|
|
64
|
+
sass (3.5.6)
|
|
65
|
+
sass-listen (~> 4.0.0)
|
|
66
|
+
sass-listen (4.0.0)
|
|
67
|
+
rb-fsevent (~> 0.9, >= 0.9.4)
|
|
68
|
+
rb-inotify (~> 0.9, >= 0.9.7)
|
|
33
69
|
simplecov (0.12.0)
|
|
34
70
|
docile (~> 1.1.0)
|
|
35
71
|
json (>= 1.8, < 3)
|
|
36
72
|
simplecov-html (~> 0.10.0)
|
|
37
73
|
simplecov-html (0.10.0)
|
|
74
|
+
slop (4.6.2)
|
|
38
75
|
term-ansicolor (1.4.0)
|
|
39
76
|
tins (~> 1.0)
|
|
77
|
+
terminal-table (1.8.0)
|
|
78
|
+
unicode-display_width (~> 1.1, >= 1.1.1)
|
|
40
79
|
thor (0.19.4)
|
|
41
80
|
tins (1.13.2)
|
|
81
|
+
unicode-display_width (1.4.0)
|
|
82
|
+
with_progress (1.0.1)
|
|
83
|
+
ruby-progressbar (~> 1.4)
|
|
42
84
|
|
|
43
85
|
PLATFORMS
|
|
44
86
|
ruby
|
|
@@ -47,10 +89,11 @@ PLATFORMS
|
|
|
47
89
|
DEPENDENCIES
|
|
48
90
|
bundler (~> 1.14)
|
|
49
91
|
coveralls (~> 0.8)
|
|
92
|
+
deep-cover (~> 0.6)
|
|
50
93
|
fakefs (~> 0.10)
|
|
51
94
|
file_data!
|
|
52
95
|
rake (~> 10.0)
|
|
53
96
|
rspec (~> 3.0)
|
|
54
97
|
|
|
55
98
|
BUNDLED WITH
|
|
56
|
-
1.
|
|
99
|
+
1.16.1
|
data/README.md
CHANGED
|
@@ -4,12 +4,27 @@ file_data
|
|
|
4
4
|
[](https://travis-ci.org/ScottHaney/file_data)
|
|
5
5
|
[](https://coveralls.io/github/ScottHaney/file_data?branch=master)
|
|
6
6
|
[](https://codeclimate.com/github/ScottHaney/file_data)
|
|
7
|
+
[](https://badge.fury.io/rb/file_data)
|
|
7
8
|
|
|
8
9
|
Ruby library that reads file metadata.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
The api provides a basic usage and an advanced usage. The basic usage will reopen and reparse the file every time it is called which is no problem when reading a single value but can be a performance drain for multiple values. The advanced usage allows the user to grab more than one value without having to read the file more than once.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
## Basic Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
filepath = '...' # Path to a jpeg or mpeg4 file
|
|
17
|
+
|
|
18
|
+
# Get the date when the file content originated. When a photo was taken, when a movie was recorded, etc
|
|
19
|
+
FileData::FileInfo.origin_date(filepath)
|
|
20
|
+
|
|
21
|
+
# Get the date when the file was considered to be created. This is usually tied in some way to when the file itself was created on a disk somewhere (not usually as useful as origin date)
|
|
22
|
+
FileData::FileInfo.creation_date(filepath)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Advanced Usage
|
|
26
|
+
|
|
27
|
+
Varies by file format type. Currently there are low level classes for parsing exif and mpeg4 metadata
|
|
13
28
|
|
|
14
29
|
## Exif documentation
|
|
15
30
|
|
|
@@ -221,4 +236,17 @@ FileData::ExifTags.tag_groups[40_965] =
|
|
|
221
236
|
}
|
|
222
237
|
```
|
|
223
238
|
|
|
239
|
+
## Mpeg4 documentation
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
|
|
243
|
+
filepath = '...' # path to an mpeg4 file
|
|
244
|
+
File.open(filepath, 'rb') do |stream|
|
|
245
|
+
parser = FileData::MvhdBoxParser # class that parses the box you want
|
|
246
|
+
method = :creation_time # attribute to get from the parse result
|
|
247
|
+
box_path = ['moov', 'mvhd'] # path to get to the box that you want
|
|
224
248
|
|
|
249
|
+
# final result that you are looking for
|
|
250
|
+
result = FileData::Mpeg4.get_value(stream, parser, method, *box_path)
|
|
251
|
+
end
|
|
252
|
+
```
|
data/file_data.gemspec
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Methods for reading values from a binary stream
|
|
2
|
+
module BinaryExtensions
|
|
3
|
+
def read_value(num_bytes)
|
|
4
|
+
bytes = each_byte.take(num_bytes)
|
|
5
|
+
bytes.reverse! if @is_little_endian
|
|
6
|
+
|
|
7
|
+
bytes.inject { |total, val| (total << 8) + val }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def read_ascii(num_bytes)
|
|
11
|
+
each_byte.take(num_bytes).map(&:chr).join
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module FileData
|
|
2
|
+
# Operations common to all files
|
|
3
|
+
class FileInfo
|
|
4
|
+
class << self
|
|
5
|
+
attr_reader :info_maps
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
@info_maps ||= {}
|
|
9
|
+
|
|
10
|
+
%w[creation_date origin_date].each do |method_name|
|
|
11
|
+
define_singleton_method(method_name) do |filename|
|
|
12
|
+
File.open(filename, 'rb') do |stream|
|
|
13
|
+
reader = reader_class(filename)
|
|
14
|
+
raise "No metadata parser class found for the file #{filename}" if reader.nil?
|
|
15
|
+
|
|
16
|
+
reader_class(filename).send(method_name, stream)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.reader_class(filename)
|
|
22
|
+
info_maps[get_reader_key(filename)]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.can_handle?(filename)
|
|
26
|
+
info_maps.key?(get_reader_key(filename))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.get_reader_key(filename)
|
|
30
|
+
File.extname(filename).downcase
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,52 +1,48 @@
|
|
|
1
|
+
require_relative '../helpers/sized_field'
|
|
2
|
+
require_relative '../helpers/stream_view'
|
|
3
|
+
|
|
1
4
|
module FileData
|
|
2
5
|
# Represents a Jpeg image stream
|
|
3
6
|
class Jpeg
|
|
4
7
|
SOI_BYTES = [255, 216].freeze
|
|
8
|
+
EOI_BYTES = [255, 217].freeze
|
|
5
9
|
SECTION_HEADER_SIZE = 4
|
|
6
10
|
INVALID_HEADER_MSG = 'the given file is not a jpeg file since it does not'\
|
|
7
11
|
'begin with the start of image (SOI) bytes.'.freeze
|
|
8
12
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def each_section
|
|
14
|
-
read_header
|
|
15
|
-
Enumerator.new { |e| yield_sections(e) }.lazy
|
|
13
|
+
def self.each_section(stream)
|
|
14
|
+
view = Helpers::StreamView.new(stream)
|
|
15
|
+
read_header(view)
|
|
16
|
+
Enumerator.new { |e| yield_sections(view, e) }.lazy
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def read_header
|
|
19
|
-
soi =
|
|
19
|
+
def self.read_header(stream)
|
|
20
|
+
soi = stream.each_byte.take(SOI_BYTES.size)
|
|
20
21
|
raise INVALID_HEADER_MSG unless soi == SOI_BYTES
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
def yield_sections(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
break
|
|
27
|
-
@stream.seek(next_section_pos)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def yield_section(e)
|
|
32
|
-
section_start_pos = @stream.pos + 2
|
|
33
|
-
marker, size = read_section_header
|
|
34
|
-
e.yield marker, size
|
|
35
|
-
section_start_pos + size
|
|
36
|
-
end
|
|
24
|
+
def self.yield_sections(stream, enumerator)
|
|
25
|
+
until stream.eof?
|
|
26
|
+
marker = stream.each_byte.take(2)
|
|
27
|
+
break if marker == EOI_BYTES
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
section = current_section(stream, marker)
|
|
30
|
+
enumerator.yield section
|
|
31
|
+
stream.seek(section.content_stream.end_pos + 1)
|
|
32
|
+
end
|
|
42
33
|
end
|
|
43
34
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
# def self.section_pos?(stream)
|
|
36
|
+
# # Make sure that there are enough bytes for a section header.
|
|
37
|
+
# # This also handles an ending two byte JPEG EOI sequence.
|
|
38
|
+
# stream.size >= SECTION_HEADER_SIZE
|
|
39
|
+
# end
|
|
47
40
|
|
|
48
|
-
def
|
|
49
|
-
|
|
41
|
+
def self.current_section(stream, marker)
|
|
42
|
+
content_stream = Helpers::SizedField.create_view(stream, 2)
|
|
43
|
+
JpegSection.new(marker, content_stream)
|
|
50
44
|
end
|
|
51
45
|
end
|
|
46
|
+
|
|
47
|
+
JpegSection = Struct.new(:marker, :content_stream)
|
|
52
48
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
require_relative 'exif_reader'
|
|
2
2
|
require_relative 'exif_jpeg'
|
|
3
|
+
require 'time'
|
|
3
4
|
|
|
4
5
|
module FileData
|
|
5
6
|
# Convenience class for extracting exif data from a file or stream
|
|
6
7
|
class Exif
|
|
8
|
+
['.jpeg', '.jpg'].each { |e| FileInfo.info_maps[e] = Exif }
|
|
9
|
+
|
|
7
10
|
# Create methods that forward to ExifReader
|
|
8
11
|
# Each method requires the stream as a parameter to help the user
|
|
9
12
|
# fall into a "pit of success" by only opening and closing
|
|
@@ -23,10 +26,19 @@ module FileData
|
|
|
23
26
|
|
|
24
27
|
def self.streamify(input)
|
|
25
28
|
if input.is_a?(String)
|
|
26
|
-
File.open(input, 'rb') { |f| yield f }
|
|
29
|
+
::File.open(input, 'rb') { |f| yield f }
|
|
27
30
|
else
|
|
28
31
|
yield input
|
|
29
32
|
end
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
def self.creation_date(input)
|
|
36
|
+
raw_tag = FileData::Exif.only_image_tag(input, [34_665, 36_867])
|
|
37
|
+
Time.strptime(raw_tag, '%Y:%m:%d %H:%M:%S') unless raw_tag.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.origin_date(input)
|
|
41
|
+
creation_date(input)
|
|
42
|
+
end
|
|
31
43
|
end
|
|
32
44
|
end
|
|
@@ -15,14 +15,20 @@ module FileData
|
|
|
15
15
|
ExifStream.new(@stream) if seek_exif
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
private
|
|
19
|
+
|
|
18
20
|
def seek_exif
|
|
19
|
-
Jpeg.
|
|
20
|
-
.select { |
|
|
21
|
+
Jpeg.each_section(@stream)
|
|
22
|
+
.select { |section| exif_section?(section) }
|
|
21
23
|
.first
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def exif_section?(
|
|
25
|
-
marker == APP1_BYTES &&
|
|
26
|
+
def exif_section?(section)
|
|
27
|
+
section.marker == APP1_BYTES && read_exif_id(section)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def read_exif_id(section)
|
|
31
|
+
section.content_stream.each_byte.take(EXIF_ID.size) == EXIF_ID
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require 'forwardable'
|
|
2
|
+
require_relative '../../core_extensions/binary_extensions'
|
|
2
3
|
|
|
3
4
|
module FileData
|
|
4
5
|
# Wraps a stream with exif specific logic
|
|
5
6
|
class ExifStream
|
|
7
|
+
include BinaryExtensions
|
|
8
|
+
|
|
6
9
|
MOTOROLLA_BYTES = 'MM'.bytes.to_a.freeze
|
|
7
10
|
INTEL_BYTES = 'II'.bytes.to_a.freeze
|
|
8
11
|
|
|
@@ -20,7 +23,7 @@ module FileData
|
|
|
20
23
|
VALUE_OFFSET_SIZE = 4
|
|
21
24
|
|
|
22
25
|
extend Forwardable
|
|
23
|
-
def_delegators :@stream, :seek, :pos
|
|
26
|
+
def_delegators :@stream, :seek, :pos, :each_byte
|
|
24
27
|
|
|
25
28
|
def initialize(stream)
|
|
26
29
|
@stream = stream
|
|
@@ -28,10 +31,10 @@ module FileData
|
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def read_header
|
|
31
|
-
@
|
|
34
|
+
@is_little_endian =
|
|
32
35
|
case @stream.each_byte.take(2)
|
|
33
|
-
when INTEL_BYTES then
|
|
34
|
-
when MOTOROLLA_BYTES then
|
|
36
|
+
when INTEL_BYTES then true
|
|
37
|
+
when MOTOROLLA_BYTES then false
|
|
35
38
|
else raise 'the byte order bytes did not match any expected value'
|
|
36
39
|
end
|
|
37
40
|
|
|
@@ -63,7 +66,7 @@ module FileData
|
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
def read_undefined(size)
|
|
66
|
-
[read_raw_val(size), @
|
|
69
|
+
[read_raw_val(size), @is_little_endian]
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def read_raw_val(size)
|
|
@@ -95,12 +98,5 @@ module FileData
|
|
|
95
98
|
def to_slong(raw_value)
|
|
96
99
|
-(raw_value & HIGH_BIT_MASK) + (raw_value & ~HIGH_BIT_MASK)
|
|
97
100
|
end
|
|
98
|
-
|
|
99
|
-
def read_value(num_bytes)
|
|
100
|
-
bytes = @stream.each_byte.take(num_bytes)
|
|
101
|
-
bytes.reverse! unless @is_big_endian
|
|
102
|
-
|
|
103
|
-
bytes.inject { |total, val| (total << 8) + val }
|
|
104
|
-
end
|
|
105
101
|
end
|
|
106
102
|
end
|
|
@@ -22,11 +22,11 @@ module FileData
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def process_ifd(ifd,
|
|
25
|
+
def process_ifd(ifd, enumerator)
|
|
26
26
|
# Yield the tags or just skip ahead
|
|
27
27
|
|
|
28
28
|
if ifds_to_include.include?(ifd.index)
|
|
29
|
-
ifd.tags.each { |t|
|
|
29
|
+
ifd.tags.each { |t| enumerator.yield t }
|
|
30
30
|
else
|
|
31
31
|
# Avoid skipping the last ifd as this is needless work
|
|
32
32
|
ifd.skip unless ifd.index == 1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative '../../helpers/stream_view'
|
|
2
|
+
|
|
3
|
+
module FileData
|
|
4
|
+
# Mpeg4 box
|
|
5
|
+
class Box
|
|
6
|
+
attr_reader :type, :content_stream, :end_pos
|
|
7
|
+
|
|
8
|
+
def initialize(type, content_stream)
|
|
9
|
+
@type = type
|
|
10
|
+
@content_stream = content_stream
|
|
11
|
+
@end_pos = @content_stream.end_pos
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.parse(view)
|
|
15
|
+
type, pos, size = parse_header(view)
|
|
16
|
+
new(type, Helpers::SubStreamView.new(view.stream, pos, size))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.parse_header(view)
|
|
20
|
+
start_pos = view.pos
|
|
21
|
+
first_field = view.read_value(4)
|
|
22
|
+
type = view.read_ascii(4)
|
|
23
|
+
|
|
24
|
+
total_size =
|
|
25
|
+
if first_field == 1
|
|
26
|
+
view.read_value(8)
|
|
27
|
+
else
|
|
28
|
+
first_field
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
content_pos = view.pos
|
|
32
|
+
header_size = content_pos - start_pos
|
|
33
|
+
content_size = total_size - header_size
|
|
34
|
+
|
|
35
|
+
[type, content_pos, content_size]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative '../boxes_reader'
|
|
2
|
+
require_relative '../../../helpers/stream_view'
|
|
3
|
+
require_relative 'ilst_data_box'
|
|
4
|
+
|
|
5
|
+
module FileData
|
|
6
|
+
# Parsers for the 'ilst' box
|
|
7
|
+
class IlstBoxParser
|
|
8
|
+
def self.parse(view)
|
|
9
|
+
size = view.read_value(4)
|
|
10
|
+
index = view.read_value(4)
|
|
11
|
+
|
|
12
|
+
db = find_data_box(view, size)
|
|
13
|
+
data_box = db.nil? ? nil : IlstDataBoxParser.parse(db)
|
|
14
|
+
|
|
15
|
+
IlstBox.new(index, data_box)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.find_data_box(parent_view, parent_size)
|
|
19
|
+
view = Helpers::SubStreamView.new(parent_view.stream, parent_view.stream.pos, parent_size - 8)
|
|
20
|
+
BoxesReader.read(view).find { |box| box.type == 'data' }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
IlstBox = Struct.new(:index, :data_box)
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module FileData
|
|
2
|
+
# Parser for the 'data' box
|
|
3
|
+
class IlstDataBoxParser
|
|
4
|
+
def self.parse(box)
|
|
5
|
+
view = box.content_stream
|
|
6
|
+
|
|
7
|
+
# TO DO - Currently a text value is always assumed...
|
|
8
|
+
data_type = view.read_value(4)
|
|
9
|
+
locale = view.read_value(4)
|
|
10
|
+
value = view.read_ascii(view.remaining_bytes)
|
|
11
|
+
|
|
12
|
+
DataBox.new(data_type, locale, value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
DataBox = Struct.new(:data_type, :locale, :value_text)
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative '../../../helpers/sized_field'
|
|
2
|
+
|
|
3
|
+
module FileData
|
|
4
|
+
# Parser for the 'keys' box
|
|
5
|
+
class KeysBoxParser
|
|
6
|
+
def self.parse(view)
|
|
7
|
+
view.read_value(1) # version field
|
|
8
|
+
view.read_value(3) # flags field
|
|
9
|
+
|
|
10
|
+
entry_count = view.read_value(4)
|
|
11
|
+
Array.new(entry_count) { |index| parse_key(view, index) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.parse_key(view, index)
|
|
15
|
+
key_view = Helpers::SizedField.create_view(view, 4)
|
|
16
|
+
namespace = key_view.read_ascii(4)
|
|
17
|
+
value = key_view.read_ascii(key_view.remaining_bytes)
|
|
18
|
+
|
|
19
|
+
Key.new(index + 1, namespace, value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Key = Struct.new(:index, :namespace, :value)
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require_relative 'keys_box'
|
|
2
|
+
require_relative 'ilst_box'
|
|
3
|
+
require_relative '../box_path'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module FileData
|
|
7
|
+
# Parser for the 'meta' box
|
|
8
|
+
class MetaBoxParser
|
|
9
|
+
def self.parse(view)
|
|
10
|
+
creation_key = get_creation_key(view)
|
|
11
|
+
return MetaBox.new(nil) if creation_key.nil?
|
|
12
|
+
|
|
13
|
+
creation_date_data = get_creation_date(view, creation_key.index)
|
|
14
|
+
return MetaBox.new(nil) if creation_date_data.nil?
|
|
15
|
+
|
|
16
|
+
MetaBox.new(Time.parse(creation_date_data.data_box.value_text))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.get_creation_key(view)
|
|
20
|
+
kb = BoxPath.get_path(view, 'keys')
|
|
21
|
+
return nil if kb.nil?
|
|
22
|
+
|
|
23
|
+
keys = KeysBoxParser.parse(kb.content_stream)
|
|
24
|
+
keys.find { |key| key.value == 'com.apple.quicktime.creationdate' }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.get_creation_date(view, index)
|
|
28
|
+
ilst_boxes = get_ilst_boxes(view)
|
|
29
|
+
ilst_boxes.find { |x| x.index == index }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.get_ilst_boxes(view)
|
|
33
|
+
view.seek view.start_pos
|
|
34
|
+
box = BoxPath.get_path(view, 'ilst')
|
|
35
|
+
ilst_boxes = []
|
|
36
|
+
ilst_boxes << IlstBoxParser.parse(box.content_stream) until box.content_stream.eof?
|
|
37
|
+
ilst_boxes
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
MetaBox = Struct.new(:creation_date)
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module FileData
|
|
2
|
+
# Parser for the 'mvhd' box
|
|
3
|
+
class MvhdBoxParser
|
|
4
|
+
def self.parse(view)
|
|
5
|
+
MvhdBox.new(parse_mvhd_creation_date(view))
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.parse_mvhd_creation_date(view)
|
|
9
|
+
version = view.read_value(1)
|
|
10
|
+
view.read_value(3) # Flags bytes
|
|
11
|
+
|
|
12
|
+
creation_time = view.read_value(version == 1 ? 8 : 4)
|
|
13
|
+
epoch_delta = 2_082_844_800
|
|
14
|
+
Time.at(creation_time - epoch_delta)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
MvhdBox = Struct.new(:creation_time)
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative 'boxes_reader'
|
|
2
|
+
|
|
3
|
+
module FileData
|
|
4
|
+
# Finds Mpeg4 boxes within a stream
|
|
5
|
+
class BoxPath
|
|
6
|
+
def self.get_root_path(stream, *box_path)
|
|
7
|
+
get_path(Helpers::StreamView.new(stream), *box_path)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# def self.get_box_path(box, *box_path)
|
|
11
|
+
# get_path(box.content_stream, *box_path)
|
|
12
|
+
# end
|
|
13
|
+
|
|
14
|
+
def self.get_path(stream_view, *box_path)
|
|
15
|
+
match = BoxesReader.read(stream_view).find { |x| x.type == box_path[0] }
|
|
16
|
+
|
|
17
|
+
if match.nil?
|
|
18
|
+
nil
|
|
19
|
+
elsif box_path.length == 1
|
|
20
|
+
match
|
|
21
|
+
else
|
|
22
|
+
get_path(match.content_stream, *box_path[1..-1])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require_relative 'box'
|
|
2
|
+
require_relative '../../helpers/stream_view'
|
|
3
|
+
|
|
4
|
+
module FileData
|
|
5
|
+
# Returns all boxes starting from the current position of a stream
|
|
6
|
+
class BoxesReader
|
|
7
|
+
def self.read(view)
|
|
8
|
+
Enumerator.new do |e|
|
|
9
|
+
view.seek view.start_pos
|
|
10
|
+
until view.eof?
|
|
11
|
+
box = Box.parse(view)
|
|
12
|
+
|
|
13
|
+
e.yield box
|
|
14
|
+
view.seek box.end_pos + 1
|
|
15
|
+
end
|
|
16
|
+
end.lazy
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative 'box_path'
|
|
2
|
+
require_relative 'box_parsers/meta_box'
|
|
3
|
+
require_relative 'box_parsers/mvhd_box'
|
|
4
|
+
|
|
5
|
+
module FileData
|
|
6
|
+
# Parses and returns metadata from an Mpeg4 file
|
|
7
|
+
class Mpeg4
|
|
8
|
+
class << self
|
|
9
|
+
['.mp4', '.mpeg4', '.m4v', '.mov'].each { |e| FileInfo.info_maps[e] = Mpeg4 }
|
|
10
|
+
|
|
11
|
+
values = [['origin_date', MetaBoxParser,
|
|
12
|
+
'creation_date', 'moov', 'meta'],
|
|
13
|
+
['creation_date', MvhdBoxParser,
|
|
14
|
+
'creation_time', 'moov', 'mvhd']]
|
|
15
|
+
|
|
16
|
+
values.each do |v|
|
|
17
|
+
define_method(v[0]) do |stream|
|
|
18
|
+
get_value(*v.drop(1).unshift(stream))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.get_value(stream, parser, method, *box_path)
|
|
24
|
+
box = BoxPath.get_root_path(stream, *box_path)
|
|
25
|
+
parser.parse(box.content_stream).send(method) unless box.nil?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Mpeg4ValueInfo = Struct.new(:name, :parser_class, :method_name, :box_path)
|
|
30
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative 'stream_view'
|
|
2
|
+
|
|
3
|
+
module Helpers
|
|
4
|
+
# Binary block that has a size equal to the value of its first field
|
|
5
|
+
class SizedField
|
|
6
|
+
def self.create_view(view, size_len)
|
|
7
|
+
content_size = view.read_value(size_len) - size_len
|
|
8
|
+
SubStreamView.new(view.stream, view.stream.pos, content_size)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require_relative '../core_extensions/binary_extensions'
|
|
3
|
+
|
|
4
|
+
module Helpers
|
|
5
|
+
# Abstract view of a stream
|
|
6
|
+
class BaseStreamView
|
|
7
|
+
extend Forwardable
|
|
8
|
+
include BinaryExtensions
|
|
9
|
+
|
|
10
|
+
attr_reader :stream, :start_pos
|
|
11
|
+
|
|
12
|
+
def initialize(stream, start_pos)
|
|
13
|
+
@stream = stream
|
|
14
|
+
@start_pos = start_pos
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def_delegators :@stream, :seek, :each_byte, :pos
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# View of a stream that has a specified size in bytes
|
|
21
|
+
class SubStreamView < BaseStreamView
|
|
22
|
+
attr_reader :end_pos, :size
|
|
23
|
+
|
|
24
|
+
def initialize(stream, start_pos, size)
|
|
25
|
+
super(stream, start_pos)
|
|
26
|
+
@end_pos = @start_pos + size - 1
|
|
27
|
+
@size = size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def remaining_bytes
|
|
31
|
+
@end_pos - pos + 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def eof?
|
|
35
|
+
pos > @end_pos || @stream.eof?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# View of a stream that ends when eof? is true
|
|
40
|
+
class StreamView < BaseStreamView
|
|
41
|
+
def initialize(stream)
|
|
42
|
+
super(stream, 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def eof?
|
|
46
|
+
@stream.eof?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/file_data/version.rb
CHANGED
data/lib/file_data.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: file_data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Scott
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2018-07-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0.10'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: deep-cover
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.6'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.6'
|
|
83
97
|
description: Extracts file metadata information (currently only supports exif metadata
|
|
84
98
|
for jpeg files)
|
|
85
99
|
email:
|
|
@@ -100,7 +114,9 @@ files:
|
|
|
100
114
|
- Rakefile
|
|
101
115
|
- file_data.gemspec
|
|
102
116
|
- lib/file_data.rb
|
|
117
|
+
- lib/file_data/core_extensions/binary_extensions.rb
|
|
103
118
|
- lib/file_data/core_extensions/enumerable_extensions.rb
|
|
119
|
+
- lib/file_data/file_types/file_info.rb
|
|
104
120
|
- lib/file_data/file_types/jpeg.rb
|
|
105
121
|
- lib/file_data/formats/exif/exif.rb
|
|
106
122
|
- lib/file_data/formats/exif/exif_data.rb
|
|
@@ -111,6 +127,18 @@ files:
|
|
|
111
127
|
- lib/file_data/formats/exif/exif_tags.rb
|
|
112
128
|
- lib/file_data/formats/exif/ifd.rb
|
|
113
129
|
- lib/file_data/formats/exif/ordinal_ifd.rb
|
|
130
|
+
- lib/file_data/formats/mpeg4/box.rb
|
|
131
|
+
- lib/file_data/formats/mpeg4/box_factory.rb
|
|
132
|
+
- lib/file_data/formats/mpeg4/box_parsers/ilst_box.rb
|
|
133
|
+
- lib/file_data/formats/mpeg4/box_parsers/ilst_data_box.rb
|
|
134
|
+
- lib/file_data/formats/mpeg4/box_parsers/keys_box.rb
|
|
135
|
+
- lib/file_data/formats/mpeg4/box_parsers/meta_box.rb
|
|
136
|
+
- lib/file_data/formats/mpeg4/box_parsers/mvhd_box.rb
|
|
137
|
+
- lib/file_data/formats/mpeg4/box_path.rb
|
|
138
|
+
- lib/file_data/formats/mpeg4/boxes_reader.rb
|
|
139
|
+
- lib/file_data/formats/mpeg4/mpeg4.rb
|
|
140
|
+
- lib/file_data/helpers/sized_field.rb
|
|
141
|
+
- lib/file_data/helpers/stream_view.rb
|
|
114
142
|
- lib/file_data/version.rb
|
|
115
143
|
homepage: ''
|
|
116
144
|
licenses:
|
|
@@ -133,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
133
161
|
version: '0'
|
|
134
162
|
requirements: []
|
|
135
163
|
rubyforge_project:
|
|
136
|
-
rubygems_version: 2.
|
|
164
|
+
rubygems_version: 2.7.6
|
|
137
165
|
signing_key:
|
|
138
166
|
specification_version: 4
|
|
139
167
|
summary: Extracts file metadata information (currently only supports exif metadata
|