Skip to content

Commit 33dfb92

Browse files
authored
Various accessibility metadata changes (#565)
1 parent fc92cb7 commit 33dfb92

File tree

8 files changed

+177
-19
lines changed

8 files changed

+177
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. Take a look
1111
#### Shared
1212

1313
* Support for [W3C's Text & data mining Reservation Protocol](https://www.w3.org/community/reports/tdmrep/CG-FINAL-tdmrep-20240510/) in our metadata models.
14+
* Support for [accessibility exemption metadata](https://readium.org/webpub-manifest/contexts/default/#exemption), which allows content creators to identify publications that do not meet conformance requirements but fall under exemptions in a given juridiction.
15+
* Support for [EPUB Accessibility 1.1](https://www.w3.org/TR/epub-a11y-11/) conformance profiles.
1416

1517
#### LCP
1618

@@ -22,6 +24,7 @@ All notable changes to this project will be documented in this file. Take a look
2224

2325
* The `absoluteURL` and `relativeURL` extensions on `URLConvertible` were removed as they conflict with the native `URL.absoluteURL`.
2426
* If you were using them, you can for example still use `anyURL.absoluteURL` instead.
27+
* [go-toolkit#92](https://github.com/readium/go-toolkit/issues/92) The accessibility feature `printPageNumbers` is deprecated in favor of `pageNavigation`.
2528

2629
#### Streamer
2730

Sources/Shared/Publication/Accessibility.swift

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public struct Accessibility: Hashable, Sendable {
4848
/// https://www.w3.org/2021/a11y-discov-vocab/latest/#accessibilityHazard
4949
public var hazards: [Hazard]
5050

51+
/// Justifications for non-conformance based on exemptions in a given
52+
/// jurisdiction.
53+
public var exemptions: [Exemption]
54+
5155
/// Accessibility profile.
5256
public struct Profile: Hashable, Sendable {
5357
public let uri: String
@@ -62,6 +66,24 @@ public struct Accessibility: Hashable, Sendable {
6266
public static let epubA11y10WCAG20AA = Profile("http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa")
6367
/// EPUB Accessibility 1.0 - WCAG 2.0 Level AAA
6468
public static let epubA11y10WCAG20AAA = Profile("http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa")
69+
/// EPUB Accessibility 1.1 - WCAG 2.0 Level A
70+
public static let epubA11y11WCAG20A = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a")
71+
/// EPUB Accessibility 1.1 - WCAG 2.0 Level AA
72+
public static let epubA11y11WCAG20AA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aa")
73+
/// EPUB Accessibility 1.1 - WCAG 2.0 Level AAA
74+
public static let epubA11y11WCAG20AAA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aaa")
75+
/// EPUB Accessibility 1.1 - WCAG 2.1 Level A
76+
public static let epubA11y11WCAG21A = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-a")
77+
/// EPUB Accessibility 1.1 - WCAG 2.1 Level AA
78+
public static let epubA11y11WCAG21AA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aa")
79+
/// EPUB Accessibility 1.1 - WCAG 2.1 Level AAA
80+
public static let epubA11y11WCAG21AAA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aaa")
81+
/// EPUB Accessibility 1.1 - WCAG 2.2 Level A
82+
public static let epubA11y11WCAG22A = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-a")
83+
/// EPUB Accessibility 1.1 - WCAG 2.2 Level AA
84+
public static let epubA11y11WCAG22AA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aa")
85+
/// EPUB Accessibility 1.1 - WCAG 2.2 Level AAA
86+
public static let epubA11y11WCAG22AAA = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aaa")
6587
}
6688

6789
public struct Certification: Hashable, Sendable {
@@ -177,8 +199,25 @@ public struct Accessibility: Hashable, Sendable {
177199
/// The work includes an index to the content.
178200
public static let index = Feature("index")
179201

180-
/// The work includes equivalent print page numbers. This setting is most commonly used with ebooks for which
181-
/// there is a print equivalent.
202+
/// The resource includes static page markers, such as those identified
203+
/// by the doc-pagebreak role (DPUB-ARIA-1.0).
204+
///
205+
/// This value is most commonly used with ebooks for which there is a
206+
/// statically paginated equivalent, such as a print edition, but it is
207+
/// not required that the page markers correspond to another work. The
208+
/// markers may exist solely to facilitate navigation in purely digital
209+
/// works.
210+
public static let pageBreakMarkers = Feature("pageBreakMarkers")
211+
212+
/// The resource includes a means of navigating to static page break
213+
/// locations.
214+
///
215+
/// The most common way of providing page navigation in digital
216+
/// publications is through a page list.
217+
public static let pageNavigation = Feature("pageNavigation")
218+
219+
// https://github.com/readium/go-toolkit/issues/92
220+
@available(*, deprecated, renamed: "pageNavigation")
182221
public static let printPageNumbers = Feature("printPageNumbers")
183222

184223
/// The reading order of the content is clearly defined in the markup (e.g., figures, sidebars and other
@@ -341,14 +380,58 @@ public struct Accessibility: Hashable, Sendable {
341380
public static let none = Hazard("none")
342381
}
343382

383+
/// ``Exemption`` allows content creators to identify publications that do
384+
/// not meet conformance requirements but fall under exemptions in a given
385+
/// juridiction.
386+
///
387+
/// While this list is currently limited to exemptions covered by the
388+
/// European Accessibility Act, it will be extended to cover additional
389+
/// exemptions in the future.
390+
public struct Exemption: Hashable, Sendable {
391+
public let id: String
392+
393+
public init(_ id: String) {
394+
self.id = id
395+
}
396+
397+
/// Article 14, paragraph 1 of the European Accessibility Act states
398+
/// that its accessibility requirements shall apply only to the extent
399+
/// that compliance: … (b) does not result in the imposition of a
400+
/// disproportionate burden on the economic operators concerned
401+
/// https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e2148-70-1
402+
public static let eaaDisproportionateBurden = Exemption("eaa-disproportionate-burden")
403+
404+
/// Article 14, paragraph 1 of the European Accessibility Act states
405+
/// that its accessibility requirements shall apply only to the extent
406+
/// that compliance: (a) does not require a significant change in a
407+
/// product or service that results in the fundamental alteration of its
408+
/// basic nature
409+
/// https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e2148-70-1
410+
public static let eaaFundamentalAlteration = Exemption("eaa-fundamental-alteration")
411+
412+
/// The European Accessibility Act defines a microenterprise as: an
413+
/// enterprise which employs fewer than 10 persons and which has an
414+
/// annual turnover not exceeding EUR 2 million or an annual balance
415+
/// sheet total not exceeding EUR 2 million.
416+
///
417+
/// It further states in Article 4, paragraph 5: Microenterprises
418+
/// providing services shall be exempt from complying with the
419+
/// accessibility requirements referred to in paragraph 3 of this
420+
/// Article and any obligations relating to the compliance with those
421+
/// requirements.
422+
/// https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e1798-70-1
423+
public static let eaaMicroenterprise = Exemption("eaa-microenterprise")
424+
}
425+
344426
public init(
345427
conformsTo: [Profile] = [],
346428
certification: Certification? = nil,
347429
summary: String? = nil,
348430
accessModes: [AccessMode] = [],
349431
accessModesSufficient: [[PrimaryAccessMode]] = [],
350432
features: [Feature] = [],
351-
hazards: [Hazard] = []
433+
hazards: [Hazard] = [],
434+
exemptions: [Exemption] = []
352435
) {
353436
self.conformsTo = conformsTo
354437
self.certification = certification
@@ -357,6 +440,7 @@ public struct Accessibility: Hashable, Sendable {
357440
self.accessModesSufficient = accessModesSufficient
358441
self.features = features
359442
self.hazards = hazards
443+
self.exemptions = exemptions
360444
}
361445

362446
public init?(json: Any?, warnings: WarningLogger? = nil) throws {
@@ -394,7 +478,8 @@ public struct Accessibility: Hashable, Sendable {
394478
}
395479
.filter { !$0.isEmpty },
396480
features: parseArray(jsonObject["feature"]).map(Feature.init),
397-
hazards: parseArray(jsonObject["hazard"]).map(Hazard.init)
481+
hazards: parseArray(jsonObject["hazard"]).map(Hazard.init),
482+
exemptions: parseArray(jsonObject["exemption"]).map(Exemption.init)
398483
)
399484
}
400485

@@ -413,6 +498,7 @@ public struct Accessibility: Hashable, Sendable {
413498
"accessModeSufficient": encodeIfNotEmpty(accessModesSufficient.map { $0.map(\.rawValue) }),
414499
"feature": encodeIfNotEmpty(features.map(\.id)),
415500
"hazard": encodeIfNotEmpty(hazards.map(\.id)),
501+
"exemption": encodeIfNotEmpty(exemptions.map(\.id)),
416502
])
417503
}
418504
}

Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ final class EPUBMetadataParser: Loggable {
190190
accessModes: accessibilityAccessModes(),
191191
accessModesSufficient: accessibilityAccessModesSufficient(),
192192
features: accessibilityFeatures(),
193-
hazards: accessibilityHazards()
193+
hazards: accessibilityHazards(),
194+
exemptions: accessibilityExemptions()
194195
)
195196

196197
return accessibility.takeIf { $0 != Accessibility() }
@@ -204,26 +205,51 @@ final class EPUBMetadataParser: Loggable {
204205

205206
private func accessibilityProfile(from value: String) -> Accessibility.Profile? {
206207
switch value {
207-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level A",
208-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
208+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
209209
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
210210
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
211211
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a":
212212
return .epubA11y10WCAG20A
213213

214-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AA",
215-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
214+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
216215
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
217216
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
218217
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa":
219218
return .epubA11y10WCAG20AA
220219

221-
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA",
222-
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
220+
case "http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
223221
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
224222
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
225223
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa":
226224
return .epubA11y10WCAG20AAA
225+
226+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level A":
227+
return .epubA11y11WCAG20A
228+
229+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AA":
230+
return .epubA11y11WCAG20AA
231+
232+
case "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA":
233+
return .epubA11y11WCAG20AAA
234+
235+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level A":
236+
return .epubA11y11WCAG21A
237+
238+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level AA":
239+
return .epubA11y11WCAG21AA
240+
241+
case "EPUB Accessibility 1.1 - WCAG 2.1 Level AAA":
242+
return .epubA11y11WCAG21AAA
243+
244+
case "EPUB Accessibility 1.1 - WCAG 2.2 Level A":
245+
return .epubA11y11WCAG22A
246+
247+
case "EPUB Accessibility 1.1 - WCAG 2.2 Level AA":
248+
return .epubA11y11WCAG22AA
249+
250+
case "EPUB Accessibility 1.1 - WCAG 2.2 Level AAA":
251+
return .epubA11y11WCAG22AAA
252+
227253
default:
228254
return nil
229255
}
@@ -277,6 +303,11 @@ final class EPUBMetadataParser: Loggable {
277303
.map { Accessibility.Hazard($0.content) }
278304
}
279305

306+
private func accessibilityExemptions() -> [Accessibility.Exemption] {
307+
metas["exemption", in: .a11y]
308+
.map { Accessibility.Exemption($0.content) }
309+
}
310+
280311
/// https://www.w3.org/community/reports/tdmrep/CG-FINAL-tdmrep-20240510/#sec-epub3
281312
private func tdm() -> TDM? {
282313
guard

Sources/Streamer/Parser/EPUB/OPFMeta.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ struct OPFMetaList {
303303
/// List of properties that should not be added to `otherMetadata` because they are already
304304
/// consumed by the RWPM model.
305305
private let rwpmProperties: [OPFVocabulary: [String]] = [
306-
.a11y: ["certifiedBy", "certifierCredential", "certifierReport"],
306+
.a11y: ["certifiedBy", "certifierCredential", "certifierReport", "exemption"],
307307
.defaultMetadata: ["cover"],
308308
.dcterms: [
309309
"contributor", "creator", "date", "description", "identifier",

Tests/SharedTests/Publication/AccessibilityTests.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class AccessibilityTests: XCTestCase {
3333
"accessModeSufficient": [["visual", "tactile"]],
3434
"feature": ["readingOrder", "alternativeText"],
3535
"hazard": ["flashing", "motionSimulation"],
36+
"exemption": ["eaa-fundamental-alteration", "eaa-microenterprise"],
3637
] as [String: Any]),
3738
Accessibility(
3839
conformsTo: [
@@ -48,7 +49,8 @@ class AccessibilityTests: XCTestCase {
4849
accessModes: [.auditory, .chartOnVisual],
4950
accessModesSufficient: [[.visual, .tactile]],
5051
features: [.readingOrder, .alternativeText],
51-
hazards: [.flashing, .motionSimulation]
52+
hazards: [.flashing, .motionSimulation],
53+
exemptions: [.eaaFundamentalAlteration, .eaaMicroenterprise]
5254
)
5355
)
5456
}
@@ -164,6 +166,25 @@ class AccessibilityTests: XCTestCase {
164166
)
165167
}
166168

169+
func testParseExemptions() {
170+
XCTAssertEqual(
171+
try? Accessibility(json: [
172+
"exemption": ["eaa-microenterprise"],
173+
]),
174+
Accessibility(
175+
exemptions: [.eaaMicroenterprise]
176+
)
177+
)
178+
XCTAssertEqual(
179+
try? Accessibility(json: [
180+
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"],
181+
]),
182+
Accessibility(
183+
exemptions: [.eaaDisproportionateBurden, .eaaMicroenterprise]
184+
)
185+
)
186+
}
187+
167188
func testGetMinimalJSON() {
168189
AssertJSONEqual(
169190
Accessibility().json,
@@ -186,7 +207,8 @@ class AccessibilityTests: XCTestCase {
186207
accessModes: [.auditory, .chartOnVisual],
187208
accessModesSufficient: [[.auditory], [.visual, .tactile], [.visual]],
188209
features: [.readingOrder, .alternativeText],
189-
hazards: [.flashing, .motionSimulation]
210+
hazards: [.flashing, .motionSimulation],
211+
exemptions: [.eaaDisproportionateBurden, .eaaMicroenterprise]
190212
).json
191213
AssertJSONEqual(
192214
expected,
@@ -202,6 +224,7 @@ class AccessibilityTests: XCTestCase {
202224
"accessModeSufficient": [["auditory"], ["visual", "tactile"], ["visual"]],
203225
"feature": ["readingOrder", "alternativeText"],
204226
"hazard": ["flashing", "motionSimulation"],
227+
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"],
205228
] as [String: Any]
206229
)
207230
}

Tests/StreamerTests/Fixtures/OPF/accessibility-epub2.opf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
<dc:conformsTo>any profile</dc:conformsTo>
77
<dc:conformsTo>http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a</dc:conformsTo>
8+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.0 Level AAA</dc:conformsTo>
9+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.1 Level AA</dc:conformsTo>
810
<meta name="schema:accessibilitySummary" content="The publication contains structural and page navigation."/>
911

1012
<meta name="schema:accessMode" content="textual"/>
@@ -20,6 +22,10 @@
2022
<meta name="a11y:certifiedBy" content="Accessibility Testers Group"/>
2123
<meta name="a11y:certifierCredential" content="DAISY OK"/>
2224
<meta name="a11y:certifierReport" content="https://example.com/a11y-report/"/>
25+
26+
<meta name="a11y:exemption" content="eaa-microenterprise"/>
27+
<meta name="a11y:exemption" content="eaa-fundamental-alteration"/>
28+
<meta name="a11y:exemption" content="eaa-disproportionate-burden"/>
2329
</metadata>
2430
<manifest>
2531
<item id="titlepage" href="titlepage.xhtml"/>

Tests/StreamerTests/Fixtures/OPF/accessibility-epub3.opf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
<dc:conformsTo>any profile</dc:conformsTo>
77
<dc:conformsTo>http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a</dc:conformsTo>
8+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.0 Level AAA</dc:conformsTo>
9+
<dc:conformsTo>EPUB Accessibility 1.1 - WCAG 2.1 Level AA</dc:conformsTo>
810
<meta property="schema:accessibilitySummary">The publication contains structural and page navigation.</meta>
911

1012
<meta property="schema:accessMode">textual</meta>
@@ -23,7 +25,10 @@
2325
<meta property="a11y:certifierCredential" refines="#certifier">DAISY OK</meta>
2426
<link rel="a11y:certifierReport" refines="#certifier2" href="https://example.com/fakereport"/>
2527
<link rel="a11y:certifierReport" refines="#certifier" href="https://example.com/a11y-report/"/>
26-
28+
29+
<meta property="a11y:exemption">eaa-microenterprise</meta>
30+
<meta property="a11y:exemption">eaa-fundamental-alteration</meta>
31+
<meta property="a11y:exemption">eaa-disproportionate-burden</meta>
2732
</metadata>
2833
<manifest>
2934
<item id="titlepage" href="titlepage.xhtml"/>

0 commit comments

Comments
 (0)