mirror of
https://github.com/ciromattia/kcc
synced 2026-04-15 05:28:49 +00:00
Compare commits
28 Commits
revert-753
...
v7.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b7926366 | ||
|
|
92f3308e1c | ||
|
|
9e87ccef4e | ||
|
|
9a2a09eab9 | ||
|
|
88cf2fd21f | ||
|
|
7a3ed262b1 | ||
|
|
9e204aad76 | ||
|
|
e1f9d12676 | ||
|
|
24ab72fcbc | ||
|
|
4af6a75874 | ||
|
|
28b6188a3f | ||
|
|
f000af1207 | ||
|
|
299f916580 | ||
|
|
c39e403595 | ||
|
|
e787dd2897 | ||
|
|
01625904d1 | ||
|
|
5f8526da44 | ||
|
|
1159e737a0 | ||
|
|
5bbdb715e9 | ||
|
|
1a3cd6c916 | ||
|
|
e1e6d587f4 | ||
|
|
ca5c0bdd61 | ||
|
|
c6f491d27e | ||
|
|
c9ed3feef1 | ||
|
|
be147fe7e5 | ||
|
|
62ffa2bc80 | ||
|
|
11186d07c0 | ||
|
|
4b3cd6882a |
@@ -25,6 +25,11 @@ jobs:
|
||||
# - name: build binary
|
||||
# run: |
|
||||
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
path: .
|
||||
spec: ./kcc.spec
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
@@ -38,13 +43,28 @@ jobs:
|
||||
- name: rename binaries
|
||||
run: |
|
||||
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
|
||||
mv dist/windows/kcc.exe dist/windows/KCC_${version_built}.exe
|
||||
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${version_built}.exe
|
||||
mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
|
||||
- name: upload build
|
||||
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: dist/windows/*.exe
|
||||
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v1.1
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
|
||||
project-slug: 'kcc'
|
||||
signing-policy-slug: 'release-signing'
|
||||
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/windows/signed/'
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
@@ -54,4 +74,4 @@ jobs:
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
dist/windows/*.exe
|
||||
dist/windows/signed/*.exe
|
||||
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
language: generic
|
||||
osx_image: xcode11.1
|
||||
|
||||
before_install:
|
||||
- pip3 install --upgrade pip setuptools wheel
|
||||
|
||||
install:
|
||||
- pip3 install -r requirements.txt
|
||||
- pip3 install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip
|
||||
- npm install -g appdmg
|
||||
|
||||
script: python3 setup.py build_binary
|
||||
|
||||
before_deploy:
|
||||
- shopt -s extglob
|
||||
- rm -r dist/!(*.deb|*.dmg)
|
||||
|
||||
deploy:
|
||||
provider: gcs
|
||||
access_key_id: GOOG1EC62457RKUYFR2TIZUWV4EFSV2EP5LVLPPFXUAKADWJFDYPFW63BQSLA
|
||||
secret_access_key:
|
||||
secure: sxYjeho7U3im0Ezf6cz6TjYDiLvf0kAM2ETQHYoFNbD1VVvhJJyymDCnPH80zpFKmhc1MWTB6ndwsrPfcyZDLR2meSdWGPjZfFPY3RcrfImndKi7ln+mYQDBQ7W1lGit4YcH3Ju7LHceaTbRA7fVTX8pWKOcbXL2oM+lQxTJHH32+crVma+ChhbjzTWsSLRoakt3Nhiveec5p/qSW7AFe4Zq+b3C85IgwjSJI/xVwzaWrs6p915h1zZi7KL7YCMIxfQFrvRPFR2KTbh/DoLCCrqfbD4qh0PVy1li51Ac3hd/u3foiNnTNchzgE3Nv/nbKmtFU6huuLNgzkQGuLA+yn7mKYzBwA3ZmFgoimdH9+yRCMkZ8B5VHpvfN1hgpJcyEl1T98Kv4cdtRYNB4w9iAMy1qSVxhjeI+2rjuWGoXro0lU6L4LIRCOruY3AuLCAKG8Qw5Ak9ksmIKBhZ9soxpoIwu/TYDUQkFj29IrUQucg9TEp7uAoxu8/7EHxB7hWnBRaBAAQbMuIRg7yysT3FT0Os6SB0t9+RBsVMSPuIti9JJZ2Lu0uRI1+Se+g7ItzYtJoPhBJAzAa+J9OONj0RNj2z8Vq2oIBhH4z6b6zTRMVroos3cdfYl5qIKs9SQ7rmeHoPRROcqpCznsUZ/ESa4f2MewFU/7AYcEnCesZV4xg=
|
||||
bucket: kcc-deploy
|
||||
local-dir: dist
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: AcidWeb/KCC
|
||||
@@ -1,8 +1,9 @@
|
||||
ISC LICENSE
|
||||
|
||||
Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||
Copyright (c) 2012-2025 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||
Copyright (c) 2013-2019 Paweł Jastrzębski <pawelj@iosphe.re>
|
||||
Copyright (c) 2021-2023 Darodi
|
||||
Copyright (c) 2021-2023 Darodi (https://github.com/darodi)
|
||||
Copyright (c) 2023-2025 Alex Xu (https://github.com/axu2)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted, provided that the
|
||||
|
||||
13
README.md
13
README.md
@@ -11,6 +11,8 @@ It was initially developed for Kindle but since version 4.6 it outputs valid EPU
|
||||
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
|
||||
It can also optionally optimize images by applying a number of transformations.
|
||||
|
||||

|
||||
|
||||
### A word of warning
|
||||
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
|
||||
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
|
||||
@@ -31,6 +33,9 @@ If you find **KCC** valuable you can consider donating to the authors:
|
||||
- Alex Xu (active 2023-Present)
|
||||
- [](https://www.paypal.com/donate/?business=QFJVE7A6LCP6U&no_recurring=0&item_name=Kindle+Comic+Converter¤cy_code=USD)
|
||||
|
||||
## Sponsors
|
||||
|
||||
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
||||
|
||||
## DOWNLOADS
|
||||
|
||||
@@ -68,9 +73,11 @@ If you have issues detecting it, get stuck on the MOBI conversion step, or use L
|
||||
|
||||
### 7-Zip
|
||||
|
||||
This is only required for certain files and advanced features.
|
||||
This is optional but will make conversions much faster.
|
||||
|
||||
KCC will ask you to install if needed.
|
||||
This is required for certain files and advanced features.
|
||||
|
||||
KCC will ask you to install if needed.
|
||||
|
||||
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
|
||||
|
||||
@@ -317,5 +324,5 @@ The app relies and includes the following scripts:
|
||||
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||
|
||||
## COPYRIGHT
|
||||
Copyright (c) 2012-2023 Ciro Mattia Gonano, Paweł Jastrzębski and Darodi.
|
||||
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
|
||||
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
|
||||
|
||||
14
appveyor.yml
14
appveyor.yml
@@ -1,14 +0,0 @@
|
||||
environment:
|
||||
PYTHON: "C:\\Python37-x64"
|
||||
|
||||
install:
|
||||
- set PATH="%PYTHON%\\Scripts";%PATH%
|
||||
- "%PYTHON%\\python.exe -m pip install --upgrade pip setuptools wheel"
|
||||
- "%PYTHON%\\python.exe -m pip install -r requirements.txt"
|
||||
- "%PYTHON%\\python.exe -m pip install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip"
|
||||
|
||||
build_script:
|
||||
- "%PYTHON%\\python.exe setup.py build_binary"
|
||||
|
||||
artifacts:
|
||||
- path: dist\KCC*
|
||||
39
kcc.spec
Normal file
39
kcc.spec
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['kcc.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='kcc',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None , icon='icons\\comic2ebook.ico')
|
||||
@@ -37,7 +37,7 @@ from packaging.version import Version
|
||||
from raven import Client
|
||||
from tempfile import gettempdir
|
||||
|
||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
||||
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
|
||||
from . import __version__
|
||||
from . import comic2ebook
|
||||
from . import metadata
|
||||
@@ -317,13 +317,8 @@ class WorkerThread(QThread):
|
||||
GUI.progress.content = ''
|
||||
self.errors = True
|
||||
_, _, traceback = sys.exc_info()
|
||||
if len(err.args) == 1:
|
||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
||||
else:
|
||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||
% (jobargv[-1], str(err.args[0]), err.args[1]), 'error')
|
||||
GUI.sentry.extra_context({'realTraceback': err.args[1]})
|
||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
||||
if ' is corrupted.' not in str(err):
|
||||
GUI.sentry.captureException()
|
||||
MW.addMessage.emit('Error during conversion! Please consult '
|
||||
@@ -1071,19 +1066,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.addMessage('Since you are a new user of <b>KCC</b> please see few '
|
||||
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
||||
'info')
|
||||
try:
|
||||
subprocess_run(['tar'], stdout=PIPE, stderr=STDOUT)
|
||||
self.tar = True
|
||||
except FileNotFoundError:
|
||||
self.tar = False
|
||||
try:
|
||||
subprocess_run(['7z'], stdout=PIPE, stderr=STDOUT)
|
||||
self.sevenzip = True
|
||||
except FileNotFoundError:
|
||||
self.sevenzip = False
|
||||
if not self.tar:
|
||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
|
||||
|
||||
self.tar = 'tar' in available_archive_tools()
|
||||
self.sevenzip = '7z' in available_archive_tools()
|
||||
if not any([self.tar, self.sevenzip]):
|
||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
|
||||
self.detectKindleGen(True)
|
||||
|
||||
APP.messageFromOtherInstance.connect(self.handleMessage)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '7.2.1'
|
||||
__version__ = '7.3.1'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -23,7 +23,7 @@ import pathlib
|
||||
import re
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from time import strftime, gmtime
|
||||
from time import perf_counter, strftime, gmtime
|
||||
from copy import copy
|
||||
from glob import glob, escape
|
||||
from re import sub
|
||||
@@ -33,14 +33,14 @@ from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||
from shutil import move, copytree, rmtree, copyfile
|
||||
from multiprocessing import Pool
|
||||
from uuid import uuid4
|
||||
from natsort import os_sorted
|
||||
from natsort import os_sort_keygen
|
||||
from slugify import slugify as slugify_ext
|
||||
from PIL import Image, ImageFile
|
||||
from subprocess import STDOUT, PIPE
|
||||
from psutil import virtual_memory, disk_usage
|
||||
from html import escape as hescape
|
||||
|
||||
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
|
||||
from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
|
||||
from . import comic2panel
|
||||
from . import image
|
||||
from . import comicarchive
|
||||
@@ -51,7 +51,7 @@ from . import kindle
|
||||
from . import __version__
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
OS_SORT_KEY = os_sort_keygen()
|
||||
|
||||
def main(argv=None):
|
||||
global options
|
||||
@@ -77,15 +77,14 @@ def main(argv=None):
|
||||
return 0
|
||||
|
||||
|
||||
def buildHTML(path, imgfile, imgfilepath):
|
||||
imgfilepath = md5Checksum(imgfilepath)
|
||||
def buildHTML(path, imgfile):
|
||||
filename = getImageFileName(imgfile)
|
||||
deviceres = options.profileData[1]
|
||||
if not options.noprocessing and "Rotated" in options.imgMetadata[imgfilepath]:
|
||||
if not options.noprocessing and "Rotated" in imgfile:
|
||||
rotatedPage = True
|
||||
else:
|
||||
rotatedPage = False
|
||||
if not options.noprocessing and "BlackBackground" in options.imgMetadata[imgfilepath]:
|
||||
if not options.noprocessing and "BlackBackground" in imgfile:
|
||||
additionalStyle = 'background-color:#000000;'
|
||||
else:
|
||||
additionalStyle = ''
|
||||
@@ -425,7 +424,6 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
||||
"</container>"])
|
||||
f.close()
|
||||
|
||||
|
||||
def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||
filelist = []
|
||||
chapterlist = []
|
||||
@@ -506,6 +504,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||
"display: none;\n",
|
||||
"}\n"])
|
||||
f.close()
|
||||
build_html_start = perf_counter()
|
||||
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
chapter = False
|
||||
dirnames, filenames = walkSort(dirnames, filenames)
|
||||
@@ -515,10 +514,12 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||
'cover' + getImageFileName(afile)[1])
|
||||
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
|
||||
tomenumber), options.uuid))
|
||||
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
|
||||
if not chapter:
|
||||
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
|
||||
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
|
||||
chapter = True
|
||||
filelist.append(buildHTML(dirpath, afile))
|
||||
build_html_end = perf_counter()
|
||||
print(f"buildHTML: {build_html_end - build_html_start} seconds")
|
||||
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
|
||||
if not chapternames and options.chapters and not ischunked:
|
||||
chapterlist = []
|
||||
@@ -539,7 +540,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||
global_diff = 0
|
||||
|
||||
for x in range(0, pageid + cur_diff + 1):
|
||||
if '-kcc-b' in filelist[x][1]:
|
||||
if '-KCC-B' in filelist[x][1]:
|
||||
pageid += diff_delta
|
||||
global_diff += diff_delta
|
||||
|
||||
@@ -555,8 +556,6 @@ def imgDirectoryProcessing(path):
|
||||
global workerPool, workerOutput
|
||||
workerPool = Pool(maxtasksperchild=100)
|
||||
workerOutput = []
|
||||
options.imgMetadata = {}
|
||||
options.imgOld = []
|
||||
work = []
|
||||
pagenumber = 0
|
||||
for dirpath, _, filenames in os.walk(path):
|
||||
@@ -566,19 +565,19 @@ def imgDirectoryProcessing(path):
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit(str(pagenumber))
|
||||
if len(work) > 0:
|
||||
img_processing_start = perf_counter()
|
||||
for i in work:
|
||||
workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick)
|
||||
workerPool.close()
|
||||
workerPool.join()
|
||||
img_processing_end = perf_counter()
|
||||
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
|
||||
if GUI and not GUI.conversionAlive:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(workerOutput) > 0:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
||||
for file in options.imgOld:
|
||||
if os.path.isfile(file):
|
||||
os.remove(file)
|
||||
else:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise UserWarning("Source directory is empty.")
|
||||
@@ -588,11 +587,7 @@ def imgFileProcessingTick(output):
|
||||
if isinstance(output, tuple):
|
||||
workerOutput.append(output)
|
||||
workerPool.terminate()
|
||||
else:
|
||||
for page in output:
|
||||
if page is not None:
|
||||
options.imgMetadata[page[0]] = page[1]
|
||||
options.imgOld.append(page[2])
|
||||
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('tick')
|
||||
if not GUI.conversionAlive:
|
||||
@@ -657,16 +652,14 @@ def getWorkFolder(afile):
|
||||
path = cbx.extract(workdir)
|
||||
sanitizePermissions(path)
|
||||
tdir = os.listdir(workdir)
|
||||
is_nested_single_dir = False
|
||||
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
|
||||
tdir.remove('ComicInfo.xml')
|
||||
is_nested_single_dir = os.path.isdir(os.path.join(workdir, tdir[0]))
|
||||
if is_nested_single_dir:
|
||||
if os.path.isdir(os.path.join(workdir, tdir[0])):
|
||||
os.replace(
|
||||
os.path.join(workdir, 'ComicInfo.xml'),
|
||||
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
|
||||
)
|
||||
if len(tdir) == 1 and is_nested_single_dir:
|
||||
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
|
||||
path = os.path.join(workdir, tdir[0])
|
||||
except OSError as e:
|
||||
rmtree(workdir, True)
|
||||
@@ -674,8 +667,7 @@ def getWorkFolder(afile):
|
||||
else:
|
||||
raise UserWarning("Failed to open source file/directory.")
|
||||
newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
copytree(path, os.path.join(newpath, 'OEBPS', 'Images'))
|
||||
rmtree(workdir, True)
|
||||
os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
|
||||
return newpath
|
||||
|
||||
|
||||
@@ -739,9 +731,7 @@ def getComicInfo(path, originalpath):
|
||||
except Exception:
|
||||
os.remove(xmlPath)
|
||||
return
|
||||
if xml.data['Title']:
|
||||
options.title = hescape(xml.data['Title'])
|
||||
elif defaultTitle:
|
||||
if defaultTitle:
|
||||
if xml.data['Series']:
|
||||
options.title = hescape(xml.data['Series'])
|
||||
if xml.data['Volume']:
|
||||
@@ -793,22 +783,22 @@ def getPanelViewSize(deviceres, size):
|
||||
|
||||
def sanitizeTree(filetree):
|
||||
chapterNames = {}
|
||||
for root, dirs, files in os.walk(filetree, False):
|
||||
for i, name in enumerate(os_sorted(files)):
|
||||
page = 1
|
||||
for root, dirs, files in os.walk(filetree):
|
||||
dirs.sort(key=OS_SORT_KEY)
|
||||
files.sort(key=OS_SORT_KEY)
|
||||
for name in files:
|
||||
splitname = os.path.splitext(name)
|
||||
|
||||
# file needs kcc at front AND back to avoid renaming issues
|
||||
slugified = f'kcc-{i:04}'
|
||||
for suffix in '-KCC', '-KCC-A', '-KCC-B', '-KCC-C':
|
||||
if splitname[0].endswith(suffix):
|
||||
slugified += suffix.lower()
|
||||
break
|
||||
# 9999 page limit
|
||||
slugified = f'kcc-{page:04}'
|
||||
page += 1
|
||||
|
||||
newKey = os.path.join(root, slugified + splitname[1])
|
||||
key = os.path.join(root, name)
|
||||
if key != newKey:
|
||||
os.replace(key, newKey)
|
||||
for name in dirs:
|
||||
for i, name in enumerate(dirs):
|
||||
tmpName = name
|
||||
slugified = slugify(name)
|
||||
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
|
||||
@@ -818,6 +808,7 @@ def sanitizeTree(filetree):
|
||||
key = os.path.join(root, name)
|
||||
if key != newKey:
|
||||
os.replace(key, newKey)
|
||||
dirs[i] = newKey
|
||||
return chapterNames
|
||||
|
||||
|
||||
@@ -890,8 +881,7 @@ def chunk_process(path, mode, parent):
|
||||
firstTome = False
|
||||
return output
|
||||
|
||||
|
||||
def detectCorruption(tmppath, orgpath):
|
||||
def detectSuboptimalProcessing(tmppath, orgpath):
|
||||
imageNumber = 0
|
||||
imageSmaller = 0
|
||||
alreadyProcessed = False
|
||||
@@ -907,9 +897,6 @@ def detectCorruption(tmppath, orgpath):
|
||||
raise RuntimeError('Image file %s is corrupted.' % pathOrg)
|
||||
try:
|
||||
img = Image.open(path)
|
||||
img.verify()
|
||||
img = Image.open(path)
|
||||
img.load()
|
||||
imageNumber += 1
|
||||
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
|
||||
imageSmaller += 1
|
||||
@@ -953,17 +940,27 @@ def slugify(value):
|
||||
|
||||
|
||||
def makeZIP(zipfilename, basedir, isepub=False):
|
||||
start = perf_counter()
|
||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||
if isepub:
|
||||
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
|
||||
for dirpath, _, filenames in os.walk(basedir):
|
||||
for name in filenames:
|
||||
path = os.path.normpath(os.path.join(dirpath, name))
|
||||
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name))
|
||||
if os.path.isfile(path):
|
||||
zipOutput.write(path, aPath)
|
||||
zipOutput.close()
|
||||
if '7z' in available_archive_tools():
|
||||
if isepub:
|
||||
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
||||
mimetypeFile.write('application/epub+zip')
|
||||
mimetypeFile.close()
|
||||
subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
|
||||
else:
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||
if isepub:
|
||||
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
|
||||
for dirpath, _, filenames in os.walk(basedir):
|
||||
for name in filenames:
|
||||
path = os.path.normpath(os.path.join(dirpath, name))
|
||||
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name))
|
||||
if os.path.isfile(path):
|
||||
zipOutput.write(path, aPath)
|
||||
zipOutput.close()
|
||||
end = perf_counter()
|
||||
print(f"makeZIP time: {end - start} seconds")
|
||||
return zipfilename
|
||||
|
||||
|
||||
@@ -1138,9 +1135,7 @@ def checkTools(source):
|
||||
source = source.upper()
|
||||
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
|
||||
source.endswith('.ZIP') or source.endswith('.CBZ'):
|
||||
try:
|
||||
subprocess_run(['7z'], stdout=PIPE, stderr=STDOUT)
|
||||
except FileNotFoundError:
|
||||
if '7z' not in available_archive_tools():
|
||||
print('ERROR: 7z is missing!')
|
||||
sys.exit(1)
|
||||
if options.format == 'MOBI':
|
||||
@@ -1170,6 +1165,7 @@ def checkPre(source):
|
||||
|
||||
|
||||
def makeBook(source, qtgui=None):
|
||||
start = perf_counter()
|
||||
global GUI
|
||||
GUI = qtgui
|
||||
if GUI:
|
||||
@@ -1181,7 +1177,8 @@ def makeBook(source, qtgui=None):
|
||||
path = getWorkFolder(source)
|
||||
print("Checking images...")
|
||||
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
|
||||
detectCorruption(os.path.join(path, "OEBPS", "Images"), source)
|
||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
if options.webtoon:
|
||||
y = image.ProfileData.Profiles[options.profile][1][1]
|
||||
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
|
||||
@@ -1194,7 +1191,6 @@ def makeBook(source, qtgui=None):
|
||||
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('1')
|
||||
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
if options.batchsplit > 0:
|
||||
tomes = chunk_directory(path)
|
||||
else:
|
||||
@@ -1272,6 +1268,8 @@ def makeBook(source, qtgui=None):
|
||||
elif os.path.isdir(source):
|
||||
rmtree(source)
|
||||
|
||||
end = perf_counter()
|
||||
print(f"makeBook: {end - start} seconds")
|
||||
return filepath
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ class ComicArchive:
|
||||
['unar', self.filepath, '-f', '-o', targetdir]
|
||||
)
|
||||
|
||||
extraction_commands.reverse()
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
extraction_commands.append(
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import mozjpeg_lossless_optimization
|
||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
||||
from .shared import md5Checksum
|
||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||
|
||||
@@ -141,7 +141,13 @@ class ComicPageParser:
|
||||
self.source = source
|
||||
self.size = self.opt.profileData[1]
|
||||
self.payload = []
|
||||
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
|
||||
|
||||
# Detect corruption in source image, let caller catch any exceptions triggered.
|
||||
srcImgPath = os.path.join(source[0], source[1])
|
||||
self.image = Image.open(srcImgPath)
|
||||
self.image.verify()
|
||||
self.image = Image.open(srcImgPath).convert('RGB')
|
||||
|
||||
self.color = self.colorCheck()
|
||||
self.fill = self.fillCheck()
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
@@ -293,13 +299,12 @@ class ComicPage:
|
||||
|
||||
def saveToDir(self):
|
||||
try:
|
||||
flags = []
|
||||
if not self.opt.forcecolor and not self.opt.forcepng:
|
||||
self.image = self.image.convert('L')
|
||||
if self.rotated:
|
||||
flags.append('Rotated')
|
||||
self.targetPath += '-Rotated'
|
||||
if self.fill != 'white':
|
||||
flags.append('BlackBackground')
|
||||
self.targetPath += '-BlackBackground'
|
||||
if self.opt.forcepng:
|
||||
self.image.info["transparency"] = None
|
||||
self.targetPath += '.png'
|
||||
@@ -315,7 +320,9 @@ class ComicPage:
|
||||
output_jpeg_file.write(output_jpeg_bytes)
|
||||
else:
|
||||
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
|
||||
return [md5Checksum(self.targetPath), flags, self.orgPath]
|
||||
if os.path.isfile(self.orgPath):
|
||||
os.remove(self.orgPath)
|
||||
return Path(self.targetPath).name
|
||||
except IOError as err:
|
||||
raise RuntimeError('Cannot save image. ' + str(err))
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
from functools import lru_cache
|
||||
import os
|
||||
from hashlib import md5
|
||||
from html.parser import HTMLParser
|
||||
@@ -74,16 +75,6 @@ def walkLevel(some_dir, level=1):
|
||||
del dirs[:]
|
||||
|
||||
|
||||
def md5Checksum(fpath):
|
||||
with open(fpath, 'rb') as fh:
|
||||
m = md5()
|
||||
while True:
|
||||
data = fh.read(8192)
|
||||
if not data:
|
||||
break
|
||||
m.update(data)
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
def sanitizeTrace(traceback):
|
||||
return ''.join(format_tb(traceback))\
|
||||
@@ -137,6 +128,19 @@ def dependencyCheck(level):
|
||||
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
||||
sys.exit(1)
|
||||
|
||||
@lru_cache
|
||||
def available_archive_tools():
|
||||
available = []
|
||||
|
||||
for tool in ['tar', '7z', 'unar', 'unrar']:
|
||||
try:
|
||||
subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
available.append(tool)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return available
|
||||
|
||||
def subprocess_run(command, **kwargs):
|
||||
if (os.name == 'nt'):
|
||||
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
|
||||
|
||||
Reference in New Issue
Block a user