Coverage for src/epublib/create.py: 100%
75 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-07 09:53 -0300
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-07 09:53 -0300
1import io
2import zipfile
3from datetime import datetime
4from typing import TypedDict, Unpack
6from epublib.exceptions import EPUBError
7from epublib.identifier import EPUBId
8from epublib.media_type import MediaType
9from epublib.package.manifest import ManifestItem
10from epublib.package.resource import PackageDocument
11from epublib.package.spine import SpineItemRef
12from epublib.util import get_epublib_version
15class EPUBCreationError(EPUBError):
16 pass
19mimetype = "application/epub+zip"
20container_xml: str = """\
21<?xml version="1.0" encoding="UTF-8"?>
22<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
23 <rootfiles>
24 <rootfile full-path="{package_document_path}" media-type="application/oebps-package+xml"/>
25 </rootfiles>
26</container>
27"""
29package_document_skeleton: str = """\
30<?xml version="1.0" encoding="UTF-8"?>
31<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="{unique_identifier}">
32 <metadata
33 xmlns:dc="http://purl.org/dc/elements/1.1/"
34 xmlns:opf="http://www.idpf.org/2007/opf"
35 ></metadata>
36 <manifest></manifest>
37 <spine></spine>
38</package>
39"""
41navigation_document_skeleton: str = """\
42<?xml version="1.0" encoding="utf-8"?>
43<!DOCTYPE html>
44<html
45 xmlns="http://www.w3.org/1999/xhtml"
46 xmlns:epub="http://www.idpf.org/2007/ops"
47>
48 <head>
49 <title>{title}</title>
50 <meta charset="utf-8"/>
51 </head>
52 <body>
53 <nav epub:type="toc" id="toc" role="doc-toc">
54 <h1>{toc_title}</h1>
55 <ol>
56 <li><a href="#toc">{toc_title}</a></li>
57 </ol>
58 </nav>
59 </body>
60</html>
61"""
64class EPUBCreationOptions(TypedDict, total=False):
65 language: str
66 book_title: str
67 unique_identifier: str
68 package_document_path: str
69 navigation_document_path: str
70 navigation_document_title: str
71 toc_title: str
72 add_generator_tag: bool
75class EPUBCreator:
76 def __init__(self, **options: Unpack[EPUBCreationOptions]):
77 self.language: str = options.get("language", "")
78 self.book_title: str = options.get("book_title", "")
79 self.unique_identifier: str = options.get("unique_identifier", "bookid")
80 self.package_document_path: str = options.get(
81 "package_document_path",
82 "content.opf",
83 )
84 self.navigation_document_path: str = options.get(
85 "navigation_document_path", "Text/nav.xhtml"
86 )
87 self.navigation_document_title: str = options.get(
88 "navigation_document_title",
89 "",
90 )
91 self.toc_title: str = options.get(
92 "toc_title",
93 "",
94 )
95 self.add_generator_tag: bool = options.get(
96 "add_generator_tag",
97 True,
98 )
100 if not self.package_document_path:
101 raise EPUBCreationError("Package document path cannot be empty")
103 if not self.package_document_path.endswith(".opf"):
104 raise EPUBCreationError("Package document path must end with .opf")
106 if not self.navigation_document_path:
107 raise EPUBCreationError("Navigation document path cannot be empty")
109 if not self.navigation_document_path.endswith(".xhtml"):
110 raise EPUBCreationError("Navigation document path must end with .xhtml")
112 def new_container_file(self):
113 return container_xml.format(package_document_path=self.package_document_path)
115 def new_package_document(self):
116 content = package_document_skeleton.format(
117 unique_identifier=self.unique_identifier
118 )
120 resource = PackageDocument(content.encode(), self.package_document_path)
122 resource.metadata.identifier = ""
123 resource.metadata["identifier"].tag["id"] = self.unique_identifier
125 resource.metadata.title = self.book_title
126 resource.metadata.language = self.language
127 resource.metadata.modified = datetime.now()
129 if self.add_generator_tag:
130 version = get_epublib_version()
131 __ = resource.metadata.add_opf("generator", "Created with epublib")
132 if version:
133 __ = resource.metadata.add_opf("epublib version", version)
135 item = resource.manifest.add_item(
136 ManifestItem(
137 soup=resource.soup,
138 filename=self.navigation_document_path,
139 id=EPUBId("nav"),
140 media_type=MediaType.XHTML.value,
141 properties=["nav"],
142 own_filename=self.package_document_path,
143 )
144 )
145 __ = resource.spine.add_item(SpineItemRef(soup=resource.soup, idref=item.id))
147 return resource.content
149 def new_navigation_document(self):
150 return navigation_document_skeleton.format(
151 title=self.navigation_document_title,
152 toc_title=self.toc_title,
153 language=self.language,
154 )
156 def to_file(self):
157 file = io.BytesIO()
158 with zipfile.ZipFile(file, "w") as zf:
159 zf.writestr("mimetype", mimetype)
160 zf.writestr("META-INF/container.xml", self.new_container_file())
161 zf.writestr(self.package_document_path, self.new_package_document())
162 zf.writestr(self.navigation_document_path, self.new_navigation_document())
164 __ = file.seek(0)
165 return file
167 def to_bytes(self):
168 return self.to_file().getvalue()