depix 1.1.6 → 2.0.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.
@@ -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