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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jonathon Mah
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,53 @@
1
+ # cineSync RubyGem
2
+
3
+ ## Quick Start
4
+ ### Installing the Gem
5
+
6
+ gem install cinesync
7
+
8
+ ### Creating a session file
9
+
10
+ #!/usr/bin/ruby
11
+ require 'rubygems'
12
+ require 'cinesync'
13
+
14
+ s = CineSync::Session.new
15
+ s.media << CineSync::MediaFile.new("http://cinesync.com/files/sample_qt.mov")
16
+ s.media << CineSync::MediaFile.new("/System/Library/Compositions/Fish.mov")
17
+ File.open("/tmp/session.csc", "w") {|f| f << s.to_xml }
18
+
19
+
20
+ ### Running in response to an event
21
+
22
+ #!/usr/bin/ruby
23
+ require 'rubygems'
24
+ require 'cinesync'
25
+
26
+ CineSync.event_handler do |evt|
27
+ puts "cineSync online with key #{evt.session_key}" unless evt.offline?
28
+ puts "Playlist has #{evt.session.media.length} files"
29
+ active_file = evt.session.media.find {|m| m.active? }
30
+ puts "Currently viewing #{active_file.name}" if active_file
31
+ end
32
+
33
+
34
+ ## Scripting Overview
35
+
36
+ cineSync 3.0 has new support for calling user-defined scripts. (Scripting requires a cineSync Pro account.) These are configured in cineSync's preferences. Scripts can be run from the Session &gt; Run Script menu, and can also be set to automatically trigger on certain events. When triggered, the script will be passed some arguments about the current environment (the current session key, where frames are being saved, and the save frame format), and the current session will be serialized and sent to it through standard I/O.
37
+
38
+ Additionally, a script can be run from a different application, set up a session, and send it to cineSync.
39
+
40
+ (More info to come: URLs etc)
41
+
42
+
43
+ ## Links
44
+
45
+ * [cineSync homepage](http://cinesync.com/)
46
+
47
+ ## Files
48
+
49
+ * `cineSync Session v3 Schema.rnc`: Session XML schema in [RELAX NG](http://relaxng.org/) Compact syntax
50
+
51
+ ## Copyright
52
+
53
+ Copyright (c) 2010 Rising Sun Research Pty Ltd. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "cinesync"
8
+ gem.summary = %Q{Library for scripting the cineSync collaborative video review tool}
9
+ gem.description = <<-EOF
10
+ This gem provides a Ruby interface to the cineSync session file format,
11
+ which is used by cineSync's scripting system. Use it to integrate
12
+ cineSync into your workflow.
13
+ EOF
14
+ gem.email = ["jmah@cinesync.com", "info@cinesync.com"]
15
+ gem.homepage = "http://github.com/jmah/cinesync"
16
+ gem.authors = ["Jonathon Mah", "Rising Sun Research"]
17
+ gem.add_dependency "activesupport", ">= 2.3"
18
+ gem.add_dependency "andand", ">= 1.3.1"
19
+ gem.add_dependency "builder", ">= 2.1"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
25
+ end
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ begin
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ end
41
+ rescue LoadError
42
+ task :rcov do
43
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
44
+ end
45
+ end
46
+
47
+ task :test => :check_dependencies
48
+
49
+ task :default => :test
50
+
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+
55
+ rdoc.rdoc_dir = 'rdoc'
56
+ rdoc.title = "cinesync #{version}"
57
+ rdoc.rdoc_files.include('README*')
58
+ rdoc.rdoc_files.include('lib/**/*.rb')
59
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # This script reads the current session from cineSync and converts all notes
4
+ # made on the session, each media file, and each frame to a CSV file. This file
5
+ # can then be processed by other scripts or opened in a spreadsheet program
6
+ # (Microsoft Excel, OpenOffice.org Calc, Apple Numbers).
7
+ #
8
+
9
+ require 'rubygems'
10
+ require 'cinesync'
11
+ require 'csv'
12
+
13
+
14
+ CineSync.event_handler do |evt|
15
+ name = "Notes from #{evt.offline? ? "offline session" : evt.session_key}.csv"
16
+ path = CineSync::UI.prompt_to_save("Save CSV file as:", name)
17
+ exit if path.nil? # User cancelled
18
+
19
+ CSV.open(path, "w") do |csv|
20
+ csv << ["Media File", "Frame", "Notes"] # Header row
21
+
22
+ csv << ["", "[Session]", evt.session.notes] unless evt.session.notes.empty?
23
+
24
+ evt.session.media.each do |media|
25
+ csv << [media.name, "[Media File]", media.notes] unless media.notes.empty?
26
+ media.annotations.keys.sort.each do |frame|
27
+ ann = media.annotations[frame]
28
+ next if ann.notes.empty?
29
+ csv << [media.name, frame, ann.notes]
30
+ end
31
+ end
32
+ end
33
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.6
@@ -0,0 +1,164 @@
1
+ # ==== Namespace ====
2
+ default namespace csc = "http://www.cinesync.com/ns/session/3.0"
3
+
4
+ # ==== Data types ====
5
+ tBool = "true" | "false"
6
+ tPositiveFloat = xsd:float { minExclusive = "0" }
7
+ tFrameNumber = xsd:integer { minInclusive = "1" }
8
+ tAlpha = xsd:float { minInclusive = "0" maxInclusive = "1" }
9
+ tColorOff = xsd:float { minInclusive = "-0.2" maxInclusive = "0.2" }
10
+ tColorExp = xsd:float { minInclusive = "0.367879" maxInclusive = "2.718282" } # exp(-1) .. exp(1)
11
+ tUnitFloat = xsd:float { minInclusive = "-1" maxInclusive = "1" }
12
+ tColor = xsd:string { pattern = "#[0-9a-fA-F]{6}" }
13
+ tShortHash = xsd:string { length = "40" }
14
+ tFilePath = xsd:string { minLength = "1" }
15
+ tURL = xsd:string { minLength = "1" }
16
+ tStereoType = "side-by-side" | "composite" | "interlaced"
17
+ tPoint = xsd:float
18
+
19
+
20
+ # ==== Session format ====
21
+ start = eSession
22
+
23
+ eSession = element session {
24
+ attribute version { xsd:integer { minInclusive = "3" } } &
25
+ attribute sessionFeatures { "standard" | "pro" } &
26
+ attribute hashToken { string }? &
27
+ aUserData? &
28
+ eGroup* &
29
+ eNotes? &
30
+ eChat? &
31
+ eStereo? &
32
+ eMedia* }
33
+
34
+ MediaBase =
35
+ aUserData? &
36
+ attribute active { tBool }? &
37
+ attribute currentFrame { tFrameNumber }? &
38
+ eGroup* &
39
+ ePlayRange?
40
+
41
+ eMedia |= element media {
42
+ # Normal media file
43
+ MediaBase &
44
+ element name { xsd:string { minLength = "1" } } &
45
+ element locators { eLocator+ } &
46
+ eNotes? &
47
+ eZoomState? &
48
+ ePixelRatio? &
49
+ eMask? &
50
+ eColorGrading? &
51
+ eFrameAnnotation* }
52
+
53
+ eMedia |= element media {
54
+ # Group movie
55
+ MediaBase &
56
+ element groupMovie { eGroup } }
57
+
58
+ eLocator |= element path { tFilePath }
59
+ eLocator |= element shortHash { tShortHash }
60
+ eLocator |= element url { tURL }
61
+
62
+ ePlayRange = element playRange {
63
+ element inFrame { attribute value { tFrameNumber } } &
64
+ element outFrame { attribute value { tFrameNumber } } &
65
+ element playOnlyRange { aBoolValue } }
66
+
67
+ eZoomState = element zoomState {
68
+ element center { aXY } &
69
+ eScaleFactor }
70
+
71
+ ePixelRatio = element pixelRatio {
72
+ element source { aRatio } &
73
+ element target { aRatio } }
74
+
75
+ eMask = element mask {
76
+ aAlpha &
77
+ element center { aXY } &
78
+ element ratio { aRatio } &
79
+ eScaleFactor }
80
+
81
+ eNotes = element notes { text }
82
+
83
+ eChat = element chat { text }
84
+
85
+ eStereo = element stereo {
86
+ attribute enabled { tBool } &
87
+ attribute type { tStereoType } &
88
+ element anamorphic { aBoolValue }? &
89
+ element anaglyph { aBoolValue }? &
90
+ element grayscale { aBoolValue }? &
91
+ element separation {
92
+ attribute annotation { tUnitFloat } &
93
+ attribute image { tUnitFloat } }? }
94
+
95
+ eColorGrading = element colorGrading {
96
+ element offset {
97
+ attribute red { tColorOff } &
98
+ attribute green { tColorOff } &
99
+ attribute blue { tColorOff } }? &
100
+
101
+ element brightness {
102
+ attribute rgb { tColorExp } &
103
+ attribute red { tColorExp } &
104
+ attribute green { tColorExp } &
105
+ attribute blue { tColorExp } }? &
106
+
107
+ element saturation { attribute value { tColorExp } }? &
108
+ element gamma { attribute value { tColorExp } }? &
109
+ element contrast { attribute value { tColorExp } }? &
110
+
111
+ element linearToLog { aBoolValue }? &
112
+ element lutPath { attribute value { tFilePath } }? }
113
+
114
+
115
+ # ==== Frame annotations ====
116
+ eFrameAnnotation = element annotation {
117
+ attribute frame { tFrameNumber } &
118
+ eNotes? &
119
+ eObject* }
120
+
121
+ eObject |= element line { aObjectID? & aAlphaColor & LinePath }
122
+ eObject |= element erase { aObjectID? & ErasePath }
123
+ eObject |= element circle { aObjectID? & aAlphaColor & Rect }
124
+ eObject |= element arrow {
125
+ aObjectID? &
126
+ aAlphaColor &
127
+ Rect &
128
+ element special { aBoolValue }? &
129
+ element flipSide { aBoolValue }? &
130
+ element reverse { aBoolValue }? }
131
+
132
+ eObject |= element text {
133
+ aObjectID? &
134
+ Rect &
135
+ element background { aAlphaColor } &
136
+ element foreground { aAlphaColor } &
137
+ element p { text } }
138
+
139
+
140
+ # ==== Components ====
141
+ aAlpha = attribute alpha { tAlpha }
142
+ aAlphaColor = attribute color { tColor } & aAlpha
143
+ aBoolValue = attribute value { tBool }
144
+ aObjectID = attribute id { xsd:string { minLength = "1" } }
145
+ aUserData = attribute userData { text }
146
+ eGroup = element group { xsd:string { minLength = "1" } }
147
+ eScaleFactor = element scaleFactor { attribute value { tPositiveFloat } }
148
+
149
+ aRatio &= attribute width { tPositiveFloat }
150
+ aRatio &= attribute height { tPositiveFloat }
151
+
152
+ aXY &= attribute x { tPoint }
153
+ aXY &= attribute y { tPoint }
154
+
155
+
156
+ # ==== Object components ====
157
+ LinePath &= attribute thickness { tPositiveFloat }
158
+ LinePath &= element point { aXY & aAlpha? }+
159
+
160
+ ErasePath &= attribute thickness { tPositiveFloat }
161
+ ErasePath &= element point { aXY }+
162
+
163
+ Rect &= element startPoint { aXY }
164
+ Rect &= element endPoint { aXY }
data/cinesync.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{cinesync}
8
+ s.version = "0.9.6"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jonathon Mah", "Rising Sun Research"]
12
+ s.date = %q{2010-05-08}
13
+ s.description = %q{ This gem provides a Ruby interface to the cineSync session file format,
14
+ which is used by cineSync's scripting system. Use it to integrate
15
+ cineSync into your workflow.
16
+ }
17
+ s.email = ["jmah@cinesync.com", "info@cinesync.com"]
18
+ s.extra_rdoc_files = [
19
+ "LICENSE",
20
+ "README.markdown"
21
+ ]
22
+ s.files = [
23
+ ".document",
24
+ ".gitignore",
25
+ "LICENSE",
26
+ "README.markdown",
27
+ "Rakefile",
28
+ "Samples/Export Notes to CSV.rb",
29
+ "VERSION",
30
+ "cineSync Session v3 Schema.rnc",
31
+ "cinesync.gemspec",
32
+ "lib/cinesync.rb",
33
+ "lib/cinesync/color_grading.rb",
34
+ "lib/cinesync/event_handler.rb",
35
+ "lib/cinesync/frame_annotation.rb",
36
+ "lib/cinesync/mask.rb",
37
+ "lib/cinesync/media_file.rb",
38
+ "lib/cinesync/pixel_ratio.rb",
39
+ "lib/cinesync/play_range.rb",
40
+ "lib/cinesync/session.rb",
41
+ "lib/cinesync/ui.rb",
42
+ "lib/cinesync/ui/standard_additions.rb",
43
+ "lib/cinesync/ui/win32_save_file_dialog.rb",
44
+ "lib/cinesync/xml.rb",
45
+ "lib/cinesync/zoom_state.rb",
46
+ "test/helper.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/jmah/cinesync}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.6}
52
+ s.summary = %q{Library for scripting the cineSync collaborative video review tool}
53
+ s.test_files = [
54
+ "test/helper.rb"
55
+ ]
56
+
57
+ if s.respond_to? :specification_version then
58
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
59
+ s.specification_version = 3
60
+
61
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
62
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3"])
63
+ s.add_runtime_dependency(%q<andand>, [">= 1.3.1"])
64
+ s.add_runtime_dependency(%q<builder>, [">= 2.1"])
65
+ else
66
+ s.add_dependency(%q<activesupport>, [">= 2.3"])
67
+ s.add_dependency(%q<andand>, [">= 1.3.1"])
68
+ s.add_dependency(%q<builder>, [">= 2.1"])
69
+ end
70
+ else
71
+ s.add_dependency(%q<activesupport>, [">= 2.3"])
72
+ s.add_dependency(%q<andand>, [">= 1.3.1"])
73
+ s.add_dependency(%q<builder>, [">= 2.1"])
74
+ end
75
+ end
76
+
@@ -0,0 +1,70 @@
1
+ module CineSync
2
+ class ColorGrading
3
+ attr_reader :offset, :brightness
4
+ attr_accessor :saturation, :gamma, :contrast, :linear_to_log, :lut_path
5
+
6
+ alias_method :linear_to_log?, :linear_to_log
7
+
8
+ def initialize
9
+ @offset = RGBArray.new([0.0] * 3)
10
+ @brightness = BrightnessArray.new([1.0] * 4)
11
+ @saturation = 1.0
12
+ @gamma = 1.0
13
+ @contrast = 1.0
14
+ @linear_to_log = false
15
+ @lut_path = nil
16
+ end
17
+
18
+ def default?
19
+ offset == [0.0]*3 and brightness == [1.0]*4 and
20
+ [saturation, gamma, contrast].all? {|f| f == 1.0 } and
21
+ not linear_to_log? and lut_path.nil?
22
+ end
23
+
24
+ def valid?
25
+ exp_range = Math.exp(-1)..Math.exp(1)
26
+ default? or (offset.length == 3 and brightness.length == 4 and
27
+ offset.all? {|o| (-0.2..0.2) === o } and
28
+ [saturation, gamma, contrast, *brightness].all? {|b| exp_range === b }) rescue false
29
+ end
30
+ end
31
+
32
+
33
+ class RGBArray < Array
34
+ # Implements an array of three values (corresponding to red, green, and
35
+ # blue) that can be indexed by position or string-convertible object:
36
+ # r,g,b = rgbary[0], rgbary[1], rgbary[2]
37
+ # r,g,b = rgbary['red'], rgbary[:green], rgbary[:b]
38
+
39
+ def [](key)
40
+ index = index_for_key(key)
41
+ fail "Unknown key used to index #{self.class}: #{key.inspect}" unless index
42
+ self.at(index)
43
+ end
44
+
45
+ def []=(key, value)
46
+ index = index_for_key(key)
47
+ fail "Unknown key used to index #{self.class}: #{key.inspect}" unless index
48
+ super(index, value)
49
+ end
50
+
51
+ protected
52
+ def index_for_key(key)
53
+ if Fixnum === key then key
54
+ elsif %w[red r].include?(key.to_s.downcase) then 0
55
+ elsif %w[green g].include?(key.to_s.downcase) then 1
56
+ elsif %w[blue b].include?(key.to_s.downcase) then 2
57
+ end
58
+ end
59
+ end
60
+
61
+
62
+ class BrightnessArray < RGBArray
63
+ protected
64
+ def index_for_key(key)
65
+ if %w[all rgb].include?(key.to_s.downcase) then 3
66
+ else super
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,67 @@
1
+ require 'pathname'
2
+
3
+
4
+ module CineSync
5
+ class EventHandler
6
+ attr_reader :save_format, :save_ext, :save_parent, :url, :session_key, :session
7
+
8
+ def initialize(argv, session)
9
+ @save_format = arg_value(argv, :save_fmt).downcase.to_sym
10
+ @save_ext = { :jpeg => 'jpg', :png => 'png' }[@save_format]
11
+ sp = arg_value(argv, :save_path, false)
12
+ @save_parent = Pathname.new(sp) if sp
13
+
14
+ @url = arg_value(argv, :url, false)
15
+
16
+ key_val = arg_value(argv, :key)
17
+ @session_key = key_val if key_val != OfflineKey
18
+
19
+ @session = session
20
+ end
21
+
22
+ def offline?
23
+ @session_key.nil?
24
+ end
25
+
26
+ def saved_frame_path(media_file, frame)
27
+ return nil unless save_parent
28
+
29
+ # This algorithm is ugly and will be improved before release
30
+ base = ('%s-%05d' % [media_file.name, frame])
31
+ i = 1; p2 = nil
32
+ begin
33
+ p = p2
34
+ p2, i = saved_frame_ver_path(base, i)
35
+ end while p2.exist?
36
+ p
37
+ end
38
+
39
+
40
+ private
41
+
42
+ ArgMatchers = { :key => /^--key=(\w+)$/,
43
+ :save_fmt => /^--save-format=(.+)$/,
44
+ :save_path => /^--save-dir=(.+)$/,
45
+ :url => /^--url=(.+)$/ }
46
+
47
+ def arg_value(argv, arg_key, required = true)
48
+ re = ArgMatchers[arg_key]
49
+ fail "Unknown symbolic argument key: #{arg_key.inspect}" unless re
50
+ args = argv.select {|a| a =~ re }
51
+ if args.empty?
52
+ if required
53
+ fail "Unable to find argument matching #{re.inspect} (argument key: #{arg_key.inspect})"
54
+ else
55
+ return nil
56
+ end
57
+ end
58
+ fail "Found multiple arguments matching #{re.inspect}: #{args.inspect}" if args.size > 1
59
+ args[0].match(re)[1]
60
+ end
61
+
62
+ def saved_frame_ver_path(base, version)
63
+ basename = base + (version == 1 ? '' : " (#{version})") + '.' + save_ext
64
+ [save_parent + basename, version + 1]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ module CineSync
2
+ class FrameAnnotation
3
+ attr_accessor :frame, :notes, :drawing_objects
4
+
5
+ def initialize(frame_num)
6
+ @frame = frame_num
7
+ @notes = ''
8
+ @drawing_objects = []
9
+ end
10
+
11
+ def default?
12
+ notes.empty? and drawing_objects.empty?
13
+ end
14
+
15
+ def valid?
16
+ frame >= 1
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ module CineSync
2
+ class Mask
3
+ attr_accessor :alpha, :center, :scale_factor
4
+ attr_accessor :width, :height
5
+
6
+ def initialize(ratio_or_width = nil, height = nil)
7
+ @alpha = 1.0
8
+ @center = [0.5, 0.5]
9
+ @scale_factor = 1.0
10
+ if ratio_or_width and height.nil?
11
+ self.ratio = ratio_or_width
12
+ elsif ratio_or_width and height
13
+ @width = Float(ratio_or_width)
14
+ @height = Float(height)
15
+ else
16
+ @width = 1
17
+ @height = 1
18
+ end
19
+ end
20
+
21
+ def ratio
22
+ wh = [width, height]
23
+ # Convert to ints if the float represents an int
24
+ wh.map {|f| (f == f.floor) ? f.to_i : f }.map {|num| num.to_s }.join(':')
25
+ end
26
+
27
+ def ratio=(rat)
28
+ @width, @height = rat.split(':', 2).map(&:to_f)
29
+ end
30
+
31
+ def default?
32
+ alpha == 1.0 and center == [0.5, 0.5] and scale_factor == 1.0 and
33
+ width > 0.0 and width == height
34
+ end
35
+
36
+ def valid?
37
+ default? or (width > 0.0 and height > 0.0 and center.length == 2 and
38
+ [alpha, scale_factor, *center].all? {|f| (0.0..1.0) === f }) rescue false
39
+ end
40
+ end
41
+ end