iiif_manifest 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18a5aee09b4751f68e8ee7a56e328a842fae63f439df914743ba04be4159b233
4
- data.tar.gz: aba2e5b1ae194000b48fb0d31dca76a9ebc7e6a48da5bf295d6d44184804cf3d
3
+ metadata.gz: 21edd1255e73222b1c238869e8c2d47fb0d33bc22fd0e490e0b64e034de1a147
4
+ data.tar.gz: f969ee0b9bc4c9f1e2933d0cce4aeaec8cbcd532520cfa1b03e88a07f0f55d9e
5
5
  SHA512:
6
- metadata.gz: e14265c4bd0f290d3a7c6c3c782ce420609d4212406c4121aece8199d71f537f2d9db96c4ec81cd9e6c6c4542e1e675812627ffb119018cb9093a65563ea32c7
7
- data.tar.gz: 9b5cdc68f0141a3df37c80f86d5234084b0a480ccccc11b5e01cc7197dc0e1a5e3f775ee59c19d67c18ba7987632de66bad3cd95cf6cbdbf26d5bf0c86697159
6
+ metadata.gz: 9dba593b91faf18b8ba717e668cad98c1ca3384a97b989fa380e15ba20cced1c4e7d9d01d8698860d2a45fdaf3d7808cf93ba767853cac8af7acc2fbc8aef417
7
+ data.tar.gz: 1e9738fc181e851f23ab5bf37f797c9aa40c561a6201e9c6d56590a84d7b088641eef3d884fa59107a6e9f47d5570b22151a87f04c4747bc9b59558b249bc50f
data/.circleci/config.yml CHANGED
@@ -34,6 +34,9 @@ workflows:
34
34
  version: 2
35
35
  ci:
36
36
  jobs:
37
+ - test:
38
+ name: "ruby3-3"
39
+ ruby_version: "3.3.0"
37
40
  - test:
38
41
  name: "ruby3-2"
39
42
  ruby_version: "3.2.0"
@@ -62,6 +65,9 @@ workflows:
62
65
  only:
63
66
  - main
64
67
  jobs:
68
+ - test:
69
+ name: "ruby3-3"
70
+ ruby_version: "3.3.0"
65
71
  - test:
66
72
  name: "ruby3-2"
67
73
  ruby_version: "3.2.0"
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .byebug_history
data/README.md CHANGED
@@ -32,6 +32,8 @@ Additionally it **_should_** implement `#manifest_metadata` to provide an array
32
32
 
33
33
  Additionally it **_may_** implement `#search_service` to contain the url for a IIIF search api compliant search endpoint and `#autocomplete_service` to contain the url for a IIIF search api compliant autocomplete endpoint. Please note, the autocomplete service is embedded within the search service description so if an autocomplete_service is supplied without a search_service it will be ignored. The IIIF `profile` added to the service descriptions is version 0 as this is the version supported by the current version of Universal Viewer. Only include a search_service within the manifest if your application has implemented a IIIF search service at the endpoint specified in the manifest.
34
34
 
35
+ Additionally it **_may_** implement `#service` to contain an array of hashes for services other than `search_service` or `autocomplete_service`. Each must contain "@id" (V2) or "id" (V3) and "@context" (V2) or "type" (V3) and may contain any other arbitrary properties.
36
+
35
37
  Additionally it **_may_** implement `#sequence_rendering` to contain an array of hashes for file downloads to be offered at sequences level. Each hash must contain "@id", "format" (mime type) and "label" (eg. `{ "@id" => "download url", "format" => "application/pdf", "label" => "user friendly label" }`).
36
38
 
37
39
  Finally, it **_may_** implement `ranges`, which returns an array of objects which represent a table of contents or similar structure, each of which responds to `label`, `ranges`, and `file_set_presenters`.
@@ -76,6 +78,16 @@ For example:
76
78
  "http://test.host/books/#{@id}/autocomplete"
77
79
  end
78
80
 
81
+ def service
82
+ [
83
+ {
84
+ "@context" => "http://iiif.io/api/annext/services/example/context.json",
85
+ "@id" => "https://example.org/service",
86
+ "profile" => "https://example.org/docs/service"
87
+ }
88
+ ]
89
+ end
90
+
79
91
  def sequence_rendering
80
92
  [{"@id" => "http://test.host/file_set/id/download", "format" => "application/pdf", "label" => "Download"}]
81
93
  end
@@ -107,7 +119,12 @@ For example:
107
119
 
