Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Created November 23, 2023 07:15
Show Gist options
  • Save ssokolow/482f9277251c7fdbd907c2711109f050 to your computer and use it in GitHub Desktop.
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
#!/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