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

1import io 

2import zipfile 

3from datetime import datetime 

4from typing import TypedDict, Unpack 

5 

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 

13 

14 

15class EPUBCreationError(EPUBError): 

16 pass 

17 

18 

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""" 

28 

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""" 

40 

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""" 

62 

63 

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 

73 

74 

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 ) 

99 

100 if not self.package_document_path: 

101 raise EPUBCreationError("Package document path cannot be empty") 

102 

103 if not self.package_document_path.endswith(".opf"): 

104 raise EPUBCreationError("Package document path must end with .opf") 

105 

106 if not self.navigation_document_path: 

107 raise EPUBCreationError("Navigation document path cannot be empty") 

108 

109 if not self.navigation_document_path.endswith(".xhtml"): 

110 raise EPUBCreationError("Navigation document path must end with .xhtml") 

111 

112 def new_container_file(self): 

113 return container_xml.format(package_document_path=self.package_document_path) 

114 

115 def new_package_document(self): 

116 content = package_document_skeleton.format( 

117 unique_identifier=self.unique_identifier 

118 ) 

119 

120 resource = PackageDocument(content.encode(), self.package_document_path) 

121 resource.metadata.identifier = "" 

122 resource.metadata["identifier"].tag["id"] = self.unique_identifier 

123 

124 resource.metadata.title = self.book_title 

125 resource.metadata.language = self.language 

126 resource.metadata.modified = datetime.now() 

127 

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) 

133 

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)) 

144 

145 return resource.content 

146 

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 ) 

153 

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()) 

161 

162 __ = file.seek(0) 

163 return file 

164 

165 def to_bytes(self): 

166 return self.to_file().getvalue()