108
120
  The class that represents the leaf nodes, must implement `#id`. It must also implement `#display_image` which returns an instance of `IIIFManifest::DisplayImage`
109
121
 
110
- Additionally it **_may_** implement `#sequence_rendering` to contain an array of hashes for file downloads to be offered at each leaf node. This follows a similar format as `#sequence_rendering` at sequences level.
122
+ In Presentation 3.0, additionally it **_may_** implement;
123
+ - `#item_metadata` to contain an array of hashes for metadata to be displayed at each leaf node. Items must contain "label" and "value" properties.
124
+ - `#sequence_rendering` to contain an array of hashes for file downloads to be offered at each leaf node. This follows a similar format as `#sequence_rendering` at sequences level
125
+ - `#see_also` to contain an array of hashes for related resources to be offered at each leaf node. Items must contain "id" and "type" properties. Items should contain "label", "format", and "profile" properties.
126
+ - `#part_of` to contain an array of hashes for parent resources to be offered at each leaf node. Items must contain "id" and "type" properties. Items should contain "label".
127
+ - `#placeholder_content` to contain an instance of `IIIFManifest::V3::DisplayContent` for [`placeholderCanvas`](https://iiif.io/api/presentation/3.0/#placeholdercanvas) at each leaf node
111
128
 
112
129
  ```ruby
113
130
  class Page
@@ -128,10 +145,28 @@ Additionally it **_may_** implement `#sequence_rendering` to contain an array of
128
145
  )
129
146
  end
130
147
 
148
+ # --------------------------------------- Presentation 3.0 (Alpha) ---------------------------------------
131
149
  def sequence_rendering
132
150
  [{"@id" => "http://test.host/display_image/id/download", "format" => "application/pdf", "label" => "Download"}]
133
151
  end
134
152
 
153
+ def see_also
154
+ [{"id" => "http://test.host/display_image/id/image.json", "type" => "dataset", "format" => "application/json", "label" => "Related Resource"}]
155
+ end
156
+
157
+ def part_of
158
+ [{"id" => "http://test.host/display_image/id/parent.json", "type" => "manifest"}]
159
+ end
160
+
161
+ def placeholder_content
162
+ IIIFManifest::V3::DisplayContent.new(id,
163
+ width: 100,
164
+ height: 100,
165
+ type: "Image",
166
+ format: "image/jpeg")
167
+ end
168
+ # --------------------------------------- Presentation 3.0 (Alpha) ---------------------------------------
169
+
135
170
  private
136
171
 
137
172
  def endpoint
@@ -148,9 +183,9 @@ Then you can produce the manifest on the book object like this:
148
183
  IIIFManifest::ManifestFactory.new(book).to_h.to_json
149
184
  ```
150
185
 
151
- ## Presentation 3.0 (Alpha)
186
+ ## Presentation 3.0
152
187
 
153
- Provisional support for the [3.0 alpha version of the IIIF presentation api spec](https://iiif.io/api/presentation/3.0/) has been added with a focus on audiovisual content. The [change log](https://iiif.io/api/presentation/3.0/change-log/) lists the changes to the specification.
188
+ Provisional support for the [3.0 version of the IIIF presentation api spec](https://iiif.io/api/presentation/3.0/) has been added with a focus on audiovisual content. The [change log](https://iiif.io/api/presentation/3.0/change-log/) lists the changes to the specification.
154
189
 
155
190
  The presentation 3.0 support has been contained to the `V3` namespace. Version 2.0 manifests are still being built using `IIIFManifest::ManifestFactory` while version 3.0 manifests can now be built using `IIIFManifest::V3::ManifestFactory`.
156
191
 
@@ -166,7 +201,13 @@ The presentation 3.0 support has been contained to the `V3` namespace. Version 2
166
201
  - Presenters **_may_** implement `#homepage` to contain a hash for linking back to a repository webpage for this manifest. The hash must contain "id", "format" (mime type), "type", and "label" (eg. `{ "id" => "repository url", "format" => "text/html", "type" => "Text", "label" => { "en": ["View in repository"] }`).
167
202
  - File set presenters may target a fragment of its content by providing `#media_fragment` which will be appended to its `id`.
168
203
  - Range objects may now implement `#items` instead of `#ranges` and `#file_set_presenters` to allow for interleaving these objects. `#items` is not required and existing range objects should continue to work.
