depix 1.1.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 < Dict
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
@@ -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
- # The hear of Depix
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?(Dict) ? describe_struct(v, pad_offset + 2) : v }
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
@@ -1,9 +1,6 @@
1
- require File.dirname(__FILE__) + '/dict'
2
-
3
-
4
1
  module Depix
5
2
 
6
- class FileInfo < Dict
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 < Dict
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 < Dict
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 < Dict
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 < Dict
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 < Dict
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 < Dict
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
- #:include:DPX_HEADER_STRUCTURE.txt
138
- class DPX < Dict
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
@@ -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
- assert_nothing_raised do
88
- desc = Depix.describe_file(SAMPLE_DPX)
89
- assert_match(/320/, desc)
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
- assert_nothing_raised do
98
- dpx = Depix.from_string(original_header)
99
- packed = Depix::DPX.pack(dpx, original_header.dup)
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