iiif-presentation 1.4.1 → 1.5.0

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +8 -1
  3. data/.standard.yml +1 -0
  4. data/Gemfile +5 -5
  5. data/README.md +1 -1
  6. data/Rakefile +4 -2
  7. data/VERSION +1 -1
  8. data/bin/standardrb +16 -0
  9. data/iiif-presentation.gemspec +23 -23
  10. data/lib/iiif/hash_behaviours.rb +29 -33
  11. data/lib/iiif/ordered_hash.rb +33 -33
  12. data/lib/iiif/presentation/abstract_resource.rb +17 -22
  13. data/lib/iiif/presentation/annotation.rb +9 -11
  14. data/lib/iiif/presentation/annotation_list.rb +8 -10
  15. data/lib/iiif/presentation/canvas.rb +11 -13
  16. data/lib/iiif/presentation/collection.rb +7 -9
  17. data/lib/iiif/presentation/image_resource.rb +33 -35
  18. data/lib/iiif/presentation/layer.rb +8 -10
  19. data/lib/iiif/presentation/manifest.rb +9 -12
  20. data/lib/iiif/presentation/range.rb +8 -10
  21. data/lib/iiif/presentation/resource.rb +5 -7
  22. data/lib/iiif/presentation/sequence.rb +8 -11
  23. data/lib/iiif/presentation/service.rb +1 -2
  24. data/lib/iiif/presentation.rb +7 -7
  25. data/lib/iiif/service.rb +121 -102
  26. data/lib/iiif/v3/abstract_resource.rb +132 -144
  27. data/lib/iiif/v3/presentation/annotation.rb +20 -21
  28. data/lib/iiif/v3/presentation/annotation_collection.rb +7 -9
  29. data/lib/iiif/v3/presentation/annotation_page.rb +14 -15
  30. data/lib/iiif/v3/presentation/canvas.rb +21 -22
  31. data/lib/iiif/v3/presentation/choice.rb +14 -15
  32. data/lib/iiif/v3/presentation/collection.rb +7 -8
  33. data/lib/iiif/v3/presentation/image_resource.rb +21 -22
  34. data/lib/iiif/v3/presentation/manifest.rb +18 -19
  35. data/lib/iiif/v3/presentation/nav_place.rb +125 -29
  36. data/lib/iiif/v3/presentation/range.rb +10 -10
  37. data/lib/iiif/v3/presentation/resource.rb +4 -5
  38. data/lib/iiif/v3/presentation/sequence.rb +10 -11
  39. data/lib/iiif/v3/presentation/service.rb +12 -13
  40. data/lib/iiif/v3/presentation.rb +8 -8
  41. data/spec/integration/iiif/presentation/image_resource_spec.rb +54 -58
  42. data/spec/integration/iiif/service_spec.rb +76 -77
  43. data/spec/integration/iiif/v3/abstract_resource_spec.rb +73 -73
  44. data/spec/integration/iiif/v3/presentation/image_resource_spec.rb +53 -56
  45. data/spec/spec_helper.rb +56 -64
  46. data/spec/unit/iiif/hash_behaviours_spec.rb +384 -387
  47. data/spec/unit/iiif/ordered_hash_spec.rb +86 -91
  48. data/spec/unit/iiif/presentation/abstract_resource_spec.rb +64 -70
  49. data/spec/unit/iiif/presentation/annotation_list_spec.rb +1 -3
  50. data/spec/unit/iiif/presentation/annotation_spec.rb +1 -3
  51. data/spec/unit/iiif/presentation/canvas_spec.rb +11 -14
  52. data/spec/unit/iiif/presentation/collection_spec.rb +26 -33
  53. data/spec/unit/iiif/presentation/image_resource_spec.rb +4 -6
  54. data/spec/unit/iiif/presentation/layer_spec.rb +16 -20
  55. data/spec/unit/iiif/presentation/manifest_spec.rb +45 -48
  56. data/spec/unit/iiif/presentation/range_spec.rb +18 -22
  57. data/spec/unit/iiif/presentation/resource_spec.rb +3 -6
  58. data/spec/unit/iiif/presentation/sequence_spec.rb +58 -63
  59. data/spec/unit/iiif/presentation/shared_examples/abstract_resource_only_keys.rb +4 -10
  60. data/spec/unit/iiif/presentation/shared_examples/any_type_keys.rb +5 -9
  61. data/spec/unit/iiif/presentation/shared_examples/array_only_keys.rb +9 -17
  62. data/spec/unit/iiif/presentation/shared_examples/int_only_keys.rb +6 -14
  63. data/spec/unit/iiif/presentation/shared_examples/string_only_keys.rb +6 -13
  64. data/spec/unit/iiif/service_spec.rb +8 -13
  65. data/spec/unit/iiif/v3/abstract_resource_define_methods_for_spec.rb +39 -37
  66. data/spec/unit/iiif/v3/abstract_resource_spec.rb +128 -143
  67. data/spec/unit/iiif/v3/presentation/annotation_collection_spec.rb +14 -17
  68. data/spec/unit/iiif/v3/presentation/annotation_page_spec.rb +71 -69
  69. data/spec/unit/iiif/v3/presentation/annotation_spec.rb +249 -233
  70. data/spec/unit/iiif/v3/presentation/canvas_spec.rb +211 -193
  71. data/spec/unit/iiif/v3/presentation/choice_spec.rb +81 -77
  72. data/spec/unit/iiif/v3/presentation/collection_spec.rb +24 -29
  73. data/spec/unit/iiif/v3/presentation/image_resource_spec.rb +70 -71
  74. data/spec/unit/iiif/v3/presentation/manifest_spec.rb +248 -237
  75. data/spec/unit/iiif/v3/presentation/nav_place_spec.rb +249 -49
  76. data/spec/unit/iiif/v3/presentation/range_spec.rb +28 -29
  77. data/spec/unit/iiif/v3/presentation/resource_spec.rb +83 -82
  78. data/spec/unit/iiif/v3/presentation/sequence_spec.rb +126 -120
  79. data/spec/unit/iiif/v3/presentation/service_spec.rb +123 -124
  80. data/spec/unit/iiif/v3/presentation/shared_examples/abstract_resource_only_keys.rb +4 -8
  81. data/spec/unit/iiif/v3/presentation/shared_examples/any_type_keys.rb +5 -7
  82. data/spec/unit/iiif/v3/presentation/shared_examples/array_only_keys.rb +9 -13
  83. data/spec/unit/iiif/v3/presentation/shared_examples/hash_only_keys.rb +9 -13
  84. data/spec/unit/iiif/v3/presentation/shared_examples/int_only_keys.rb +6 -10
  85. data/spec/unit/iiif/v3/presentation/shared_examples/numeric_only_keys.rb +6 -10
  86. data/spec/unit/iiif/v3/presentation/shared_examples/string_only_keys.rb +6 -10
  87. data/spec/unit/iiif/v3/presentation/shared_examples/uri_only_keys.rb +9 -13
  88. metadata +27 -69
