mirror of
https://github.com/ciromattia/kcc
synced 2026-04-15 13:38:46 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9d4f274d | ||
|
|
b5de6fd39d | ||
|
|
b4b9e41a0c | ||
|
|
9b9181a715 | ||
|
|
472fdc97b5 | ||
|
|
6fdfddd7d9 | ||
|
|
34fb68ac65 | ||
|
|
b4d72cd581 | ||
|
|
1dead9af8f | ||
|
|
b42f05686e | ||
|
|
adf48d24f9 | ||
|
|
723fa4c0b8 | ||
|
|
2632d18e2c | ||
|
|
94e4937566 | ||
|
|
f7ce1cf271 | ||
|
|
ab93c03838 | ||
|
|
541b1d876b | ||
|
|
d189f9909d | ||
|
|
1dce4f8d2c | ||
|
|
58aab0cb65 | ||
|
|
d2dc089c62 | ||
|
|
3660f2370f | ||
|
|
87c6e3a35e | ||
|
|
981c556550 | ||
|
|
123d603cbd | ||
|
|
a344dd73bf | ||
|
|
095694e9cf | ||
|
|
4b4860b976 | ||
|
|
56e8e24176 | ||
|
|
b0f8f1c633 | ||
|
|
38acc3bf05 | ||
|
|
fbd5980b9b | ||
|
|
667d702b8a | ||
|
|
9a4143ce62 | ||
|
|
f63387cae4 | ||
|
|
f5fd2bb7fe | ||
|
|
4baca03214 | ||
|
|
7de212dca3 | ||
|
|
c99444b96a | ||
|
|
6d7a635c3d | ||
|
|
be86bcbf6a | ||
|
|
5cbc07e65d | ||
|
|
42d94d8202 | ||
|
|
7897627c43 | ||
|
|
8e42fc1162 | ||
|
|
d6b0e43d70 | ||
|
|
af189ed265 | ||
|
|
aa5f4991dd | ||
|
|
f9064ef0e4 | ||
|
|
e14abe1787 | ||
|
|
c58387f4f4 | ||
|
|
9b63b7af2c | ||
|
|
f74e108a3e | ||
|
|
f088ad732e | ||
|
|
8e5d57364d | ||
|
|
b767d5dc2c | ||
|
|
7228055bca | ||
|
|
8c57fbf318 | ||
|
|
7e94861fa1 | ||
|
|
9992ca4d26 | ||
|
|
f47d1427f0 | ||
|
|
ce8998375c | ||
|
|
8870898a87 | ||
|
|
a017cfd00d | ||
|
|
3f4ef3e21e | ||
|
|
4733c6348b | ||
|
|
5ad23d9629 | ||
|
|
db4eb78963 | ||
|
|
988fc93dc5 | ||
|
|
74fee9346c | ||
|
|
9fcacd7ae6 |
6
.github/workflows/package-linux.yml
vendored
6
.github/workflows/package-linux.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
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
|
||||
- name: build binary
|
||||
run: |
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
env:
|
||||
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: AppImage
|
||||
path: './*.AppImage*'
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
LICENSE.txt
|
||||
*.AppImage*
|
||||
|
||||
10
.github/workflows/package-macos.yml
vendored
10
.github/workflows/package-macos.yml
vendored
@@ -25,8 +25,10 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-13, macos-14 ]
|
||||
os: [ macos-15-intel, macos-14 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up Python
|
||||
@@ -36,7 +38,7 @@ jobs:
|
||||
cache: 'pip'
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
|
||||
python -m pip install --upgrade pip pyinstaller certifi
|
||||
pip install -r requirements.txt
|
||||
- name: Install the Apple certificate and provisioning profile
|
||||
# TODO signing
|
||||
@@ -78,7 +80,7 @@ jobs:
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: mac-os-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
@@ -87,7 +89,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.dmg
|
||||
- name: Clean up keychain and provisioning profile
|
||||
|
||||
8
.github/workflows/package-osx-legacy.yml
vendored
8
.github/workflows/package-osx-legacy.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ macos-13 ]
|
||||
os: [ macos-15-intel ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
|
||||
pip3 install --upgrade pip pyinstaller certifi
|
||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||
./gen_ui_files.sh
|
||||
- uses: actions/setup-node@v6
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
python3 setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: osx-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
LICENSE.txt
|
||||
dist/*.dmg
|
||||
|
||||
6
.github/workflows/package-windows.yml
vendored
6
.github/workflows/package-windows.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
env:
|
||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
- name: build binary
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
python setup.py ${{ matrix.command }}
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/*.exe
|
||||
@@ -73,6 +73,6 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
|
||||
6
.github/workflows/package-windows7.yml
vendored
6
.github/workflows/package-windows7.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-win7.txt
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
.\gen_ui_files.bat
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
python setup.py build_binary
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
@@ -55,6 +55,6 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM python:3.13-slim-bullseye AS builder
|
||||
|
||||
# Install system dependencies
|
||||
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" && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}
|
||||
|
||||
94
README.md
94
README.md
@@ -7,12 +7,19 @@
|
||||
[](https://github.com/ciromattia/kcc/releases)
|
||||
|
||||
|
||||
**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
|
||||
**Kindle Comic Converter** optimizes black & white (or color) comics and manga for E-ink ereaders
|
||||
like Kindle, Kobo, ReMarkable, and more.
|
||||
Pages display in fullscreen without margins,
|
||||
with proper fixed layout support.
|
||||
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||
KCC runs on Windows, macOS, and Linux.
|
||||
|
||||
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
|
||||
|
||||
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
|
||||
|
||||
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
|
||||
|
||||
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
|
||||
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
||||
@@ -34,6 +41,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
|
||||
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
|
||||
4) incorrect page turn direction for manga that's read right to left
|
||||
5) unaligned two page spreads in landscape, where pages are shifted over by 1
|
||||
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
|
||||
|
||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
||||
|
||||
@@ -46,7 +54,9 @@ You can change the default output directory by holding `Shift` while clicking th
|
||||
Then just drag and drop the generated output files onto your device's documents folder via USB.
|
||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
|
||||
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
|
||||
|
||||
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
|
||||
### A word of warning
|
||||
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
|
||||
@@ -92,13 +102,13 @@ Click on **Assets** of the latest release.
|
||||
You probably want either
|
||||
- `KCC_*.*.*.exe` (Windows)
|
||||
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
|
||||
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 12+)
|
||||
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
|
||||
|
||||
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
|
||||
|
||||
The `c2e` and `c2p` versions are command line tools for power users.
|
||||
|
||||
On Mac, right click open to get past the security warning.
|
||||
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
|
||||
|
||||
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||
|
||||
@@ -177,38 +187,44 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
||||
### Profiles:
|
||||
|
||||
```
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
||||
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
|
||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
|
||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
|
||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
|
||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
|
||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
|
||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
|
||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
|
||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
|
||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
|
||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
|
||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
|
||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
|
||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
|
||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
|
||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||
'KV': ("Kindle Voyage", (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),
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), 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),
|
||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
|
||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
|
||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
|
||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
|
||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
|
||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
|
||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
|
||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
|
||||
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
|
||||
'OTHER': ("Other", (0, 0), Palette16, 1.0),
|
||||
```
|
||||
|
||||
### Standalone `kcc-c2e.py` usage:
|
||||
@@ -232,6 +248,8 @@ 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.
|
||||
--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
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
@@ -252,9 +270,12 @@ 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
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--noquantize Don't quantize PNG images to 16 colors
|
||||
--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.
|
||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
||||
|
||||
@@ -273,6 +294,7 @@ OUTPUT SETTINGS:
|
||||
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
|
||||
--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.
|
||||
--filefusion Combines all input files into a single file.
|
||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: kcc
|
||||
channels:
|
||||
- conda-forge
|
||||
- defaults
|
||||
dependencies:
|
||||
- python=3.11
|
||||
- Pillow>=11.3.0
|
||||
- psutil>=5.9.5
|
||||
- python-slugify>=1.2.1
|
||||
- raven>=6.0.0
|
||||
- distro
|
||||
- natsort>=8.4.0
|
||||
- pip
|
||||
- pip:
|
||||
- mozjpeg-lossless-optimization>=1.1.2
|
||||
- pyside6>=6.5.1
|
||||
1219
gui/KCC.ui
1219
gui/KCC.ui
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ import itertools
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
|
||||
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
|
||||
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
|
||||
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
||||
|
||||
import os
|
||||
@@ -42,7 +42,7 @@ from raven import Client
|
||||
from tempfile import gettempdir
|
||||
|
||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
||||
from .comicarchive import SEVENZIP, available_archive_tools
|
||||
from .comicarchive import SEVENZIP, TAR, available_archive_tools
|
||||
from . import __version__
|
||||
from . import comic2ebook
|
||||
from . import metadata
|
||||
@@ -327,6 +327,12 @@ class WorkerThread(QThread):
|
||||
options.maximizestrips = True
|
||||
if GUI.disableProcessingBox.isChecked():
|
||||
options.noprocessing = True
|
||||
if GUI.pdfExtractBox.isChecked():
|
||||
options.pdfextract = True
|
||||
if GUI.pdfWidthBox.isChecked():
|
||||
options.pdfwidth = True
|
||||
if GUI.coverFillBox.isChecked():
|
||||
options.coverfill = True
|
||||
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
options.metadatatitle = 1
|
||||
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
|
||||
@@ -341,12 +347,18 @@ class WorkerThread(QThread):
|
||||
options.filefusion = False
|
||||
if GUI.noRotateBox.isChecked():
|
||||
options.norotate = True
|
||||
if GUI.rotateRightBox.isChecked():
|
||||
options.rotateright = True
|
||||
if GUI.rotateFirstBox.isChecked():
|
||||
options.rotatefirst = True
|
||||
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
options.forcepng = True
|
||||
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
||||
options.mozjpeg = True
|
||||
if GUI.noQuantizeBox.isChecked():
|
||||
options.noquantize = True
|
||||
if GUI.jpegQualityBox.isChecked():
|
||||
options.jpegquality = GUI.jpegQualitySpinBox.value()
|
||||
if GUI.currentMode > 2:
|
||||
options.customwidth = str(GUI.widthBox.value())
|
||||
options.customheight = str(GUI.heightBox.value())
|
||||
@@ -519,6 +531,7 @@ class WorkerThread(QThread):
|
||||
if os.path.exists(item.replace('.epub', '.mobi')):
|
||||
os.remove(item.replace('.epub', '.mobi'))
|
||||
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
|
||||
MW.addMessage.emit(self.kindlegenErrorCode[1], 'error', False)
|
||||
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
|
||||
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
|
||||
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
|
||||
@@ -610,12 +623,30 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'Comic (*.pdf);;All (*.*)')
|
||||
for fname in fnames[0]:
|
||||
if fname != '':
|
||||
if sys.platform.startswith('win'):
|
||||
fname = fname.replace('/', '\\')
|
||||
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
|
||||
GUI.jobList.addItem(fname)
|
||||
GUI.jobList.scrollToBottom()
|
||||
|
||||
def selectDir(self):
|
||||
if self.needClean:
|
||||
self.needClean = False
|
||||
GUI.jobList.clear()
|
||||
|
||||
dialog = QFileDialog(MW, 'Select input folder(s)', self.lastPath)
|
||||
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
||||
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
||||
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||||
dialog.findChild(QTreeView).setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
|
||||
if dialog.exec():
|
||||
dnames = dialog.selectedFiles()
|
||||
for dname in dnames:
|
||||
if dname != '':
|
||||
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
|
||||
GUI.jobList.addItem(dname)
|
||||
GUI.jobList.scrollToBottom()
|
||||
|
||||
|
||||
def selectFileMetaEditor(self, sname):
|
||||
if not sname:
|
||||
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
||||
@@ -724,6 +755,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.croppingWidget.setVisible(False)
|
||||
self.changeCroppingPower(100) # 1.0
|
||||
|
||||
def togglejpegqualityBox(self, value):
|
||||
if value:
|
||||
GUI.jpegQualityWidget.setVisible(True)
|
||||
else:
|
||||
GUI.jpegQualityWidget.setVisible(False)
|
||||
|
||||
def togglewebtoonBox(self, value):
|
||||
if value:
|
||||
self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
|
||||
@@ -754,7 +791,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.rotateBox.setEnabled(True)
|
||||
GUI.borderBox.setEnabled(True)
|
||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||
if profile['Label'] != 'KS':
|
||||
if not profile['Label'].startswith('KS'):
|
||||
GUI.upscaleBox.setEnabled(True)
|
||||
GUI.croppingBox.setEnabled(True)
|
||||
GUI.interPanelCropBox.setEnabled(True)
|
||||
@@ -779,7 +816,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
def toggleImageFormatBox(self, value):
|
||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||
if value == 1:
|
||||
if profile['Label'] == 'KS':
|
||||
if profile['Label'].startswith('KS'):
|
||||
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||
for bad_format in ('MOBI', 'EPUB'):
|
||||
if bad_format in current_format:
|
||||
@@ -841,7 +878,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if not GUI.webtoonBox.isChecked():
|
||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||
if profile['Label'] == 'KS':
|
||||
if profile['Label'].startswith('KS'):
|
||||
GUI.upscaleBox.setDisabled(True)
|
||||
else:
|
||||
if not GUI.webtoonBox.isChecked():
|
||||
@@ -852,6 +889,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
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')
|
||||
break
|
||||
elif profile['Label'] == 'KDX':
|
||||
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
if not profile['PVOptions']:
|
||||
GUI.qualityBox.setChecked(False)
|
||||
if str(GUI.deviceBox.currentText()) == 'Other':
|
||||
@@ -878,7 +918,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
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')
|
||||
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||
|
||||
def stripTags(self, html):
|
||||
s = HTMLStripper()
|
||||
@@ -1006,8 +1046,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'colorBox': GUI.colorBox.checkState(),
|
||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
|
||||
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
|
||||
'jpegQuality': GUI.jpegQualitySpinBox.value(),
|
||||
'widthBox': GUI.widthBox.value(),
|
||||
'heightBox': GUI.heightBox.value(),
|
||||
'deleteBox': GUI.deleteBox.checkState(),
|
||||
@@ -1015,6 +1061,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||
'rotateRightBox': GUI.rotateRightBox.checkState(),
|
||||
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
||||
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
||||
'gammaSlider': float(self.gammaValue) * 100,
|
||||
@@ -1059,7 +1106,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if message[-1] == '/':
|
||||
message = message[:-1]
|
||||
self.handleMessage(message)
|
||||
GUI.jobList.sortItems()
|
||||
# sorting may conflict with manual file fusion order
|
||||
# GUI.jobList.sortItems()
|
||||
|
||||
def forceShutdown(self):
|
||||
self.saveSettings(None)
|
||||
@@ -1146,6 +1194,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
||||
"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'},
|
||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||
@@ -1162,9 +1211,24 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
||||
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
||||
"Kindle Scribe": {
|
||||
"Kindle 1860x1920": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1860',
|
||||
},
|
||||
"Kindle 1920x1920": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1920',
|
||||
},
|
||||
"Kindle 1240x1860": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
|
||||
},
|
||||
"Kindle Scribe 1/2": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
||||
},
|
||||
"Kindle Scribe 3": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
|
||||
},
|
||||
"Kindle Scribe Colorsoft": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
|
||||
},
|
||||
"Kindle 11": {
|
||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
|
||||
},
|
||||
@@ -1239,9 +1303,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'Label': 'OTHER'},
|
||||
}
|
||||
profilesGUI = [
|
||||
"Kindle Scribe Colorsoft",
|
||||
"Kindle Scribe 3",
|
||||
"Kindle Colorsoft",
|
||||
"Kindle Paperwhite 12",
|
||||
"Kindle Scribe",
|
||||
"Kindle Scribe 1/2",
|
||||
"Kindle Paperwhite 11",
|
||||
"Kindle 11",
|
||||
"Kindle Oasis 9/10",
|
||||
@@ -1261,6 +1327,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"Separator",
|
||||
"Other",
|
||||
"Separator",
|
||||
"Kindle 1920x1920",
|
||||
"Kindle 1860x1920",
|
||||
"Kindle 1240x1860",
|
||||
"Kindle 8/10",
|
||||
"Kindle Oasis 8",
|
||||
"Kindle Paperwhite 7/10",
|
||||
@@ -1308,7 +1377,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
||||
'info')
|
||||
|
||||
self.tar = 'tar' in available_archive_tools()
|
||||
self.tar = TAR in available_archive_tools()
|
||||
self.sevenzip = SEVENZIP in available_archive_tools()
|
||||
if not any([self.tar, self.sevenzip]):
|
||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||
@@ -1319,6 +1388,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
|
||||
GUI.clearButton.clicked.connect(self.clearJobs)
|
||||
GUI.fileButton.clicked.connect(self.selectFile)
|
||||
GUI.directoryButton.clicked.connect(self.selectDir)
|
||||
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
||||
GUI.wikiButton.clicked.connect(self.openWiki)
|
||||
GUI.kofiButton.clicked.connect(self.openKofi)
|
||||
@@ -1327,6 +1397,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
|
||||
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
|
||||
GUI.jpegQualityBox.stateChanged.connect(self.togglejpegqualityBox)
|
||||
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
|
||||
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
|
||||
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
|
||||
@@ -1388,6 +1459,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.croppingPowerSlider.setValue(int(self.options[option]))
|
||||
self.changeCroppingPower(int(self.options[option]))
|
||||
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
|
||||
elif str(option) == "jpegQuality":
|
||||
GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
|
||||
elif str(option) == "chunkSizeBox":
|
||||
GUI.chunkSizeBox.setValue(int(self.options[option]))
|
||||
else:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
__version__ = '9.3.4'
|
||||
__version__ = '9.6.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -48,6 +48,7 @@ from .comicarchive import SEVENZIP, available_archive_tools
|
||||
from . import comic2panel
|
||||
from . import image
|
||||
from . import comicarchive
|
||||
from . import pdfjpgextract
|
||||
from . import dualmetafix
|
||||
from . import metadata
|
||||
from . import kindle
|
||||
@@ -65,16 +66,16 @@ def main(argv=None):
|
||||
parser.print_help()
|
||||
return 0
|
||||
if sys.platform.startswith('win'):
|
||||
sources = set([source for option in options.input for source in glob(escape(option))])
|
||||
sources = [source for option in options.input for source in glob(escape(option))]
|
||||
else:
|
||||
sources = set(options.input)
|
||||
sources = options.input
|
||||
if len(sources) == 0:
|
||||
print('No matching files found.')
|
||||
return 1
|
||||
if options.filefusion:
|
||||
fusion_path = makeFusion(list(sources))
|
||||
sources.clear()
|
||||
sources.add(fusion_path)
|
||||
sources.append(fusion_path)
|
||||
for source in sources:
|
||||
source = source.rstrip('\\').rstrip('/')
|
||||
options = copy(args)
|
||||
@@ -659,7 +660,7 @@ def imgDirectoryProcessing(path, job_progress=''):
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(workerOutput) > 0:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
||||
raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
||||
else:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise UserWarning("C2E: Source directory is empty.")
|
||||
@@ -706,8 +707,12 @@ def imgFileProcessing(work):
|
||||
pass
|
||||
elif opt.forcepng:
|
||||
img.convertToGrayscale()
|
||||
if opt.format != 'PDF':
|
||||
if not opt.noquantize:
|
||||
img.quantizeImage()
|
||||
if opt.format == 'PDF':
|
||||
img.convertToGrayscale()
|
||||
elif opt.profile == 'KDX' and opt.format == 'CBZ':
|
||||
img.convertToGrayscale()
|
||||
else:
|
||||
img.convertToGrayscale()
|
||||
output.append(img.saveToDir())
|
||||
@@ -739,7 +744,9 @@ def render_page(vector):
|
||||
cpu = vector[1] # number of CPUs
|
||||
filename = vector[2] # document filename
|
||||
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
|
||||
num_pages = doc.page_count # get number of pages
|
||||
|
||||
@@ -750,7 +757,10 @@ def render_page(vector):
|
||||
|
||||
for i in range(seg_from, seg_to): # work through our page segment
|
||||
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)
|
||||
# TODO: decide colorspace earlier so later color check is cheaper.
|
||||
# This is actually pretty hard when you have to deal with color vector text
|
||||
@@ -800,9 +810,7 @@ def extract_page(vector):
|
||||
if len(image_list) > 1:
|
||||
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
|
||||
if not image_list:
|
||||
width, height = int(page.rect.width), int(page.rect.height)
|
||||
blank_page = Image.new("RGB", (width, height), "white")
|
||||
blank_page.save(output_path)
|
||||
continue
|
||||
else:
|
||||
xref = image_list[0][0]
|
||||
d = doc.extract_image(xref)
|
||||
@@ -817,7 +825,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
|
||||
with pymupdf.open(filename) as doc:
|
||||
for page in doc:
|
||||
@@ -837,7 +845,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
|
||||
cpu = cpu_count()
|
||||
|
||||
# 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))
|
||||
|
||||
|
||||
@@ -851,14 +859,17 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
|
||||
|
||||
|
||||
|
||||
def getWorkFolder(afile):
|
||||
def getWorkFolder(afile, workdir=None):
|
||||
if not workdir:
|
||||
workdir = mkdtemp('', 'KCC-')
|
||||
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
else:
|
||||
fullPath = workdir
|
||||
if os.path.isdir(afile):
|
||||
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
try:
|
||||
os.rmdir(workdir)
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
copytree(afile, fullPath)
|
||||
sanitizePermissions(fullPath)
|
||||
return workdir
|
||||
@@ -869,26 +880,28 @@ def getWorkFolder(afile):
|
||||
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
if afile.lower().endswith('.pdf'):
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
if not os.path.exists(fullPath):
|
||||
os.makedirs(fullPath)
|
||||
path = workdir
|
||||
sanitizePermissions(path)
|
||||
target_height = options.profileData[1][1]
|
||||
if options.pdfextract:
|
||||
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||
njpg = pdf.extract()
|
||||
if njpg == 0:
|
||||
raise UserWarning("Failed to extract images from PDF file.")
|
||||
return workdir
|
||||
target_width, target_height = options.profileData[1]
|
||||
if options.cropping == 1:
|
||||
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
|
||||
elif options.cropping == 2:
|
||||
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
|
||||
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:
|
||||
rmtree(path, True)
|
||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
||||
return workdir
|
||||
else:
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
if not os.path.exists(fullPath):
|
||||
os.makedirs(fullPath)
|
||||
try:
|
||||
@@ -905,7 +918,9 @@ def getWorkFolder(afile):
|
||||
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
|
||||
)
|
||||
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
|
||||
path = os.path.join(fullPath, tdir[0])
|
||||
for file in os.listdir(os.path.join(fullPath, tdir[0])):
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath)
|
||||
os.rmdir(os.path.join(fullPath, tdir[0]))
|
||||
return workdir
|
||||
|
||||
except OSError as e:
|
||||
@@ -957,6 +972,13 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
|
||||
counter += 1
|
||||
filename = basename + '_kcc' + str(counter) + ext
|
||||
elif options.format == 'MOBI' and ext == '.epub':
|
||||
counter = 0
|
||||
basename = os.path.splitext(filename)[0]
|
||||
if os.path.isfile(basename + '.mobi'):
|
||||
while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
|
||||
counter += 1
|
||||
filename = basename + '_kcc' + str(counter) + ext
|
||||
return filename
|
||||
|
||||
|
||||
@@ -1066,7 +1088,7 @@ def removeNonImages(filetree):
|
||||
raise UserWarning('No images detected, nested archives are not supported.')
|
||||
|
||||
|
||||
def sanitizeTree(filetree):
|
||||
def sanitizeTree(filetree, prefix='kcc'):
|
||||
chapterNames = {}
|
||||
page = 1
|
||||
cover_path = None
|
||||
@@ -1076,7 +1098,7 @@ def sanitizeTree(filetree):
|
||||
_, ext = getImageFileName(name)
|
||||
|
||||
# 9999 page limit
|
||||
unique_name = f'kcc-{page:04}'
|
||||
unique_name = f'{prefix}-{page:04}'
|
||||
page += 1
|
||||
|
||||
newKey = os.path.join(root, unique_name + ext)
|
||||
@@ -1125,7 +1147,7 @@ def chunk_directory(path):
|
||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
for f in files:
|
||||
# Windows MAX_LEN = 260 plus some buffer
|
||||
if os.name == 'nt' and len(os.path.join(root, f)) > 180:
|
||||
if os.name == 'nt' and len(os.path.join(root, f)) > 220:
|
||||
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
level = 1
|
||||
break
|
||||
@@ -1237,7 +1259,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
||||
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
|
||||
, 'warning', False)
|
||||
GUI.addMessage.emit('', '', False)
|
||||
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and options.profile != 'KS':
|
||||
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and not options.profile.startswith('KS'):
|
||||
print("WARNING: More than 25% of images are smaller than target device resolution. "
|
||||
"Consider enabling stretching or upscaling to improve readability.")
|
||||
if GUI:
|
||||
@@ -1269,10 +1291,13 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||
if SEVENZIP in available_archive_tools():
|
||||
if isepub:
|
||||
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
||||
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||
mimetypeFile.write('application/epub+zip')
|
||||
mimetypeFile.close()
|
||||
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
|
||||
# crazy hack to ensure mimetype is first when using 7zip
|
||||
if isepub:
|
||||
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
|
||||
else:
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||
if isepub:
|
||||
@@ -1337,11 +1362,19 @@ def makeParser():
|
||||
help="Shift first page to opposite side in landscape for spread alignment")
|
||||
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,
|
||||
help="Rotate double page spreads in opposite direction.")
|
||||
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
|
||||
help="Put rotated 2 page spread first in spread splitter option.")
|
||||
|
||||
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("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
|
||||
help="Render vector PDFs to device width instead of height.")
|
||||
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,
|
||||
help="Resize images smaller than device's resolution")
|
||||
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
||||
@@ -1378,8 +1411,12 @@ def makeParser():
|
||||
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
|
||||
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||
help="Create PNG files instead JPEG")
|
||||
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,
|
||||
help="Create JPEG files using mozJpeg")
|
||||
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
|
||||
help="The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.")
|
||||
processing_options.add_argument("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
|
||||
help="Turn 1x4 strips to 2x2 strips")
|
||||
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
|
||||
@@ -1402,6 +1439,11 @@ def checkOptions(options):
|
||||
options.isKobo = False
|
||||
options.bordersColor = None
|
||||
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':
|
||||
options.targetsize = 195
|
||||
options.format = 'EPUB'
|
||||
@@ -1413,6 +1455,8 @@ def checkOptions(options):
|
||||
options.format = 'MOBI'
|
||||
if options.batchsplit != 2:
|
||||
options.batchsplit = 1
|
||||
if not options.targetsize and options.profile.startswith('Rmk'):
|
||||
options.targetsize = 95
|
||||
if options.format == 'MOBI+EPUB':
|
||||
options.keep_epub = True
|
||||
options.format = 'MOBI'
|
||||
@@ -1458,9 +1502,6 @@ def checkOptions(options):
|
||||
if 'Ko' in options.profile:
|
||||
options.panelview = 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
|
||||
if options.format == 'KFX':
|
||||
options.format = 'EPUB'
|
||||
@@ -1479,6 +1520,24 @@ def checkOptions(options):
|
||||
image.ProfileData.Profiles["Custom"] = newProfile
|
||||
options.profile = "Custom"
|
||||
options.profileData = image.ProfileData.Profiles[options.profile]
|
||||
if not options.jpegquality:
|
||||
if options.profile.startswith('KS') or options.profile == 'KCS':
|
||||
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
|
||||
|
||||
# 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:
|
||||
options.profileData = list(image.ProfileData.Profiles[options.profile])
|
||||
options.profileData[1] = list(options.profileData[1])
|
||||
options.profileData[1][0] = min(1920, options.profileData[1][0])
|
||||
return options
|
||||
|
||||
|
||||
@@ -1526,21 +1585,28 @@ def makeFusion(sources: List[str]):
|
||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||
print("Running Fusion")
|
||||
|
||||
for source in sources:
|
||||
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||
path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
|
||||
needs_prefix = os_sorted(path_names) != path_names
|
||||
|
||||
for index, source in enumerate(sources, start=1):
|
||||
print(f"Processing {source}...")
|
||||
checkPre(source)
|
||||
print("Checking images...")
|
||||
path = getWorkFolder(source)
|
||||
pathfinder = os.path.join(path, "OEBPS", "Images")
|
||||
sanitizeTree(pathfinder)
|
||||
# TODO: remove flattenTree when subchapters are supported
|
||||
flattenTree(pathfinder)
|
||||
source_path = Path(source)
|
||||
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||
prefix = ''
|
||||
if needs_prefix:
|
||||
prefix = f'fusion_{index:04d}_'
|
||||
if source_path.is_file():
|
||||
os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
|
||||
else:
|
||||
os.renames(pathfinder, fusion_path.joinpath(source_path.name))
|
||||
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
|
||||
|
||||
getWorkFolder(source, str(targetpath))
|
||||
sanitizeTree(targetpath, prefix='fusion')
|
||||
# TODO: remove flattenTree when subchapters are supported
|
||||
flattenTree(targetpath)
|
||||
|
||||
end = perf_counter()
|
||||
print(f"makefusion: {end - start} seconds")
|
||||
@@ -1557,8 +1623,6 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
GUI.progressBarTick.emit('1')
|
||||
else:
|
||||
checkTools(source)
|
||||
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||
checkPre(source)
|
||||
print(f"{job_progress}Preparing source images...")
|
||||
path = getWorkFolder(source)
|
||||
@@ -1567,6 +1631,9 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
if options.filefusion:
|
||||
# Strip the fusion_0001_ sort prefix from makeFusion if present
|
||||
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
|
||||
cover = None
|
||||
if not options.webtoon:
|
||||
cover = image.Cover(cover_path, options)
|
||||
@@ -1758,4 +1825,3 @@ def makeMOBI(work, qtgui=None):
|
||||
makeMOBIWorkerPool.close()
|
||||
makeMOBIWorkerPool.join()
|
||||
return makeMOBIWorkerOutput
|
||||
|
||||
|
||||
@@ -62,18 +62,19 @@ def mergeDirectory(work):
|
||||
imagesValid.append(i[0])
|
||||
# Silently drop directories that contain too many images
|
||||
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
||||
if targetHeight > 131072 * 3:
|
||||
if targetHeight > 131072 * 4:
|
||||
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
|
||||
result = Image.new('RGB', (targetWidth, targetHeight))
|
||||
y = 0
|
||||
for i in imagesValid:
|
||||
img = Image.open(i).convert('RGB')
|
||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||
widthPercent = (targetWidth / float(img.size[0]))
|
||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
result.paste(img, (0, y))
|
||||
y += img.size[1]
|
||||
with Image.open(i) as img:
|
||||
img = img.convert('RGB')
|
||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||
widthPercent = (targetWidth / float(img.size[0]))
|
||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
result.paste(img, (0, y))
|
||||
y += img.size[1]
|
||||
os.remove(i)
|
||||
savePath = os.path.split(imagesValid[0])
|
||||
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
|
||||
@@ -253,10 +254,8 @@ def main(argv=None, job_progress='', qtgui=None):
|
||||
return 1
|
||||
if args.height > 0:
|
||||
for sourceDir in args.input:
|
||||
targetDir = sourceDir + "-Splitted"
|
||||
targetDir = sourceDir
|
||||
if os.path.isdir(sourceDir):
|
||||
rmtree(targetDir, True)
|
||||
os.renames(sourceDir, targetDir)
|
||||
work = []
|
||||
pagenumber = 1
|
||||
splitWorkerOutput = []
|
||||
@@ -313,8 +312,6 @@ def main(argv=None, job_progress='', qtgui=None):
|
||||
rmtree(targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||
splitWorkerOutput[0][1])
|
||||
if args.inPlace:
|
||||
os.renames(targetDir, sourceDir)
|
||||
else:
|
||||
rmtree(targetDir, True)
|
||||
raise UserWarning("C2P: Source directory is empty.")
|
||||
|
||||
@@ -30,6 +30,7 @@ from .shared import IMAGE_TYPES, subprocess_run
|
||||
|
||||
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
||||
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
||||
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
|
||||
|
||||
|
||||
class ComicArchive:
|
||||
@@ -73,7 +74,7 @@ class ComicArchive:
|
||||
missing = []
|
||||
|
||||
extraction_commands = [
|
||||
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
|
||||
[TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
|
||||
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
|
||||
]
|
||||
|
||||
@@ -125,7 +126,7 @@ class ComicArchive:
|
||||
def available_archive_tools():
|
||||
available = []
|
||||
|
||||
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
|
||||
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
|
||||
try:
|
||||
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
||||
available.append(tool)
|
||||
|
||||
@@ -101,8 +101,13 @@ class ProfileData:
|
||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe", (1860, 2480), 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),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
}
|
||||
|
||||
ProfilesKindle = {
|
||||
@@ -153,7 +158,7 @@ class ComicPageParser:
|
||||
|
||||
# Detect corruption in source image, let caller catch any exceptions triggered.
|
||||
srcImgPath = os.path.join(source[0], source[1])
|
||||
Image.open(srcImgPath).verify()
|
||||
# Image.open(srcImgPath).verify()
|
||||
with Image.open(srcImgPath) as im:
|
||||
self.image = im.copy()
|
||||
|
||||
@@ -192,7 +197,10 @@ class ComicPageParser:
|
||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||
spread = self.image
|
||||
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])
|
||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||
if self.opt.splitter != 1:
|
||||
@@ -213,7 +221,10 @@ class ComicPageParser:
|
||||
if self.opt.splitter > 0:
|
||||
spread = self.image
|
||||
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])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
@@ -406,13 +417,13 @@ class ComicPage:
|
||||
targetPath += '.jpg'
|
||||
if self.opt.mozjpeg:
|
||||
with io.BytesIO() as output:
|
||||
image.save(output, format="JPEG", optimize=1, quality=85)
|
||||
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
|
||||
input_jpeg_bytes = output.getvalue()
|
||||
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
||||
with open(targetPath, "wb") as output_jpeg_file:
|
||||
output_jpeg_file.write(output_jpeg_bytes)
|
||||
else:
|
||||
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
||||
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
|
||||
return targetPath
|
||||
|
||||
def gammaCorrectImage(self):
|
||||
@@ -485,7 +496,7 @@ class ComicPage:
|
||||
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
|
||||
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
|
||||
elif self.image.size[0] > self.size[0] * 2 or self.image.size[1] > self.size[1]:
|
||||
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1], Image.Resampling.LANCZOS))
|
||||
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1]), Image.Resampling.LANCZOS)
|
||||
return
|
||||
|
||||
ratio_device = float(self.size[1]) / float(self.size[0])
|
||||
@@ -496,7 +507,9 @@ class ComicPage:
|
||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||
pass
|
||||
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)
|
||||
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)
|
||||
@@ -562,8 +575,13 @@ class Cover:
|
||||
|
||||
size = list(self.options.profileData[1])
|
||||
if self.options.kindle_scribe_azw3:
|
||||
size[0] = min(size[0], 1920)
|
||||
size[1] = min(size[1], 1920)
|
||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||
if self.options.coverfill and not self.options.kindle_scribe_azw3:
|
||||
# TODO: Kindle Scribe case
|
||||
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
|
||||
else:
|
||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||
|
||||
def crop_main_cover(self):
|
||||
w, h = self.image.size
|
||||
@@ -581,7 +599,7 @@ class Cover:
|
||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
||||
try:
|
||||
if tomeid == 0:
|
||||
self.image.save(target, "JPEG", optimize=1, quality=85)
|
||||
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||
else:
|
||||
copy = self.image.copy()
|
||||
draw = ImageDraw.Draw(copy)
|
||||
@@ -595,7 +613,7 @@ class Cover:
|
||||
stroke_fill=0,
|
||||
stroke_width=25
|
||||
)
|
||||
copy.save(target, "JPEG", optimize=1, quality=85)
|
||||
copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||
except IOError:
|
||||
raise RuntimeError('Failed to save cover.')
|
||||
|
||||
@@ -603,6 +621,6 @@ class Cover:
|
||||
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
||||
try:
|
||||
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
||||
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
|
||||
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=self.options.jpegquality)
|
||||
except IOError:
|
||||
raise RuntimeError('Failed to upload cover.')
|
||||
|
||||
@@ -160,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
|
||||
for box in edge_bbox:
|
||||
edge = bw_img.crop(box)
|
||||
h = edge.histogram()
|
||||
if not edge.height or not edge.width:
|
||||
continue
|
||||
imperfections = h[255] / (edge.height * edge.width)
|
||||
if imperfections > 0 and imperfections < .02:
|
||||
bw_img.paste(im=0, box=box)
|
||||
|
||||
75
kindlecomicconverter/pdfjpgextract.py
Normal file
75
kindlecomicconverter/pdfjpgextract.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||
#
|
||||
# Based upon the code snippet by Ned Batchelder
|
||||
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for
|
||||
# any purpose with or without fee is hereby granted, provided that the
|
||||
# above copyright notice and this permission notice appear in all
|
||||
# copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
# skip stray images a few pixels in size in some PDFs
|
||||
# typical images are many thousands in length
|
||||
# https://github.com/ciromattia/kcc/pull/546
|
||||
STRAY_IMAGE_LENGTH_THRESHOLD = 300
|
||||
|
||||
|
||||
class PdfJpgExtract:
|
||||
def __init__(self, fname, fullPath):
|
||||
self.fname = fname
|
||||
self.path = fullPath
|
||||
|
||||
def getPath(self):
|
||||
return self.path
|
||||
|
||||
def extract(self):
|
||||
pdf = open(self.fname, "rb").read()
|
||||
startmark = b"\xff\xd8"
|
||||
startfix = 0
|
||||
endmark = b"\xff\xd9"
|
||||
endfix = 2
|
||||
i = 0
|
||||
njpg = 0
|
||||
while True:
|
||||
istream = pdf.find(b"stream", i)
|
||||
if istream < 0:
|
||||
break
|
||||
istart = pdf.find(startmark, istream, istream + 20)
|
||||
if istart < 0:
|
||||
i = istream + 20
|
||||
continue
|
||||
iend = pdf.find(b"endstream", istart)
|
||||
if iend < 0:
|
||||
raise Exception("Didn't find end of stream!")
|
||||
iend = pdf.find(endmark, iend - 20)
|
||||
if iend < 0:
|
||||
raise Exception("Didn't find end of JPG!")
|
||||
istart += startfix
|
||||
iend += endfix
|
||||
i = iend
|
||||
|
||||
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
|
||||
continue
|
||||
|
||||
jpg = pdf[istart:iend]
|
||||
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
|
||||
jpgfile.write(jpg)
|
||||
jpgfile.close()
|
||||
njpg += 1
|
||||
|
||||
return njpg
|
||||
@@ -1,4 +1,4 @@
|
||||
PySide6==6.5.2
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
|
||||
4
setup.py
4
setup.py
@@ -40,8 +40,8 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
if sys.platform == 'darwin':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
|
||||
if min_os:
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
|
||||
if min_os.startswith('10.1'):
|
||||
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
||||
else:
|
||||
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
||||
|
||||
Reference in New Issue
Block a user