Skip to content
Open
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Flutter Package to parse EPUB files (EBooks), with support for Media Overlays!
✅ List sections (also commonly named "chapters") in default reading order
✅ Get section audio (if exists)
✅ Get text-audio synchronization info for each section
✅ Get text segment given a time for each section
✅ Get text segment given a time for each section
✅ Get page progression direction from the EPUB spine

### Work in progress

Expand Down Expand Up @@ -318,6 +319,29 @@ And then, access its properties:

</details>

- **Retrieving page progression direction:** `epub.pageProgressionDirection`

```dart
import 'package:epub_decoder/epub_decoder.dart' show PageProgressionDirection;

final PageProgressionDirection? direction = epub.pageProgressionDirection;

switch (direction) {
case PageProgressionDirection.ltr:
// left-to-right
break;
case PageProgressionDirection.rtl:
// right-to-left
break;
case PageProgressionDirection.$default:
// The publication declares no preference.
break;
case null:
// Attribute is not declared.
break;
}
```

## Additional information

To understand EPUB3 specification and therefore plan the structure of this package, the following page was taken as reference: [Package and Metadata | EPUB3 Best Practices](https://www.oreilly.com/library/view/epub-3-best/9781449329129/ch01.html).
Expand Down
32 changes: 28 additions & 4 deletions lib/epub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ class Epub extends Equatable {
/// The [fileBytes] parameter should contain the raw bytes of the EPUB file.
Epub.fromBytes(Uint8List fileBytes)
: zip = ZipDecoder().decodeBytes(fileBytes) {
_rootFileContentCache = Lazy(_initializeRootFileContent);
_metadata = Lazy(_initializeMetadata);
_items = Lazy(_initializeItems);
_sections = Lazy(_initializeSections);
_pageProgressionDirection = Lazy(_initializePageProgressionDirection);
}

/// Constructs an [Epub] instance from a [File].
Expand All @@ -31,17 +33,21 @@ class Epub extends Equatable {
Epub.fromFile(File file)
: assert(file.path._extension == 'epub'),
zip = ZipDecoder().decodeBytes(file.readAsBytesSync()) {
_rootFileContentCache = Lazy(_initializeRootFileContent);
_metadata = Lazy(_initializeMetadata);
_items = Lazy(_initializeItems);
_sections = Lazy(_initializeSections);
_pageProgressionDirection = Lazy(_initializePageProgressionDirection);
}

/// The decoded ZIP archive of the EPUB file.
final Archive zip;

late final Lazy<XmlDocument> _rootFileContentCache;
late final Lazy<List<Metadata>> _metadata;
late final Lazy<List<Item>> _items;
late final Lazy<List<Section>> _sections;
late final Lazy<PageProgressionDirection?> _pageProgressionDirection;

String get title =>
metadata
Expand Down Expand Up @@ -91,11 +97,14 @@ class Epub extends Equatable {
/// Content of the root file as an XML document.
///
/// Throws a [FormatException] if the root file is not found.
XmlDocument get _rootFileContent {
/// Parsed once and cached, since [_metadata], [_items], [_sections]
/// and [_pageProgressionDirection] all read from it.
XmlDocument get _rootFileContent => _rootFileContentCache.value;

XmlDocument _initializeRootFileContent() {
final file = zip.findFile(_rootFilePath);
file ?? (throw const FormatException('Root file not found.'));
final content = XmlDocument.parse(utf8.decode(file.content));
return content;
return XmlDocument.parse(utf8.decode(file.content));
}

/// Metadata of the EPUB file, such as title, authors, media overlays, etc.
Expand Down Expand Up @@ -201,6 +210,20 @@ class Epub extends Equatable {
return sections;
}

/// Reading direction of the EPUB, as declared in its `<spine>` element.
///
/// Returns null when not specified.
PageProgressionDirection? get pageProgressionDirection => _pageProgressionDirection.value;

PageProgressionDirection? _initializePageProgressionDirection() {
final spinexml = _rootFileContent.xpath('/package/spine').first;
final direction = spinexml.getAttribute('page-progression-direction');

return direction != null
? PageProgressionDirection.fromValue(direction)
: null;
}

/// The list of properties that are used to determine whether two instances are equal.
///
/// props[0] = metadata, props[1] = items, props[2] = sections
Expand All @@ -212,7 +235,8 @@ class Epub extends Equatable {
// _sections.isInitialized ? _sections.value : null,
_metadata,
_items,
_sections
_sections,
_pageProgressionDirection,
];
}

Expand Down
1 change: 1 addition & 0 deletions lib/models/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export 'document_metadata.dart';
export 'item.dart';
export 'item_media_type.dart';
export 'item_property.dart';
export 'page_progression_direction.dart';
export 'section.dart';
export 'smil_parallel.dart';
export 'lazy.dart';
16 changes: 16 additions & 0 deletions lib/models/page_progression_direction.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// Reading direction declared on the EPUB's `<spine>` element.
///
/// Reference taken from https://www.w3.org/TR/epub-33/#attrdef-spine-page-progression-direction
enum PageProgressionDirection {
ltr('ltr'),
rtl('rtl'),
$default('default');

const PageProgressionDirection(this.value);
final String value;

static PageProgressionDirection fromValue(String value) {
return PageProgressionDirection.values
.firstWhere((item) => item.value == value);
}
}
5 changes: 5 additions & 0 deletions test/epub_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,9 @@ void main() {

expect(epubWithoutCover.cover, isNull);
});

test('''Epub.pageProgressionDirection returns the direction declared
in <spine>''', () {
expect(epub.pageProgressionDirection, PageProgressionDirection.ltr);
});
}