julik-depix 1.0.2

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,59 @@
1
+ # Generates an RDoc description of the DPX structs from the structs.rb file
2
+ class RdocExplainer #:nodoc:
3
+ attr_accessor :io, :attr_template, :struct_template
4
+
5
+ TPL = <<eof
6
+ = DPX header structure description
7
+
8
+ DPX metadata gets returned as a Depix::DPX object with nested properties.
9
+
10
+ meta.file.magic # => "SDPX"
11
+
12
+ == Metadata structure
13
+
14
+ %s
15
+ eof
16
+
17
+ def initialize
18
+ @padding = ' '
19
+ @attr_template = "%s* <tt>%s</tt> %s"
20
+ @struct_template = "%s* <tt>%s</tt> %s:"
21
+ @array_template = "%s* <tt>%s</tt> %s:"
22
+
23
+ end
24
+
25
+ def get_rdoc_for(struct)
26
+ @io = StringIO.new
27
+ explain_struct(struct)
28
+ TPL % @io.string
29
+ end
30
+
31
+ include Depix
32
+
33
+ def explain_struct(struct, padding = '') #:nodoc:
34
+ struct.fields.each do | e |
35
+ if e.is_a?(InnerField)
36
+
37
+ @io.puts( @struct_template % [padding, e.name, e.explain])
38
+ explain_struct(e.rtype, padding + @padding)
39
+
40
+ elsif e.is_a?(ArrayField)
41
+
42
+ @io.puts( @array_template % [padding, e.name, e.explain])
43
+
44
+ inner_struct = e.members[0]
45
+
46
+ if inner_struct.is_a?(InnerField)
47
+ explain_struct(inner_struct.rtype, padding + @padding)
48
+ end
49
+ else
50
+ explain_attr(padding, e)
51
+ end
52
+ end
53
+ end
54
+
55
+ def explain_attr(padding, e)
56
+ type_name = e.rtype ? "(#{e.rtype})" : nil
57
+ @io.puts( @attr_template % [padding, e.name, e.explain])
58
+ end
59
+ end
@@ -0,0 +1,137 @@
1
+ require File.dirname(__FILE__) + '/dict'
2
+
3
+
4
+ module Depix
5
+
6
+ class FileInfo < Dict
7
+ char :magic, 4, :desc => 'Endianness (SDPX is big endian)', :req => true
8
+ u32 :image_offset, :desc => 'Offset to image data in bytes', :req => true
9
+ char :version, 8, :desc => 'Version of header format', :req => true
10
+
11
+ u32 :file_size, :desc => "Total image size in bytes", :req => true
12
+ u32 :ditto_key, :desc => 'Whether the basic headers stay the same through the sequence (1 means they do)'
13
+ u32 :generic_size, :desc => 'Generic header length'
14
+ u32 :industry_size, :desc => 'Industry header length'
15
+ u32 :user_size, :desc => 'User header length'
16
+
17
+ char :filename, 100, :desc => 'Original filename'
18
+ char :timestamp, 24, :desc => 'Creation 15'
19
+ char :creator, 100, :desc => 'Creator application'
20
+ char :roject, 200, :desc => 'Project name'
21
+ char :copyright, 200, :desc => 'Copyright'
22
+
23
+ u32 :encrypt_key, :desc => 'Encryption key'
24
+ char :reserve, 104
25
+ end
26
+
27
+ class FilmInfo < Dict
28
+ char :id, 2, :desc => 'Film mfg. ID code (2 digits from film edge code)'
29
+ char :type, 2, :desc => 'Film type (2 digits from film edge code)'
30
+ char :offset, 2, :desc => 'Offset in perfs (2 digits from film edge code)'
31
+ char :prefix, 6, :desc => 'Prefix (6 digits from film edge code'
32
+ char :count, 4, :desc => 'Count (4 digits from film edge code)'
33
+ char :format, 32, :desc => 'Format (e.g. Academy)'
34
+
35
+ u32 :frame_position, :desc => 'Frame position in sequence'
36
+ u32 :sequence_extent, :desc => 'Sequence length'
37
+ u32 :held_count, :desc => 'For how many frames the frame is held'
38
+
39
+ r32 :frame_rate, :desc => 'Frame rate'
40
+ r32 :shutter_angle, :desc => 'Shutter angle'
41
+
42
+ char :frame_id, 32, :desc => 'Frame identification (keyframe)'
43
+ char :slate, 100, :desc => 'Slate information'
44
+ char :reserve, 56
45
+ end
46
+
47
+ class ImageElement < Dict
48
+ u32 :data_sign, :desc => 'Data sign (0=unsigned, 1=signed). Core is unsigned', :req => true
49
+
50
+ u32 :low_data, :desc => 'Reference low data code value'
51
+ r32 :low_quantity, :desc => 'Reference low quantity represented'
52
+ u32 :high_data, :desc => 'Reference high data code value (1023 for 10bit per channel)'
53
+ r32 :high_quantity, :desc => 'Reference high quantity represented'
54
+
55
+ # TODO: Autoreplace with enum values.
56
+ u8 :descriptor, :desc => 'Descriptor for this image element (ie Video or Film), by enum', :req => true
57
+ u8 :transfer, :desc => 'Transfer function (ie Linear), by enum', :req => true
58
+ u8 :colorimetric, :desc => 'Colorimetric (ie YcbCr), by enum', :req => true
59
+ u8 :bit_size, :desc => 'Bit size for element (ie 10)', :req => true
60
+
61
+ u16 :packing, :desc => 'Packing (0=Packed into 32-bit words, 1=Filled to 32-bit words))', :req => true
62
+ u16 :encoding, :desc => "Encoding (0=None, 1=RLE)", :req => true
63
+ u32 :data_offset, :desc => 'Offset to data for this image element', :req => true
64
+ u32 :end_of_line_padding, :desc => "End-of-line padding for this image element"
65
+ u32 :end_of_image_padding, :desc => "End-of-line padding for this image element"
66
+ char :description, 32
67
+ end
68
+
69
+ class OrientationInfo < Dict
70
+
71
+ u32 :x_offset
72
+ u32 :y_offset
73
+
74
+ r32 :x_center
75
+ r32 :y_center
76
+
77
+ u32 :x_size, :desc => 'Original X size'
78
+ u32 :y_size, :desc => 'Original Y size'
79
+
80
+ char :filename, 100, :desc => "Source image filename"
81
+ char :timestamp, 24, :desc => "Source image/tape timestamp"
82
+ char :device, 32, :desc => "Input device or tape"
83
+ char :serial, 32, :desc => "Input device serial number"
84
+
85
+ array :border, :u16, 4, :desc => 'Border validity: XL, XR, YT, YB'
86
+ array :aspect_ratio , :u32, 2, :desc => "Aspect (H:V)"
87
+
88
+ char :reserve, 28
89
+ end
90
+
91
+ class TelevisionInfo < Dict
92
+ u32 :time_code, :desc => "Timecode, formatted as HH:MM:SS:FF in the 4 higher bits of each 8bit group"
93
+ u32 :user_bits, :desc => "Timecode UBITs"
94
+ u8 :interlace, :desc => "Interlace (0 = noninterlaced; 1 = 2:1 interlace"
95
+
96
+ u8 :field_number, :desc => 'Field number'
97
+ u8 :video_signal, :desc => "Video signal (by enum)"
98
+ u8 :padding, :desc => "Zero (for byte alignment)"
99
+
100
+ r32 :horizontal_sample_rate, :desc => 'Horizontal sampling Hz'
101
+ r32 :vertical_sample_rate, :desc => 'Vertical sampling Hz'
102
+ r32 :frame_rate, :desc => 'Frame rate'
103
+ r32 :time_offset, :desc => 'From sync pulse to first pixel'
104
+ r32 :gamma, :desc => 'Gamma'
105
+ r32 :black_level, :desc => 'Black pedestal code value'
106
+ r32 :black_gain, :desc => 'Black gain code value'
107
+ r32 :break_point, :desc => 'Break point (?)'
108
+ r32 :white_level, :desc => 'White level'
109
+ r32 :integration_times, :desc => 'Integration times (S)'
110
+ r32 :reserve
111
+ end
112
+
113
+ class UserInfo < Dict
114
+ char :id, 32, :desc => 'Name of the user data tag'
115
+ u32 :user_data_ptr
116
+ end
117
+
118
+ class ImageInfo < Dict
119
+ u16 :orientation, OrientationInfo, :desc => 'Orientation descriptor', :req => true
120
+ u16 :number_elements, :desc => 'How many elements to scan', :req => true
121
+
122
+ u32 :pixels_per_line, :desc => 'Pixels per horizontal line', :req => true
123
+ u32 :lines_per_element, :desc => 'Line count', :req => true
124
+ array :image_elements, ImageElement, 8, :desc => "Image elements"
125
+ char :reserve, 52
126
+ end
127
+
128
+ #:include:DPX_HEADER_STRUCTURE.txt
129
+ class DPX < Dict
130
+ inner :file, FileInfo, :desc => "File information"
131
+ inner :image, ImageInfo, :desc => "Image information"
132
+ inner :orientation, OrientationInfo, :desc => "Orientation"
133
+ inner :film, FilmInfo, :desc => "Film industry info"
134
+ inner :television, TelevisionInfo, :desc => "TV industry info"
135
+ inner :user, UserInfo, :desc => "User info"
136
+ end
137
+ end
data/lib/depix.rb ADDED
@@ -0,0 +1,142 @@
1
+ require 'stringio'
2
+ require 'rubygems'
3
+ require 'timecode'
4
+
5
+ require File.dirname(__FILE__) + '/depix/dict'
6
+ require File.dirname(__FILE__) + '/depix/structs'
7
+ require File.dirname(__FILE__) + '/depix/compact_structs'
8
+ require File.dirname(__FILE__) + '/depix/enums'
9
+
10
+ module Depix
11
+ VERSION = '1.0.3'
12
+
13
+ class InvalidHeader < RuntimeError; end
14
+
15
+ # Offers convenience access to a few common attributes bypassing the piecemeal structs
16
+ module Synthetics
17
+ def keycode
18
+ [film.id, film.type, film.offset, film.prefix, film.count].compact.join(' ')
19
+ end
20
+
21
+ # Return the flame reel name. The data after the first null byte is not meant to be seen and is used by Flame internally
22
+ # as it seems
23
+ def flame_reel
24
+ orientation.device.split("\000").shift
25
+ end
26
+
27
+ def time_code
28
+ Timecode.from_uint(television.time_code) #, film.frame_rate)
29
+ end
30
+
31
+ # Get the name of the transfer function (Linear, Logarithmic, ...)
32
+ def colorimetric
33
+ COLORIMETRIC.invert[image.image_elements[0].colorimetric]
34
+ end
35
+
36
+ # Get the name of the compnent type (RGB, YCbCr, ...)
37
+ def component_type
38
+ COMPONENT_TYPE.invert[image.image_elements[0].descriptor]
39
+ end
40
+
41
+ # Is this DPX file little-endian? This would be an exception, but still useful
42
+ def le?
43
+ file.magic == 'XPDS'
44
+ end
45
+ end
46
+
47
+ class DPX < Dict
48
+ include Synthetics
49
+ end
50
+
51
+ # Return a DPX object describing a file at path.
52
+ # The second argument specifies whether you need a compact or a full description
53
+ def self.from_file(path, compact = false)
54
+ Reader.new.from_file(path, compact)
55
+ end
56
+
57
+ # Return a DPX object describing headers embedded at the start of the string.
58
+ # The second argument specifies whether you need a compact or a full description
59
+ def self.from_string(string, compact = false)
60
+ Reader.new.parse(string, compact)
61
+ end
62
+
63
+ # Retrurn a formatted description of the DPX file at path. Empty values are omitted.
64
+ def self.describe_file(path, compact = false)
65
+ Reader.new.describe_file(path, compact)
66
+ end
67
+
68
+ class Reader
69
+
70
+ # Returns a printable report on all the headers present in the file at the path passed
71
+ def describe_file(path, compact = false)
72
+ header = File.open(path, 'r') { |f| f.read(DPX.length) }
73
+ describe_struct(parse(header, false))
74
+ end
75
+
76
+ def from_file(path, compact)
77
+ header = File.open(path, 'r') { |f| f.read(DPX.length) }
78
+ begin
79
+ parse(header, compact)
80
+ rescue InvalidHeader => e
81
+ raise InvalidHeader, "Invalid header in file #{path}"
82
+ end
83
+ end
84
+
85
+ # The hear of Depix
86
+ def parse(data, compact)
87
+ magic = data[0..3]
88
+
89
+ raise InvalidHeader unless %w( SDPX XPDS).include?(magic)
90
+
91
+ struct = compact ? CompactDPX : DPX
92
+
93
+ is_be = (magic == "SDPX")
94
+ version_check = FileInfo.only(:magic, :version)
95
+
96
+ result = begin
97
+ if is_be
98
+ version_check.consume!(data.unpack(version_check.pattern))
99
+ else
100
+ version_check.consume!(data.unpack(make_le(version_check.pattern)))
101
+ end
102
+ rescue ArgumentError
103
+ raise InvalidHeader
104
+ end
105
+
106
+ raise InvalidHeader unless result.version == "V1.0"
107
+
108
+ template = is_be ? DPX.pattern : make_le(DPX.pattern)
109
+ struct.consume!(data.unpack(struct.pattern))
110
+ end
111
+
112
+ # Describe a filled DPX structure
113
+ def describe_struct(result, pad_offset = 0)
114
+ result.class.fields.inject([]) do | info, field |
115
+ value = result.send(field.name)
116
+ parts = []
117
+ if value
118
+ parts << field.desc if field.desc
119
+ parts << if field.is_a?(InnerField)
120
+ describe_struct(value, pad_offset + 1)
121
+ elsif field.is_a?(ArrayField)
122
+ # Exception for image elements
123
+ value = result.image_elements[0...result.number_elements] if field.name == :image_elements
124
+ value.map { | v | v.is_a?(Dict) ? describe_struct(v, pad_offset + 2) : v }
125
+ else
126
+ value
127
+ end
128
+ end
129
+ if parts.any?
130
+ info << parts.join(' ')
131
+ end
132
+ info
133
+ end.map{|e| (' ' * pad_offset) + e }.join("\n")
134
+ end
135
+
136
+ # Convert an unpack pattern to LE
137
+ def make_le(pattern)
138
+ pattern.gsub(/n/, "v").gsub(/N/, "V").gsub(/g/, "f")
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,98 @@
1
+ require File.dirname(__FILE__) + '/../lib/depix'
2
+ require 'test/unit'
3
+
4
+ class ReaderTest < Test::Unit::TestCase
5
+
6
+ SAMPLE_DPX = File.dirname(__FILE__) + '/samples/E012_P001_L000002_lin.0001.dpx'
7
+
8
+ def test_parsed_properly
9
+ file = SAMPLE_DPX
10
+ parsed = Depix.from_file(file)
11
+ assert_equal "SDPX", parsed.file.magic
12
+ assert_equal 8192, parsed.file.image_offset
13
+ assert_equal "V1.0", parsed.file.version
14
+ assert_equal 319488, parsed.file.file_size
15
+ assert_equal 1, parsed.file.ditto_key
16
+ assert_equal 1664, parsed.file.generic_size
17
+ assert_equal 384, parsed.file.industry_size
18
+ assert_equal 6144, parsed.file.user_size
19
+ assert_equal "E012_P001_L000002_lin.0001.dpx", parsed.file.filename
20
+ assert_equal "2008:12:19:01:18:37:CEST", parsed.file.timestamp
21
+ assert_equal "UTODESK", parsed.file.creator
22
+ assert_equal 0, parsed.image.orientation
23
+ assert_equal 1, parsed.image.number_elements
24
+ assert_equal 320, parsed.image.pixels_per_line
25
+ assert_equal 240, parsed.image.lines_per_element
26
+ assert_equal 0, parsed.image.image_elements[0].data_sign
27
+ assert_equal 0, parsed.image.image_elements[0].low_data
28
+ assert_equal 0.0, parsed.image.image_elements[0].low_quantity
29
+ assert_equal 1023, parsed.image.image_elements[0].high_data
30
+ assert_in_delta 2.04699993133545, parsed.image.image_elements[0].high_quantity, 1.0 ** -10
31
+
32
+ assert_equal 50, parsed.image.image_elements[0].descriptor # RGB :-)
33
+ assert_equal 2, parsed.image.image_elements[0].transfer
34
+ assert_equal 2, parsed.image.image_elements[0].colorimetric
35
+ assert_equal 10, parsed.image.image_elements[0].bit_size
36
+ assert_equal 1, parsed.image.image_elements[0].packing
37
+ assert_equal 0, parsed.image.image_elements[0].encoding
38
+ assert_equal 8192, parsed.image.image_elements[0].data_offset
39
+ assert_equal 0, parsed.image.image_elements[0].end_of_line_padding
40
+ assert_equal 0, parsed.image.image_elements[0].end_of_image_padding
41
+ assert_equal "IMAGE DESCRIPTION DATA \000P", parsed.image.image_elements[0].description
42
+ assert_equal "E012\000\000\000\000x\340\264\020\000\000\000\000\005",
43
+ parsed.orientation.device #- this is where Flame writes the reel
44
+
45
+ assert_equal 853, parsed.orientation.aspect_ratio[0]
46
+ assert_equal 640, parsed.orientation.aspect_ratio[1]
47
+
48
+ assert_equal '75', parsed.film.id
49
+ assert_equal '00', parsed.film.type
50
+ assert_equal '19', parsed.film.offset
51
+ assert_equal '740612', parsed.film.prefix
52
+ assert_equal '9841', parsed.film.count
53
+ assert_equal 1, parsed.film.frame_position
54
+ assert_equal 2, parsed.film.sequence_extent
55
+ assert_equal 1, parsed.film.held_count
56
+ assert_equal 25.0, parsed.film.frame_rate
57
+ assert_equal 18157848, parsed.television.time_code
58
+ assert_equal 0, parsed.television.user_bits
59
+ end
60
+
61
+ def test_syntethics
62
+ assert_nothing_raised { Depix::Synthetics }
63
+
64
+ file = SAMPLE_DPX
65
+ parsed = Depix.from_file(file)
66
+ assert_equal false, parsed.le?
67
+ assert_equal "75 00 19 740612 9841", parsed.keycode
68
+ assert_equal "01:15:11:18", parsed.time_code.to_s
69
+ assert_equal :RGB, parsed.component_type
70
+ assert_equal :Linear, parsed.colorimetric
71
+ assert_equal "E012", parsed.flame_reel
72
+ end
73
+
74
+ def test_parsed_properly_using_compact_structs
75
+ file = SAMPLE_DPX
76
+ assert_nothing_raised { Depix.from_file(file, compact = true) }
77
+ end
78
+
79
+ def test_describe
80
+ assert_nothing_raised do
81
+ desc = Depix.describe_file(SAMPLE_DPX)
82
+ assert_match(/320/, desc)
83
+ assert_match(/Offset to data for this image element/, desc)
84
+ end
85
+ end
86
+
87
+ def test_parsing_something_else_should_raise
88
+ s = "Mary had a little lamb"
89
+ assert_raise(Depix::InvalidHeader) { Depix.from_string(s) }
90
+
91
+ s = "Mary had a little lamb" * 1000
92
+ assert_raise(Depix::InvalidHeader) { Depix.from_string(s) }
93
+
94
+ s = "SDPX Mary had a little lamb" * 1000
95
+ assert_raise(Depix::InvalidHeader) { Depix.from_string(s) }
96
+
97
+ end
98
+ end