Compare commits

..
Author SHA1 Message Date
dependabot[bot] 1d9758d4bd Bump signpath/github-action-submit-signing-request from 2.2 to 2.3
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2.2 to 2.3.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v2.2...v2.3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 01:44:07 +00:00
Alex Xu 3e6cbcb91d Update README.md 2026-08-16 07:26:16 -07:00
Alex Xu 2bae809e5f kindlegen not needed for koreader users 2026-08-16 07:25:34 -07:00
Alex Xu bd0328a12f bump to 11.0.1 2026-08-02 10:47:25 -07:00
Alex Xu 846a755761 fix label feature on Windows (#1413) 2026-08-01 15:58:45 -07:00
Alex Xu ff80d275e6 bump to 11.0.0 2026-07-31 22:26:11 -07:00
Alex Xu b3e1d393b6 Manually label spreads button (#1412)
* initial commit

* initial commit 2

* json it

* UI labels

* basics

* add todo

* options to getWorkFolder

* refactor options

* refactor UI

* cleanup

* pdf fixed size

* remove comment

* finalized json creation

* fix help text

* finalized

* remove print

* add youtube
2026-07-31 22:25:41 -07:00
Alex Xu b4c71b3b91 pass options to slugify (#1411) 2026-07-29 17:06:52 -07:00
Alex Xu 7e412f12ae pass options to getWorkFolder (#1410) 2026-07-29 16:41:34 -07:00
Nils Leo 2076775d94 Create the destination dir before flattening a single-subfolder archive (#1405) 2026-07-23 22:43:16 -07:00
Alex Xu 59df22b62d add sort order note to readme 2026-07-20 15:06:12 -07:00
Alex Xu ff5edc5ce5 fix: Destination path... already exists (#1404) 2026-07-20 14:21:42 -07:00
Alex Xu a52c0ce03a Merge pull request #1394 from ciromattia/axu2-patch-1
Clarify Kobo profile + EPUB -> KEPUB
2026-07-10 18:42:03 -07:00
Alex Xu 1c0506fe7d Clarify Kobo profile + EPUB -> KEPUB 2026-07-10 18:41:48 -07:00
Alex Xu 806c6232c3 bump to 10.4.0 2026-07-10 18:39:41 -07:00
Alex Xu 6f9f5067d8 Merge pull request #1384 from drshliapa/fix/912-preserve-comicinfo
add Keep ComicInfo.xml in CBZ output option
2026-07-10 18:38:05 -07:00
Alex Xu 11be0dffcc make it an option 2026-07-10 18:32:44 -07:00
Alex Xu 4c10569ab1 millisecond timestamp kindlegen log (#1393) 2026-07-10 17:47:34 -07:00
Alex Xu 2a5cd6718d new user first startup: default is Paperwhite 12th Gen, split and rotate, RTL (#1392) 2026-07-10 14:48:48 -07:00
Alex Xu fd1468904f stop webtoon unchecking automatically check customm autocontrast (#1391) 2026-07-10 14:26:41 -07:00
Alex Xu feec40ffc8 more detailed kindlegen warnings (#1390)
* show kindlegen warnings and errors

* show kindlegen warnings

* dump kindlegen logs to txt

* move warnings to end

* don't show webtoon cover warning

* fix typo

* add timestamp to kindlegen log dump

* double enter

* add TODO

* add more TODO

* fix comment

* final clean up
2026-07-10 13:57:20 -07:00
Marian Leontiev f6ae7661bc fix macOS app version showing 0.0.0 in Info.plist (#1385) 2026-07-09 15:25:42 -07:00
Alex Xu ca14a4c813 don't crash on empty nested folders (#1388) 2026-07-08 17:46:46 -07:00
Alex Xu 0c1071a358 fix Access is denied with read only input folder (2013 bug) (#1387)
* fix Access is denied on read only input folders

* minimize changes
2026-07-08 17:22:23 -07:00
Alex Xu 858251f509 show entire kindlegen error (#1386) 2026-07-07 10:02:26 -07:00
Marian Leontiev 3c66aa9a67 preserve ComicInfo.xml in CBZ output for Other profile 2026-07-05 21:07:26 +02:00
Aldijonas 077957e665 Rename 'cover.jpg' file to '##cover.jpg' (#1383) 2026-07-05 07:34:26 -07:00
Aldijonas c3f86e29c8 Rename cover.jpg to !cover.jpg in CBZ (#1381)
Change cover image filename from 'cover.jpg' to '!cover.jpg' in line 1907 for correct cover display when using CBZ file format
2026-07-02 16:41:37 -07:00
Nils Leo 72e1546c2c fix: enable unrar on all distros, not just Fedora (#1376)
* fix: enable unrar on all distros, not just Fedora

unrar was gated behind distro.id() == 'fedora', so on Debian/Ubuntu
the binary was installed but never invoked. Try unrar unconditionally
after 7z, with an optional rarfile Python fallback.

Fixes CBR extraction on non-Fedora Linux distributions.

* fix: gate unrar on Linux and drop unused distro dependency

Debian/Ubuntu's '7zip' package ships the upstream 7zz binary, which
excludes RAR support for licensing reasons, so 7z fails to read .cbr
(RAR) archives. unrar was installed but only invoked when
distro.id() == 'fedora', so on Debian it was never tried and extraction
failed.

Per maintainer feedback, gate unrar on platform.system() == 'Linux'
(not needed on Windows or macOS) and drop the rarfile PyPI fallback to
avoid a new dependency. Since the code no longer imports distro, also
remove it from the requirements files and setup.py.
2026-07-02 15:07:36 -07:00
sa-mi 544e866b51 fix crash in smart cover crop for landscape covers (#1379) 2026-07-01 21:30:08 -07:00
Alex Xu 363a36a764 reduce job list minimum height from 150 to 110 pixels (#1378) 2026-07-01 17:34:17 -07:00
Alex Xu bece5290a3 remove 2nd row of buttons (#1377) 2026-06-29 17:17:41 -07:00
17 changed files with 1832 additions and 1547 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
name: windows-build-${{ matrix.entry }} name: windows-build-${{ matrix.entry }}
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.2 uses: signpath/github-action-submit-signing-request@v2.3
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
name: windows7-build name: windows7-build
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.2 uses: signpath/github-action-submit-signing-request@v2.3
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+6
View File
@@ -42,6 +42,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
4) incorrect page turn direction for manga that's read right to left 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 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 6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
7) Fixing page order problems due to Windows sort being different than Kindle/Kobo sort
The GUI looks like this, built in Qt6, with my most commonly used settings: The GUI looks like this, built in Qt6, with my most commonly used settings:
@@ -123,6 +124,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
Going back a few pages and exiting and re-entering book should fix it temporarily. Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use? - What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025. - MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
- Where is KEPUB option?
- Choosing a Kobo profile and EPUB output will output KEPUB.
- All options have additional information in tooltips if you hover over the option. - 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 - 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? - Kindle panel view not working?
@@ -145,6 +148,8 @@ You'll need to install various tools to access important but optional features.
### KindleGen ### KindleGen
(This is not needed for Koreader users on jailbroken Kindle. Just set output to CBZ instead.)
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it. On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
@@ -293,6 +298,7 @@ OUTPUT SETTINGS:
-t TITLE, --title TITLE -t TITLE, --title TITLE
Comic title [Default=filename or directory name] Comic title [Default=filename or directory name]
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0] --metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
--keepcomicinfo Keep any original ComicInfo.xml files [Default=0]
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
--language EPUB language [Default=en-US] --language EPUB language [Default=en-US]
+724 -743
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
# -*- mode: python ; coding: utf-8 -*-
# macOS GUI build, used by setup.py build_binary
import sys
sys.path.insert(0, SPECPATH)
from kindlecomicconverter import __version__
a = Analysis(
['kcc.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='Kindle Comic Converter',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['icons/comic2ebook.icns'],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=True,
upx=True,
upx_exclude=[],
name='Kindle Comic Converter',
)
app = BUNDLE(
coll,
name='Kindle Comic Converter.app',
icon='icons/comic2ebook.icns',
bundle_identifier=None,
info_plist={
'CFBundleShortVersionString': __version__,
'CFBundleVersion': __version__,
},
)
+224 -141
View File
@@ -19,10 +19,11 @@
from datetime import datetime, timezone from datetime import datetime, timezone
import itertools import itertools
import json
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QImage, QKeyEvent, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView) from PySide6.QtWidgets import (QApplication, QDialogButtonBox, QHBoxLayout, QLabel, QListWidgetItem, QMainWindow, QSizePolicy, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView, QWidget)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer) from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os import os
@@ -38,10 +39,15 @@ from xml.sax.saxutils import escape
from psutil import Process from psutil import Process
from copy import copy from copy import copy
from packaging.version import Version from packaging.version import Version
from tempfile import gettempdir from tempfile import gettempdir, mkdtemp
from PIL import Image
from PIL.Image import Dither
from .KCC_spread_label import LabelSpreadsDialog
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, TAR, available_archive_tools from .comicarchive import SEVENZIP, TAR, available_archive_tools
from .comic2ebook import OS_SORT_KEY, flattenTree, getWorkFolder, removeNonImages, sanitizeTree
from . import __version__ from . import __version__
from . import comic2ebook from . import comic2ebook
from . import metadata from . import metadata
@@ -234,6 +240,138 @@ class ProgressThread(QThread):
self.running = False self.running = False
def get_options():
parser = comic2ebook.makeParser()
options = parser.parse_args()
options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
options.format = gui_current_format
if GUI.mangaBox.isChecked():
options.righttoleft = True
if GUI.lightnovelBox.isChecked():
options.lightnovel = True
if GUI.ebokBox.isChecked():
options.ebok = True
if GUI.invertDirectionBox.isChecked():
options.invertdirection = True
if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
options.splitter = 1
if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True
if GUI.vertical4PanelBox.isChecked():
options.vertical4panel = True
if GUI.webtoonBox.isChecked():
options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.stretch = True
elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
options.noautocontrast = True
elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
options.colorautocontrast = True
if GUI.croppingBox.isChecked():
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
options.cropping = 1
else:
options.cropping = 2
else:
options.cropping = 0
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value()
if GUI.interPanelCropBox.isChecked():
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
options.interpanelcrop = 1
else:
options.interpanelcrop = 2
else:
options.interpanelcrop = 0
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
options.black_borders = True
if GUI.outputSplit.isChecked():
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.legacyExtractBox.isChecked():
options.legacyextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = 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:
options.metadatatitle = 2
if GUI.keepComicInfoBox.isChecked():
options.keepcomicinfo = True
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked():
options.noquantize = True
if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2:
options.customwidth = str(GUI.widthBox.value())
options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '':
options.output = GUI.targetDirectory
if GUI.titleEdit.text():
options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.languageEdit.text():
options.language = str(GUI.languageEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
return options, gui_current_format
class WorkerThread(QThread): class WorkerThread(QThread):
def __init__(self): def __init__(self):
QThread.__init__(self) QThread.__init__(self)
@@ -263,134 +401,9 @@ class WorkerThread(QThread):
def run(self): def run(self):
MW.modeConvert.emit(0) MW.modeConvert.emit(0)
parser = comic2ebook.makeParser()
options = parser.parse_args()
argv = '' argv = ''
currentJobs = [] currentJobs = []
options, gui_current_format = get_options()
options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
options.format = gui_current_format
if GUI.mangaBox.isChecked():
options.righttoleft = True
if GUI.lightnovelBox.isChecked():
options.lightnovel = True
if GUI.ebokBox.isChecked():
options.ebok = True
if GUI.invertDirectionBox.isChecked():
options.invertdirection = True
if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
options.splitter = 1
if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True
if GUI.vertical4PanelBox.isChecked():
options.vertical4panel = True
if GUI.webtoonBox.isChecked():
options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.stretch = True
elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
options.noautocontrast = True
elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
options.colorautocontrast = True
if GUI.croppingBox.isChecked():
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
options.cropping = 1
else:
options.cropping = 2
else:
options.cropping = 0
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value()
if GUI.interPanelCropBox.isChecked():
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
options.interpanelcrop = 1
else:
options.interpanelcrop = 2
else:
options.interpanelcrop = 0
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
options.black_borders = True
if GUI.outputSplit.isChecked():
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.legacyExtractBox.isChecked():
options.legacyextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = 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:
options.metadatatitle = 2
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked():
options.noquantize = True
if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2:
options.customwidth = str(GUI.widthBox.value())
options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '':
options.output = GUI.targetDirectory
if GUI.titleEdit.text():
options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.languageEdit.text():
options.language = str(GUI.languageEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
for i in range(GUI.jobList.count()): for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do # Make sure that we don't consider any system message as job to do
@@ -558,8 +571,6 @@ class WorkerThread(QThread):
MW.addMessage.emit('Created EPUB file was too big. Weird file structure?', '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', MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
False) False)
if self.kindlegenErrorCode[0] == 3221226505:
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
else: else:
for item in outputPath: for item in outputPath:
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item): if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
@@ -659,6 +670,76 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.addItem(dname) GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom() GUI.jobList.scrollToBottom()
def labelSpreadsStart(self):
currentJobs = []
# TODO: make this a function since it's copy pasted
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text()))
for job in currentJobs:
images = []
spreads = []
options, _ = get_options()
options.profileData = [(600, 800), (600,800)]
path = getWorkFolder(job, options)
removeNonImages(path)
sanitizeTree(path, options)
flattenTree(path)
if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(job))
else:
workdir = mkdtemp('', 'KCC-')
for root, _, files in os.walk(path):
files.sort(key=OS_SORT_KEY)
start_index = 0
if job.endswith('.pdf') or job.endswith('.epub'):
start_index = 1
if options.spreadshift:
start_index = 0 if start_index == 1 else 1
for i in range(start_index, len(files), 2):
if i == len(files) - 1:
continue
# TODO: with statements
# TODO: ignore 1% of top and bottom too?
im1 = Image.open(os.path.join(root, files[i])).convert('1', dither=Dither.NONE)
size1 = im1.size
crop1 = im1.crop((0, 0, 0.2*size1[0], size1[1]))
crop11 = im1.crop((0.01*size1[0], 0, 0.04*size1[0], size1[1]))
#crop1 = crop11
im2 = Image.open(os.path.join(root, files[i+1])).convert('1', dither=Dither.NONE)
size2 = im2.size
crop2 = im2.crop((0.8*size2[0], 0, size2[0], size2[1]))
crop22 = im2.crop((0.96*size2[0], 0, .99* size2[0], size2[1]))
#crop2 = crop22
# dst = Image.new('1', (im1.width + im2.width, im1.height))
# dst.paste(im2, (0, 0))
# dst.paste(im1, (im1.width, 0))
hist1 = crop11.histogram()
hist2 = crop22.histogram()
# TODO: small percentage instead of zero
if hist1[0] == 0 or hist1[-1] == 0 or hist2[0] == 0 or hist2[-1] == 0:
continue
dst = Image.new('1', (crop1.width + crop2.width, crop1.height))
dst.paste(crop2, (0, 0))
dst.paste(crop1, (crop1.width, 0))
dst.save(os.path.join(workdir, f'label-{i:04}.png'))
images.append(os.path.join(workdir, f'label-{i:04}.png'))
if images:
dlg = LabelSpreadsDialog(APP.primaryScreen().availableGeometry().height(), images, spreads)
dlg.setWindowTitle(job)
if dlg.exec() == 1:
with open(job+'.json', "w") as fp:
# TODO: not very clean to grab index from filename
spreads = [int(filename[6:10]) for filename in spreads]
json.dump({'spreads': sorted(set(spreads))} , fp)
rmtree(path, True)
rmtree(workdir, True)
def selectFileMetaEditor(self, sname): def selectFileMetaEditor(self, sname):
files = [] files = []
@@ -750,7 +831,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
else: else:
status = True status = True
GUI.editorButton.setEnabled(status) GUI.editorButton.setEnabled(status)
GUI.wikiButton.setEnabled(status)
GUI.deviceBox.setEnabled(status) GUI.deviceBox.setEnabled(status)
GUI.defaultOutputFolderButton.setEnabled(status) GUI.defaultOutputFolderButton.setEnabled(status)
GUI.clearButton.setEnabled(status) GUI.clearButton.setEnabled(status)
@@ -839,7 +919,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.interPanelCropBox.setEnabled(True) GUI.interPanelCropBox.setEnabled(True)
GUI.autoLevelBox.setEnabled(True) GUI.autoLevelBox.setEnabled(True)
GUI.autocontrastBox.setEnabled(True) GUI.autocontrastBox.setEnabled(True)
GUI.autocontrastBox.setChecked(True)
def togglequalityBox(self, value): def togglequalityBox(self, value):
@@ -1113,6 +1192,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(), 'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(), 'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(), 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'keepComicInfoBox': GUI.keepComicInfoBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(), 'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(), 'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(), 'webpBox': GUI.webpBox.checkState(),
@@ -1221,11 +1301,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str)) self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
if not os.path.exists(self.defaultOutputFolder): if not os.path.exists(self.defaultOutputFolder):
self.defaultOutputFolder = '' self.defaultOutputFolder = ''
self.lastDevice = self.settings.value('lastDevice', 0, type=int)
# default is Kindle Paperwhite 12th Gen
self.lastDevice = self.settings.value('lastDevice', 3, type=int)
self.currentFormat = self.settings.value('currentFormat', 0, type=int) self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int) self.startNumber = self.settings.value('startNumber', 0, type=int)
self.windowSize = self.settings.value('windowSize', '0x0', type=str) self.windowSize = self.settings.value('windowSize', '0x0', type=str)
default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100} default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100, 'rotateBox': 1, 'mangaBox': 2}
try: try:
self.options = self.settings.value('options', default_options) self.options = self.settings.value('options', default_options)
except Exception: except Exception:
@@ -1252,7 +1335,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.windowSize == '0x0': if self.windowSize == '0x0':
MW.resize(500, 500) MW.resize(500, 500)
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox', for element in ['editorButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
'convertButton', 'formatBox']: 'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0)) getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1) GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
@@ -1433,8 +1516,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
link_dict = { link_dict = {
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc", 'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq", 'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj", 'WIKI': "https://github.com/ciromattia/kcc/wiki",
'COMMISSIONS': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions", 'YOUTUBE': "https://www.youtube.com/@eink-dude",
'TUTORIAL': "https://youtu.be/QQ6zJcMF2Iw?si=80rfm6DU6OUJdFqa",
'EMAIL': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations", 'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461", 'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461",
'DISCORD': "https://discord.com/invite/qj7wpnUHav", 'DISCORD': "https://discord.com/invite/qj7wpnUHav",
@@ -1466,12 +1551,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.fileButton.clicked.connect(self.selectFile) GUI.fileButton.clicked.connect(self.selectFile)
GUI.directoryButton.clicked.connect(self.selectDir) GUI.directoryButton.clicked.connect(self.selectDir)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor) GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi) GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble) GUI.humbleButton.clicked.connect(self.openHumble)
GUI.youtubeButton.clicked.connect(self.openYouTube)
GUI.discordButton.clicked.connect(self.openDiscord)
GUI.convertButton.clicked.connect(self.convertStart) GUI.convertButton.clicked.connect(self.convertStart)
GUI.labelSpreadsButton.clicked.connect(self.labelSpreadsStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma) GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox) GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox) GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
+92
View File
@@ -0,0 +1,92 @@
import os
from PySide6.QtCore import (Qt)
from PySide6.QtGui import (QKeyEvent, QPixmap)
from PySide6.QtWidgets import (QHBoxLayout, QLabel, QDialog)
class LabelSpreadsDialog(QDialog):
def __init__(self, available_height, images, spreads):
super().__init__()
self.index = 0
self.images = images
self.spreads = spreads
self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
self.setWindowTitle("TODO: Filename goes here")
# self.setGeometry(APP.primaryScreen().availableGeometry())
# self.setMaximumSize(APP.primaryScreen().availableSize())
self.available_height = available_height
layout = QHBoxLayout()
self.setLayout(layout)
label = QLabel()
label2 = QLabel()
self.label = label
self.label2 = label2
layout.addWidget(label)
layout.addWidget(label2)
label2.setText("not a spread")
help_text = [
"Use arrows to change index.",
"Use space bar to confirm spreads.",
"Use spread shift option to offset by 1.",
"Use enter key to confirm all spreads."
"Close window to cancel."
]
buttonLabel = QLabel('\n'.join(help_text))
layout.addWidget(buttonLabel)
# print(label.size())
# print(label.maximumSize())
# l, t, r, b = layout.getContentsMargins()
#label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
label.setPixmap(pixmap)
#label.setScaledContents(True)
#label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# label2.setPixmap(pixmap2)
#label2.setScaledContents(True)
#self.resize(pixmap2.width(), pixmap2.height())
def keyReleaseEvent(self, event):
# t = 20
# b = 20
if isinstance(event, QKeyEvent):
if event.key() == Qt.Key.Key_Left:
self.index = max(0, self.index - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Right:
self.index = min(self.index + 1, len(self.images) - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Space:
if self.label2.text() == "not a spread":
self.spreads.append(os.path.basename(self.images[self.index]))
self.label2.setText('spread')
else:
self.spreads.remove(os.path.basename(self.images[self.index]))
self.label2.setText('not a spread')
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
self.accept()
else:
super().keyReleaseEvent(event)
else:
super().keyReleaseEvent(event)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.3.0' __version__ = '11.0.1'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi' __copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
+98 -23
View File
@@ -19,6 +19,8 @@
# #
from collections import Counter from collections import Counter
from datetime import datetime
import json
import os import os
import pathlib import pathlib
import re import re
@@ -871,7 +873,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height): def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height, pdfwidth):
render = False render = False
with pymupdf.open(filename) as doc: with pymupdf.open(filename) as doc:
for page in doc: for page in doc:
@@ -891,7 +893,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
cpu = cpu_count() cpu = cpu_count()
# make vectors of arguments for the processes # make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)] vectors = [(i, cpu, filename, output_dir, target_width, target_height, pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename)) print("Starting %i processes for '%s'." % (cpu, filename))
@@ -905,7 +907,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
def getWorkFolder(afile, workdir=None): def getWorkFolder(afile, options, workdir=None):
if not workdir: if not workdir:
if options.tempdir: if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile)) workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
@@ -954,7 +956,7 @@ def getWorkFolder(afile, workdir=None):
target_height *= 1.25 #Account for possible margin at the top and bottom with page number target_height *= 1.25 #Account for possible margin at the top and bottom with page number
target_width *= 1.25 target_width *= 1.25
try: try:
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height) mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height, options.pdfwidth)
except Exception as e: except Exception as e:
rmtree(path, True) rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}") raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -978,9 +980,16 @@ def getWorkFolder(afile, workdir=None):
os.path.join(fullPath, tdir[0], 'ComicInfo.xml') os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
) )
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])): if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
if options.tempdir:
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
else:
workdir2 = mkdtemp('', 'KCC-')
fullPath2 = os.path.join(workdir2, 'OEBPS', 'Images')
os.makedirs(fullPath2, exist_ok=True)
for file in os.listdir(os.path.join(fullPath, tdir[0])): for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath) move(os.path.join(fullPath, tdir[0], file), fullPath2)
os.rmdir(os.path.join(fullPath, tdir[0])) rmtree(workdir, True)
return workdir2
if options.legacyextract: if options.legacyextract:
return workdir return workdir
@@ -1103,6 +1112,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
def getMetadata(path, originalpath): def getMetadata(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml') xmlPath = os.path.join(path, 'ComicInfo.xml')
options.comicinfo_chapters = [] options.comicinfo_chapters = []
options.comicinfo_xml = None
options.summary = '' options.summary = ''
titleSuffix = '' titleSuffix = ''
options.volume = '' options.volume = ''
@@ -1159,6 +1169,10 @@ def getMetadata(path, originalpath):
options.summary = xml.data['Summary'] options.summary = xml.data['Summary']
if xml.data['Series']: if xml.data['Series']:
options.series = xml.data['Series'] options.series = xml.data['Series']
# ComicInfo.xml in output may break readers like the Kobo native CBZ reader
if options.keepcomicinfo and options.format == 'CBZ':
with open(xmlPath, 'rb') as f:
options.comicinfo_xml = f.read()
os.remove(xmlPath) os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'): if originalpath.lower().endswith('.pdf'):
@@ -1201,14 +1215,22 @@ def removeNonImages(filetree):
os.remove(os.path.join(root, name)) os.remove(os.path.join(root, name))
# remove empty nested folders # remove empty nested folders
for root, dirs, files in os.walk(filetree, False): for root, dirs, files in os.walk(filetree, False):
if not files and not dirs: if not os.listdir(root):
os.rmdir(root) os.rmdir(root)
if not os.listdir(Path(filetree).parent): if not os.listdir(Path(filetree).parent):
raise UserWarning('No images detected, nested archives are not supported.') warning = [
'No images detected.',
'',
'Possible causes:',
'',
'1) Incompatible image file extension like .jxl. Convert to .png first outside of KCC.',
'2) Nested archive: Either extract the nested archive outside of KCC or use File Fusion option.',
]
raise RuntimeError('\n'.join(warning))
def sanitizeTree(filetree, prefix='kcc'): def sanitizeTree(filetree, options, prefix='kcc'):
chapterNames = {} chapterNames = {}
page = 1 page = 1
cover_path = None cover_path = None
@@ -1233,7 +1255,7 @@ def sanitizeTree(filetree, prefix='kcc'):
dirs.sort(key=OS_SORT_KEY) dirs.sort(key=OS_SORT_KEY)
for i, name in enumerate(dirs): for i, name in enumerate(dirs):
tmpName = name tmpName = name
slugified = slugify(name, is_natural_sorted) slugified = slugify(name, options, is_natural_sorted)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper(): while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
slugified += "A" slugified += "A"
chapterNames[slugified] = tmpName chapterNames[slugified] = tmpName
@@ -1254,6 +1276,7 @@ def flattenTree(filetree):
def sanitizePermissions(filetree): def sanitizePermissions(filetree):
os.chmod(filetree, S_IWRITE | S_IREAD | S_IEXEC)
for root, dirs, files in os.walk(filetree, False): for root, dirs, files in os.walk(filetree, False):
for name in files: for name in files:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
@@ -1395,7 +1418,7 @@ def createNewTome(parent):
return tomePath, tomePathRoot return tomePath, tomePathRoot
def slugify(value, is_natural_sorted): def slugify(value, options, is_natural_sorted):
if options.format == 'CBZ' and is_natural_sorted: if options.format == 'CBZ' and is_natural_sorted:
return value return value
if options.format != 'CBZ': if options.format != 'CBZ':
@@ -1475,6 +1498,8 @@ def makeParser():
output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0, output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0,
help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema " help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema "
"2: Use Title only") "2: Use Title only")
output_options.add_argument("--keepcomicinfo", type=int, dest="keepcomicinfo", default=0,
help="Keep any original ComicInfo.xml files")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor", output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]") help="Author name [Default=KCC]")
output_options.add_argument("--language", action="store", dest="language", default="en-US", output_options.add_argument("--language", action="store", dest="language", default="en-US",
@@ -1758,10 +1783,10 @@ def makeFusion(sources: List[str]):
else: else:
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}') targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
path = getWorkFolder(source, str(targetpath)) path = getWorkFolder(source, options, str(targetpath))
if path != str(targetpath): if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath) move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
sanitizeTree(targetpath, prefix='fusion') sanitizeTree(targetpath, options, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported # TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath) flattenTree(targetpath)
@@ -1784,7 +1809,7 @@ def makeBook(source, qtgui=None, job_progress=''):
if not options.filefusion: if not options.filefusion:
checkPre('LLL-') checkPre('LLL-')
print(f"{job_progress}Preparing source images...") print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source, options)
print(f"{job_progress}Checking images...") print(f"{job_progress}Checking images...")
if options.lightnovel: if options.lightnovel:
@@ -1818,7 +1843,26 @@ def makeBook(source, qtgui=None, job_progress=''):
getMetadata(os.path.join(path, "OEBPS", "Images"), source) getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images")) removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'), options)
if os.path.exists(source+'.json'):
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
with open(source+'.json') as f:
data = json.load(f)
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
sorted_files = os_sorted(files)
for i in range(len(files)):
if i in data['spreads']:
im1 = Image.open(os.path.join(root, sorted_files[i]))
im2 = Image.open(os.path.join(root, sorted_files[i+1]))
dst = Image.new('RGB', (im1.width + im2.width, im1.height))
dst.paste(im2, (0, 0))
dst.paste(im1, (im1.width, 0))
base, _ = os.path.splitext(os.path.basename(sorted_files[i]))
dst.save(os.path.join(path, 'OEBPS', 'Images', f'{base}-merged.png'))
os.remove(os.path.join(root, sorted_files[i]))
os.remove(os.path.join(root, sorted_files[i+1]))
if options.filefusion: if options.filefusion:
# Strip the fusion_0001_ sort prefix from makeFusion if present # Strip the fusion_0001_ sort prefix from makeFusion if present
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()} chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
@@ -1904,7 +1948,10 @@ def makeBook(source, qtgui=None, job_progress=''):
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if cover and cover.smartcover: if cover and cover.smartcover:
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes)) cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', '##cover.jpg'), tomeNumber, len(tomes))
if options.comicinfo_xml:
with open(os.path.join(tome, 'OEBPS', 'Images', 'ComicInfo.xml'), 'wb') as xmlOutput:
xmlOutput.write(options.comicinfo_xml)
makeZIP(filepath[-1], os.path.join(tome, "OEBPS", "Images"), job_progress) makeZIP(filepath[-1], os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF': elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...") print(f"{job_progress}Creating PDF file with PyMuPDF...")
@@ -1983,8 +2030,14 @@ def makeMOBIWorkerTick(output):
makeMOBIWorkerOutput.append(output) makeMOBIWorkerOutput.append(output)
if output[0] != 0: if output[0] != 0:
makeMOBIWorkerPool.terminate() makeMOBIWorkerPool.terminate()
for warning in output[3]:
print(warning)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if output[3]:
for warning in output[3]:
GUI.addMessage.emit(warning, 'warning', False)
GUI.addMessage.emit('', '', False)
if not GUI.conversionAlive: if not GUI.conversionAlive:
makeMOBIWorkerPool.terminate() makeMOBIWorkerPool.terminate()
@@ -1994,8 +2047,10 @@ def makeMOBIWorker(item):
kindlegenErrorCode = 0 kindlegenErrorCode = 0
kindlegenError = '' kindlegenError = ''
try: try:
# TODO: This size check is incorrect, I think kindlegen increased the limit
if os.path.getsize(item) < 629145600: if os.path.getsize(item) < 629145600:
start = perf_counter() start = perf_counter()
# TODO: should anything be done with the kindlegen output during successes?
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item], output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True) stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
end = perf_counter() end = perf_counter()
@@ -2003,27 +2058,47 @@ def makeMOBIWorker(item):
else: else:
# ERROR: EPUB too big # ERROR: EPUB too big
kindlegenErrorCode = 23026 kindlegenErrorCode = 23026
return [kindlegenErrorCode, kindlegenError, item] return [kindlegenErrorCode, kindlegenError, item, []]
except CalledProcessError as err: except CalledProcessError as err:
warnings = []
for line in err.stdout.splitlines(): for line in err.stdout.splitlines():
# ERROR: Generic error # ERROR: Generic error
if "Error(" in line: if "Error(" in line:
kindlegenErrorCode = 1 kindlegenErrorCode = 1
kindlegenError = line kindlegenError = '\n\n'.join(warnings + [line, 'kindlegen logs dumped'])
try:
timestamp = datetime.now().isoformat(timespec='milliseconds').replace(':', '-').replace('.', '-')
with open(os.path.join(os.path.dirname(item), f'kindlegen-log-{timestamp}.txt'), 'w') as f:
f.write(err.stdout)
except Exception as e:
print(e)
# examples
# Warning(prcgen):W14019: Cover is too small
if "Warning(" in line:
if ":W14016: Cover not specified" in line and options.webtoon:
pass
else:
warnings.append(line)
# ERROR: EPUB too big # ERROR: EPUB too big
if ":E23026:" in line: if ":E23026:" in line:
kindlegenErrorCode = 23026 kindlegenErrorCode = 23026
if ":E23028:" in line:
kindlegenErrorCode = 23028
if kindlegenErrorCode > 0: if kindlegenErrorCode > 0:
break break
if ":I1036: Mobi file built successfully" in line: if ":I1036: Mobi file built successfully" in line:
return [0, '', item] return [0, '', item, warnings]
if ":I1037: Mobi file built with WARNINGS!" in line: if ":I1037: Mobi file built with WARNINGS!" in line:
return [0, '', item] return [0, '', item, warnings]
# ERROR: KCC unknown generic error # ERROR: KCC unknown generic error
if kindlegenErrorCode == 0: if kindlegenErrorCode == 0:
kindlegenErrorCode = err.returncode kindlegenErrorCode = -1
kindlegenError = err.stdout if err.returncode == 3221226505:
return [kindlegenErrorCode, kindlegenError, item] kindlegenError = f'Error {err.returncode}: Unknown Windows error. Possibly filepath too long?'
else:
kindlegenError = f'Error {err.returncode}'
return [kindlegenErrorCode, kindlegenError, item, warnings]
def makeMOBI(work, qtgui=None): def makeMOBI(work, qtgui=None):
+6 -7
View File
@@ -22,7 +22,6 @@ from functools import cached_property, lru_cache
import os import os
from pathlib import Path from pathlib import Path
import platform import platform
import distro
from subprocess import STDOUT, PIPE, CalledProcessError from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
@@ -41,12 +40,12 @@ class ComicArchive:
self.dirname, self.basename = os.path.split(filepath) self.dirname, self.basename = os.path.split(filepath)
@cached_property @cached_property
def type(self): def type(self):
extraction_commands = [ extraction_commands = [
[SEVENZIP, 'l', '-y', '-p1', self.basename], [SEVENZIP, 'l', '-y', '-p1', self.basename],
] ]
if distro.id() == 'fedora' or distro.like() == 'fedora': if platform.system() == 'Linux':
extraction_commands.append( extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.basename], ['unrar', 'l', '-y', '-p1', self.basename],
) )
@@ -85,20 +84,20 @@ class ComicArchive:
extraction_commands.reverse() extraction_commands.reverse()
if distro.id() == 'fedora' or distro.like() == 'fedora': if platform.system() == 'Linux':
extraction_commands.append( extraction_commands.append(
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir] ['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
) )
for cmd in extraction_commands: for cmd in extraction_commands:
try: try:
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname) subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
return targetdir return targetdir
except FileNotFoundError: except FileNotFoundError:
missing.append(cmd[0]) missing.append(cmd[0])
except CalledProcessError: except CalledProcessError:
pass pass
if missing: if missing:
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ') raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
else: else:
+1 -1
View File
@@ -648,7 +648,7 @@ class Cover:
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((w * .36, 0, w, h)) self.image = self.image.crop((w * .36, 0, w, h))
else: else:
self.image = self.image.crop((w, 0, .64 * w, h)) self.image = self.image.crop((0, 0, .64 * w, h))
def save_to_folder(self, target, tomeid, len_tomes=0): def save_to_folder(self, target, tomeid, len_tomes=0):
try: try:
-1
View File
@@ -5,7 +5,6 @@ python-slugify>=8.0.4
packaging>=26.2 packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.9.0
# Below requirements are compiled in Dockefile # Below requirements are compiled in Dockefile
# numpy==2.3.4 # numpy==2.3.4
# PyMuPDF==1.26.6 # PyMuPDF==1.26.6
-1
View File
@@ -6,6 +6,5 @@ python-slugify>=8.0.4
packaging>=26.2 packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.9.0
numpy<2 numpy<2
PyMuPDF==1.25.5 PyMuPDF==1.25.5
-1
View File
@@ -6,6 +6,5 @@ python-slugify>=8.0.4
packaging>=26.2 packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.9.0
numpy==1.23.5 numpy==1.23.5
PyMuPDF>=1.16 PyMuPDF>=1.16
-1
View File
@@ -6,6 +6,5 @@ python-slugify>=8.0.4,<9.0.0
packaging>=26.2 packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.9.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.18.0 PyMuPDF>=1.18.0
+1 -2
View File
@@ -38,7 +38,7 @@ class BuildBinaryCommand(setuptools.Command):
def run(self): def run(self):
VERSION = __version__ VERSION = __version__
if sys.platform == 'darwin': if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py') os.system('pyinstaller -y kcc-macos.spec')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v # TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '') min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
if min_os.startswith('10.1'): if min_os.startswith('10.1'):
@@ -155,7 +155,6 @@ setuptools.setup(
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'mozjpeg-lossless-optimization>=1.2.0', 'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro>=1.8.0',
'numpy>=1.22.4', 'numpy>=1.22.4',
'packaging>=23.2', 'packaging>=23.2',
'PyMuPDF>=1.16.1', 'PyMuPDF>=1.16.1',