fasttrack 0.1

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4dc5f004ecff917644512a6ed9c218ab813f301c
4
+ data.tar.gz: 20705b32a6e116f5582f18f03c5f8335c2939b16
5
+ SHA512:
6
+ metadata.gz: 948a92e633033f52356023a57b7c711e7a29d1bc466197265624be23ba51448ebf1474eca92a6711ac5b9505efbce22a1b6f6014189943d8a57864ed4f3e2aad
7
+ data.tar.gz: 64f802661ea14141431cad5a4020111f8f888336cd0c86547a7f6e7707e84608dd0543375ee39ab327d6024f0d1235bdae963dc7bc3b6021565b770b725b0d1d
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fasttrack.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Misty De Meo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ # Fasttrack
2
+
3
+ Fasttrack is a rubylike object-oriented interface around the [Exempi](http://libopenraw.freedesktop.org/wiki/Exempi) C library. It provides an easy way to read, write and modify embedded XMP metadata from arbitrary files.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'fasttrack'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install fasttrack
18
+
19
+ ## Usage
20
+
21
+ Opening a file:
22
+
23
+ ```ruby
24
+ file = Fasttrack::File.new 'path' # add the 'w' parameter if you want to write
25
+ ```
26
+
27
+ Editing the file's XMP:
28
+
29
+ ```ruby
30
+ file.xmp.set :tiff, 'Make', 'Samsung'
31
+ # or, more prettily
32
+ file.xmp['tiff:Make'] = 'Samsung'
33
+ ```
34
+
35
+ Iterate over the properties in a file:
36
+
37
+ ```ruby
38
+ props = file.xmp.map {|p| p[1]}
39
+ ```
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork it
44
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
45
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
46
+ 4. Push to the branch (`git push origin my-new-feature`)
47
+ 5. Create new Pull Request
48
+
49
+ ## License
50
+
51
+ 3-clause BSD, identical to the license used by Exempi and Adobe XMP Toolkit. For the license text, see LICENSE.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/fasttrack/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Misty De Meo"]
6
+ gem.email = ["mistydemeo@gmail.com"]
7
+ gem.description = <<-EOS
8
+ Fasttrack is an easy-to-use Ruby wrapper for
9
+ Exempi, a C library for managing XMP metadata.
10
+ Fasttrack provides a dead-easy, object-oriented
11
+ interface to Exempi's functions.
12
+ EOS
13
+ gem.summary = %q{Ruby sugar for Exempi}
14
+ gem.homepage = "https://github.com/mistydemeo/fasttrack"
15
+
16
+ gem.files = `git ls-files`.split($\)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.name = "fasttrack"
20
+ gem.require_paths = ["lib"]
21
+ gem.version = Fasttrack::VERSION
22
+
23
+ gem.add_dependency 'exempi', '>= 0.1'
24
+
25
+ gem.add_development_dependency 'rake', '>= 0.9.2.2'
26
+ gem.add_development_dependency 'mocha', '>= 0.13.0'
27
+ gem.add_development_dependency 'nokogiri', '>= 1.5.5'
28
+ end
@@ -0,0 +1,18 @@
1
+ require 'fasttrack/exceptions'
2
+ require 'fasttrack/file'
3
+ require 'fasttrack/xmp'
4
+ require 'fasttrack/version'
5
+
6
+ require 'exempi/exceptions'
7
+
8
+ module Fasttrack
9
+ # Checks for an Exempi error, and raises the appropriate exception.
10
+ # Should only be used when an error has been detected from the boolean
11
+ # output of one of Exempi's functions.
12
+ # @raise [Exempi::ExempiError]
13
+ def self.handle_exempi_failure
14
+ error_code = Exempi.xmp_get_error
15
+ message = Exempi.exception_for error_code
16
+ raise Exempi::ExempiError.new(error_code), "Exempi failed with the code #{message}"
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module Fasttrack
2
+ class FileFormatError < StandardError; end
3
+ class OpenError < StandardError; end
4
+ class WriteError < StandardError; end
5
+ end
@@ -0,0 +1,167 @@
1
+ # -*- coding: UTF-8 -*-
2
+ require 'fasttrack/exceptions'
3
+ require 'fasttrack/xmp'
4
+
5
+ require 'exempi'
6
+ require 'ffi'
7
+ require 'pathname'
8
+
9
+ module Fasttrack
10
+ class File
11
+ # The Exempi C pointer for this object. You normally shouldn't need
12
+ # to access this, but it is exposed so that unwrapped Exempi
13
+ # functions can be called on Fasttrack-tracked objects.
14
+ # @return [FFI::Pointer]
15
+ attr_reader :file_ptr
16
+
17
+ # The Fasttrack::XMP object associated with this file. You can
18
+ # replace it with another Fasttrack::XMP object.
19
+ # @example Replace an object's XMP with the XMP from another file
20
+ # file1.xmp = file2.xmp
21
+ # file1.save!
22
+ # @example Create a new XMP document and save it into a file
23
+ # newxmp = Fasttrack::XMP.new
24
+ # newxmp['tiff:Make'] = 'Sony'
25
+ # file.xmp = newxmp
26
+ # file.save!
27
+ # @example Create a new XMP document manually, then add it to a File object
28
+ # ptr = Exempi.xmp_new_empty
29
+ # Exempi.xmp_set_property Fasttrack::NAMESPACES[:tiff],
30
+ # 'tiff:Make', 'Sony', nil
31
+ # file.xmp = ptr
32
+ # @return [Fasttrack::XMP]
33
+ attr_reader :xmp
34
+
35
+ # @return [Pathname]
36
+ attr_reader :path
37
+
38
+ def self.finalize pointer
39
+ proc { Exempi.xmp_files_free pointer }
40
+ end
41
+
42
+ # Instantiates a new Fasttrack::File object, which is a
43
+ # representation of a file on disk and its associated XMP metadata.
44
+ # To create a new file on disk you should use Fasttrack::XMP#to_s
45
+ # instead.
46
+ # @param [String] path path to the file on disk; must exist
47
+ # @param [String] mode file mode; accepted values are "r"
48
+ # (read-only; default), "w" and "rw" (read-write)
49
+ # @raise [Fasttrack::FileFormatError] if the file can't have XMP
50
+ # metadata
51
+ def initialize path, mode="r"
52
+ @path = Pathname.new(path).expand_path
53
+ if not @path.exist?
54
+ raise Errno::ENOENT, "#{@path} does not exist"
55
+ end
56
+
57
+ @file_ptr = Exempi.xmp_files_new
58
+ @read_mode = mode
59
+ open @read_mode
60
+
61
+ ObjectSpace.define_finalizer(self, self.class.finalize(@file_ptr))
62
+ end
63
+
64
+ # Checks to see whether XMP can be written to the current file.
65
+ # If no XMP is specified, the file's associated XMP is used.
66
+ #
67
+ # @param [FFI::Pointer, Fasttrack::XMP] xmp XMP to check; can be a
68
+ # Fasttrack::XMP object or a pointer to a C XMP object
69
+ # @return [true,false]
70
+ # @raise [TypeError] if an object without an XMP pointer is passed
71
+ def can_put_xmp? xmp=@xmp
72
+ if xmp.is_a? Fasttrack::XMP
73
+ xmp = xmp.xmp_ptr
74
+ end
75
+
76
+ raise TypeError, "#{xmp} is not a pointer" unless xmp.is_a? FFI::Pointer
77
+
78
+ Exempi.xmp_files_can_put_xmp @file_ptr, xmp
79
+ end
80
+
81
+ # Replaces the file's currently associated XMP object. The new XMP
82
+ # will not be written to disk until #save! or #close! is called.
83
+ # @param [Fasttrack::XMP, FFI::Pointer] xmp XMP object to copy. Must
84
+ # be a Fasttrack::XMP object or an XMP pointer.
85
+ # @return [Fasttrack::XMP, FFI::Pointer] the copied object.
86
+ # @raise [Fasttrack::WriteError] if the file can't be written to
87
+ def xmp= new_xmp
88
+ if new_xmp.is_a? FFI::Pointer
89
+ new_xmp = Fasttrack::XMP.new new_xmp
90
+ end
91
+ if not can_put_xmp? new_xmp
92
+ message = "Unable to write XMP"
93
+ message << "; file opened read-only" if @read_mode == "r"
94
+ raise Fasttrack::WriteError, message
95
+ end
96
+
97
+ @xmp = new_xmp.dup
98
+ Exempi.xmp_files_put_xmp @file_ptr, @xmp.xmp_ptr
99
+ end
100
+
101
+ # Save changes to a file.
102
+ # Exempi only saves changes when a file is closed; this method
103
+ # closes and then reopens the file so it can continue to be used.
104
+ # This always uses Exempi's "safe close", which writes into a
105
+ # temporary file and swap in case of unexpected termination.
106
+ # @return [Boolean] true if successful
107
+ # @raise [Fasttrack::WriteError] if the file is read-only or closed
108
+ def save!
109
+ if @read_mode == "r"
110
+ raise Fasttrack::WriteError, "file opened read-only"
111
+ end
112
+
113
+ raise Fasttrack::WriteError, "file is closed" unless @open
114
+ # Make sure we let Exempi know there's new XMP to write
115
+ Exempi.xmp_files_put_xmp @file_ptr, @xmp.xmp_ptr
116
+ close!
117
+ open @read_mode
118
+ end
119
+
120
+ # Closes the current file and frees its memory.
121
+ # While this will not save changes made to the current
122
+ # XMP object, it still has the potential to make changes to
123
+ # the file being closed.
124
+ # @return [Boolean] true if successful
125
+ # @raise [Fasttrack::WriteError] if the file is already closed
126
+ def close!
127
+ raise Fasttrack::WriteError, "file is already closed" unless @open
128
+
129
+ @open = !Exempi.xmp_files_close(@file_ptr, :XMP_CLOSE_SAFEUPDATE)
130
+ if @open # did not successfully close
131
+ Fasttrack.handle_exempi_failure
132
+ else
133
+ true
134
+ end
135
+ end
136
+
137
+ # Reopens a closed Fasttrack::File object.
138
+ #
139
+ # @param [String] mode file mode
140
+ # @raise [Exempi::ExempiError] if Exempi reports an error while
141
+ # attempting to open the file
142
+ # @raise [Fasttrack::OpenError] if an opened file is reopened
143
+ def open mode=@read_mode
144
+ raise Fasttrack::OpenError, "file is already open" if @open
145
+
146
+ case mode
147
+ when 'r'
148
+ open_option = :XMP_OPEN_READ
149
+ when 'w', 'rw'
150
+ open_option = :XMP_OPEN_FORUPDATE
151
+ else
152
+ open_option = :XMP_OPEN_NOOPTION
153
+ end
154
+
155
+ @open = Exempi.xmp_files_open @file_ptr, @path.to_s, open_option
156
+
157
+ if not @open
158
+ Fasttrack.handle_exempi_failure
159
+ else
160
+ @xmp = Fasttrack::XMP.from_file_pointer @file_ptr
161
+ end
162
+
163
+ @open
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,12 @@
1
+ require 'exempi/namespaces'
2
+
3
+ module Fasttrack
4
+ # Populated at runtime with the namespace values from
5
+ # Exempi::Namespaces
6
+ NAMESPACES = {}
7
+ Exempi::Namespaces.constants.each do |const|
8
+ name = const.to_s.match(/XMP_NS_(.+)/)[1].downcase.to_sym
9
+ uri = Exempi::Namespaces.const_get const
10
+ NAMESPACES[name] = uri
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Fasttrack
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,362 @@
1
+ # -*- coding: UTF-8 -*-
2
+ require 'fasttrack/namespaces'
3
+
4
+ require 'exempi'
5
+ require 'ffi'
6
+
7
+ module Fasttrack
8
+ class XMP
9
+ # The Exempi C pointer for this object. You normally shouldn't need
10
+ # to access this, but it is exposed so that unwrapped Exempi
11
+ # functions can be called on Fasttrack-tracked objects.
12
+ # @return [FFI::Pointer]
13
+ attr_accessor :xmp_ptr
14
+
15
+ include Enumerable
16
+
17
+ def self.finalize pointer
18
+ proc { Exempi.xmp_free pointer }
19
+ end
20
+
21
+ def self.finalize_iterator pointer
22
+ proc { Exempi.xmp_iterator_free pointer }
23
+ end
24
+
25
+ # Creates a new XMP object.
26
+ # If a pointer to an XMP chunk is provided, a copy of it will be used;
27
+ # otherwise, a new empty XMP chunk will be created.
28
+ #
29
+ # Note that if you create an XMP object from a pre-existing pointer,
30
+ # you'll need to remember to free the original pointer with
31
+ # xmp_free(). Garbage collection will only free the
32
+ # Fasttrack::XMP version for you.
33
+ # @param [FFI::Pointer, nil] xmp_ptr XMP pointer to use, or nil
34
+ def initialize xmp_ptr=nil
35
+ if xmp_ptr and xmp_ptr.is_a? FFI::Pointer
36
+ @xmp_ptr = Exempi.xmp_copy xmp_ptr
37
+ else
38
+ @xmp_ptr = Exempi.xmp_new_empty
39
+ end
40
+
41
+ @iterator = nil
42
+ @iterator_opts = nil
43
+
44
+ # capture the namespaces that exist at load time, with
45
+ # a count of the number of times each uri is present
46
+ ns_ary = map {|ns,_,_,_| ns}
47
+ @namespaces = ns_ary.uniq.each_with_object(Hash.new(0)) do |ns, hsh|
48
+ hsh[ns] = ns_ary.count(ns) - 1 # one empty item returned per ns
49
+ end
50
+
51
+ ObjectSpace.define_finalizer(self, self.class.finalize(@xmp_ptr))
52
+ end
53
+
54
+ # Creates a new XMP object based on the metadata in a file
55
+ # represented by an Exempi file pointer. The file must already have
56
+ # been opened using xmp_files_open()
57
+ # @param [FFI::Pointer] file_ptr an Exempi pointer
58
+ # @return [Fasttrack::XMP] a new XMP object
59
+ def self.from_file_pointer file_ptr
60
+ xmp_ptr = Exempi.xmp_files_get_new_xmp file_ptr
61
+ xmp = Fasttrack::XMP.new xmp_ptr
62
+ Exempi.xmp_free xmp_ptr
63
+
64
+ xmp
65
+ end
66
+
67
+ # Creates a new XMP object from an XML string.
68
+ # @param [String] xml a string containing valid XMP
69
+ # @return [Fasttrack::XMP] a new XMP object
70
+ def self.parse xml
71
+ ptr = Exempi.xmp_new xml, xml.bytesize
72
+ xmp = Fasttrack::XMP.new ptr
73
+ Exempi.xmp_free ptr
74
+
75
+ xmp
76
+ end
77
+
78
+ # This ensures that the clone is created with a new XMP pointer.
79
+ def initialize_copy orig
80
+ super
81
+ @xmp_ptr = Exempi.xmp_copy @xmp_ptr
82
+
83
+ # if we don't do this, the new clone's finalizer will reference
84
+ # the pointer from the original object - not the clone's
85
+ ObjectSpace.undefine_finalizer self
86
+ ObjectSpace.define_finalizer(self, self.class.finalize(@xmp_ptr))
87
+ end
88
+
89
+ # Return an object from the global namespace.
90
+ # @example Gets the value of the 'tiff:Make' property
91
+ # xmp.get :tiff, 'tiff:Make' #=> 'Sony'
92
+ # # you can also leave off the namespace prefix
93
+ # xmp.get :tiff, 'Make' #=> 'Sony'
94
+ # # You can use the namespace URI string too
95
+ # xmp.get 'http://ns.adobe.com/tiff/1.0/', 'Make' #=> 'Sony'
96
+ # @param [String, Symbol] namespace namespace URI to use. If a
97
+ # symbol is provided, Fasttrack will look up the URI from a set of
98
+ # common recognized namespaces.
99
+ # @param [String] prop property to look up.
100
+ # @return [String, nil] the value of the requested property, or nil
101
+ # if not found.
102
+ def get namespace, prop
103
+ if namespace.is_a? Symbol
104
+ namespace = namespace_for namespace
105
+ end
106
+
107
+ prop_str = Exempi.xmp_string_new
108
+ success = Exempi.xmp_get_property @xmp_ptr, namespace, prop, prop_str, nil
109
+ if success
110
+ result = Exempi.xmp_string_cstr prop_str
111
+
112
+ result
113
+ else
114
+ result = nil
115
+ end
116
+
117
+ Exempi.xmp_string_free prop_str
118
+
119
+ result
120
+ end
121
+
122
+ alias_method :get_property, :get
123
+
124
+ # Modifies an existing XMP property or creates a new property with
125
+ # the specified value.
126
+ # @example Sets the 'tiff:Make' property to 'Sony'
127
+ # xmp.set :tiff, 'tiff:Make', 'Sony' #=> 'Sony'
128
+ # @param [String, Symbol] namespace namespace to use. If a symbol is
129
+ # provided, Fasttrack will look up from a set of common recognized
130
+ # namespaces.
131
+ # @param [String] prop property to set.
132
+ # @param [String] value value to set.
133
+ # @return [String] the new value
134
+ # @raise [Exempi::ExempiError] if Exempi reports that it failed
135
+ def set namespace, prop, value
136
+ if namespace.is_a? Symbol
137
+ namespace = namespace_for namespace
138
+ end
139
+
140
+ success = Exempi.xmp_set_property @xmp_ptr, namespace, prop, value, nil
141
+ if success
142
+ @namespaces[namespace] += 1
143
+ value
144
+ else
145
+ Fasttrack.handle_exempi_failure
146
+ end
147
+ end
148
+
149
+ alias_method :set_property, :set
150
+
151
+ # Fetches an XMP property given a string containing the namespace
152
+ # prefix and the property name, e.g. "tiff:Make".
153
+ # @example Returns the value of 'tiff:Make'
154
+ # xmp['tiff:Make'] #=> 'Sony'
155
+ # @param [String] query query
156
+ # @return [String, nil] the property's value, or nil if not found
157
+ def [] query
158
+ if query =~ /.+:.+/
159
+ ns_prefix, property = query.scan(/(.+):(.+)/).flatten
160
+ end
161
+
162
+ ns_uri = namespace_for ns_prefix.downcase.to_sym
163
+
164
+ get_property ns_uri, property
165
+ end
166
+
167
+ # Sets an XMP property given a string containing the namespace
168
+ # prefix and the property name, e.g. "tiff:Make".
169
+ # @example Sets the value of 'tiff:Make' to 'Sony'
170
+ # xmp['tiff:Make'] = 'Sony' #=> 'Sony'
171
+ # @param [String] property property
172
+ # @param [String] value value to set
173
+ # @return [String] the new value
174
+ def []= property, value
175
+ if property =~ /.+:.+/
176
+ ns_prefix, property = property.scan(/(.+):(.+)/).flatten
177
+ end
178
+
179
+ ns_uri = namespace_for ns_prefix.downcase.to_sym
180
+
181
+ set_property ns_uri, property, value
182
+ end
183
+
184
+ # Deletes a given XMP property. If the property exists returns the
185
+ # deleted property, otherwise returns nil.
186
+ # @param (see #get_property)
187
+ # @return [String, nil] the value of the deleted property, or nil if
188
+ # not found.
189
+ def delete namespace, prop
190
+ if namespace.is_a? Symbol
191
+ namespace = namespace_for namespace
192
+ end
193
+
194
+ deleted_prop = get_property namespace, prop
195
+ Exempi.xmp_delete_property @xmp_ptr, namespace, prop
196
+ @namespaces[namespace] -= 1 unless deleted_prop.nil?
197
+
198
+ deleted_prop
199
+ end
200
+
201
+ alias_method :delete_property, :delete
202
+
203
+ # Returns a list of namespace URIs in use in the specified XMP data.
204
+ # @return [Array<String>] an array of URI strings
205
+ def namespaces
206
+ @namespaces.keys
207
+ end
208
+
209
+ # Serializes the XMP object to an XML string.
210
+ # @return [String]
211
+ def serialize
212
+ xmp_str = Exempi.xmp_string_new
213
+ Exempi.xmp_serialize @xmp_ptr, xmp_str, 0, 0
214
+ string = Exempi.xmp_string_cstr xmp_str
215
+ Exempi.xmp_string_free xmp_str
216
+
217
+ string
218
+ end
219
+
220
+ def == other_xmp
221
+ serialize == other_xmp.serialize
222
+ end
223
+
224
+ # @yieldparam (see #iterate_for)
225
+ def each &block
226
+ return to_enum unless block_given?
227
+
228
+ iterate_for do |returned|
229
+ block.call(returned)
230
+ end
231
+ end
232
+
233
+ # Iterates over all properties, with the iteration rules guided
234
+ # by the specified options. Options should be specified in an array.
235
+ # @param [Array<Symbol>] opts array of one or more options
236
+ # @option opts :properties Iterate the property tree of a TXMPMeta
237
+ # object.
238
+ # @option opts :aliases Iterate the global namespace table.
239
+ # @option opts :just_children Just do the immediate children of the
240
+ # root, default is subtree.
241
+ # @option opts :just_leaf_nodes Just do the leaf nodes, default is
242
+ # all nodes in the subtree.
243
+ # @option opts :just_leaf_name Return just the leaf part of the
244
+ # path, default is the full path.
245
+ # @option opts :include_aliases Include aliases, default is just
246
+ # actual properties.
247
+ # @option opts :omit_qualifiers Omit all qualifiers.
248
+ # @yieldparam (see #iterate_for)
249
+ # @return [Enumerator] if no block is given
250
+ def each_with_options opts, &block
251
+ return enum_for(:each_with_options, opts) unless block_given?
252
+
253
+ options = opts.map {|o| ("XMP_ITER_"+o.to_s.delete("_")).to_sym}
254
+ # filter out invalid options
255
+ options.keep_if {|o| Exempi::XMP_ITER_OPTIONS.find o}
256
+
257
+ iterate_for({:options => options}) do |returned|
258
+ block.call(returned)
259
+ end
260
+ end
261
+
262
+ # Iterates over all properties in a specified namespace.
263
+ # The namespace parameter can be the URI of the namespace to use,
264
+ # or a symbol representing the namespace prefix, e.g. :exif.
265
+ # The recognized namespace prefixes are based on a set of common
266
+ # namespace prefixes (generated at runtime in Fasttrack::NAMESPACES)
267
+ # as well as the local namespaces currently in use.
268
+ # @param [String, Symbol] ns namespace to iterate over
269
+ # @param [Array<Symbol>] opts a set of options to restrict the
270
+ # iteration; see #each_with_options for supported options
271
+ # @yieldparam (see #iterate_for)
272
+ # @return [Enumerator] if no block is given
273
+ def each_in_namespace ns, opts=[], &block
274
+ return enum_for(:each_in_namespace, ns) unless block_given?
275
+
276
+ opts = {:namespace => ns}
277
+ iterate_for(opts) do |returned|
278
+ block.call(returned)
279
+ end
280
+ end
281
+
282
+ def rewind
283
+ @iterator = new_iterator
284
+ end
285
+
286
+ private
287
+
288
+ # Attempts to find the namespace URI given a symbol representation.
289
+ # @param [Symbol]
290
+ # @return [String, nil]
291
+ def namespace_for sym
292
+ Fasttrack::NAMESPACES[sym]
293
+ end
294
+
295
+ # Creates a new iterator based on the options specified in the
296
+ # @iterator_opts ivar, or an options hash if specified.
297
+ # The options hash can specify the following:
298
+ # :namespace => Limits the iteration to a specific namespace URI
299
+ # :options => Options for the iteration; must be an array composed
300
+ # of one or more symbols specified in the Exempi::XmpIterOptions
301
+ # enum.
302
+ # @param [Hash] params hash containing the options for the new
303
+ # iterator.
304
+ # @return [FFI::Pointer] pointer to the new iterator
305
+ def new_iterator params=@iterator_opts
306
+ ns = params[:namespace]
307
+ # property support is currently disabled
308
+ prop = nil
309
+ opts = params[:options]
310
+ iterator = Exempi.xmp_iterator_new @xmp_ptr, ns, prop, opts
311
+ ObjectSpace.define_finalizer(iterator, self.class.finalize_iterator(iterator))
312
+
313
+ iterator
314
+ end
315
+
316
+ # This method is the plumbing which is used by the various
317
+ # Enumerable mixin methods.
318
+ # @param (see #new_iterator)
319
+ # @yieldparam [String] uri the uri for the property
320
+ # @yieldparam [String] name the property's name
321
+ # @yieldparam [String] value the property's value
322
+ # @yieldparam [Hash] options additional metadata about the property
323
+ def iterate_for opts={}
324
+ # Select the namespace; lookup symbol if appropriate, otherwise
325
+ # use string or nil
326
+ if opts[:namespace].is_a? Symbol
327
+ ns = namespace_for opts[:namespace]
328
+ else
329
+ ns = opts[:namespace]
330
+ end
331
+
332
+ # record iterator options; these are necessary to call subsequent
333
+ # iterator functions
334
+ @iterator_opts = {
335
+ :namespace => ns,
336
+ # note that :property is currently unimplemented in Fasttrack
337
+ :property => opts[:property],
338
+ :options => opts[:options] || []
339
+ }
340
+
341
+ @iterator = new_iterator
342
+
343
+ returned_ns = Exempi.xmp_string_new
344
+ returned_prop_path = Exempi.xmp_string_new
345
+ returned_prop_value = Exempi.xmp_string_new
346
+ returned_prop_opts = FFI::MemoryPointer.new :uint32
347
+
348
+ # keep iterating until xmp_iterator_next() returns false, which
349
+ # indicates it has finished traversing all the properties
350
+ while Exempi.xmp_iterator_next(@iterator, returned_ns, returned_prop_path, returned_prop_value, nil)
351
+ ary = [returned_ns, returned_prop_path, returned_prop_value].map do |xmp_str|
352
+ Exempi.xmp_string_cstr xmp_str
353
+ end
354
+
355
+ ary << Exempi.parse_bitmask(returned_prop_opts.read_uint32,
356
+ Exempi::XMP_PROPS_BITS, true)
357
+
358
+ yield ary
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,25 @@
1
+ <?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
2
+ <x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 9.00'>
3
+ <rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
4
+
5
+ <rdf:Description rdf:about=''
6
+ xmlns:exif='http://ns.adobe.com/exif/1.0/'>
7
+ <exif:DateTimeOriginal>2012-03-17T11:45:16-04:00</exif:DateTimeOriginal>
8
+ <exif:ExposureProgram>1</exif:ExposureProgram>
9
+ <exif:ExposureTime>1/30</exif:ExposureTime>
10
+ <exif:FNumber>5/1</exif:FNumber>
11
+ <exif:GPSMapDatum>WGS-84</exif:GPSMapDatum>
12
+ <exif:GPSStatus>V</exif:GPSStatus>
13
+ <exif:GPSVersionID>2.2.0.0</exif:GPSVersionID>
14
+ </rdf:Description>
15
+
16
+ <rdf:Description rdf:about=''
17
+ xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
18
+ <tiff:ImageLength>1080</tiff:ImageLength>
19
+ <tiff:ImageWidth>1920</tiff:ImageWidth>
20
+ <tiff:Make>Sony</tiff:Make>
21
+ <tiff:Model>NEX-FS100UK</tiff:Model>
22
+ </rdf:Description>
23
+ </rdf:RDF>
24
+ </x:xmpmeta>
25
+ <?xpacket end='w'?>
Binary file
@@ -0,0 +1,11 @@
1
+ require 'minitest/autorun'
2
+ require 'fasttrack'
3
+
4
+ describe Fasttrack do
5
+ it "should be able to handle Exempi errors" do
6
+ lambda do
7
+ Exempi.xmp_files_open_new "invalid_file", nil
8
+ Fasttrack.handle_exempi_failure
9
+ end.must_raise Exempi::ExempiError
10
+ end
11
+ end
@@ -0,0 +1,129 @@
1
+ require 'fasttrack'
2
+
3
+ require 'mocha/setup'
4
+ require 'minitest/autorun'
5
+ require 'fileutils'
6
+ require 'tmpdir'
7
+
8
+ describe Fasttrack::File do
9
+ before do
10
+ @test_data = File.expand_path File.join(__FILE__,"..","data","avchd.xmp")
11
+ @test_image = File.expand_path File.join(__FILE__,"..","data","image.jpg")
12
+
13
+ @tmpdir = Dir.mktmpdir
14
+ Dir.chdir @tmpdir
15
+ end
16
+
17
+ it "should be able to create a new file object" do
18
+ Fasttrack::File.new(@test_data).must_be_kind_of Fasttrack::File
19
+ end
20
+
21
+ it "should raise when created with a file that doesn't exist" do
22
+ lambda do
23
+ Fasttrack::File.new "no_file_here"
24
+ end.must_raise Errno::ENOENT
25
+ end
26
+
27
+ it "should be able to report whether XMP can be written to a file" do
28
+ # If the file is opened read-only the answer should be false
29
+ file = Fasttrack::File.new @test_data, "r"
30
+ refute file.can_put_xmp?
31
+
32
+ # also test when file is opened for writing
33
+ file = Fasttrack::File.new @test_image, "w"
34
+ assert file.can_put_xmp?
35
+ end
36
+
37
+ it "should be able to save changes to a file" do
38
+ file1 = Fasttrack::File.new @test_data
39
+ FileUtils.copy File.expand_path(@test_image), "temp.jpg"
40
+ file2 = Fasttrack::File.new "temp.jpg", "w"
41
+ file2.xmp = file1.xmp
42
+ assert file2.save!
43
+ assert file2.close!
44
+ end
45
+
46
+ it "should be able to reopen a closed file" do
47
+ FileUtils.copy File.expand_path(@test_image), "temp.jpg"
48
+ file1 = Fasttrack::File.new "temp.jpg", "w"
49
+ file1.close!
50
+ file2 = Fasttrack::File.new @test_data
51
+ file1.open
52
+ file1.xmp = file2.xmp
53
+ assert file1.save!
54
+ end
55
+
56
+ it "should raise when saving changes to a closed file" do
57
+ lambda do
58
+ file1 = Fasttrack::File.new @test_data
59
+ file1.close!
60
+ file2 = Fasttrack::File.new @test_image
61
+ file1.xmp = file2.xmp
62
+ end.must_raise Fasttrack::WriteError
63
+ end
64
+
65
+ it "should raise when reopening an open file" do
66
+ lambda do
67
+ file1 = Fasttrack::File.new @test_data
68
+ file1.open
69
+ end.must_raise Fasttrack::OpenError
70
+ end
71
+
72
+ it "should be able to copy XMP file to file" do
73
+ file1 = Fasttrack::File.new @test_data
74
+ FileUtils.copy File.expand_path(@test_image), "temp.jpg"
75
+ file2 = Fasttrack::File.new "temp.jpg", "w"
76
+
77
+ file2_orig = file2.xmp
78
+ file2.xmp = file1.xmp
79
+ file2.save!
80
+ file2.xmp.wont_be_same_as file2_orig
81
+ end
82
+
83
+ it "should be able to copy manually-created XMP into a file" do
84
+ FileUtils.copy File.expand_path(@test_image), "temp.jpg"
85
+ file = Fasttrack::File.new "temp.jpg", "w"
86
+
87
+ new_xmp = Exempi.xmp_new_empty
88
+ Exempi.xmp_set_property new_xmp, Fasttrack::NAMESPACES[:tiff],
89
+ 'tiff:Make', 'Sony', nil
90
+
91
+ old_xmp = file.xmp
92
+ file.xmp = new_xmp
93
+ file.save!
94
+ file.xmp.wont_be_same_as old_xmp
95
+ end
96
+
97
+ it "should raise when trying to write xmp into a read-only file" do
98
+ file1 = Fasttrack::File.new @test_data
99
+ file2 = Fasttrack::File.new @test_image
100
+
101
+ lambda {file2.xmp = file1.xmp}.must_raise Fasttrack::WriteError
102
+ end
103
+
104
+ it "should raise when trying to save changes into a read-only file" do
105
+ file = Fasttrack::File.new @test_data, 'r'
106
+ lambda {file.save!}.must_raise Fasttrack::WriteError
107
+ end
108
+
109
+ it "should raise when trying to copy non-XMP data into a file" do
110
+ FileUtils.copy File.expand_path(@test_image), "temp.jpg"
111
+ file = Fasttrack::File.new "temp.jpg", "w"
112
+
113
+ lambda {file.xmp = 'xmp'}.must_raise TypeError
114
+ end
115
+
116
+ it "should raise if Exempi fails to close a file" do
117
+ Exempi.stubs(:xmp_files_close).returns(false)
118
+ lambda {Fasttrack::File.new(@test_data).close!}.must_raise Exempi::ExempiError
119
+ end
120
+
121
+ it "should raise if Exempi fails to open a file" do
122
+ Exempi.stubs(:xmp_files_open).returns(false)
123
+ lambda {Fasttrack::File.new(@test_data)}.must_raise Exempi::ExempiError
124
+ end
125
+
126
+ after do
127
+ FileUtils.remove_entry_secure @tmpdir
128
+ end
129
+ end
@@ -0,0 +1,172 @@
1
+ require 'fasttrack'
2
+
3
+ require 'mocha/setup'
4
+ require 'minitest/autorun'
5
+ require 'nokogiri'
6
+
7
+ describe Fasttrack::XMP do
8
+ before do
9
+ @test_data = File.join(__FILE__,"..","data","avchd.xmp")
10
+ end
11
+
12
+ it "should be able to create an empty XMP packet" do
13
+ xmp = Fasttrack::XMP.new
14
+ xmp.namespaces.must_be_empty
15
+ end
16
+
17
+ it "should return the correct namespaces" do
18
+ file = Fasttrack::File.new @test_data
19
+ file.xmp.namespaces.must_equal [Fasttrack::NAMESPACES[:exif],
20
+ Fasttrack::NAMESPACES[:tiff]]
21
+ end
22
+
23
+ it "should be able to fetch properties" do
24
+ file = Fasttrack::File.new @test_data
25
+ file.xmp['tiff:Make'].must_equal 'Sony'
26
+ file.xmp.get(:tiff, 'tiff:Make').must_equal 'Sony'
27
+ file.xmp.get(:tiff, 'Make').must_equal 'Sony'
28
+ # Test looking up the namespaces from the table
29
+ file.xmp.get(Fasttrack::NAMESPACES[:tiff],
30
+ 'Make').must_equal 'Sony'
31
+ # Test using the literal URI string
32
+ file.xmp.get('http://ns.adobe.com/tiff/1.0/',
33
+ 'Make').must_equal 'Sony'
34
+ end
35
+
36
+ it "should be able to set properties" do
37
+ file = Fasttrack::File.new @test_data
38
+ file.xmp['tiff:Make'] = 'Samsung'
39
+ file.xmp['tiff:Make'].must_equal 'Samsung'
40
+
41
+ file.xmp.set :tiff, 'tiff:Make', 'Canon'
42
+ file.xmp['tiff:Make'].must_equal 'Canon'
43
+
44
+ file.xmp.set :tiff, 'Make', 'Olympus'
45
+ file.xmp['tiff:Make'].must_equal 'Olympus'
46
+
47
+ file.xmp.set Fasttrack::NAMESPACES[:tiff],
48
+ 'Make', 'Panasonic'
49
+ file.xmp['tiff:Make'].must_equal 'Panasonic'
50
+
51
+ file.xmp.set 'http://ns.adobe.com/tiff/1.0/', 'Make', 'Pentax'
52
+ file.xmp['tiff:Make'].must_equal 'Pentax'
53
+ end
54
+
55
+ it "should be able to delete properties" do
56
+ file = Fasttrack::File.new @test_data
57
+ file.xmp.delete(:tiff, 'tiff:Make').must_equal 'Sony'
58
+ file.xmp['tiff:Make'].must_be_nil
59
+ end
60
+
61
+ it "should return nil when deleting a property which doesn't exist" do
62
+ xmp = Fasttrack::XMP.new
63
+ xmp.delete(:exif, 'foo').must_be_nil
64
+ end
65
+
66
+ it "should not decrement the namespace count when deleting a nonextant property" do
67
+ file = Fasttrack::File.new @test_data
68
+ file.xmp.instance_variable_get(:@namespaces)["http://ns.adobe.com/exif/1.0/"].must_equal 7
69
+
70
+ file.xmp.delete(:exif, 'foo')
71
+ file.xmp.instance_variable_get(:@namespaces)["http://ns.adobe.com/exif/1.0/"].must_equal 7
72
+ end
73
+
74
+ it "should be able to iterate over properties" do
75
+ file = Fasttrack::File.new @test_data
76
+ file.xmp.each.must_be_kind_of Enumerator
77
+ ary = file.xmp.each.to_a
78
+ ary.first.must_be_kind_of Array
79
+ ary.first.wont_be_empty
80
+ # yeah, the first entry has empty properties
81
+ # look at the hash later
82
+ ary.first[0..-2].must_equal ['http://ns.adobe.com/exif/1.0/', '', '']
83
+
84
+ # let's look at something with properties instead
85
+ ary[1].must_include 'http://ns.adobe.com/exif/1.0/'
86
+ ary[1].must_include 'exif:DateTimeOriginal'
87
+ ary[1].must_include '2012-03-17T11:45:16-04:00'
88
+
89
+ ary = file.xmp.map(&:last)
90
+ ary.first.must_be_kind_of Hash
91
+ ary.first.must_include :has_type
92
+ end
93
+
94
+ it "should be able to restrict iterations via namespace" do
95
+ file = Fasttrack::File.new @test_data
96
+ file.xmp.each_in_namespace(:exif).count.must_equal 8
97
+ end
98
+
99
+ it "should be able to rewind iterations" do
100
+ file = Fasttrack::File.new @test_data
101
+ enum = file.xmp.each
102
+ enum.next
103
+ enum.next
104
+ ary = enum.next
105
+ enum.rewind
106
+ enum.next.wont_equal ary
107
+ end
108
+
109
+ it "should correctly track namespace usage" do
110
+ file = Fasttrack::File.new @test_data
111
+ file.xmp.namespaces.must_be_kind_of Array
112
+ file.xmp.namespaces.count.must_equal 2
113
+ ns = file.xmp.instance_variable_get :@namespaces
114
+ ns.must_be_kind_of Hash
115
+ ns['http://ns.adobe.com/exif/1.0/'].must_equal 7
116
+
117
+ # is it properly decremented when we delete one of those properties?
118
+ date = file.xmp.delete :exif, 'exif:DateTimeOriginal'
119
+ ns['http://ns.adobe.com/exif/1.0/'].must_equal 6
120
+
121
+ # how about incremented if we add one?
122
+ file.xmp['exif:DateTimeOriginal'] = date
123
+ ns['http://ns.adobe.com/exif/1.0/'].must_equal 7
124
+
125
+ # is the hash udpated if we add a totally new namespace?
126
+ file.xmp['pdf:Foo'] = 'bar'
127
+ file.xmp.namespaces.must_include 'http://ns.adobe.com/pdf/1.3/'
128
+ end
129
+
130
+ it "should create copies with unique pointers" do
131
+ file = Fasttrack::File.new @test_data
132
+ xmp = file.xmp
133
+ xmp2 = xmp.dup
134
+ xmp.xmp_ptr.wont_equal xmp2.xmp_ptr
135
+ end
136
+
137
+ it "should be able to create XMP objects from XML strings" do
138
+ xml_string = File.read File.expand_path(@test_data)
139
+ xmp = Fasttrack::XMP.parse xml_string
140
+ xmp.must_be_kind_of Fasttrack::XMP
141
+ xmp['tiff:Make'].must_equal 'Sony'
142
+
143
+ xmp_from_file = Fasttrack::File.new(@test_data).xmp
144
+ xmp_from_file.must_equal xmp
145
+ end
146
+
147
+ it "should be able to serialize XMP to a string" do
148
+ xmp = Fasttrack::XMP.new
149
+ xmp['tiff:Make'] = 'Sony'
150
+ xml = Nokogiri::XML.parse xmp.serialize
151
+ xml.xpath("/x:xmpmeta/rdf:RDF/rdf:Description/tiff:Make",
152
+ 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
153
+ 'tiff' => 'http://ns.adobe.com/tiff/1.0/',
154
+ 'x' => 'adobe:ns:meta/').text.must_equal 'Sony'
155
+ end
156
+
157
+ it "should be able to create XMP objects from a file pointer" do
158
+ file = Exempi.xmp_files_new
159
+ Exempi.xmp_files_open file, File.expand_path(@test_data), :XMP_OPEN_READ
160
+
161
+ xmp = Fasttrack::XMP.from_file_pointer file
162
+ xmp['tiff:Make'].must_equal 'Sony'
163
+
164
+ Exempi.xmp_files_free file
165
+ end
166
+
167
+ it "should raise if Exempi fails to set properties" do
168
+ Exempi.stubs(:xmp_set_property).returns(false)
169
+
170
+ lambda {Fasttrack::XMP.new.set(:exif, 'Make', 'Foo')}.must_raise Exempi::ExempiError
171
+ end
172
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fasttrack
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Misty De Meo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: exempi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.2.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.2.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: mocha
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.13.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.13.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 1.5.5
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 1.5.5
69
+ description: |2
70
+ Fasttrack is an easy-to-use Ruby wrapper for
71
+ Exempi, a C library for managing XMP metadata.
72
+ Fasttrack provides a dead-easy, object-oriented
73
+ interface to Exempi's functions.
74
+ email:
75
+ - mistydemeo@gmail.com
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - ".gitignore"
81
+ - Gemfile
82
+ - LICENSE
83
+ - README.md
84
+ - Rakefile
85
+ - fasttrack.gemspec
86
+ - lib/fasttrack.rb
87
+ - lib/fasttrack/exceptions.rb
88
+ - lib/fasttrack/file.rb
89
+ - lib/fasttrack/namespaces.rb
90
+ - lib/fasttrack/version.rb
91
+ - lib/fasttrack/xmp.rb
92
+ - test/data/avchd.xmp
93
+ - test/data/image.jpg
94
+ - test/fasttrack_test.rb
95
+ - test/file_test.rb
96
+ - test/xmp_test.rb
97
+ homepage: https://github.com/mistydemeo/fasttrack
98
+ licenses: []
99
+ metadata: {}
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 2.4.5.1
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Ruby sugar for Exempi
120
+ test_files:
121
+ - test/data/avchd.xmp
122
+ - test/data/image.jpg
123
+ - test/fasttrack_test.rb
124
+ - test/file_test.rb
125
+ - test/xmp_test.rb
126
+ has_rdoc: