1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 13:38:46 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Alex Xu
8861299d24 bump 9.2.2 2025-11-04 13:08:02 -08:00
Alex Xu
636447bb62 Partially check W/B Margins if you don't want KCC to extend the image margins in CBZ/PDF (#1152) 2025-11-04 13:05:16 -08:00
Alex Xu
b23c7744cb Make webtoon tall error more informative (#1151)
* Make webtoon tall error more informative

* fix mistake
2025-11-04 12:54:29 -08:00
Alex Xu
2398a5b1ac raise max res to 6000x8000 (#1150) 2025-11-04 12:40:23 -08:00
José Cerezo
2b2ac8ff55 Added file fusion to C2E (#1149)
* Added file fusion to C2E

* Updated README.md
2025-11-02 19:01:02 -08:00
Alex Xu
5209d9a7b8 fix cover autocontrast (#1141) 2025-10-30 11:55:06 -07:00
Alex Xu
5336870097 fix webtoon too tall (#1146) 2025-10-30 11:54:18 -07:00
Alex Xu
4371d14391 bump 9.2.1 2025-10-26 15:45:23 -07:00
Alex Xu
f96b7cb22b further refine color detection 2025-10-26 14:11:13 -07:00
Alex Xu
4dfd2ea942 weird file structure message 2025-10-26 11:02:02 -07:00
Alex Xu
ba7f4336a5 add threshold comment 2025-10-25 23:16:52 -07:00
Alex Xu
9561b04bec make color detection super precise (#1137) 2025-10-25 23:14:53 -07:00
Alex Xu
2a8f8e9ab4 Fix png transparency (#1136)
* remove transparency

* pop transparency
2025-10-25 18:45:10 -07:00
Alex Xu
b9cef59912 remove pyinstaller-action 2025-10-24 21:12:07 -07:00
Alex Xu
f2ab730691 Fix tint color detection (#1135)
* make color detection even more precise

* fix tinted images
2025-10-24 20:55:18 -07:00
Alex Xu
44401583e4 fix typo 2025-10-24 10:58:03 -07:00
Alex Xu
28faf524c4 build windows command line versions directly using faster Python 3.11 (#1134)
* draft CLI

* fix windows c2e

* fix typos

* update github workflows
2025-10-24 10:52:31 -07:00
Alex Xu
2d288f72ea fix truncated file read (#1133) 2025-10-21 20:41:18 -07:00
Alex Xu
fb9b3c676b add panel view to faq 2025-10-21 14:17:40 -07:00
Alex Xu
cff1de4fa5 Remove LICENSE.txt from package-macos.yml files list
Removed LICENSE.txt from the files list in the workflow.
2025-10-21 14:00:54 -07:00
16 changed files with 166 additions and 134 deletions

View File

@@ -89,7 +89,6 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile
# TODO signing

View File

@@ -1,62 +0,0 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build KCC for windows with docker
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
jobs:
build:
strategy:
matrix:
entry: [ kcc-c2e, kcc-c2p ]
include:
- entry: kcc-c2e
capital: KCC_c2e
- entry: kcc-c2p
capital: KCC_c2p
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./${{ matrix.entry }}.spec
- name: rename binaries
run: |
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: windows-build-${{ matrix.entry }}
path: dist/windows/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.3
if: ${{ github.repository == 'ciromattia/kcc' }}
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/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
files: |
LICENSE.txt
dist/windows/*.exe

View File

@@ -23,6 +23,16 @@ on:
jobs:
build:
strategy:
matrix:
entry: [ kcc, kcc-c2e, kcc-c2p ]
include:
- entry: kcc
command: build_binary
- entry: kcc-c2e
command: build_c2e
- entry: kcc-c2p
command: build_c2p
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
@@ -40,12 +50,12 @@ jobs:
pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary
run: |
python setup.py build_binary
python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.3
@@ -65,5 +75,4 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
LICENSE.txt
dist/*.exe

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ dist/
build/
KindleComicConverter*.egg-info/
.idea/
.vscode/
win7
osx10.11
/venv/

View File

@@ -116,6 +116,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
- All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Kindle panel view not working?
- Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted?
@@ -272,6 +274,7 @@ OUTPUT SETTINGS:
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option.
--rotatefirst Put rotated spread first in spread splitter option.
--filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
CUSTOM PROFILE:

View File

@@ -416,7 +416,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Resolution of the target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum">
<number>3200</number>
<number>6000</number>
</property>
</widget>
</item>
@@ -442,7 +442,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Resolution of the target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="maximum">
<number>5120</number>
<number>8000</number>
</property>
</widget>
</item>

View File

@@ -522,7 +522,7 @@ class WorkerThread(QThread):
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
if self.kindlegenErrorCode[0] == 23026:
MW.addMessage.emit('Created EPUB file was too big.', 'error', False)
MW.addMessage.emit('Created EPUB file was too big. Weird file structure?', 'error', False)
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
False)
if self.kindlegenErrorCode[0] == 3221226505:
@@ -876,6 +876,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.chunkSizeCheckBox.setChecked(False)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
def stripTags(self, html):
s = HTMLStripper()

View File

@@ -221,7 +221,7 @@ class Ui_mainWindow(object):
self.widthBox = QSpinBox(self.customWidget)
self.widthBox.setObjectName(u"widthBox")
self.widthBox.setMaximum(3200)
self.widthBox.setMaximum(6000)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
@@ -234,7 +234,7 @@ class Ui_mainWindow(object):
self.heightBox = QSpinBox(self.customWidget)
self.heightBox.setObjectName(u"heightBox")
self.heightBox.setMaximum(5120)
self.heightBox.setMaximum(8000)
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)

View File

@@ -1,4 +1,4 @@
__version__ = '9.2.0'
__version__ = '9.2.2'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'

View File

@@ -71,12 +71,23 @@ def main(argv=None):
if len(sources) == 0:
print('No matching files found.')
return 1
if options.filefusion:
fusion_path = makeFusion(list(sources))
sources.clear()
sources.add(fusion_path)
for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(args)
options = checkOptions(options)
print('Working on ' + source + '...')
makeBook(source)
if options.filefusion:
for path in sources:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
return 0
@@ -1330,6 +1341,8 @@ def makeParser():
help="Disable autocontrast.")
output_options.add_argument("--colorautocontrast", action="store_true", dest="colorautocontrast", default=False,
help="Autocontrast color pages too. Skipped for pages without near blacks or whites.")
output_options.add_argument("--filefusion", action="store_true", dest="filefusion", default=False,
help="Combines all input files into a single file.")
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",

View File

@@ -24,10 +24,12 @@ import sys
from argparse import ArgumentParser
from shutil import rmtree
from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
from PIL.Image import Dither
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
ImageFile.LOAD_TRUNCATED_IMAGES = True
def mergeDirectoryTick(output):
if output:
@@ -60,8 +62,8 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072 * 2:
raise RuntimeError(f'Image too tall at {targetHeight} pixels.')
if targetHeight > 131072 * 3:
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
result = Image.new('RGB', (targetWidth, targetHeight))
y = 0
for i in imagesValid:

View File

@@ -24,13 +24,14 @@ import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
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
AUTO_CROP_THRESHOLD = 0.015
ImageFile.LOAD_TRUNCATED_IMAGES = True
class ProfileData:
@@ -296,9 +297,9 @@ class ComicPage:
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
# for better accuracy, you could split the image in half and analyze each half separately
def histograms_cutoff(self, cb, cr, cutoff=(2, 2)):
cb_hist = cb.histogram()
cr_hist = cr.histogram()
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
if cutoff == (0, 0):
return cb_hist, cr_hist
for h in cb_hist, cr_hist:
# get number of pixels
@@ -327,60 +328,50 @@ class ComicPage:
break
return cb_hist, cr_hist
def color_precision(self, cb_hist_original, cr_hist_original, cutoff, diff_threshold):
cb_hist, cr_hist = self.histograms_cutoff(cb_hist_original.copy(), cr_hist_original.copy(), cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment, don't go lower than 7
SPREAD_THRESHOLD = 7
if self.opt.forcecolor:
if any([
cb_nonzero[0] > 128,
cr_nonzero[0] > 128,
cb_nonzero[-1] < 128,
cr_nonzero[-1] < 128,
]):
return True, True
elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return True, False
DIFF_THRESHOLD = diff_threshold
if any([
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
]):
return True, True
return False, None
def calculate_color(self):
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist_original = cb.histogram()
cr_hist_original = cr.histogram()
# get rid of some jpg compression
cutoff = (.2, .2)
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment
SPREAD_THRESHOLD = 5
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
# check for large amount of extreme colors
# 11 if too high. 10 is barely enough. If needed make it magnitude of both
DIFF_THRESHOLD = 10
if any([
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
]):
return True
# get ride of most jpg compression
cutoff = (2, 2)
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment
SPREAD_THRESHOLD = 5
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
# check for any amount of mild colors still remaining
DIFF_THRESHOLD = 6
if any([
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
]):
return True
else:
return False
# you can increase 22 but don't increase 10. 4 maybe can go higher
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
if done:
return decision
return False
def saveToDir(self):
try:
@@ -405,7 +396,7 @@ class ComicPage:
def save_with_codec(self, image, targetPath):
if self.opt.forcepng:
image.info["transparency"] = None
image.info.pop('transparency', None)
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
@@ -557,7 +548,7 @@ class Cover:
def process(self):
self.image = self.image.convert('RGB')
self.image = ImageOps.autocontrast(self.image)
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.crop_main_cover()

View File

@@ -1,8 +1,10 @@
from PIL import Image, ImageFilter, ImageOps
from PIL import Image, ImageFilter, ImageOps, ImageFile
import numpy as np
from typing import Literal
from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).

View File

@@ -1,7 +1,9 @@
from PIL import ImageOps, ImageFilter
from PIL import ImageOps, ImageFilter, ImageFile
import numpy as np
from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
Some assupmptions on the page number sizes

View File

@@ -1,5 +1,8 @@
import numpy as np
from PIL import Image
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
def fourier_transform_image(img):
"""

View File

@@ -8,6 +8,8 @@ Install as Python package:
Create EXE/APP:
python3 setup.py build_binary
python3 setup.py build_c2e
python3 setup.py build_c2p
"""
import os
@@ -57,10 +59,75 @@ class BuildBinaryCommand(setuptools.Command):
else:
sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2ECommand(setuptools.Command):
description = 'build binary c2e release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "KCC C2E" -c -s kcc-c2e.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_win7_legacy_' + VERSION + ' -c --noupx kcc-c2e.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_' + VERSION + ' -c --noupx kcc-c2e.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2e_linux_' + VERSION + ' kcc-c2e.py')
sys.exit(0)
else:
sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2PCommand(setuptools.Command):
description = 'build binary c2p release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -n "KCC C2P" -c -s kcc-c2p.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_win7_legacy_' + VERSION + ' -c --noupx kcc-c2p.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_' + VERSION + ' -c --noupx kcc-c2p.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2p_linux_' + VERSION + ' kcc-c2p.py')
sys.exit(0)
else:
sys.exit(0)
setuptools.setup(
cmdclass={
'build_binary': BuildBinaryCommand,
'build_c2e': BuildC2ECommand,
'build_c2p': BuildC2PCommand,
},
name=NAME,
version=VERSION,