Created
November 23, 2023 07:15
-
-
Save ssokolow/482f9277251c7fdbd907c2711109f050 to your computer and use it in GitHub Desktop.
Quick script to split a CBZ file full of two-page scans into pages and then bundle them into data URIs in a .html file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
"""A quick helper for splitting manga pages in a CBZ containing two-page | |
spreads and then bundling them into base64 URLs in an HTML file so the mobile | |
Safari preview inside the iOS Files app can act as a quick-and-dirty manga | |
reader. | |
Takes .cbz files and produces large .html files. | |
Currently assumes first image will be a cover and doesn't split it. | |
""" | |
# NOTE: This is a quick hack. Most of the polish is from a template that | |
# I start all my quick hacks from. | |
__author__ = "Stephan Sokolow (deitarion/SSokolow)" | |
__appname__ = "Manga to HTML" | |
__version__ = "0.1" | |
__license__ = "MIT" | |
import base64, logging, os # noqa: E401 | |
from io import BytesIO | |
from zipfile import ZipFile | |
from PIL import Image # type: ignore | |
log = logging.getLogger(__name__) | |
IMAGE_EXTS = Image.registered_extensions() # Dict[extension, format name] | |
def write_datauri(image_in: Image, html_fobj): | |
tmp = BytesIO() | |
image_in.save(tmp, 'jpeg') | |
html_fobj.write(b'<img src="data:image/jpeg;base64,') | |
html_fobj.write(base64.encodebytes(tmp.getbuffer())) | |
html_fobj.write(b'" />') | |
def process_arg(path): | |
log.info("Processing archive %s...", path) | |
zbase, zext = os.path.splitext(path) | |
outpath = '{}.html'.format(zbase) | |
cover_seen = False | |
with ZipFile(path) as zobj_in, open(outpath, 'wb') as fobj_out: | |
fobj_out.write(b'<!DOCTYPE html>' | |
b'<html><head>\n<meta charset="utf-8">' | |
b"<style>\n* { margin: 0; padding: 0 }\n" | |
b"body { background: black; }\n" | |
b"img { width: 100%; margin-bottom: 2px; }\n</style>" | |
b"</head><body>\n") | |
for zinfo in sorted(zobj_in.infolist(), key=lambda x: x.filename): | |
ibase, iext = os.path.splitext(zinfo.filename) | |
iext = iext.lower() | |
if zinfo.is_dir() or iext not in IMAGE_EXTS: | |
log.info("Skipping non-image: %s", zinfo.filename) | |
else: | |
im = Image.open(zobj_in.open(zinfo.filename, 'r')) | |
if not cover_seen: | |
log.info("Copying cover verbatim: %s", zinfo.filename) | |
write_datauri(im, fobj_out) | |
im.close() | |
cover_seen = True | |
continue | |
log.info("Processing image: %s", zinfo.filename) | |
# im.crop will ideally just take a reference to `im` | |
# | |
# I'm not sure if this will duplicate the column of pixels at | |
# split_point, but it doesn't matter | |
split_point = im.size[0] // 2 | |
page_right = im.crop((split_point, 0, im.size[0], im.size[1])) | |
page_left = im.crop((0, 0, split_point, im.size[1])) | |
for page in (page_right, page_left): | |
write_datauri(page, fobj_out) | |
page.close() | |
fobj_out.write(b"</body>\n</html>") | |
def main(): | |
"""The main entry point, compatible with setuptools entry points.""" | |
from argparse import ArgumentParser, RawDescriptionHelpFormatter | |
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter, | |
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0]) | |
parser.add_argument('-V', '--version', action='version', | |
version="%%(prog)s v%s" % __version__) | |
parser.add_argument('-v', '--verbose', action="count", | |
default=3, help="Increase the verbosity. Use twice for extra effect.") | |
parser.add_argument('-q', '--quiet', action="count", | |
default=0, help="Decrease the verbosity. Use twice for extra effect.") | |
parser.add_argument('path', action="store", nargs="+", | |
help="Path to operate on") | |
args = parser.parse_args() | |
# Set up clean logging to stderr | |
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING, | |
logging.INFO, logging.DEBUG] | |
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1) | |
args.verbose = max(args.verbose, 0) | |
logging.basicConfig(level=log_levels[args.verbose], | |
format='%(levelname)s: %(message)s') | |
for path in args.path: | |
process_arg(path) | |
if __name__ == '__main__': # pragma: nocover | |
main() | |
# vim: set sw=4 sts=4 expandtab : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment