Skip to content

Commit 0d2b5dc

Browse files
authored
Various accessibility metadata changes (#635)
1 parent 00ca5cd commit 0d2b5dc

File tree

7 files changed

+222
-15
lines changed

7 files changed

+222
-15
lines changed

CHANGELOG.md

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

99
### Added
1010

11+
#### Shared
12+
13+
* 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.
14+
* Support for [EPUB Accessibility 1.1](https://www.w3.org/TR/epub-a11y-11/) conformance profiles.
15+
1116
#### Navigator
1217

1318
* The `EpubNavigatorFragment.Configuration.disablePageTurnsWhileScrolling` property disables horizontal swipes for navigating to previous or next resources when scroll mode is enabled. When set to `true`, you must implement your own mechanism to move to the next resource (contributed by [@tm-bookshop](https://github.com/readium/kotlin-toolkit/pull/624)).
@@ -20,6 +25,10 @@ All notable changes to this project will be documented in this file. Take a look
2025

2126
* Jetifier is not required anymore, you can remove `android.enableJetifier=true` from your `gradle.properties` if you were using Readium as a local clone.
2227

28+
#### Shared
29+
30+
* [go-toolkit#92](https://github.com/readium/go-toolkit/issues/92) The accessibility feature `printPageNumbers` is deprecated in favor of `pageNavigation`.
31+
2332

2433
## [3.0.3]
2534

readium/shared/src/main/java/org/readium/r2/shared/publication/Accessibility.kt

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.readium.r2.shared.InternalReadiumApi
1818
import org.readium.r2.shared.JSONable
1919
import org.readium.r2.shared.extensions.*
2020
import org.readium.r2.shared.publication.Accessibility.AccessMode.Companion.toJSONArray
21+
import org.readium.r2.shared.publication.Accessibility.Exemption.Companion.toJSONArray
2122
import org.readium.r2.shared.publication.Accessibility.Feature.Companion.toJSONArray
2223
import org.readium.r2.shared.publication.Accessibility.Hazard.Companion.toJSONArray
2324
import org.readium.r2.shared.publication.Accessibility.PrimaryAccessMode.Companion.toJSONArray
@@ -46,16 +47,19 @@ import org.readium.r2.shared.util.logging.log
4647
* supported enhancements for accessibility.
4748
* @property [hazards] A characteristic of the described resource that is physiologically
4849
* dangerous to some users.
50+
* @property [exemptions] Justifications for non-conformance based on exemptions in a given
51+
* jurisdiction.
4952
*/
5053
@Parcelize
5154
public data class Accessibility(
52-
val conformsTo: Set<Profile>,
55+
val conformsTo: Set<Profile> = emptySet(),
5356
val certification: Certification? = null,
5457
val summary: String? = null,
55-
val accessModes: Set<AccessMode>,
56-
val accessModesSufficient: Set<Set<PrimaryAccessMode>>,
57-
val features: Set<Feature>,
58-
val hazards: Set<Hazard>,
58+
val accessModes: Set<AccessMode> = emptySet(),
59+
val accessModesSufficient: Set<Set<PrimaryAccessMode>> = emptySet(),
60+
val features: Set<Feature> = emptySet(),
61+
val hazards: Set<Hazard> = emptySet(),
62+
val exemptions: Set<Exemption> = emptySet(),
5963
) : JSONable, Parcelable {
6064

6165
/**
@@ -66,18 +70,48 @@ public data class Accessibility(
6670

6771
public companion object {
6872

73+
/** EPUB Accessibility 1.0 - WCAG 2.0 Level A */
6974
public val EPUB_A11Y_10_WCAG_20_A: Profile = Profile(
7075
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a"
7176
)
7277

78+
/** EPUB Accessibility 1.0 - WCAG 2.0 Level AA */
7379
public val EPUB_A11Y_10_WCAG_20_AA: Profile = Profile(
7480
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"
7581
)
7682

83+
/** EPUB Accessibility 1.0 - WCAG 2.0 Level AAA */
7784
public val EPUB_A11Y_10_WCAG_20_AAA: Profile = Profile(
7885
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa"
7986
)
8087

88+
/** EPUB Accessibility 1.1 - WCAG 2.0 Level A */
89+
public val EPUB_A11Y_11_WCAG_20_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a")
90+
91+
/** EPUB Accessibility 1.1 - WCAG 2.0 Level AA */
92+
public val EPUB_A11Y_11_WCAG_20_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aa")
93+
94+
/** EPUB Accessibility 1.1 - WCAG 2.0 Level AAA */
95+
public val EPUB_A11Y_11_WCAG_20_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aaa")
96+
97+
/** EPUB Accessibility 1.1 - WCAG 2.1 Level A */
98+
public val EPUB_A11Y_11_WCAG_21_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-a")
99+
100+
/** EPUB Accessibility 1.1 - WCAG 2.1 Level AA */
101+
public val EPUB_A11Y_11_WCAG_21_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aa")
102+
103+
/** EPUB Accessibility 1.1 - WCAG 2.1 Level AAA */
104+
public val EPUB_A11Y_11_WCAG_21_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aaa")
105+
106+
/** EPUB Accessibility 1.1 - WCAG 2.2 Level A */
107+
public val EPUB_A11Y_11_WCAG_22_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-a")
108+
109+
/** EPUB Accessibility 1.1 - WCAG 2.2 Level AA */
110+
public val EPUB_A11Y_11_WCAG_22_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aa")
111+
112+
/** EPUB Accessibility 1.1 - WCAG 2.2 Level AAA */
113+
public val EPUB_A11Y_11_WCAG_22_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aaa")
114+
81115
public fun Set<Profile>.toJSONArray(): JSONArray =
82116
JSONArray(this.map(Profile::uri))
83117
}
@@ -305,10 +339,32 @@ public data class Accessibility(
305339
*/
306340
public val INDEX: Feature = Feature("index")
307341

342+
/**
343+
* The resource includes static page markers, such as those identified by the
344+
* doc-pagebreak role (DPUB-ARIA-1.0).
345+
*
346+
* This value is most commonly used with ebooks for which there is a statically
347+
* paginated equivalent, such as a print edition, but it is not required that the page
348+
* markers correspond to another work. The markers may exist solely to facilitate
349+
* navigation in purely digital works.
350+
*/
351+
public val PAGE_BREAK_MARKERS: Feature = Feature("pageBreakMarkers")
352+
353+
/**
354+
* The resource includes a means of navigating to static page break locations.
355+
*
356+
* The most common way of providing page navigation in digital publications is through
357+
* a page list.
358+
*/
359+
public val PAGE_NAVIGATION: Feature = Feature("pageNavigation")
360+
308361
/**
309362
* The work includes equivalent print page numbers. This setting is most commonly used
310363
* with ebooks for which there is a print equivalent.
364+
*
365+
* Deprecated: https://github.com/readium/go-toolkit/issues/92
311366
*/
367+
@Deprecated("Deprecated in favor of PAGE_NAVIGATION", ReplaceWith("PAGE_NAVIGATION"))
312368
public val PRINT_PAGE_NUMBERS: Feature = Feature("printPageNumbers")
313369

314370
/**
@@ -553,6 +609,65 @@ public data class Accessibility(
553609
}
554610
}
555611

612+
/**
613+
* [Exemption] allows content creators to identify publications that do not meet conformance
614+
* requirements but fall under exemptions in a given juridiction.
615+
*
616+
* While this list is currently limited to exemptions covered by the European Accessibility Act,
617+
* it will be extended to cover additional exemptions in the future.
618+
*/
619+
@Parcelize
620+
public data class Exemption(public val value: String) : Parcelable {
621+
622+
public companion object {
623+
624+
/**
625+
* Article 14, paragraph 1 of the European Accessibility Act states that its
626+
* accessibility requirements shall apply only to the extent that compliance: … (b) does
627+
* not result in the imposition of a disproportionate burden on the economic operators
628+
* concerned
629+
*
630+
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?
631+
*/
632+
public val EAA_DISPROPORTIONATE_BURDEN: Exemption = Exemption("eaa-disproportionate-burden")
633+
634+
/**
635+
* Article 14, paragraph 1 of the European Accessibility Act states that its
636+
* accessibility requirements shall apply only to the extent that compliance: (a) does
637+
* not require a significant change in a product or service that results in the
638+
* fundamental alteration of its basic nature
639+
*
640+
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e2148-70-1
641+
*/
642+
public val EAA_FUNDAMENTAL_ALTERATION: Exemption = Exemption("eaa-fundamental-alteration")
643+
644+
/**
645+
* The European Accessibility Act defines a microenterprise as: an enterprise which
646+
* employs fewer than 10 persons and which has an annual turnover not exceeding EUR 2
647+
* million or an annual balance sheet total not exceeding EUR 2 million.
648+
*
649+
* It further states in Article 4, paragraph 5: Microenterprises providing services
650+
* shall be exempt from complying with the accessibility requirements referred to in
651+
* paragraph 3 of this Article and any obligations relating to the compliance with those
652+
* requirements.
653+
*
654+
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e1798-70-1
655+
*/
656+
public val EAA_MICROENTERPRISE: Exemption = Exemption("eaa-microenterprise")
657+
658+
/**
659+
* Creates a list of [Exemption] from its RWPM JSON representation.
660+
*/
661+
public fun fromJSONArray(json: JSONArray?): List<Exemption> =
662+
json?.filterIsInstance(String::class.java)
663+
?.map { Exemption(it) }
664+
.orEmpty()
665+
666+
public fun Set<Exemption>.toJSONArray(): JSONArray =
667+
JSONArray(this.map(Exemption::value))
668+
}
669+
}
670+
556671
override fun toJSON(): JSONObject = JSONObject().apply {
557672
putIfNotEmpty("conformsTo", conformsTo.toJSONArray())
558673
put("certification", certification?.toJSON())
@@ -561,6 +676,7 @@ public data class Accessibility(
561676
putIfNotEmpty("accessModeSufficient", accessModesSufficient.map { it.toJSONArray() })
562677
putIfNotEmpty("hazard", hazards.toJSONArray())
563678
putIfNotEmpty("feature", features.toJSONArray())
679+
putIfNotEmpty("exemption", exemptions.toJSONArray())
564680
}
565681

566682
public companion object {
@@ -592,6 +708,7 @@ public data class Accessibility(
592708

593709
val features = Feature.fromJSONArray(json.remove("feature") as? JSONArray)
594710
val hazards = Hazard.fromJSONArray(json.remove("hazard") as? JSONArray)
711+
val exemptions = Exemption.fromJSONArray(json.remove("exemption") as? JSONArray)
595712

596713
return Accessibility(
597714
conformsTo = conformsTo.toSet(),
@@ -600,7 +717,8 @@ public data class Accessibility(
600717
accessModes = accessModes.toSet(),
601718
accessModesSufficient = accessModesSufficient.toSet(),
602719
features = features.toSet(),
603-
hazards = hazards.toSet()
720+
hazards = hazards.toSet(),
721+
exemptions = exemptions.toSet()
604722
)
605723
}
606724
}

readium/shared/src/test/java/org/readium/r2/shared/publication/AccessibilityTest.kt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class AccessibilityTest {
107107
hazards = setOf(
108108
Accessibility.Hazard.FLASHING,
109109
Accessibility.Hazard.MOTION_SIMULATION
110+
),
111+
exemptions = setOf(
112+
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
113+
Accessibility.Exemption.EAA_MICROENTERPRISE
110114
)
111115
),
112116
Accessibility.fromJSON(
@@ -122,7 +126,8 @@ class AccessibilityTest {
122126
"accessMode": ["auditory", "chartOnVisual"],
123127
"accessModeSufficient": [["visual", "tactile"]],
124128
"feature": ["readingOrder", "alternativeText"],
125-
"hazard": ["flashing", "motionSimulation"]
129+
"hazard": ["flashing", "motionSimulation"],
130+
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"]
126131
}"""
127132
)
128133
)
@@ -270,7 +275,8 @@ class AccessibilityTest {
270275
Accessibility.Hazard.FLASHING,
271276
Accessibility.Hazard.NO_SOUND_HAZARD,
272277
Accessibility.Hazard.MOTION_SIMULATION
273-
)
278+
),
279+
exemptions = emptySet()
274280
),
275281
Accessibility.fromJSON(
276282
JSONObject(
@@ -282,6 +288,33 @@ class AccessibilityTest {
282288
)
283289
}
284290

291+
@Test
292+
fun `exemptions are correctly parsed`() {
293+
assertEquals(
294+
Accessibility(
295+
conformsTo = setOf(),
296+
certification = null,
297+
summary = null,
298+
accessModes = emptySet(),
299+
accessModesSufficient = emptySet(),
300+
features = emptySet(),
301+
hazards = emptySet(),
302+
exemptions = setOf(
303+
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
304+
Accessibility.Exemption.EAA_FUNDAMENTAL_ALTERATION,
305+
Accessibility.Exemption.EAA_MICROENTERPRISE,
306+
)
307+
),
308+
Accessibility.fromJSON(
309+
JSONObject(
310+
"""{
311+
"exemption": ["eaa-disproportionate-burden", "eaa-fundamental-alteration", "eaa-microenterprise"]
312+
}"""
313+
)
314+
)
315+
)
316+
}
317+
285318
@Test
286319
fun `get full JSON`() {
287320
assertJSONEquals(
@@ -297,7 +330,8 @@ class AccessibilityTest {
297330
"accessMode": ["auditory", "chartOnVisual"],
298331
"accessModeSufficient": [["auditory"], ["visual", "tactile"], ["visual"]],
299332
"feature": ["readingOrder", "alternativeText"],
300-
"hazard": ["flashing", "motionSimulation"]
333+
"hazard": ["flashing", "motionSimulation"],
334+
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"]
301335
}"""
302336
),
303337
Accessibility(
@@ -330,6 +364,10 @@ class AccessibilityTest {
330364
hazards = setOf(
331365
Accessibility.Hazard.FLASHING,
332366
Accessibility.Hazard.MOTION_SIMULATION
367+
),
368+
exemptions = setOf(
369+
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
370+
Accessibility.Exemption.EAA_MICROENTERPRISE
333371
)
334372
).toJSON()
335373
)

readium/streamer/src/main/java/org/readium/r2/streamer/parser/epub/AccessibilityAdapter.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ internal class AccessibilityAdapter {
3939
.map { Accessibility.Hazard(it.value) }
4040
.toSet()
4141

42+
val exemptions = itemsHolder
43+
.adapt { it.takeAllWithProperty(Vocabularies.A11Y + "exemption") }
44+
.map { Accessibility.Exemption(it.value) }
45+
.toSet()
46+
4247
val certification = itemsHolder
4348
.adapt(::adaptCertification)
4449

@@ -52,7 +57,8 @@ internal class AccessibilityAdapter {
5257
accessModes = accessModes,
5358
accessModesSufficient = accessModesSufficient,
5459
features = features,
55-
hazards = hazards
60+
hazards = hazards,
61+
exemptions = exemptions
5662
)
5763
accessibility to itemsHolder.remainingItems
5864
}
@@ -151,27 +157,33 @@ internal class AccessibilityAdapter {
151157
isWCAG_20_A(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A
152158
isWCAG_20_AA(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_AA
153159
isWCAG_20_AAA(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_AAA
160+
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_A
161+
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_AA
162+
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_AAA
163+
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_A
164+
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_AA
165+
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_AAA
166+
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_A
167+
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_AA
168+
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_AAA
154169
else -> null
155170
}
156171

157172
private fun isWCAG_20_A(value: String) = value in setOf(
158-
"EPUB Accessibility 1.1 - WCAG 2.0 Level A",
159173
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
160174
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
161175
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-a",
162176
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a"
163177
)
164178

165179
private fun isWCAG_20_AA(value: String) = value in setOf(
166-
"EPUB Accessibility 1.1 - WCAG 2.0 Level AA",
167180
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
168181
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
169182
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa",
170183
"https://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"
171184
)
172185

173186
private fun isWCAG_20_AAA(value: String) = value in setOf(
174-
"EPUB Accessibility 1.1 - WCAG 2.0 Level AAA",
175187
"http://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
176188
"http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",
177189
"https://idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa",

0 commit comments

Comments
 (0)