fasttrack 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +51 -0
- data/Rakefile +11 -0
- data/fasttrack.gemspec +28 -0
- data/lib/fasttrack.rb +18 -0
- data/lib/fasttrack/exceptions.rb +5 -0
- data/lib/fasttrack/file.rb +167 -0
- data/lib/fasttrack/namespaces.rb +12 -0
- data/lib/fasttrack/version.rb +3 -0
- data/lib/fasttrack/xmp.rb +362 -0
- data/test/data/avchd.xmp +25 -0
- data/test/data/image.jpg +0 -0
- data/test/fasttrack_test.rb +11 -0
- data/test/file_test.rb +129 -0
- data/test/xmp_test.rb +172 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/fasttrack.gemspec
ADDED
@@ -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
|
data/lib/fasttrack.rb
ADDED
@@ -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,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,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
|
data/test/data/avchd.xmp
ADDED
@@ -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'?>
|
data/test/data/image.jpg
ADDED
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
|
data/test/file_test.rb
ADDED
@@ -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
|
data/test/xmp_test.rb
ADDED
@@ -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:
|