169
- - File set presenters may provide `#display_content` which should return an instance of `IIIFManifest::V3::DisplayContent` (or an array of instances in the case of a user `Choice`). `#display_image` is no longer required but will still work if provided.
204
+ - File set presenters may provide,
205
+ - `#display_content` which should return an instance of `IIIFManifest::V3::DisplayContent` (or an array of instances in the case of a user `Choice`)
206
+ - `#annotation_content` which should return an instance of `IIIFManifest::V3::AnnotationContent` (or an array of instances in the case of a canvas having multiple annotations)
207
+ - `#display_image` is no longer required but will still work if provided
208
+ - `#sequence_rendering` is supported at leaf node level, to present an array of file downloads available at each leaf node
209
+ - `#part_of` is supported at leaf node level, to present an array of parent resources available at each leaf node.
210
+ - `#placeholder_content` which returns an instance of `IIIFManifest::V3::DisplayContent` presents a [`placeholderCanvas`](https://iiif.io/api/presentation/3.0/#placeholdercanvas) at leaf node level
170
211
  - DisplayContent may provide `#auth_service` which should return a hash containing a IIIF Authentication service definition (<https://iiif.io/api/auth/1.0/>) that will be included on the content resource.
171
212
 
172
213
  ## Configuration
@@ -48,15 +48,24 @@ module IIIFManifest
48
48
  @iiif_autocomplete_service ||= iiif_autocomplete_service_factory.new
49
49
  end
50
50
 
51
+ def custom_services
52
+ record.respond_to?(:service) ? record.send(:service) : []
53
+ end
54
+
51
55
  # Build services. Currently supported:
52
56
  # search_service, with (optional) embedded autocomplete service
53
57
  #
54
58
  # @return [Array] array of services
55
59
  def services
56
- iiif_search_service.search_service = search_service
57
- iiif_autocomplete_service.autocomplete_service = autocomplete_service
58
- iiif_search_service.service = iiif_autocomplete_service if autocomplete_service.present?
59
- [iiif_search_service]
60
+ services = []
61
+ if search_service.present?
62
+ iiif_search_service.search_service = search_service
63
+ iiif_autocomplete_service.autocomplete_service = autocomplete_service
64
+ iiif_search_service.service = iiif_autocomplete_service if autocomplete_service.present?
65
+ services << iiif_search_service
66
+ end
67
+ services += Array(custom_services)
68
+ services
60
69
  end
61
70
 
62
71
  # Validate manifest_metadata against the IIIF spec format for metadata
@@ -0,0 +1,23 @@
1
+ module IIIFManifest
2
+ module V3
3
+ class AnnotationContent
4
+ attr_reader :annotation_id, :body_id, :type, :motivation, :format, :language, :label, :value, :media_fragment
5
+
6
+ def initialize(type:, motivation:, **kwargs)
7
+ # If a user requires a specific ID at the annotation level, this attr overrides
8
+ # the automatic ID creation.
9
+ @annotation_id = kwargs[:annotation_id]
10
+ # Body level ids are only required for annotations delivering content,
11
+ # such as a transcript/caption file or an annotation containing an image.
12
+ @body_id = kwargs[:body_id]
13
+ @type = type
14
+ @motivation = motivation
15
+ @format = kwargs[:format]
16
+ @language = kwargs[:language]
17
+ @label = kwargs[:label]
18
+ @value = kwargs[:value]
19
+ @media_fragment = kwargs[:media_fragment]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ module IIIFManifest
2
+ module V3
3
+ class ManifestBuilder
4
+ class AnnotationContentBuilder
5
+ attr_reader :annotation_content, :iiif_annotation_factory, :body_builder_factory
6
+ def initialize(annotation_content, iiif_annotation_factory:, body_builder_factory:)
7
+ @annotation_content = annotation_content
8
+ @iiif_annotation_factory = iiif_annotation_factory
9
+ @body_builder_factory = body_builder_factory
10
+ build_annotation_resource
11
+ end
12
+
13
+ def apply(canvas)
14
+ # Assume first item in canvas annotations is an annotation page
15
+ canvas_id = canvas.annotations.first['id']
16
+ generic_annotation['id'] = annotation_id(canvas_id)
17
+ generic_annotation['target'] = target(canvas)
18
+ generic_annotation['motivation'] = motivation
19
+ generic_annotation
20
+ end
21
+
22
+ private
23
+
24
+ def build_annotation_resource
25
+ annotation_body_builder.apply(generic_annotation)
26
+ end
27
+
28
+ def annotation_body_builder
29
+ body_builder_factory.new(annotation_content)
30
+ end
31
+
32
+ def generic_annotation
33
+ @generic_annotation ||= iiif_annotation_factory.new
34
+ end
35
+
36
+ def annotation_id(canvas_id)
37
+ if annotation_content.try(:annotation_id).blank?
38
+ "#{canvas_id}/#{motivation.presence || 'annotation'}/#{generic_annotation.index}"
39
+ else
40
+ annotation_content.annotation_id
41
+ end
42
+ end
43
+
44
+ def motivation
45
+ annotation_content.motivation if annotation_content.try(:motivation).present?
46
+ end
47
+
48
+ def target(canvas)
49
+ if annotation_content.try(:media_fragment).present?
50
+ canvas['id'] + "##{annotation_content.media_fragment}"
51
+ else
52
+ canvas['id']
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -2,9 +2,9 @@ module IIIFManifest
2
2
  module V3
3
3
  class ManifestBuilder
4
4
  class BodyBuilder
5
- attr_reader :display_content, :iiif_body_factory, :image_service_builder_factory
6
- def initialize(display_content, iiif_body_factory:, image_service_builder_factory:)
7
- @display_content = display_content
5
+ attr_reader :content, :iiif_body_factory, :image_service_builder_factory
6
+ def initialize(content, iiif_body_factory:, image_service_builder_factory:)
7
+ @content = content
8
8
  @iiif_body_factory = iiif_body_factory
9
9
  @image_service_builder_factory = image_service_builder_factory
10
10
  end
@@ -19,25 +19,40 @@ module IIIFManifest
19
19
  private
20
20
 
21
21
  def build_body
22
- body['id'] = display_content.url
22
+ body['id'] = body_id
23
23
  body['type'] = body_type
24
- body['height'] = display_content.height if display_content.try(:height).present?
25
- body['width'] = display_content.width if display_content.try(:width).present?
26
- body['duration'] = display_content.duration if display_content.try(:duration).present?
27
- body['format'] = display_content.format if display_content.try(:format).present?
28
- body['label'] = ManifestBuilder.language_map(display_content.label) if display_content.try(:label).present?
24
+ body_display_dimensions
25
+ body['format'] = content.format if content.try(:format).present?
26
+ body['label'] = ManifestBuilder.language_map(content.label) if content.try(:label).present?
27
+ body['language'] = content.language if content.try(:language).present?
28
+ body['value'] = content.value if content.try(:value).present?
29
29
  end
30
30
 
31
31
  def body
32
32
  @body ||= iiif_body_factory.new
33
33
  end
34
34
 
35
+ def body_id
36
+ return if content.try(:body_id).blank? && content.try(:url).blank?
37
+ if content.try(:body_id).present?
38
+ content.body_id
39
+ else
40
+ content.url
41
+ end
42
+ end
43
+
35
44
  def body_type
36
- display_content.try(:type) || 'Image'
45
+ content.try(:type) || 'Image'
46
+ end
47
+
48
+ def body_display_dimensions
49
+ body['height'] = content.height if content.try(:height).present?
50
+ body['width'] = content.width if content.try(:width).present?
51
+ body['duration'] = content.duration if content.try(:duration).present?
37
52
  end
38
53
 
39
54
  def iiif_endpoint
40
- display_content.try(:iiif_endpoint)
55
+ content.try(:iiif_endpoint)
41
56
  end
42
57
 
43
58
  def image_service_builder
@@ -45,7 +60,7 @@ module IIIFManifest
45
60
  end
46
61
 
47
62
  def auth_service
48
- display_content.try(:auth_service)
63
+ content.try(:auth_service)
49
64
  end
50
65
 
51
66
  def apply_auth_service
@@ -2,28 +2,35 @@ module IIIFManifest
2
2
  module V3
3
3
  class ManifestBuilder
4
4
  class CanvasBuilder
5
- attr_reader :record, :parent, :iiif_canvas_factory, :content_builder,
6
- :choice_builder, :iiif_annotation_page_factory, :thumbnail_builder_factory
5
+ attr_reader :record, :parent, :iiif_canvas_factory, :content_builder, :choice_builder,
6
+ :annotation_content_builder, :iiif_annotation_page_factory,
7
+ :thumbnail_builder_factory, :placeholder_canvas_builder_factory
7
8
 
8
9
  def initialize(record,
9
10
  parent,
10
11
  iiif_canvas_factory:,
11
12
  content_builder:,
12
13
  choice_builder:,
14
+ annotation_content_builder:,
13
15
  iiif_annotation_page_factory:,
14
- thumbnail_builder_factory:)
16
+ thumbnail_builder_factory:,
17
+ placeholder_canvas_builder_factory:)
15
18
  @record = record
16
19
  @parent = parent
17
20
  @iiif_canvas_factory = iiif_canvas_factory
18
21
  @content_builder = content_builder
19
22
  @choice_builder = choice_builder
23
+ @annotation_content_builder = annotation_content_builder
20
24
  @iiif_annotation_page_factory = iiif_annotation_page_factory
21
25
  @thumbnail_builder_factory = thumbnail_builder_factory
26
+ @placeholder_canvas_builder_factory = placeholder_canvas_builder_factory
22
27
  apply_record_properties
23
28
  # Presentation 2.x approach
24
29
  attach_image if display_image
25
30
  # Presentation 3.0 approach
26
31
  attach_content if display_content
32
+ attach_annotation if annotation_content
33
+ attach_placeholder_canvas if placeholder_content
27
34
  end
28
35
 
29
36
  def canvas
@@ -44,22 +51,43 @@ module IIIFManifest
44
51
  private
45
52
 
46
53
  def display_image
47
- record.display_image if record.respond_to?(:display_image)
54
+ @display_image ||= record.display_image if record.respond_to?(:display_image)
48
55
  end
49
56
 
50
57
  # @return [Array<Object>] if the record has a display content
51
58
  # @return [NilClass] if there is no display content
52
59
  def display_content
53
- Array.wrap(record.display_content) if record.respond_to?(:display_content) && record.display_content.present?
60
+ @display_content ||= Array.wrap(record.display_content) if record.respond_to?(:display_content)
61
+ @display_content.presence
54
62
  end
55
63
 
64
+ # @return [Array<Object>] if the record has generic annotation content
65
+ # @return [NilClass] if there is no annotation content
66
+ def annotation_content
67
+ @annotation_content ||= Array.wrap(record.annotation_content) if record.respond_to?(:annotation_content)
68
+ @annotation_content.presence
69
+ end
70
+
71
+ def placeholder_content
72
+ @placeholder_content ||= record.placeholder_content if record.respond_to?(:placeholder_content)
73
+ end
74
+
75
+ # rubocop:disable Metrics/AbcSize
56
76
  def apply_record_properties
57
77
  canvas['id'] = path
58
- canvas.label = ManifestBuilder.language_map(record.to_s) if record.to_s.present?
59
78
  annotation_page['id'] = "#{path}/annotation_page/#{annotation_page.index}"
60
79
  canvas.items = [annotation_page]
80
+ apply_canvas_attributes(canvas)
81
+ apply_annotation_content_to(canvas)
61
82
  apply_thumbnail_to(canvas)
62
- canvas.rendering = populate_rendering if populate_rendering.present?
83
+ end
84
+ # rubocop:enable Metrics/AbcSize
85
+
86
+ def apply_annotation_content_to(canvas)
87
+ return if annotation_content.blank?
88
+
89
+ generic_annotation_page['id'] = "#{path}/annotation_page/#{generic_annotation_page.index}"
90
+ canvas.annotations = [generic_annotation_page]
63
91
  end
64
92
 
65
93
  def apply_thumbnail_to(canvas)
@@ -70,10 +98,26 @@ module IIIFManifest
70
98
  end
71
99
  end
72
100
 
101
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
102
+ def apply_canvas_attributes(canvas)
103
+ canvas.label = ManifestBuilder.language_map(record.to_s) if record.to_s.present?
104
+ canvas.rendering = populate(:rendering) if populate(:rendering).present?
105
+ canvas.see_also = populate(:see_also) if populate(:see_also).present?
106
+ canvas.part_of = populate(:part_of) if populate(:part_of).present?
107
+ canvas.metadata = metadata_from_record(record) if metadata_from_record(record).present?
108
+ canvas.summary = ManifestBuilder.language_map(record.description) if record.respond_to?(:description) &&
109
+ record.description.present?
110
+ end
111
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
112
+
73
113
  def annotation_page
74
114
  @annotation_page ||= iiif_annotation_page_factory.new
75
115
  end
76
116
 
117
+ def generic_annotation_page
118
+ @generic_annotation_page ||= iiif_annotation_page_factory.new
119
+ end
120
+
77
121
  def attach_image
78
122
  content_builder.new(display_image).apply(canvas)
79
123
  end
@@ -86,15 +130,52 @@ module IIIFManifest
86
130
  end
87
131
  end
88
132
 
89
- def populate_rendering
90
- return unless record.respond_to?(:sequence_rendering)
91
- record.sequence_rendering.collect do |rendering|
92
- sequence_rendering = rendering.to_h.except('@id', 'label')
93
- sequence_rendering['id'] = rendering['@id']
94
- if rendering['label'].present?
95
- sequence_rendering['label'] = ManifestBuilder.language_map(rendering['label'])
96
- end
97
- sequence_rendering
133
+ def attach_annotation
134
+ annotation_content.each do |an|
135
+ annotation = annotation_content_builder.new(an).apply(canvas)
136
+ generic_annotation_page.items += [annotation]
137
+ end
138
+ end
139
+
140
+ def attach_placeholder_canvas
141
+ canvas.placeholder_canvas = placeholder_canvas_builder_factory.new(placeholder_content, path).build
142
+ end
143
+
144
+ def populate(property)
145
+ property = :sequence_rendering if property == :rendering
146
+
147
+ return unless record.respond_to?(property) && record.send(property).present?
148
+ record.send(property).collect do |prop|
149
+ output = prop.to_h.except('@id', 'label')
150
+ output['id'] = prop['@id']
151
+ output['label'] = ManifestBuilder.language_map(prop['label']) if prop['label'].present?
152
+ output
153
+ end
154
+ end
155
+
156
+ def metadata_from_record(record)
157
+ return unless valid_v3_metadata?
158
+ record.item_metadata
159
+ end
160
+
161
+ # Validate item_metadata against the IIIF spec format for metadata
162
+ #
163
+ # @return [Boolean]
164
+ def valid_v3_metadata?
165
+ return false unless record.respond_to?(:item_metadata)
166
+ metadata = record.item_metadata
167
+ valid_v3_metadata_fields?(metadata)
168
+ end
169
+
170
+ # Item metadata must be an array containing hashes
171
+ #
172
+ # @param metadata [Array<Hash>] a list of metadata with label and value as required keys for each entry
173
+ # @return [Boolean]
174
+ def valid_v3_metadata_fields?(metadata)
175
+ metadata.is_a?(Array) && metadata.all? do |metadata_field|
176
+ metadata_field.is_a?(Hash) &&
177
+ ManifestBuilder.valid_language_map?(metadata_field['label']) &&
178
+ ManifestBuilder.valid_language_map?(metadata_field['value'])
98
179
  end
99
180
  end
100
181
  end
@@ -140,6 +140,44 @@ module IIIFManifest
140
140
  def rendering=(rendering)
141
141
  inner_hash['rendering'] = rendering
142
142
  end
143
+
144
+ def placeholder_canvas=(placeholder_canvas)
145
+ return unless placeholder_canvas.present?
146
+ inner_hash['placeholderCanvas'] = placeholder_canvas
147
+ end
148
+
149
+ def placeholderCanvas
150
+ inner_hash['placeholderCanvas']
151
+ end
152
+
153
+ def annotations
154
+ inner_hash['annotations']
155
+ end
156
+
157
+ def annotations=(annotations)
158
+ inner_hash['annotations'] = annotations
159
+ end
160
+
161
+ def see_also=(see_also)
162
+ inner_hash['seeAlso'] = see_also
163
+ end
164
+
165
+ def part_of=(part_of)
166
+ inner_hash['partOf'] = part_of
167
+ end
168
+
169
+ def metadata=(metadata)
170
+ inner_hash['metadata'] = metadata
171
+ end
172
+
173
+ def summary
174
+ inner_hash['summary']
175
+ end
176
+
177
+ def summary=(summary)
178
+ return unless summary.present?
179
+ inner_hash['summary'] = summary
180
+ end
143
181
  end
144
182
 
145
183
  class Range < IIIFService
@@ -0,0 +1,57 @@
1
+ module IIIFManifest
2
+ module V3
3
+ class ManifestBuilder
4
+ class PlaceholderCanvasBuilder
5
+ attr_reader :placeholder_content, :canvas_path, :iiif_placeholder_canvas_factory, :iiif_annotation_page_factory,
6
+ :content_builder
7
+ def initialize(placeholder_content,
8
+ canvas_path,
9
+ iiif_placeholder_canvas_factory:,
10
+ iiif_annotation_page_factory:,
11
+ content_builder:)
12
+ @placeholder_content = placeholder_content
13
+ @canvas_path = canvas_path
14
+ @iiif_placeholder_canvas_factory = iiif_placeholder_canvas_factory
15
+ @iiif_annotation_page_factory = iiif_annotation_page_factory
16
+ @content_builder = content_builder
17
+ end
18
+
19
+ def build
20
+ return nil if placeholder_content.nil?
21
+
22
+ build_placeholder_canvas
23
+ attach_content
24
+
25
+ placeholder_canvas
26
+ end
27
+
28
+ private
29
+
30
+ def path
31
+ "#{canvas_path}/placeholder"
32
+ end
33
+
34
+ def placeholder_canvas
35
+ @placeholder_canvas ||= iiif_placeholder_canvas_factory.new
36
+ end
37
+
38
+ def build_placeholder_canvas
39
+ placeholder_canvas['id'] = path
40
+ placeholder_canvas['width'] = placeholder_content.width if placeholder_content.width.present?
41
+ placeholder_canvas['height'] = placeholder_content.height if placeholder_content.height.present?
42
+ placeholder_canvas['duration'] = placeholder_content.duration if placeholder_content.duration.present?
43
+ annotation_page['id'] = "#{path}/annotation_page/#{annotation_page.index}"
44
+ placeholder_canvas.items = [annotation_page]
45
+ end
46
+
47
+ def attach_content
48
+ content_builder.new(placeholder_content).apply(placeholder_canvas)
49
+ end
50
+
51
+ def annotation_page
52
+ @annotation_page ||= iiif_annotation_page_factory.new
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -53,7 +53,7 @@ module IIIFManifest
53
53
  manifest.behavior = viewing_hint if viewing_hint.present?
54
54
  manifest.metadata = metadata_from_record(record) if metadata_from_record(record).present?
55
55
  manifest.viewing_direction = viewing_direction if viewing_direction.present?
56
- manifest.service = services if search_service.present?
56
+ manifest.service = services if services.present?
57
57
  manifest.rendering = populate_rendering if populate_rendering.present?
58
58
  homepage = ::IIIFManifest.config.manifest_value_for(record, property: :homepage)
59
59
  manifest.homepage = homepage if homepage.present?
@@ -32,7 +32,7 @@ module IIIFManifest
32
32
  range['id'] = path
33
33
  range['label'] = ManifestBuilder.language_map(record.label) if record.try(:label).present?
34
34
  range['behavior'] = 'top' if top?
35
- range['items'] = canvas_builders.collect { |cb| { 'type' => 'Canvas', 'id' => cb.path } }
35
+ range['items'] = file_set_presenters.collect { |fs| { 'type' => 'Canvas', 'id' => build_canvas_id(fs) } }
36
36
  end
37
37
 
38
38
  def canvas_builders
@@ -69,8 +69,7 @@ module IIIFManifest
69
69
  end
70
70
 
71
71
  def canvas_range_item(range_item)
72
- canvas_builder = canvas_builder_factory.new(range_item, parent)
73
- { 'type' => 'Canvas', 'id' => canvas_builder.path }
72
+ { 'type' => 'Canvas', 'id' => build_canvas_id(range_item) }
74
73
  end
75
74
 
76
75
  def range_range_item(range_item)
@@ -81,6 +80,20 @@ module IIIFManifest
81
80
  iiif_range_factory: iiif_range_factory
82
81
  )
