mirror of
https://github.com/ciromattia/kcc
synced 2026-05-18 13:31:45 +00:00
Compare commits
36 Commits
v10.0.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c405066c68 | ||
|
|
7ceeb29fae | ||
|
|
c385ef7ae0 | ||
|
|
9827f11944 | ||
|
|
8030884148 | ||
|
|
949fb0acb4 | ||
|
|
84f69a0950 | ||
|
|
868a31cb00 | ||
|
|
1af24b394f | ||
|
|
401876da22 | ||
|
|
ffeaaeca19 | ||
|
|
b95bb12393 | ||
|
|
4a6e4622ed | ||
|
|
a491810810 | ||
|
|
75d0342fe1 | ||
|
|
60d41b25e4 | ||
|
|
0db788589d | ||
|
|
f42e6aea5c | ||
|
|
53ae057cbb | ||
|
|
d729839976 | ||
|
|
42a50ed670 | ||
|
|
e6ef7c1732 | ||
|
|
f2a806a42a | ||
|
|
8798d71bfa | ||
|
|
19ce14eeee | ||
|
|
5a1e2dafcb | ||
|
|
f149ae23f3 | ||
|
|
2878e5d41b | ||
|
|
bd691989a9 | ||
|
|
d4aeb798c7 | ||
|
|
997a514e2a | ||
|
|
a3672f7a1e | ||
|
|
1b48a9fc5e | ||
|
|
d5dde46989 | ||
|
|
92c85c18e9 | ||
|
|
e5122cc188 |
10
.github/workflows/docker-publish.yml
vendored
10
.github/workflows/docker-publish.yml
vendored
@@ -24,17 +24,17 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Release Date
|
||||
id: release_date
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||
# Always creates the "latest" tag
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
type=raw,value=${{ steps.release_date.outputs.release_date }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
|
||||
4
.github/workflows/package-linux.yml
vendored
4
.github/workflows/package-linux.yml
vendored
@@ -59,12 +59,12 @@ jobs:
|
||||
env:
|
||||
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: AppImage
|
||||
path: './*.AppImage*'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
4
.github/workflows/package-macos.yml
vendored
4
.github/workflows/package-macos.yml
vendored
@@ -80,12 +80,12 @@ jobs:
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-os-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
4
.github/workflows/package-osx-legacy.yml
vendored
4
.github/workflows/package-osx-legacy.yml
vendored
@@ -51,12 +51,12 @@ jobs:
|
||||
run: |
|
||||
python3 setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: osx-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
6
.github/workflows/package-windows.yml
vendored
6
.github/workflows/package-windows.yml
vendored
@@ -53,12 +53,12 @@ jobs:
|
||||
python setup.py ${{ matrix.command }}
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
uses: signpath/github-action-submit-signing-request@v2.2
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
6
.github/workflows/package-windows7.yml
vendored
6
.github/workflows/package-windows7.yml
vendored
@@ -46,12 +46,12 @@ jobs:
|
||||
python setup.py build_binary
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
uses: signpath/github-action-submit-signing-request@v2.2
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
setup.bat
|
||||
kindlecomicconverter/sentry.py
|
||||
other/windows/kindlegen.exe
|
||||
dist/
|
||||
build/
|
||||
|
||||
@@ -199,6 +199,8 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
||||
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
@@ -245,7 +247,7 @@ MAIN:
|
||||
|
||||
PROCESSING:
|
||||
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
|
||||
--legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
|
||||
--pdfwidth Render vector PDFs based on device width instead of height.
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
@@ -267,7 +269,7 @@ PROCESSING:
|
||||
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
|
||||
--blackborders Disable autodetection and force black borders
|
||||
--whiteborders Disable autodetection and force white borders
|
||||
--nosmartcovercrop Disable attempt to crop main cover from wide image
|
||||
--smartcovercrop Attempt to crop main cover from wide image
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG for black and white images
|
||||
@@ -295,6 +297,7 @@ OUTPUT SETTINGS:
|
||||
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
||||
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||
--onepagelandscape Show a single centered page in landscape
|
||||
--norotate Do not rotate double page spreads in spread splitter option.
|
||||
--rotateright Rotate double page spreads in opposite direction.
|
||||
--rotatefirst Put rotated spread first in spread splitter option.
|
||||
@@ -440,7 +443,6 @@ Older links (dead):
|
||||
## PRIVACY
|
||||
**KCC** is initiating internet connections in two cases:
|
||||
* During startup - Version check and announcement check.
|
||||
* When error occurs - Automatic reporting on Windows and macOS.
|
||||
|
||||
## KNOWN ISSUES
|
||||
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||
|
||||
28
gui/KCC.ui
28
gui/KCC.ui
@@ -655,12 +655,12 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="QCheckBox" name="noSmartCoverCropBox">
|
||||
<widget class="QCheckBox" name="smartCoverCropBox">
|
||||
<property name="toolTip">
|
||||
<string>Disable attempt to crop main cover from wide image.</string>
|
||||
<string><html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>No Smart Cover Crop</string>
|
||||
<string>Smart Cover Crop</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -750,14 +750,12 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QCheckBox" name="pdfExtractBox">
|
||||
<widget class="QCheckBox" name="legacyExtractBox">
|
||||
<property name="toolTip">
|
||||
<string>Use the PDF image extraction method from KCC 8 and earlier.
|
||||
|
||||
Useful for really weird PDFs.</string>
|
||||
<string><html><head/><body><p>Use the PDF/EPUB image extraction method from older KCC versions.</p><p><br/></p><p>Use if standard extraction fails for whatever reason.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>PDF Legacy Extract</string>
|
||||
<string>Legacy Extract</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -866,7 +864,7 @@ Useful if you plan to crop a little off the top and bottom to fill screen.</stri
|
||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Autocontrast</string>
|
||||
<string>Custom Autocontrast</string>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>true</bool>
|
||||
@@ -885,7 +883,7 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<item row="12" column="2">
|
||||
<widget class="QCheckBox" name="tempDirBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html></string>
|
||||
@@ -895,6 +893,16 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QCheckBox" name="onePageLandscapeBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 2 page landscape<br/></span>2 viewports for left and right pages</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 1 page landscape<br/></span>A single centered viewport for 1 page</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>1 Page Landscape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -38,7 +38,6 @@ from xml.sax.saxutils import escape
|
||||
from psutil import Process
|
||||
from copy import copy
|
||||
from packaging.version import Version
|
||||
from raven import Client
|
||||
from tempfile import gettempdir
|
||||
|
||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
||||
@@ -327,12 +326,12 @@ class WorkerThread(QThread):
|
||||
options.maximizestrips = True
|
||||
if GUI.disableProcessingBox.isChecked():
|
||||
options.noprocessing = True
|
||||
if GUI.pdfExtractBox.isChecked():
|
||||
options.pdfextract = True
|
||||
if GUI.legacyExtractBox.isChecked():
|
||||
options.legacyextract = True
|
||||
if GUI.pdfWidthBox.isChecked():
|
||||
options.pdfwidth = True
|
||||
if GUI.noSmartCoverCropBox.isChecked():
|
||||
options.nosmartcovercrop = True
|
||||
if GUI.smartCoverCropBox.isChecked():
|
||||
options.smartcovercrop = True
|
||||
if GUI.coverFillBox.isChecked():
|
||||
options.coverfill = True
|
||||
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
@@ -345,6 +344,8 @@ class WorkerThread(QThread):
|
||||
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:
|
||||
@@ -445,8 +446,6 @@ class WorkerThread(QThread):
|
||||
_, _, traceback = sys.exc_info()
|
||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
||||
if ' is corrupted.' not in str(err):
|
||||
GUI.sentry.captureException()
|
||||
MW.addMessage.emit('Error during conversion! Please consult '
|
||||
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
|
||||
'for more details.', 'error', False)
|
||||
@@ -564,6 +563,7 @@ class WorkerThread(QThread):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
comic2ebook.checkPre('LLL-')
|
||||
GUI.progress.content = ''
|
||||
GUI.progress.stop()
|
||||
MW.hideProgressBar.emit()
|
||||
@@ -627,7 +627,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.jobList.clear()
|
||||
if self.tar or self.sevenzip:
|
||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
|
||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
|
||||
else:
|
||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||
'Comic (*.pdf);;All (*.*)')
|
||||
@@ -681,7 +681,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.editor.loadData(sname)
|
||||
except Exception as err:
|
||||
_, _, traceback = sys.exc_info()
|
||||
GUI.sentry.captureException()
|
||||
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||
else:
|
||||
@@ -946,10 +945,16 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
|
||||
GUI.chunkSizeCheckBox.setEnabled(False)
|
||||
GUI.chunkSizeCheckBox.setChecked(False)
|
||||
elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
|
||||
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
GUI.upscaleBox.setChecked(True)
|
||||
elif not GUI.webtoonBox.isChecked():
|
||||
GUI.chunkSizeCheckBox.setEnabled(True)
|
||||
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
|
||||
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
else:
|
||||
GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
|
||||
|
||||
def stripTags(self, html):
|
||||
s = HTMLStripper()
|
||||
@@ -1077,9 +1082,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'colorBox': GUI.colorBox.checkState(),
|
||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||
'legacyExtractBox': GUI.legacyExtractBox.checkState(),
|
||||
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(),
|
||||
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||
@@ -1094,6 +1099,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'deleteBox': GUI.deleteBox.checkState(),
|
||||
'tempDirBox': GUI.tempDirBox.checkState(),
|
||||
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
||||
'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
|
||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||
@@ -1117,7 +1123,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.jobList.clear()
|
||||
formats = ['.pdf']
|
||||
if self.tar or self.sevenzip:
|
||||
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
|
||||
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
|
||||
if os.path.isdir(message):
|
||||
GUI.jobList.addItem(message)
|
||||
GUI.jobList.scrollToBottom()
|
||||
@@ -1204,7 +1210,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.croppingPowerValue = 1.0
|
||||
self.currentMode = 1
|
||||
self.targetDirectory = ''
|
||||
self.sentry = Client(release=__version__)
|
||||
if sys.platform.startswith('win'):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from psutil import BELOW_NORMAL_PRIORITY_CLASS
|
||||
@@ -1231,7 +1236,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
||||
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
||||
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
||||
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
||||
"KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
|
||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
|
||||
@@ -1256,6 +1261,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"Kindle 1240x1860": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
|
||||
},
|
||||
"Kindle 1324x1986": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1324',
|
||||
},
|
||||
"Kindle Scribe 1/2": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
||||
},
|
||||
@@ -1363,6 +1371,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"Separator",
|
||||
"Other",
|
||||
"Separator",
|
||||
"Kindle 1324x1986",
|
||||
"Kindle 1920x1920",
|
||||
"Kindle 1860x1920",
|
||||
"Kindle 1240x1860",
|
||||
@@ -1570,7 +1579,6 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
||||
self.parser.saveXML()
|
||||
except Exception as err:
|
||||
_, _, traceback = sys.exc_info()
|
||||
GUI.sentry.captureException()
|
||||
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||
self.ui.close()
|
||||
|
||||
@@ -344,10 +344,10 @@ class Ui_mainWindow(object):
|
||||
|
||||
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
|
||||
|
||||
self.noSmartCoverCropBox = QCheckBox(self.optionWidget)
|
||||
self.noSmartCoverCropBox.setObjectName(u"noSmartCoverCropBox")
|
||||
self.smartCoverCropBox = QCheckBox(self.optionWidget)
|
||||
self.smartCoverCropBox.setObjectName(u"smartCoverCropBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.noSmartCoverCropBox, 11, 1, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.smartCoverCropBox, 11, 1, 1, 1)
|
||||
|
||||
self.rotateFirstBox = QCheckBox(self.optionWidget)
|
||||
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
||||
@@ -389,10 +389,10 @@ class Ui_mainWindow(object):
|
||||
|
||||
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
||||
|
||||
self.pdfExtractBox = QCheckBox(self.optionWidget)
|
||||
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
|
||||
self.legacyExtractBox = QCheckBox(self.optionWidget)
|
||||
self.legacyExtractBox.setObjectName(u"legacyExtractBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.legacyExtractBox, 9, 0, 1, 1)
|
||||
|
||||
self.colorBox = QCheckBox(self.optionWidget)
|
||||
self.colorBox.setObjectName(u"colorBox")
|
||||
@@ -453,7 +453,12 @@ class Ui_mainWindow(object):
|
||||
self.tempDirBox = QCheckBox(self.optionWidget)
|
||||
self.tempDirBox.setObjectName(u"tempDirBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.tempDirBox, 12, 1, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.tempDirBox, 12, 2, 1, 1)
|
||||
|
||||
self.onePageLandscapeBox = QCheckBox(self.optionWidget)
|
||||
self.onePageLandscapeBox.setObjectName(u"onePageLandscapeBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.onePageLandscapeBox, 12, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||
@@ -753,9 +758,9 @@ class Ui_mainWindow(object):
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.noSmartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable attempt to crop main cover from wide image.", None))
|
||||
self.smartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.noSmartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"No Smart Cover Crop", None))
|
||||
self.smartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"Smart Cover Crop", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
@@ -785,11 +790,9 @@ class Ui_mainWindow(object):
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pdfExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF image extraction method from KCC 8 and earlier.\n"
|
||||
"\n"
|
||||
"Useful for really weird PDFs.", None))
|
||||
self.legacyExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use the PDF/EPUB image extraction method from older KCC versions.</p><p><br/></p><p>Use if standard extraction fails for whatever reason.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
|
||||
self.legacyExtractBox.setText(QCoreApplication.translate("mainWindow", u"Legacy Extract", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
@@ -819,7 +822,7 @@ class Ui_mainWindow(object):
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Autocontrast", None))
|
||||
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Custom Autocontrast", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.webpBox.setToolTip(QCoreApplication.translate("mainWindow", u"Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.\n"
|
||||
"\n"
|
||||
@@ -830,6 +833,10 @@ class Ui_mainWindow(object):
|
||||
self.tempDirBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.onePageLandscapeBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 2 page landscape<br/></span>2 viewports for left and right pages</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 1 page landscape<br/></span>A single centered viewport for 1 page</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.onePageLandscapeBox.setText(QCoreApplication.translate("mainWindow", u"1 Page Landscape", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '10.0.1'
|
||||
__version__ = '10.1.3'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
from collections import Counter
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from argparse import ArgumentParser
|
||||
from time import perf_counter, strftime, gmtime
|
||||
from copy import copy
|
||||
@@ -29,8 +32,8 @@ from glob import glob, escape
|
||||
from re import sub
|
||||
from stat import S_IWRITE, S_IREAD, S_IEXEC
|
||||
from typing import List
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||
from zipfile import ZipFile, ZIP_STORED
|
||||
from tempfile import mkdtemp, gettempdir
|
||||
from shutil import move, copytree, rmtree, copyfile
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from uuid import uuid4
|
||||
@@ -43,7 +46,7 @@ from psutil import virtual_memory, disk_usage
|
||||
from html import escape as hescape
|
||||
import pymupdf
|
||||
|
||||
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
|
||||
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean, get_contain_resolution
|
||||
from .comicarchive import SEVENZIP, available_archive_tools
|
||||
from . import comic2panel
|
||||
from . import image
|
||||
@@ -89,6 +92,7 @@ def main(argv=None):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
checkPre('LLL-')
|
||||
return 0
|
||||
|
||||
|
||||
@@ -143,7 +147,7 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
||||
f.write('<div style="display:none;">.</div>\n')
|
||||
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
|
||||
if imgfile2:
|
||||
f.write(f'<img style="bottom: 0" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
||||
f.write(f'<img style="top: 1920px" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
||||
f.write("</div>\n")
|
||||
if options.iskindle and options.panelview:
|
||||
if options.autoscale:
|
||||
@@ -324,8 +328,19 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
|
||||
if options.iskindle and options.profile != 'Custom':
|
||||
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
||||
"<meta name=\"original-resolution\" content=\"",
|
||||
str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
|
||||
])
|
||||
if not options.kfx_resolution:
|
||||
f.writelines([
|
||||
"<meta name=\"original-resolution\" content=\"",
|
||||
str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
|
||||
])
|
||||
else:
|
||||
x, y = options.kfx_resolution
|
||||
f.writelines([
|
||||
"<meta name=\"original-resolution\" content=\"",
|
||||
str(x) + "x" + str(y) + "\"/>\n",
|
||||
])
|
||||
f.writelines([
|
||||
"<meta name=\"book-type\" content=\"comic\"/>\n",
|
||||
"<meta name=\"primary-writing-mode\" content=\"" + writingmode + "\"/>\n",
|
||||
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
||||
@@ -454,6 +469,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
pageside = "right"
|
||||
|
||||
for entry, prop in zip(reflist, page_spread_property_list):
|
||||
if options.onepagelandscape:
|
||||
prop = 'center'
|
||||
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
|
||||
|
||||
f.write("</spine>\n</package>\n")
|
||||
@@ -880,9 +897,12 @@ def getWorkFolder(afile, workdir=None):
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
else:
|
||||
fullPath = workdir
|
||||
check_path = gettempdir()
|
||||
|
||||
if options.tempdir:
|
||||
check_path = os.path.dirname(afile)
|
||||
else:
|
||||
check_path = gettempdir()
|
||||
|
||||
if os.path.isdir(afile):
|
||||
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
@@ -901,7 +921,7 @@ def getWorkFolder(afile, workdir=None):
|
||||
os.makedirs(fullPath)
|
||||
path = workdir
|
||||
sanitizePermissions(path)
|
||||
if options.pdfextract:
|
||||
if options.legacyextract:
|
||||
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||
njpg = pdf.extract()
|
||||
if njpg == 0:
|
||||
@@ -940,11 +960,68 @@ def getWorkFolder(afile, workdir=None):
|
||||
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]))
|
||||
|
||||
if options.legacyextract:
|
||||
return workdir
|
||||
|
||||
if afile.lower().endswith('.epub'):
|
||||
container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
|
||||
opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
|
||||
opf_path = os.path.join(path, opf_path)
|
||||
opf = ET.parse(opf_path)
|
||||
spine = []
|
||||
for spine_item in opf.findall(r'.//{*}itemref'):
|
||||
spine.append(spine_item.attrib.get('idref'))
|
||||
manifest_dict = {}
|
||||
for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
|
||||
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:
|
||||
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
|
||||
img_path = None
|
||||
for img in imgs:
|
||||
for key in img.attrib:
|
||||
if 'src' in key or 'href' in key:
|
||||
temp_img_path = img.attrib[key]
|
||||
if temp_img_path.startswith('..'):
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
else:
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
try:
|
||||
temp_size = os.path.getsize(temp_img_path)
|
||||
if temp_size > largest_size:
|
||||
largest_size = temp_size
|
||||
img_path = temp_img_path
|
||||
except OSError:
|
||||
pass
|
||||
# TODO empty image
|
||||
if img_path:
|
||||
ordered_image_paths.append(img_path)
|
||||
# fallback if naive spine extraction fails
|
||||
if not ordered_image_paths:
|
||||
return workdir
|
||||
|
||||
if options.tempdir:
|
||||
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
else:
|
||||
workdir2 = mkdtemp('', 'KCC-')
|
||||
for i, img_path in enumerate(ordered_image_paths):
|
||||
_, ext = os.path.splitext(img_path)
|
||||
fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
|
||||
os.makedirs(fullpath2, exist_ok=True)
|
||||
shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
|
||||
rmtree(workdir, True)
|
||||
return workdir2
|
||||
|
||||
return workdir
|
||||
|
||||
except OSError as e:
|
||||
rmtree(workdir, True)
|
||||
raise UserWarning(e)
|
||||
finally:
|
||||
pass
|
||||
else:
|
||||
raise UserWarning("Failed to open source file/directory.")
|
||||
|
||||
@@ -1381,6 +1458,8 @@ def makeParser():
|
||||
"2: Consider every subdirectory as separate volume [Default=0]")
|
||||
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
|
||||
help="Shift first page to opposite side in landscape for spread alignment")
|
||||
output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
|
||||
help="Show a single centered page in landscape")
|
||||
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
|
||||
help="Do not rotate double page spreads in spread splitter option.")
|
||||
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
|
||||
@@ -1390,12 +1469,12 @@ def makeParser():
|
||||
|
||||
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
|
||||
help="Do not modify image and ignore any profile or processing option")
|
||||
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
|
||||
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
|
||||
processing_options.add_argument("--legacyextract", action="store_true", dest="legacyextract", default=False,
|
||||
help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
|
||||
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
|
||||
help="Render vector PDFs to device width instead of height.")
|
||||
processing_options.add_argument("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False,
|
||||
help="Disable attempt to crop main cover from wide image")
|
||||
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
|
||||
help="Attempt to crop main cover from wide image")
|
||||
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
|
||||
help="Crop cover to fill screen")
|
||||
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||
@@ -1470,6 +1549,15 @@ def checkOptions(options):
|
||||
options.isKobo = False
|
||||
options.bordersColor = None
|
||||
options.keep_epub = False
|
||||
|
||||
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||
options.iskindle = True
|
||||
else:
|
||||
options.isKobo = 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')
|
||||
|
||||
if options.format == 'PDF-200MB':
|
||||
options.targetsize = 195
|
||||
options.format = 'PDF'
|
||||
@@ -1501,10 +1589,7 @@ def checkOptions(options):
|
||||
options.format = 'PDF'
|
||||
else:
|
||||
options.format = 'EPUB'
|
||||
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||
options.iskindle = True
|
||||
else:
|
||||
options.isKobo = True
|
||||
|
||||
if options.white_borders:
|
||||
options.bordersColor = 'white'
|
||||
if options.black_borders:
|
||||
@@ -1535,6 +1620,7 @@ def checkOptions(options):
|
||||
options.hq = False
|
||||
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
||||
if options.format == 'KFX':
|
||||
options.targetsize = 195
|
||||
options.format = 'EPUB'
|
||||
options.kfx = True
|
||||
options.panelview = False
|
||||
@@ -1556,6 +1642,7 @@ def checkOptions(options):
|
||||
options.jpegquality = 90
|
||||
else:
|
||||
options.jpegquality = 85
|
||||
|
||||
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
|
||||
|
||||
@@ -1589,33 +1676,30 @@ def checkTools(source):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def checkPre(source):
|
||||
def checkPre(source='KCC-'):
|
||||
# Make sure that all temporary files are gone
|
||||
for root, dirs, _ in walkLevel(gettempdir(), 0):
|
||||
for tempdir in dirs:
|
||||
if tempdir.startswith('KCC-'):
|
||||
if tempdir.startswith(source):
|
||||
rmtree(os.path.join(root, tempdir), True)
|
||||
# Make sure that target directory is writable
|
||||
if os.path.isdir(source):
|
||||
src = os.path.abspath(os.path.join(source, '..'))
|
||||
else:
|
||||
src = os.path.dirname(source)
|
||||
try:
|
||||
with TemporaryFile(prefix='KCC-', dir=src):
|
||||
pass
|
||||
except Exception:
|
||||
raise UserWarning("Target directory is not writable.")
|
||||
|
||||
|
||||
def makeFusion(sources: List[str]):
|
||||
if len(sources) < 2:
|
||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
||||
start = perf_counter()
|
||||
first_path = Path(sources[0])
|
||||
if first_path.is_file():
|
||||
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
|
||||
|
||||
if options.tempdir:
|
||||
fusion_parent = first_path.parent
|
||||
else:
|
||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||
# LLL is after KCC
|
||||
checkPre('LLL-')
|
||||
fusion_parent = Path(mkdtemp('', 'LLL-'))
|
||||
|
||||
if first_path.is_file():
|
||||
fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
|
||||
else:
|
||||
fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
|
||||
print("Running Fusion")
|
||||
|
||||
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||
@@ -1624,7 +1708,6 @@ def makeFusion(sources: List[str]):
|
||||
|
||||
for index, source in enumerate(sources, start=1):
|
||||
print(f"Processing {source}...")
|
||||
checkPre(source)
|
||||
print("Checking images...")
|
||||
source_path = Path(source)
|
||||
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||
@@ -1656,7 +1739,9 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
GUI.progressBarTick.emit('1')
|
||||
else:
|
||||
checkTools(source)
|
||||
checkPre(source)
|
||||
checkPre()
|
||||
if not options.filefusion:
|
||||
checkPre('LLL-')
|
||||
print(f"{job_progress}Preparing source images...")
|
||||
path = getWorkFolder(source)
|
||||
print(f"{job_progress}Checking images...")
|
||||
@@ -1671,9 +1756,43 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
if not options.webtoon:
|
||||
cover = image.Cover(cover_path, options)
|
||||
|
||||
x, y = image.ProfileData.Profiles[options.profile][1]
|
||||
if options.webtoon:
|
||||
x, y = image.ProfileData.Profiles[options.profile][1]
|
||||
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
|
||||
|
||||
options.kfx_resolution = None
|
||||
if options.kfx:
|
||||
original_resolutions = []
|
||||
normalized_resolutions = []
|
||||
for root, _, files in os.walk(os.path.join(path, "OEBPS", "Images")):
|
||||
for file in files:
|
||||
with Image.open(os.path.join(root, file)) as imagef:
|
||||
original_resolutions.append(imagef.size)
|
||||
size = get_contain_resolution(imagef, (x, y))
|
||||
normalized_resolutions.append(size)
|
||||
|
||||
counter = Counter(normalized_resolutions)
|
||||
|
||||
aspect_ratios = []
|
||||
filtered_resolutions = []
|
||||
for w, h in normalized_resolutions:
|
||||
aspect_ratio = h / w
|
||||
# page-like aspect ratios, could be improved
|
||||
if aspect_ratio > 1.3 and aspect_ratio < 1.7:
|
||||
aspect_ratios.append(aspect_ratio)
|
||||
filtered_resolutions.append((w, h))
|
||||
|
||||
most_common_res, most_common_count = counter.most_common(1)[0]
|
||||
options.kfx_resolution = most_common_res
|
||||
if most_common_count / sum(counter.values()) > .6:
|
||||
pass
|
||||
#elif max(aspect_ratios) - min(aspect_ratios) < .2:
|
||||
else:
|
||||
# get the widest resolution
|
||||
options.kfx_resolution = max(filtered_resolutions)
|
||||
# else:
|
||||
# raise UserWarning('Aspect ratio of pages too different for KFX conversion')
|
||||
|
||||
if options.noprocessing:
|
||||
print(f"{job_progress}Do not process image, ignore any profile or processing option")
|
||||
else:
|
||||
|
||||
@@ -29,6 +29,7 @@ from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
|
||||
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||
from .shared import get_contain_resolution
|
||||
|
||||
AUTO_CROP_THRESHOLD = 0.015
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
@@ -105,6 +106,7 @@ class ProfileData:
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
@@ -517,7 +519,17 @@ class ComicPage:
|
||||
ratio_device = float(self.size[1]) / float(self.size[0])
|
||||
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
||||
method = self.resize_method()
|
||||
if self.opt.stretch:
|
||||
if self.opt.kfx:
|
||||
ratio_kfx = self.opt.kfx_resolution[1] / self.opt.kfx_resolution[0]
|
||||
contain_size = get_contain_resolution(self.image, self.size)
|
||||
if abs(ratio_image - ratio_kfx) < AUTO_CROP_THRESHOLD:
|
||||
if contain_size[0] > self.opt.kfx_resolution[0] or contain_size[1] > self.opt.kfx_resolution[1]:
|
||||
self.image = ImageOps.fit(self.image, self.opt.kfx_resolution, method=method)
|
||||
else:
|
||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||
else:
|
||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||
elif self.opt.stretch:
|
||||
self.image = self.image.resize(self.size, method)
|
||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||
pass
|
||||
@@ -526,7 +538,7 @@ class ComicPage:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
|
||||
elif (self.opt.format in ('CBZ', 'PDF')) and not self.opt.white_borders:
|
||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||
else:
|
||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||
@@ -587,7 +599,7 @@ class Cover:
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
if not self.options.forcecolor:
|
||||
self.image = self.image.convert('L')
|
||||
if not self.options.nosmartcovercrop:
|
||||
if self.options.smartcovercrop:
|
||||
self.crop_main_cover()
|
||||
|
||||
size = list(self.options.profileData[1])
|
||||
|
||||
@@ -61,6 +61,23 @@ def getImageFileName(imgfile):
|
||||
ext = ext.lower()
|
||||
return [name, ext]
|
||||
|
||||
def get_contain_resolution(image, size):
|
||||
'''same code as Pillow ImageOps.contain()'''
|
||||
im_ratio = image.width / image.height
|
||||
dest_ratio = size[0] / size[1]
|
||||
|
||||
if im_ratio != dest_ratio:
|
||||
if im_ratio > dest_ratio:
|
||||
new_height = round(image.height / image.width * size[0])
|
||||
if new_height != size[1]:
|
||||
size = (size[0], new_height)
|
||||
else:
|
||||
new_width = round(image.width / image.height * size[1])
|
||||
if new_width != size[0]:
|
||||
size = (new_width, size[1])
|
||||
|
||||
return size
|
||||
|
||||
|
||||
def walkSort(dirnames, filenames):
|
||||
convert = lambda text: int(text) if text.isdigit() else text
|
||||
@@ -105,10 +122,6 @@ def dependencyCheck(level):
|
||||
missing.append('PySide 6.0.0')
|
||||
except ImportError:
|
||||
missing.append('PySide 6.0.0+')
|
||||
try:
|
||||
import raven
|
||||
except ImportError:
|
||||
missing.append('raven 6.0.0+')
|
||||
if level > 1:
|
||||
try:
|
||||
from psutil import __version__ as psutilVersion
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
packaging>=23.2
|
||||
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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
PySide6==6.1.3
|
||||
Pillow>=9
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
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
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
PySide6<6.10
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1,<9.0.0
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user