format_parser 0.2.0 → 0.3.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 +4 -4
- data/.rubocop.yml +2 -0
- data/.travis.yml +1 -0
- data/README.md +14 -11
- data/format_parser.gemspec +11 -10
- data/lib/care.rb +9 -17
- data/lib/format_parser.rb +11 -13
- data/lib/format_parser/version.rb +1 -1
- data/lib/io_constraint.rb +3 -3
- data/lib/io_utils.rb +4 -10
- data/lib/parsers/aiff_parser.rb +9 -10
- data/lib/parsers/dpx_parser.rb +42 -42
- data/lib/parsers/dsl.rb +2 -2
- data/lib/parsers/exif_parser.rb +3 -8
- data/lib/parsers/fdx_parser.rb +3 -3
- data/lib/parsers/gif_parser.rb +3 -5
- data/lib/parsers/jpeg_parser.rb +4 -8
- data/lib/parsers/moov_parser.rb +8 -6
- data/lib/parsers/moov_parser/decoder.rb +105 -122
- data/lib/parsers/mp3_parser.rb +36 -46
- data/lib/parsers/mp3_parser/id3_v1.rb +7 -13
- data/lib/parsers/mp3_parser/id3_v2.rb +6 -6
- data/lib/parsers/png_parser.rb +5 -12
- data/lib/parsers/psd_parser.rb +2 -2
- data/lib/parsers/tiff_parser.rb +10 -12
- data/lib/parsers/wav_parser.rb +3 -3
- data/lib/read_limiter.rb +3 -7
- data/lib/remote_io.rb +3 -6
- data/spec/care_spec.rb +10 -10
- data/spec/file_information_spec.rb +1 -3
- data/spec/format_parser_spec.rb +6 -6
- data/spec/io_utils_spec.rb +7 -7
- data/spec/parsers/exif_parser_spec.rb +2 -3
- data/spec/parsers/gif_parser_spec.rb +1 -1
- data/spec/parsers/jpeg_parser_spec.rb +0 -1
- data/spec/parsers/moov_parser_spec.rb +2 -3
- data/spec/parsers/png_parser_spec.rb +1 -1
- data/spec/parsers/tiff_parser_spec.rb +0 -1
- data/spec/parsers/wav_parser_spec.rb +3 -3
- data/spec/read_limiter_spec.rb +0 -1
- data/spec/remote_fetching_spec.rb +34 -20
- data/spec/remote_io_spec.rb +20 -21
- data/spec/spec_helper.rb +2 -2
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 606211b4e5b24b26244fdc1a869e9e3c3a1960ea
|
4
|
+
data.tar.gz: c8da075299f9373ababeaffe28454c94329adf2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff3f7310ba2ff1b414b03b066bbddc42ee80fd448e51dfccb11d2bfe4a9d088d4b92b4e4c9cfcbf39173a9d69c627125b112cc4e7902d66080b113751f9a3b2e
|
7
|
+
data.tar.gz: 2e6146596b92f490641d41d48e71d51486900edcd7aa25a060345e7ee7c6d6272220a7a019d047b67296d9e9f7a062b3af431817b624b8c1c11891c422eaf14c
|
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# format_parser
|
2
2
|
|
3
|
+
|
3
4
|
is a Ruby library for prying open video, image, document, and audio files.
|
4
5
|
It includes a number of parser modules that try to recover metadata useful for post-processing and layout while reading the absolute
|
5
6
|
minimum amount of data possible.
|
@@ -7,35 +8,37 @@ minimum amount of data possible.
|
|
7
8
|
`format_parser` is inspired by [imagesize,](https://rubygems.org/gem/imagesize) [fastimage](https://github.com/sdsykes/fastimage)
|
8
9
|
and [dimensions,](https://github.com/sstephenson/dimensions) borrowing from them where appropriate.
|
9
10
|
|
11
|
+
[](https://badge.fury.io/rb/format_parser) [](https://travis-ci.org/WeTransfer/format_parser)
|
12
|
+
|
10
13
|
## Currently supported filetypes:
|
11
14
|
|
12
15
|
`TIFF, PSD, PNG, MP3, JPEG, GIF, DPX, AIFF, WAV, FDX, MOV, MP4`
|
13
16
|
|
14
|
-
...with more on the way!
|
17
|
+
...with [more](https://github.com/WeTransfer/format_parser/issues?q=is%3Aissue+is%3Aopen+label%3Aformats) on the way!
|
15
18
|
|
16
19
|
## Basic usage
|
17
20
|
|
18
|
-
Pass an IO object that responds to `read` and `seek` to `FormatParser` and
|
21
|
+
Pass an IO object that responds to `read` and `seek` to `FormatParser` and the first confirmed match will be returned.
|
19
22
|
|
20
23
|
```ruby
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
match = FormatParser.parse(File.open("myimage.jpg", "rb"))
|
25
|
+
match.nature #=> :image
|
26
|
+
match.format #=> :jpg
|
27
|
+
match.width_px #=> 320
|
28
|
+
match.height_px #=> 240
|
29
|
+
match.orientation #=> :top_left
|
27
30
|
```
|
28
31
|
|
29
|
-
If you would rather receive
|
32
|
+
If you would rather receive all potential results from the gem, call the gem as follows:
|
30
33
|
|
31
34
|
```ruby
|
32
|
-
FormatParser.parse(File.open("myimage.jpg", "rb"),
|
35
|
+
FormatParser.parse(File.open("myimage.jpg", "rb"), results: :all)
|
33
36
|
```
|
34
37
|
|
35
38
|
You can also optimize the metadata extraction by providing hints to the gem:
|
36
39
|
|
37
40
|
```ruby
|
38
|
-
FormatParser.parse(File.open("myimage", "rb"), natures: [:video, :image], formats: [:jpg, :png, :mp4])
|
41
|
+
FormatParser.parse(File.open("myimage", "rb"), natures: [:video, :image], formats: [:jpg, :png, :mp4], results: :all)
|
39
42
|
```
|
40
43
|
|
41
44
|
## Creating your own parsers
|
data/format_parser.gemspec
CHANGED
@@ -1,42 +1,42 @@
|
|
1
|
-
|
1
|
+
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'format_parser/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'format_parser'
|
8
8
|
spec.version = FormatParser::VERSION
|
9
9
|
spec.authors = ['Noah Berman', 'Julik Tarkhanov']
|
10
10
|
spec.email = ['noah@noahberman.org', 'me@julik.nl']
|
11
11
|
spec.licenses = ['MIT']
|
12
|
-
spec.summary =
|
12
|
+
spec.summary = 'A library for efficient parsing of file metadata'
|
13
13
|
spec.description = "A Ruby library for prying open files you can convert to a previewable format, such as video, image and audio files. It includes
|
14
14
|
a number of parser modules that try to recover metadata useful for post-processing and layout while reading the absolute
|
15
15
|
minimum amount of data possible."
|
16
|
-
spec.homepage =
|
17
|
-
spec.license =
|
16
|
+
spec.homepage = 'https://github.com/WeTransfer/format_parser'
|
17
|
+
spec.license = 'MIT'
|
18
18
|
# Alert people to a change in the gem's interface, will remove in a subsequent version
|
19
19
|
spec.post_install_message = %q{
|
20
20
|
-----------------------------------------------------------------------------
|
21
|
-
|
|
21
|
+
| ALERT: format_parser **v0.3.0** introduces changes to the gem's interface.|
|
22
22
|
| See https://github.com/WeTransfer/format_parser#basic-usage |
|
23
23
|
| for up-to-date usage instructions. Thank you for using format_parser! :) |
|
24
24
|
-----------------------------------------------------------------------------
|
25
25
|
}
|
26
26
|
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
27
27
|
if spec.respond_to?(:metadata)
|
28
|
-
spec.metadata['allowed_push_host'] =
|
28
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
29
29
|
else
|
30
|
-
raise
|
30
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
31
31
|
end
|
32
32
|
|
33
33
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
34
34
|
# Make sure large fixture files are not packaged with the gem every time
|
35
35
|
f.match(%r{^spec/fixtures/})
|
36
36
|
end
|
37
|
-
spec.bindir =
|
37
|
+
spec.bindir = 'exe'
|
38
38
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
39
|
-
spec.require_paths = [
|
39
|
+
spec.require_paths = ['lib']
|
40
40
|
|
41
41
|
spec.add_dependency 'ks', '~> 0.0.1'
|
42
42
|
spec.add_dependency 'exifr', '~> 1.0'
|
@@ -47,4 +47,5 @@ Gem::Specification.new do |spec|
|
|
47
47
|
spec.add_development_dependency 'simplecov', '~> 0.15'
|
48
48
|
spec.add_development_dependency 'pry', '~> 0.11'
|
49
49
|
spec.add_development_dependency 'yard', '~> 0.9'
|
50
|
+
spec.add_development_dependency 'wetransfer_style', '0.4.0'
|
50
51
|
end
|
data/lib/care.rb
CHANGED
@@ -7,8 +7,9 @@ class Care
|
|
7
7
|
DEFAULT_PAGE_SIZE = 16 * 1024
|
8
8
|
|
9
9
|
class IOWrapper
|
10
|
-
def initialize(io, cache=Cache.new(DEFAULT_PAGE_SIZE))
|
11
|
-
@io
|
10
|
+
def initialize(io, cache = Cache.new(DEFAULT_PAGE_SIZE))
|
11
|
+
@io = io
|
12
|
+
@cache = cache
|
12
13
|
@pos = 0
|
13
14
|
end
|
14
15
|
|
@@ -26,8 +27,9 @@ class Care
|
|
26
27
|
|
27
28
|
def read(n_bytes)
|
28
29
|
return '' if n_bytes == 0 # As hardcoded for all Ruby IO objects
|
30
|
+
raise ArgumentError, "negative length #{n_bytes} given" if n_bytes < 0 # also as per Ruby IO objects
|
29
31
|
read = @cache.byteslice(@io, @pos, n_bytes)
|
30
|
-
return
|
32
|
+
return unless read && !read.empty?
|
31
33
|
@pos += read.bytesize
|
32
34
|
read
|
33
35
|
end
|
@@ -40,10 +42,6 @@ class Care
|
|
40
42
|
clear
|
41
43
|
@io.close if @io.respond_to?(:close)
|
42
44
|
end
|
43
|
-
|
44
|
-
def size
|
45
|
-
@io.size
|
46
|
-
end
|
47
45
|
end
|
48
46
|
|
49
47
|
# Stores cached pages of data from the given IO as strings.
|
@@ -51,7 +49,7 @@ class Care
|
|
51
49
|
class Cache
|
52
50
|
def initialize(page_size = DEFAULT_PAGE_SIZE)
|
53
51
|
@page_size = page_size.to_i
|
54
|
-
raise ArgumentError,
|
52
|
+
raise ArgumentError, 'The page size must be a positive Integer' unless @page_size > 0
|
55
53
|
@pages = {}
|
56
54
|
@lowest_known_empty_page = nil
|
57
55
|
end
|
@@ -72,7 +70,7 @@ class Care
|
|
72
70
|
first_page = at / @page_size
|
73
71
|
last_page = (at + n_bytes) / @page_size
|
74
72
|
|
75
|
-
relevant_pages = (first_page..last_page).map{|i| hydrate_page(io, i) }
|
73
|
+
relevant_pages = (first_page..last_page).map { |i| hydrate_page(io, i) }
|
76
74
|
|
77
75
|
# Create one string combining all the pages which are relevant for
|
78
76
|
# us - it is much easier to address that string instead of piecing
|
@@ -96,11 +94,7 @@ class Care
|
|
96
94
|
|
97
95
|
# Returning an empty string from read() is very confusing for the caller,
|
98
96
|
# and no builtins do this - if we are at EOF we should return nil
|
99
|
-
if slice && !slice.empty?
|
100
|
-
slice
|
101
|
-
else
|
102
|
-
nil
|
103
|
-
end
|
97
|
+
slice if slice && !slice.empty?
|
104
98
|
end
|
105
99
|
|
106
100
|
def clear
|
@@ -110,9 +104,7 @@ class Care
|
|
110
104
|
def hydrate_page(io, page_i)
|
111
105
|
# Avoid trying to read the page if we know there is no content to fill it
|
112
106
|
# in the underlying IO
|
113
|
-
if @lowest_known_empty_page && page_i >= @lowest_known_empty_page
|
114
|
-
return nil
|
115
|
-
end
|
107
|
+
return if @lowest_known_empty_page && page_i >= @lowest_known_empty_page
|
116
108
|
|
117
109
|
@pages[page_i] ||= read_page(io, page_i)
|
118
110
|
end
|
data/lib/format_parser.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'thread'
|
2
|
-
|
3
1
|
module FormatParser
|
4
2
|
require_relative 'image'
|
5
3
|
require_relative 'audio'
|
@@ -28,7 +26,7 @@ module FormatParser
|
|
28
26
|
end
|
29
27
|
end
|
30
28
|
|
31
|
-
def self.parse_http(url)
|
29
|
+
def self.parse_http(url, **kwargs)
|
32
30
|
remote_io = RemoteIO.new(url)
|
33
31
|
cached_io = Care::IOWrapper.new(remote_io)
|
34
32
|
|
@@ -36,25 +34,27 @@ module FormatParser
|
|
36
34
|
# by all parsers anyway. Additionally, when using RemoteIO we need
|
37
35
|
# to explicitly obtain the size of the resource, which is only available
|
38
36
|
# after having performed at least one successful GET - at least on S3
|
39
|
-
cached_io.read(1)
|
37
|
+
cached_io.read(1)
|
38
|
+
cached_io.seek(0)
|
40
39
|
|
41
|
-
parse(cached_io)
|
40
|
+
parse(cached_io, **kwargs)
|
42
41
|
end
|
43
42
|
|
44
|
-
|
43
|
+
# Return all by default
|
44
|
+
def self.parse(io, natures: @natures.to_a, formats: @formats.to_a, results: :first)
|
45
45
|
# If the cache is preconfigured do not apply an extra layer. It is going
|
46
46
|
# to be preconfigured when using parse_http.
|
47
47
|
io = Care::IOWrapper.new(io) unless io.is_a?(Care::IOWrapper)
|
48
48
|
|
49
49
|
# How many results has the user asked for? Used to determinate whether an array
|
50
50
|
# is returned or not.
|
51
|
-
amount = case
|
51
|
+
amount = case results
|
52
52
|
when :all
|
53
53
|
@parsers.count
|
54
|
-
when :
|
54
|
+
when :first
|
55
55
|
1
|
56
56
|
else
|
57
|
-
throw ArgumentError.new(
|
57
|
+
throw ArgumentError.new(':results does not match any supported mode (:all, :first)')
|
58
58
|
end
|
59
59
|
|
60
60
|
# Always instantiate parsers fresh for each input, since they might
|
@@ -64,7 +64,7 @@ module FormatParser
|
|
64
64
|
# We need to rewind for each parser, anew
|
65
65
|
io.seek(0)
|
66
66
|
# Limit how many operations the parser can perform
|
67
|
-
limited_io = ReadLimiter.new(io, max_bytes: 512*1024, max_reads: 64*1024, max_seeks: 64*1024)
|
67
|
+
limited_io = ReadLimiter.new(io, max_bytes: 512 * 1024, max_reads: 64 * 1024, max_seeks: 64 * 1024)
|
68
68
|
begin
|
69
69
|
parser.call(limited_io)
|
70
70
|
rescue IOUtils::InvalidRead
|
@@ -82,14 +82,12 @@ module FormatParser
|
|
82
82
|
results.to_a
|
83
83
|
end
|
84
84
|
|
85
|
-
private
|
86
|
-
|
87
85
|
def self.parsers_for(natures, formats)
|
88
86
|
# returns lazy enumerator for only computing the minimum amount of work (see :returns keyword argument)
|
89
87
|
@parsers.map(&:new).select do |parser|
|
90
88
|
# Do a given parser contain any nature and/or format asked by the user?
|
91
89
|
(natures & parser.natures).size > 0 && (formats & parser.formats).size > 0
|
92
|
-
|
90
|
+
end.lazy
|
93
91
|
end
|
94
92
|
|
95
93
|
Dir.glob(__dir__ + '/parsers/*.rb').sort.each do |parser_file|
|
data/lib/io_constraint.rb
CHANGED
@@ -19,15 +19,15 @@ class FormatParser::IOConstraint
|
|
19
19
|
def initialize(io)
|
20
20
|
@io = io
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
def read(n_bytes)
|
24
24
|
@io.read(n_bytes)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def seek(absolute_offset)
|
28
28
|
@io.seek(absolute_offset)
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
31
|
def size
|
32
32
|
@io.size
|
33
33
|
end
|
data/lib/io_utils.rb
CHANGED
@@ -3,12 +3,10 @@ module FormatParser::IOUtils
|
|
3
3
|
end
|
4
4
|
|
5
5
|
def safe_read(io, n)
|
6
|
-
if n.nil?
|
7
|
-
raise ArgumentError, "Unbounded reads are not supported"
|
8
|
-
end
|
6
|
+
raise ArgumentError, 'Unbounded reads are not supported' if n.nil?
|
9
7
|
buf = io.read(n)
|
10
8
|
|
11
|
-
|
9
|
+
unless buf
|
12
10
|
raise InvalidRead, "We wanted to read #{n} bytes from the IO, but the IO is at EOF"
|
13
11
|
end
|
14
12
|
if buf.bytesize != n
|
@@ -19,15 +17,11 @@ module FormatParser::IOUtils
|
|
19
17
|
end
|
20
18
|
|
21
19
|
def safe_skip(io, n)
|
22
|
-
if n.nil?
|
23
|
-
raise ArgumentError, "Unbounded skips are not supported"
|
24
|
-
end
|
20
|
+
raise ArgumentError, 'Unbounded skips are not supported' if n.nil?
|
25
21
|
|
26
22
|
return if n == 0
|
27
23
|
|
28
|
-
if n < 0
|
29
|
-
raise InvalidRead, "Negative skips are not supported"
|
30
|
-
end
|
24
|
+
raise InvalidRead, 'Negative skips are not supported' if n < 0
|
31
25
|
|
32
26
|
if io.respond_to?(:pos)
|
33
27
|
io.seek(io.pos + n)
|
data/lib/parsers/aiff_parser.rb
CHANGED
@@ -25,11 +25,11 @@ class FormatParser::AIFFParser
|
|
25
25
|
def call(io)
|
26
26
|
io = FormatParser::IOConstraint.new(io)
|
27
27
|
form_chunk_type, chunk_size = safe_read(io, 8).unpack('a4N')
|
28
|
-
return unless form_chunk_type ==
|
28
|
+
return unless form_chunk_type == 'FORM' && chunk_size > 4
|
29
29
|
|
30
30
|
fmt_chunk_type = safe_read(io, 4)
|
31
|
-
|
32
|
-
return unless fmt_chunk_type ==
|
31
|
+
|
32
|
+
return unless fmt_chunk_type == 'AIFF'
|
33
33
|
|
34
34
|
# There might be COMT chunks, for example in Logic exports
|
35
35
|
loop do
|
@@ -49,16 +49,15 @@ class FormatParser::AIFFParser
|
|
49
49
|
safe_skip(io, chunk_size)
|
50
50
|
next
|
51
51
|
else # This most likely not an AIFF
|
52
|
-
return
|
52
|
+
return
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
57
|
def unpack_comm_chunk(io)
|
58
58
|
# Parse the COMM chunk
|
59
|
-
channels, sample_frames,
|
59
|
+
channels, sample_frames, _sample_size, sample_rate_extended = safe_read(io, 2 + 4 + 2 + 10).unpack('nNna10')
|
60
60
|
sample_rate = unpack_extended_float(sample_rate_extended)
|
61
|
-
bytes_per_sample = (sample_size - 1) / 8 + 1
|
62
61
|
|
63
62
|
return unless sample_frames > 0
|
64
63
|
|
@@ -71,18 +70,18 @@ class FormatParser::AIFFParser
|
|
71
70
|
num_audio_channels: channels,
|
72
71
|
audio_sample_rate_hz: sample_rate.to_i,
|
73
72
|
media_duration_frames: sample_frames,
|
74
|
-
media_duration_seconds: duration_in_seconds
|
73
|
+
media_duration_seconds: duration_in_seconds
|
75
74
|
)
|
76
75
|
end
|
77
|
-
|
76
|
+
|
78
77
|
def unpack_extended_float(ten_bytes_string)
|
79
78
|
extended = ten_bytes_string.unpack('B80')[0]
|
80
79
|
|
81
80
|
sign = extended[0, 1]
|
82
81
|
exponent = extended[1, 15].to_i(2) - ((1 << 14) - 1)
|
83
82
|
fraction = extended[16, 64].to_i(2)
|
84
|
-
|
85
|
-
(
|
83
|
+
|
84
|
+
(sign == '1' ? -1.0 : 1.0) * (fraction.to_f / ((1 << 63) - 1)) * (2**exponent)
|
86
85
|
end
|
87
86
|
|
88
87
|
FormatParser.register_parser_constructor self
|
data/lib/parsers/dpx_parser.rb
CHANGED
@@ -6,7 +6,7 @@ class FormatParser::DPXParser
|
|
6
6
|
formats :dpx
|
7
7
|
|
8
8
|
FILE_INFO = [
|
9
|
-
# :x4, # magic bytes SDPX, we read them anyway so not in the pattern
|
9
|
+
# :x4, # magic bytes SDPX, we read them anyway so not in the pattern
|
10
10
|
:x4, # u32 :image_offset, :desc => 'Offset to image data in bytes', :req => true
|
11
11
|
:x8, # char :version, 8, :desc => 'Version of header format', :req => true
|
12
12
|
:x4, # u32 :file_size, :desc => "Total image size in bytes", :req => true
|
@@ -20,48 +20,48 @@ class FormatParser::DPXParser
|
|
20
20
|
:x200, # char :project, 200, :desc => 'Project name'
|
21
21
|
:x200, # char :copyright, 200, :desc => 'Copyright'
|
22
22
|
:x4, # u32 :encrypt_key, :desc => 'Encryption key'
|
23
|
-
:x104,
|
23
|
+
:x104, # blanking :reserve, 104
|
24
24
|
].join
|
25
25
|
|
26
26
|
FILM_INFO = [
|
27
|
-
:x2,
|
28
|
-
:x2,
|
29
|
-
:x2,
|
30
|
-
:x6,
|
31
|
-
:x4,
|
32
|
-
:x32
|
33
|
-
:x4,
|
34
|
-
:x4,
|
35
|
-
:x4,
|
36
|
-
:x4,
|
37
|
-
:x4,
|
38
|
-
:x4,
|
39
|
-
:x4,
|
40
|
-
:x4,
|
27
|
+
:x2, # char :id, 2, :desc => 'Film mfg. ID code (2 digits from film edge code)'
|
28
|
+
:x2, # char :type, 2, :desc => 'Film type (2 digits from film edge code)'
|
29
|
+
:x2, # char :offset, 2, :desc => 'Offset in perfs (2 digits from film edge code)'
|
30
|
+
:x6, # char :prefix, 6, :desc => 'Prefix (6 digits from film edge code'
|
31
|
+
:x4, # char :count, 4, :desc => 'Count (4 digits from film edge code)'
|
32
|
+
:x32, # char :format, 32, :desc => 'Format (e.g. Academy)'
|
33
|
+
:x4, # u32 :frame_position, :desc => 'Frame position in sequence'
|
34
|
+
:x4, # u32 :sequence_extent, :desc => 'Sequence length'
|
35
|
+
:x4, # u32 :held_count, :desc => 'For how many frames the frame is held'
|
36
|
+
:x4, # r32 :frame_rate, :desc => 'Frame rate'
|
37
|
+
:x4, # r32 :shutter_angle, :desc => 'Shutter angle'
|
38
|
+
:x4, # char :frame_id, 32, :desc => 'Frame identification (keyframe)'
|
39
|
+
:x4, # char :slate, 100, :desc => 'Slate information'
|
40
|
+
:x4, # blanking :reserve, 56
|
41
41
|
].join
|
42
42
|
|
43
43
|
IMAGE_ELEMENT = [
|
44
|
-
:x4,
|
44
|
+
:x4, # u32 :data_sign, :desc => 'Data sign (0=unsigned, 1=signed). Core is unsigned', :req => true
|
45
45
|
#
|
46
|
-
:x4,
|
47
|
-
:x4,
|
48
|
-
:x4,
|
49
|
-
:x4,
|
46
|
+
:x4, # u32 :low_data, :desc => 'Reference low data code value'
|
47
|
+
:x4, # r32 :low_quantity, :desc => 'Reference low quantity represented'
|
48
|
+
:x4, # u32 :high_data, :desc => 'Reference high data code value (1023 for 10bit per channel)'
|
49
|
+
:x4, # r32 :high_quantity, :desc => 'Reference high quantity represented'
|
50
50
|
#
|
51
|
-
:x1,
|
51
|
+
:x1, # u8 :descriptor, :desc => 'Descriptor for this image element (ie Video or Film), by enum', :req => true
|
52
52
|
# TODO - colirimetry information might be handy to recover,
|
53
53
|
# as well as "bit size per element" (how many bits _per component_ we have) -
|
54
54
|
# this will be different for, say, 8-bit DPX files versus 10-bit etc.
|
55
|
-
:x1,
|
56
|
-
:x1,
|
57
|
-
:x1,
|
55
|
+
:x1, # u8 :transfer, :desc => 'Transfer function (ie Linear), by enum', :req => true
|
56
|
+
:x1, # u8 :colorimetric, :desc => 'Colorimetric (ie YcbCr), by enum', :req => true
|
57
|
+
:x1, # u8 :bit_size, :desc => 'Bit size for element (ie 10)', :req => true
|
58
58
|
#
|
59
|
-
:x2,
|
60
|
-
:x2,
|
61
|
-
:x4,
|
62
|
-
:x4,
|
63
|
-
:x4,
|
64
|
-
:x32
|
59
|
+
:x2, # u16 :packing, :desc => 'Packing (0=Packed into 32-bit words, 1=Filled to 32-bit words))', :req => true
|
60
|
+
:x2, # u16 :encoding, :desc => "Encoding (0=None, 1=RLE)", :req => true
|
61
|
+
:x4, # u32 :data_offset, :desc => 'Offset to data for this image element', :req => true
|
62
|
+
:x4, # u32 :end_of_line_padding, :desc => "End-of-line padding for this image element"
|
63
|
+
:x4, # u32 :end_of_image_padding, :desc => "End-of-line padding for this image element"
|
64
|
+
:x32, # char :description, 32
|
65
65
|
].join
|
66
66
|
|
67
67
|
IMAGE_INFO = [
|
@@ -93,7 +93,7 @@ class FormatParser::DPXParser
|
|
93
93
|
:x4,
|
94
94
|
:x4,
|
95
95
|
|
96
|
-
# TODO - the aspect ratio might be handy to recover since it
|
96
|
+
# TODO: - the aspect ratio might be handy to recover since it
|
97
97
|
# will be used in DPX files in, say, anamorphic (non-square pixels)
|
98
98
|
:x4, # array :aspect_ratio , :u32, 2, :desc => "Aspect (H:V)"
|
99
99
|
:x4,
|
@@ -107,16 +107,16 @@ class FormatParser::DPXParser
|
|
107
107
|
ORIENTATION_INFO,
|
108
108
|
].join
|
109
109
|
|
110
|
-
DPX_INFO_LE = DPX_INFO.tr(
|
110
|
+
DPX_INFO_LE = DPX_INFO.tr('n', 'v').tr('N', 'V')
|
111
111
|
|
112
112
|
SIZEOF = ->(pattern) {
|
113
113
|
bytes_per_element = {
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
114
|
+
'v' => 2, # 16bit uints
|
115
|
+
'n' => 2,
|
116
|
+
'V' => 4, # 32bit uints
|
117
|
+
'N' => 4,
|
118
|
+
'C' => 1,
|
119
|
+
'x' => 1,
|
120
120
|
}
|
121
121
|
pattern.scan(/[^\d]\d+/).map do |pattern|
|
122
122
|
unpack_code = pattern[0]
|
@@ -133,15 +133,15 @@ class FormatParser::DPXParser
|
|
133
133
|
io = FormatParser::IOConstraint.new(io)
|
134
134
|
magic = io.read(4)
|
135
135
|
|
136
|
-
return
|
136
|
+
return unless [BE_MAGIC, LE_MAGIC].include?(magic)
|
137
137
|
|
138
138
|
unpack_pattern = DPX_INFO
|
139
139
|
unpack_pattern = DPX_INFO_LE if magic == LE_MAGIC
|
140
|
-
|
140
|
+
_num_elements, pixels_per_line, num_lines, *_ = safe_read(io, HEADER_SIZE).unpack(unpack_pattern)
|
141
141
|
FormatParser::Image.new(
|
142
142
|
format: :dpx,
|
143
143
|
width_px: pixels_per_line,
|
144
|
-
height_px: num_lines
|
144
|
+
height_px: num_lines
|
145
145
|
)
|
146
146
|
end
|
147
147
|
|