Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Created November 23, 2023 06:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ssokolow/c2e24fd3ae7f1bf83b601156c20e6c69 to your computer and use it in GitHub Desktop.
Save ssokolow/c2e24fd3ae7f1bf83b601156c20e6c69 to your computer and use it in GitHub Desktop.
Quick script to split manga CBZs scanned as two-page spreads so they'll convert with proper page order in Calibre
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""A quick helper for splitting manga pages so Calibre doesn't split them for
you and present them in the wrong order.
Takes and produces .cbz 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 Page Splitter"
__version__ = "0.1"
__license__ = "MIT"
import logging, os # noqa: E401
from zipfile import ZipFile
from PIL import Image # type: ignore
log = logging.getLogger(__name__)
IMAGE_EXTS = Image.registered_extensions() # Dict[extension, format name]
def process_arg(path):
log.info("Processing archive %s...", path)
zbase, zext = os.path.splitext(path)
outpath = '{}_split{}'.format(zbase, zext)
cover_seen = False
with ZipFile(path) as zobj_in, ZipFile(outpath, 'w') as zobj_out:
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():
log.info("Skipping directory entry: %s", zinfo.filename)
continue # ZipInfo.mkdir was only added in Python 3.11
elif iext in IMAGE_EXTS:
if not cover_seen:
log.info("Copying cover verbatim: %s", zinfo.filename)
zobj_out.writestr(zinfo, zobj_in.read(zinfo))
cover_seen = True
continue
log.info("Processing image: %s", zinfo.filename)
im = Image.open(zobj_in.open(zinfo.filename, 'r'))
fname_right = '{}_1{}'.format(ibase, iext)
fname_left = '{}_2{}'.format(ibase, iext)
im_format = IMAGE_EXTS[iext]
split_point = im.size[0] // 2
# 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
page_right = im.crop((split_point, 0, im.size[0], im.size[1]))
page_right.save(zobj_out.open(fname_right, 'w'), im_format)
page_right.close()
page_left = im.crop((0, 0, split_point, im.size[1]))
page_left.save(zobj_out.open(fname_left, 'w'), im_format)
page_left.close()
else:
log.info("Copying non-image verbatim: %s", zinfo.filename)
zobj_out.writestr(zinfo, zobj_in.read(zinfo))
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