1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-20 07:58:51 +00:00

Compare commits

...

37 Commits

Author SHA1 Message Date
Alex Xu
a7a9f35686 Update README.md 2026-04-19 10:27:52 -07:00
Alex Xu
d5146d02fc Update README.md 2026-04-19 10:10:18 -07:00
Alex Xu
b0374e127d Smarter covers (#1300)
* smart crop semi-wide covers

* make it even smarter

* make it even smarter

* adjust ratio
2026-04-18 07:39:23 -07:00
Alex Xu
894dbfc8a2 don't bisect images with < 1.16 or > 1.75 aspect ratios (#1301) 2026-04-17 18:03:31 -07:00
Alex Xu
72f98bb032 bump to 9.7.2 2026-04-16 13:30:08 -07:00
Alex Xu
96bf14d386 Kindle Scribe 2025 default is PDF (#1297) 2026-04-16 13:26:04 -07:00
Alex Xu
d4e1565e4a colorsoft is wrong too 2026-04-16 13:25:41 -07:00
Alex Xu
c3030e8bd1 Paperwhite 12th Gen is actually 1272x1696 2026-04-16 13:25:41 -07:00
Alex Xu
c5744117e3 bump to 9.7.1 2026-04-15 21:59:27 -07:00
Alex Xu
cd2eeb4d0f option: create temporary files directory on source file drive (#1296) 2026-04-15 21:40:03 -07:00
Alex Xu
e7b7054b0e smart cover crop is default on and implemented for all formats (#1295) 2026-04-15 21:25:08 -07:00
Alex Xu
6f26bd5874 add epub series metadata (#1294) 2026-04-15 17:22:00 -07:00
Alex Xu
d5ca8fb407 don't bisect images with aspect ratio > 2 (#1293) 2026-04-13 23:03:26 -07:00
Alex Xu
8d61a9e558 bump to 9.7.0 2026-04-12 11:31:27 -07:00
Alex Xu
8aaedf274d add youtube, discord, humble buttons (#1292)
* add youtube, discord, humble buttons

* restore margins of top buttons

* fix button height on windows

* don't bold messages
2026-04-12 11:30:18 -07:00
Alex Xu
5782a44e7b sign windows 7 version (#1291) 2026-04-12 09:17:23 -07:00
Alex Xu
e4c918f0f3 add webp output support (ignored for Kindle MOBI/EPUB and all PDF) (#1290)
* add webp output support (ignored for Kindle MOBI/EPUB)

* disable png extra optons by default

* pdf webp is not supported
2026-04-12 09:04:21 -07:00
Alex Xu
8f4072bfab disable extra png options when png is not selected (#1289) 2026-04-11 19:32:49 -07:00
Alex Xu
61f3097be5 smart cover crop is now an option default off (#1288) 2026-04-11 19:09:04 -07:00
Alex Xu
fa33ef8f89 revert 2x render, too expensive (#1287) 2026-04-11 18:06:03 -07:00
Alex Xu
232bac00a9 ZIP stored instead of deflate (#1283) 2026-04-06 21:03:17 -07:00
Alex Xu
d19a4754fa if cropping, render pdf to 2x (#1276) 2026-03-26 08:09:25 -07:00
Alex Xu
9a93cc4b17 Update __init__.py 2026-03-17 16:11:57 -07:00
Alex Xu
e0471b2dc9 fix autocrop in certain situations (#1274) 2026-03-17 16:11:40 -07:00
Alex Xu
a87eb318cf bump 9.6.1 2026-03-17 15:10:02 -07:00
Alex Xu
87987c9ebf fix humble bundle pdf png autocontrast (#1273) 2026-03-17 15:09:02 -07:00
Alex Xu
f5fe8d93b0 color images are always saved as JPG by default (#1272)
* use jpg for color images always

* add colorOutput variable

* fix typos

* remove dither

* add box

* clarify png

* remove debug code

* remove unneeded check
2026-03-17 13:41:41 -07:00
Alex Xu
249f823f01 add PNG legacy option (#1271) 2026-03-13 21:45:05 -07:00
Alex Xu
3a9d4f274d bump to 9.6.0 2026-03-13 16:29:50 -07:00
Alex Xu
b5de6fd39d add pdf width box (#1270) 2026-03-13 16:28:53 -07:00
Alex Xu
b4b9e41a0c add no quantize option (#1269) 2026-03-13 15:07:41 -07:00
Alex Xu
9b9181a715 Add rotate right option (#1268) 2026-03-13 14:15:14 -07:00
Alex Xu
472fdc97b5 PDF PNG half size, Kindle DX PNG CBZ fixed (#1267)
* PNG for PDF or Kindle DX CBZ is dithered to 16 colors for ~1/2 filesize reduction

* Kindle DX default no borders
2026-03-13 12:17:38 -07:00
Alex Xu
6fdfddd7d9 bump to 9.5.1 2026-03-10 14:00:54 -07:00
Alex Xu
34fb68ac65 downgrade to pyside 6.9 2026-03-10 13:50:59 -07:00
Alex Xu
b4d72cd581 remove setuptools and wheel (#1265) 2026-03-10 13:37:59 -07:00
Alex Xu
1dead9af8f add PDF 200 MB option (#1264) 2026-03-10 13:29:46 -07:00
14 changed files with 1716 additions and 1294 deletions

View File

@@ -35,7 +35,7 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0 sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
- name: build binary - name: build binary
run: | run: |

View File

@@ -38,7 +38,7 @@ jobs:
cache: 'pip' cache: 'pip'
- name: Install python dependencies - name: Install python dependencies
run: | run: |
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi python -m pip install --upgrade pip pyinstaller certifi
pip install -r requirements.txt pip install -r requirements.txt
- name: Install the Apple certificate and provisioning profile - name: Install the Apple certificate and provisioning profile
# TODO signing # TODO signing

View File

@@ -40,7 +40,7 @@ jobs:
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
python3 --version python3 --version
pip3 install --upgrade pip setuptools wheel pyinstaller certifi pip3 install --upgrade pip pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh ./gen_ui_files.sh
- uses: actions/setup-node@v6 - uses: actions/setup-node@v6

View File

@@ -45,7 +45,7 @@ jobs:
env: env:
PYINSTALLER_COMPILE_BOOTLOADER: 1 PYINSTALLER_COMPILE_BOOTLOADER: 1
run: | run: |
python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
pip install certifi pyinstaller --no-binary pyinstaller pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary - name: build binary

View File

@@ -37,7 +37,7 @@ jobs:
env: env:
PYINSTALLER_COMPILE_BOOTLOADER: 1 PYINSTALLER_COMPILE_BOOTLOADER: 1
run: | run: |
python -m pip install --upgrade pip setuptools wheel python -m pip install --upgrade pip
pip install -r requirements-win7.txt pip install -r requirements-win7.txt
pip install certifi pyinstaller --no-binary pyinstaller pip install certifi pyinstaller --no-binary pyinstaller
.\gen_ui_files.bat .\gen_ui_files.bat
@@ -50,6 +50,17 @@ jobs:
with: with:
name: windows7-build name: windows7-build
path: dist/*.exe path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')

View File

@@ -4,7 +4,7 @@ FROM python:3.13-slim-bullseye AS builder
# Install system dependencies # Install system dependencies
RUN set -x && \ RUN set -x && \
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev python3-setuptools python3-wheel" && \ BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev" && \
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \ RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \ DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS} apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}

View File

@@ -11,7 +11,7 @@
like Kindle, Kobo, ReMarkable, and more. like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, Pages display in fullscreen without margins,
with proper fixed layout support. with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs. Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF. Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux. KCC runs on Windows, macOS, and Linux.
@@ -115,15 +115,14 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
## FAQ ## FAQ
- Should I use Calibre? - Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting. - No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
Additionally, it will break page numbers.
Viewing KCC output in Calibre will also not work properly. Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder. Direct USB dropping is reccomended.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- Blank pages? - Blank pages?
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. - May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. You can try PDF output.
Going back a few pages and exiting and re-entering book should fix it temporarily. Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use? - What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable. - MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
- All options have additional information in tooltips if you hover over the option. - All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB - To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Kindle panel view not working? - Kindle panel view not working?
@@ -137,9 +136,6 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps. (no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- How to make AZW3 instead of MOBI? - How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons. - The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- Huge margins / slow page turns? - Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB. - You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -198,8 +194,9 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0), 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0), 'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0), 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0), 'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0), 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
@@ -249,6 +246,7 @@ MAIN:
PROCESSING: PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option -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. --pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
--pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution -u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution -s, --stretch Stretch images to device's resolution
-r SPLITTER, --splitter SPLITTER -r SPLITTER, --splitter SPLITTER
@@ -269,13 +267,19 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0] Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--nosmartcovercrop Disable attempt to crop main cover from wide image
--coverfill Center-crop only the cover to fill target device screen --coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale --forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG --forcepng Create PNG files instead JPEG for black and white images
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
--force-png-rgb Force color images to be saved as PNG
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
--noquantize Don't quantize PNG images to 16 colors
--mozjpeg Create JPEG files using mozJpeg --mozjpeg Create JPEG files using mozJpeg
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices. --jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
--maximizestrips Turn 1x4 strips to 2x2 strips --maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not recoverable. -d, --delete Delete source file(s) or a directory. It's not recoverable.
--tempdir Create temporary files directory on source file drive.
OUTPUT SETTINGS: OUTPUT SETTINGS:
-o OUTPUT, --output OUTPUT -o OUTPUT, --output OUTPUT
@@ -292,6 +296,7 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] 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 --spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option. --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. --rotatefirst Put rotated spread first in spread splitter option.
--filefusion Combines all input files into a single file. --filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies --eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies

1470
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -195,7 +195,7 @@ class VersionThread(QThread):
icon = 'bindle' icon = 'bindle'
if category == 'kofi': if category == 'kofi':
icon = 'kofi' icon = 'kofi'
message = f"<b>{payload.get('name')}</b>" message = f"{payload.get('name')}"
if payload.get('link'): if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name')) message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'): if payload.get('showDeadline'):
@@ -329,6 +329,10 @@ class WorkerThread(QThread):
options.noprocessing = True options.noprocessing = True
if GUI.pdfExtractBox.isChecked(): if GUI.pdfExtractBox.isChecked():
options.pdfextract = True options.pdfextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.noSmartCoverCropBox.isChecked():
options.nosmartcovercrop = True
if GUI.coverFillBox.isChecked(): if GUI.coverFillBox.isChecked():
options.coverfill = True options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -337,6 +341,8 @@ class WorkerThread(QThread):
options.metadatatitle = 2 options.metadatatitle = 2
if GUI.deleteBox.isChecked(): if GUI.deleteBox.isChecked():
options.delete = True options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked(): if GUI.spreadShiftBox.isChecked():
options.spreadshift = True options.spreadshift = True
if GUI.fileFusionBox.isChecked(): if GUI.fileFusionBox.isChecked():
@@ -345,12 +351,22 @@ class WorkerThread(QThread):
options.filefusion = False options.filefusion = False
if GUI.noRotateBox.isChecked(): if GUI.noRotateBox.isChecked():
options.norotate = True options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked(): if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked: elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True 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(): if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value() options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2: if GUI.currentMode > 2:
@@ -682,6 +698,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass # noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude')) QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
def openHumble(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://humblebundleinc.sjv.io/3JaR3A'))
def openYouTube(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://www.youtube.com/@eink-dude'))
def openDiscord(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://discord.gg/um5JRKwmGT'))
def modeChange(self, mode): def modeChange(self, mode):
if mode == 1: if mode == 1:
self.currentMode = 1 self.currentMode = 1
@@ -816,6 +844,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if bad_format in current_format: if bad_format in current_format:
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning') self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
break break
GUI.pngLegacyBox.setEnabled(True)
GUI.noQuantizeBox.setEnabled(True)
GUI.forcePngRgbBox.setEnabled(True)
else:
GUI.pngLegacyBox.setEnabled(False)
GUI.noQuantizeBox.setEnabled(False)
GUI.forcePngRgbBox.setEnabled(False)
def togglechunkSizeCheckBox(self, value): def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value) GUI.chunkSizeWidget.setVisible(value)
@@ -883,6 +919,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if bad_format in current_format: if bad_format in current_format:
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info') self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
break break
elif profile['Label'] == 'KDX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
if not profile['PVOptions']: if not profile['PVOptions']:
GUI.qualityBox.setChecked(False) GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other': if str(GUI.deviceBox.currentText()) == 'Other':
@@ -909,7 +949,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
elif not GUI.webtoonBox.isChecked(): elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True) GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked(): 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') self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
def stripTags(self, html): def stripTags(self, html):
s = HTMLStripper() s = HTMLStripper()
@@ -1038,18 +1078,26 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(), 'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(), 'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(), 'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(), 'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(), 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(), 'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(),
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
'jpegQualityBox': GUI.jpegQualityBox.checkState(), 'jpegQualityBox': GUI.jpegQualityBox.checkState(),
'jpegQuality': GUI.jpegQualitySpinBox.value(), 'jpegQuality': GUI.jpegQualitySpinBox.value(),
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(), 'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState(), 'deleteBox': GUI.deleteBox.checkState(),
'tempDirBox': GUI.tempDirBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState(), 'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState(), 'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(), 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(), 'noRotateBox': GUI.noRotateBox.checkState(),
'rotateRightBox': GUI.rotateRightBox.checkState(),
'rotateFirstBox': GUI.rotateFirstBox.checkState(), 'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState(), 'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100, 'gammaSlider': float(self.gammaValue) * 100,
@@ -1172,7 +1220,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'convertButton', 'formatBox']: 'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0)) getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1) GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']: for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'gridLayout_6', 'horizontalLayout_2']:
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0) getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
if self.windowSize == '0x0': if self.windowSize == '0x0':
MW.resize(500, 500) MW.resize(500, 500)
@@ -1182,6 +1230,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'}, "EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'}, "CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'}, "PDF": {'icon': 'EPUB', 'format': 'PDF'},
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'}, "KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'}, "MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}, "EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1211,10 +1260,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
}, },
"Kindle Scribe 3": { "Kindle Scribe 3": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
}, },
"Kindle Scribe Colorsoft": { "Kindle Scribe Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
}, },
"Kindle 11": { "Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
@@ -1223,7 +1272,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
}, },
"Kindle Paperwhite 12": { "Kindle Paperwhite 12": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW6',
}, },
"Kindle Colorsoft": { "Kindle Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
@@ -1379,6 +1428,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.clicked.connect(self.selectFileMetaEditor) GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki) GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi) GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble)
GUI.youtubeButton.clicked.connect(self.openYouTube)
GUI.discordButton.clicked.connect(self.openDiscord)
GUI.convertButton.clicked.connect(self.convertStart) GUI.convertButton.clicked.connect(self.convertStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma) GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox) GUI.gammaBox.stateChanged.connect(self.togglegammaBox)

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
__version__ = '9.5.0' __version__ = '9.7.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi' __copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'

View File

@@ -310,6 +310,15 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"]) f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
for author in options.authors: for author in options.authors:
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"]) f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
if not options.iskindle and options.series:
f.writelines(['<meta property="belongs-to-collection" id="c02">', hescape(options.series), "</meta>\n"])
f.writelines(['<meta refines="#c02" property="collection-type">', "series", "</meta>\n"])
if options.volume and options.number:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(f"{options.volume}.{options.number}"), "</meta>\n"])
elif options.volume:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.volume), "</meta>\n"])
elif options.number:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.number), "</meta>\n"])
f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n") f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n")
if cover: if cover:
f.write("<meta name=\"cover\" content=\"cover\"/>\n") f.write("<meta name=\"cover\" content=\"cover\"/>\n")
@@ -353,6 +362,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
mt = 'image/png' mt = 'image/png'
elif '.gif' == filename[1]: elif '.gif' == filename[1]:
mt = 'image/gif' mt = 'image/gif'
elif '.webp' == filename[1]:
mt = 'image/webp'
else: else:
mt = 'image/jpeg' mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" + f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
@@ -545,7 +556,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
f.close() f.close()
build_html_start = perf_counter() build_html_start = perf_counter()
if cover: if cover:
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes) cover.save_to_folder(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
dot_clean(path) dot_clean(path)
options.covers.append((cover, options.uuid)) options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')): for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
@@ -689,7 +700,6 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt) workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload: for i in workImg.payload:
img = image.ComicPage(opt, *i) img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon: if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon: if opt.cropping == 1 and not opt.webtoon:
@@ -699,18 +709,24 @@ def imgFileProcessing(work):
img.gammaCorrectImage() img.gammaCorrectImage()
if not img.colorOutput:
img.convertToGrayscale()
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.eraserainbow, is_color) img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
if is_color: if img.colorOutput:
pass pass
elif opt.forcepng: elif opt.forcepng:
img.convertToGrayscale() if not opt.noquantize:
if opt.format != 'PDF':
img.quantizeImage() img.quantizeImage()
else: if opt.format == 'PDF':
img.convertToGrayscale() img.convertToGrayscale()
elif opt.profile == 'KDX' and opt.format == 'CBZ':
img.convertToGrayscale()
elif opt.pnglegacy:
img.convertToGrayscale()
output.append(img.saveToDir()) output.append(img.saveToDir())
return output return output
except Exception: except Exception:
@@ -740,7 +756,9 @@ def render_page(vector):
cpu = vector[1] # number of CPUs cpu = vector[1] # number of CPUs
filename = vector[2] # document filename filename = vector[2] # document filename
output_dir = vector[3] output_dir = vector[3]
target_height = vector[4] target_width = vector[4]
target_height = vector[5]
pdf_width = vector[6]
with pymupdf.open(filename) as doc: # open the document with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages num_pages = doc.page_count # get number of pages
@@ -751,7 +769,10 @@ def render_page(vector):
for i in range(seg_from, seg_to): # work through our page segment for i in range(seg_from, seg_to): # work through our page segment
page = doc[i] page = doc[i]
zoom = target_height / page.rect.height if not pdf_width or page.rect.width > page.rect.height:
zoom = target_height / page.rect.height
else:
zoom = target_width / page.rect.width
mat = pymupdf.Matrix(zoom, zoom) mat = pymupdf.Matrix(zoom, zoom)
# TODO: decide colorspace earlier so later color check is cheaper. # TODO: decide colorspace earlier so later color check is cheaper.
# This is actually pretty hard when you have to deal with color vector text # This is actually pretty hard when you have to deal with color vector text
@@ -816,7 +837,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height): def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
render = False render = False
with pymupdf.open(filename) as doc: with pymupdf.open(filename) as doc:
for page in doc: for page in doc:
@@ -836,7 +857,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
cpu = cpu_count() cpu = cpu_count()
# make vectors of arguments for the processes # make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)] vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename)) print("Starting %i processes for '%s'." % (cpu, filename))
@@ -853,7 +874,8 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def getWorkFolder(afile, workdir=None): def getWorkFolder(afile, workdir=None):
if not workdir: if not workdir:
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-')
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile)) if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else: else:
fullPath = workdir fullPath = workdir
@@ -881,13 +903,15 @@ def getWorkFolder(afile, workdir=None):
if njpg == 0: if njpg == 0:
raise UserWarning("Failed to extract images from PDF file.") raise UserWarning("Failed to extract images from PDF file.")
return workdir return workdir
target_height = options.profileData[1][1] target_width, target_height = options.profileData[1]
if options.cropping == 1: if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom target_height *= 1.2 #Account for possible margin at the top and bottom
target_width *= 1.2
elif options.cropping == 2: elif options.cropping == 2:
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number target_height *= 1.25 #Account for possible margin at the top and bottom with page number
target_width *= 1.25
try: try:
mupdf_pdf_process_pages_parallel(afile, fullPath, target_height) mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
except Exception as e: except Exception as e:
rmtree(path, True) rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}") raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -978,6 +1002,9 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = [] options.comicinfo_chapters = []
options.summary = '' options.summary = ''
titleSuffix = '' titleSuffix = ''
options.volume = ''
options.number = ''
options.series = ''
if options.title == 'defaulttitle': if options.title == 'defaulttitle':
defaultTitle = True defaultTitle = True
if os.path.isdir(originalpath): if os.path.isdir(originalpath):
@@ -1006,8 +1033,10 @@ def getMetadata(path, originalpath):
options.title = xml.data['Series'] options.title = xml.data['Series']
if xml.data['Volume']: if xml.data['Volume']:
titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2) titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
options.volume = xml.data['Volume']
if xml.data['Number']: if xml.data['Number']:
titleSuffix += ' #' + xml.data['Number'].zfill(3) titleSuffix += ' #' + xml.data['Number'].zfill(3)
options.number = xml.data['Number']
if options.metadatatitle == 1 and xml.data['Title']: if options.metadatatitle == 1 and xml.data['Title']:
titleSuffix += ': ' + xml.data['Title'] titleSuffix += ': ' + xml.data['Title']
options.title += titleSuffix options.title += titleSuffix
@@ -1025,6 +1054,8 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = xml.data['Bookmarks'] options.comicinfo_chapters = xml.data['Bookmarks']
if xml.data['Summary']: if xml.data['Summary']:
options.summary = xml.data['Summary'] options.summary = xml.data['Summary']
if xml.data['Series']:
options.series = xml.data['Series']
os.remove(xmlPath) os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'): if originalpath.lower().endswith('.pdf'):
@@ -1285,12 +1316,12 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
mimetypeFile.write('application/epub+zip') mimetypeFile.write('application/epub+zip')
mimetypeFile.close() mimetypeFile.close()
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir) subprocess_run([SEVENZIP, 'a', '-mx0', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
# crazy hack to ensure mimetype is first when using 7zip # crazy hack to ensure mimetype is first when using 7zip
if isepub: if isepub:
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir) subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
else: else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED) zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
if isepub: if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED) zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir): for dirpath, _, filenames in os.walk(basedir):
@@ -1353,6 +1384,8 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment") help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False, output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.") help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
help="Rotate double page spreads in opposite direction.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False, output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.") help="Put rotated 2 page spread first in spread splitter option.")
@@ -1360,6 +1393,10 @@ def makeParser():
help="Do not modify image and ignore any profile or processing option") help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False, 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") help="Use the legacy PDF image extraction method from KCC 8 and earlier")
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("--coverfill", action="store_true", dest="coverfill", default=False, processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen") help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False, processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
@@ -1397,7 +1434,15 @@ def makeParser():
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False, output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies") help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False, processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG") help="Create PNG files instead JPEG for black and white images")
processing_options.add_argument("--force-png-rgb", action="store_true", dest="force_png_rgb", default=False,
help="Force color images to be saved as PNG")
processing_options.add_argument("--webp", action="store_true", dest="webp", default=False,
help="Replace JPG with lossy WEBP and PNG with lossless WEBP")
processing_options.add_argument("--pnglegacy", action="store_true", dest="pnglegacy", default=False,
help="Use a more compatible 8 bit png instead of 4 bit")
processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
help="Don't quantize to 16 color PNG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False, processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
help="Create JPEG files using mozJpeg") help="Create JPEG files using mozJpeg")
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality", processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
@@ -1406,6 +1451,8 @@ def makeParser():
help="Turn 1x4 strips to 2x2 strips") help="Turn 1x4 strips to 2x2 strips")
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False, processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
help="Delete source file(s) or a directory. It's not recoverable.") help="Delete source file(s) or a directory. It's not recoverable.")
processing_options.add_argument("--tempdir", action="store_true", dest="tempdir", default=False,
help="Create temporary files directory on source file drive.")
custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0, custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
help="Replace screen width provided by device profile") help="Replace screen width provided by device profile")
@@ -1424,6 +1471,11 @@ def checkOptions(options):
options.isKobo = False options.isKobo = False
options.bordersColor = None options.bordersColor = None
options.keep_epub = False options.keep_epub = False
if options.format == 'PDF-200MB':
options.targetsize = 195
options.format = 'PDF'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'EPUB-200MB': if options.format == 'EPUB-200MB':
options.targetsize = 195 options.targetsize = 195
options.format = 'EPUB' options.format = 'EPUB'
@@ -1482,9 +1534,6 @@ def checkOptions(options):
if 'Ko' in options.profile: if 'Ko' in options.profile:
options.panelview = False options.panelview = False
options.hq = False options.hq = False
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.customheight = 1200
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin # KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
if options.format == 'KFX': if options.format == 'KFX':
options.format = 'EPUB' options.format = 'EPUB'
@@ -1510,6 +1559,15 @@ def checkOptions(options):
options.jpegquality = 85 options.jpegquality = 85
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format) 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 options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
options.webp_output = options.format != 'PDF' and not options.kindle_azw3 and options.webp
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1])
options.profileData[1][1] = 1200
if options.kindle_scribe_azw3: if options.kindle_scribe_azw3:
options.profileData = list(image.ProfileData.Profiles[options.profile]) options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1]) options.profileData[1] = list(options.profileData[1])
@@ -1657,12 +1715,16 @@ def makeBook(source, qtgui=None, job_progress=''):
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber))) filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if 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) makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF': elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...") print(f"{job_progress}Creating PDF file with PyMuPDF...")
# determine output filename based on source and tome count # determine output filename based on source and tome count
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else '' suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
output_file = getOutputFilename(source, options.output, '.pdf', suffix) output_file = getOutputFilename(source, options.output, '.pdf', suffix)
if cover.smartcover:
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
# use optimized buildPDF logic with streaming and compression # use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file) output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
filepath.append(output_pdf) filepath.append(output_pdf)

View File

@@ -98,14 +98,15 @@ class ProfileData:
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0), 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0), 'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0), 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0), 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0), 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0), 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0), 'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0), 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0), 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0), 'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
} }
@@ -193,14 +194,21 @@ class ComicPageParser:
new_image.paste(pageone, (0, 0)) new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height)) new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.fill]) self.payload.append(['N', self.source, new_image, self.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \ elif self.opt.webtoon:
and not self.opt.webtoon and self.opt.splitter == 1: self.payload.append(['N', self.source, self.image, self.fill])
# rotate only TODO dead code?
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill]) self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon: # elif wide enough to split
if self.opt.splitter != 1: elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
# if (split) or (split and rotate)
if self.opt.splitter != 1 and width / height < 1.75:
if width > height: if width > height:
leftbox = (0, 0, int(width / 2), height) leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height) rightbox = (int(width / 2), 0, width, height)
@@ -215,10 +223,15 @@ class ComicPageParser:
pagetwo = self.image.crop(rightbox) pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill]) self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill]) self.payload.append(['S2', self.source, pagetwo, self.fill])
if self.opt.splitter > 0:
# if (rotate) or (split and rotate)
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill]) self.payload.append(['R', self.source, spread, self.fill])
else: else:
self.payload.append(['N', self.source, self.image, self.fill]) self.payload.append(['N', self.source, self.image, self.fill])
@@ -271,6 +284,8 @@ class ComicPage:
self.original_color_mode = image.mode self.original_color_mode = image.mode
# TODO: color check earlier # TODO: color check earlier
self.image = image.convert("RGB") self.image = image.convert("RGB")
self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor
self.fill = fill self.fill = fill
self.rotated = False self.rotated = False
self.orgPath = os.path.join(path[0], path[1]) self.orgPath = os.path.join(path[0], path[1])
@@ -289,8 +304,7 @@ class ComicPage:
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@cached_property def colorCheck(self):
def color(self):
if self.original_color_mode in ("L", "1"): if self.original_color_mode in ("L", "1"):
return False return False
if self.opt.webtoon: if self.opt.webtoon:
@@ -399,17 +413,23 @@ class ComicPage:
raise RuntimeError('Cannot save image. ' + str(err)) raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath): def save_with_codec(self, image, targetPath):
if self.opt.forcepng: if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
image.info.pop('transparency', None) image.info.pop('transparency', None)
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format): if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
elif self.opt.kindle_azw3:
targetPath += '.gif' targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False) image.save(targetPath, 'GIF', optimize=1, interlace=False)
else: else:
targetPath += '.png' targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1) image.save(targetPath, 'PNG', optimize=1)
else: else:
targetPath += '.jpg' if self.opt.webp_output:
if self.opt.mozjpeg: targetPath += '.webp'
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
elif self.opt.mozjpeg:
targetPath += '.jpg'
with io.BytesIO() as output: with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality) image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue() input_jpeg_bytes = output.getvalue()
@@ -417,6 +437,7 @@ class ComicPage:
with open(targetPath, "wb") as output_jpeg_file: with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes) output_jpeg_file.write(output_jpeg_bytes)
else: else:
targetPath += '.jpg'
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality) image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath return targetPath
@@ -501,7 +522,9 @@ class ComicPage:
elif method == Image.Resampling.BICUBIC and not self.opt.upscale: elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass pass
else: # if image bigger than device resolution or smaller with upscaling else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD: if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
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) 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') or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill) self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
@@ -509,7 +532,7 @@ class ComicPage:
self.image = ImageOps.contain(self.image, self.size, method=method) self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self): def resize_method(self):
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]: if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
return Image.Resampling.BICUBIC return Image.Resampling.BICUBIC
else: else:
return Image.Resampling.LANCZOS return Image.Resampling.LANCZOS
@@ -553,6 +576,7 @@ class Cover:
self.options = opt self.options = opt
self.source = source self.source = source
self.image = Image.open(source) self.image = Image.open(source)
self.smartcover = False
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@@ -563,7 +587,8 @@ class Cover:
self.image = ImageOps.autocontrast(self.image, preserve_tone=True) self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor: if not self.options.forcecolor:
self.image = self.image.convert('L') self.image = self.image.convert('L')
self.crop_main_cover() if not self.options.nosmartcovercrop:
self.crop_main_cover()
size = list(self.options.profileData[1]) size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3: if self.options.kindle_scribe_azw3:
@@ -578,17 +603,37 @@ class Cover:
def crop_main_cover(self): def crop_main_cover(self):
w, h = self.image.size w, h = self.image.size
if w / h > 2: if w / h > 2:
self.smartcover = True
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h)) self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h)) self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.83:
self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((w * .19, 0, w * .575, h))
else:
self.image = self.image.crop((w * .425, 0, .81 * w, h))
elif w / h > 1.7:
self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((w * .2, 0, w * .583, h))
else:
self.image = self.image.crop((w * .417, 0, .8 * w, h))
elif w / h > 1.34: elif w / h > 1.34:
self.smartcover = True
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h)) self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h)) self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
elif w / h > 1.0:
self.smartcover = True
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))
def save_to_epub(self, target, tomeid, len_tomes=0): def save_to_folder(self, target, tomeid, len_tomes=0):
try: try:
if tomeid == 0: if tomeid == 0:
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality) self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)

View File

@@ -1,4 +1,4 @@
PySide6>6 PySide6<6.10
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=5.9.5
requests>=2.31.0 requests>=2.31.0