mirror of
https://github.com/ciromattia/kcc
synced 2026-08-02 18:11:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0328a12f | ||
|
|
846a755761 | ||
|
|
ff80d275e6 | ||
|
|
b3e1d393b6 | ||
|
|
b4c71b3b91 | ||
|
|
7e412f12ae | ||
|
|
2076775d94 | ||
|
|
59df22b62d | ||
|
|
ff5edc5ce5 | ||
|
|
a52c0ce03a | ||
|
|
1c0506fe7d | ||
|
|
806c6232c3 | ||
|
|
6f9f5067d8 | ||
|
|
11be0dffcc | ||
|
|
4c10569ab1 | ||
|
|
2a5cd6718d | ||
|
|
fd1468904f | ||
|
|
feec40ffc8 | ||
|
|
f6ae7661bc | ||
|
|
ca14a4c813 | ||
|
|
0c1071a358 | ||
|
|
858251f509 | ||
|
|
3c66aa9a67 | ||
|
|
077957e665 | ||
|
|
c3f86e29c8 | ||
|
|
72e1546c2c | ||
|
|
544e866b51 | ||
|
|
363a36a764 | ||
|
|
bece5290a3 | ||
|
|
cab3703217 | ||
|
|
3723bf9d52 | ||
|
|
2a6d61530f | ||
|
|
5396f1f9c4 | ||
|
|
f1b58c83d6 | ||
|
|
0f009755b1 | ||
|
|
ee5bd150e5 | ||
|
|
a55c0ddb08 | ||
|
|
799961407e | ||
|
|
bc28df1f53 | ||
|
|
a8b2c055bf | ||
|
|
afa9c7e7e6 | ||
|
|
1f1b9a37fa | ||
|
|
c4d9512cbc | ||
|
|
89fc6437dd | ||
|
|
1e57da08a9 | ||
|
|
b4ef37dbb9 | ||
|
|
b95cf6e179 | ||
|
|
08070cdd97 | ||
|
|
df3d174437 | ||
|
|
dc4475bcb0 | ||
|
|
b2e7fd3f5a | ||
|
|
4102643110 | ||
|
|
ca6b3b7611 | ||
|
|
6d5db71b5b | ||
|
|
87c6b7143a | ||
|
|
a5bd995a6b | ||
|
|
bcc69b0f05 | ||
|
|
f96adc5dc3 | ||
|
|
f54b06e058 | ||
|
|
7ceeb29fae | ||
|
|
c385ef7ae0 | ||
|
|
9827f11944 | ||
|
|
8030884148 | ||
|
|
949fb0acb4 | ||
|
|
84f69a0950 | ||
|
|
868a31cb00 | ||
|
|
1af24b394f | ||
|
|
401876da22 | ||
|
|
ffeaaeca19 | ||
|
|
b95bb12393 | ||
|
|
4a6e4622ed | ||
|
|
a491810810 | ||
|
|
75d0342fe1 | ||
|
|
60d41b25e4 | ||
|
|
0db788589d | ||
|
|
f42e6aea5c | ||
|
|
53ae057cbb | ||
|
|
d729839976 | ||
|
|
42a50ed670 | ||
|
|
e6ef7c1732 | ||
|
|
f2a806a42a | ||
|
|
8798d71bfa | ||
|
|
19ce14eeee | ||
|
|
5a1e2dafcb | ||
|
|
f149ae23f3 | ||
|
|
2878e5d41b | ||
|
|
bd691989a9 | ||
|
|
d4aeb798c7 | ||
|
|
997a514e2a | ||
|
|
a3672f7a1e | ||
|
|
1b48a9fc5e | ||
|
|
d5dde46989 | ||
|
|
92c85c18e9 | ||
|
|
e5122cc188 | ||
|
|
61be6aa78e | ||
|
|
d6834063c1 | ||
|
|
1a8d74de4a | ||
|
|
a0a194ecf1 | ||
|
|
290578d66e | ||
|
|
f97398d481 | ||
|
|
a7a9f35686 | ||
|
|
d5146d02fc | ||
|
|
b0374e127d | ||
|
|
894dbfc8a2 | ||
|
|
72f98bb032 | ||
|
|
96bf14d386 | ||
|
|
d4e1565e4a | ||
|
|
c3030e8bd1 | ||
|
|
c5744117e3 | ||
|
|
cd2eeb4d0f | ||
|
|
e7b7054b0e | ||
|
|
6f26bd5874 | ||
|
|
d5ca8fb407 | ||
|
|
8d61a9e558 | ||
|
|
8aaedf274d | ||
|
|
5782a44e7b | ||
|
|
e4c918f0f3 | ||
|
|
8f4072bfab | ||
|
|
61f3097be5 | ||
|
|
fa33ef8f89 | ||
|
|
232bac00a9 | ||
|
|
d19a4754fa | ||
|
|
9a93cc4b17 | ||
|
|
e0471b2dc9 | ||
|
|
a87eb318cf | ||
|
|
87987c9ebf | ||
|
|
f5fe8d93b0 | ||
|
|
249f823f01 | ||
|
|
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 |
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -21,20 +21,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Release Date
|
||||
id: release_date
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||
# Always creates the "latest" tag
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
type=raw,value=${{ steps.release_date.outputs.release_date }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -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,16 +59,16 @@ 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@v7
|
||||
with:
|
||||
name: AppImage
|
||||
path: './*.AppImage*'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
LICENSE.txt
|
||||
*.AppImage*
|
||||
|
||||
@@ -27,8 +27,10 @@ jobs:
|
||||
matrix:
|
||||
os: [ macos-15-intel, macos-14 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -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,16 +80,16 @@ jobs:
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-os-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.dmg
|
||||
- name: Clean up keychain and provisioning profile
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
PYTHON_VERSION: 3.11.9
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get Python
|
||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||
- name: Install Python
|
||||
@@ -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,16 +51,16 @@ jobs:
|
||||
run: |
|
||||
python3 setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: osx-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
LICENSE.txt
|
||||
dist/*.dmg
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
command: build_c2p
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -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,12 +53,12 @@ jobs:
|
||||
python setup.py ${{ matrix.command }}
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
uses: signpath/github-action-submit-signing-request@v2.2
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
@@ -69,10 +69,10 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
env:
|
||||
WINDOWS_7: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
@@ -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,15 +46,26 @@ jobs:
|
||||
python setup.py build_binary
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.2
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
files: |
|
||||
dist/*.exe
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
setup.bat
|
||||
kindlecomicconverter/sentry.py
|
||||
other/windows/kindlegen.exe
|
||||
dist/
|
||||
build/
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
@@ -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 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.
|
||||
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,8 @@ 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
|
||||
7) Fixing page order problems due to Windows sort being different than Kindle/Kobo sort
|
||||
|
||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
||||
|
||||
@@ -46,7 +55,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,28 +103,29 @@ 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
|
||||
|
||||
## FAQ
|
||||
- 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.
|
||||
Additionally, it will break page numbers.
|
||||
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.
|
||||
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.
|
||||
Direct USB dropping is reccomended.
|
||||
- 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.
|
||||
- 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.
|
||||
- Where is KEPUB option?
|
||||
- Choosing a Kobo profile and EPUB output will output KEPUB.
|
||||
- All options have additional information in tooltips if you hover over the option.
|
||||
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
||||
- Kindle panel view not working?
|
||||
@@ -127,9 +139,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.
|
||||
- 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.
|
||||
- [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?
|
||||
- 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.
|
||||
|
||||
@@ -177,38 +186,47 @@ 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),
|
||||
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), 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),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
'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:
|
||||
@@ -224,14 +242,20 @@ MAIN:
|
||||
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE)
|
||||
[Default=KV]
|
||||
-m, --manga-style Manga style (right-to-left reading and splitting)
|
||||
--lightnovel Only resize images and preserve original file structure.
|
||||
--ebok Force EBOK tag instead of PDOC for MOBI
|
||||
--invertdirection Invert page turn direction
|
||||
-q, --hq Try to increase the quality of magnification
|
||||
-2, --two-panel Display two not four panels in Panel View mode
|
||||
--vertical4panel Show side panels first in virtual panel view
|
||||
-w, --webtoon Webtoon processing mode
|
||||
--ts TARGETSIZE, --targetsize TARGETSIZE
|
||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||
|
||||
PROCESSING:
|
||||
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||
--legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
|
||||
--pdfwidth Render vector PDFs based on device width instead of height.
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
@@ -252,11 +276,19 @@ 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
|
||||
--smartcovercrop Attempt to crop main cover from wide image
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--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
|
||||
--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.
|
||||
--tempdir Create temporary files directory on source file drive.
|
||||
|
||||
OUTPUT SETTINGS:
|
||||
-o OUTPUT, --output OUTPUT
|
||||
@@ -264,15 +296,19 @@ OUTPUT SETTINGS:
|
||||
-t TITLE, --title TITLE
|
||||
Comic title [Default=filename or directory name]
|
||||
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
|
||||
--keepcomicinfo Keep any original ComicInfo.xml files [Default=0]
|
||||
-a AUTHOR, --author AUTHOR
|
||||
Author name [Default=KCC]
|
||||
--language EPUB language [Default=en-US]
|
||||
-f FORMAT, --format FORMAT
|
||||
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
|
||||
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
||||
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
||||
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||
--onepagelandscape Show a single centered page in landscape
|
||||
--norotate Do not rotate double page spreads in spread splitter option.
|
||||
--rotateright Rotate double page spreads in opposite direction.
|
||||
--rotatefirst Put rotated spread first in spread splitter option.
|
||||
--filefusion Combines all input files into a single file.
|
||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||
@@ -338,6 +374,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python -m venv venv
|
||||
venv\Scripts\activate.bat
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
@@ -363,6 +400,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
@@ -416,7 +454,6 @@ Older links (dead):
|
||||
## PRIVACY
|
||||
**KCC** is initiating internet connections in two cases:
|
||||
* During startup - Version check and announcement check.
|
||||
* When error occurs - Automatic reporting on Windows and macOS.
|
||||
|
||||
## KNOWN ISSUES
|
||||
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||
|
||||
@@ -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
|
||||
+856
-555
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,19 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="volumeLine"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="bulkVolumeCheck">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> → sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> → range from 1 to 10<br><code>1, 3, 5</code> → specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
@@ -195,6 +208,7 @@
|
||||
<tabstops>
|
||||
<tabstop>seriesLine</tabstop>
|
||||
<tabstop>volumeLine</tabstop>
|
||||
<tabstop>bulkVolumeCheck</tabstop>
|
||||
<tabstop>titleLine</tabstop>
|
||||
<tabstop>numberLine</tabstop>
|
||||
<tabstop>writerLine</tabstop>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# macOS GUI build, used by setup.py build_binary
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, SPECPATH)
|
||||
from kindlecomicconverter import __version__
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['kcc.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Kindle Comic Converter',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icons/comic2ebook.icns'],
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=True,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='Kindle Comic Converter',
|
||||
)
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name='Kindle Comic Converter.app',
|
||||
icon='icons/comic2ebook.icns',
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'CFBundleShortVersionString': __version__,
|
||||
'CFBundleVersion': __version__,
|
||||
},
|
||||
)
|
||||
+666
-192
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import (Qt)
|
||||
from PySide6.QtGui import (QKeyEvent, QPixmap)
|
||||
from PySide6.QtWidgets import (QHBoxLayout, QLabel, QDialog)
|
||||
|
||||
class LabelSpreadsDialog(QDialog):
|
||||
def __init__(self, available_height, images, spreads):
|
||||
super().__init__()
|
||||
self.index = 0
|
||||
self.images = images
|
||||
self.spreads = spreads
|
||||
self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
|
||||
|
||||
self.setWindowTitle("TODO: Filename goes here")
|
||||
# self.setGeometry(APP.primaryScreen().availableGeometry())
|
||||
# self.setMaximumSize(APP.primaryScreen().availableSize())
|
||||
self.available_height = available_height
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
label = QLabel()
|
||||
label2 = QLabel()
|
||||
self.label = label
|
||||
self.label2 = label2
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(label2)
|
||||
label2.setText("not a spread")
|
||||
|
||||
help_text = [
|
||||
"Use arrows to change index.",
|
||||
"Use space bar to confirm spreads.",
|
||||
"Use spread shift option to offset by 1.",
|
||||
"Use enter key to confirm all spreads."
|
||||
"Close window to cancel."
|
||||
]
|
||||
buttonLabel = QLabel('\n'.join(help_text))
|
||||
layout.addWidget(buttonLabel)
|
||||
# print(label.size())
|
||||
# print(label.maximumSize())
|
||||
# l, t, r, b = layout.getContentsMargins()
|
||||
|
||||
#label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
|
||||
label.setPixmap(pixmap)
|
||||
#label.setScaledContents(True)
|
||||
|
||||
#label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
# pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# label2.setPixmap(pixmap2)
|
||||
#label2.setScaledContents(True)
|
||||
#self.resize(pixmap2.width(), pixmap2.height())
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
# t = 20
|
||||
# b = 20
|
||||
if isinstance(event, QKeyEvent):
|
||||
if event.key() == Qt.Key.Key_Left:
|
||||
self.index = max(0, self.index - 1)
|
||||
if self.index2page[self.index] in self.spreads:
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.label2.setText('not a spread')
|
||||
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
|
||||
self.label.setPixmap(pixmap)
|
||||
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# self.label2.setPixmap(pixmap2)
|
||||
elif event.key() == Qt.Key.Key_Right:
|
||||
self.index = min(self.index + 1, len(self.images) - 1)
|
||||
if self.index2page[self.index] in self.spreads:
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.label2.setText('not a spread')
|
||||
|
||||
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
|
||||
self.label.setPixmap(pixmap)
|
||||
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# self.label2.setPixmap(pixmap2)
|
||||
elif event.key() == Qt.Key.Key_Space:
|
||||
if self.label2.text() == "not a spread":
|
||||
self.spreads.append(os.path.basename(self.images[self.index]))
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.spreads.remove(os.path.basename(self.images[self.index]))
|
||||
self.label2.setText('not a spread')
|
||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||
self.accept()
|
||||
else:
|
||||
super().keyReleaseEvent(event)
|
||||
else:
|
||||
super().keyReleaseEvent(event)
|
||||
+668
-442
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||
QVBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
|
||||
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
||||
QSizePolicy, QVBoxLayout, QWidget)
|
||||
from . import KCC_rc
|
||||
|
||||
class Ui_editorDialog(object):
|
||||
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
|
||||
|
||||
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
|
||||
|
||||
self.bulkVolumeCheck = QCheckBox(self.editorWidget)
|
||||
self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck")
|
||||
self.bulkVolumeCheck.setVisible(False)
|
||||
|
||||
self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1)
|
||||
|
||||
self.label_3 = QLabel(self.editorWidget)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
|
||||
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
|
||||
self.verticalLayout.addWidget(self.optionWidget)
|
||||
|
||||
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.titleLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
|
||||
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
|
||||
QWidget.setTabOrder(self.titleLine, self.numberLine)
|
||||
QWidget.setTabOrder(self.numberLine, self.writerLine)
|
||||
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
|
||||
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
|
||||
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
||||
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
||||
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"<b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> \u2192 sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> \u2192 range from 1 to 10<br><code>1, 3, 5</code> \u2192 specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setText("")
|
||||
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
|
||||
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '9.3.6'
|
||||
__version__ = '11.0.1'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
+511
-152
File diff suppressed because it is too large
Load Diff
@@ -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.")
|
||||
|
||||
@@ -22,7 +22,6 @@ from functools import cached_property, lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import distro
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.parsers.expat import ExpatError
|
||||
@@ -30,6 +29,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:
|
||||
@@ -40,12 +40,12 @@ class ComicArchive:
|
||||
self.dirname, self.basename = os.path.split(filepath)
|
||||
|
||||
@cached_property
|
||||
def type(self):
|
||||
def type(self):
|
||||
extraction_commands = [
|
||||
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||
]
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'l', '-y', '-p1', self.basename],
|
||||
)
|
||||
@@ -73,7 +73,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],
|
||||
]
|
||||
|
||||
@@ -84,20 +84,20 @@ class ComicArchive:
|
||||
|
||||
extraction_commands.reverse()
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||
)
|
||||
|
||||
|
||||
for cmd in extraction_commands:
|
||||
try:
|
||||
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||
return targetdir
|
||||
return targetdir
|
||||
except FileNotFoundError:
|
||||
missing.append(cmd[0])
|
||||
except CalledProcessError:
|
||||
pass
|
||||
|
||||
|
||||
if missing:
|
||||
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
|
||||
else:
|
||||
@@ -125,7 +125,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)
|
||||
|
||||
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
|
||||
|
||||
|
||||
class DualMobiMetaFix:
|
||||
def __init__(self, infile, outfile, asin, is_pdoc):
|
||||
def __init__(self, outfile, asin, is_pdoc):
|
||||
cdetype = b'EBOK'
|
||||
if is_pdoc:
|
||||
cdetype = b'PDOC'
|
||||
|
||||
shutil.copyfile(infile, outfile)
|
||||
f = open(outfile, "r+b")
|
||||
self.datain = mmap.mmap(f.fileno(), 0)
|
||||
self.datain_rec0 = readsection(self.datain, 0)
|
||||
|
||||
+114
-42
@@ -29,6 +29,7 @@ from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
|
||||
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||
from .shared import get_contain_resolution
|
||||
|
||||
AUTO_CROP_THRESHOLD = 0.015
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
@@ -98,11 +99,18 @@ class ProfileData:
|
||||
'KV': ("Kindle Voyage", (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),
|
||||
'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),
|
||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||
}
|
||||
|
||||
ProfilesKindle = {
|
||||
@@ -153,11 +161,14 @@ 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()
|
||||
|
||||
self.fill = self.fillCheck()
|
||||
self.page_background_color = self.fillCheck()
|
||||
self.fill = self.page_background_color
|
||||
if self.opt.bordersColor:
|
||||
self.fill = self.opt.bordersColor
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
@@ -187,15 +198,23 @@ class ComicPageParser:
|
||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||
new_image.paste(pageone, (0, 0))
|
||||
new_image.paste(pagetwo, (0, height))
|
||||
self.payload.append(['N', self.source, new_image, self.fill])
|
||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||
self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
|
||||
elif self.opt.webtoon:
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, 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
|
||||
if not self.opt.norotate:
|
||||
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:
|
||||
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.page_background_color, self.fill])
|
||||
# elif wide enough to split
|
||||
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
|
||||
# if (split) or (split and rotate)
|
||||
BISECT_THRESHOLD = 1.8
|
||||
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
|
||||
if width > height:
|
||||
leftbox = (0, 0, int(width / 2), height)
|
||||
rightbox = (int(width / 2), 0, width, height)
|
||||
@@ -208,18 +227,23 @@ class ComicPageParser:
|
||||
else:
|
||||
pageone = self.image.crop(leftbox)
|
||||
pagetwo = self.image.crop(rightbox)
|
||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||
if self.opt.splitter > 0:
|
||||
self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
|
||||
|
||||
# if (rotate) or (split and rotate)
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
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.page_background_color, self.fill])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
||||
|
||||
def fillCheck(self):
|
||||
if self.opt.bordersColor:
|
||||
if False:
|
||||
return self.opt.bordersColor
|
||||
else:
|
||||
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
||||
@@ -258,7 +282,7 @@ class ComicPageParser:
|
||||
|
||||
|
||||
class ComicPage:
|
||||
def __init__(self, options, mode, path, image, fill):
|
||||
def __init__(self, options, mode, path, image, page_background_color, fill):
|
||||
self.opt = options
|
||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||
if self.opt.hq:
|
||||
@@ -266,6 +290,9 @@ class ComicPage:
|
||||
self.original_color_mode = image.mode
|
||||
# TODO: color check earlier
|
||||
self.image = image.convert("RGB")
|
||||
self.color = self.colorCheck()
|
||||
self.colorOutput = self.color and self.opt.forcecolor
|
||||
self.page_background_color = page_background_color
|
||||
self.fill = fill
|
||||
self.rotated = False
|
||||
self.orgPath = os.path.join(path[0], path[1])
|
||||
@@ -284,8 +311,7 @@ class ComicPage:
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
|
||||
@cached_property
|
||||
def color(self):
|
||||
def colorCheck(self):
|
||||
if self.original_color_mode in ("L", "1"):
|
||||
return False
|
||||
if self.opt.webtoon:
|
||||
@@ -394,25 +420,32 @@ class ComicPage:
|
||||
raise RuntimeError('Cannot save image. ' + str(err))
|
||||
|
||||
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)
|
||||
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'
|
||||
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
||||
else:
|
||||
targetPath += '.png'
|
||||
image.save(targetPath, 'PNG', optimize=1)
|
||||
else:
|
||||
targetPath += '.jpg'
|
||||
if self.opt.mozjpeg:
|
||||
if self.opt.webp_output:
|
||||
targetPath += '.webp'
|
||||
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
|
||||
elif self.opt.mozjpeg:
|
||||
targetPath += '.jpg'
|
||||
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)
|
||||
targetPath += '.jpg'
|
||||
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
|
||||
return targetPath
|
||||
|
||||
def gammaCorrectImage(self):
|
||||
@@ -485,26 +518,38 @@ 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])
|
||||
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
||||
method = self.resize_method()
|
||||
if self.opt.stretch:
|
||||
if self.opt.kfx:
|
||||
ratio_kfx = self.opt.kfx_resolution[1] / self.opt.kfx_resolution[0]
|
||||
contain_size = get_contain_resolution(self.image, self.size)
|
||||
if abs(ratio_image - ratio_kfx) < AUTO_CROP_THRESHOLD:
|
||||
if contain_size[0] > self.opt.kfx_resolution[0] or contain_size[1] > self.opt.kfx_resolution[1]:
|
||||
self.image = ImageOps.fit(self.image, self.opt.kfx_resolution, method=method)
|
||||
else:
|
||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||
else:
|
||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||
elif self.opt.stretch:
|
||||
self.image = self.image.resize(self.size, method)
|
||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||
pass
|
||||
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 (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
|
||||
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')) and not self.opt.white_borders:
|
||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||
else:
|
||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||
|
||||
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
|
||||
else:
|
||||
return Image.Resampling.LANCZOS
|
||||
@@ -521,7 +566,7 @@ class ComicPage:
|
||||
self.image = self.image.crop(box)
|
||||
|
||||
def cropPageNumber(self, power, minimum):
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -531,7 +576,7 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropMargin(self, power, minimum):
|
||||
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
||||
bbox = get_bbox_crop_margin(self.image, power, self.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -541,13 +586,14 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropInterPanelEmptySections(self, direction):
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
|
||||
|
||||
class Cover:
|
||||
def __init__(self, source, opt):
|
||||
self.options = opt
|
||||
self.source = source
|
||||
self.image = Image.open(source)
|
||||
self.smartcover = False
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
@@ -558,30 +604,56 @@ class Cover:
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
if not self.options.forcecolor:
|
||||
self.image = self.image.convert('L')
|
||||
self.crop_main_cover()
|
||||
if self.options.smartcovercrop:
|
||||
self.crop_main_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
|
||||
if w / h > 2:
|
||||
self.smartcover = True
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
||||
else:
|
||||
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:
|
||||
self.smartcover = True
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
||||
else:
|
||||
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((0, 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:
|
||||
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 +667,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 +675,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)
|
||||
|
||||
@@ -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
|
||||
@@ -61,6 +61,23 @@ def getImageFileName(imgfile):
|
||||
ext = ext.lower()
|
||||
return [name, ext]
|
||||
|
||||
def get_contain_resolution(image, size):
|
||||
'''same code as Pillow ImageOps.contain()'''
|
||||
im_ratio = image.width / image.height
|
||||
dest_ratio = size[0] / size[1]
|
||||
|
||||
if im_ratio != dest_ratio:
|
||||
if im_ratio > dest_ratio:
|
||||
new_height = round(image.height / image.width * size[0])
|
||||
if new_height != size[1]:
|
||||
size = (size[0], new_height)
|
||||
else:
|
||||
new_width = round(image.width / image.height * size[1])
|
||||
if new_width != size[0]:
|
||||
size = (new_width, size[1])
|
||||
|
||||
return size
|
||||
|
||||
|
||||
def walkSort(dirnames, filenames):
|
||||
convert = lambda text: int(text) if text.isdigit() else text
|
||||
@@ -105,10 +122,6 @@ def dependencyCheck(level):
|
||||
missing.append('PySide 6.0.0')
|
||||
except ImportError:
|
||||
missing.append('PySide 6.0.0+')
|
||||
try:
|
||||
import raven
|
||||
except ImportError:
|
||||
missing.append('raven 6.0.0+')
|
||||
if level > 1:
|
||||
try:
|
||||
from psutil import __version__ as psutilVersion
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
# Below requirements are compiled in Dockefile
|
||||
# numpy==2.3.4
|
||||
# PyMuPDF==1.26.6
|
||||
@@ -1,12 +1,10 @@
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy<2
|
||||
PyMuPDF>=1.26.1
|
||||
PyMuPDF==1.25.5
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
PySide6==6.1.3
|
||||
Pillow>=9
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.32.4
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy==1.23.0
|
||||
numpy==1.23.5
|
||||
PyMuPDF>=1.16
|
||||
|
||||
+4
-6
@@ -1,12 +1,10 @@
|
||||
PySide6<6.10
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1,<9.0.0
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4,<9.0.0
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy>=1.22.4
|
||||
PyMuPDF>=1.18.0
|
||||
|
||||
@@ -38,10 +38,10 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
def run(self):
|
||||
VERSION = __version__
|
||||
if sys.platform == 'darwin':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
||||
os.system('pyinstaller -y kcc-macos.spec')
|
||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
|
||||
if min_os:
|
||||
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')
|
||||
@@ -153,11 +153,10 @@ setuptools.setup(
|
||||
'psutil>=5.9.5',
|
||||
'requests>=2.31.0',
|
||||
'python-slugify>=1.2.1,<9.0.0',
|
||||
'raven>=6.0.0',
|
||||
'mozjpeg-lossless-optimization>=1.2.0',
|
||||
'natsort>=8.4.0',
|
||||
'distro>=1.8.0',
|
||||
'numpy>=1.22.4',
|
||||
'packaging>=23.2',
|
||||
'PyMuPDF>=1.16.1',
|
||||
],
|
||||
classifiers=[],
|
||||
|
||||
Reference in New Issue
Block a user