iiif-presentation 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +2 -0
- data/LICENSE +23 -0
- data/README.md +173 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/gemfiles/rails3.gemfile +5 -0
- data/gemfiles/rails4.gemfile +5 -0
- data/iiif-presentation.gemspec +28 -0
- data/lib/active_support/ordered_hash.rb +147 -0
- data/lib/iiif/hash_behaviours.rb +150 -0
- data/lib/iiif/presentation.rb +25 -0
- data/lib/iiif/presentation/abstract_resource.rb +75 -0
- data/lib/iiif/presentation/annotation.rb +25 -0
- data/lib/iiif/presentation/annotation_list.rb +28 -0
- data/lib/iiif/presentation/canvas.rb +45 -0
- data/lib/iiif/presentation/collection.rb +29 -0
- data/lib/iiif/presentation/image_resource.rb +115 -0
- data/lib/iiif/presentation/layer.rb +34 -0
- data/lib/iiif/presentation/manifest.rb +39 -0
- data/lib/iiif/presentation/range.rb +32 -0
- data/lib/iiif/presentation/resource.rb +21 -0
- data/lib/iiif/presentation/sequence.rb +35 -0
- data/lib/iiif/service.rb +418 -0
- data/spec/fixtures/manifests/complete_from_spec.json +171 -0
- data/spec/fixtures/manifests/minimal.json +40 -0
- data/spec/fixtures/manifests/service_only.json +11 -0
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +159 -0
- data/spec/integration/iiif/presentation/image_resource_spec.rb +123 -0
- data/spec/integration/iiif/service_spec.rb +211 -0
- data/spec/spec_helper.rb +104 -0
- data/spec/unit/active_support/ordered_hash_spec.rb +155 -0
- data/spec/unit/iiif/hash_behaviours_spec.rb +569 -0
- data/spec/unit/iiif/presentation/abstract_resource_spec.rb +133 -0
- data/spec/unit/iiif/presentation/annotation_list_spec.rb +7 -0
- data/spec/unit/iiif/presentation/annotation_spec.rb +7 -0
- data/spec/unit/iiif/presentation/canvas_spec.rb +40 -0
- data/spec/unit/iiif/presentation/collection_spec.rb +54 -0
- data/spec/unit/iiif/presentation/image_resource_spec.rb +13 -0
- data/spec/unit/iiif/presentation/layer_spec.rb +38 -0
- data/spec/unit/iiif/presentation/manifest_spec.rb +89 -0
- data/spec/unit/iiif/presentation/range_spec.rb +43 -0
- data/spec/unit/iiif/presentation/resource_spec.rb +16 -0
- data/spec/unit/iiif/presentation/sequence_spec.rb +110 -0
- data/spec/unit/iiif/presentation/shared_examples/abstract_resource_only_keys.rb +43 -0
- data/spec/unit/iiif/presentation/shared_examples/any_type_keys.rb +33 -0
- data/spec/unit/iiif/presentation/shared_examples/array_only_keys.rb +44 -0
- data/spec/unit/iiif/presentation/shared_examples/int_only_keys.rb +49 -0
- data/spec/unit/iiif/presentation/shared_examples/string_only_keys.rb +29 -0
- data/spec/unit/iiif/service_spec.rb +10 -0
- metadata +262 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'service')
|
2
|
+
%w{
|
3
|
+
abstract_resource
|
4
|
+
annotation
|
5
|
+
annotation_list
|
6
|
+
canvas
|
7
|
+
collection
|
8
|
+
layer
|
9
|
+
manifest
|
10
|
+
resource
|
11
|
+
image_resource
|
12
|
+
sequence
|
13
|
+
range
|
14
|
+
}.each do |f|
|
15
|
+
require File.join(File.dirname(__FILE__), 'presentation', f)
|
16
|
+
end
|
17
|
+
|
18
|
+
module IIIF
|
19
|
+
module Presentation
|
20
|
+
CONTEXT ||= 'http://iiif.io/api/presentation/2/context.json'
|
21
|
+
|
22
|
+
class MissingRequiredKeyError < StandardError; end
|
23
|
+
class IllegalValueError < StandardError; end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../service')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class AbstractResource < Service
|
6
|
+
|
7
|
+
# Every subclass should override the following five methods where
|
8
|
+
# appropriate, see Subclasses for how.
|
9
|
+
def required_keys
|
10
|
+
%w{ @type }
|
11
|
+
end
|
12
|
+
|
13
|
+
def any_type_keys # these are allowed on all classes
|
14
|
+
%w{ label description thumbnail attribution license logo see_also
|
15
|
+
related within }
|
16
|
+
end
|
17
|
+
|
18
|
+
def string_only_keys
|
19
|
+
%w{ viewing_hint } # should any of the any_type_keys be here?
|
20
|
+
end
|
21
|
+
|
22
|
+
def array_only_keys
|
23
|
+
%w{ metadata }
|
24
|
+
end
|
25
|
+
|
26
|
+
def abstract_resource_only_keys
|
27
|
+
super + [ { key: 'service', type: IIIF::Service } ]
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash_only_keys
|
31
|
+
%w{ }
|
32
|
+
end
|
33
|
+
|
34
|
+
def int_only_keys
|
35
|
+
%w{ }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Not every subclass is allowed to have viewingDirect, but when it is,
|
39
|
+
# it must be one of these values
|
40
|
+
def legal_viewing_direction_values
|
41
|
+
%w{ left-to-right right-to-left top-to-bottom bottom-to-top }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initialize a Presentation node
|
45
|
+
# @param [Hash] hsh - Anything in this hash will be added to the Object.'
|
46
|
+
# Order is only guaranteed if an ActiveSupport::OrderedHash is passed.
|
47
|
+
# @param [boolean] include_context (default: false). Pass true if the'
|
48
|
+
# context should be included.
|
49
|
+
def initialize(hsh={})
|
50
|
+
if self.class == IIIF::Presentation::AbstractResource
|
51
|
+
raise "#{self.class} is an abstract class. Please use one of its subclasses."
|
52
|
+
end
|
53
|
+
super(hsh)
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
# Options:
|
58
|
+
# * force: (true|false). Skips validations.
|
59
|
+
# * include_context: (true|false). Adds the @context to the top of the
|
60
|
+
# document if it doesn't exist. Default: true.
|
61
|
+
# * sort_json_ld_keys: (true|false). Brings all properties starting with
|
62
|
+
# '@'. Default: true. to the top of the document and sorts them.
|
63
|
+
def to_ordered_hash(opts={})
|
64
|
+
include_context = opts.fetch(:include_context, true)
|
65
|
+
if include_context && !self.has_key?('@context')
|
66
|
+
self['@context'] = IIIF::Presentation::CONTEXT
|
67
|
+
end
|
68
|
+
super(opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Annotation < AbstractResource
|
6
|
+
|
7
|
+
TYPE = 'oa:Annotation'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ motivation }
|
11
|
+
end
|
12
|
+
|
13
|
+
def abstract_resource_only_keys
|
14
|
+
super + [ { key: 'resource', type: IIIF::Presentation::Resource } ]
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(hsh={})
|
18
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
19
|
+
hsh['motivation'] = 'sc:painting' unless hsh.has_key? 'motivation'
|
20
|
+
super(hsh)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class AnnotationList < AbstractResource
|
6
|
+
|
7
|
+
TYPE = 'sc:AnnotationList'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ @id }
|
11
|
+
end
|
12
|
+
|
13
|
+
def array_only_keys;
|
14
|
+
super + %w{ resources };
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(hsh={})
|
18
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
19
|
+
super(hsh)
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate
|
23
|
+
# Each member or resources must be a kind of Annotation
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Canvas < AbstractResource
|
6
|
+
|
7
|
+
# TODO (?) a simple 'Image Canvas' constructor.
|
8
|
+
|
9
|
+
TYPE = 'sc:Canvas'
|
10
|
+
|
11
|
+
def required_keys
|
12
|
+
super + %w{ @id width height label }
|
13
|
+
end
|
14
|
+
|
15
|
+
def any_type_keys
|
16
|
+
super + %w{ }
|
17
|
+
end
|
18
|
+
|
19
|
+
def array_only_keys
|
20
|
+
super + %w{ images other_content }
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: test and validate
|
24
|
+
def int_only_keys
|
25
|
+
super + %w{ width height }
|
26
|
+
end
|
27
|
+
|
28
|
+
def legal_viewing_hint_values
|
29
|
+
super + %w{ non-paged }
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(hsh={})
|
33
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
34
|
+
super(hsh)
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
# all members of images must be an annotation
|
39
|
+
# all members of otherContent must be an annotation list
|
40
|
+
super
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Collection < AbstractResource
|
6
|
+
|
7
|
+
TYPE = 'sc:Collection'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ @id label }
|
11
|
+
end
|
12
|
+
|
13
|
+
def array_only_keys
|
14
|
+
super + %w{ collections manifests }
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(hsh={})
|
18
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
19
|
+
super(hsh)
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate
|
23
|
+
# each member of collections and manifests must be a Hash
|
24
|
+
# each member of collections and manifests MUST have @id, @type, and label
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'resource')
|
2
|
+
require 'faraday'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module IIIF
|
6
|
+
module Presentation
|
7
|
+
class ImageResource < Resource
|
8
|
+
|
9
|
+
TYPE = 'dctypes:Image'
|
10
|
+
|
11
|
+
def int_only_keys
|
12
|
+
super + %w{ width height }
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(hsh={})
|
16
|
+
hsh['@type'] = 'dcterms:Image' unless hsh.has_key? '@type'
|
17
|
+
super(hsh)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg'
|
22
|
+
IMAGE_API_CONTEXT = 'http://iiif.io/api/image/2/context.json'
|
23
|
+
DEFAULT_FORMAT = 'image/jpeg'
|
24
|
+
# Create a new ImageResource that includes a IIIF Image API Service
|
25
|
+
# See http://iiif.io/api/presentation/2.0/#image-resources
|
26
|
+
#
|
27
|
+
# Params
|
28
|
+
# * :service_id (required) - The base URI for the image on the image
|
29
|
+
# server.
|
30
|
+
# * :resource_id - The id for the resource; if supplied this should
|
31
|
+
# resolve to an actual image. Default:
|
32
|
+
# "#{:service_id}/full/!200,200/0/default.jpg"
|
33
|
+
# * :format - The format of the image that is returned when
|
34
|
+
# `:resource_id` is resolved. Default: 'image/jpeg'
|
35
|
+
# * :height (Integer)
|
36
|
+
# * :profile (String)
|
37
|
+
# * :width (Integer) - If width, height, and profile are not supplied,
|
38
|
+
# this method will try to get the info from the server (based on
|
39
|
+
# :resource_id) and raise an Exception if this is not possible for
|
40
|
+
# some reason.
|
41
|
+
# * :copy_info (bool)- Even if width and height are supplied, try to
|
42
|
+
# get the info.json from the server and copy it in. Default: false
|
43
|
+
#
|
44
|
+
# Raises:
|
45
|
+
# * KeyError if `:service_id` is not supplied
|
46
|
+
# * Expections related to HTTP problems if a call to an image server fails
|
47
|
+
#
|
48
|
+
# The result is something like this:
|
49
|
+
#
|
50
|
+
# {
|
51
|
+
# "@id":"http://www.example.org/iiif/book1/res/page1.jpg",
|
52
|
+
# "@type":"dctypes:Image",
|
53
|
+
# "format":"image/jpeg",
|
54
|
+
# "service": {
|
55
|
+
# "@context": "http://iiif.io/api/image/2/context.json",
|
56
|
+
# "@id":"http://www.example.org/images/book1-page1",
|
57
|
+
# "profile":"http://iiif.io/api/image/2/profiles/level2.json",
|
58
|
+
# },
|
59
|
+
# "height":2000,
|
60
|
+
# "width":1500
|
61
|
+
# }
|
62
|
+
#
|
63
|
+
def create_image_api_image_resource(params={})
|
64
|
+
|
65
|
+
service_id = params.fetch(:service_id)
|
66
|
+
resource_id_default = "#{service_id}#{IMAGE_API_DEFAULT_PARAMS}"
|
67
|
+
resource_id = params.fetch(:resource_id, resource_id_default)
|
68
|
+
format = params.fetch(:format, DEFAULT_FORMAT)
|
69
|
+
height = params.fetch(:height, nil)
|
70
|
+
profile = params.fetch(:profile, nil)
|
71
|
+
width = params.fetch(:width, nil)
|
72
|
+
copy_info = params.fetch(:copy_info, false)
|
73
|
+
|
74
|
+
have_whp = [width, height, profile].all? { |prop| !prop.nil? }
|
75
|
+
|
76
|
+
remote_info = get_info(service_id) if !have_whp || copy_info
|
77
|
+
|
78
|
+
resource = self.new
|
79
|
+
resource['@id'] = resource_id
|
80
|
+
resource.format = format
|
81
|
+
resource.width = width.nil? ? remote_info['width'] : width
|
82
|
+
resource.height = height.nil? ? remote_info['height'] : height
|
83
|
+
resource.service = Service.new
|
84
|
+
if copy_info
|
85
|
+
resource.service.merge!(remote_info)
|
86
|
+
else
|
87
|
+
resource.service['@context'] = IMAGE_API_CONTEXT
|
88
|
+
resource.service['@id'] = service_id
|
89
|
+
if profile.nil?
|
90
|
+
if remote_info['profile'].kind_of?(Array)
|
91
|
+
resource.service['profile'] = remote_info['profile'][0]
|
92
|
+
else
|
93
|
+
resource.service['profile'] = remote_info['profile'][0]
|
94
|
+
end
|
95
|
+
else
|
96
|
+
resource.service['profile'] = profile
|
97
|
+
end
|
98
|
+
end
|
99
|
+
return resource
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
def get_info(svc_id)
|
104
|
+
conn = Faraday.new("#{svc_id}/info.json") do |c|
|
105
|
+
c.use Faraday::Response::RaiseError
|
106
|
+
c.use Faraday::Adapter::NetHttp
|
107
|
+
end
|
108
|
+
resp = conn.get # raises exceptions that indicate HTTP problems
|
109
|
+
JSON.parse(resp.body)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Layer < AbstractResource
|
6
|
+
|
7
|
+
TYPE = 'sc:Layer'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ @id label }
|
11
|
+
end
|
12
|
+
|
13
|
+
def array_only_keys
|
14
|
+
super + %w{ other_content }
|
15
|
+
end
|
16
|
+
|
17
|
+
def string_only_keys
|
18
|
+
super + %w{ viewing_direction } # should any of the any_type_keys be here?
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(hsh={})
|
22
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
23
|
+
super(hsh)
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate
|
27
|
+
# Must all members of otherContent and images must be a URI (string), or
|
28
|
+
# can they be inline?
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'abstract_resource')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Manifest < AbstractResource
|
6
|
+
|
7
|
+
TYPE = 'sc:Manifest'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ @id label }
|
11
|
+
end
|
12
|
+
|
13
|
+
def string_only_keys
|
14
|
+
super + %w{ viewing_direction }
|
15
|
+
end
|
16
|
+
|
17
|
+
def array_only_keys
|
18
|
+
super + %w{ sequences structures }
|
19
|
+
end
|
20
|
+
|
21
|
+
def legal_viewing_hint_values
|
22
|
+
%w{ individuals paged continuous }
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(hsh={})
|
26
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
27
|
+
super(hsh)
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate
|
31
|
+
# TODO: check types of sequences and structure members
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'sequence')
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module Presentation
|
5
|
+
class Range < Sequence
|
6
|
+
|
7
|
+
TYPE = 'sc:Range'
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ @id label }
|
11
|
+
end
|
12
|
+
|
13
|
+
def array_only_keys
|
14
|
+
super + %w{ ranges }
|
15
|
+
end
|
16
|
+
|
17
|
+
def legal_viewing_hint_values
|
18
|
+
super + %w{ top }
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(hsh={})
|
22
|
+
hsh['@type'] = TYPE unless hsh.has_key? '@type'
|
23
|
+
super(hsh)
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate
|
27
|
+
# Values of the ranges array must be strings
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|