mirror of
https://github.com/ciromattia/kcc
synced 2026-08-03 10:25:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0328a12f | ||
|
|
846a755761 | ||
|
|
ff80d275e6 | ||
|
|
b3e1d393b6 | ||
|
|
b4c71b3b91 | ||
|
|
7e412f12ae | ||
|
|
2076775d94 | ||
|
|
59df22b62d | ||
|
|
ff5edc5ce5 | ||
|
|
a52c0ce03a | ||
|
|
1c0506fe7d | ||
|
|
806c6232c3 | ||
|
|
6f9f5067d8 | ||
|
|
11be0dffcc | ||
|
|
4c10569ab1 | ||
|
|
2a5cd6718d | ||
|
|
fd1468904f | ||
|
|
feec40ffc8 | ||
|
|
f6ae7661bc | ||
|
|
ca14a4c813 | ||
|
|
0c1071a358 | ||
|
|
858251f509 | ||
|
|
3c66aa9a67 | ||
|
|
077957e665 | ||
|
|
c3f86e29c8 | ||
|
|
72e1546c2c | ||
|
|
544e866b51 | ||
|
|
363a36a764 | ||
|
|
bece5290a3 | ||
|
|
cab3703217 | ||
|
|
3723bf9d52 | ||
|
|
2a6d61530f | ||
|
|
5396f1f9c4 | ||
|
|
f1b58c83d6 | ||
|
|
0f009755b1 | ||
|
|
ee5bd150e5 | ||
|
|
a55c0ddb08 | ||
|
|
799961407e | ||
|
|
bc28df1f53 | ||
|
|
a8b2c055bf | ||
|
|
afa9c7e7e6 | ||
|
|
1f1b9a37fa | ||
|
|
c4d9512cbc | ||
|
|
89fc6437dd | ||
|
|
1e57da08a9 | ||
|
|
b4ef37dbb9 | ||
|
|
b95cf6e179 | ||
|
|
08070cdd97 | ||
|
|
df3d174437 |
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
PYTHON_VERSION: 3.11.9
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get Python
|
||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||
- name: Install Python
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
command: build_c2p
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
env:
|
||||
WINDOWS_7: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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?
|
||||
@@ -239,8 +242,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)
|
||||
[Default=KV]
|
||||
-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
|
||||
-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
|
||||
--ts TARGETSIZE, --targetsize TARGETSIZE
|
||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||
@@ -289,8 +296,10 @@ OUTPUT SETTINGS:
|
||||
-t TITLE, --title TITLE
|
||||
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]
|
||||
--keepcomicinfo Keep any original ComicInfo.xml files [Default=0]
|
||||
-a AUTHOR, --author AUTHOR
|
||||
Author name [Default=KCC]
|
||||
--language EPUB language [Default=en-US]
|
||||
-f FORMAT, --format FORMAT
|
||||
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'
|
||||
|
||||
+904
-870
File diff suppressed because it is too large
Load Diff
@@ -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__,
|
||||
},
|
||||
)
|
||||
+241
-143
@@ -19,10 +19,11 @@
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import itertools
|
||||
import json
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
|
||||
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
|
||||
from PySide6.QtGui import (QColor, QIcon, QImage, QKeyEvent, QPixmap, QDesktopServices)
|
||||
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)
|
||||
|
||||
import os
|
||||
@@ -38,10 +39,15 @@ from xml.sax.saxutils import escape
|
||||
from psutil import Process
|
||||
from copy import copy
|
||||
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 .comicarchive import SEVENZIP, TAR, available_archive_tools
|
||||
from .comic2ebook import OS_SORT_KEY, flattenTree, getWorkFolder, removeNonImages, sanitizeTree
|
||||
from . import __version__
|
||||
from . import comic2ebook
|
||||
from . import metadata
|
||||
@@ -234,6 +240,138 @@ class ProgressThread(QThread):
|
||||
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):
|
||||
def __init__(self):
|
||||
QThread.__init__(self)
|
||||
@@ -263,124 +401,9 @@ class WorkerThread(QThread):
|
||||
def run(self):
|
||||
MW.modeConvert.emit(0)
|
||||
|
||||
parser = comic2ebook.makeParser()
|
||||
options = parser.parse_args()
|
||||
argv = ''
|
||||
currentJobs = []
|
||||
|
||||
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.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.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.chunkSizeCheckBox.isChecked():
|
||||
options.targetsize = int(GUI.chunkSizeBox.value())
|
||||
options, gui_current_format = get_options()
|
||||
|
||||
for i in range(GUI.jobList.count()):
|
||||
# Make sure that we don't consider any system message as job to do
|
||||
@@ -393,7 +416,10 @@ class WorkerThread(QThread):
|
||||
for job in currentJobs:
|
||||
bookDir.append(job)
|
||||
try:
|
||||
fusion_source_parent = str(Path(bookDir[0]).parent)
|
||||
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
||||
if options.output is None:
|
||||
options.output = fusion_source_parent
|
||||
currentJobs.clear()
|
||||
currentJobs.append(comic2ebook.makeFusion(bookDir))
|
||||
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
|
||||
@@ -465,7 +491,7 @@ class WorkerThread(QThread):
|
||||
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
|
||||
else:
|
||||
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(str(len(outputPath) * 2 + 1))
|
||||
MW.progressBarTick.emit('tick')
|
||||
@@ -506,7 +532,6 @@ class WorkerThread(QThread):
|
||||
for item in outputPath:
|
||||
GUI.progress.content = ''
|
||||
mobiPath = item.replace('.epub', '.mobi')
|
||||
os.remove(mobiPath + '_toclean')
|
||||
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath):
|
||||
try:
|
||||
move(mobiPath, GUI.targetDirectory)
|
||||
@@ -527,8 +552,6 @@ class WorkerThread(QThread):
|
||||
mobiPath = item.replace('.epub', '.mobi')
|
||||
if os.path.exists(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.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
|
||||
else:
|
||||
@@ -548,8 +571,6 @@ class WorkerThread(QThread):
|
||||
MW.addMessage.emit('Created EPUB file was too big. Weird file structure?', 'error', False)
|
||||
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
|
||||
False)
|
||||
if self.kindlegenErrorCode[0] == 3221226505:
|
||||
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
|
||||
else:
|
||||
for item in outputPath:
|
||||
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
|
||||
@@ -557,13 +578,6 @@ class WorkerThread(QThread):
|
||||
move(item, GUI.targetDirectory)
|
||||
except Exception:
|
||||
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.stop()
|
||||
MW.hideProgressBar.emit()
|
||||
@@ -656,6 +670,76 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.jobList.addItem(dname)
|
||||
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):
|
||||
files = []
|
||||
@@ -747,7 +831,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
else:
|
||||
status = True
|
||||
GUI.editorButton.setEnabled(status)
|
||||
GUI.wikiButton.setEnabled(status)
|
||||
GUI.deviceBox.setEnabled(status)
|
||||
GUI.defaultOutputFolderButton.setEnabled(status)
|
||||
GUI.clearButton.setEnabled(status)
|
||||
@@ -836,7 +919,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.interPanelCropBox.setEnabled(True)
|
||||
GUI.autoLevelBox.setEnabled(True)
|
||||
GUI.autocontrastBox.setEnabled(True)
|
||||
GUI.autocontrastBox.setChecked(True)
|
||||
|
||||
|
||||
def togglequalityBox(self, value):
|
||||
@@ -1084,8 +1166,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.settings.setValue('startNumber', self.startNumber + 1)
|
||||
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
|
||||
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(),
|
||||
'qualityBox': GUI.qualityBox.checkState(),
|
||||
'vertical4PanelBox': GUI.vertical4PanelBox.checkState(),
|
||||
'gammaBox': GUI.gammaBox.checkState(),
|
||||
'autoLevelBox': GUI.autoLevelBox.checkState(),
|
||||
'autocontrastBox': GUI.autocontrastBox.checkState(),
|
||||
@@ -1105,6 +1192,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||
'keepComicInfoBox': GUI.keepComicInfoBox.checkState(),
|
||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
|
||||
'webpBox': GUI.webpBox.checkState(),
|
||||
@@ -1193,6 +1281,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.kindleGen = False
|
||||
if startup:
|
||||
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):
|
||||
global APP, MW, GUI
|
||||
@@ -1202,17 +1295,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.setupUi(MW)
|
||||
self.editor = KCCGUI_MetaEditor()
|
||||
self.icons = Icons()
|
||||
self.settings = QSettings('ciromattia', 'kcc9')
|
||||
self.settings = QSettings('ciromattia', 'kcc10')
|
||||
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
|
||||
self.lastPath = self.settings.value('lastPath', '', type=str)
|
||||
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
|
||||
if not os.path.exists(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.startNumber = self.settings.value('startNumber', 0, type=int)
|
||||
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:
|
||||
self.options = self.settings.value('options', default_options)
|
||||
except Exception:
|
||||
@@ -1239,7 +1335,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if self.windowSize == '0x0':
|
||||
MW.resize(500, 500)
|
||||
elif sys.platform.startswith('darwin'):
|
||||
for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
|
||||
for element in ['editorButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
|
||||
'convertButton', 'formatBox']:
|
||||
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
||||
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
||||
@@ -1420,8 +1516,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
link_dict = {
|
||||
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
|
||||
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
|
||||
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj",
|
||||
'COMMISSIONS': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
|
||||
'WIKI': "https://github.com/ciromattia/kcc/wiki",
|
||||
'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",
|
||||
'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461",
|
||||
'DISCORD': "https://discord.com/invite/qj7wpnUHav",
|
||||
@@ -1453,12 +1551,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.fileButton.clicked.connect(self.selectFile)
|
||||
GUI.directoryButton.clicked.connect(self.selectDir)
|
||||
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
||||
GUI.wikiButton.clicked.connect(self.openWiki)
|
||||
GUI.kofiButton.clicked.connect(self.openKofi)
|
||||
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.labelSpreadsButton.clicked.connect(self.labelSpreadsStart)
|
||||
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
|
||||
@@ -1516,6 +1612,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.widthBox.setValue(int(self.options[option]))
|
||||
elif str(option) == "heightBox":
|
||||
GUI.heightBox.setValue(int(self.options[option]))
|
||||
elif str(option) == "languageEdit":
|
||||
GUI.languageEdit.setText(str(self.options[option]))
|
||||
elif str(option) == "gammaSlider":
|
||||
if GUI.gammaSlider.isEnabled():
|
||||
GUI.gammaSlider.setValue(int(self.options[option]))
|
||||
|
||||
@@ -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)
|
||||
+536
-503
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
__version__ = '10.2.0'
|
||||
__version__ = '11.0.1'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
#
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
@@ -34,12 +36,12 @@ from stat import S_IWRITE, S_IREAD, S_IEXEC
|
||||
from typing import List
|
||||
from zipfile import ZipFile, ZIP_STORED
|
||||
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 uuid import uuid4
|
||||
from natsort import os_sort_keygen, os_sorted
|
||||
from slugify import slugify as slugify_ext
|
||||
from PIL import Image, ImageFile
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from pathlib import Path
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from psutil import virtual_memory, disk_usage
|
||||
@@ -76,23 +78,19 @@ def main(argv=None):
|
||||
print('No matching files found.')
|
||||
return 1
|
||||
if options.filefusion:
|
||||
fusion_source_parent = str(Path(sources[0]).parent)
|
||||
fusion_path = makeFusion(list(sources))
|
||||
sources.clear()
|
||||
sources.append(fusion_path)
|
||||
for source in sources:
|
||||
source = source.rstrip('\\').rstrip('/')
|
||||
options = copy(args)
|
||||
if options.filefusion and options.output is None:
|
||||
options.output = fusion_source_parent
|
||||
options = checkOptions(options)
|
||||
print('Working on ' + source + '...')
|
||||
makeBook(source)
|
||||
|
||||
if options.filefusion:
|
||||
for path in sources:
|
||||
if os.path.isfile(path):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
checkPre('LLL-')
|
||||
return 0
|
||||
|
||||
|
||||
@@ -230,7 +228,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
|
||||
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
|
||||
f = open(ncxfile, "w", encoding='UTF-8')
|
||||
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",
|
||||
"<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
|
||||
"<meta name=\"dtb:depth\" content=\"1\"/>\n",
|
||||
@@ -296,10 +294,22 @@ def buildNAV(dstdir, title, chapters, chapternames):
|
||||
def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
||||
deviceres = options.profileData[1]
|
||||
if options.righttoleft:
|
||||
writingmode = "horizontal-rl"
|
||||
|
||||
if options.vertical4panel:
|
||||
writingmode = "vertical"
|
||||
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.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
"<package version=\"3.0\" unique-identifier=\"BookID\" ",
|
||||
@@ -307,7 +317,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
|
||||
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\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:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
|
||||
if len(options.summary) > 0:
|
||||
@@ -398,13 +408,22 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
else:
|
||||
return ""
|
||||
|
||||
if options.righttoleft:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||
pageside = "right"
|
||||
if options.invertdirection:
|
||||
if options.righttoleft:
|
||||
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:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||
pageside = "left"
|
||||
if originalpath.lower().endswith('.pdf'):
|
||||
if options.righttoleft:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||
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":
|
||||
pageside = "left"
|
||||
else:
|
||||
@@ -854,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
|
||||
with pymupdf.open(filename) as doc:
|
||||
for page in doc:
|
||||
@@ -874,7 +893,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
||||
cpu = cpu_count()
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
@@ -888,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 options.tempdir:
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
@@ -937,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_width *= 1.25
|
||||
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:
|
||||
rmtree(path, True)
|
||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
||||
@@ -948,6 +967,8 @@ def getWorkFolder(afile, workdir=None):
|
||||
try:
|
||||
cbx = comicarchive.ComicArchive(afile)
|
||||
path = cbx.extract(fullPath)
|
||||
if options.lightnovel:
|
||||
return workdir
|
||||
sanitizePermissions(path)
|
||||
|
||||
tdir = os.listdir(fullPath)
|
||||
@@ -959,9 +980,16 @@ def getWorkFolder(afile, workdir=None):
|
||||
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
|
||||
)
|
||||
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])):
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath)
|
||||
os.rmdir(os.path.join(fullPath, tdir[0]))
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath2)
|
||||
rmtree(workdir, True)
|
||||
return workdir2
|
||||
|
||||
if options.legacyextract:
|
||||
return workdir
|
||||
@@ -979,10 +1007,11 @@ def getWorkFolder(afile, workdir=None):
|
||||
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
|
||||
ordered_image_paths = []
|
||||
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
|
||||
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')
|
||||
|
||||
largest_size = 0
|
||||
@@ -1083,6 +1112,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
def getMetadata(path, originalpath):
|
||||
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
||||
options.comicinfo_chapters = []
|
||||
options.comicinfo_xml = None
|
||||
options.summary = ''
|
||||
titleSuffix = ''
|
||||
options.volume = ''
|
||||
@@ -1139,6 +1169,10 @@ def getMetadata(path, originalpath):
|
||||
options.summary = xml.data['Summary']
|
||||
if 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)
|
||||
|
||||
if originalpath.lower().endswith('.pdf'):
|
||||
@@ -1181,14 +1215,22 @@ def removeNonImages(filetree):
|
||||
os.remove(os.path.join(root, name))
|
||||
# remove empty nested folders
|
||||
for root, dirs, files in os.walk(filetree, False):
|
||||
if not files and not dirs:
|
||||
if not os.listdir(root):
|
||||
os.rmdir(root)
|
||||
|
||||
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 = {}
|
||||
page = 1
|
||||
cover_path = None
|
||||
@@ -1213,7 +1255,7 @@ def sanitizeTree(filetree, prefix='kcc'):
|
||||
dirs.sort(key=OS_SORT_KEY)
|
||||
for i, name in enumerate(dirs):
|
||||
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():
|
||||
slugified += "A"
|
||||
chapterNames[slugified] = tmpName
|
||||
@@ -1234,6 +1276,7 @@ def flattenTree(filetree):
|
||||
|
||||
|
||||
def sanitizePermissions(filetree):
|
||||
os.chmod(filetree, S_IWRITE | S_IREAD | S_IEXEC)
|
||||
for root, dirs, files in os.walk(filetree, False):
|
||||
for name in files:
|
||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
||||
@@ -1375,7 +1418,7 @@ def createNewTome(parent):
|
||||
return tomePath, tomePathRoot
|
||||
|
||||
|
||||
def slugify(value, is_natural_sorted):
|
||||
def slugify(value, options, is_natural_sorted):
|
||||
if options.format == 'CBZ' and is_natural_sorted:
|
||||
return value
|
||||
if options.format != 'CBZ':
|
||||
@@ -1388,7 +1431,6 @@ def slugify(value, is_natural_sorted):
|
||||
|
||||
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||
start = perf_counter()
|
||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||
if SEVENZIP in available_archive_tools():
|
||||
if isepub:
|
||||
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||
@@ -1431,10 +1473,18 @@ def makeParser():
|
||||
" [Default=KV]")
|
||||
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||
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,
|
||||
help="Try to increase the quality of magnification")
|
||||
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
|
||||
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,
|
||||
help="Webtoon processing mode"),
|
||||
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
|
||||
@@ -1448,8 +1498,12 @@ def makeParser():
|
||||
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 "
|
||||
"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",
|
||||
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",
|
||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
|
||||
"[Default=Auto]")
|
||||
@@ -1557,6 +1611,9 @@ def checkOptions(options):
|
||||
else:
|
||||
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):
|
||||
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
|
||||
|
||||
@@ -1676,6 +1733,11 @@ def checkTools(source):
|
||||
except (FileNotFoundError, CalledProcessError):
|
||||
print('ERROR: KindleGen is missing!')
|
||||
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-'):
|
||||
@@ -1690,8 +1752,8 @@ def makeFusion(sources: List[str]):
|
||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
||||
start = perf_counter()
|
||||
first_path = Path(sources[0])
|
||||
|
||||
if True:
|
||||
|
||||
if options.tempdir:
|
||||
fusion_parent = first_path.parent
|
||||
else:
|
||||
# LLL is after KCC
|
||||
@@ -1721,8 +1783,10 @@ def makeFusion(sources: List[str]):
|
||||
else:
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
|
||||
|
||||
getWorkFolder(source, str(targetpath))
|
||||
sanitizeTree(targetpath, prefix='fusion')
|
||||
path = getWorkFolder(source, options, str(targetpath))
|
||||
if path != str(targetpath):
|
||||
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
|
||||
sanitizeTree(targetpath, options, prefix='fusion')
|
||||
# TODO: remove flattenTree when subchapters are supported
|
||||
flattenTree(targetpath)
|
||||
|
||||
@@ -1745,12 +1809,60 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
if not options.filefusion:
|
||||
checkPre('LLL-')
|
||||
print(f"{job_progress}Preparing source images...")
|
||||
path = getWorkFolder(source)
|
||||
path = getWorkFolder(source, options)
|
||||
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)
|
||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
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:
|
||||
# Strip the fusion_0001_ sort prefix from makeFusion if present
|
||||
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
|
||||
@@ -1836,8 +1948,11 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
else:
|
||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||
if cover and cover.smartcover:
|
||||
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)
|
||||
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)
|
||||
elif options.format == 'PDF':
|
||||
print(f"{job_progress}Creating PDF file with PyMuPDF...")
|
||||
# determine output filename based on source and tome count
|
||||
@@ -1856,19 +1971,11 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
else:
|
||||
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
|
||||
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
||||
makeZIP(tome + '_comic', 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
|
||||
makeZIP(filepath[-1], tome, job_progress, True)
|
||||
rmtree(tome, True)
|
||||
if GUI:
|
||||
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...")
|
||||
work = []
|
||||
for i in filepath:
|
||||
@@ -1887,8 +1994,6 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
if not output[0]:
|
||||
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
|
||||
return filepath
|
||||
else:
|
||||
os.remove(i.replace('.epub', '.mobi') + '_toclean')
|
||||
if cover and k.path and k.coverSupport:
|
||||
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
|
||||
if options.delete:
|
||||
@@ -1899,22 +2004,23 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
|
||||
end = perf_counter()
|
||||
print(f"{job_progress}makeBook: {end - start} seconds")
|
||||
# Clean up temporary workspace
|
||||
try:
|
||||
rmtree(path, True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if options.filefusion:
|
||||
rmtree(source, True)
|
||||
checkPre('LLL-')
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def makeMOBIFix(item, uuid):
|
||||
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
|
||||
if options.ebok:
|
||||
is_pdoc = False
|
||||
if not options.keep_epub:
|
||||
os.remove(item)
|
||||
mobiPath = item.replace('.epub', '.mobi')
|
||||
move(mobiPath, mobiPath + '_toclean')
|
||||
try:
|
||||
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
|
||||
dualmetafix.DualMobiMetaFix(mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
|
||||
return [True]
|
||||
except Exception as err:
|
||||
return [False, format(err)]
|
||||
@@ -1924,8 +2030,14 @@ def makeMOBIWorkerTick(output):
|
||||
makeMOBIWorkerOutput.append(output)
|
||||
if output[0] != 0:
|
||||
makeMOBIWorkerPool.terminate()
|
||||
for warning in output[3]:
|
||||
print(warning)
|
||||
if GUI:
|
||||
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:
|
||||
makeMOBIWorkerPool.terminate()
|
||||
|
||||
@@ -1935,33 +2047,58 @@ def makeMOBIWorker(item):
|
||||
kindlegenErrorCode = 0
|
||||
kindlegenError = ''
|
||||
try:
|
||||
# TODO: This size check is incorrect, I think kindlegen increased the limit
|
||||
if os.path.getsize(item) < 629145600:
|
||||
start = perf_counter()
|
||||
# TODO: should anything be done with the kindlegen output during successes?
|
||||
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
|
||||
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
|
||||
end = perf_counter()
|
||||
print(f"kindlegen: {end - start} sec")
|
||||
else:
|
||||
# ERROR: EPUB too big
|
||||
kindlegenErrorCode = 23026
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
return [kindlegenErrorCode, kindlegenError, item, []]
|
||||
except CalledProcessError as err:
|
||||
warnings = []
|
||||
for line in err.stdout.splitlines():
|
||||
# ERROR: Generic error
|
||||
if "Error(" in line:
|
||||
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
|
||||
if ":E23026:" in line:
|
||||
kindlegenErrorCode = 23026
|
||||
if ":E23028:" in line:
|
||||
kindlegenErrorCode = 23028
|
||||
if kindlegenErrorCode > 0:
|
||||
break
|
||||
if ":I1036: Mobi file built successfully" in line:
|
||||
return [0, '', item]
|
||||
return [0, '', item, warnings]
|
||||
if ":I1037: Mobi file built with WARNINGS!" in line:
|
||||
return [0, '', item]
|
||||
return [0, '', item, warnings]
|
||||
# ERROR: KCC unknown generic error
|
||||
if kindlegenErrorCode == 0:
|
||||
kindlegenErrorCode = err.returncode
|
||||
kindlegenError = err.stdout
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
kindlegenErrorCode = -1
|
||||
if err.returncode == 3221226505:
|
||||
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):
|
||||
|
||||
@@ -22,7 +22,6 @@ from functools import cached_property, lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import distro
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.parsers.expat import ExpatError
|
||||
@@ -41,12 +40,12 @@ class ComicArchive:
|
||||
self.dirname, self.basename = os.path.split(filepath)
|
||||
|
||||
@cached_property
|
||||
def type(self):
|
||||
def type(self):
|
||||
extraction_commands = [
|
||||
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||
]
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'l', '-y', '-p1', self.basename],
|
||||
)
|
||||
@@ -85,20 +84,20 @@ class ComicArchive:
|
||||
|
||||
extraction_commands.reverse()
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||
)
|
||||
|
||||
|
||||
for cmd in extraction_commands:
|
||||
try:
|
||||
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||
return targetdir
|
||||
return targetdir
|
||||
except FileNotFoundError:
|
||||
missing.append(cmd[0])
|
||||
except CalledProcessError:
|
||||
pass
|
||||
|
||||
|
||||
if missing:
|
||||
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
|
||||
else:
|
||||
|
||||
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
|
||||
|
||||
|
||||
class DualMobiMetaFix:
|
||||
def __init__(self, infile, outfile, asin, is_pdoc):
|
||||
def __init__(self, outfile, asin, is_pdoc):
|
||||
cdetype = b'EBOK'
|
||||
if is_pdoc:
|
||||
cdetype = b'PDOC'
|
||||
|
||||
shutil.copyfile(infile, outfile)
|
||||
f = open(outfile, "r+b")
|
||||
self.datain = mmap.mmap(f.fileno(), 0)
|
||||
self.datain_rec0 = readsection(self.datain, 0)
|
||||
|
||||
@@ -648,7 +648,7 @@ class Cover:
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((w * .36, 0, w, h))
|
||||
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):
|
||||
try:
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
# Below requirements are compiled in Dockefile
|
||||
# numpy==2.3.4
|
||||
# PyMuPDF==1.26.6
|
||||
@@ -1,11 +1,10 @@
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy<2
|
||||
PyMuPDF==1.25.5
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
PySide6==6.1.3
|
||||
Pillow>=9
|
||||
psutil>=5.9.5
|
||||
psutil>=7.2.2
|
||||
requests>=2.32.4
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy==1.23.0
|
||||
numpy==1.23.5
|
||||
PyMuPDF>=1.16
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
PySide6<6.10
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4,<9.0.0
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy>=1.22.4
|
||||
PyMuPDF>=1.18.0
|
||||
|
||||
@@ -38,7 +38,7 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
def run(self):
|
||||
VERSION = __version__
|
||||
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
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
|
||||
if min_os.startswith('10.1'):
|
||||
@@ -155,7 +155,6 @@ setuptools.setup(
|
||||
'python-slugify>=1.2.1,<9.0.0',
|
||||
'mozjpeg-lossless-optimization>=1.2.0',
|
||||
'natsort>=8.4.0',
|
||||
'distro>=1.8.0',
|
||||
'numpy>=1.22.4',
|
||||
'packaging>=23.2',
|
||||
'PyMuPDF>=1.16.1',
|
||||
|
||||
Reference in New Issue
Block a user