iiif-presentation 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/README.md +22 -2
- data/VERSION +1 -1
- data/iiif-presentation.gemspec +1 -1
- data/lib/iiif/hash_behaviours.rb +1 -1
- data/lib/iiif/presentation/canvas.rb +4 -0
- data/lib/iiif/presentation/service.rb +12 -0
- data/lib/iiif/presentation.rb +5 -4
- data/lib/iiif/service.rb +40 -105
- data/lib/iiif/v3/abstract_resource.rb +491 -0
- data/lib/iiif/v3/presentation/annotation.rb +74 -0
- data/lib/iiif/v3/presentation/annotation_collection.rb +38 -0
- data/lib/iiif/v3/presentation/annotation_page.rb +53 -0
- data/lib/iiif/v3/presentation/canvas.rb +82 -0
- data/lib/iiif/v3/presentation/choice.rb +51 -0
- data/lib/iiif/v3/presentation/collection.rb +52 -0
- data/lib/iiif/v3/presentation/image_resource.rb +110 -0
- data/lib/iiif/v3/presentation/manifest.rb +82 -0
- data/lib/iiif/v3/presentation/range.rb +39 -0
- data/lib/iiif/v3/presentation/resource.rb +30 -0
- data/lib/iiif/v3/presentation/sequence.rb +66 -0
- data/lib/iiif/v3/presentation/service.rb +51 -0
- data/lib/iiif/v3/presentation.rb +36 -0
- data/spec/fixtures/v3/manifests/complete_from_spec.json +195 -0
- data/spec/fixtures/v3/manifests/minimal.json +49 -0
- data/spec/fixtures/v3/manifests/service_only.json +14 -0
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette.json +1 -1
- data/spec/fixtures/vcr_cassettes/pul_loris_cassette_v3.json +1 -0
- data/spec/integration/iiif/presentation/image_resource_spec.rb +0 -1
- data/spec/integration/iiif/service_spec.rb +17 -32
- data/spec/integration/iiif/v3/abstract_resource_spec.rb +202 -0
- data/spec/integration/iiif/v3/presentation/image_resource_spec.rb +118 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/iiif/presentation/canvas_spec.rb +0 -1
- data/spec/unit/iiif/presentation/manifest_spec.rb +1 -1
- data/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +78 -0
- data/spec/unit/iiif/v3/abstract_resource_spec.rb +293 -0
- data/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +36 -0
- data/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +131 -0
- data/spec/unit/iiif/v3/presentation/annotation_spec.rb +389 -0
- data/spec/unit/iiif/v3/presentation/canvas_spec.rb +337 -0
- data/spec/unit/iiif/v3/presentation/choice_spec.rb +120 -0
- data/spec/unit/iiif/v3/presentation/collection_spec.rb +55 -0
- data/spec/unit/iiif/v3/presentation/image_resource_spec.rb +189 -0
- data/spec/unit/iiif/v3/presentation/manifest_spec.rb +370 -0
- data/spec/unit/iiif/v3/presentation/range_spec.rb +54 -0
- data/spec/unit/iiif/v3/presentation/resource_spec.rb +174 -0
- data/spec/unit/iiif/v3/presentation/sequence_spec.rb +222 -0
- data/spec/unit/iiif/v3/presentation/service_spec.rb +220 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +41 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +31 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +40 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +40 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +45 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +45 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +26 -0
- data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +31 -0
- metadata +82 -11
- data/.travis.yml +0 -11
@@ -0,0 +1,82 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Canvas < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Canvas'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
super + %w{ id label }
|
10
|
+
end
|
11
|
+
|
12
|
+
def prohibited_keys
|
13
|
+
super + PAGING_PROPERTIES + %w{ viewing_direction format nav_date start_canvas content_annotations }
|
14
|
+
end
|
15
|
+
|
16
|
+
def int_only_keys
|
17
|
+
super + %w{ depth }
|
18
|
+
end
|
19
|
+
|
20
|
+
def array_only_keys
|
21
|
+
super + %w{ content }
|
22
|
+
end
|
23
|
+
|
24
|
+
def legal_viewing_hint_values
|
25
|
+
super + %w{ paged continuous non-paged facing-pages auto-advance }
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(hsh={})
|
29
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
30
|
+
super(hsh)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate
|
34
|
+
super
|
35
|
+
|
36
|
+
id_uri = URI.parse(self['id'])
|
37
|
+
unless self['id'] =~ /^https?:/ && id_uri.fragment.nil?
|
38
|
+
err_msg = "id must be an http(s) URI without a fragment for #{self.class}"
|
39
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
40
|
+
end
|
41
|
+
|
42
|
+
content = self['content']
|
43
|
+
if content && content.any?
|
44
|
+
unless content.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::AnnotationPage) }
|
45
|
+
err_msg = 'All entries in the content list must be a IIIF::V3::Presentation::AnnotationPage'
|
46
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
47
|
+
end
|
48
|
+
content.each do |anno_page|
|
49
|
+
annos = anno_page['items']
|
50
|
+
if annos && annos.any?
|
51
|
+
unless annos.all? { |anno| anno.target == self.id }
|
52
|
+
err_msg = 'URI of the canvas must be repeated in the target field of included Annotations'
|
53
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# "A canvas MUST have exactly one width and one height, or exactly one duration.
|
60
|
+
# It may have width, height and duration.""
|
61
|
+
height = self['height']
|
62
|
+
width = self['width']
|
63
|
+
if (!!height ^ !!width) # this is an exclusive or: forces height and width to boolean
|
64
|
+
extent_err_msg = "#{self.class} requires both height and width or neither"
|
65
|
+
raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg
|
66
|
+
end
|
67
|
+
# NOTE: relaxing requirement for (exactly one width and one height, and/or exactly one duration)
|
68
|
+
# as Stanford has objects (such as txt files) for which this makes no sense
|
69
|
+
# (see sul-dlss/purl/issues/169)
|
70
|
+
# duration = self['duration']
|
71
|
+
# unless (height && width) || duration
|
72
|
+
# extent_err_msg = "#{self.class} must have (a height and a width) and/or a duration"
|
73
|
+
# raise IIIF::V3::Presentation::IllegalValueError, extent_err_msg
|
74
|
+
# end
|
75
|
+
|
76
|
+
# TODO: Content must not be associated with space or time outside of the Canvas’s dimensions,
|
77
|
+
# such as at coordinates below 0,0, greater than the height or width, before 0 seconds, or after the duration.
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Choice < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Choice'.freeze
|
7
|
+
|
8
|
+
def prohibited_keys
|
9
|
+
super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES +
|
10
|
+
%w{ nav_date viewing_direction start_canvas content_annotations }
|
11
|
+
end
|
12
|
+
|
13
|
+
def any_type_keys
|
14
|
+
super + %w{ default }
|
15
|
+
end
|
16
|
+
|
17
|
+
def string_only_keys
|
18
|
+
super + %w{ choice_hint }
|
19
|
+
end
|
20
|
+
|
21
|
+
def array_only_keys;
|
22
|
+
super + %w{ items };
|
23
|
+
end
|
24
|
+
|
25
|
+
def legal_viewing_hint_values
|
26
|
+
%w{ none }
|
27
|
+
end
|
28
|
+
|
29
|
+
def legal_choice_hint_values
|
30
|
+
%w{ client user }
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(hsh={})
|
34
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
35
|
+
super(hsh)
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate
|
39
|
+
super
|
40
|
+
|
41
|
+
if self.has_key?('choice_hint')
|
42
|
+
unless self.legal_choice_hint_values.include?(self['choice_hint'])
|
43
|
+
m = "choiceHint for #{self.class} must be one of #{self.legal_choice_hint_values}."
|
44
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Collection < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Collection'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
super + %w{ id label }
|
10
|
+
end
|
11
|
+
|
12
|
+
def array_only_keys
|
13
|
+
super + %w{ collections manifests }
|
14
|
+
end
|
15
|
+
|
16
|
+
# TODO: navDate (collection or manifest only) - The value must be an xsd:dateTime literal in UTC, expressed in the form “YYYY-MM-DDThh:mm:ssZ”; There must be at most one navDate associated with any given resource.
|
17
|
+
|
18
|
+
# TODO: paging properties
|
19
|
+
# Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w.
|
20
|
+
# ---
|
21
|
+
# first, last, next, prev
|
22
|
+
# id is URI, but may have other info
|
23
|
+
# total, startIndex
|
24
|
+
# The value must be a non-negative integer.
|
25
|
+
|
26
|
+
def legal_viewing_hint_values
|
27
|
+
%w{ auto-advance together }
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(hsh={})
|
31
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
32
|
+
super(hsh)
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate
|
36
|
+
super
|
37
|
+
# TODO: each member of collections and manifests must be a Hash
|
38
|
+
# TODO: each member of collections and manifests MUST have id, type, and label
|
39
|
+
# TODO: navDate (collection or manifest only) - The value must be an xsd:dateTime literal in UTC, expressed in the form “YYYY-MM-DDThh:mm:ssZ”; There must be at most one navDate associated with any given resource.
|
40
|
+
|
41
|
+
# TODO: paging properties
|
42
|
+
# Collection, AnnotationCollection, (formerly layer --> AnnotationPage???) allow; forbidden o.w.
|
43
|
+
# ---
|
44
|
+
# first, last, next, prev
|
45
|
+
# id is URI, but may have other info
|
46
|
+
# total, startIndex
|
47
|
+
# The value must be a non-negative integer.
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module IIIF
|
4
|
+
module V3
|
5
|
+
module Presentation
|
6
|
+
class ImageResource < Resource
|
7
|
+
|
8
|
+
TYPE = 'Image'.freeze
|
9
|
+
|
10
|
+
def initialize(hsh={})
|
11
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
12
|
+
super(hsh)
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
IMAGE_API_DEFAULT_PARAMS = '/full/!200,200/0/default.jpg'.freeze
|
17
|
+
DEFAULT_FORMAT = 'image/jpeg'.freeze
|
18
|
+
# Create a new ImageResource that includes a IIIF Image API Service
|
19
|
+
# See http://iiif.io/api/presentation/2.0/#image-resources
|
20
|
+
#
|
21
|
+
# Params
|
22
|
+
# * :service_id (required) - The base URI for the image on the image
|
23
|
+
# server.
|
24
|
+
# * :resource_id - The id for the resource; if supplied this should
|
25
|
+
# resolve to an actual image. Default:
|
26
|
+
# "#{:service_id}/full/!200,200/0/default.jpg"
|
27
|
+
# * :format - The format of the image that is returned when
|
28
|
+
# `:resource_id` is resolved. Default: 'image/jpeg'
|
29
|
+
# * :height (Integer)
|
30
|
+
# * :profile (String)
|
31
|
+
# * :width (Integer) - If width, height, and profile are not supplied,
|
32
|
+
# this method will try to get the info from the server (based on
|
33
|
+
# :resource_id) and raise an Exception if this is not possible for
|
34
|
+
# some reason.
|
35
|
+
# * :copy_info (bool)- Even if width and height are supplied, try to
|
36
|
+
# get the info.json from the server and copy it in. Default: false
|
37
|
+
#
|
38
|
+
# Raises:
|
39
|
+
# * KeyError if `:service_id` is not supplied
|
40
|
+
# * Expections related to HTTP problems if a call to an image server fails
|
41
|
+
#
|
42
|
+
# The result is something like this:
|
43
|
+
#
|
44
|
+
# {
|
45
|
+
# "id":"http://www.example.org/iiif/book1/res/page1.jpg",
|
46
|
+
# "type": "Image",
|
47
|
+
# "format": "image/jpeg",
|
48
|
+
# "service": {
|
49
|
+
# "@context": "http://iiif.io/api/image/2/context.json",
|
50
|
+
# "id":"http://www.example.org/images/book1-page1",
|
51
|
+
# "profile":"http://iiif.io/api/image/2/profiles/level2.json",
|
52
|
+
# },
|
53
|
+
# "height":2000,
|
54
|
+
# "width":1500
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
def create_image_api_image_resource(params={})
|
58
|
+
|
59
|
+
service_id = params.fetch(:service_id)
|
60
|
+
resource_id_default = "#{service_id}#{IMAGE_API_DEFAULT_PARAMS}"
|
61
|
+
resource_id = params.fetch(:resource_id, resource_id_default)
|
62
|
+
format = params.fetch(:format, DEFAULT_FORMAT)
|
63
|
+
height = params.fetch(:height, nil)
|
64
|
+
profile = params.fetch(:profile, nil)
|
65
|
+
width = params.fetch(:width, nil)
|
66
|
+
copy_info = params.fetch(:copy_info, false)
|
67
|
+
|
68
|
+
have_whp = [width, height, profile].all? { |prop| !prop.nil? }
|
69
|
+
|
70
|
+
remote_info = get_info(service_id) if !have_whp || copy_info
|
71
|
+
|
72
|
+
resource = self.new
|
73
|
+
resource['id'] = resource_id
|
74
|
+
resource.format = format
|
75
|
+
resource.width = width.nil? ? remote_info['width'] : width
|
76
|
+
resource.height = height.nil? ? remote_info['height'] : height
|
77
|
+
resource_service = Service.new
|
78
|
+
if copy_info
|
79
|
+
resource_service.merge!(remote_info)
|
80
|
+
resource_service['id'] ||= resource_service.delete('@id')
|
81
|
+
else
|
82
|
+
resource_service['id'] = service_id
|
83
|
+
if profile.nil?
|
84
|
+
if remote_info['profile'].kind_of?(Array)
|
85
|
+
resource_service['profile'] = remote_info['profile'][0]
|
86
|
+
else
|
87
|
+
resource_service['profile'] = remote_info['profile']
|
88
|
+
end
|
89
|
+
else
|
90
|
+
resource_service['profile'] = profile
|
91
|
+
end
|
92
|
+
end
|
93
|
+
resource.service = [resource_service]
|
94
|
+
return resource
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
def get_info(svc_id)
|
99
|
+
conn = Faraday.new("#{svc_id}/info.json") do |c|
|
100
|
+
c.use Faraday::Response::RaiseError
|
101
|
+
c.adapter :net_http
|
102
|
+
end
|
103
|
+
resp = conn.get # raises exceptions that indicate HTTP problems
|
104
|
+
JSON.parse(resp.body)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Manifest < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Manifest'.freeze
|
7
|
+
|
8
|
+
def required_keys
|
9
|
+
# NOTE: relaxing requirement for items as Universal Viewer currently only accepts sequences
|
10
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
11
|
+
# super + %w{ id label items }
|
12
|
+
super + %w{ id label }
|
13
|
+
end
|
14
|
+
|
15
|
+
def prohibited_keys
|
16
|
+
super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ start_canvas content_annotation }
|
17
|
+
end
|
18
|
+
|
19
|
+
def uri_only_keys
|
20
|
+
super + %w{ id }
|
21
|
+
end
|
22
|
+
|
23
|
+
def array_only_keys
|
24
|
+
# NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences
|
25
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
26
|
+
# super + %w{ items structures }
|
27
|
+
super + %w{ items structures sequences }
|
28
|
+
end
|
29
|
+
|
30
|
+
def legal_viewing_hint_values
|
31
|
+
%w{ individuals paged continuous auto-advance }
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(hsh={})
|
35
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
36
|
+
super(hsh)
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate
|
40
|
+
super # also checks navDate format
|
41
|
+
|
42
|
+
unless self['id'] =~ /^https?:/
|
43
|
+
err_msg = "id must be an http(s) URI for #{self.class}"
|
44
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
45
|
+
end
|
46
|
+
|
47
|
+
# Items object list
|
48
|
+
unless self&.[]('items')&.any?
|
49
|
+
m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
|
50
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
51
|
+
end
|
52
|
+
validate_items_list(self['items']) if self['items']
|
53
|
+
|
54
|
+
# TODO: when embedding a sequence without any extensions within a manifest, the sequence must not have the @context field.
|
55
|
+
|
56
|
+
# TODO: AnnotationLists must not be embedded within the manifest
|
57
|
+
|
58
|
+
if self['structures']
|
59
|
+
unless self['structures'].all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Range)}
|
60
|
+
m = 'All entries in the structures list must be a IIIF::V3::Presentation::Range'
|
61
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# NOTE: allowing 'items' or 'sequences' as Universal Viewer currently only accepts sequences
|
67
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
68
|
+
def validate_items_list(items_array)
|
69
|
+
unless items_array.size >= 1
|
70
|
+
m = 'The items list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
|
71
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
72
|
+
end
|
73
|
+
|
74
|
+
unless items_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) }
|
75
|
+
m = 'All entries in the items list must be a IIIF::V3::Presentation::Canvas'
|
76
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
# Ranges are linked or embedded within the manifest in a structures field
|
5
|
+
class Range < Sequence
|
6
|
+
|
7
|
+
TYPE = 'Range'.freeze
|
8
|
+
|
9
|
+
def required_keys
|
10
|
+
super + %w{ id label }
|
11
|
+
end
|
12
|
+
|
13
|
+
# TODO: contentAnnotations: links to AnnotationCollection
|
14
|
+
# TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
|
15
|
+
|
16
|
+
def array_only_keys
|
17
|
+
super + %w{ members }
|
18
|
+
end
|
19
|
+
|
20
|
+
def legal_viewing_hint_values
|
21
|
+
super + %w{ top }
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(hsh={})
|
25
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
26
|
+
super(hsh)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate
|
30
|
+
super
|
31
|
+
# TODO: Ranges must have URIs and they should be http(s) URIs.
|
32
|
+
# TODO: Values of the members array must be canvas or range
|
33
|
+
# TODO: contentAnnotations: links to AnnotationCollection
|
34
|
+
# TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
# class for generic content resource
|
5
|
+
class Resource < IIIF::V3::AbstractResource
|
6
|
+
|
7
|
+
def required_keys
|
8
|
+
super + %w{ id }
|
9
|
+
end
|
10
|
+
|
11
|
+
def prohibited_keys
|
12
|
+
super + PAGING_PROPERTIES + %w{ nav_date viewing_direction start_canvas content_annotations}
|
13
|
+
end
|
14
|
+
|
15
|
+
def uri_only_keys
|
16
|
+
super + %w{ id }
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate
|
20
|
+
super
|
21
|
+
|
22
|
+
unless self['id'] =~ /^https?:/
|
23
|
+
err_msg = "id must be an http(s) URI for #{self.class}"
|
24
|
+
raise IIIF::V3::Presentation::IllegalValueError, err_msg
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
class Sequence < IIIF::V3::AbstractResource
|
5
|
+
|
6
|
+
TYPE = 'Sequence'.freeze
|
7
|
+
|
8
|
+
# NOTE: relaxing requirement for items as Universal Viewer currently only accepts canvases
|
9
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
10
|
+
# def required_keys
|
11
|
+
# super + %w{ items }
|
12
|
+
# end
|
13
|
+
|
14
|
+
def prohibited_keys
|
15
|
+
super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ nav_date content_annotations }
|
16
|
+
end
|
17
|
+
|
18
|
+
# NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases
|
19
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
20
|
+
def array_only_keys
|
21
|
+
super + %w{ canvases }
|
22
|
+
end
|
23
|
+
|
24
|
+
def legal_viewing_hint_values
|
25
|
+
%w{ individuals paged continuous auto-advance }
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(hsh={})
|
29
|
+
hsh['type'] = TYPE unless hsh.has_key? 'type'
|
30
|
+
super(hsh)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate
|
34
|
+
super
|
35
|
+
|
36
|
+
# Canvas object list
|
37
|
+
# NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases
|
38
|
+
# see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
|
39
|
+
unless (self['items'] && self['items'].any?) ||
|
40
|
+
(self['canvases'] && self['canvases'].any?)
|
41
|
+
m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
|
42
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
43
|
+
end
|
44
|
+
validate_canvas_list(self['items']) if self['items']
|
45
|
+
validate_canvas_list(self['canvases']) if self['canvases']
|
46
|
+
|
47
|
+
# TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
|
48
|
+
|
49
|
+
# TODO: All external Sequences must have a dereference-able http(s) URI
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_canvas_list(canvas_array)
|
53
|
+
unless canvas_array.size >= 1
|
54
|
+
m = 'The (items or canvases) list must have at least one entry (and it must be a IIIF::V3::Presentation::Canvas)'
|
55
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
56
|
+
end
|
57
|
+
|
58
|
+
unless canvas_array.all? { |entry| entry.instance_of?(IIIF::V3::Presentation::Canvas) }
|
59
|
+
m = 'All entries in the (items or canvases) list must be a IIIF::V3::Presentation::Canvas'
|
60
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module IIIF
|
2
|
+
module V3
|
3
|
+
module Presentation
|
4
|
+
# See http://prezi3.iiif.io/api/annex/services for more info
|
5
|
+
class Service < AbstractResource
|
6
|
+
|
7
|
+
# constants included here for convenience
|
8
|
+
IIIF_IMAGE_V2_TYPE = 'ImageService2'.freeze
|
9
|
+
IIIF_IMAGE_V2_LEVEL1_PROFILE = 'http://iiif.io/api/image/2/level1.json'.freeze
|
10
|
+
IIIF_AUTHENTICATION_V1_LOGIN_PROFILE = 'http://iiif.io/api/auth/1/login'.freeze
|
11
|
+
IIIF_AUTHENTICATION_V1_TOKEN_PROFILE = 'http://iiif.io/api/auth/1/token'.freeze
|
12
|
+
|
13
|
+
# service class doesn't require type
|
14
|
+
def required_keys
|
15
|
+
super.reject {|el| el == 'type' }
|
16
|
+
end
|
17
|
+
|
18
|
+
def prohibited_keys
|
19
|
+
super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES +
|
20
|
+
%w{ nav_date viewing_direction start_canvas content_annotations }
|
21
|
+
end
|
22
|
+
|
23
|
+
def uri_only_keys
|
24
|
+
super + %w{ @context id @id }
|
25
|
+
end
|
26
|
+
|
27
|
+
def any_type_keys
|
28
|
+
super + %w{ profile }
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate
|
32
|
+
super
|
33
|
+
if IIIF_IMAGE_V2_TYPE == self['type'] || IIIF_IMAGE_V2_TYPE == self['@type']
|
34
|
+
unless self.has_key?('id') || self.has_key?('@id')
|
35
|
+
m = "id or @id values are required for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
|
36
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
37
|
+
end
|
38
|
+
if self.has_key?('id') && self.has_key?('@id') && (self['@id'] != self['id'])
|
39
|
+
m = "id and @id values must match for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
|
40
|
+
raise IIIF::V3::Presentation::IllegalValueError, m
|
41
|
+
end
|
42
|
+
unless self.has_key?('profile')
|
43
|
+
m = "profile should be present for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
|
44
|
+
raise IIIF::V3::Presentation::MissingRequiredKeyError, m
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'abstract_resource'
|
2
|
+
require_relative '../ordered_hash'
|
3
|
+
|
4
|
+
# NOTE: image_resource must follow resource due to inheritance
|
5
|
+
# NOTE: range must follow sequence due to inheritance
|
6
|
+
%w{
|
7
|
+
annotation
|
8
|
+
annotation_collection
|
9
|
+
annotation_page
|
10
|
+
canvas
|
11
|
+
choice
|
12
|
+
collection
|
13
|
+
manifest
|
14
|
+
resource
|
15
|
+
image_resource
|
16
|
+
sequence
|
17
|
+
range
|
18
|
+
service
|
19
|
+
}.each do |f|
|
20
|
+
require File.join(File.dirname(__FILE__), 'presentation', f)
|
21
|
+
end
|
22
|
+
|
23
|
+
module IIIF
|
24
|
+
module V3
|
25
|
+
module Presentation
|
26
|
+
CONTEXT ||= [
|
27
|
+
'http://www.w3.org/ns/anno.jsonld',
|
28
|
+
'http://iiif.io/api/presentation/3/context.json'
|
29
|
+
]
|
30
|
+
|
31
|
+
class MissingRequiredKeyError < StandardError; end
|
32
|
+
class ProhibitedKeyError < StandardError; end
|
33
|
+
class IllegalValueError < StandardError; end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|