diff --git a/bin/avm2avm b/bin/avm2avm
new file mode 100755
index 0000000..70ca7f6
--- /dev/null
+++ b/bin/avm2avm
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+require 'thor'
+require 'avm/cli'
+
+AVM::CLI.start
+
diff --git a/lib/avm/cli.rb b/lib/avm/cli.rb
new file mode 100644
index 0000000..bf18cee
--- /dev/null
+++ b/lib/avm/cli.rb
@@ -0,0 +1,17 @@
+require 'thor'
+require 'avm/image'
+require 'pp'
+
+module AVM
+ class CLI < ::Thor
+ default_task :convert
+
+ desc 'convert', "Convert a file from one format to another"
+ def convert
+ data = $stdin.read
+
+ pp AVM::Image.from_xml(data).to_h
+ end
+ end
+end
+
diff --git a/lib/avm/creator.rb b/lib/avm/creator.rb
index 52b64a8..cadea35 100644
--- a/lib/avm/creator.rb
+++ b/lib/avm/creator.rb
@@ -75,12 +75,12 @@ module AVM
def from_xml(image, document)
contacts = []
document.get_refs do |refs|
- refs[:dublin_core].search('.//rdf:li').each do |name|
- contacts << { :name => name.text }
+ refs[:dublin_core].search('.//dc:creator//rdf:li').each do |name|
+ contacts << { :name => name.text.strip }
end
IPTC_MULTI_FIELD_MAP.each do |key, element_name|
- if node = refs[:iptc].at_xpath("//Iptc4xmpCore:#{element_name}")
+ if node = refs[:iptc].at_xpath(".//Iptc4xmpCore:#{element_name}")
node.text.split(',').collect(&:strip).each_with_index do |value, index|
contacts[index][key] = value
end
diff --git a/lib/avm/image.rb b/lib/avm/image.rb
index d4ebb52..281ee8d 100644
--- a/lib/avm/image.rb
+++ b/lib/avm/image.rb
@@ -11,12 +11,16 @@ module AVM
class Image
DUBLIN_CORE_FIELDS = [ :title, :description ]
+ PHOTOSHOP_SINGLES = {
+ 'Headline' => :headline,
+ 'DateCreated' => :date
+ }
+
AVM_SINGLE_FIELDS = [
'Distance.Notes',
'Spectral.Notes',
'ReferenceURL',
'Credit',
- 'Date',
'ID',
'Type',
'Image.ProductQuality',
@@ -40,7 +44,6 @@ module AVM
:spectral_notes,
:reference_url,
:credit,
- :date,
:id,
:type,
:quality,
@@ -64,7 +67,6 @@ module AVM
:spectral_notes,
:reference_url,
:credit,
- :string_date,
:id,
:image_type,
:image_quality,
@@ -141,6 +143,7 @@ module AVM
end
refs[:photoshop].add_child(%{#{headline}})
+ refs[:photoshop].add_child(%{#{string_date}})
AVM_SINGLES_FOR_MESSAGES.each do |tag, message|
if value = send(message)
@@ -229,14 +232,18 @@ module AVM
end
end
- if node = refs[:photoshop].at_xpath('./photoshop:Headline')
- options[:headline] = node.text
+ PHOTOSHOP_SINGLES.each do |tag, field|
+ if node = refs[:photoshop].at_xpath("./photoshop:#{tag}")
+ options[field] = node.text
+ end
end
if node = refs[:avm].at_xpath('./avm:Distance')
list_values = node.search('.//rdf:li').collect { |li| li.text }
case list_values.length
+ when 0
+ options[:light_years] = node.text
when 1
options[:light_years] = list_values.first
when 2
diff --git a/lib/avm/node.rb b/lib/avm/node.rb
new file mode 100644
index 0000000..465a265
--- /dev/null
+++ b/lib/avm/node.rb
@@ -0,0 +1,27 @@
+require 'delegate'
+
+module AVM
+ class Node < DelegateClass(Nokogiri::XML::Node)
+ def initialize(xmp, node)
+ @xmp, @node = xmp, node
+ super(@node)
+ end
+
+ def at_xpath(path)
+ if node = @node.at_xpath(path, @xmp.namespaces)
+ self.class.new(@xmp, node)
+ else
+ nil
+ end
+ end
+
+ def search(path)
+ self.class.from_nodeset(@xmp, @node.search(path, @xmp.namespaces))
+ end
+
+ def self.from_nodeset(xmp, nodeset)
+ nodeset.collect { |node| new(xmp, node) }
+ end
+ end
+end
+
diff --git a/lib/avm/xmp.rb b/lib/avm/xmp.rb
index e9bc4c4..50d763f 100644
--- a/lib/avm/xmp.rb
+++ b/lib/avm/xmp.rb
@@ -1,4 +1,5 @@
require 'nokogiri'
+require 'avm/node'
module AVM
class XMP
@@ -9,6 +10,15 @@ module AVM
'avm' => 'AVM'
}
+ REQUIRED_NAMESPACES = {
+ :x => "adobe:ns:meta/",
+ :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ :dc => "http://purl.org/dc/elements/1.1/",
+ :photoshop => "http://ns.adobe.com/photoshop/1.0/",
+ :avm => "http://www.communicatingastronomy.org/avm/1.0/",
+ :Iptc4xmpCore => "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
+ }
+
attr_reader :doc
def initialize(doc = nil)
@@ -25,37 +35,73 @@ module AVM
new(Nokogiri::XML(string))
end
+ def ensure_xmlns(string)
+ string.gsub(%r{([@])(\w+):}) { |all, matches| $1 + (prefix_map[$2] || $2) + ':' }
+ end
+
+ alias :% :ensure_xmlns
+
+ def ensure_xpath(path)
+ [ ensure_xmlns(path), namespaces ]
+ end
+
+ def search(path, node = doc)
+ node.search(*ensure_xpath(path))
+ end
+
+ def at_xpath(path, node = doc)
+ node.at_xpath(*ensure_xpath(path))
+ end
+
+ def namespaces
+ @namespaces ||= doc.document.collect_namespaces
+ end
+
private
+ def prefix_map
+ @prefix_map ||= Hash[doc.document.collect_namespaces.collect { |prefix, namespace|
+ prefix = prefix.gsub('xmlns:', '')
+ result = nil
+
+ REQUIRED_NAMESPACES.each do |original_prefix, target_namespace|
+ result = [ original_prefix.to_s, prefix ] if namespace == target_namespace
+ end
+ result
+ }.compact]
+ end
+
def ensure_namespaces!
- {
- :x => "adobe:ns:meta/",
- :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
- :dc => "http://purl.org/dc/elements/1.1/",
- :photoshop => "http://ns.adobe.com/photoshop/1.0/",
- :avm => "http://www.communicatingastronomy.org/avm/1.0/",
- :Iptc4xmpCore => "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
- }.each do |namespace, url|
- doc.root.add_namespace_definition(namespace.to_s, url)
+ existing = doc.document.collect_namespaces
+
+ REQUIRED_NAMESPACES.each do |namespace, url|
+ doc.root.add_namespace_definition(namespace.to_s, url) if !existing.values.include?(url)
end
end
def ensure_descriptions_findable!
added = []
- doc.search('//rdf:Description').each do |description|
+ search('//rdf:Description').each do |description|
if first_child = description.first_element_child
if first_child.namespace
prefix = first_child.namespace.prefix
- description['rdf:about'] = PREFIXES[prefix]
- added << prefix
+ if prefix_description = PREFIXES[prefix_map.index(prefix)]
+ description[self % 'rdf:about'] = prefix_description
+ added << prefix
+ end
end
end
end
+ if !at_xpath('//rdf:RDF')
+ doc.first_element_child.add_child(self % '')
+ end
+
+
PREFIXES.each do |prefix, about|
if !added.include?(prefix)
- doc.at_xpath('//rdf:RDF').add_child(%{})
+ at_xpath('//rdf:RDF').add_child(self % %{})
end
end
end
@@ -77,7 +123,7 @@ module AVM
end
def at_rdf_description(about)
- @doc.at_xpath(%{//rdf:Description[@rdf:about="#{about}"]})
+ AVM::Node.new(self, at_xpath(%{//rdf:Description[@rdf:about="#{about}"]}))
end
def empty_xml_doc
diff --git a/ruby-avm-library.gemspec b/ruby-avm-library.gemspec
index ef7efee..9d985b9 100644
--- a/ruby-avm-library.gemspec
+++ b/ruby-avm-library.gemspec
@@ -23,4 +23,5 @@ Gem::Specification.new do |s|
s.add_development_dependency 'mocha'
s.add_dependency 'nokogiri'
+ s.add_dependency 'thor'
end
diff --git a/spec/avm/cli_spec.rb b/spec/avm/cli_spec.rb
new file mode 100644
index 0000000..e69de29
diff --git a/spec/avm/image_spec.rb b/spec/avm/image_spec.rb
index 12bdc5d..d1a66d9 100644
--- a/spec/avm/image_spec.rb
+++ b/spec/avm/image_spec.rb
@@ -157,6 +157,15 @@ describe AVM::Image do
end
context "image in it" do
+ context 'distance as a single value, assume light years' do
+ let(:file_path) { 'spec/sample_files/image/single_value_light_years.xmp' }
+
+ has_most_options
+
+ its(:redshift) { should be_nil }
+ its(:light_years) { should == light_years }
+ end
+
context "distance in light years" do
let(:file_path) { 'spec/sample_files/image/light_years.xmp' }
@@ -197,7 +206,7 @@ describe AVM::Image do
it "should have basic tags" do
xml.at_xpath('//rdf:RDF').should_not be_nil
xml.search('//rdf:RDF/rdf:Description').should_not be_empty
- avm.at_xpath('./avm:Date').should be_nil
+ photoshop.at_xpath('./photoshop:DateCreated').should_not be_nil
end
end
@@ -221,7 +230,7 @@ describe AVM::Image do
xpath_text(avm, './avm:Spectral.Notes').should == spectral_notes
xpath_text(avm, './avm:ReferenceURL').should == reference_url
xpath_text(avm, './avm:Credit').should == credit
- xpath_text(avm, './avm:Date').should == date
+ xpath_text(photoshop, './photoshop:DateCreated').should == date
xpath_text(avm, './avm:ID').should == id
xpath_text(avm, './avm:Type').should == type
xpath_text(avm, './avm:Image.ProductQuality').should == image_quality
diff --git a/spec/avm/xmp_spec.rb b/spec/avm/xmp_spec.rb
index d2c30f3..b062122 100644
--- a/spec/avm/xmp_spec.rb
+++ b/spec/avm/xmp_spec.rb
@@ -24,11 +24,63 @@ describe AVM::XMP do
end
end
- describe '.from_string' do
+ describe 'xml from string' do
let(:xmp) { self.class.describes.from_string(string) }
- let(:string) { '' }
+ let(:doc) { xmp.doc }
- specify { xmp.doc.at_xpath('//node').should_not be_nil }
+ describe '.from_string' do
+ let(:string) { '' }
+
+ specify { xmp.doc.at_xpath('//node').should_not be_nil }
+ end
+
+ describe '#ensure_namespaces! and #ensure_xmlns' do
+ let(:rdf_namespace) { AVM::XMP::REQUIRED_NAMESPACES[:rdf] }
+
+ def self.all_default_namespaces
+ it "should have all the namespaces with the default prefixes" do
+ namespaces = doc.document.collect_namespaces
+
+ namespaces_to_test = AVM::XMP::REQUIRED_NAMESPACES.dup
+ yield namespaces_to_test if block_given?
+
+ namespaces_to_test.each do |prefix, namespace|
+ if namespace
+ namespaces["xmlns:#{prefix}"].should == namespace
+ end
+ end
+ end
+ end
+
+ before { doc }
+
+ context 'none of the namespaces exist' do
+ let(:string) { '' }
+
+ all_default_namespaces
+
+ specify { xmp.ensure_xmlns('.//rdf:what').should == './/rdf:what' }
+ end
+
+ context 'one namespace exists with the same prefix' do
+ let(:string) { %{} }
+
+ all_default_namespaces
+
+ specify { xmp.ensure_xmlns('.//rdf:what').should == './/rdf:what' }
+ end
+
+ context 'one namespace exists with a different prefix' do
+ let(:string) { %{} }
+
+ all_default_namespaces { |namespaces|
+ namespaces.delete(:rdf)
+ namespaces[:r] = AVM::XMP::REQUIRED_NAMESPACES[:rdf]
+ }
+
+ specify { xmp.ensure_xmlns('.//rdf:what').should == './/r:what' }
+ end
+ end
end
describe '#ensure_descriptions_findable!' do
@@ -46,7 +98,7 @@ describe AVM::XMP do
let(:content) { '' }
[ 'Dublin Core', 'IPTC', 'Photoshop', 'AVM' ].each do |which|
- specify { xmp.doc.at_xpath(%{//rdf:Description[@rdf:about="#{which}"]}).children.should be_empty }
+ specify { xmp.at_xpath(%{//rdf:Description[@rdf:about="#{which}"]}).children.should be_empty }
end
end
@@ -67,8 +119,28 @@ describe AVM::XMP do
XML
[ 'Dublin Core', 'IPTC', 'Photoshop', 'AVM' ].each do |which|
- specify { xmp.doc.at_xpath(%{//rdf:Description[@rdf:about="#{which}"]}).should_not be_nil }
+ specify { xmp.at_xpath(%{//rdf:Description[@rdf:about="#{which}"]}).should_not be_nil }
end
end
+
+ context 'has a namespace it should know about with a different prefix' do
+ let(:content) { <<-XML }
+
+
+
+ XML
+
+ specify { xmp.at_xpath(%{//rdf:Description[@rdf:about="Dublin Core"]}).should_not be_nil }
+ end
+
+ context 'has a namespace it knows nothing about' do
+ let(:content) { <<-XML }
+
+
+
+ XML
+
+ it { expect { xmp }.to_not raise_error }
+ end
end
end
diff --git a/spec/sample_files/creator/one_creator.xmp b/spec/sample_files/creator/one_creator.xmp
index 8518979..0e923eb 100644
--- a/spec/sample_files/creator/one_creator.xmp
+++ b/spec/sample_files/creator/one_creator.xmp
@@ -5,6 +5,11 @@
John Bintz
+
+
+ This is an image
+
+
diff --git a/spec/sample_files/image/both.xmp b/spec/sample_files/image/both.xmp
index 79f615c..228fce5 100644
--- a/spec/sample_files/image/both.xmp
+++ b/spec/sample_files/image/both.xmp
@@ -22,6 +22,7 @@
Headline
+ 2010-01-01
@@ -29,7 +30,6 @@
Distance Notes
Reference URL
Credit
- 2010-01-01
ID
Observation
Good
diff --git a/spec/sample_files/image/light_years.xmp b/spec/sample_files/image/light_years.xmp
index 01c25e0..a2c0e51 100644
--- a/spec/sample_files/image/light_years.xmp
+++ b/spec/sample_files/image/light_years.xmp
@@ -21,6 +21,7 @@
Headline
+ 2010-01-01
@@ -28,7 +29,6 @@
Distance Notes
Reference URL
Credit
- 2010-01-01
ID
Observation
Good
diff --git a/spec/sample_files/image/redshift.xmp b/spec/sample_files/image/redshift.xmp
index 0023b24..5106eea 100644
--- a/spec/sample_files/image/redshift.xmp
+++ b/spec/sample_files/image/redshift.xmp
@@ -22,6 +22,7 @@
Headline
+ 2010-01-01
@@ -29,7 +30,6 @@
Distance Notes
Reference URL
Credit
- 2010-01-01
ID
Observation
Good
diff --git a/spec/sample_files/image/single_value_light_years.xmp b/spec/sample_files/image/single_value_light_years.xmp
new file mode 100644
index 0000000..ec32efe
--- /dev/null
+++ b/spec/sample_files/image/single_value_light_years.xmp
@@ -0,0 +1,77 @@
+
+
+
+ :
+
+ My title
+
+
+
+
+ Description
+
+
+
+
+
+
+
+
+ Headline
+ 2010-01-01
+
+
+ Spectral Notes
+ Distance Notes
+ Reference URL
+ Credit
+ ID
+ Observation
+ Good
+ Light years
+ ICRS
+ 100
+
+
+ 100
+ 50
+
+
+
+
+ 200
+ 150
+
+
+
+
+ 25
+ 15
+
+
+
+
+ 40
+ 35
+
+
+ 10
+ TAN
+ Full
+ Spatial Notes
+ FITS header
+
+
+ 1
+ 2
+ 3
+ 4
+
+
+
+
+