fasttrack 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: