Skip to content

Various accessibility metadata changes #635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file. Take a look

### Added

#### Shared

* 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.
* Support for [EPUB Accessibility 1.1](https://www.w3.org/TR/epub-a11y-11/) conformance profiles.

#### Navigator

* 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)).
Expand All @@ -20,6 +25,10 @@ All notable changes to this project will be documented in this file. Take a look

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

#### Shared

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


## [3.0.3]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.readium.r2.shared.InternalReadiumApi
import org.readium.r2.shared.JSONable
import org.readium.r2.shared.extensions.*
import org.readium.r2.shared.publication.Accessibility.AccessMode.Companion.toJSONArray
import org.readium.r2.shared.publication.Accessibility.Exemption.Companion.toJSONArray
import org.readium.r2.shared.publication.Accessibility.Feature.Companion.toJSONArray
import org.readium.r2.shared.publication.Accessibility.Hazard.Companion.toJSONArray
import org.readium.r2.shared.publication.Accessibility.PrimaryAccessMode.Companion.toJSONArray
Expand Down Expand Up @@ -46,16 +47,19 @@ import org.readium.r2.shared.util.logging.log
* supported enhancements for accessibility.
* @property [hazards] A characteristic of the described resource that is physiologically
* dangerous to some users.
* @property [exemptions] Justifications for non-conformance based on exemptions in a given
* jurisdiction.
*/
@Parcelize
public data class Accessibility(
val conformsTo: Set<Profile>,
val conformsTo: Set<Profile> = emptySet(),
val certification: Certification? = null,
val summary: String? = null,
val accessModes: Set<AccessMode>,
val accessModesSufficient: Set<Set<PrimaryAccessMode>>,
val features: Set<Feature>,
val hazards: Set<Hazard>,
val accessModes: Set<AccessMode> = emptySet(),
val accessModesSufficient: Set<Set<PrimaryAccessMode>> = emptySet(),
val features: Set<Feature> = emptySet(),
val hazards: Set<Hazard> = emptySet(),
val exemptions: Set<Exemption> = emptySet(),
) : JSONable, Parcelable {

/**
Expand All @@ -66,18 +70,48 @@ public data class Accessibility(

public companion object {

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

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

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

/** EPUB Accessibility 1.1 - WCAG 2.0 Level A */
public val EPUB_A11Y_11_WCAG_20_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-a")

/** EPUB Accessibility 1.1 - WCAG 2.0 Level AA */
public val EPUB_A11Y_11_WCAG_20_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aa")

/** EPUB Accessibility 1.1 - WCAG 2.0 Level AAA */
public val EPUB_A11Y_11_WCAG_20_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.0-aaa")

/** EPUB Accessibility 1.1 - WCAG 2.1 Level A */
public val EPUB_A11Y_11_WCAG_21_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-a")

/** EPUB Accessibility 1.1 - WCAG 2.1 Level AA */
public val EPUB_A11Y_11_WCAG_21_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aa")

/** EPUB Accessibility 1.1 - WCAG 2.1 Level AAA */
public val EPUB_A11Y_11_WCAG_21_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.1-aaa")

/** EPUB Accessibility 1.1 - WCAG 2.2 Level A */
public val EPUB_A11Y_11_WCAG_22_A: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-a")

/** EPUB Accessibility 1.1 - WCAG 2.2 Level AA */
public val EPUB_A11Y_11_WCAG_22_AA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aa")

/** EPUB Accessibility 1.1 - WCAG 2.2 Level AAA */
public val EPUB_A11Y_11_WCAG_22_AAA: Profile = Profile("https://www.w3.org/TR/epub-a11y-11#wcag-2.2-aaa")

public fun Set<Profile>.toJSONArray(): JSONArray =
JSONArray(this.map(Profile::uri))
}
Expand Down Expand Up @@ -305,10 +339,32 @@ public data class Accessibility(
*/
public val INDEX: Feature = Feature("index")

/**
* The resource includes static page markers, such as those identified by the
* doc-pagebreak role (DPUB-ARIA-1.0).
*
* This value is most commonly used with ebooks for which there is a statically
* paginated equivalent, such as a print edition, but it is not required that the page
* markers correspond to another work. The markers may exist solely to facilitate
* navigation in purely digital works.
*/
public val PAGE_BREAK_MARKERS: Feature = Feature("pageBreakMarkers")

/**
* The resource includes a means of navigating to static page break locations.
*
* The most common way of providing page navigation in digital publications is through
* a page list.
*/
public val PAGE_NAVIGATION: Feature = Feature("pageNavigation")

/**
* The work includes equivalent print page numbers. This setting is most commonly used
* with ebooks for which there is a print equivalent.
*
* Deprecated: https://github.com/readium/go-toolkit/issues/92
*/
@Deprecated("Deprecated in favor of PAGE_NAVIGATION", ReplaceWith("PAGE_NAVIGATION"))
public val PRINT_PAGE_NUMBERS: Feature = Feature("printPageNumbers")

/**
Expand Down Expand Up @@ -553,6 +609,65 @@ public data class Accessibility(
}
}

/**
* [Exemption] allows content creators to identify publications that do not meet conformance
* requirements but fall under exemptions in a given juridiction.
*
* While this list is currently limited to exemptions covered by the European Accessibility Act,
* it will be extended to cover additional exemptions in the future.
*/
@Parcelize
public data class Exemption(public val value: String) : Parcelable {

public companion object {

/**
* Article 14, paragraph 1 of the European Accessibility Act states that its
* accessibility requirements shall apply only to the extent that compliance: … (b) does
* not result in the imposition of a disproportionate burden on the economic operators
* concerned
*
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?
*/
public val EAA_DISPROPORTIONATE_BURDEN: Exemption = Exemption("eaa-disproportionate-burden")

/**
* Article 14, paragraph 1 of the European Accessibility Act states that its
* accessibility requirements shall apply only to the extent that compliance: (a) does
* not require a significant change in a product or service that results in the
* fundamental alteration of its basic nature
*
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e2148-70-1
*/
public val EAA_FUNDAMENTAL_ALTERATION: Exemption = Exemption("eaa-fundamental-alteration")

/**
* The European Accessibility Act defines a microenterprise as: an enterprise which
* employs fewer than 10 persons and which has an annual turnover not exceeding EUR 2
* million or an annual balance sheet total not exceeding EUR 2 million.
*
* It further states in Article 4, paragraph 5: Microenterprises providing services
* shall be exempt from complying with the accessibility requirements referred to in
* paragraph 3 of this Article and any obligations relating to the compliance with those
* requirements.
*
* https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32019L0882#d1e1798-70-1
*/
public val EAA_MICROENTERPRISE: Exemption = Exemption("eaa-microenterprise")

/**
* Creates a list of [Exemption] from its RWPM JSON representation.
*/
public fun fromJSONArray(json: JSONArray?): List<Exemption> =
json?.filterIsInstance(String::class.java)
?.map { Exemption(it) }
.orEmpty()

public fun Set<Exemption>.toJSONArray(): JSONArray =
JSONArray(this.map(Exemption::value))
}
}

override fun toJSON(): JSONObject = JSONObject().apply {
putIfNotEmpty("conformsTo", conformsTo.toJSONArray())
put("certification", certification?.toJSON())
Expand All @@ -561,6 +676,7 @@ public data class Accessibility(
putIfNotEmpty("accessModeSufficient", accessModesSufficient.map { it.toJSONArray() })
putIfNotEmpty("hazard", hazards.toJSONArray())
putIfNotEmpty("feature", features.toJSONArray())
putIfNotEmpty("exemption", exemptions.toJSONArray())
}

public companion object {
Expand Down Expand Up @@ -592,6 +708,7 @@ public data class Accessibility(

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

return Accessibility(
conformsTo = conformsTo.toSet(),
Expand All @@ -600,7 +717,8 @@ public data class Accessibility(
accessModes = accessModes.toSet(),
accessModesSufficient = accessModesSufficient.toSet(),
features = features.toSet(),
hazards = hazards.toSet()
hazards = hazards.toSet(),
exemptions = exemptions.toSet()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class AccessibilityTest {
hazards = setOf(
Accessibility.Hazard.FLASHING,
Accessibility.Hazard.MOTION_SIMULATION
),
exemptions = setOf(
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
Accessibility.Exemption.EAA_MICROENTERPRISE
)
),
Accessibility.fromJSON(
Expand All @@ -122,7 +126,8 @@ class AccessibilityTest {
"accessMode": ["auditory", "chartOnVisual"],
"accessModeSufficient": [["visual", "tactile"]],
"feature": ["readingOrder", "alternativeText"],
"hazard": ["flashing", "motionSimulation"]
"hazard": ["flashing", "motionSimulation"],
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"]
}"""
)
)
Expand Down Expand Up @@ -270,7 +275,8 @@ class AccessibilityTest {
Accessibility.Hazard.FLASHING,
Accessibility.Hazard.NO_SOUND_HAZARD,
Accessibility.Hazard.MOTION_SIMULATION
)
),
exemptions = emptySet()
),
Accessibility.fromJSON(
JSONObject(
Expand All @@ -282,6 +288,33 @@ class AccessibilityTest {
)
}

@Test
fun `exemptions are correctly parsed`() {
assertEquals(
Accessibility(
conformsTo = setOf(),
certification = null,
summary = null,
accessModes = emptySet(),
accessModesSufficient = emptySet(),
features = emptySet(),
hazards = emptySet(),
exemptions = setOf(
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
Accessibility.Exemption.EAA_FUNDAMENTAL_ALTERATION,
Accessibility.Exemption.EAA_MICROENTERPRISE,
)
),
Accessibility.fromJSON(
JSONObject(
"""{
"exemption": ["eaa-disproportionate-burden", "eaa-fundamental-alteration", "eaa-microenterprise"]
}"""
)
)
)
}

@Test
fun `get full JSON`() {
assertJSONEquals(
Expand All @@ -297,7 +330,8 @@ class AccessibilityTest {
"accessMode": ["auditory", "chartOnVisual"],
"accessModeSufficient": [["auditory"], ["visual", "tactile"], ["visual"]],
"feature": ["readingOrder", "alternativeText"],
"hazard": ["flashing", "motionSimulation"]
"hazard": ["flashing", "motionSimulation"],
"exemption": ["eaa-disproportionate-burden", "eaa-microenterprise"]
}"""
),
Accessibility(
Expand Down Expand Up @@ -330,6 +364,10 @@ class AccessibilityTest {
hazards = setOf(
Accessibility.Hazard.FLASHING,
Accessibility.Hazard.MOTION_SIMULATION
),
exemptions = setOf(
Accessibility.Exemption.EAA_DISPROPORTIONATE_BURDEN,
Accessibility.Exemption.EAA_MICROENTERPRISE
)
).toJSON()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ internal class AccessibilityAdapter {
.map { Accessibility.Hazard(it.value) }
.toSet()

val exemptions = itemsHolder
.adapt { it.takeAllWithProperty(Vocabularies.A11Y + "exemption") }
.map { Accessibility.Exemption(it.value) }
.toSet()

val certification = itemsHolder
.adapt(::adaptCertification)

Expand All @@ -52,7 +57,8 @@ internal class AccessibilityAdapter {
accessModes = accessModes,
accessModesSufficient = accessModesSufficient,
features = features,
hazards = hazards
hazards = hazards,
exemptions = exemptions
)
accessibility to itemsHolder.remainingItems
}
Expand Down Expand Up @@ -151,27 +157,33 @@ internal class AccessibilityAdapter {
isWCAG_20_A(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_A
isWCAG_20_AA(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_AA
isWCAG_20_AAA(value) -> Accessibility.Profile.EPUB_A11Y_10_WCAG_20_AAA
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_A
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_AA
value == "EPUB Accessibility 1.1 - WCAG 2.0 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_20_AAA
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_A
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_AA
value == "EPUB Accessibility 1.1 - WCAG 2.1 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_21_AAA
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level A" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_A
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level AA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_AA
value == "EPUB Accessibility 1.1 - WCAG 2.2 Level AAA" -> Accessibility.Profile.EPUB_A11Y_11_WCAG_22_AAA
else -> null
}

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

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

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