diff --git a/.flutter-plugins b/.flutter-plugins new file mode 100644 index 00000000..8d5602ec --- /dev/null +++ b/.flutter-plugins @@ -0,0 +1,6 @@ +# This is a generated file; do not edit or check into version control. +flutter_image_compress=/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress-2.3.0/ +flutter_image_compress_common=/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_common-1.0.5/ +flutter_image_compress_macos=/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_macos-1.0.2/ +flutter_image_compress_ohos=/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_ohos-0.0.3/ +flutter_image_compress_web=/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_web-0.1.4+1/ diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies new file mode 100644 index 00000000..9881206f --- /dev/null +++ b/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_image_compress_common","path":"/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_common-1.0.5/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_image_compress_common","path":"/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_common-1.0.5/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_image_compress_macos","path":"/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_macos-1.0.2/","native_build":true,"dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"flutter_image_compress_web","path":"/home/jesse/.pub-cache/hosted/pub.dev/flutter_image_compress_web-0.1.4+1/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_image_compress","dependencies":["flutter_image_compress_common","flutter_image_compress_web","flutter_image_compress_macos","flutter_image_compress_ohos"]},{"name":"flutter_image_compress_common","dependencies":[]},{"name":"flutter_image_compress_macos","dependencies":[]},{"name":"flutter_image_compress_ohos","dependencies":[]},{"name":"flutter_image_compress_web","dependencies":[]}],"date_created":"2024-12-23 10:21:31.746835","version":"3.24.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/lib/src/epub_reader.dart b/lib/src/epub_reader.dart index 0835fb31..7e3e21e6 100644 --- a/lib/src/epub_reader.dart +++ b/lib/src/epub_reader.dart @@ -1,22 +1,14 @@ import 'dart:async'; import 'package:archive/archive.dart'; +import 'package:epubx/epubx.dart'; -import 'entities/epub_book.dart'; -import 'entities/epub_byte_content_file.dart'; -import 'entities/epub_chapter.dart'; -import 'entities/epub_content.dart'; -import 'entities/epub_content_file.dart'; -import 'entities/epub_text_content_file.dart'; import 'readers/content_reader.dart'; import 'readers/schema_reader.dart'; -import 'ref_entities/epub_book_ref.dart'; import 'ref_entities/epub_byte_content_file_ref.dart'; -import 'ref_entities/epub_chapter_ref.dart'; import 'ref_entities/epub_content_file_ref.dart'; import 'ref_entities/epub_content_ref.dart'; import 'ref_entities/epub_text_content_file_ref.dart'; -import 'schema/opf/epub_metadata_creator.dart'; /// A class that provides the primary interface to read Epub files. /// @@ -119,8 +111,12 @@ class EpubReader { await Future.forEach(contentRef.AllFiles!.keys, (dynamic key) async { if (!result.AllFiles!.containsKey(key)) { - result.AllFiles![key] = - await readByteContentFile(contentRef.AllFiles![key]!); + try { + result.AllFiles![key] = + await readByteContentFile(contentRef.AllFiles![key]!); + } catch (FileNotFoundException) { + // Do nothing, let the file be missing. + } } }); @@ -147,7 +143,11 @@ class EpubReader { Map byteContentFileRefs) async { var result = {}; await Future.forEach(byteContentFileRefs.keys, (dynamic key) async { - result[key] = await readByteContentFile(byteContentFileRefs[key]!); + try { + result[key] = await readByteContentFile(byteContentFileRefs[key]!); + } catch (FileNotFoundException) { + // Do nothing, let the file be missing. + } }); return result; } diff --git a/lib/src/readers/book_cover_reader.dart b/lib/src/readers/book_cover_reader.dart index 1eba1547..191c67f0 100644 --- a/lib/src/readers/book_cover_reader.dart +++ b/lib/src/readers/book_cover_reader.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:collection/collection.dart' show IterableExtension; import 'package:image/image.dart' as images; +import 'package:logger/logger.dart'; import '../ref_entities/epub_book_ref.dart'; import '../ref_entities/epub_byte_content_file_ref.dart'; @@ -10,38 +11,65 @@ import '../schema/opf/epub_manifest_item.dart'; import '../schema/opf/epub_metadata_meta.dart'; class BookCoverReader { + static final logger = Logger(); + static Future readBookCover(EpubBookRef bookRef) async { var metaItems = bookRef.Schema!.Package!.Metadata!.MetaItems; if (metaItems == null || metaItems.isEmpty) return null; - var coverMetaItem = metaItems.firstWhereOrNull( (EpubMetadataMeta metaItem) => metaItem.Name != null && metaItem.Name!.toLowerCase() == 'cover'); - if (coverMetaItem == null) return null; - if (coverMetaItem.Content == null || coverMetaItem.Content!.isEmpty) { - throw Exception( - 'Incorrect EPUB metadata: cover item content is missing.'); + var coverManifestId; + var coverManifestSearchId = 'cover'; + if (coverMetaItem != null && + coverMetaItem.Content != null && + coverMetaItem.Content!.isNotEmpty) { + coverManifestId = coverMetaItem.Content!.toLowerCase(); + } else { + logger.e('Cover id is not in manifest.'); } - - var coverManifestItem = bookRef.Schema!.Package!.Manifest!.Items! - .firstWhereOrNull((EpubManifestItem manifestItem) => - manifestItem.Id!.toLowerCase() == - coverMetaItem.Content!.toLowerCase()); + var coverManifestItem; + if (coverManifestId != null) { + coverManifestItem = bookRef.Schema!.Package!.Manifest!.Items! + .firstWhereOrNull((EpubManifestItem manifestItem) => + manifestItem.Id!.toLowerCase() == coverManifestId); + } + // If manifest item with cover id is not found, search for item with text "cover" in id if (coverManifestItem == null) { - throw Exception( - 'Incorrect EPUB manifest: item with ID = \"${coverMetaItem.Content}\" is missing.'); + var coverManifestItems = bookRef.Schema!.Package!.Manifest!.Items! + .where((EpubManifestItem manifestItem) => + manifestItem.Id!.toLowerCase().contains(coverManifestSearchId)) + .toList(); + if (coverManifestItems.length == 1) { + coverManifestItem = coverManifestItems.first; + } else { + coverManifestItem = coverManifestItems.firstWhereOrNull( + (EpubManifestItem manifestItem) => + manifestItem.MediaType?.contains('image') ?? false); + } + } + if (coverManifestItem == null) { + logger.e( + 'Incorrect EPUB manifest: item with ID = \"$coverManifestId\" is missing.'); + return null; } EpubByteContentFileRef? coverImageContentFileRef; if (!bookRef.Content!.Images!.containsKey(coverManifestItem.Href)) { - throw Exception( + logger.e( 'Incorrect EPUB manifest: item with href = \"${coverManifestItem.Href}\" is missing.'); + return null; } coverImageContentFileRef = bookRef.Content!.Images![coverManifestItem.Href]; - var coverImageContent = - await coverImageContentFileRef!.readContentAsBytes(); - var retval = images.decodeImage(Uint8List.fromList(coverImageContent)); - return retval; + try { + var coverImageContent = + await coverImageContentFileRef!.readContentAsBytes(); + var retval = images.decodeImage(Uint8List.fromList(coverImageContent)); + return retval; + } catch (e) { + logger.e('Error reading cover image content: $e'); + return null; + } } } diff --git a/lib/src/readers/chapter_reader.dart b/lib/src/readers/chapter_reader.dart index 7c92e72b..6a9fa77a 100644 --- a/lib/src/readers/chapter_reader.dart +++ b/lib/src/readers/chapter_reader.dart @@ -1,3 +1,5 @@ +import 'package:epubx/src/utils/file_name_decoder.dart'; + import '../ref_entities/epub_book_ref.dart'; import '../ref_entities/epub_chapter_ref.dart'; import '../ref_entities/epub_text_content_file_ref.dart'; @@ -8,18 +10,26 @@ class ChapterReader { if (bookRef.Schema!.Navigation == null) { return []; } - return getChaptersImpl( - bookRef, bookRef.Schema!.Navigation!.NavMap!.Points!); + var navigationPoints = bookRef.Schema!.Navigation!.NavMap!.Points!; + var navigationFileNames = getAllNavigationFileNames(navigationPoints); + var unmappedChapters = getUnmappedChapters(bookRef, navigationFileNames); + var hasChapterSplittingIntoFiles = + hasChapterSplittingInFiles(navigationFileNames); + return getChaptersImpl(bookRef, navigationPoints, unmappedChapters, + hasChapterSplittingIntoFiles); } static List getChaptersImpl( - EpubBookRef bookRef, List navigationPoints) { + EpubBookRef bookRef, + List navigationPoints, + List unmappedChapters, + bool hasChapterSplittingIntoFiles, + ) { var result = []; - // navigationPoints.forEach((EpubNavigationPoint navigationPoint) { - for (var navigationPoint in navigationPoints){ + for (var navigationPoint in navigationPoints) { String? contentFileName; String? anchor; - if (navigationPoint.Content?.Source ==null) continue; + if (navigationPoint.Content?.Source == null) continue; var contentSourceAnchorCharIndex = navigationPoint.Content!.Source!.indexOf('#'); if (contentSourceAnchorCharIndex == -1) { @@ -31,7 +41,7 @@ class ChapterReader { anchor = navigationPoint.Content!.Source! .substring(contentSourceAnchorCharIndex + 1); } - contentFileName = Uri.decodeFull(contentFileName!); + contentFileName = decodeFileName(contentFileName!); EpubTextContentFileRef? htmlContentFileRef; if (!bookRef.Content!.Html!.containsKey(contentFileName)) { throw Exception( @@ -43,23 +53,82 @@ class ChapterReader { chapterRef.ContentFileName = contentFileName; chapterRef.Anchor = anchor; chapterRef.Title = navigationPoint.NavigationLabels!.first.Text; - chapterRef.SubChapters = - getChaptersImpl(bookRef, navigationPoint.ChildNavigationPoints!); - if(chapterRef.ContentFileName!.contains('_split_')) { - var fileNamePart = chapterRef.ContentFileName!.split('_split_')[0]; - for (var fileName in bookRef.Content!.Html!.keys) { - if(fileName.contains(fileNamePart)) { - if (fileName == contentFileName) { - continue; - } - chapterRef.otherTextContentFileRefs.add(bookRef.Content!.Html![fileName]!); - chapterRef.OtherContentFileNames.add(fileName); - } - } + chapterRef.SubChapters = getChaptersImpl( + bookRef, + navigationPoint.ChildNavigationPoints!, + unmappedChapters, + hasChapterSplittingIntoFiles); + if (hasChapterSplittingIntoFiles) { + addSplitChaptersToRef(bookRef, chapterRef, unmappedChapters); } result.add(chapterRef); - }; + } + ; return result; } + + static List getAllNavigationFileNames( + List points) { + var result = []; + for (var point in points) { + if (point.Content?.Source != null) { + result.add(point.Content!.Source!); + } + result + .addAll(getAllNavigationFileNames(point.ChildNavigationPoints ?? [])); + } + return result; + } + + /// Sometimes chapters are split into multiple files, + /// but the split files are not listed in the navigation. + /// We need to find these files and add them to the chapter as [OtherContentFileNames]. + static List getUnmappedChapters( + EpubBookRef bookRef, List navigationFileNames) { + var allFileNames = Set.from(bookRef.Content!.Html!.keys); + return allFileNames.difference(navigationFileNames.toSet()).toList(); + } + + /// This checks if the chapters are split into multiple files by + /// 1. Checking if the file names contain "_split_". + /// 2. Two chapters listed in the navigation file should not have the same file name part before "_split_". + static bool hasChapterSplittingInFiles(List navigationFileNames) { + var uniqueFileNameParts = {}; + for (var fileName in navigationFileNames) { + if (fileName.contains('_split_')) { + var baseName = fileName.split('_split_')[0]; + if (uniqueFileNameParts.contains(baseName)) { + return false; + } + uniqueFileNameParts.add(baseName); + } + } + return uniqueFileNameParts.isNotEmpty; + } + + static void addSplitChaptersToRef( + EpubBookRef bookRef, + EpubChapterRef chapterRef, + List unmappedChapters, + ) { + if (!chapterRef.ContentFileName!.contains('_split_')) { + return; + } + + var baseName = chapterRef.ContentFileName!.split('_split_')[0]; + var addedChapters = []; // List to store items for removal + + for (var fileName in unmappedChapters) { + if (fileName.contains(baseName) && + fileName != chapterRef.ContentFileName) { + chapterRef.otherTextContentFileRefs + .add(bookRef.Content!.Html![fileName]!); + chapterRef.OtherContentFileNames.add(fileName); + addedChapters.add(fileName); // Add to removal list + } + } + unmappedChapters + .removeWhere((fileName) => addedChapters.contains(fileName)); + } } diff --git a/lib/src/readers/content_reader.dart b/lib/src/readers/content_reader.dart index 78d0e7cf..53ef235b 100644 --- a/lib/src/readers/content_reader.dart +++ b/lib/src/readers/content_reader.dart @@ -1,3 +1,5 @@ +import 'package:epubx/src/utils/file_name_decoder.dart'; + import '../entities/epub_content_type.dart'; import '../ref_entities/epub_book_ref.dart'; import '../ref_entities/epub_byte_content_file_ref.dart'; @@ -30,7 +32,7 @@ class ContentReader { case EpubContentType.DTBOOK_NCX: var epubTextContentFile = EpubTextContentFileRef(bookRef); { - epubTextContentFile.FileName = Uri.decodeFull(fileName!); + epubTextContentFile.FileName = decodeFileName(fileName!); epubTextContentFile.ContentMimeType = contentMimeType; epubTextContentFile.ContentType = contentType; } @@ -62,7 +64,7 @@ class ContentReader { default: var epubByteContentFile = EpubByteContentFileRef(bookRef); { - epubByteContentFile.FileName = Uri.decodeFull(fileName!); + epubByteContentFile.FileName = decodeFileName(fileName!); epubByteContentFile.ContentMimeType = contentMimeType; epubByteContentFile.ContentType = contentType; } diff --git a/lib/src/readers/navigation_reader.dart b/lib/src/readers/navigation_reader.dart index 67f073b5..9f2cb2e2 100644 --- a/lib/src/readers/navigation_reader.dart +++ b/lib/src/readers/navigation_reader.dart @@ -31,8 +31,8 @@ import '../utils/zip_path_utils.dart'; class NavigationReader { static String? _tocFileEntryPath; - static Future readNavigation( - Archive epubArchive, String contentDirectoryPath, EpubPackage package) async { + static Future readNavigation(Archive epubArchive, + String contentDirectoryPath, EpubPackage package) async { var result = EpubNavigation(); if (package.Version == EpubVersion.Epub2) { var tocId = package.Spine!.TableOfContents; @@ -40,39 +40,50 @@ class NavigationReader { throw Exception('EPUB parsing error: TOC ID is empty.'); } - var tocManifestItem = package.Manifest!.Items!.cast().firstWhere( - (EpubManifestItem? item) => item!.Id!.toLowerCase() == tocId.toLowerCase(), - orElse: () => null, - ); + var tocManifestItem = + package.Manifest!.Items!.cast().firstWhere( + (EpubManifestItem? item) => + item!.Id!.toLowerCase() == tocId.toLowerCase(), + orElse: () => null, + ); if (tocManifestItem == null) { - throw Exception('EPUB parsing error: TOC item $tocId not found in EPUB manifest.'); + throw Exception( + 'EPUB parsing error: TOC item $tocId not found in EPUB manifest.'); } - _tocFileEntryPath = ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + _tocFileEntryPath = + ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); var tocFileEntry = epubArchive.files.cast().firstWhere( - (ArchiveFile? file) => file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), + (ArchiveFile? file) => + file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), orElse: () => null); if (tocFileEntry == null) { - throw Exception('EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); + throw Exception( + 'EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); } - var containerDocument = xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); + var containerDocument = + xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); var ncxNamespace = 'http://www.daisy.org/z3986/2005/ncx/'; var ncxNode = containerDocument .findAllElements('ncx', namespace: ncxNamespace) .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (ncxNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain ncx element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain ncx element.'); } var headNode = ncxNode .findAllElements('head', namespace: ncxNamespace) .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (headNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain head element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain head element.'); } var navigationHead = readNavigationHead(headNode); @@ -80,15 +91,19 @@ class NavigationReader { var docTitleNode = ncxNode .findElements('docTitle', namespace: ncxNamespace) .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (docTitleNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain docTitle element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain docTitle element.'); } var navigationDocTitle = readNavigationDocTitle(docTitleNode); result.DocTitle = navigationDocTitle; result.DocAuthors = []; - ncxNode.findElements('docAuthor', namespace: ncxNamespace).forEach((xml.XmlElement docAuthorNode) { + ncxNode + .findElements('docAuthor', namespace: ncxNamespace) + .forEach((xml.XmlElement docAuthorNode) { var navigationDocAuthor = readNavigationDocAuthor(docAuthorNode); result.DocAuthors!.add(navigationDocAuthor); }); @@ -96,9 +111,11 @@ class NavigationReader { var navMapNode = ncxNode .findElements('navMap', namespace: ncxNamespace) .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (navMapNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain navMap element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain navMap element.'); } var navMap = readNavigationMap(navMapNode); @@ -106,14 +123,17 @@ class NavigationReader { var pageListNode = ncxNode .findElements('pageList', namespace: ncxNamespace) .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (pageListNode != null) { var pageList = readNavigationPageList(pageListNode); result.PageList = pageList; } result.NavLists = []; - ncxNode.findElements('navList', namespace: ncxNamespace).forEach((xml.XmlElement navigationListNode) { + ncxNode + .findElements('navList', namespace: ncxNamespace) + .forEach((xml.XmlElement navigationListNode) { var navigationList = readNavigationList(navigationListNode); result.NavLists!.add(navigationList); }); @@ -122,29 +142,46 @@ class NavigationReader { var tocManifestItem = package.Manifest!.Items! .cast() - .firstWhere((element) => element!.Properties == 'nav', orElse: () => null); + .firstWhere((element) => element!.Properties == 'nav', + orElse: () => null); if (tocManifestItem == null) { - throw Exception('EPUB parsing error: TOC item, not found in EPUB manifest.'); + throw Exception( + 'EPUB parsing error: TOC item, not found in EPUB manifest.'); } - _tocFileEntryPath = ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); + _tocFileEntryPath = + ZipPathUtils.combine(contentDirectoryPath, tocManifestItem.Href); var tocFileEntry = epubArchive.files.cast().firstWhere( - (ArchiveFile? file) => file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), + (ArchiveFile? file) => + file!.name.toLowerCase() == _tocFileEntryPath!.toLowerCase(), orElse: () => null); if (tocFileEntry == null) { - throw Exception('EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); + throw Exception( + 'EPUB parsing error: TOC file $_tocFileEntryPath not found in archive.'); } //Get relative toc file path - _tocFileEntryPath = ((_tocFileEntryPath!.split('/')..removeLast())..removeAt(0)).join('/') + '/'; + if (_tocFileEntryPath!.contains('/')) { + _tocFileEntryPath = ((_tocFileEntryPath! + .replaceFirst(contentDirectoryPath, '') + .split('/') + ..removeLast())) + .join('/') + + '/'; + } else { + _tocFileEntryPath = '.'; + } - var containerDocument = xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); + var containerDocument = + xml.XmlDocument.parse(convert.utf8.decode(tocFileEntry.content)); var headNode = containerDocument .findAllElements('head') .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (headNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain head element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain head element.'); } result.DocTitle = EpubNavigationDocTitle(); @@ -156,9 +193,11 @@ class NavigationReader { var navNode = containerDocument .findAllElements('nav') .cast() - .firstWhere((xml.XmlElement? elem) => elem != null, orElse: () => null); + .firstWhere((xml.XmlElement? elem) => elem != null, + orElse: () => null); if (navNode == null) { - throw Exception('EPUB parsing error: TOC file does not contain head element.'); + throw Exception( + 'EPUB parsing error: TOC file does not contain head element.'); } var navMapNode = navNode.findElements('ol').single; @@ -179,9 +218,11 @@ class NavigationReader { return result; } - static EpubNavigationContent readNavigationContent(xml.XmlElement navigationContentNode) { + static EpubNavigationContent readNavigationContent( + xml.XmlElement navigationContentNode) { var result = EpubNavigationContent(); - navigationContentNode.attributes.forEach((xml.XmlAttribute navigationContentNodeAttribute) { + navigationContentNode.attributes + .forEach((xml.XmlAttribute navigationContentNodeAttribute) { var attributeValue = navigationContentNodeAttribute.value; switch (navigationContentNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -193,15 +234,18 @@ class NavigationReader { } }); if (result.Source == null || result.Source!.isEmpty) { - throw Exception('Incorrect EPUB navigation content: content source is missing.'); + throw Exception( + 'Incorrect EPUB navigation content: content source is missing.'); } return result; } - static EpubNavigationContent readNavigationContentV3(xml.XmlElement navigationContentNode) { + static EpubNavigationContent readNavigationContentV3( + xml.XmlElement navigationContentNode) { var result = EpubNavigationContent(); - navigationContentNode.attributes.forEach((xml.XmlAttribute navigationContentNodeAttribute) { + navigationContentNode.attributes + .forEach((xml.XmlAttribute navigationContentNodeAttribute) { var attributeValue = navigationContentNodeAttribute.value; switch (navigationContentNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -218,27 +262,20 @@ class NavigationReader { break; } }); - // element with span, the content will be null; - // if (result.Source == null || result.Source!.isEmpty) { - // throw Exception( - // 'Incorrect EPUB navigation content: content source is missing.'); - // } + if (result.Source == null || result.Source!.isEmpty) { + throw Exception( + 'Incorrect EPUB navigation content: content source is missing.'); + } return result; } - static String extractContentPath(String _tocFileEntryPath, String ref) { - if (!_tocFileEntryPath.endsWith('/')) _tocFileEntryPath = _tocFileEntryPath + '/'; - var r = _tocFileEntryPath + ref; - r = r.replaceAll('/\./', '/'); - r = r.replaceAll(RegExp(r'/[^/]+/\.\./'), '/'); - r = r.replaceAll(RegExp(r'^[^/]+/\.\./'), ''); - return r; - } - - static EpubNavigationDocAuthor readNavigationDocAuthor(xml.XmlElement docAuthorNode) { + static EpubNavigationDocAuthor readNavigationDocAuthor( + xml.XmlElement docAuthorNode) { var result = EpubNavigationDocAuthor(); result.Authors = []; - docAuthorNode.children.whereType().forEach((xml.XmlElement textNode) { + docAuthorNode.children + .whereType() + .forEach((xml.XmlElement textNode) { if (textNode.name.local.toLowerCase() == 'text') { result.Authors!.add(textNode.text); } @@ -246,10 +283,13 @@ class NavigationReader { return result; } - static EpubNavigationDocTitle readNavigationDocTitle(xml.XmlElement docTitleNode) { + static EpubNavigationDocTitle readNavigationDocTitle( + xml.XmlElement docTitleNode) { var result = EpubNavigationDocTitle(); result.Titles = []; - docTitleNode.children.whereType().forEach((xml.XmlElement textNode) { + docTitleNode.children + .whereType() + .forEach((xml.XmlElement textNode) { if (textNode.name.local.toLowerCase() == 'text') { result.Titles!.add(textNode.text); } @@ -261,7 +301,9 @@ class NavigationReader { var result = EpubNavigationHead(); result.Metadata = []; - headNode.children.whereType().forEach((xml.XmlElement metaNode) { + headNode.children + .whereType() + .forEach((xml.XmlElement metaNode) { if (metaNode.name.local.toLowerCase() == 'meta') { var meta = EpubNavigationHeadMeta(); metaNode.attributes.forEach((xml.XmlAttribute metaNodeAttribute) { @@ -280,10 +322,12 @@ class NavigationReader { }); if (meta.Name == null || meta.Name!.isEmpty) { - throw Exception('Incorrect EPUB navigation meta: meta name is missing.'); + throw Exception( + 'Incorrect EPUB navigation meta: meta name is missing.'); } if (meta.Content == null) { - throw Exception('Incorrect EPUB navigation meta: meta content is missing.'); + throw Exception( + 'Incorrect EPUB navigation meta: meta content is missing.'); } result.Metadata!.add(meta); @@ -292,14 +336,16 @@ class NavigationReader { return result; } - static EpubNavigationLabel readNavigationLabel(xml.XmlElement navigationLabelNode) { + static EpubNavigationLabel readNavigationLabel( + xml.XmlElement navigationLabelNode) { var result = EpubNavigationLabel(); var navigationLabelTextNode = navigationLabelNode .findElements('text', namespace: navigationLabelNode.name.namespaceUri) .firstWhereOrNull((xml.XmlElement? elem) => elem != null); if (navigationLabelTextNode == null) { - throw Exception('Incorrect EPUB navigation label: label text element is missing.'); + throw Exception( + 'Incorrect EPUB navigation label: label text element is missing.'); } result.Text = navigationLabelTextNode.text; @@ -307,15 +353,18 @@ class NavigationReader { return result; } - static EpubNavigationLabel readNavigationLabelV3(xml.XmlElement navigationLabelNode) { + static EpubNavigationLabel readNavigationLabelV3( + xml.XmlElement navigationLabelNode) { var result = EpubNavigationLabel(); result.Text = navigationLabelNode.text.trim(); return result; } - static EpubNavigationList readNavigationList(xml.XmlElement navigationListNode) { + static EpubNavigationList readNavigationList( + xml.XmlElement navigationListNode) { var result = EpubNavigationList(); - navigationListNode.attributes.forEach((xml.XmlAttribute navigationListNodeAttribute) { + navigationListNode.attributes + .forEach((xml.XmlAttribute navigationListNodeAttribute) { var attributeValue = navigationListNodeAttribute.value; switch (navigationListNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -326,7 +375,9 @@ class NavigationReader { break; } }); - navigationListNode.children.whereType().forEach((xml.XmlElement navigationListChildNode) { + navigationListNode.children + .whereType() + .forEach((xml.XmlElement navigationListChildNode) { switch (navigationListChildNode.name.local.toLowerCase()) { case 'navlabel': var navigationLabel = readNavigationLabel(navigationListChildNode); @@ -348,7 +399,9 @@ class NavigationReader { static EpubNavigationMap readNavigationMap(xml.XmlElement navigationMapNode) { var result = EpubNavigationMap(); result.Points = []; - navigationMapNode.children.whereType().forEach((xml.XmlElement navigationPointNode) { + navigationMapNode.children + .whereType() + .forEach((xml.XmlElement navigationPointNode) { if (navigationPointNode.name.local.toLowerCase() == 'navpoint') { var navigationPoint = readNavigationPoint(navigationPointNode); result.Points!.add(navigationPoint); @@ -357,10 +410,13 @@ class NavigationReader { return result; } - static EpubNavigationMap readNavigationMapV3(xml.XmlElement navigationMapNode) { + static EpubNavigationMap readNavigationMapV3( + xml.XmlElement navigationMapNode) { var result = EpubNavigationMap(); result.Points = []; - navigationMapNode.children.whereType().forEach((xml.XmlElement navigationPointNode) { + navigationMapNode.children + .whereType() + .forEach((xml.XmlElement navigationPointNode) { if (navigationPointNode.name.local.toLowerCase() == 'li') { var navigationPoint = readNavigationPointV3(navigationPointNode); result.Points!.add(navigationPoint); @@ -369,10 +425,13 @@ class NavigationReader { return result; } - static EpubNavigationPageList readNavigationPageList(xml.XmlElement navigationPageListNode) { + static EpubNavigationPageList readNavigationPageList( + xml.XmlElement navigationPageListNode) { var result = EpubNavigationPageList(); result.Targets = []; - navigationPageListNode.children.whereType().forEach((xml.XmlElement pageTargetNode) { + navigationPageListNode.children + .whereType() + .forEach((xml.XmlElement pageTargetNode) { if (pageTargetNode.name.local == 'pageTarget') { var pageTarget = readNavigationPageTarget(pageTargetNode); result.Targets!.add(pageTarget); @@ -382,10 +441,12 @@ class NavigationReader { return result; } - static EpubNavigationPageTarget readNavigationPageTarget(xml.XmlElement navigationPageTargetNode) { + static EpubNavigationPageTarget readNavigationPageTarget( + xml.XmlElement navigationPageTargetNode) { var result = EpubNavigationPageTarget(); result.NavigationLabels = []; - navigationPageTargetNode.attributes.forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { + navigationPageTargetNode.attributes + .forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { var attributeValue = navigationPageTargetNodeAttribute.value; switch (navigationPageTargetNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -395,7 +456,8 @@ class NavigationReader { result.Value = attributeValue; break; case 'type': - var converter = EnumFromString(EpubNavigationPageTargetType.values); + var converter = EnumFromString( + EpubNavigationPageTargetType.values); var type = converter.get(attributeValue); result.Type = type; break; @@ -408,7 +470,8 @@ class NavigationReader { } }); if (result.Type == EpubNavigationPageTargetType.UNDEFINED) { - throw Exception('Incorrect EPUB navigation page target: page target type is missing.'); + throw Exception( + 'Incorrect EPUB navigation page target: page target type is missing.'); } navigationPageTargetNode.children @@ -416,7 +479,8 @@ class NavigationReader { .forEach((xml.XmlElement navigationPageTargetChildNode) { switch (navigationPageTargetChildNode.name.local.toLowerCase()) { case 'navlabel': - var navigationLabel = readNavigationLabel(navigationPageTargetChildNode); + var navigationLabel = + readNavigationLabel(navigationPageTargetChildNode); result.NavigationLabels!.add(navigationLabel); break; case 'content': @@ -426,15 +490,18 @@ class NavigationReader { } }); if (result.NavigationLabels!.isEmpty) { - throw Exception('Incorrect EPUB navigation page target: at least one navLabel element is required.'); + throw Exception( + 'Incorrect EPUB navigation page target: at least one navLabel element is required.'); } return result; } - static EpubNavigationPoint readNavigationPoint(xml.XmlElement navigationPointNode) { + static EpubNavigationPoint readNavigationPoint( + xml.XmlElement navigationPointNode) { var result = EpubNavigationPoint(); - navigationPointNode.attributes.forEach((xml.XmlAttribute navigationPointNodeAttribute) { + navigationPointNode.attributes + .forEach((xml.XmlAttribute navigationPointNodeAttribute) { var attributeValue = navigationPointNodeAttribute.value; switch (navigationPointNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -454,7 +521,9 @@ class NavigationReader { result.NavigationLabels = []; result.ChildNavigationPoints = []; - navigationPointNode.children.whereType().forEach((xml.XmlElement navigationPointChildNode) { + navigationPointNode.children + .whereType() + .forEach((xml.XmlElement navigationPointChildNode) { switch (navigationPointChildNode.name.local.toLowerCase()) { case 'navlabel': var navigationLabel = readNavigationLabel(navigationPointChildNode); @@ -465,7 +534,8 @@ class NavigationReader { result.Content = content; break; case 'navpoint': - var childNavigationPoint = readNavigationPoint(navigationPointChildNode); + var childNavigationPoint = + readNavigationPoint(navigationPointChildNode); result.ChildNavigationPoints!.add(childNavigationPoint); break; } @@ -476,18 +546,22 @@ class NavigationReader { 'EPUB parsing error: navigation point ${result.Id} should contain at least one navigation label.'); } if (result.Content == null) { - throw Exception('EPUB parsing error: navigation point ${result.Id} should contain content.'); + throw Exception( + 'EPUB parsing error: navigation point ${result.Id} should contain content.'); } return result; } - static EpubNavigationPoint readNavigationPointV3(xml.XmlElement navigationPointNode) { + static EpubNavigationPoint readNavigationPointV3( + xml.XmlElement navigationPointNode) { var result = EpubNavigationPoint(); result.NavigationLabels = []; result.ChildNavigationPoints = []; - navigationPointNode.children.whereType().forEach((xml.XmlElement navigationPointChildNode) { + navigationPointNode.children + .whereType() + .forEach((xml.XmlElement navigationPointChildNode) { switch (navigationPointChildNode.name.local.toLowerCase()) { case 'a': case 'span': @@ -497,7 +571,9 @@ class NavigationReader { result.Content = content; break; case 'ol': - readNavigationMapV3(navigationPointChildNode).Points!.forEach((point) { + readNavigationMapV3(navigationPointChildNode) + .Points! + .forEach((point) { result.ChildNavigationPoints!.add(point); }); break; @@ -509,15 +585,18 @@ class NavigationReader { 'EPUB parsing error: navigation point ${result.Id} should contain at least one navigation label.'); } if (result.Content == null) { - throw Exception('EPUB parsing error: navigation point ${result.Id} should contain content.'); + throw Exception( + 'EPUB parsing error: navigation point ${result.Id} should contain content.'); } return result; } - static EpubNavigationTarget readNavigationTarget(xml.XmlElement navigationTargetNode) { + static EpubNavigationTarget readNavigationTarget( + xml.XmlElement navigationTargetNode) { var result = EpubNavigationTarget(); - navigationTargetNode.attributes.forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { + navigationTargetNode.attributes + .forEach((xml.XmlAttribute navigationPageTargetNodeAttribute) { var attributeValue = navigationPageTargetNodeAttribute.value; switch (navigationPageTargetNodeAttribute.name.local.toLowerCase()) { case 'id': @@ -535,10 +614,13 @@ class NavigationReader { } }); if (result.Id == null || result.Id!.isEmpty) { - throw Exception('Incorrect EPUB navigation target: navigation target ID is missing.'); + throw Exception( + 'Incorrect EPUB navigation target: navigation target ID is missing.'); } - navigationTargetNode.children.whereType().forEach((xml.XmlElement navigationTargetChildNode) { + navigationTargetNode.children + .whereType() + .forEach((xml.XmlElement navigationTargetChildNode) { switch (navigationTargetChildNode.name.local.toLowerCase()) { case 'navlabel': var navigationLabel = readNavigationLabel(navigationTargetChildNode); @@ -551,7 +633,8 @@ class NavigationReader { } }); if (result.NavigationLabels!.isEmpty) { - throw Exception('Incorrect EPUB navigation target: at least one navLabel element is required.'); + throw Exception( + 'Incorrect EPUB navigation target: at least one navLabel element is required.'); } return result; diff --git a/lib/src/readers/package_reader.dart b/lib/src/readers/package_reader.dart index 59153a59..7ca81c1e 100644 --- a/lib/src/readers/package_reader.dart +++ b/lib/src/readers/package_reader.dart @@ -290,6 +290,7 @@ class PackageReader { XmlElement metadataMetaNode) { var result = EpubMetadataMeta(); result.Attributes = {}; + result.Content = metadataMetaNode.value ?? metadataMetaNode.innerText; metadataMetaNode.attributes .forEach((XmlAttribute metadataMetaNodeAttribute) { var attributeValue = metadataMetaNodeAttribute.value; @@ -308,9 +309,14 @@ class PackageReader { case 'scheme': result.Scheme = attributeValue; break; + case 'name': + result.Name = attributeValue; + break; + case 'content': + result.Content = attributeValue; + break; } }); - result.Content = metadataMetaNode.text; return result; } diff --git a/lib/src/ref_entities/epub_byte_content_file_ref.dart b/lib/src/ref_entities/epub_byte_content_file_ref.dart index 85a2792d..1c13f477 100644 --- a/lib/src/ref_entities/epub_byte_content_file_ref.dart +++ b/lib/src/ref_entities/epub_byte_content_file_ref.dart @@ -1,12 +1,6 @@ -import 'dart:async'; - import 'epub_book_ref.dart'; import 'epub_content_file_ref.dart'; class EpubByteContentFileRef extends EpubContentFileRef { EpubByteContentFileRef(EpubBookRef epubBookRef) : super(epubBookRef); - - Future> readContent() { - return readContentAsBytes(); - } } diff --git a/lib/src/ref_entities/epub_content_file_ref.dart b/lib/src/ref_entities/epub_content_file_ref.dart index 178b3bf2..a59324be 100644 --- a/lib/src/ref_entities/epub_content_file_ref.dart +++ b/lib/src/ref_entities/epub_content_file_ref.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:collection/collection.dart' show IterableExtension; +import 'package:epubx/src/ref_entities/file_not_found_exception.dart'; import 'package:quiver/core.dart'; import '../entities/epub_content_type.dart'; @@ -43,7 +44,7 @@ abstract class EpubContentFileRef { .files .firstWhereOrNull((ArchiveFile x) => x.name == contentFilePath); if (contentFileEntry == null) { - throw Exception( + throw FileNotFoundException( 'EPUB parsing error: file $contentFilePath not found in archive.'); } return contentFileEntry; @@ -54,13 +55,11 @@ abstract class EpubContentFileRef { } List openContentStream(ArchiveFile contentFileEntry) { - var contentStream = []; if (contentFileEntry.content == null) { throw Exception( 'Incorrect EPUB file: content file \"$FileName\" specified in manifest is not found.'); } - contentStream.addAll(contentFileEntry.content); - return contentStream; + return contentFileEntry.content; } Future readContentAsBytes() async { diff --git a/lib/src/ref_entities/file_not_found_exception.dart b/lib/src/ref_entities/file_not_found_exception.dart new file mode 100644 index 00000000..efe78c58 --- /dev/null +++ b/lib/src/ref_entities/file_not_found_exception.dart @@ -0,0 +1,10 @@ +class FileNotFoundException implements Exception { + final String message; + + FileNotFoundException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/src/utils/file_name_decoder.dart b/lib/src/utils/file_name_decoder.dart new file mode 100644 index 00000000..aec27b8f --- /dev/null +++ b/lib/src/utils/file_name_decoder.dart @@ -0,0 +1,7 @@ +String decodeFileName(String incomingFileName) { + try { + return Uri.decodeFull(incomingFileName); + } catch (e) { + return incomingFileName; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index bf05258e..bb10ac11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,17 +5,17 @@ issue_tracker: https://github.com/rbcprolabs/epubx.dart version: 4.0.1 environment: - sdk: ">=2.12.0 <4.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: archive: ^3.1.6 quiver: ^3.0.1+1 - path: ^1.8.1 xml: ^6.0.1 image: ^4.0.17 collection: ^1.15.0 + logger: ^1.3.0 dev_dependencies: test: ^1.16.7 - path: ^1.8.0 + path: ^1.8.1 pedantic: ^1.11.0