@@ -1,4 +1,6 @@
1
- require 'geo/coord'
1
+ require "geo/coord"
2
+ require "json"
3
+ require "uri"
2
4
 
3
5
  module IIIF
4
6
  module V3
@@ -6,8 +8,35 @@ module IIIF
6
8
  class NavPlace < IIIF::V3::AbstractResource
7
9
  Rect = Struct.new(:coord1, :coord2)
8
10
 
9
- COORD_REGEX = /(?:(?<hemisphere>[NSEW])\s*)?(?<degrees>\d+)[°⁰*](?:\s*(?<minutes>\d+)[\'ʹ′])?(?:\s*(?<seconds>\d+)["ʺ″])?(?:\s*(?<hemisphere>[NSEW]))?/
10
- def initialize(coordinate_texts:, base_uri:)
11
+ DECIMAL = /\d+(?:\.\d+)?/
12
+
13
+ # Matches one coordinate, in either of two forms:
14
+ # - degrees with a degree sign, optionally followed by minutes and seconds,
15
+ # any of which may be decimal, with the hemisphere before or after
16
+ # ("W 23°54'00\"", "23°54′00″W", "E 138.5°", "N 37°26ʹ30.5″");
17
+ # - decimal degrees with no degree sign, which need the hemisphere first
18
+ # ("E 138.0").
19
+ COORD_REGEX = /
20
+ (?:(?<hemisphere>[NSEW])\s*)?(?<![\d.])(?<degrees>#{DECIMAL})[°⁰*]
21
+ (?:\s*(?<minutes>#{DECIMAL})['ʹ′])?
22
+ (?:\s*(?<seconds>#{DECIMAL})["ʺ″])?
23
+ (?:\s*(?<hemisphere>[NSEW]))?
24
+ |
25
+ (?<hemisphere>[NSEW])\s*(?<degrees>\d+\.\d+)(?![\d.°⁰*'ʹ′"ʺ″])
26
+ /x
27
+
28
+ # WKT versions of points and boxes (polygon/multipolygon).
29
+ # See: https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry
30
+ WKT_REGEX = /\A\s*(?<type>POINT|POLYGON|MULTIPOLYGON)\s*(?<body>\(.*\))\s*\z/im
31
+ WKT_NUMBER = /[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?/i
32
+
33
+ # @param [Array<String>] coordinate_texts points or boxes, either as text
34
+ # ("E 103°48ʹ/S 3°46ʹ", "W 23°54ʹ--E 53°36ʹ/N 71°19ʹ--N 33°30ʹ") or as a WKT
35
+ # POINT, POLYGON or MULTIPOLYGON; any that can't be read are skipped
36
+ # @param [String, nil] base_uri optional prefix for the Feature Collection and Feature ids
37
+ def initialize(coordinate_texts:, base_uri: nil)
38
+ raise ArgumentError, "base_uri must be an absolute HTTP(S) URI, got #{base_uri.inspect}" unless base_uri.nil? || base_uri.start_with?(/https?:\/\//)
39
+
11
40
  @coordinate_texts = coordinate_texts
12
41
  @base_uri = base_uri
13
42
  end
@@ -18,13 +47,11 @@ module IIIF
18
47
  end
19
48
 
20
49
  def build
21
- raise ArgumentError.new('invalid coordinates') unless valid?
50
+ raise ArgumentError.new("invalid coordinates") unless valid?
22
51
 
23
- {
24
- id: "#{base_uri}/feature-collection/1",
25
- type: 'FeatureCollection',
26
- features: features
27
- }
52
+ with_id("#{base_uri}/feature-collection/1",
53
+ type: "FeatureCollection",
54
+ features: features)
28
55
  end
29
56
 
30
57
  private
@@ -33,6 +60,8 @@ module IIIF
33
60
 
34
61
  def coordinates
35
62
  @coordinates ||= coordinate_texts.map do |coordinate_text|
63
+ next wkt_geometry(coordinate_text) if coordinate_text.match?(WKT_REGEX)
64
+
36
65
  coordinate_parts = coordinate_text.split(%r{ ?--|/})
37
66
  case coordinate_parts.length
38
67
  when 2
@@ -49,7 +78,9 @@ module IIIF
49
78
  return unless long_matcher && lat_matcher
50
79
 
51
80
  Geo::Coord.new(latd: lat_matcher[:degrees], latm: lat_matcher[:minutes], lats: lat_matcher[:seconds], lath: lat_matcher[:hemisphere],
52
- lngd: long_matcher[:degrees], lngm: long_matcher[:minutes], lngs: long_matcher[:seconds], lngh: long_matcher[:hemisphere])
81
+ lngd: long_matcher[:degrees], lngm: long_matcher[:minutes], lngs: long_matcher[:seconds], lngh: long_matcher[:hemisphere])
82
+ rescue ArgumentError
83
+ nil
53
84
  end
54
85
 
55
86
  def rect_for(coordinate_parts)
@@ -60,47 +91,112 @@ module IIIF
60
91
  Rect.new(coord1, coord2)
61
92
  end
62
93
 
94
+ # Turn WKT strings into GeoJSON geometry.
95
+ # @return [Hash, nil] the GeoJSON geometry, or nil if the WKT isn't one we can use
96
+ def wkt_geometry(text)
97
+ match = text.match(WKT_REGEX)
98
+ body = match[:body].gsub(/(#{WKT_NUMBER})\s+(#{WKT_NUMBER})/o) { "[#{Float($1).round(6)},#{Float($2).round(6)}]" }
99
+ coordinates = JSON.parse(body.tr("()", "[]"))
100
+
101
+ case match[:type].upcase
102
+ when "POINT"
103
+ {type: "Point", coordinates: coordinates.first} if coordinates.length == 1 && point?(coordinates.first)
104
+ when "POLYGON"
105
+ {type: "Polygon", coordinates: oriented(coordinates)} if polygon?(coordinates)
106
+ when "MULTIPOLYGON"
107
+ {type: "MultiPolygon", coordinates: coordinates.map { |polygon| oriented(polygon) }} if coordinates.any? && coordinates.all? { |polygon| polygon?(polygon) }
108
+ end
109
+ rescue JSON::ParserError, ArgumentError
110
+ nil
111
+ end
112
+
113
+ def point?(value)
114
+ value.is_a?(Array) && value.length == 2 && value.all?(Numeric) && value[0].between?(-180, 180) && value[1].between?(-90, 90)
115
+ end
116
+
117
+ def polygon?(value)
118
+ value.is_a?(Array) && value.any? && value.all? do |ring|
119
+ ring.is_a?(Array) && ring.length >= 4 && ring.all? { |position| point?(position) } && ring.first == ring.last
120
+ end
121
+ end
122
+
123
+ # WKT doesn't fix which way rings run, so turn the exterior counterclockwise
124
+ # and any holes clockwise (RFC 7946, section 3.1.6).
125
+ def oriented(polygon)
126
+ polygon.each_with_index.map do |ring, index|
127
+ area = ring.each_cons(2).sum { |(x1, y1), (x2, y2)| x1 * y2 - x2 * y1 }
128
+ ((area > 0) == index.zero?) ? ring : ring.reverse
129
+ end
130
+ end
131
+
63
132
  def features
64
133
  coordinates.map.with_index(1) do |coordinate, index|
65
- {
66
- id: "#{base_uri}/iiif/feature/#{index}",
67
- type: 'Feature',
134
+ with_id("#{base_uri}/iiif/feature/#{index}",
135
+ type: "Feature",
68
136
  properties: {},
69
- geometry: coordinate.is_a?(Rect) ? polygon_geometry(coordinate) : point_geometry(coordinate)
70
- }
137
+ geometry: geometry_for(coordinate))
71
138
  end
72
139
  end
73
140
 
141
+ def geometry_for(coordinate)
142
+ case coordinate
143
+ when Rect then polygon_geometry(coordinate)
144
+ when Geo::Coord then point_geometry(coordinate)
145
+ else coordinate # already a GeoJSON geometry, read from WKT
146
+ end
147
+ end
148
+
149
+ # The navPlace extension makes ids optional, so leave them out without a base_uri.
150
+ def with_id(id, resource)
151
+ base_uri ? {id: id}.merge(resource) : resource
152
+ end
153
+
74
154
  def point_geometry(coord)
75
155
  {
76
- type: 'Point',
156
+ type: "Point",
77
157
  coordinates: [format(coord.lng), format(coord.lat)]
78
158
  }
79
159
  end
80
160
 
81
161
  def polygon_geometry(rect)
162
+ west = edge(format(rect.coord1.lng), from: 180.0, to: -180.0)
163
+ east = edge(format(rect.coord2.lng), from: -180.0, to: 180.0)
164
+ south, north = [format(rect.coord1.lat), format(rect.coord2.lat)].minmax
165
+
166
+ return {type: "Polygon", coordinates: [ring(west, east, south, north)]} unless west > east
167
+
168
+ # A box reads west to east, so a west edge greater than its east edge means
169
+ # the box crosses the antimeridian. GeoJSON has no wrapping boxes, so such a
170
+ # box is split there into two polygons (RFC 7946, section 3.1.9).
82
171
  {
83
- type: 'Polygon',
172
+ type: "MultiPolygon",
84
173
  coordinates: [
85
- [
86
- [format(rect.coord1.lng), format(rect.coord1.lat)],
87
- [format(rect.coord2.lng), format(rect.coord1.lat)],
88
- [format(rect.coord2.lng), format(rect.coord2.lat)],
89
- [format(rect.coord1.lng), format(rect.coord2.lat)],
90
- [format(rect.coord1.lng), format(rect.coord1.lat)]
91
- ]
174
+ [ring(west, 180.0, south, north)],
175
+ [ring(-180.0, east, south, north)]
92
176
  ]
93
177
  }
94
178
  end
95
179
 
180
+ # If a box edge sits right on the antimeridian it could be counted as
181
+ # 180 or -180; we pick whichever one won't require splitting (see above).
182
+ def edge(lng, from:, to:)
183
+ (lng == from) ? to : lng
184
+ end
185
+
186
+ # Exterior rings run counterclockwise (RFC 7946, section 3.1.6).
187
+ def ring(west, east, south, north)
188
+ [[west, south], [east, south], [east, north], [west, north], [west, south]]
189
+ end
190
+
191
+ # GeoJSON positions are arrays of numbers (RFC 7946, section 3.1.1), so
192
+ # coordinates are emitted as Floats rather than strings.
96
193
  # @param [BigDecimal] coordinate value from geocoord gem
97
- # @return [String] string formatted with max 6 digits after the decimal point
98
- # The to_f ensures removal of scientific notation of BigDecimal before converting to a string.
194
+ # @return [Float] value rounded to at most 6 digits after the decimal point
99
195
  # examples:
100
- # input value is BigDecimal("-23.9") or "0.239e2", output value is "-23.9" as string
101
- # input value is BigDecimal("23.9424213434") or "0.239424213434e2", output value is "23.942421" as string
196
+ # input value is BigDecimal("-23.9") or "0.239e2", output value is -23.9
197
+ # input value is BigDecimal("23.9424216434") or "0.239424216434e2", output value is 23.942422
102
198
  def format(decimal)
103
- decimal.truncate(6).to_f.to_s
199
+ decimal.round(6).to_f
104
200
  end
105
201
  end
106
202
  end
@@ -3,33 +3,33 @@ module IIIF
3
3
  module Presentation
4
4
  # Ranges are linked or embedded within the manifest in a structures field
5
5
  class Range < Sequence
6
- TYPE = 'Range'.freeze
6
+ TYPE = "Range".freeze
7
7
  VALID_ITEM_TYPES = [IIIF::V3::Presentation::Canvas, IIIF::V3::Presentation::Range]
8
8
 
9
9
  def required_keys
10
- super + %w{ id label }
10
+ super + %w[id label]
11
11
  end
12
12
 
13
13
  # TODO: contentAnnotations: links to AnnotationCollection
14
14
  # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
15
15
 
16
16
  def array_only_keys
17
- super + %w{ members }
17
+ super + %w[members]
18
18
  end
19
19
 
20
20
  def legal_viewing_hint_values
21
- super + %w{ top }
21
+ super + %w[top]
22
22
  end
23
23
 
24
- def initialize(hsh={})
25
- hsh['type'] = TYPE unless hsh.has_key? 'type'
26
- super(hsh)
24
+ def initialize(hsh = {})
25
+ hsh["type"] = TYPE unless hsh.has_key? "type"
26
+ super
27
27
  end
28
28
 
29
29
  def validate
30
30
  super
31
- validate_list(self['items']) if self['items']
32
- validate_list(self['canvases']) if self['canvases']
31
+ validate_list(self["items"]) if self["items"]
32
+ validate_list(self["canvases"]) if self["canvases"]
33
33
  # TODO: Ranges must have URIs and they should be http(s) URIs.
34
34
  # TODO: contentAnnotations: links to AnnotationCollection
35
35
  # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
@@ -40,7 +40,7 @@ module IIIF
40
40
  def validate_list(canvas_array)
41
41
  return if canvas_array.all? { |entry| VALID_ITEM_TYPES.include?(entry.class) }
42
42
 
43
- m = "All entries in the (items or canvases) array must be one of #{VALID_ITEM_TYPES.join(', ')}"
43
+ m = "All entries in the (items or canvases) array must be one of #{VALID_ITEM_TYPES.join(", ")}"
44
44
  raise IIIF::V3::Presentation::IllegalValueError, m
45
45
  end
46
46
  end
@@ -3,23 +3,22 @@ module IIIF
3
3
  module Presentation
4
4
  # class for generic content resource
5
5
  class Resource < IIIF::V3::AbstractResource
6
-
7
6
  def required_keys
8
- super + %w{ id }
7
+ super + %w[id]
9
8
  end
10
9
 
11
10
  def prohibited_keys
12
- super + PAGING_PROPERTIES + %w{ nav_date viewing_direction start_canvas content_annotations}
11
+ super + PAGING_PROPERTIES + %w[nav_date viewing_direction start_canvas content_annotations]
13
12
  end
14
13
 
15
14
  def uri_only_keys
16
- super + %w{ id }
15
+ super + %w[id]
17
16
  end
18
17
 
19
18
  def validate
20
19
  super
21
20
 
22
- unless self['id'] =~ /^https?:/
21
+ unless /^https?:/.match?(self["id"])
23
22
  err_msg = "id must be an http(s) URI for #{self.class}"
24
23
  raise IIIF::V3::Presentation::IllegalValueError, err_msg
25
24
  end
@@ -2,8 +2,7 @@ module IIIF
2
2
  module V3
3
3
  module Presentation
4
4
  class Sequence < IIIF::V3::AbstractResource
5
-
6
- TYPE = 'Sequence'.freeze
5
+ TYPE = "Sequence".freeze
7
6
 
8
7
  # NOTE: relaxing requirement for items as Universal Viewer currently only accepts canvases
9
8
  # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
@@ -12,22 +11,22 @@ module IIIF
12
11
  # end
13
12
 
14
13
  def prohibited_keys
15
- super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w{ nav_date content_annotations }
14
+ super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES + %w[nav_date content_annotations]
16
15
  end
17
16
 
18
17
  # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases
19
18
  # see https://github.com/sul-dlss/osullivan/issues/27, sul-dlss/purl/issues/167
20
19
  def array_only_keys
21
- super + %w{ canvases }
20
+ super + %w[canvases]
22
21
  end
23
22
 
24
23
  def legal_viewing_hint_values
25
- %w{ individuals paged continuous auto-advance }
24
+ %w[individuals paged continuous auto-advance]
26
25
  end
27
26
 
28
- def initialize(hsh={})
29
- hsh['type'] = TYPE unless hsh.has_key? 'type'
30
- super(hsh)
27
+ def initialize(hsh = {})
28
+ hsh["type"] = TYPE unless hsh.has_key? "type"
29
+ super
31
30
  end
32
31
 
33
32
  def validate
@@ -36,9 +35,9 @@ module IIIF
36
35
  # Canvas object list
37
36
  # NOTE: allowing 'items' or 'canvases' as Universal Viewer currently only accepts canvases
38
37
  # 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.'
38
+ unless self["items"]&.any? ||
39
+ self["canvases"]&.any?
40
+ m = "The (items or canvases) list must have at least one entry."
42
41
  raise IIIF::V3::Presentation::MissingRequiredKeyError, m
43
42
  end
44
43
  # TODO: startCanvas: A link from a Sequence or Range to a Canvas that is contained within it
@@ -3,43 +3,42 @@ module IIIF
3
3
  module Presentation
4
4
  # See http://prezi3.iiif.io/api/annex/services for more info
5
5
  class Service < AbstractResource
6
-
7
6
  # 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
7
+ IIIF_IMAGE_V2_TYPE = "ImageService2".freeze
8
+ IIIF_IMAGE_V2_LEVEL1_PROFILE = "http://iiif.io/api/image/2/level1.json".freeze
9
+ IIIF_AUTHENTICATION_V1_LOGIN_PROFILE = "http://iiif.io/api/auth/1/login".freeze
10
+ IIIF_AUTHENTICATION_V1_TOKEN_PROFILE = "http://iiif.io/api/auth/1/token".freeze
12
11
 
13
12
  # service class doesn't require type
14
13
  def required_keys
15
- super.reject {|el| el == 'type' }
14
+ super.reject { |el| el == "type" }
16
15
  end
17
16
 
18
17
  def prohibited_keys
19
18
  super + CONTENT_RESOURCE_PROPERTIES + PAGING_PROPERTIES +
20
- %w{ nav_date viewing_direction start_canvas content_annotations }
19
+ %w[nav_date viewing_direction start_canvas content_annotations]
21
20
  end
22
21
 
23
22
  def uri_only_keys
24
- super + %w{ @context id @id }
23
+ super + %w[@context id @id]
25
24
  end
26
25
 
27
26
  def any_type_keys
28
- super + %w{ profile }
27
+ super + %w[profile]
29
28
  end
30
29
 
31
30
  def validate
32
31
  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')
32
+ if self["type"] == IIIF_IMAGE_V2_TYPE || self["@type"] == IIIF_IMAGE_V2_TYPE
33
+ unless has_key?("id") || has_key?("@id")
35
34
  m = "id or @id values are required for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
36
35
  raise IIIF::V3::Presentation::MissingRequiredKeyError, m
37
36
  end
38
- if self.has_key?('id') && self.has_key?('@id') && (self['@id'] != self['id'])
37
+ if has_key?("id") && has_key?("@id") && (self["@id"] != self["id"])
39
38
  m = "id and @id values must match for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
40
39
  raise IIIF::V3::Presentation::IllegalValueError, m
41
40
  end
42
- unless self.has_key?('profile')
41
+ unless has_key?("profile")
43
42
  m = "profile should be present for IIIF::V3::Presentation::Service with type or @type #{IIIF_IMAGE_V2_TYPE}"
44
43
  raise IIIF::V3::Presentation::MissingRequiredKeyError, m
45
44
  end
@@ -1,9 +1,9 @@
1
- require_relative 'abstract_resource'
2
- require_relative '../ordered_hash'
1
+ require_relative "abstract_resource"
2
+ require_relative "../ordered_hash"
3
3
 
4
4
  # NOTE: image_resource must follow resource due to inheritance
5
5
  # NOTE: range must follow sequence due to inheritance
6
- %w{
6
+ %w[
7
7
  annotation
8
8
  annotation_collection
9
9
  annotation_page
@@ -17,16 +17,16 @@ require_relative '../ordered_hash'
17
17
  sequence
18
18
  range
19
19
  service
20
- }.each do |f|
21
- require File.join(File.dirname(__FILE__), 'presentation', f)
20
+ ].each do |f|
21
+ require File.join(File.dirname(__FILE__), "presentation", f)
22
22
  end
23
23
 
24
24
  module IIIF
25
25
  module V3
26
26
  module Presentation
27
- CONTEXT ||= [
28
- 'http://www.w3.org/ns/anno.jsonld',
29
- 'http://iiif.io/api/presentation/3/context.json'
27
+ CONTEXT = [
28
+ "http://www.w3.org/ns/anno.jsonld",
29
+ "http://iiif.io/api/presentation/3/context.json"
30
30
  ]
31
31
 
32
32
  class MissingRequiredKeyError < StandardError; end
@@ -1,122 +1,118 @@
1
1
  describe IIIF::Presentation::ImageResource do
2
2
  vcr_options = {
3
- cassette_name: 'pul_loris_cassette',
3
+ cassette_name: "pul_loris_cassette",
4
4
  record: :new_episodes,
5
5
  serialize_with: :json
6
6
  }
7
7
 
8
- describe 'self#create_image_api_image_resource', vcr: vcr_options do
9
-
10
- let(:image_server) { 'https://libimages.princeton.edu/loris' }
8
+ describe "self#create_image_api_image_resource", vcr: vcr_options do
9
+ let(:image_server) { "https://libimages.princeton.edu/loris" }
11
10
 
12
11
  let(:valid_service_id) {
13
- id = 'pudl0001%2F4612422%2F00000001.jp2'
12
+ id = "pudl0001%2F4612422%2F00000001.jp2"
14
13
  "#{image_server}/#{id}"
15
14
  }
16
15
 
17
16
  let(:invalid_service_id) {
18
- id = 'xxxx%2F4612422%2F00000001.jp2'
17
+ id = "xxxx%2F4612422%2F00000001.jp2"
19
18
  "#{image_server}/#{id}"
20
19
  }
21
20
 
22
- it 'returns an ImageResource' do
21
+ it "returns an ImageResource" do
23
22
  instance = described_class.create_image_api_image_resource(service_id: valid_service_id)
24
23
  expect(instance.class).to be described_class
25
24
  end
26
25
 
27
-
28
- describe 'has expected values from our fixture' do
29
- it 'when copy_info is false' do
30
- opts = { service_id: valid_service_id }
26
+ describe "has expected values from our fixture" do
27
+ it "when copy_info is false" do
28
+ opts = {service_id: valid_service_id}
31
29
  resource = described_class.create_image_api_image_resource(opts)
32
30
  # expect(resource['@context']).to eq 'http://iiif.io/api/presentation/2/context.json'
33
31
  # @context is only added when we call to_json...
34
- expect(resource['@id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg'
35
- expect(resource['@type']).to eq 'dctypes:Image'
32
+ expect(resource["@id"]).to eq "https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg"
33
+ expect(resource["@type"]).to eq "dctypes:Image"
36
34
  expect(resource.format).to eq "image/jpeg"
37
35
  expect(resource.width).to eq 3047
38
36
  expect(resource.height).to eq 7200
39
- expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json'
40
- expect(resource.service['@id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2'
41
- expect(resource.service['profile']).to eq 'http://iiif.io/api/image/2/level2.json'
37
+ expect(resource.service["@context"]).to eq "http://iiif.io/api/image/2/context.json"
38
+ expect(resource.service["@id"]).to eq "https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2"
39
+ expect(resource.service["profile"]).to eq "http://iiif.io/api/image/2/level2.json"
42
40
  end
43
- it 'copies over all the info (when copy_info is true)' do
44
- opts = { service_id: valid_service_id, copy_info: true }
41
+ it "copies over all the info (when copy_info is true)" do
42
+ opts = {service_id: valid_service_id, copy_info: true}
45
43
  resource = described_class.create_image_api_image_resource(opts)
46
- expect(resource['@id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg'
47
- expect(resource['@type']).to eq 'dctypes:Image'
44
+ expect(resource["@id"]).to eq "https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2/full/!200,200/0/default.jpg"
45
+ expect(resource["@type"]).to eq "dctypes:Image"
48
46
  expect(resource.format).to eq "image/jpeg"
49
47
  expect(resource.width).to eq 3047
50
48
  expect(resource.height).to eq 7200
51
- expect(resource.service['@context']).to eq 'http://iiif.io/api/image/2/context.json'
52
- expect(resource.service['@id']).to eq 'https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2'
53
- expect(resource.service['profile']).to eq [
54
- 'http://iiif.io/api/image/2/level2.json',
49
+ expect(resource.service["@context"]).to eq "http://iiif.io/api/image/2/context.json"
50
+ expect(resource.service["@id"]).to eq "https://libimages.princeton.edu/loris/pudl0001%2F4612422%2F00000001.jp2"
51
+ expect(resource.service["profile"]).to eq [
52
+ "http://iiif.io/api/image/2/level2.json",
55
53
  {
56
- 'supports' => [
57
- 'canonicalLinkHeader', 'profileLinkHeader', 'mirroring',
58
- 'rotationArbitrary', 'regionSquare', 'sizeAboveFull'
54
+ "supports" => [
55
+ "canonicalLinkHeader", "profileLinkHeader", "mirroring",
56
+ "rotationArbitrary", "regionSquare", "sizeAboveFull"
59
57
  ],
60
- 'qualities' => ['default', 'bitonal', 'gray', 'color'],
61
- 'formats'=>['jpg', 'png', 'gif', 'webp']
58
+ "qualities" => ["default", "bitonal", "gray", "color"],
59
+ "formats" => ["jpg", "png", "gif", "webp"]
62
60
  }
63
61
  ]
64
- expect(resource.service['tiles']).to eq [ {
65
- 'width' => 1024,
66
- 'scaleFactors' => [ 1, 2, 4, 8, 16, 32 ]
67
- } ]
68
- expect(resource.service['sizes']).to eq [
69
- {'width' => 96, 'height' => 225 },
70
- {'width' => 191, 'height' => 450 },
71
- {'width' => 381, 'height' => 900 },
72
- {'width' => 762, 'height' => 1800 },
73
- {'width' => 1524, 'height' => 3600 },
74
- {'width' => 3047, 'height' => 7200 }
62
+ expect(resource.service["tiles"]).to eq [{
63
+ "width" => 1024,
64
+ "scaleFactors" => [1, 2, 4, 8, 16, 32]
65
+ }]
66
+ expect(resource.service["sizes"]).to eq [
67
+ {"width" => 96, "height" => 225},
68
+ {"width" => 191, "height" => 450},
69
+ {"width" => 381, "height" => 900},
70
+ {"width" => 762, "height" => 1800},
71
+ {"width" => 1524, "height" => 3600},
72
+ {"width" => 3047, "height" => 7200}
75
73
  ]
76
74
  end
77
75
  end
78
76
 
79
- describe 'respects the params we supply' do
80
- it ':resource_id' do
81
- r_id = 'http://example.edu/images/some.jpg'
77
+ describe "respects the params we supply" do
78
+ it ":resource_id" do
79
+ r_id = "http://example.edu/images/some.jpg"
82
80
 
83
- opts = { service_id: valid_service_id, resource_id: r_id}
81
+ opts = {service_id: valid_service_id, resource_id: r_id}
84
82
  resource = described_class.create_image_api_image_resource(opts)
85
- expect(resource['@id']).to eq r_id
83
+ expect(resource["@id"]).to eq r_id
86
84
  end
87
- it ':width' do
85
+ it ":width" do
88
86
  width = 42
89
- opts = { service_id: valid_service_id, width: width}
87
+ opts = {service_id: valid_service_id, width: width}
90
88
  resource = described_class.create_image_api_image_resource(opts)
91
89
  expect(resource.width).to eq width
92
90
  end
93
- it ':height' do
91
+ it ":height" do
94
92
  height = 42
95
- opts = { service_id: valid_service_id, height: height}
93
+ opts = {service_id: valid_service_id, height: height}
96
94
  resource = described_class.create_image_api_image_resource(opts)
97
95
  expect(resource.height).to eq height
98
96
  end
99
- it ':profile (service[\'profile\'])' do
100
- profile = 'http://iiif.io/api/image/2/level1.json'
101
- opts = { service_id: valid_service_id, profile: profile}
97
+ it ":profile (service['profile'])" do
98
+ profile = "http://iiif.io/api/image/2/level1.json"
99
+ opts = {service_id: valid_service_id, profile: profile}
102
100
  resource = described_class.create_image_api_image_resource(opts)
103
- expect(resource.service['profile']).to eq profile
101
+ expect(resource.service["profile"]).to eq profile
104
102
  end
105
103
  end
106
104
 
107
- describe 'errors' do
108
- it 'raises if :service_id is not included' do
105
+ describe "errors" do
106
+ it "raises if :service_id is not included" do
109
107
  expect {
110
108
  described_class.create_image_api_image_resource
111
109
  }.to raise_error
112
110
  end
113
- it 'raises if the info can\'t be pulled in' do
111
+ it "raises if the info can't be pulled in" do
114
112
  expect {
115
113
  described_class.create_image_api_image_resource(service_id: invalid_service_id)
116
114
  }.to raise_error
117
115
  end
118
116
  end
119
-
120
-
121
117
  end
122
118
  end