83
82
  end
83
+
84
+ private
85
+
86
+ def file_set_presenters
87
+ record.respond_to?(:file_set_presenters) ? record.file_set_presenters : []
88
+ end
89
+
90
+ def build_canvas_id(canvas_item)
91
+ path = "#{parent.manifest_url}/canvas/#{canvas_item.id}"
92
+ if canvas_item.respond_to?(:media_fragment) && canvas_item.media_fragment.present?
93
+ path << "##{canvas_item.media_fragment}"
94
+ end
95
+ path
96
+ end
84
97
  end
85
98
  end
86
99
  end
@@ -3,6 +3,8 @@ require_relative 'manifest_builder/canvas_builder'
3
3
  require_relative 'manifest_builder/record_property_builder'
4
4
  require_relative 'manifest_builder/choice_builder'
5
5
  require_relative 'manifest_builder/content_builder'
6
+ require_relative 'manifest_builder/annotation_content_builder'
7
+ require_relative 'manifest_builder/placeholdercanvas_builder'
6
8
  require_relative 'manifest_builder/body_builder'
7
9
  require_relative 'manifest_builder/structure_builder'
8
10
  require_relative 'manifest_builder/image_service_builder'
@@ -56,8 +56,19 @@ module IIIFManifest
56
56
  iiif_canvas_factory: iiif_canvas_factory,
