Coverage for src/epublib/create.py: 93%
75 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -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.mediatype 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)
121 resource.metadata.identifier = ""
122 resource.metadata["identifier"].tag["id"] = self.unique_identifier
124 resource.metadata.title = self.book_title
125 resource.metadata.language = self.language
126 resource.metadata.modified = datetime.now()
128 if self.add_generator_tag:
129 version = get_epublib_version()
130 __ = resource.metadata.add("generator", "Created with epublib")
131 if version:
132 __ = resource.metadata.add("epublib version", version)
134 item = resource.manifest.add_item(
135 ManifestItem(
136 name=self.navigation_document_path,
137 id=EPUBId("nav"),
138 media_type=MediaType.XHTML.value,
139 properties=["nav"],
140 manifest_filename=self.package_document_path,
141 )
142 )
143 __ = resource.spine.add_item(SpineItemRef(name=item.id))
145 return resource.content
147 def new_navigation_document(self):
148 return navigation_document_skeleton.format(
149 title=self.navigation_document_title,
150 toc_title=self.toc_title,
151 language=self.language,
152 )
154 def to_file(self):
155 file = io.BytesIO()
156 with zipfile.ZipFile(file, "w") as zf:
157 zf.writestr("mimetype", mimetype)
158 zf.writestr("META-INF/container.xml", self.new_container_file())
159 zf.writestr(self.package_document_path, self.new_package_document())
160 zf.writestr(self.navigation_document_path, self.new_navigation_document())
162 __ = file.seek(0)
163 return file
165 def to_bytes(self):
166 return self.to_file().getvalue()