1
0
mirror of https://github.com/ciromattia/kcc synced 2026-06-28 17:15:27 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Alex Xu cab3703217 bump to 10.3.0 2026-06-26 10:23:51 -07:00
Alex Xu 3723bf9d52 add light novel mode (#1361)
* add a novel mode where it only resizes images

* add GUI

* cleanup

* fix typo

* fully cooked

* remove print

* clean up

* cook

* cook

* handle LA case

* cleanup
2026-06-26 10:23:28 -07:00
Alex Xu 2a6d61530f epub input: fix fusion (#1375) 2026-06-26 10:23:09 -07:00
Alex Xu 5396f1f9c4 name output epub/cbz directly (#1374) 2026-06-24 21:22:49 -07:00
Alex Xu f1b58c83d6 Replace copyfile + remove with move (#1373)
* remove mobiPath toclean

* remove copyfile _comic.zip
2026-06-24 18:58:27 -07:00
Alex Xu 0f009755b1 store settings separately 2026-06-23 17:37:46 -07:00
Alex Xu ee5bd150e5 add force ebok option 2026-06-23 17:37:46 -07:00
Alex Xu a55c0ddb08 add epub language option (#1371) 2026-06-23 13:29:10 -07:00
Alex Xu 799961407e re-arrange options into 4 columns (#1370)
* re-arrange options into 4 columns

* put delete input at the top
2026-06-22 22:27:00 -07:00
Alex Xu bc28df1f53 add invert direction and vertical 4 panel options (#1369) 2026-06-22 17:31:12 -07:00
Alex Xu a8b2c055bf epub input: invert spread shift (#1368) 2026-06-22 17:17:22 -07:00
dependabot[bot] afa9c7e7e6 Update psutil requirement from >=5.9.5 to >=7.2.2 (#1358)
Updates the requirements on [psutil](https://github.com/giampaolo/psutil) to permit the latest version.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/v5.9.5...v7.2.2)

---
updated-dependencies:
- dependency-name: psutil
  dependency-version: 7.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:31 -07:00
dependabot[bot] 1f1b9a37fa Update distro requirement from >=1.8.0 to >=1.9.0 (#1357)
Updates the requirements on [distro](https://github.com/python-distro/distro) to permit the latest version.
- [Release notes](https://github.com/python-distro/distro/releases)
- [Changelog](https://github.com/python-distro/distro/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python-distro/distro/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: distro
  dependency-version: 1.9.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:15 -07:00
dependabot[bot] c4d9512cbc Bump actions/checkout from 6 to 7 (#1367)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:02:58 -07:00
Alex Xu 89fc6437dd don't crash if rosetta is not available on macOS (#1366)
* don't crash if rosetta is not available on macOS

* make error a dialog

* add newlines

* fix format

* modify strings
2026-06-20 17:55:04 -07:00
Alex Xu 1e57da08a9 time kindlegen (#1365) 2026-06-18 16:34:52 -07:00
Alex Xu b4ef37dbb9 bump Windows 7 numpy (#1362) 2026-05-31 18:43:10 -07:00
Alex Xu b95cf6e179 ignore exceptions when epub parsing (#1360)
* ignore exceptions when epub parsing

* Update comic2ebook.py

* Update comic2ebook.py
2026-05-26 09:13:22 -07:00
フィルターペーパー 08070cdd97 Fix fusion output location (#1355)
* Fix fusion output location

Use source folder when output folder is not specified

* undelete things
2026-05-25 16:41:36 -07:00
Alex Xu df3d174437 fusion temp file cleanup improvements (#1359) 2026-05-25 14:52:08 -07:00
18 changed files with 1571 additions and 1397 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v4 uses: docker/login-action@v4
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
env: env:
MACOSX_DEPLOYMENT_TARGET: '14.0' MACOSX_DEPLOYMENT_TARGET: '14.0'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9 PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14' MACOSX_DEPLOYMENT_TARGET: '10.14'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Get Python - name: Get Python
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg" run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
- name: Install Python - name: Install Python
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
command: build_c2p command: build_c2p
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
env: env:
WINDOWS_7: 1 WINDOWS_7: 1
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
+5
View File
@@ -239,8 +239,12 @@ MAIN:
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE) Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE)
[Default=KV] [Default=KV]
-m, --manga-style Manga style (right-to-left reading and splitting) -m, --manga-style Manga style (right-to-left reading and splitting)
--lightnovel Only resize images and preserve original file structure.
--ebok Force EBOK tag instead of PDOC for MOBI
--invertdirection Invert page turn direction
-q, --hq Try to increase the quality of magnification -q, --hq Try to increase the quality of magnification
-2, --two-panel Display two not four panels in Panel View mode -2, --two-panel Display two not four panels in Panel View mode
--vertical4panel Show side panels first in virtual panel view
-w, --webtoon Webtoon processing mode -w, --webtoon Webtoon processing mode
--ts TARGETSIZE, --targetsize TARGETSIZE --ts TARGETSIZE, --targetsize TARGETSIZE
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others] the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
@@ -291,6 +295,7 @@ OUTPUT SETTINGS:
--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]
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
--language EPUB language [Default=en-US]
-f FORMAT, --format FORMAT -f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto] Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
+787 -734
View File
File diff suppressed because it is too large Load Diff
+27 -12
View File
@@ -273,6 +273,12 @@ class WorkerThread(QThread):
options.format = gui_current_format options.format = gui_current_format
if GUI.mangaBox.isChecked(): if GUI.mangaBox.isChecked():
options.righttoleft = True 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: if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2 options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked: elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
@@ -281,6 +287,8 @@ class WorkerThread(QThread):
options.autoscale = True options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked: elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True options.hq = True
if GUI.vertical4PanelBox.isChecked():
options.vertical4panel = True
if GUI.webtoonBox.isChecked(): if GUI.webtoonBox.isChecked():
options.webtoon = True options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -379,6 +387,8 @@ class WorkerThread(QThread):
options.title = str(GUI.titleEdit.text()) options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text(): if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text()) options.author = str(GUI.authorEdit.text())
if GUI.languageEdit.text():
options.language = str(GUI.languageEdit.text())
if GUI.chunkSizeCheckBox.isChecked(): if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value()) options.targetsize = int(GUI.chunkSizeBox.value())
@@ -393,7 +403,10 @@ class WorkerThread(QThread):
for job in currentJobs: for job in currentJobs:
bookDir.append(job) bookDir.append(job)
try: try:
fusion_source_parent = str(Path(bookDir[0]).parent)
comic2ebook.options = comic2ebook.checkOptions(copy(options)) comic2ebook.options = comic2ebook.checkOptions(copy(options))
if options.output is None:
options.output = fusion_source_parent
currentJobs.clear() currentJobs.clear()
currentJobs.append(comic2ebook.makeFusion(bookDir)) currentJobs.append(comic2ebook.makeFusion(bookDir))
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False) MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
@@ -465,7 +478,7 @@ class WorkerThread(QThread):
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else: else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format: if 'MOBI' in gui_current_format and not options.lightnovel:
MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files') MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1)) MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
MW.progressBarTick.emit('tick') MW.progressBarTick.emit('tick')
@@ -506,7 +519,6 @@ class WorkerThread(QThread):
for item in outputPath: for item in outputPath:
GUI.progress.content = '' GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
os.remove(mobiPath + '_toclean')
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath): if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath):
try: try:
move(mobiPath, GUI.targetDirectory) move(mobiPath, GUI.targetDirectory)
@@ -527,8 +539,6 @@ class WorkerThread(QThread):
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
if os.path.exists(mobiPath): if os.path.exists(mobiPath):
os.remove(mobiPath) os.remove(mobiPath)
if os.path.exists(mobiPath + '_toclean'):
os.remove(mobiPath + '_toclean')
MW.addMessage.emit('Failed to process MOBI file!', 'error', False) MW.addMessage.emit('Failed to process MOBI file!', 'error', False)
MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical') MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
else: else:
@@ -557,13 +567,6 @@ class WorkerThread(QThread):
move(item, GUI.targetDirectory) move(item, GUI.targetDirectory)
except Exception: except Exception:
pass pass
if options.filefusion:
for path in currentJobs:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
comic2ebook.checkPre('LLL-')
GUI.progress.content = '' GUI.progress.content = ''
GUI.progress.stop() GUI.progress.stop()
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
@@ -1084,8 +1087,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('startNumber', self.startNumber + 1) self.settings.setValue('startNumber', self.startNumber + 1)
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height())) self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(), self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'lightnovelBox': GUI.lightnovelBox.checkState(),
'ebokBox': GUI.ebokBox.checkState(),
'invertDirectionBox': GUI.invertDirectionBox.checkState(),
'languageEdit': GUI.languageEdit.text(),
'rotateBox': GUI.rotateBox.checkState(), 'rotateBox': GUI.rotateBox.checkState(),
'qualityBox': GUI.qualityBox.checkState(), 'qualityBox': GUI.qualityBox.checkState(),
'vertical4PanelBox': GUI.vertical4PanelBox.checkState(),
'gammaBox': GUI.gammaBox.checkState(), 'gammaBox': GUI.gammaBox.checkState(),
'autoLevelBox': GUI.autoLevelBox.checkState(), 'autoLevelBox': GUI.autoLevelBox.checkState(),
'autocontrastBox': GUI.autocontrastBox.checkState(), 'autocontrastBox': GUI.autocontrastBox.checkState(),
@@ -1193,6 +1201,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.kindleGen = False self.kindleGen = False
if startup: if startup:
self.display_kindlegen_missing() self.display_kindlegen_missing()
except OSError as e:
self.kindleGen = False
if startup:
error = f"kindlegen: {e.strerror}\n\n Re-install Rosetta/Kindle Previewer/other Intel app?\n\nPlease email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help"
self.showDialog(error, 'error')
def __init__(self, kccapp, kccwindow): def __init__(self, kccapp, kccwindow):
global APP, MW, GUI global APP, MW, GUI
@@ -1202,7 +1215,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW) self.setupUi(MW)
self.editor = KCCGUI_MetaEditor() self.editor = KCCGUI_MetaEditor()
self.icons = Icons() self.icons = Icons()
self.settings = QSettings('ciromattia', 'kcc9') self.settings = QSettings('ciromattia', 'kcc10')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str) self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str)) self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
@@ -1516,6 +1529,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.widthBox.setValue(int(self.options[option])) GUI.widthBox.setValue(int(self.options[option]))
elif str(option) == "heightBox": elif str(option) == "heightBox":
GUI.heightBox.setValue(int(self.options[option])) GUI.heightBox.setValue(int(self.options[option]))
elif str(option) == "languageEdit":
GUI.languageEdit.setText(str(self.options[option]))
elif str(option) == "gammaSlider": elif str(option) == "gammaSlider":
if GUI.gammaSlider.isEnabled(): if GUI.gammaSlider.isEnabled():
GUI.gammaSlider.setValue(int(self.options[option])) GUI.gammaSlider.setValue(int(self.options[option]))
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.2.0' __version__ = '10.3.0'
__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'
+109 -47
View File
@@ -34,12 +34,12 @@ from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List from typing import List
from zipfile import ZipFile, ZIP_STORED from zipfile import ZipFile, ZIP_STORED
from tempfile import mkdtemp, gettempdir from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from uuid import uuid4 from uuid import uuid4
from natsort import os_sort_keygen, os_sorted from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext from slugify import slugify as slugify_ext
from PIL import Image, ImageFile from PIL import Image, ImageFile, ImageOps
from pathlib import Path from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage from psutil import virtual_memory, disk_usage
@@ -76,23 +76,19 @@ def main(argv=None):
print('No matching files found.') print('No matching files found.')
return 1 return 1
if options.filefusion: if options.filefusion:
fusion_source_parent = str(Path(sources[0]).parent)
fusion_path = makeFusion(list(sources)) fusion_path = makeFusion(list(sources))
sources.clear() sources.clear()
sources.append(fusion_path) sources.append(fusion_path)
for source in sources: for source in sources:
source = source.rstrip('\\').rstrip('/') source = source.rstrip('\\').rstrip('/')
options = copy(args) options = copy(args)
if options.filefusion and options.output is None:
options.output = fusion_source_parent
options = checkOptions(options) options = checkOptions(options)
print('Working on ' + source + '...') print('Working on ' + source + '...')
makeBook(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)
checkPre('LLL-')
return 0 return 0
@@ -230,7 +226,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx') ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
f = open(ncxfile, "w", encoding='UTF-8') f = open(ncxfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n", f"<ncx version=\"2005-1\" xml:lang=\"{options.language}\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
"<head>\n", "<head>\n",
"<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n", "<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
"<meta name=\"dtb:depth\" content=\"1\"/>\n", "<meta name=\"dtb:depth\" content=\"1\"/>\n",
@@ -296,10 +292,22 @@ def buildNAV(dstdir, title, chapters, chapternames):
def buildOPF(dstdir, title, filelist, originalpath, cover=None): def buildOPF(dstdir, title, filelist, originalpath, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
deviceres = options.profileData[1] deviceres = options.profileData[1]
if options.righttoleft:
writingmode = "horizontal-rl" if options.vertical4panel:
writingmode = "vertical"
else: else:
writingmode = "horizontal-lr" writingmode = "horizontal"
if options.invertdirection:
if options.righttoleft:
writingmode += "-lr"
else:
writingmode += "-rl"
else:
if options.righttoleft:
writingmode += "-rl"
else:
writingmode += "-lr"
f = open(opffile, "w", encoding='UTF-8') f = open(opffile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<package version=\"3.0\" unique-identifier=\"BookID\" ", "<package version=\"3.0\" unique-identifier=\"BookID\" ",
@@ -307,7 +315,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ", "<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n", "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
"<dc:title>", hescape(title), "</dc:title>\n", "<dc:title>", hescape(title), "</dc:title>\n",
"<dc:language>en-US</dc:language>\n", f"<dc:language>{options.language}</dc:language>\n",
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n", "<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"]) "<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
if len(options.summary) > 0: if len(options.summary) > 0:
@@ -398,13 +406,22 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
else: else:
return "" return ""
if options.righttoleft: if options.invertdirection:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n") if options.righttoleft:
pageside = "right" f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
else:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
pageside = "right"
else: else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n") if options.righttoleft:
pageside = "left" f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
if originalpath.lower().endswith('.pdf'): pageside = "right"
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
if originalpath.lower().endswith('.pdf') or originalpath.lower().endswith('.epub'):
if pageside == "right": if pageside == "right":
pageside = "left" pageside = "left"
else: else:
@@ -948,6 +965,8 @@ def getWorkFolder(afile, workdir=None):
try: try:
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(fullPath) path = cbx.extract(fullPath)
if options.lightnovel:
return workdir
sanitizePermissions(path) sanitizePermissions(path)
tdir = os.listdir(fullPath) tdir = os.listdir(fullPath)
@@ -979,10 +998,11 @@ def getWorkFolder(afile, workdir=None):
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href') manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
ordered_image_paths = [] ordered_image_paths = []
for i, spine_item in enumerate(spine): for i, spine_item in enumerate(spine):
if spine_item not in manifest_dict: try:
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
except Exception:
continue continue
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image') imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
largest_size = 0 largest_size = 0
@@ -1388,7 +1408,6 @@ def slugify(value, is_natural_sorted):
def makeZIP(zipfilename, basedir, job_progress='', isepub=False): def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools(): if SEVENZIP in available_archive_tools():
if isepub: if isepub:
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
@@ -1431,10 +1450,18 @@ def makeParser():
" [Default=KV]") " [Default=KV]")
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False, main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (right-to-left reading and splitting)") help="Manga style (right-to-left reading and splitting)")
main_options.add_argument("--lightnovel", action="store_true", dest="lightnovel", default=False,
help="Only resize images and preserve original file structure.")
main_options.add_argument("--ebok", action="store_true", dest="ebok", default=False,
help="Force EBOK tag instead of PDOC for MOBI")
main_options.add_argument("--invertdirection", action="store_true", dest="invertdirection", default=False,
help="Invert page turn direction")
main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False, main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
help="Try to increase the quality of magnification") help="Try to increase the quality of magnification")
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False, main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
help="Display two not four panels in Panel View mode") help="Display two not four panels in Panel View mode")
main_options.add_argument("--vertical4panel", action="store_true", dest="vertical4panel", default=False,
help="Display side panels first in virtual panel view")
main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False, main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
help="Webtoon processing mode"), help="Webtoon processing mode"),
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None, main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
@@ -1450,6 +1477,8 @@ def makeParser():
"2: Use Title only") "2: Use Title only")
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",
help="EPUB language [Default=en-US]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto", output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) " help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]") "[Default=Auto]")
@@ -1557,6 +1586,9 @@ def checkOptions(options):
else: else:
options.isKobo = True options.isKobo = True
if options.lightnovel:
options.noKepub = True
if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format): if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles') raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
@@ -1676,6 +1708,11 @@ def checkTools(source):
except (FileNotFoundError, CalledProcessError): except (FileNotFoundError, CalledProcessError):
print('ERROR: KindleGen is missing!') print('ERROR: KindleGen is missing!')
sys.exit(1) sys.exit(1)
except OSError as e:
print(f"kindlegen: {e.strerror}")
print('Re-install Rosetta/Kindle Previewer/other Intel app?')
print('Please email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help')
sys.exit(1)
def checkPre(source='KCC-'): def checkPre(source='KCC-'):
@@ -1690,8 +1727,8 @@ def makeFusion(sources: List[str]):
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?') raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter() start = perf_counter()
first_path = Path(sources[0]) first_path = Path(sources[0])
if True: if options.tempdir:
fusion_parent = first_path.parent fusion_parent = first_path.parent
else: else:
# LLL is after KCC # LLL is after KCC
@@ -1721,7 +1758,9 @@ 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}')
getWorkFolder(source, str(targetpath)) path = getWorkFolder(source, str(targetpath))
if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
sanitizeTree(targetpath, prefix='fusion') sanitizeTree(targetpath, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported # TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath) flattenTree(targetpath)
@@ -1747,6 +1786,35 @@ def makeBook(source, qtgui=None, job_progress=''):
print(f"{job_progress}Preparing source images...") print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source)
print(f"{job_progress}Checking images...") print(f"{job_progress}Checking images...")
if options.lightnovel:
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for file in files:
_, ext = os.path.splitext(file)
if ext.lower() in ('.jpg', '.jpeg', '.png', '.webp', '.gif'):
with Image.open(os.path.join(root, file)) as img:
# TODO: detect BW images saved as RGB
if not options.forcecolor:
if img.mode == 'RGB':
img = img.convert('L')
elif img.mode == 'RGBA':
img = img.convert('LA')
x, y = image.ProfileData.Profiles[options.profile][1]
if options.iskindle:
x = min(x, 1920)
y = min(y, 1920)
if img.size[0] > x or img.size[1] > y:
img = ImageOps.contain(img, (x, y))
img.save(os.path.join(root, file), quality=options.jpegquality)
_, ext = os.path.splitext(source)
if ext != '.epub':
ext = '.cbz'
output_file = getOutputFilename(source, options.output, ext, '')
makeZIP(output_file, os.path.join(path, 'OEBPS', 'Images'), job_progress)
rmtree(path, True)
return [output_file]
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)
@@ -1837,7 +1905,7 @@ def makeBook(source, qtgui=None, job_progress=''):
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))
makeZIP(tome + '_comic', 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...")
# determine output filename based on source and tome count # determine output filename based on source and tome count
@@ -1856,19 +1924,11 @@ def makeBook(source, qtgui=None, job_progress=''):
else: else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress) buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, job_progress, True) makeZIP(filepath[-1], tome, job_progress, True)
# Copy files to final destination (PDF files are already saved directly)
if options.format != 'PDF':
copyfile(tome + '_comic.zip', filepath[-1])
try:
os.remove(tome + '_comic.zip')
except FileNotFoundError:
# newly temporary created file is not found. It might have been already deleted
pass
rmtree(tome, True) rmtree(tome, True)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI': if not GUI and options.format == 'MOBI' and not options.lightnovel:
print(f"{job_progress}Creating MOBI files...") print(f"{job_progress}Creating MOBI files...")
work = [] work = []
for i in filepath: for i in filepath:
@@ -1887,8 +1947,6 @@ def makeBook(source, qtgui=None, job_progress=''):
if not output[0]: if not output[0]:
print(f'{job_progress}Error: Failed to tweak KindleGen output!') print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath return filepath
else:
os.remove(i.replace('.epub', '.mobi') + '_toclean')
if cover and k.path and k.coverSupport: if cover and k.path and k.coverSupport:
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1]) options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
if options.delete: if options.delete:
@@ -1899,22 +1957,23 @@ def makeBook(source, qtgui=None, job_progress=''):
end = perf_counter() end = perf_counter()
print(f"{job_progress}makeBook: {end - start} seconds") print(f"{job_progress}makeBook: {end - start} seconds")
# Clean up temporary workspace
try: if options.filefusion:
rmtree(path, True) rmtree(source, True)
except Exception: checkPre('LLL-')
pass
return filepath return filepath
def makeMOBIFix(item, uuid): def makeMOBIFix(item, uuid):
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys() is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
if options.ebok:
is_pdoc = False
if not options.keep_epub: if not options.keep_epub:
os.remove(item) os.remove(item)
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean')
try: try:
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc) dualmetafix.DualMobiMetaFix(mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
return [True] return [True]
except Exception as err: except Exception as err:
return [False, format(err)] return [False, format(err)]
@@ -1936,8 +1995,11 @@ def makeMOBIWorker(item):
kindlegenError = '' kindlegenError = ''
try: try:
if os.path.getsize(item) < 629145600: if os.path.getsize(item) < 629145600:
start = perf_counter()
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()
print(f"kindlegen: {end - start} sec")
else: else:
# ERROR: EPUB too big # ERROR: EPUB too big
kindlegenErrorCode = 23026 kindlegenErrorCode = 23026
+1 -2
View File
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
class DualMobiMetaFix: class DualMobiMetaFix:
def __init__(self, infile, outfile, asin, is_pdoc): def __init__(self, outfile, asin, is_pdoc):
cdetype = b'EBOK' cdetype = b'EBOK'
if is_pdoc: if is_pdoc:
cdetype = b'PDOC' cdetype = b'PDOC'
shutil.copyfile(infile, outfile)
f = open(outfile, "r+b") f = open(outfile, "r+b")
self.datain = mmap.mmap(f.fileno(), 0) self.datain = mmap.mmap(f.fileno(), 0)
self.datain_rec0 = readsection(self.datain, 0) self.datain_rec0 = readsection(self.datain, 0)
+2 -2
View File
@@ -1,11 +1,11 @@
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.34.2 requests>=2.34.2
python-slugify>=8.0.4 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.8.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
+2 -2
View File
@@ -1,11 +1,11 @@
PySide6==6.4.3 PySide6==6.4.3
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.34.2 requests>=2.34.2
python-slugify>=8.0.4 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.8.0 distro>=1.9.0
numpy<2 numpy<2
PyMuPDF==1.25.5 PyMuPDF==1.25.5
+3 -3
View File
@@ -1,11 +1,11 @@
PySide6==6.1.3 PySide6==6.1.3
Pillow>=9 Pillow>=9
psutil>=5.9.5 psutil>=7.2.2
requests>=2.32.4 requests>=2.32.4
python-slugify>=8.0.4 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.8.0 distro>=1.9.0
numpy==1.23.0 numpy==1.23.5
PyMuPDF>=1.16 PyMuPDF>=1.16
+2 -2
View File
@@ -1,11 +1,11 @@
PySide6<6.10 PySide6<6.10
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.34.2 requests>=2.34.2
python-slugify>=8.0.4,<9.0.0 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.8.0 distro>=1.9.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.18.0 PyMuPDF>=1.18.0