Extracting the content of a publication¶
:warning: The described feature is still experimental and the implementation incomplete.
Many high-level features require access to the raw content (text, media, etc.) of a publication, such as:
- Text-to-speech
- Accessibility reader
- Basic search
- Full-text search indexing
- Image or audio indexes
The ContentService
provides a way to iterate through a publication's content, extracted as semantic elements.
First, request the publication's Content
, starting from a given Locator
. If the locator is missing, the Content
will be extracted from the beginning of the publication.
val content = publication.content(startLocator)
if (content == null) {
// Abort as the content cannot be extracted
}
Extracting the raw text content¶
Getting the whole raw text of a publication is such a common use case that a helper is available on Content
:
val wholeText = content.text()
This is an expensive operation, proceed with caution and cache the result if you need to reuse it.
Iterating through the content¶
The individual Content
elements can be iterated through with a regular for
loop:
for (element in content) {
// Process element
}
Alternatively, you can get the whole list of elements with content.elements()
, or use the lower level APIs to iterate the content manually:
val iterator = content.iterator()
while (iterator.hasNext()) {
val element = iterator.next()
}
Some Content
implementations support bidirectional iterations. To iterate backwards, use:
while (iterator.hasPrevious()) {
val element = iterator.hasPrevious()
}
Processing the elements¶
The Content
iterator yields Content.Element
objects representing a single semantic portion of the publication, such as a heading, a paragraph or an embedded image.
Every element has a locator
property targeting it in the publication. You can use the locator, for example, to navigate to the element or to draw a Decoration
on top of it.
navigator.go(element.locator)
Types of elements¶
Depending on the actual implementation of Content.Element
, more properties are available to access the actual data. The toolkit ships with a number of default implementations for common types of elements.
Embedded media¶
The Content.EmbeddedElement
interface is implemented by any element referencing an external resource. It contains an embeddedLink
property you can use to get the actual content of the resource.
if (element is Content.EmbeddedElement) {
val bytes = publication
.get(element.embeddedLink)
.read().getOrThrow()
}
Here are the default available implementations:
Content.AudioElement
- audio clipsContent.VideoElement
- video clipsContent.ImageElement
- bitmap images, with the additional property:caption: String?
- figure caption, when available
Text¶
Textual elements¶
The Content.TextualElement
interface is implemented by any element which can be represented as human-readable text. This is useful when you want to extract the text content of a publication without caring for each individual type of elements.
val wholeText = publication.content()
.elements()
.filterIsInstance<Content.TextualElement>()
.mapNotNull { it.text }
.joinToString(separator = "\n")
Text elements¶
Actual text elements are instances of Content.TextElement
, which represent a single block of text such as a heading, a paragraph or a list item. It is comprised of a role
and a list of segments
.
The role
is the nature of the text element in the document. For example a heading, body, footnote or a quote. It can be used to reconstruct part of the structure of the original document.
A text element is composed of individual segments with their own locator
and attributes
. They are useful to associate attributes with a portion of a text element. For example, given the HTML paragraph:
<p>It is pronounced <span lang="fr">croissant</span>.</p>
The following TextElement
will be produced:
TextElement(
segments = listOf(
Segment(text = "It is pronounced "),
Segment(text = "croissant", attributes = mapOf(LANGUAGE to "fr")),
Segment(text = ".")
)
)
If you are not interested in the segment attributes, you can also use element.text
to get the concatenated raw text.
Element attributes¶
All types of Content.Element
can have associated attributes. Custom ContentService
implementations can use this as an extensibility point.
Use cases¶
An index of all images embedded in the publication¶
This example extracts all the embedded images in the publication and displays them in a Jetpack Compose list. Clicking on an image jumps to its location in the publication.
data class Item(
val locator: Locator,
val text: String?,
val bitmap: ImageBitmap?
)
var images by remember {
mutableStateOf<List<Item>>(emptyList())
}
LaunchedEffect(publication) {
publication.content()?.let { content ->
images = content.elements()
.filterIsInstance<Content.ImageElement>()
.map { element ->
Item(
locator = element.locator,
text = element.caption,
bitmap = publication.get(element.embeddedLink)
.readAsBitmap().getOrNull()?.asImageBitmap()
)
}
}
}
LazyColumn {
items(images) { item ->
if (item.bitmap != null) {
Column(
modifier = Modifier.clickable {
navigator.go(item.locator)
}
) {
Image(bitmap = item.bitmap, contentDescription = item.text)
Text(item.caption ?: "No caption")
}
}
}
}