mirror of
https://github.com/ciromattia/kcc
synced 2026-04-15 13:38:46 +00:00
Compare commits
19 Commits
v9.4.3
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc808c6215 | ||
|
|
b42f05686e | ||
|
|
adf48d24f9 | ||
|
|
723fa4c0b8 | ||
|
|
2632d18e2c | ||
|
|
94e4937566 | ||
|
|
f7ce1cf271 | ||
|
|
ab93c03838 | ||
|
|
541b1d876b | ||
|
|
d189f9909d | ||
|
|
1dce4f8d2c | ||
|
|
58aab0cb65 | ||
|
|
d2dc089c62 | ||
|
|
3660f2370f | ||
|
|
87c6e3a35e | ||
|
|
981c556550 | ||
|
|
123d603cbd | ||
|
|
a344dd73bf | ||
|
|
095694e9cf |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
13
README.md
13
README.md
@@ -13,6 +13,11 @@ Pages display in fullscreen without margins,
|
||||
with proper fixed layout support.
|
||||
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||
KCC runs on Windows, macOS, and Linux.
|
||||
|
||||
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
|
||||
|
||||
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
|
||||
|
||||
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
|
||||
|
||||
@@ -36,6 +41,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
|
||||
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
|
||||
4) incorrect page turn direction for manga that's read right to left
|
||||
5) unaligned two page spreads in landscape, where pages are shifted over by 1
|
||||
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
|
||||
|
||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
||||
|
||||
@@ -48,7 +54,9 @@ You can change the default output directory by holding `Shift` while clicking th
|
||||
Then just drag and drop the generated output files onto your device's documents folder via USB.
|
||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
|
||||
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
|
||||
|
||||
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
|
||||
### 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.
|
||||
@@ -100,7 +108,7 @@ There are also legacy macOS 10.14+ and Windows 7 experimental versions available
|
||||
|
||||
The `c2e` and `c2p` versions are command line tools for power users.
|
||||
|
||||
On Mac, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
|
||||
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
|
||||
|
||||
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||
|
||||
@@ -261,6 +269,7 @@ PROCESSING:
|
||||
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
|
||||
--blackborders Disable autodetection and force black borders
|
||||
--whiteborders Disable autodetection and force white borders
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--mozjpeg Create JPEG files using mozJpeg
|
||||
|
||||
14
gui/KCC.ui
14
gui/KCC.ui
@@ -473,7 +473,7 @@
|
||||
<item>
|
||||
<widget class="QSpinBox" name="chunkSizeBox">
|
||||
<property name="minimum">
|
||||
<number>100</number>
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>600</number>
|
||||
@@ -908,6 +908,17 @@ Useful for really weird PDFs.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QCheckBox" name="coverFillBox">
|
||||
<property name="toolTip">
|
||||
<string>Resize cover to exact device resolution by center-cropping to aspect ratio first.
|
||||
May crop top/bottom or left/right depending on source aspect ratio. Not implemented for Kindle Scribe.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cover Fill</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -1037,6 +1048,7 @@ Useful for really weird PDFs.</string>
|
||||
<tabstop>noRotateBox</tabstop>
|
||||
<tabstop>interPanelCropBox</tabstop>
|
||||
<tabstop>metadataTitleBox</tabstop>
|
||||
<tabstop>coverFillBox</tabstop>
|
||||
<tabstop>chunkSizeCheckBox</tabstop>
|
||||
<tabstop>chunkSizeBox</tabstop>
|
||||
<tabstop>eraseRainbowBox</tabstop>
|
||||
|
||||
@@ -42,7 +42,7 @@ from raven import Client
|
||||
from tempfile import gettempdir
|
||||
|
||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
||||
from .comicarchive import SEVENZIP, available_archive_tools
|
||||
from .comicarchive import SEVENZIP, TAR, available_archive_tools
|
||||
from . import __version__
|
||||
from . import comic2ebook
|
||||
from . import metadata
|
||||
@@ -329,6 +329,8 @@ class WorkerThread(QThread):
|
||||
options.noprocessing = True
|
||||
if GUI.pdfExtractBox.isChecked():
|
||||
options.pdfextract = True
|
||||
if GUI.coverFillBox.isChecked():
|
||||
options.coverfill = True
|
||||
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
options.metadatatitle = 1
|
||||
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
|
||||
@@ -523,6 +525,7 @@ class WorkerThread(QThread):
|
||||
if os.path.exists(item.replace('.epub', '.mobi')):
|
||||
os.remove(item.replace('.epub', '.mobi'))
|
||||
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
|
||||
MW.addMessage.emit(self.kindlegenErrorCode[1], 'error', False)
|
||||
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
|
||||
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
|
||||
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
|
||||
@@ -1035,9 +1038,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
|
||||
'jpegQuality': GUI.jpegQualitySpinBox.value(),
|
||||
'widthBox': GUI.widthBox.value(),
|
||||
'heightBox': GUI.heightBox.value(),
|
||||
'deleteBox': GUI.deleteBox.checkState(),
|
||||
@@ -1089,7 +1094,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if message[-1] == '/':
|
||||
message = message[:-1]
|
||||
self.handleMessage(message)
|
||||
GUI.jobList.sortItems()
|
||||
# sorting may conflict with manual file fusion order
|
||||
# GUI.jobList.sortItems()
|
||||
|
||||
def forceShutdown(self):
|
||||
self.saveSettings(None)
|
||||
@@ -1358,7 +1364,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
||||
'info')
|
||||
|
||||
self.tar = 'tar' in available_archive_tools()
|
||||
self.tar = TAR in available_archive_tools()
|
||||
self.sevenzip = SEVENZIP 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>'
|
||||
@@ -1440,6 +1446,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.croppingPowerSlider.setValue(int(self.options[option]))
|
||||
self.changeCroppingPower(int(self.options[option]))
|
||||
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
|
||||
elif str(option) == "jpegQuality":
|
||||
GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
|
||||
elif str(option) == "chunkSizeBox":
|
||||
GUI.chunkSizeBox.setValue(int(self.options[option]))
|
||||
else:
|
||||
|
||||
@@ -266,7 +266,7 @@ class Ui_mainWindow(object):
|
||||
|
||||
self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
|
||||
self.chunkSizeBox.setObjectName(u"chunkSizeBox")
|
||||
self.chunkSizeBox.setMinimum(100)
|
||||
self.chunkSizeBox.setMinimum(50)
|
||||
self.chunkSizeBox.setMaximum(600)
|
||||
self.chunkSizeBox.setValue(400)
|
||||
|
||||
@@ -467,6 +467,11 @@ class Ui_mainWindow(object):
|
||||
|
||||
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
|
||||
|
||||
self.coverFillBox = QCheckBox(self.optionWidget)
|
||||
self.coverFillBox.setObjectName(u"coverFillBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.coverFillBox, 9, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||
|
||||
@@ -549,7 +554,8 @@ class Ui_mainWindow(object):
|
||||
QWidget.setTabOrder(self.fileFusionBox, self.noRotateBox)
|
||||
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
||||
QWidget.setTabOrder(self.interPanelCropBox, self.metadataTitleBox)
|
||||
QWidget.setTabOrder(self.metadataTitleBox, self.chunkSizeCheckBox)
|
||||
QWidget.setTabOrder(self.metadataTitleBox, self.coverFillBox)
|
||||
QWidget.setTabOrder(self.coverFillBox, self.chunkSizeCheckBox)
|
||||
QWidget.setTabOrder(self.chunkSizeCheckBox, self.chunkSizeBox)
|
||||
QWidget.setTabOrder(self.chunkSizeBox, self.eraseRainbowBox)
|
||||
QWidget.setTabOrder(self.eraseRainbowBox, self.rotateFirstBox)
|
||||
@@ -744,6 +750,11 @@ class Ui_mainWindow(object):
|
||||
"Useful for really weird PDFs.", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.coverFillBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resize cover to exact device resolution by center-cropping to aspect ratio first.\n"
|
||||
"May crop top/bottom or left/right depending on source aspect ratio. Not implemented for Kindle Scribe.", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.coverFillBox.setText(QCoreApplication.translate("mainWindow", u"Cover Fill", None))
|
||||
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
|
||||
self.jpegQualityLabel.setText(QCoreApplication.translate("mainWindow", u"JPEG Quality:", None))
|
||||
# retranslateUi
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '9.4.3'
|
||||
__version__ = '9.5.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -660,7 +660,7 @@ def imgDirectoryProcessing(path, job_progress=''):
|
||||
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])
|
||||
raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
||||
else:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise UserWarning("C2E: Source directory is empty.")
|
||||
@@ -963,6 +963,13 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
|
||||
counter += 1
|
||||
filename = basename + '_kcc' + str(counter) + ext
|
||||
elif options.format == 'MOBI' and ext == '.epub':
|
||||
counter = 0
|
||||
basename = os.path.splitext(filename)[0]
|
||||
if os.path.isfile(basename + '.mobi'):
|
||||
while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
|
||||
counter += 1
|
||||
filename = basename + '_kcc' + str(counter) + ext
|
||||
return filename
|
||||
|
||||
|
||||
@@ -1275,10 +1282,13 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||
if SEVENZIP in available_archive_tools():
|
||||
if isepub:
|
||||
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
||||
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||
mimetypeFile.write('application/epub+zip')
|
||||
mimetypeFile.close()
|
||||
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
|
||||
# crazy hack to ensure mimetype is first when using 7zip
|
||||
if isepub:
|
||||
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
|
||||
else:
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||
if isepub:
|
||||
@@ -1350,6 +1360,8 @@ def makeParser():
|
||||
help="Do not modify image and ignore any profile or processing option")
|
||||
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
|
||||
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
|
||||
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
|
||||
help="Crop cover to fill screen")
|
||||
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||
help="Resize images smaller than device's resolution")
|
||||
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
||||
@@ -1423,6 +1435,8 @@ def checkOptions(options):
|
||||
options.format = 'MOBI'
|
||||
if options.batchsplit != 2:
|
||||
options.batchsplit = 1
|
||||
if not options.targetsize and options.profile.startswith('Rmk'):
|
||||
options.targetsize = 95
|
||||
if options.format == 'MOBI+EPUB':
|
||||
options.keep_epub = True
|
||||
options.format = 'MOBI'
|
||||
@@ -1547,15 +1561,24 @@ def makeFusion(sources: List[str]):
|
||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||
print("Running Fusion")
|
||||
|
||||
for source in sources:
|
||||
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||
path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
|
||||
needs_prefix = os_sorted(path_names) != path_names
|
||||
|
||||
for index, source in enumerate(sources, start=1):
|
||||
print(f"Processing {source}...")
|
||||
checkPre(source)
|
||||
print("Checking images...")
|
||||
source_path = Path(source)
|
||||
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||
prefix = ''
|
||||
if needs_prefix:
|
||||
prefix = f'fusion_{index:04d}_'
|
||||
if source_path.is_file():
|
||||
targetpath = fusion_path.joinpath(source_path.stem)
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
|
||||
else:
|
||||
targetpath = fusion_path.joinpath(source_path.name)
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
|
||||
|
||||
getWorkFolder(source, str(targetpath))
|
||||
sanitizeTree(targetpath, prefix='fusion')
|
||||
# TODO: remove flattenTree when subchapters are supported
|
||||
@@ -1584,6 +1607,9 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
if options.filefusion:
|
||||
# Strip the fusion_0001_ sort prefix from makeFusion if present
|
||||
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
|
||||
cover = None
|
||||
if not options.webtoon:
|
||||
cover = image.Cover(cover_path, options)
|
||||
@@ -1775,4 +1801,3 @@ def makeMOBI(work, qtgui=None):
|
||||
makeMOBIWorkerPool.close()
|
||||
makeMOBIWorkerPool.join()
|
||||
return makeMOBIWorkerOutput
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from .shared import IMAGE_TYPES, subprocess_run
|
||||
|
||||
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
||||
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
||||
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
|
||||
|
||||
|
||||
class ComicArchive:
|
||||
@@ -73,7 +74,7 @@ class ComicArchive:
|
||||
missing = []
|
||||
|
||||
extraction_commands = [
|
||||
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
|
||||
[TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
|
||||
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
|
||||
]
|
||||
|
||||
@@ -125,7 +126,7 @@ class ComicArchive:
|
||||
def available_archive_tools():
|
||||
available = []
|
||||
|
||||
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
|
||||
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
|
||||
try:
|
||||
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
||||
available.append(tool)
|
||||
|
||||
@@ -569,7 +569,11 @@ class Cover:
|
||||
if self.options.kindle_scribe_azw3:
|
||||
size[0] = min(size[0], 1920)
|
||||
size[1] = min(size[1], 1920)
|
||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||
if self.options.coverfill and not self.options.kindle_scribe_azw3:
|
||||
# TODO: Kindle Scribe case
|
||||
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
|
||||
else:
|
||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||
|
||||
def crop_main_cover(self):
|
||||
w, h = self.image.size
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
PySide6<6.10
|
||||
PySide6>6
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
|
||||
Reference in New Issue
Block a user