├── .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 |