57
57
  content_builder: content_builder,
58
58
  choice_builder: choice_builder,
59
+ annotation_content_builder: annotation_content_builder,
59
60
  iiif_annotation_page_factory: iiif_annotation_page_factory,
60
- thumbnail_builder_factory: thumbnail_builder_factory
61
+ thumbnail_builder_factory: thumbnail_builder_factory,
62
+ placeholder_canvas_builder_factory: placeholder_canvas_builder_factory
63
+ )
64
+ end
65
+
66
+ def placeholder_canvas_builder_factory
67
+ IIIFManifest::ManifestServiceLocator::InjectedFactory.new(
68
+ ManifestBuilder::PlaceholderCanvasBuilder,
69
+ iiif_placeholder_canvas_factory: iiif_placeholder_canvas_factory,
70
+ iiif_annotation_page_factory: iiif_annotation_page_factory,
71
+ content_builder: content_builder
61
72
  )
62
73
  end
63
74
 
@@ -78,6 +89,14 @@ module IIIFManifest
78
89
  )
79
90
  end
80
91
 
92
+ def annotation_content_builder
93
+ IIIFManifest::ManifestServiceLocator::InjectedFactory.new(
94
+ ManifestBuilder::AnnotationContentBuilder,
95
+ iiif_annotation_factory: iiif_annotation_factory,
96
+ body_builder_factory: body_builder_factory
97
+ )
98
+ end
99
+
81
100
  def body_builder_factory
