├── Gemfile ├── lib ├── xmpr │ └── version.rb └── xmpr.rb ├── .gitignore ├── test ├── test_helper.rb ├── xmpr_test.rb └── fixtures │ └── xmp.xml ├── .travis.yml ├── Rakefile ├── xmpr.gemspec ├── LICENSE └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/xmpr/version.rb: -------------------------------------------------------------------------------- 1 | module XMPR 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /Gemfile.lock 3 | /doc/ 4 | /pkg/ 5 | /tmp/ 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "xmpr" 3 | 4 | require "minitest/autorun" 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.1 5 | - 2.2 6 | - 2.3 7 | - 2.4 8 | - 2.5 9 | before_install: gem install bundler 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /xmpr.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require_relative "lib/xmpr/version" 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "xmpr" 6 | spec.version = XMPR::VERSION 7 | spec.author = "Samuel Cochran" 8 | spec.email = "sj26@sj26.com" 9 | spec.summary = "Parse XMP data" 10 | spec.description = "Parse XMP data extracted from an image into rich data types" 11 | spec.homepage = "https://github.com/sj26/xmpr" 12 | spec.license = "MIT" 13 | 14 | spec.required_ruby_version = "~> 2.1" 15 | 16 | spec.files = Dir["README.md", "LICENSE", "lib/**/*"] 17 | spec.test_files = Dir["test/**/*"] 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "nokogiri", "~> 1.6" 21 | 22 | spec.add_development_dependency "bundler", "~> 2.0" 23 | spec.add_development_dependency "rake" 24 | spec.add_development_dependency "minitest" 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Samuel Cochran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/xmpr_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "test_helper" 4 | 5 | class XMPRTest < MiniTest::Test 6 | def xmp 7 | XMPR.parse(File.read("test/fixtures/xmp.xml")) 8 | end 9 | 10 | def test_embedded_attribute 11 | assert_equal "Kategoria", xmp["photoshop", "Category"] 12 | end 13 | 14 | def test_explicit_namespace 15 | assert_equal "Miejsce", xmp["http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", "Location"] 16 | end 17 | 18 | def test_standalone_alt_attribute 19 | assert_equal "Tytuł zdjęcia", xmp["dc", "title"] 20 | end 21 | 22 | def test_standalone_alt_attribute_with_lang 23 | assert_equal "Something else", xmp["dc", "title", lang: "en-US"] 24 | end 25 | 26 | def test_standalone_bag_attribute 27 | assert_equal ["Słowa kluczowe", "Opis zdjęcia"], xmp["dc", "subject"] 28 | end 29 | 30 | def test_standalone_seq_attribute 31 | assert_equal ["John Doe", "Jane Smith"], xmp["dc", "creator"] 32 | end 33 | 34 | def test_to_hash 35 | assert_equal({"http://www.w3.org/1999/02/22-rdf-syntax-ns#"=>{"about"=>""}, "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"=>{"Location"=>"Miejsce"}, "http://ns.adobe.com/photoshop/1.0/"=>{"Category"=>"Kategoria"}, "http://purl.org/dc/elements/1.1/"=>{"title"=>"Tytuł zdjęcia", "subject"=>["Słowa kluczowe", "Opis zdjęcia"], "creator"=>["John Doe", "Jane Smith"]}}, xmp.to_hash) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/fixtures/xmp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | Tytuł zdjęcia 14 | Something else 15 | 16 | 17 | 18 | 19 | Słowa kluczowe 20 | Opis zdjęcia 21 | 22 | 23 | 24 | 25 | John Doe 26 | Jane Smith 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XMP Reader 2 | 3 | [![Build Status](https://travis-ci.org/sj26/xmpr?branch=master)](https://travis-ci.org/sj26/xmpr) 4 | 5 | XMP Reader in Ruby. Parse XMP data extracted from an image into rich data types. 6 | 7 | ## Usage 8 | 9 | Use something like imagemagick to extract the XMP, then read it with this class: 10 | 11 | ```ruby 12 | require "xmpr" 13 | raw_xmp = `convert image.jpg xmp:-` 14 | xmp = XMPR.parse(raw_xmp) 15 | xmp["dc", "title"] # => "Amazing Photo" 16 | xmp["photoshop", "Category"] # => "summer" 17 | xmp["photoshop", "SupplementalCategories"] # => ["morning", "sea"] 18 | ``` 19 | 20 | The xmp instance fetches namespaced attributes. You can use fully qualified namespaces, or some namespaces have shortcuts: 21 | 22 | ``` 23 | xmp["http://purl.org/dc/elements/1.1/", "title"] # => "Amazing Photo" 24 | xmp["dc", "title"] # => "Amazing Photo" (same thing) 25 | ``` 26 | 27 | The following namespaces have shortcuts: 28 | 29 | * `aux` — `http://ns.adobe.com/exif/1.0/aux/` 30 | * `cc` — `http://creativecommons.org/ns#` ([Creative Commons](http://creativecommons.org)) 31 | * `crs` — `http://ns.adobe.com/camera-raw-settings/1.0/` 32 | * `dc` — `http://purl.org/dc/elements/1.1/` ([Dublin Core](http://dublincore.org/)) 33 | * `exif` — `http://ns.adobe.com/exif/1.0/` 34 | * `Iptc4xmpCore` — `http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/` ([IPTC](http://iptc.org/)) 35 | * `pdf` — `http://ns.adobe.com/pdf/1.3/` 36 | * `photoshop` — `http://ns.adobe.com/photoshop/1.0/` 37 | * `rdf` — `http://www.w3.org/1999/02/22-rdf-syntax-ns#` 38 | * `tiff` — `http://ns.adobe.com/tiff/1.0/` 39 | * `x` — `adobe:ns:meta/` 40 | * `xap` — `http://ns.adobe.com/xap/1.0/` 41 | * `xmp` — `http://ns.adobe.com/xap/1.0/` ([XMP](http://www.adobe.com/products/xmp.html)) 42 | * `xmpidq` — `http://ns.adobe.com/xmp/Identifier/qual/1.0/` 43 | * `xmpBJ` — `http://ns.adobe.com/xap/1.0/bj/` 44 | * `xmpRights` — `http://ns.adobe.com/xap/1.0/rights/` 45 | * `xmpMM` — `http://ns.adobe.com/xap/1.0/mm/` 46 | * `xmpTPg` — `http://ns.adobe.com/xap/1.0/t/pg/` 47 | 48 | ## Thanks 49 | 50 | Refactored from [XMP][xmp-gem]. Inspired by [ExifTool][exiftool]. 51 | 52 | [xmp-gem]: https://github.com/amberbit/xmp 53 | [exiftool]: http://www.sno.phy.queensu.ca/~phil/exiftool/ 54 | 55 | ## References 56 | 57 | * [XMP specification](https://www.adobe.com/devnet/xmp.html) 58 | 59 | ## License 60 | 61 | MIT license, see [LICENSE](LICENSE). 62 | -------------------------------------------------------------------------------- /lib/xmpr.rb: -------------------------------------------------------------------------------- 1 | # XMPR 2 | # 3 | # XMP reader 4 | # 5 | # XMPR has some known namespaces, like "dc" for dublin core. See the NAMESPACES 6 | # constants for a complete list. 7 | # 8 | # ## Example 9 | # 10 | # xmp = XMPR.parse(File.read('xmp.xml')) 11 | # xmp["dc", "title"] # => "Amazing Photo" 12 | # xmp["photoshop", "Category"] # => "summer" 13 | # xmp["photoshop", "SupplementalCategories"] # => ["morning", "sea"] 14 | # 15 | # ## References 16 | # 17 | # * https://www.aiim.org/documents/standards/xmpspecification.pdf 18 | # 19 | module XMPR 20 | # Namespace shortcuts, and fallbacks for undeclared namespaces. 21 | NAMESPACES = { 22 | "aux" => "http://ns.adobe.com/exif/1.0/aux/", 23 | "cc" => "http://creativecommons.org/ns#", 24 | "crs" => "http://ns.adobe.com/camera-raw-settings/1.0/", 25 | "dc" => "http://purl.org/dc/elements/1.1/", 26 | "exif" => "http://ns.adobe.com/exif/1.0/", 27 | "Iptc4xmpCore" => "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", 28 | "pdf" => "http://ns.adobe.com/pdf/1.3/", 29 | "photoshop" => "http://ns.adobe.com/photoshop/1.0/", 30 | "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 31 | "tiff" => "http://ns.adobe.com/tiff/1.0/", 32 | "x" => "adobe:ns:meta/", 33 | "xap" => "http://ns.adobe.com/xap/1.0/", 34 | "xmp" => "http://ns.adobe.com/xap/1.0/", 35 | "xmpidq" => "http://ns.adobe.com/xmp/Identifier/qual/1.0/", 36 | "xmpBJ" => "http://ns.adobe.com/xap/1.0/bj/", 37 | "xmpRights" => "http://ns.adobe.com/xap/1.0/rights/", 38 | "xmpMM" => "http://ns.adobe.com/xap/1.0/mm/", 39 | "xmpTPg" => "http://ns.adobe.com/xap/1.0/t/pg/", 40 | } 41 | 42 | DEFAULT_NAMESPACE = "http://ns.adobe.com/xap/1.0/" 43 | 44 | def self.parse(value) 45 | XMP.new(value) 46 | end 47 | 48 | class XMP 49 | # Nokogiri document of parsed xml 50 | attr_reader :xml 51 | 52 | def initialize(source) 53 | require "nokogiri" 54 | @xml = Nokogiri::XML(source) 55 | end 56 | 57 | def [](namespace_or_name, name=nil, **options) 58 | if name 59 | namespace = namespace_or_name 60 | else 61 | namespace, name = DEFAULT_NAMESPACE, namespace_or_name 62 | end 63 | 64 | if NAMESPACES.has_key? namespace 65 | namespace = NAMESPACES[namespace] 66 | end 67 | 68 | embedded_attribute(namespace, name) || 69 | standalone_attribute(namespace, name, **options) 70 | end 71 | 72 | def to_hash 73 | {}.tap do |hash| 74 | xml.at("//rdf:Description", NAMESPACES).attributes.each do |(_, attribute)| 75 | hash[attribute.namespace.href] ||= {} 76 | hash[attribute.namespace.href][attribute.name] = attribute.value 77 | end 78 | xml.xpath("//rdf:Description/*", NAMESPACES).each do |element| 79 | hash[element.namespace.href] ||= {} 80 | hash[element.namespace.href][element.name] = standalone_value(element, lang: nil) 81 | end 82 | end 83 | end 84 | 85 | private 86 | 87 | def embedded_attribute(namespace, name) 88 | if element = xml.at("//rdf:Description[@ns:#{name}]", "rdf" => NAMESPACES["rdf"], "ns" => namespace) 89 | element.attribute_with_ns(name, namespace).text 90 | end 91 | end 92 | 93 | def standalone_attribute(namespace, name, lang: nil) 94 | if element = xml.xpath("//ns:#{name}", "ns" => namespace).first 95 | standalone_value(element, lang: lang) 96 | end 97 | end 98 | 99 | def standalone_value(element, lang:) 100 | if alt_element = element.xpath("./rdf:Alt", NAMESPACES).first 101 | alt_value(alt_element, lang: lang) 102 | elsif array_element = element.xpath("./rdf:Bag | ./rdf:Seq", NAMESPACES).first 103 | array_value(array_element) 104 | end 105 | end 106 | 107 | def alt_value(element, lang:) 108 | if lang && item = element.xpath("./rdf:li[@xml:lang=#{lang.inspect}]", NAMESPACES).first 109 | item.text 110 | elsif item = element.xpath(%{./rdf:li[@xml:lang="x-default"]}, NAMESPACES).first 111 | item.text 112 | elsif item = element.xpath("./rdf:li", NAMESPACES).first 113 | item.text 114 | end 115 | end 116 | 117 | def array_value(element) 118 | element.xpath("./rdf:li", NAMESPACES).map(&:text) 119 | end 120 | end 121 | end 122 | --------------------------------------------------------------------------------