depix 1.1.6 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/{DPX_HEADER_STRUCTURE.txt → DPX_HEADER_STRUCTURE.rdoc} +0 -0
- data/History.txt +5 -0
- data/Manifest.txt +6 -4
- data/{README.txt → README.rdoc} +3 -3
- data/Rakefile +4 -1
- data/lib/depix.rb +12 -73
- data/lib/depix/binary/descriptor.rb +56 -0
- data/lib/depix/binary/fields.rb +302 -0
- data/lib/depix/binary/structure.rb +239 -0
- data/lib/depix/compact_structs.rb +1 -1
- data/lib/depix/reader.rb +5 -4
- data/lib/depix/structs.rb +9 -12
- data/lib/depix/synthetics.rb +64 -0
- data/test/test_depix.rb +8 -11
- data/test/test_dict.rb +52 -52
- metadata +16 -48
- data/lib/depix/benchmark.rb +0 -15
- data/lib/depix/dict.rb +0 -531
@@ -0,0 +1,239 @@
|
|
1
|
+
# A basic C structs library (only works by value).
|
2
|
+
# Here's the basic mode of operation:
|
3
|
+
# 1) You define a struct, with a number of fields in it. This hould be a subclass of Dict within which you
|
4
|
+
# create Field objects which are saved in a class variable
|
5
|
+
# 3) Each created Field instance knows how big it is and how to produce a pattern to get it's value from the byte stream
|
6
|
+
# by using Ruby's "pack/unpack". Each field thus provides an unpack pattern, and patterns are ordered
|
7
|
+
# into a stack, starting with the first unpack pattern
|
8
|
+
# 4) When you parse some bytes using the struct:
|
9
|
+
# - An unpack pattern will be compiled from all of the fields composing the struct,
|
10
|
+
# and it will be a single string. The string gets applied to the bytes passed to parse()
|
11
|
+
# - An array of unpacked values returned by unpack is then passed to the struct's consumption engine,
|
12
|
+
# which lets each field take as many items off the stack as it needs. A field might happily produce
|
13
|
+
# 4 items for unpacking and then take the same 4 items off the stack of parsed values. Or not.
|
14
|
+
# - A new structure gets created and for every named field it defines an attr_accessor. When consuming,
|
15
|
+
# the values returned by Field objects get set using the accessors (so accessors can be overridden too!)
|
16
|
+
# 5) When you save out the struct roughly the same happens but in reverse (readers are called per field,
|
17
|
+
# then it's checked whether the data can be packed and fits into the alloted number of bytes, and then
|
18
|
+
# one big array of values is composed and passed on to Array#pack)
|
19
|
+
#
|
20
|
+
# For example
|
21
|
+
#
|
22
|
+
# class OneIntegerAndOneFloat < Structure
|
23
|
+
# uint32 :identifier, :description => "This is the important ID", :required => true
|
24
|
+
# real :value, :description => "The value that we store"
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# ready_struct = OneIntegerAndOneFloat.new
|
28
|
+
# ready_struct.identifier = 23 # Plain Ruby assignment
|
29
|
+
# ready_struct.value = 45.0
|
30
|
+
#
|
31
|
+
# binary_file.write(OneIntegerAndOneFloat.pack(ready_struct)) # dumps the packed struct with paddings
|
32
|
+
class Depix::Binary::Structure
|
33
|
+
|
34
|
+
DEF_OPTS = { :req => false, :desc => nil }
|
35
|
+
|
36
|
+
# Allows us to use field names from Fields module
|
37
|
+
def self.const_missing(c)
|
38
|
+
Depix::Binary::Fields.const_get(c)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get the array of fields defined in this struct
|
42
|
+
def self.fields
|
43
|
+
@fields ||= []
|
44
|
+
end
|
45
|
+
|
46
|
+
# Validate a passed instance
|
47
|
+
def self.validate!(instance)
|
48
|
+
fields.each do | f |
|
49
|
+
f.validate!(instance.send(f.name)) if f.name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Define a 4-byte unsigned integer
|
54
|
+
def self.u32(name, *extras)
|
55
|
+
count, opts = count_and_opts_from(extras)
|
56
|
+
attr_accessor name
|
57
|
+
fields << U32Field.new( {:name => name }.merge(opts) )
|
58
|
+
end
|
59
|
+
|
60
|
+
# Define a double-width unsigned integer
|
61
|
+
def self.u16(name, *extras)
|
62
|
+
count, opts = count_and_opts_from(extras)
|
63
|
+
attr_accessor name
|
64
|
+
fields << U16Field.new( {:name => name }.merge(opts) )
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Define a small unsigned integer
|
69
|
+
def self.u8(name, *extras)
|
70
|
+
count, opts = count_and_opts_from(extras)
|
71
|
+
attr_accessor name
|
72
|
+
fields << U8Field.new( {:name => name }.merge(opts) )
|
73
|
+
end
|
74
|
+
|
75
|
+
# Define a real number
|
76
|
+
def self.r32(name, *extras)
|
77
|
+
count, opts = count_and_opts_from(extras)
|
78
|
+
attr_accessor name
|
79
|
+
fields << R32Field.new( {:name => name}.merge(opts) )
|
80
|
+
end
|
81
|
+
|
82
|
+
# Define an array of values
|
83
|
+
def self.array(name, mapped_to, *extras)
|
84
|
+
count, opts = count_and_opts_from(extras)
|
85
|
+
attr_accessor name
|
86
|
+
a = ArrayField.new({:name => name}.merge(opts))
|
87
|
+
a.members = if mapped_to.is_a?(Class) # Array of structs
|
88
|
+
[InnerField.new(:cast => mapped_to)] * count
|
89
|
+
else
|
90
|
+
c = Depix::Binary::Fields.const_get("#{mapped_to.to_s.upcase}Field")
|
91
|
+
[c.new] * count
|
92
|
+
end
|
93
|
+
yield a.members if block_given?
|
94
|
+
fields << a
|
95
|
+
end
|
96
|
+
|
97
|
+
# Define a nested struct
|
98
|
+
def self.inner(name, mapped_to, *extras)
|
99
|
+
count, opts = count_and_opts_from(extras)
|
100
|
+
attr_accessor name
|
101
|
+
fields << InnerField.new({:name => name, :cast => mapped_to}.merge(opts))
|
102
|
+
end
|
103
|
+
|
104
|
+
# Define a char field
|
105
|
+
def self.char(name, *extras)
|
106
|
+
count, opts = count_and_opts_from(extras)
|
107
|
+
attr_accessor name
|
108
|
+
fields << CharField.new( {:name => name, :length => count}.merge(opts) )
|
109
|
+
end
|
110
|
+
|
111
|
+
# Get the pattern that will be used to unpack this structure and all of it's descendants
|
112
|
+
def self.pattern
|
113
|
+
fields.map{|f| f.pattern }.join
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the pattern that will be used to unpack this structure and all of it's descendants
|
117
|
+
# from a buffer with pieces in little-endian byte order
|
118
|
+
def self.pattern_le
|
119
|
+
pattern.tr("gN", "eV")
|
120
|
+
end
|
121
|
+
|
122
|
+
# How many bytes are needed to complete this structure
|
123
|
+
def self.length
|
124
|
+
fields.inject(0){|_, s| _ + s.length.to_i }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Consume a stack of unpacked values, letting each field decide how many to consume
|
128
|
+
def self.consume!(stack_of_unpacked_values)
|
129
|
+
new_item = new
|
130
|
+
@fields.each do | field |
|
131
|
+
new_item.send("#{field.name}=", field.consume!(stack_of_unpacked_values)) unless field.name.nil?
|
132
|
+
end
|
133
|
+
new_item
|
134
|
+
end
|
135
|
+
|
136
|
+
# Apply this structure to data in the string, returning an instance of this structure with fields completed
|
137
|
+
def self.apply!(string)
|
138
|
+
consume!(string.unpack(pattern))
|
139
|
+
end
|
140
|
+
|
141
|
+
# Apply this structure to data in the string, returning an instance of this structure with fields completed
|
142
|
+
# assume little-endian fields
|
143
|
+
def self.apply_le!(string)
|
144
|
+
consume!(string.unpack(pattern_le))
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get a class that would parse just the same, preserving only the fields passed in the array. This speeds
|
148
|
+
# up parsing because we only extract and conform the fields that we need
|
149
|
+
def self.only(*field_names)
|
150
|
+
distillate = fields.inject([]) do | m, f |
|
151
|
+
if field_names.include?(f.name) # preserve
|
152
|
+
m.push(f)
|
153
|
+
else # create filler
|
154
|
+
unless m[-1].is_a?(Filler)
|
155
|
+
m.push(Filler.new(:length => f.length))
|
156
|
+
else
|
157
|
+
m[-1].length += f.length
|
158
|
+
end
|
159
|
+
m
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
anon = Class.new(self)
|
164
|
+
anon.fields.replace(distillate)
|
165
|
+
only_items = distillate.map{|n| n.name }
|
166
|
+
|
167
|
+
anon
|
168
|
+
end
|
169
|
+
|
170
|
+
# Get an opaque struct based on this one, that will consume exactly as many bytes as this
|
171
|
+
# structure would occupy, but discard them instead
|
172
|
+
def self.filler
|
173
|
+
only([])
|
174
|
+
end
|
175
|
+
|
176
|
+
# Only relevant for 1.9
|
177
|
+
def self.byteify_string(string)
|
178
|
+
string.force_encoding("ASCII-8BIT")
|
179
|
+
end
|
180
|
+
|
181
|
+
# Pack the instance of this struct
|
182
|
+
def self.pack(instance, buffer = nil)
|
183
|
+
|
184
|
+
# Preallocate a buffer just as big as me since we want everything to remain at fixed offsets
|
185
|
+
buffer ||= ("\000" * length)
|
186
|
+
|
187
|
+
# We need to enforce ASCII-8bit encoding which in Ruby parlance is actually "bytestream"
|
188
|
+
byteify_string(buffer) unless RUBY_VERSION < '1.9.0'
|
189
|
+
|
190
|
+
# If the instance is nil return pure padding
|
191
|
+
return buffer if instance.nil?
|
192
|
+
|
193
|
+
# Now for the important stuff. For each field that we have, replace a piece at offsets in the buffer
|
194
|
+
# with the packed results, skipping fillers
|
195
|
+
fields.each_with_index do | f, i |
|
196
|
+
|
197
|
+
# Skip blanking, we just dont touch it. TODO - test!
|
198
|
+
next if f.is_a?(Filler)
|
199
|
+
|
200
|
+
# Where should we put that value?
|
201
|
+
offset = fields[0...i].inject(0){|_, s| _ + s.length }
|
202
|
+
|
203
|
+
val = instance.send(f.name)
|
204
|
+
|
205
|
+
# Validate the passed value using the format the field supports
|
206
|
+
f.validate!(val)
|
207
|
+
|
208
|
+
packed = f.pack(val)
|
209
|
+
|
210
|
+
# Signal offset violation
|
211
|
+
raise "Improper length for #{f.name} - packed #{packed.length} bytes but #{f.length} is required to fill the slot" if packed.length != f.length
|
212
|
+
|
213
|
+
# See above, byt we need to do this with the packed string as well
|
214
|
+
byteify_string(packed) unless RUBY_VERSION < '1.9.0'
|
215
|
+
|
216
|
+
buffer[offset...(offset+f.length)] = packed
|
217
|
+
end
|
218
|
+
raise "Resulting buffer not the same length, expected #{length} bytes but compued #{buffer.length}" if buffer.length != length
|
219
|
+
buffer
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# extract_options! on a diet
|
225
|
+
def self.count_and_opts_from(args)
|
226
|
+
options, count = (args[-1].is_a?(Hash) ? DEF_OPTS.merge(args.pop) : DEF_OPTS), (args.shift || 1)
|
227
|
+
[count, options]
|
228
|
+
end
|
229
|
+
|
230
|
+
public
|
231
|
+
|
232
|
+
def []=(field, value)
|
233
|
+
send("#{field}=", value)
|
234
|
+
end
|
235
|
+
|
236
|
+
def [](field)
|
237
|
+
send(field)
|
238
|
+
end
|
239
|
+
end
|
@@ -30,7 +30,7 @@ module Depix
|
|
30
30
|
|
31
31
|
|
32
32
|
# A version of the DPX structure that only accounts for the values that change per frame if the ditto_key is set to 1
|
33
|
-
class CompactDPX <
|
33
|
+
class CompactDPX < Binary::Structure
|
34
34
|
inner :file, CompactInfo, :desc => "File information, only frame-transient values"
|
35
35
|
|
36
36
|
inner :image, ImageInfo.filler
|
data/lib/depix/reader.rb
CHANGED
@@ -9,11 +9,12 @@ module Depix
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def describe_synthetics_of_struct(struct)
|
12
|
-
Synthetics.instance_methods.reject{|m| m.include?('=')}.map do | m |
|
12
|
+
Synthetics.instance_methods.reject{|m| m.to_s.include?('=')}.map do | m |
|
13
13
|
[m, struct.send(m)].join(' : ')
|
14
14
|
end.unshift("============").unshift("\nSynthetic properties").join("\n")
|
15
15
|
end
|
16
16
|
|
17
|
+
# Parse DPX headers at the start of file
|
17
18
|
def from_file(path, compact)
|
18
19
|
header = File.open(path, 'r') { |f| f.read(DPX.length) }
|
19
20
|
begin
|
@@ -23,7 +24,7 @@ module Depix
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
|
-
#
|
27
|
+
# Parse a DPX header (blob of bytes starting at the magic word)
|
27
28
|
def parse(data, compact)
|
28
29
|
magic = data[0..3]
|
29
30
|
raise InvalidHeader, "No magic bytes found at start" unless %w( SDPX XPDS).include?(magic)
|
@@ -49,12 +50,12 @@ module Depix
|
|
49
50
|
parts = []
|
50
51
|
if value
|
51
52
|
parts << field.desc if field.desc
|
52
|
-
parts << if field.is_a?(InnerField)
|
53
|
+
parts << if field.is_a?(Depix::Binary::Fields::InnerField)
|
53
54
|
describe_struct(value, pad_offset + 1)
|
54
55
|
elsif field.is_a?(ArrayField)
|
55
56
|
# Exception for image elements
|
56
57
|
value = result.image_elements[0...result.number_elements] if field.name == :image_elements
|
57
|
-
value.map { | v | v.is_a?(
|
58
|
+
value.map { | v | v.is_a?(Depix::Binary::Structure) ? describe_struct(v, pad_offset + 2) : v }
|
58
59
|
else
|
59
60
|
value
|
60
61
|
end
|
data/lib/depix/structs.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/dict'
|
2
|
-
|
3
|
-
|
4
1
|
module Depix
|
5
2
|
|
6
|
-
class FileInfo <
|
3
|
+
class FileInfo < Binary::Structure
|
7
4
|
char :magic, 4, :desc => 'Endianness (SDPX is big endian)', :req => true
|
8
5
|
u32 :image_offset, :desc => 'Offset to image data in bytes', :req => true
|
9
6
|
char :version, 8, :desc => 'Version of header format', :req => true
|
@@ -24,7 +21,7 @@ module Depix
|
|
24
21
|
char :reserve, 104
|
25
22
|
end
|
26
23
|
|
27
|
-
class FilmInfo <
|
24
|
+
class FilmInfo < Binary::Structure
|
28
25
|
char :id, 2, :desc => 'Film mfg. ID code (2 digits from film edge code)'
|
29
26
|
char :type, 2, :desc => 'Film type (2 digits from film edge code)'
|
30
27
|
char :offset, 2, :desc => 'Offset in perfs (2 digits from film edge code)'
|
@@ -44,7 +41,7 @@ module Depix
|
|
44
41
|
char :reserve, 56
|
45
42
|
end
|
46
43
|
|
47
|
-
class ImageElement <
|
44
|
+
class ImageElement < Binary::Structure
|
48
45
|
u32 :data_sign, :desc => 'Data sign (0=unsigned, 1=signed). Core is unsigned', :req => true
|
49
46
|
|
50
47
|
u32 :low_data, :desc => 'Reference low data code value'
|
@@ -66,7 +63,7 @@ module Depix
|
|
66
63
|
char :description, 32
|
67
64
|
end
|
68
65
|
|
69
|
-
class OrientationInfo <
|
66
|
+
class OrientationInfo < Binary::Structure
|
70
67
|
|
71
68
|
u32 :x_offset
|
72
69
|
u32 :y_offset
|
@@ -88,7 +85,7 @@ module Depix
|
|
88
85
|
char :reserve, 28
|
89
86
|
end
|
90
87
|
|
91
|
-
class TelevisionInfo <
|
88
|
+
class TelevisionInfo < Binary::Structure
|
92
89
|
u32 :time_code, :desc => "Timecode, formatted as HH:MM:SS:FF in the 4 higher bits of each 8bit group"
|
93
90
|
u32 :user_bits, :desc => "Timecode UBITs"
|
94
91
|
u8 :interlace, :desc => "Interlace (0 = noninterlaced; 1 = 2:1 interlace"
|
@@ -110,12 +107,12 @@ module Depix
|
|
110
107
|
r32 :reserve
|
111
108
|
end
|
112
109
|
|
113
|
-
class UserInfo <
|
110
|
+
class UserInfo < Binary::Structure
|
114
111
|
char :id, 32, :desc => 'Name of the user data tag'
|
115
112
|
u32 :user_data_ptr
|
116
113
|
end
|
117
114
|
|
118
|
-
class ImageInfo <
|
115
|
+
class ImageInfo < Binary::Structure
|
119
116
|
u16 :orientation, OrientationInfo, :desc => 'Orientation descriptor', :req => true
|
120
117
|
u16 :number_elements, :desc => 'How many elements to scan', :req => true
|
121
118
|
|
@@ -134,8 +131,8 @@ module Depix
|
|
134
131
|
end
|
135
132
|
end
|
136
133
|
|
137
|
-
|
138
|
-
class DPX <
|
134
|
+
# This is the main structure represinting headers of one DPX file, see DPX_HEADER_STRUCTURE.rdoc
|
135
|
+
class DPX < Binary::Structure
|
139
136
|
inner :file, FileInfo, :desc => "File information", :req => true
|
140
137
|
inner :image, ImageInfo, :desc => "Image information", :req => true
|
141
138
|
inner :orientation, OrientationInfo, :desc => "Orientation", :req => true
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Offers convenience access to a number of interesting fields of the DPX object
|
2
|
+
# already decoded into the most usable form (and pulled from a field that you
|
3
|
+
# won't expect)
|
4
|
+
module Depix::Synthetics
|
5
|
+
|
6
|
+
DEFAULT_DPX_FPS = 25
|
7
|
+
|
8
|
+
# Get formatted keycode as string, empty elements are omitted
|
9
|
+
def keycode
|
10
|
+
[film.id, film.type, film.offset, film.prefix, film.count].compact.join(' ')
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return the flame reel name. The data after the first null byte is not meant to be seen
|
14
|
+
# and is used by Flame internally
|
15
|
+
# as it seems
|
16
|
+
def flame_reel
|
17
|
+
return nil unless orientation.device
|
18
|
+
orientation.device.split(0x00.chr).shift
|
19
|
+
end
|
20
|
+
|
21
|
+
# Assign reel name
|
22
|
+
def flame_reel=(new_reel)
|
23
|
+
orientation.device = new_reel
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get television.time_code as a Timecode object with a framerate.
|
27
|
+
# We explicitly use the television frame rate since Northlight
|
28
|
+
# writes different rates for television and film time code
|
29
|
+
def time_code
|
30
|
+
framerates = [television.frame_rate, film.frame_rate, DEFAULT_DPX_FPS]
|
31
|
+
framerate = framerates.find{|e| !e.nil? && !e.zero? }
|
32
|
+
if television.time_code
|
33
|
+
Timecode.from_uint(television.time_code, framerate)
|
34
|
+
else
|
35
|
+
# Assume frame position
|
36
|
+
Timecode.new(film.frame_position, framerate)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Assign frame rate and timecode from a Timecode object
|
41
|
+
def time_code=(new_tc)
|
42
|
+
television.time_code, television.frame_rate = new_tc.to_uint, new_tc.fps
|
43
|
+
end
|
44
|
+
|
45
|
+
# Get the name of the transfer function (Linear, Logarithmic, ...)
|
46
|
+
def colorimetric
|
47
|
+
Depix::COLORIMETRIC.invert[image.image_elements[0].colorimetric]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Get the name of the compnent type (RGB, YCbCr, ...)
|
51
|
+
def component_type
|
52
|
+
Depix::COMPONENT_TYPE.invert[image.image_elements[0].descriptor]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Aspect in it's traditional representation (1.77 for 16x9 and so on)
|
56
|
+
def aspect
|
57
|
+
"%.2f" % (orientation.aspect_ratio[0].to_f / orientation.aspect_ratio[1].to_f)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Is this DPX file little-endian?
|
61
|
+
def le?
|
62
|
+
file.magic == 'XPDS'
|
63
|
+
end
|
64
|
+
end
|
data/test/test_depix.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/../lib/depix'
|
1
|
+
require File.expand_path(File.dirname(__FILE__)) + '/../lib/depix'
|
2
2
|
require 'test/unit'
|
3
3
|
require "fileutils"
|
4
4
|
|
@@ -84,22 +84,19 @@ class ReaderTest < Test::Unit::TestCase
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def test_describe
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
assert_match(/Offset to data for this image element/, desc)
|
91
|
-
end
|
87
|
+
desc = Depix.describe_file(SAMPLE_DPX)
|
88
|
+
assert_match(/320/, desc)
|
89
|
+
assert_match(/Offset to data for this image element/, desc)
|
92
90
|
end
|
93
91
|
|
94
92
|
def test_packing
|
95
93
|
original_header = File.read(SAMPLE_DPX)[0...Depix::DPX.length]
|
96
94
|
|
97
|
-
|
98
|
-
|
99
|
-
|
95
|
+
# If these do not raise anything we are good on the encodings front
|
96
|
+
dpx = Depix.from_string(original_header)
|
97
|
+
packed = Depix::DPX.pack(dpx, original_header.dup)
|
98
|
+
dpx2 = Depix.from_string(packed)
|
100
99
|
|
101
|
-
dpx2 = Depix.from_string(packed)
|
102
|
-
end
|
103
100
|
end
|
104
101
|
|
105
102
|
def test_parsing_something_else_should_raise
|