osullivan 0.0.2
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 +4 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +2 -0
- data/LICENSE +23 -0
- data/README.md +166 -0
- data/Rakefile +12 -0
- data/VERSION +1 -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/osullivan.gemspec +27 -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 +246 -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
|