cinesync 0.9.6
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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.markdown +53 -0
- data/Rakefile +59 -0
- data/Samples/Export Notes to CSV.rb +33 -0
- data/VERSION +1 -0
- data/cineSync Session v3 Schema.rnc +164 -0
- data/cinesync.gemspec +76 -0
- data/lib/cinesync/color_grading.rb +70 -0
- data/lib/cinesync/event_handler.rb +67 -0
- data/lib/cinesync/frame_annotation.rb +19 -0
- data/lib/cinesync/mask.rb +41 -0
- data/lib/cinesync/media_file.rb +122 -0
- data/lib/cinesync/pixel_ratio.rb +22 -0
- data/lib/cinesync/play_range.rb +21 -0
- data/lib/cinesync/session.rb +24 -0
- data/lib/cinesync/ui/standard_additions.rb +473 -0
- data/lib/cinesync/ui/win32_save_file_dialog.rb +95 -0
- data/lib/cinesync/ui.rb +68 -0
- data/lib/cinesync/xml.rb +436 -0
- data/lib/cinesync/zoom_state.rb +18 -0
- data/lib/cinesync.rb +46 -0
- data/test/helper.rb +10 -0
- metadata +128 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'dl'
|
2
|
+
|
3
|
+
|
4
|
+
class CineSync::UI::Win32SaveFileDialog
|
5
|
+
OFN_READONLY = 0x00000001
|
6
|
+
OFN_OVERWRITEPROMPT = 0x00000002
|
7
|
+
OFN_HIDEREADONLY = 0x00000004
|
8
|
+
OFN_NOCHANGEDIR = 0x00000008
|
9
|
+
OFN_SHOWHELP = 0x00000010
|
10
|
+
OFN_ENABLEHOOK = 0x00000020
|
11
|
+
OFN_ENABLETEMPLATE = 0x00000040
|
12
|
+
OFN_ENABLETEMPLATEHANDLE = 0x00000080
|
13
|
+
OFN_NOVALIDATE = 0x00000100
|
14
|
+
OFN_ALLOWMULTISELECT = 0x00000200
|
15
|
+
OFN_EXTENSIONDIFFERENT = 0x00000400
|
16
|
+
OFN_PATHMUSTEXIST = 0x00000800
|
17
|
+
OFN_FILEMUSTEXIST = 0x00001000
|
18
|
+
OFN_CREATEPROMPT = 0x00002000
|
19
|
+
OFN_SHAREAWARE = 0x00004000
|
20
|
+
OFN_NOREADONLYRETURN = 0x00008000
|
21
|
+
OFN_NOTESTFILECREATE = 0x00010000
|
22
|
+
OFN_NONETWORKBUTTON = 0x00020000
|
23
|
+
OFN_NOLONGNAMES = 0x00040000
|
24
|
+
OFN_EXPLORER = 0x00080000
|
25
|
+
OFN_NODEREFERENCELINKS = 0x00100000
|
26
|
+
OFN_LONGNAMES = 0x00200000
|
27
|
+
OFN_ENABLEINCLUDENOTIFY = 0x00400000
|
28
|
+
OFN_ENABLESIZING = 0x00800000
|
29
|
+
OFN_DONTADDTORECENT = 0x02000000
|
30
|
+
OFN_FORCESHOWHIDDEN = 0x10000000
|
31
|
+
OFN_EX_NOPLACESBAR = 0x00000001
|
32
|
+
PROTOTYPE = 'LIISSLLSLSLSSLHHSIPS'
|
33
|
+
MAX_PATH = 260
|
34
|
+
|
35
|
+
def initialize(opts = {})
|
36
|
+
@opts = {:title => "", :default_name => "", :extension => nil, :file_type => nil}.merge(opts)
|
37
|
+
@opts[:file_type] ||= "#{opts[:extension].upcase} file"
|
38
|
+
|
39
|
+
@ofn = DL.malloc(DL.sizeof(PROTOTYPE))
|
40
|
+
@ofn.struct!(PROTOTYPE,
|
41
|
+
:lStructSize,
|
42
|
+
:hwndOwner,
|
43
|
+
:hInstance,
|
44
|
+
:lpstrFilter,
|
45
|
+
:lpstrCustomFilter,
|
46
|
+
:nMaxCustFilter,
|
47
|
+
:nFilterIndex,
|
48
|
+
:lpstrFile,
|
49
|
+
:nMaxFile,
|
50
|
+
:lpstrFileTitle,
|
51
|
+
:nMaxFileTitle,
|
52
|
+
:lpstrInitialDir,
|
53
|
+
:lpstrTitle,
|
54
|
+
:Flags,
|
55
|
+
:nFileOffset,
|
56
|
+
:nFileExtension,
|
57
|
+
:lpstrDefExt,
|
58
|
+
:lCustData,
|
59
|
+
:lpfnHook,
|
60
|
+
:lpTemplateName)
|
61
|
+
|
62
|
+
@ofn[:lStructSize] = DL.sizeof(PROTOTYPE)
|
63
|
+
@ofn[:hwndOwner] = 0
|
64
|
+
@ofn[:lpstrFile] = DL.malloc(1024)
|
65
|
+
@ofn[:lpstrFile] = @opts[:default_name] + "\0"
|
66
|
+
@ofn[:nMaxFile] = MAX_PATH
|
67
|
+
if @opts[:extension]
|
68
|
+
ext = "*#{@opts[:extension]}"
|
69
|
+
@ofn[:lpstrFilter] = "#{@opts[:file_type]} (#{ext})\0#{ext}\0"
|
70
|
+
else
|
71
|
+
@ofn[:lpstrFilter] = "\0"
|
72
|
+
end
|
73
|
+
@ofn[:nFilterIndex] = 1
|
74
|
+
@ofn[:lpstrTitle] = @opts[:title] + "\0"
|
75
|
+
@ofn[:lpstrFileTitle] = "\0"
|
76
|
+
@ofn[:nMaxFileTitle] = 0
|
77
|
+
@ofn[:lpstrInitialDir] = "\0"
|
78
|
+
@ofn[:Flags] = OFN_OVERWRITEPROMPT
|
79
|
+
|
80
|
+
@@comdlg32 ||= DL.dlopen('comdlg32.dll')
|
81
|
+
@GetSaveFileName = @@comdlg32['GetSaveFileName','IP']
|
82
|
+
end
|
83
|
+
|
84
|
+
def execute!
|
85
|
+
if @GetSaveFileName.call(@ofn)[0]
|
86
|
+
@ofn[:lpstrFile].to_s
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def filename
|
93
|
+
@ofn[:lpstrFile].to_s
|
94
|
+
end
|
95
|
+
end
|
data/lib/cinesync/ui.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module CineSync
|
2
|
+
module UI
|
3
|
+
def self.open_url(url)
|
4
|
+
case RUBY_PLATFORM
|
5
|
+
when /darwin/
|
6
|
+
system('open', url)
|
7
|
+
when /mswin32|mingw32/
|
8
|
+
system('cmd', '/c', 'start', '', '/b', url) # Of course
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.show_dialog(msg)
|
13
|
+
puts msg
|
14
|
+
case RUBY_PLATFORM
|
15
|
+
when /darwin/
|
16
|
+
mac_osax.display_alert("cineSync Script", :message => msg) if mac_osax
|
17
|
+
when /mswin32|mingw32/
|
18
|
+
require 'dl'
|
19
|
+
user32 = DL.dlopen('user32')
|
20
|
+
msgbox = user32['MessageBoxA', 'ILSSI']
|
21
|
+
msgbox.call(0, msg, "cineSync Script", 0x41000) # MB_TOPMOST | MB_SYSTEMMODAL
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.prompt_to_save(prompt, filename)
|
26
|
+
default_path = File.expand_path("~/Desktop/") + filename
|
27
|
+
default_new_path = default_path unless File.exist?(default_path)
|
28
|
+
|
29
|
+
case RUBY_PLATFORM
|
30
|
+
when /darwin/
|
31
|
+
if mac_osax
|
32
|
+
begin
|
33
|
+
val = mac_osax.choose_file_name(:with_prompt => prompt, :default_name => filename)
|
34
|
+
String(val)
|
35
|
+
rescue Appscript::CommandError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
else
|
39
|
+
# Default to the Desktop
|
40
|
+
puts "Unable to create scripting object; using Desktop as save location."
|
41
|
+
default_new_path
|
42
|
+
end
|
43
|
+
when /mswin32|mingw32/
|
44
|
+
require 'cinesync/ui/win32_save_file_dialog'
|
45
|
+
opts = {:title => prompt}
|
46
|
+
opts[:default_name] = filename
|
47
|
+
opts[:extension] = File.extname(filename)
|
48
|
+
dlg = CineSync::UI::Win32SaveFileDialog.new(opts)
|
49
|
+
dlg.execute!
|
50
|
+
else
|
51
|
+
# Default to the Desktop
|
52
|
+
default_new_path
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def self.mac_osax
|
58
|
+
begin
|
59
|
+
require 'appscript'
|
60
|
+
require 'osax'
|
61
|
+
require 'cinesync/ui/standard_additions' # Dumped terminology for 64-bit
|
62
|
+
@mac_osax ||= OSAX::ScriptingAddition.new("StandardAdditions", CineSync::UI::StandardAdditions).by_name("cineSync")
|
63
|
+
rescue
|
64
|
+
$stderr.puts "Unable to create scripting object. Check that rb-appscript gem is installed."
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/cinesync/xml.rb
ADDED
@@ -0,0 +1,436 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'andand'
|
3
|
+
require 'builder'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
|
8
|
+
module CineSync
|
9
|
+
SessionV3Namespace = 'http://www.cinesync.com/ns/session/3.0'
|
10
|
+
|
11
|
+
class Session
|
12
|
+
# eSession = element session {
|
13
|
+
# attribute version { xsd:integer { minInclusive = "3" } } &
|
14
|
+
# attribute sessionFeatures { "standard" | "pro" } &
|
15
|
+
# aUserData? &
|
16
|
+
# eGroup* &
|
17
|
+
# eNotes? &
|
18
|
+
# eChat? &
|
19
|
+
# eMedia* }
|
20
|
+
|
21
|
+
def to_xml
|
22
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
23
|
+
|
24
|
+
x = Builder::XmlMarkup.new(:indent => 4)
|
25
|
+
x.instruct!
|
26
|
+
|
27
|
+
attrs = {}
|
28
|
+
attrs['xmlns'] = SessionV3Namespace
|
29
|
+
# Always write as the version we know, not the version we loaded
|
30
|
+
attrs['version'] = SessionV3XMLFileVersion
|
31
|
+
attrs['sessionFeatures'] = session_features
|
32
|
+
attrs['userData'] = user_data unless user_data.empty?
|
33
|
+
|
34
|
+
x.session(attrs) {
|
35
|
+
groups.each {|g| x.group(g) }
|
36
|
+
x.notes(notes) unless notes.empty?
|
37
|
+
x << chat_elem.to_s if chat_elem
|
38
|
+
x << stereo_elem.to_s if stereo_elem
|
39
|
+
media.each {|m| m.to_xml(x) }
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def self.load(str_or_io, silent = false)
|
45
|
+
doc = REXML::Document.new(str_or_io)
|
46
|
+
|
47
|
+
# Do a few checks (the user should have already confirmed that the
|
48
|
+
# document conforms to the schema, but we'll try to fail early in case)
|
49
|
+
fail 'Expected to find root <session> element' unless doc.root.name == 'session'
|
50
|
+
fail %Q[Root <session> element must have attribute xmlns="#{SessionV3Namespace}"] unless doc.root.attribute('xmlns').value == SessionV3Namespace
|
51
|
+
|
52
|
+
doc_version = doc.root.attribute('version').value.to_i
|
53
|
+
if doc_version > SessionV3XMLFileVersion
|
54
|
+
$stderr.puts("Warning: Loading session file with a newer version (#{doc_version}) than this library (#{SessionV3XMLFileVersion})") unless silent
|
55
|
+
end
|
56
|
+
|
57
|
+
elem = doc.root
|
58
|
+
|
59
|
+
returning self.new do |s|
|
60
|
+
s.instance_variable_set(:@file_version, doc_version)
|
61
|
+
|
62
|
+
s.user_data = elem.attribute('userData').andand.value || ''
|
63
|
+
s.groups = elem.get_elements('group').map {|g_elem| g_elem.text }
|
64
|
+
s.notes = elem.elements['notes'].andand.text || ''
|
65
|
+
s.chat_elem = elem.get_elements('chat').andand[0]
|
66
|
+
s.stereo_elem = elem.get_elements('stereo').andand[0]
|
67
|
+
s.media = elem.get_elements('media').map {|e| MediaBase.load(e) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
class MediaBase
|
74
|
+
# MediaBase =
|
75
|
+
# aUserData? &
|
76
|
+
# attribute active { tBool }? &
|
77
|
+
# attribute currentFrame { tFrameNumber }? &
|
78
|
+
# eGroup* &
|
79
|
+
# ePlayRange?
|
80
|
+
|
81
|
+
def to_xml(x)
|
82
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
83
|
+
|
84
|
+
attrs = {}
|
85
|
+
attrs['userData'] = user_data unless user_data.empty?
|
86
|
+
attrs['active'] = active? if active?
|
87
|
+
attrs['currentFrame'] = current_frame if current_frame != 1
|
88
|
+
|
89
|
+
x.media(attrs) {
|
90
|
+
yield x
|
91
|
+
groups.each {|g| x.group(g) }
|
92
|
+
play_range.to_xml(x)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def self.load(elem)
|
98
|
+
klass = elem.elements['groupMovie'] ? GroupMovie : MediaFile
|
99
|
+
klass.load(elem)
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
def self.common_load(elem, inst)
|
104
|
+
returning inst do |m|
|
105
|
+
m.user_data = elem.attribute('userData').andand.value || ''
|
106
|
+
m.active = elem.attribute('active').andand.value == 'true'
|
107
|
+
m.current_frame = elem.attribute('currentFrame').andand.value.andand.to_i || 1
|
108
|
+
m.groups = elem.get_elements('group').map {|g_elem| g_elem.text }
|
109
|
+
|
110
|
+
play_range_elem = elem.elements['playRange']
|
111
|
+
m.play_range = PlayRange.load(play_range_elem) if play_range_elem
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
class MediaFile < MediaBase
|
118
|
+
# eMedia |= element media {
|
119
|
+
# # Normal media file
|
120
|
+
# MediaBase &
|
121
|
+
# element name { xsd:string { minLength = "1" } } &
|
122
|
+
# element locators { eLocator+ } &
|
123
|
+
# eNotes? &
|
124
|
+
# eZoomState? &
|
125
|
+
# ePixelRatio? &
|
126
|
+
# eMask? &
|
127
|
+
# eColorGrading? &
|
128
|
+
# eFrameAnnotation* }
|
129
|
+
|
130
|
+
def to_xml(x)
|
131
|
+
super(x) do |x|
|
132
|
+
x.name(name)
|
133
|
+
locator.to_xml(x)
|
134
|
+
x.notes(notes) unless notes.empty?
|
135
|
+
|
136
|
+
zoom_state.to_xml(x)
|
137
|
+
pixel_ratio.to_xml(x)
|
138
|
+
mask.to_xml(x)
|
139
|
+
color_grading.to_xml(x)
|
140
|
+
|
141
|
+
annotations.values.each {|ann| ann.to_xml(x) }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def self.load(elem)
|
147
|
+
returning self.new do |m|
|
148
|
+
common_load(elem, m)
|
149
|
+
|
150
|
+
m.name = elem.elements['name'].text
|
151
|
+
m.locator = MediaLocator.load(elem.elements['locators'])
|
152
|
+
m.notes = elem.elements['notes'].andand.text || ''
|
153
|
+
|
154
|
+
elem.get_elements('annotation').each do |ann_elem|
|
155
|
+
FrameAnnotation.load(ann_elem, m)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Load optional structures
|
159
|
+
%w(zoomState pixelRatio mask colorGrading).each do |camel_name|
|
160
|
+
set_sym = (camel_name + '=').underscore.to_sym
|
161
|
+
e = elem.elements[camel_name]
|
162
|
+
next if e.nil?
|
163
|
+
|
164
|
+
obj = ('CineSync::' + camel_name.camelize).constantize.load(e)
|
165
|
+
m.send(set_sym, obj)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
class GroupMovie < MediaBase
|
173
|
+
# eMedia |= element media {
|
174
|
+
# # Group movie
|
175
|
+
# MediaBase &
|
176
|
+
# element groupMovie { eGroup } }
|
177
|
+
|
178
|
+
def to_xml(x)
|
179
|
+
super(x) do |x|
|
180
|
+
x.groupMovie {
|
181
|
+
x.group(group)
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
def self.load(elem)
|
188
|
+
group = elem.elements['groupMovie/group'].text
|
189
|
+
returning self.new(group) do |m|
|
190
|
+
common_load(elem, m)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
class MediaLocator
|
197
|
+
# eLocator |= element path { tFilePath }
|
198
|
+
# eLocator |= element shortHash { tShortHash }
|
199
|
+
# eLocator |= element url { tURL }
|
200
|
+
|
201
|
+
def to_xml(x)
|
202
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
203
|
+
|
204
|
+
x.locators {
|
205
|
+
x.path(path) if path
|
206
|
+
x.shortHash(short_hash) if short_hash
|
207
|
+
x.url(String(url)) if url
|
208
|
+
}
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
def self.load(elem)
|
213
|
+
returning self.new do |loc|
|
214
|
+
loc.path = elem.elements['path'].andand.text
|
215
|
+
loc.short_hash = elem.elements['shortHash'].andand.text
|
216
|
+
if elem.elements['url']
|
217
|
+
loc.url = URI::parse(elem.elements['url'].text)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
class PlayRange
|
225
|
+
# ePlayRange = element playRange {
|
226
|
+
# element inFrame { attribute value { tFrameNumber } } &
|
227
|
+
# element outFrame { attribute value { tFrameNumber } } &
|
228
|
+
# element playOnlyRange { aBoolValue } }
|
229
|
+
|
230
|
+
def to_xml(x)
|
231
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
232
|
+
return if default?
|
233
|
+
|
234
|
+
x.playRange {
|
235
|
+
x.inFrame(:value => in_frame)
|
236
|
+
x.outFrame(:value => out_frame)
|
237
|
+
x.playOnlyRange(:value => play_only_range?)
|
238
|
+
}
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
def self.load(elem)
|
243
|
+
returning self.new do |pr|
|
244
|
+
pr.in_frame = elem.elements['inFrame/attribute::value'].value.to_i
|
245
|
+
pr.out_frame = elem.elements['outFrame/attribute::value'].value.to_i
|
246
|
+
pr.play_only_range = elem.elements['playOnlyRange/attribute::value'].value == 'true'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
|
252
|
+
class ZoomState
|
253
|
+
# eZoomState = element zoomState {
|
254
|
+
# element center { aXY } &
|
255
|
+
# eScaleFactor }
|
256
|
+
|
257
|
+
def to_xml(x)
|
258
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
259
|
+
return if default?
|
260
|
+
|
261
|
+
x.zoomState {
|
262
|
+
x.center(:x => center[0], :y => center[1])
|
263
|
+
x.scaleFactor(:value => scale_factor)
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
|
268
|
+
def self.load(elem)
|
269
|
+
returning self.new do |zs|
|
270
|
+
x = elem.elements['center/attribute::x'].value.to_f
|
271
|
+
y = elem.elements['center/attribute::y'].value.to_f
|
272
|
+
zs.center = [x, y]
|
273
|
+
zs.scale_factor = elem.elements['scaleFactor/attribute::value'].value.to_f
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
class PixelRatio
|
280
|
+
# ePixelRatio = element pixelRatio {
|
281
|
+
# element source { aRatio } &
|
282
|
+
# element target { aRatio } }
|
283
|
+
# aRatio &= attribute width { tPositiveFloat }
|
284
|
+
# aRatio &= attribute height { tPositiveFloat }
|
285
|
+
|
286
|
+
def to_xml(x)
|
287
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
288
|
+
return if default?
|
289
|
+
|
290
|
+
x.pixelRatio {
|
291
|
+
x.source(:width => source_width, :height => source_height)
|
292
|
+
x.target(:width => target_width, :height => target_height)
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
def self.load(elem)
|
298
|
+
returning self.new do |pr|
|
299
|
+
pr.source_width = elem.elements['source/attribute::width'].value.to_f
|
300
|
+
pr.source_height = elem.elements['source/attribute::height'].value.to_f
|
301
|
+
pr.target_width = elem.elements['target/attribute::width'].value.to_f
|
302
|
+
pr.target_height = elem.elements['target/attribute::height'].value.to_f
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
class Mask
|
309
|
+
# eMask = element mask {
|
310
|
+
# aAlpha &
|
311
|
+
# element center { aXY } &
|
312
|
+
# element ratio { aRatio } &
|
313
|
+
# eScaleFactor }
|
314
|
+
|
315
|
+
def to_xml(x)
|
316
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
317
|
+
return if default?
|
318
|
+
|
319
|
+
x.mask(:alpha => alpha) {
|
320
|
+
x.center(:x => center[0], :y => center[1])
|
321
|
+
x.ratio(:width => width, :height => height)
|
322
|
+
x.scaleFactor(:value => scale_factor)
|
323
|
+
}
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
def self.load(elem)
|
328
|
+
returning self.new do |mask|
|
329
|
+
mask.alpha = elem.attribute('alpha').value.to_f
|
330
|
+
x = elem.elements['center/attribute::x'].value.to_f
|
331
|
+
y = elem.elements['center/attribute::y'].value.to_f
|
332
|
+
mask.center = [x, y]
|
333
|
+
mask.width = elem.elements['ratio/attribute::width'].value.to_f
|
334
|
+
mask.height = elem.elements['ratio/attribute::height'].value.to_f
|
335
|
+
mask.scale_factor = elem.elements['scaleFactor/attribute::value'].value.to_f
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
class ColorGrading
|
342
|
+
# eColorGrading = element colorGrading {
|
343
|
+
# element offset {
|
344
|
+
# attribute red { tColorOff } &
|
345
|
+
# attribute green { tColorOff } &
|
346
|
+
# attribute blue { tColorOff } }? &
|
347
|
+
#
|
348
|
+
# element brightness {
|
349
|
+
# attribute rgb { tColorExp } &
|
350
|
+
# attribute red { tColorExp } &
|
351
|
+
# attribute green { tColorExp } &
|
352
|
+
# attribute blue { tColorExp } }? &
|
353
|
+
#
|
354
|
+
# element saturation { attribute value { tColorExp } }? &
|
355
|
+
# element gamma { attribute value { tColorExp } }? &
|
356
|
+
# element contrast { attribute value { tColorExp } }? &
|
357
|
+
#
|
358
|
+
# element linearToLog { aBoolValue }? &
|
359
|
+
# element lutPath { attribute value { tFilePath } }? }
|
360
|
+
|
361
|
+
def to_xml(x)
|
362
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
363
|
+
return if default?
|
364
|
+
|
365
|
+
x.colorGrading {
|
366
|
+
x.offset(:red => offset[:r], :green => offset[:g], :blue => offset[:b])
|
367
|
+
br = brightness
|
368
|
+
x.brightness(:rgb => br[:rgb], :red => br[:r], :green => br[:g], :blue => br[:b])
|
369
|
+
x.saturation(:value => saturation)
|
370
|
+
x.gamma(:value => gamma)
|
371
|
+
x.contrast(:value => contrast)
|
372
|
+
x.linearToLog(:value => linear_to_log?)
|
373
|
+
x.lutPath(:value => lut_path) if lut_path
|
374
|
+
}
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
def self.load(elem)
|
379
|
+
returning self.new do |cg|
|
380
|
+
offset_elem = elem.elements['offset']
|
381
|
+
if offset_elem
|
382
|
+
%w(red green blue).each do |attr|
|
383
|
+
cg.offset[attr] = offset_elem.attribute(attr).value.to_f
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
brightness_elem = elem.elements['brightness']
|
388
|
+
if brightness_elem
|
389
|
+
%w(rgb red green blue).each do |attr|
|
390
|
+
cg.brightness[attr] = brightness_elem.attribute(attr).value.to_f
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Everything is optional
|
395
|
+
(cg.saturation = elem.elements['saturation/attribute::value'].value.to_f) rescue :ignore
|
396
|
+
(cg.gamma = elem.elements['gamma/attribute::value'].value.to_f ) rescue :ignore
|
397
|
+
(cg.contrast = elem.elements['contrast/attribute::value'].value.to_f ) rescue :ignore
|
398
|
+
|
399
|
+
(cg.linear_to_log = elem.elements['linearToLog/attribute::value'].value == 'true') rescue :ignore
|
400
|
+
(cg.lut_path = elem.elements['lutPath/attribute::value']) rescue :ignore
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
class FrameAnnotation
|
407
|
+
# eFrameAnnotation = element annotation {
|
408
|
+
# attribute frame { tFrameNumber } &
|
409
|
+
# eNotes? &
|
410
|
+
# eObject* }
|
411
|
+
DrawingObjectElements = %w(line erase circle arrow text)
|
412
|
+
|
413
|
+
def to_xml(x)
|
414
|
+
fail "#{self.inspect}: Invalid" unless valid?
|
415
|
+
return if default?
|
416
|
+
|
417
|
+
attrs = {}
|
418
|
+
attrs['frame'] = frame
|
419
|
+
|
420
|
+
x.annotation(attrs) {
|
421
|
+
x.notes(notes) unless notes.empty?
|
422
|
+
drawing_objects.each {|obj| x << obj.to_s }
|
423
|
+
}
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
def self.load(elem, media)
|
428
|
+
returning media.annotations[elem.attribute('frame').value.to_i] do |ann|
|
429
|
+
ann.notes = elem.elements['notes'].andand.text || ann.notes
|
430
|
+
DrawingObjectElements.each do |obj_name|
|
431
|
+
ann.drawing_objects += elem.get_elements(obj_name)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CineSync
|
2
|
+
class ZoomState
|
3
|
+
attr_accessor :center, :scale_factor
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@center = [0.5, 0.5]
|
7
|
+
@scale_factor = 1.0
|
8
|
+
end
|
9
|
+
|
10
|
+
def default?
|
11
|
+
center == [0.5, 0.5] and scale_factor == 1.0
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
default? or (center.length == 2 and center.all? {|p| (0.0..1.0) === p } and scale_factor > 0.0) rescue false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/cinesync.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Dir["#{File.dirname(__FILE__)}/vendor/gems/**"].map do |dir|
|
2
|
+
$: << (File.directory?(lib = "#{dir}/lib") ? lib : dir)
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'cinesync/session'
|
6
|
+
require 'cinesync/media_file'
|
7
|
+
require 'cinesync/play_range'
|
8
|
+
require 'cinesync/zoom_state'
|
9
|
+
require 'cinesync/pixel_ratio'
|
10
|
+
require 'cinesync/mask'
|
11
|
+
require 'cinesync/color_grading'
|
12
|
+
require 'cinesync/xml'
|
13
|
+
require 'cinesync/event_handler'
|
14
|
+
require 'cinesync/ui'
|
15
|
+
require 'digest'
|
16
|
+
|
17
|
+
|
18
|
+
module CineSync
|
19
|
+
SessionV3XMLFileVersion = 3
|
20
|
+
ShortHashSampleSize = 2048
|
21
|
+
AllFilesGroup = 'All Files'
|
22
|
+
OfflineKey = '_OFFLINE_'
|
23
|
+
|
24
|
+
# Utility functions
|
25
|
+
def self.short_hash(path)
|
26
|
+
File.open(path, 'rb') do |f|
|
27
|
+
size = f.stat.size
|
28
|
+
buf = ''
|
29
|
+
|
30
|
+
if size <= ShortHashSampleSize
|
31
|
+
buf << f.read(size)
|
32
|
+
buf << [].pack('x' * (ShortHashSampleSize - size))
|
33
|
+
else
|
34
|
+
buf << f.read(ShortHashSampleSize / 2)
|
35
|
+
f.seek(-ShortHashSampleSize / 2, IO::SEEK_END)
|
36
|
+
buf << f.read(ShortHashSampleSize / 2)
|
37
|
+
end
|
38
|
+
Digest::SHA1.hexdigest([size].pack('N').reverse + buf)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.event_handler
|
43
|
+
session = Session::load($stdin) rescue nil
|
44
|
+
yield CineSync::EventHandler.new(ARGV, session)
|
45
|
+
end
|
46
|
+
end
|
data/test/helper.rb
ADDED