82
101
  IIIFManifest::ManifestServiceLocator::InjectedFactory.new(
83
102
  ManifestBuilder::BodyBuilder,
@@ -141,6 +160,10 @@ module IIIFManifest
141
160
  IIIFManifest::V3::ManifestBuilder::IIIFManifest::Canvas
142
161
  end
143
162
 
163
+ def iiif_placeholder_canvas_factory
164
+ IIIFManifest::V3::ManifestBuilder::IIIFManifest::Canvas
165
+ end
166
+
144
167
  def iiif_range_factory
145
168
  IIIFManifest::V3::ManifestBuilder::IIIFManifest::Range
146
169
  end
@@ -9,5 +9,6 @@ module IIIFManifest
9
9
  autoload :ManifestFactory
10
10
  autoload :ManifestServiceLocator
11
11
  autoload :DisplayContent
12
+ autoload :AnnotationContent
12
13
  end
13
14
  end
@@ -1,3 +1,3 @@
1
1
  module IIIFManifest
2
- VERSION = '1.3.1'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iiif_manifest
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Coyne
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-02-21 00:00:00.000000000 Z
12
+ date: 2024-02-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -159,14 +159,17 @@ files:
159
159
  - lib/iiif_manifest/manifest_factory.rb
160
160
  - lib/iiif_manifest/manifest_service_locator.rb
161
161
  - lib/iiif_manifest/v3.rb
162
+ - lib/iiif_manifest/v3/annotation_content.rb
162
163
  - lib/iiif_manifest/v3/display_content.rb
163
164
  - lib/iiif_manifest/v3/manifest_builder.rb
165
+ - lib/iiif_manifest/v3/manifest_builder/annotation_content_builder.rb
164
166
  - lib/iiif_manifest/v3/manifest_builder/body_builder.rb
165
167
  - lib/iiif_manifest/v3/manifest_builder/canvas_builder.rb
166
168
  - lib/iiif_manifest/v3/manifest_builder/choice_builder.rb
167
169
  - lib/iiif_manifest/v3/manifest_builder/content_builder.rb
168
170
  - lib/iiif_manifest/v3/manifest_builder/iiif_service.rb
169
171
  - lib/iiif_manifest/v3/manifest_builder/image_service_builder.rb
172
+ - lib/iiif_manifest/v3/manifest_builder/placeholdercanvas_builder.rb
170
173
  - lib/iiif_manifest/v3/manifest_builder/record_property_builder.rb
171
174
  - lib/iiif_manifest/v3/manifest_builder/structure_builder.rb
172
175
  - lib/iiif_manifest/v3/manifest_builder/thumbnail_builder.rb
@@ -192,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
195
  - !ruby/object:Gem::Version
193
196
  version: '0'
194
197
  requirements: []
195
- rubygems_version: 3.1.6
198
+ rubygems_version: 3.5.6
196
199
  signing_key:
197
200
  specification_version: 4
198
201
  summary: Generate IIIF presentation manifests for Hydra::Works