├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── comicepub ├── __init__.py ├── __version__.py ├── cli.py ├── comicepub.py ├── render.py └── template │ ├── container.xml │ ├── cover.xhtml │ ├── fixed-layout-jp.css │ ├── mimetype │ ├── navigation-documents.xhtml │ ├── p.xhtml │ └── standard.opf └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Jetbrains 2 | .idea 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 moeoverflow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include comicepub/template/* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # comicepub 2 | digital comic epub3 generator 3 | 4 | 5 | 6 | ### Install 7 | 8 | ```Shell 9 | pip3 install comicepub 10 | ``` 11 | 12 | 13 | 14 | ### Usage 15 | 16 | ```shell 17 | comicepub --help 18 | usage: comicepub [-h] -t TITLE --author AUTHOR [--publisher PUBLISHER] [--language LANGUAGE] -i INPUT -o OUTPUT 19 | [--cover COVER] [--version] 20 | 21 | optional arguments: 22 | -h, --help show this help message and exit 23 | -t TITLE, --title TITLE 24 | Book title 25 | --author AUTHOR Book author 26 | --publisher PUBLISHER 27 | Book publisher 28 | --language LANGUAGE Book language 29 | -i INPUT, --input INPUT 30 | Source images directory of comic book 31 | -o OUTPUT, --output OUTPUT 32 | Output filename of comic book 33 | --cover COVER cover image file of comic book 34 | --version show program's version number and exit 35 | ``` 36 | 37 | ```shell 38 | comicepub \ 39 | -i /path/to/your/comicdir \ 40 | -o /path/to/save/output.epub \ 41 | --title "Comicbook" \ 42 | --author "comicepub" \ 43 | --language "zh" 44 | ``` 45 | 46 | ```python 47 | from comicepub import ComicEpub 48 | 49 | comicepub = ComicEpub("path/to/output.epub") 50 | 51 | comicepub.title = ("Title", "Title") 52 | comicepub.authors = [("Author 1", "Author 1")] 53 | comicepub.publisher = ('Comicbook', 'Comicbook') 54 | comicepub.language = "ja" 55 | 56 | comicepub.add_comic_page(cover_data, cover_ext, cover=True) 57 | for image_data, image_ext in images: 58 | comicepub.add_comic_page(image_data, image_ext, cover=False) 59 | 60 | comicepub.save() 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /comicepub/__init__.py: -------------------------------------------------------------------------------- 1 | from .comicepub import ComicEpub # NOQA 2 | -------------------------------------------------------------------------------- /comicepub/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.4" 2 | -------------------------------------------------------------------------------- /comicepub/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from comicepub import ComicEpub 4 | from comicepub.__version__ import __version__ 5 | 6 | 7 | def parse_args(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-t", "--title", help="Book title", required=True) 10 | parser.add_argument("--author", help="Book author", required=True) 11 | parser.add_argument("--publisher", help="Book publisher") 12 | parser.add_argument("--language", help="Book language") 13 | 14 | parser.add_argument( 15 | "-i", "--input", help="Source images directory of comic book", required=True 16 | ) 17 | parser.add_argument( 18 | "-o", "--output", help="Output filename of comic book", required=True 19 | ) 20 | parser.add_argument("--cover", help="cover image file of comic book") 21 | 22 | parser.add_argument( 23 | "--version", 24 | action="version", 25 | version="%(prog)s {version}".format(version=__version__), 26 | ) 27 | 28 | return parser.parse_args() 29 | 30 | 31 | def generate_epub(args): 32 | comicepub = ComicEpub(args.output) 33 | 34 | comicepub.title = (args.title, args.title) 35 | comicepub.authors = [(args.author, args.author)] 36 | if args.publisher is not None: 37 | comicepub.publisher = (args.publisher, args.publisher) 38 | if args.language is not None: 39 | comicepub.language = args.language 40 | 41 | images = os.listdir(args.input) 42 | images.sort() 43 | 44 | for index, image in enumerate(images): 45 | is_cover = index == 0 46 | with open(os.path.join(args.input, image), "rb") as file: 47 | data = file.read() 48 | ext = os.path.splitext(image)[1] 49 | comicepub.add_comic_page(data, ext, cover=is_cover) 50 | 51 | comicepub.save() 52 | 53 | 54 | def main(): 55 | args = parse_args() 56 | generate_epub(args) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /comicepub/comicepub.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import zipfile 3 | import uuid 4 | import datetime 5 | from typing import Tuple, List, Set 6 | from mimetypes import MimeTypes 7 | from .render import render_mimetype 8 | from .render import render_container_xml 9 | from .render import render_navigation_documents_xhtml 10 | from .render import render_standard_opf 11 | from .render import render_xhtml 12 | from .render import get_fixed_layout_jp_css 13 | 14 | 15 | class ComicEpub: 16 | """ 17 | This ComicEpub class is dedicated to generating ditigal comic EPUB files conataining image-only content. 18 | """ 19 | 20 | def __init__( 21 | self, filename, 22 | epubid: str = uuid.uuid1(), 23 | title: Tuple[str, str] = None, 24 | subjects: Set[str] = None, 25 | authors: List[Tuple[str, str, str]] = None, 26 | publisher: Tuple[str, str] = None, 27 | language: str = "ja", 28 | updated_date: str = datetime.datetime.now().isoformat(), 29 | view_width: int = 848, 30 | view_height: int = 1200, 31 | ): 32 | """ 33 | Create a zip file as an EPUB container, which is only epub-valid after calling the save() method. 34 | 35 | :rtype: instance of ComicEpub 36 | :param filename: epub file path to save 37 | :param epubid: unique epub id - Default: random uuid 38 | :param title: epub title - Tuple(title, file_as) - Default: Unknown Title 39 | :param authors: epub authors - List of Tuple(author_name, file_as) - Default: Unknown Author 40 | :param publisher: epub publisher - Tuple(publisher_name, file_as) - Default: Unknown Publisher 41 | :param language: epub language - Default: ja 42 | :param updated_date: epub updated_date - Default: current time 43 | :param view_width: epub view_width - Default: 848 44 | :param view_height: epub view_height - Default: 1200 45 | """ 46 | if title is None: 47 | self.title = ('Unknown Title', 'Unknown Title') 48 | else: 49 | self.title = title 50 | if subjects is None: 51 | self.subjects = set() 52 | else: 53 | self.subjects = subjects 54 | if authors is None: 55 | self.authors = [('Unknown Author', 'Unknown Author')] 56 | else: 57 | self.authors = authors 58 | if publisher is None: 59 | self.publisher = ('Unknown Publisher', 'Unknown Publisher') 60 | else: 61 | self.publisher = publisher 62 | 63 | self.epubid = epubid 64 | self.authors = authors 65 | self.publisher = publisher 66 | self.language = language 67 | self.updated_date = updated_date 68 | self.view_width = view_width 69 | self.view_height = view_height 70 | 71 | self.manifest_images: List[Tuple[str, str, str]] = [] 72 | self.manifest_xhtmls: List[Tuple[str, str]] = [] 73 | self.manifest_spines: List[str] = [] 74 | 75 | self.nav_title = "Navigation" 76 | self.nav_items: List[Tuple[str, str]] = [] 77 | 78 | self.epub = None 79 | self.__open(filename) 80 | 81 | self.mime = MimeTypes() 82 | 83 | def __open(self, filename): 84 | 85 | if '.epub' not in filename: 86 | filename += '.epub' 87 | 88 | full_file_name = os.path.expanduser(filename) 89 | path = os.path.split(full_file_name)[0] 90 | if not os.path.exists(path): 91 | os.makedirs(path) 92 | self.epub = zipfile.ZipFile(full_file_name, 'w') 93 | 94 | def __close(self): 95 | self.epub.close() 96 | 97 | def __add_image(self, index: int, image_data, image_ext, cover: bool = False): 98 | if cover: 99 | image_id = "cover" 100 | else: 101 | image_id = "i-" + "%04d" % index 102 | 103 | path = "item/image/" + image_id + image_ext 104 | self.epub.writestr(path, image_data) 105 | 106 | mimetype = self.mime.guess_type('test' + image_ext) 107 | if mimetype[0] is None: 108 | image_mimetype = "image/jpeg" 109 | else: 110 | image_mimetype = mimetype[0] 111 | return image_id, image_ext, image_mimetype 112 | 113 | def __add_xhtml(self, index: int, title: str, image_id: str, image_ext: str, cover: bool = False): 114 | if cover: 115 | xhtml_id = "p-cover" 116 | else: 117 | xhtml_id = "p-" + "%04d" % index 118 | 119 | content = render_xhtml(title, image_id, image_ext, self.view_width, self.view_height, cover) 120 | self.epub.writestr("item/xhtml/" + xhtml_id + ".xhtml", content) 121 | return xhtml_id 122 | 123 | def add_comic_page(self, image_data, image_ext, cover=False): 124 | """ 125 | Add images to the page in order, each image is a page. 126 | 127 | :param image_data: data of image 128 | :param image_ext: extension of image 129 | :param cover: true if image is cover 130 | """ 131 | index = len(self.manifest_xhtmls) 132 | image_id, image_ext, image_mimetype = self.__add_image(index, image_data, image_ext, cover) 133 | xhtml_id = self.__add_xhtml(index, self.title[0], image_id, image_ext, cover) 134 | 135 | self.manifest_images.append((image_id, image_ext, image_mimetype)) 136 | self.manifest_xhtmls.append((xhtml_id, image_id)) 137 | self.manifest_spines.append(xhtml_id) 138 | 139 | def save(self): 140 | """ 141 | generate epub required files, then close and save epub file. 142 | """ 143 | self.epub.writestr("mimetype", render_mimetype()) 144 | self.epub.writestr("META-INF/container.xml", render_container_xml()) 145 | self.epub.writestr("item/standard.opf", render_standard_opf( 146 | uuid=self.epubid, 147 | title=self.title, 148 | subjects=self.subjects, 149 | authors=self.authors, 150 | publisher=self.publisher, 151 | language=self.language, 152 | updated_date=self.updated_date, 153 | view_width=self.view_width, 154 | view_height=self.view_height, 155 | manifest_images=self.manifest_images, 156 | manifest_xhtmls=self.manifest_xhtmls, 157 | manifest_spines=self.manifest_spines, 158 | )) 159 | self.nav_items.append(('p-cover', 'Cover')) 160 | self.epub.writestr("item/navigation-documents.xhtml", render_navigation_documents_xhtml( 161 | title=self.nav_title, 162 | nav_items=self.nav_items, 163 | )) 164 | self.epub.writestr("item/style/fixed-layout-jp.css", get_fixed_layout_jp_css()) 165 | 166 | self.__close() 167 | -------------------------------------------------------------------------------- /comicepub/render.py: -------------------------------------------------------------------------------- 1 | import os 2 | from jinja2 import Environment 3 | from typing import List, Tuple, Set 4 | 5 | 6 | def get_content_from_file(path): 7 | with open(os.path.join(os.path.dirname(__file__), path), 'r', encoding='utf-8') as f: 8 | return f.read() 9 | 10 | 11 | def render_mimetype(): 12 | return get_content_from_file('./template/mimetype') 13 | 14 | 15 | def render_container_xml(): 16 | return get_content_from_file('./template/container.xml') 17 | 18 | 19 | def render_standard_opf( 20 | uuid: str, 21 | title: Tuple[str, str], 22 | subjects: Set[str], 23 | authors: List[Tuple[str, str, str]], 24 | publisher: Tuple[str, str], 25 | language: str, 26 | updated_date: str, 27 | view_width: int, 28 | view_height: int, 29 | manifest_images: List[Tuple[str, str]], 30 | manifest_xhtmls: List[Tuple[str, str]], 31 | manifest_spines: List[str], 32 | ) -> str: 33 | template = get_content_from_file('./template/standard.opf') 34 | return Environment().from_string(template).render( 35 | uuid=uuid, 36 | title=title, 37 | subjects=subjects, 38 | authors=authors, 39 | publisher=publisher, 40 | language=language, 41 | updated_date=updated_date, 42 | view_width=view_width, 43 | view_height=view_height, 44 | manifest_images=manifest_images, 45 | manifest_xhtmls=manifest_xhtmls, 46 | manifest_spines=manifest_spines, 47 | ) 48 | 49 | 50 | def render_navigation_documents_xhtml( 51 | title: str, 52 | nav_items: List[Tuple[str, str]], 53 | ) -> str: 54 | template = get_content_from_file('./template/navigation-documents.xhtml') 55 | return Environment().from_string(template).render( 56 | title=title, 57 | nav_items=nav_items, 58 | ) 59 | 60 | 61 | def render_xhtml( 62 | title: str, 63 | image_id: str, 64 | image_ext: str, 65 | view_width: int, 66 | view_height: int, 67 | cover: bool = False, 68 | ) -> str: 69 | template = get_content_from_file('./template/p.xhtml') 70 | return Environment().from_string(template).render( 71 | title=title, 72 | image_id=image_id, 73 | image_ext=image_ext, 74 | view_width=view_width, 75 | view_height=view_height, 76 | cover=cover, 77 | ) 78 | 79 | 80 | def get_fixed_layout_jp_css(): 81 | return get_content_from_file('./template/fixed-layout-jp.css') 82 | -------------------------------------------------------------------------------- /comicepub/template/container.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /comicepub/template/cover.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 作品名 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /comicepub/template/fixed-layout-jp.css: -------------------------------------------------------------------------------- 1 | /* Custom Style */ 2 | -------------------------------------------------------------------------------- /comicepub/template/mimetype: -------------------------------------------------------------------------------- 1 | application/epub+zip -------------------------------------------------------------------------------- /comicepub/template/navigation-documents.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | {{ nav_title }} 12 | 13 | 14 | 15 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /comicepub/template/p.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | {{ title }} 12 | 13 | 14 | 15 | 16 | 17 | {% if cover %} 18 | 19 | {% else %} 20 | 21 | {% endif %} 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /comicepub/template/standard.opf: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | {{ title[0] }} 14 | {{ title[1] }} 15 | 16 | 17 | {% for author in authors %} 18 | {{ author[0] }} 19 | 20 | {{ author[1] }} 21 | {{ loop.index }} 22 | {% endfor %} 23 | 24 | 25 | {{ publisher[0] }} 26 | {{ publisher[1] }} 27 | 28 | 29 | {% for subject in subjects %} 30 | {{ subject }} 31 | {% endfor %} 32 | 33 | 34 | {{ language }} 35 | 36 | 37 | {{ uuid }} 38 | 39 | 40 | {{ updated_date }} 41 | 42 | 43 | pre-paginated 44 | landscape 45 | 46 | 47 | width={{ view_width }}, height={{ view_height }} 48 | 49 | 50 | 1.1 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {% for (id, ext, mimetype) in manifest_images %} 62 | {% if id == "cover" %} 63 | 64 | {% else %} 65 | 66 | {% endif %} 67 | {% endfor %} 68 | 69 | 70 | {% for (id, image_id) in manifest_xhtmls %} 71 | {% if id == "p-cover" %} 72 | 73 | {% else %} 74 | 75 | {% endif %} 76 | {% endfor %} 77 | 78 | 79 | 80 | {% for xhtml_id in manifest_spines %} 81 | {% if xhtml_id == "cover" %} 82 | 83 | {% else %} 84 | {% if loop.index % 2 == 0 %} 85 | 86 | {% else %} 87 | 88 | {% endif %} 89 | {% endif %} 90 | {% endfor %} 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "comicepub" 7 | description = "Japanese comic EPUB3 generate tool" 8 | readme = "README.md" 9 | requires-python = ">=3.7, < 3.13" 10 | license = { file = "LICENSE" } 11 | authors = [{ name = "ShinCurry", email = "shincurryyang@gmail.com" }] 12 | classifiers = [ 13 | "Programming Language :: Python :: 3", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | ] 17 | dependencies = ["Jinja2>=2.10.1"] 18 | dynamic = ["version"] 19 | 20 | [project.urls] 21 | Homepage = "https://github.com/moeoverflow/comicepub" 22 | Source = "https://github.com/pypa/comicepub/" 23 | 24 | [project.scripts] 25 | comicepub = "comicepub.cli:main" 26 | 27 | [tool.setuptools.dynamic] 28 | version = { attr = "comicepub.__version__.__version__" } 29 | --------------------------------------------------------------------------------