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.
- 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:
|