mirror of
https://github.com/ciromattia/kcc
synced 2026-06-10 16:40:30 +00:00
Compare commits
201 Commits
v9.2.0
...
webtoon-8x
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e68ce380c | |||
| 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 | |||
| db4eb78963 | |||
| 988fc93dc5 | |||
| 74fee9346c | |||
| 9fcacd7ae6 | |||
| 8ac58e361f | |||
| 61d6972e22 | |||
| c7c1557e72 | |||
| cb93704e08 | |||
| 62c5183609 | |||
| a629f267a1 | |||
| aeec4dd294 | |||
| 0d3076465b | |||
| 984d44b371 | |||
| 1111263893 | |||
| 5035c7403e | |||
| 067aa68162 | |||
| 72d07d53ea | |||
| 8c242d45d7 | |||
| c655922a57 | |||
| 77e8952f12 | |||
| 5b069322a4 | |||
| 2444a28127 | |||
| 3aad79fc30 | |||
| 2dbc13303f | |||
| 4c36c7c586 | |||
| 65007aec07 | |||
| 9429bed91c | |||
| 3a3ee15cba | |||
| 2394aa3747 | |||
| b57992a754 | |||
| c7a62fdcd6 | |||
| 8861299d24 | |||
| 636447bb62 | |||
| b23c7744cb | |||
| 2398a5b1ac | |||
| 2b2ac8ff55 | |||
| 5209d9a7b8 | |||
| 5336870097 | |||
| 4371d14391 | |||
| f96b7cb22b | |||
| 4dfd2ea942 | |||
| ba7f4336a5 | |||
| 9561b04bec | |||
| 2a8f8e9ab4 | |||
| b9cef59912 | |||
| f2ab730691 | |||
| 44401583e4 | |||
| 28faf524c4 | |||
| 2d288f72ea | |||
| fb9b3c676b | |||
| cff1de4fa5 |
+28
-1
@@ -1,13 +1,40 @@
|
|||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
|
|
||||||
build
|
build
|
||||||
dist
|
dist
|
||||||
KindleComicConverter.egg-info
|
KindleComicConverter.egg-info
|
||||||
|
|
||||||
.dockerignore
|
.dockerignore
|
||||||
.gitignore
|
.gitignore
|
||||||
.travis.yml
|
.travis.yml
|
||||||
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
venv
|
venv
|
||||||
|
.venv
|
||||||
|
__pycache__/
|
||||||
|
*/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
*.md
|
*.md
|
||||||
LICENSE.txt
|
*.txt
|
||||||
|
!requirements-docker.txt
|
||||||
MANIFEST.in
|
MANIFEST.in
|
||||||
|
|
||||||
|
*.yml
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
*.svg
|
||||||
|
*.jpg
|
||||||
|
*.json
|
||||||
|
|
||||||
|
gen_ui_files.bat
|
||||||
|
gen_ui_files.sh
|
||||||
|
|
||||||
|
gui/
|
||||||
|
icons/
|
||||||
|
|
||||||
|
kindlecomicconverter/KCC_gui.py
|
||||||
|
kindlecomicconverter/KCC_rc.py
|
||||||
|
kindlecomicconverter/KCC_ui_editor.py
|
||||||
|
kindlecomicconverter/KCC_ui.py
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
name: Docker base
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags: [ 'docker-base-*' ]
|
|
||||||
|
|
||||||
# Don't trigger if it's just a documentation update
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- '**.MD'
|
|
||||||
- '**.yml'
|
|
||||||
- 'docs/**'
|
|
||||||
- 'LICENSE'
|
|
||||||
- '.gitattributes'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.dockerignore'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_push:
|
|
||||||
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
|
|
||||||
with:
|
|
||||||
docker_build_file: ./Dockerfile-base
|
|
||||||
platform_linux_arm32v7_enabled: true
|
|
||||||
platform_linux_arm64v8_enabled: true
|
|
||||||
platform_linux_amd64_enabled: true
|
|
||||||
push_enabled: true
|
|
||||||
build_nohealthcheck: false
|
|
||||||
ghcr_repo_owner: ${{ github.repository_owner }}
|
|
||||||
ghcr_repo: ${{ github.repository }}
|
|
||||||
build_latest: false
|
|
||||||
secrets:
|
|
||||||
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Docker
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
# Publish semver tags as releases.
|
tags:
|
||||||
tags: [ 'v*.*.*' ]
|
- 'v*.*.*'
|
||||||
|
|
||||||
# Don't trigger if it's just a documentation update
|
# Don't trigger if it's just a documentation update
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
@@ -15,19 +15,53 @@ on:
|
|||||||
- 'LICENSE'
|
- 'LICENSE'
|
||||||
- '.gitattributes'
|
- '.gitattributes'
|
||||||
- '.gitignore'
|
- '.gitignore'
|
||||||
- '.dockerignore'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_push:
|
build_and_publish_base_image:
|
||||||
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
|
runs-on: ubuntu-latest
|
||||||
with:
|
steps:
|
||||||
platform_linux_arm32v7_enabled: true
|
- name: Checkout
|
||||||
platform_linux_arm64v8_enabled: true
|
uses: actions/checkout@v6
|
||||||
platform_linux_amd64_enabled: true
|
|
||||||
push_enabled: true
|
- name: Login to GitHub Container Registry
|
||||||
build_nohealthcheck: false
|
uses: docker/login-action@v4
|
||||||
ghcr_repo_owner: ${{ github.repository_owner }}
|
with:
|
||||||
ghcr_repo: ${{ github.repository }}
|
registry: ghcr.io
|
||||||
secrets:
|
username: ${{ github.repository_owner }}
|
||||||
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Set Release Date
|
||||||
|
id: release_date
|
||||||
|
run: |
|
||||||
|
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||||
|
# Always creates the "latest" tag
|
||||||
|
flavor: |
|
||||||
|
latest=true
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=${{ steps.release_date.outputs.release_date }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=ghcr.io/ciromattia/kcc:cache
|
||||||
|
type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
|
||||||
|
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
|
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
|
||||||
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
|
python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
- name: build binary
|
- name: build binary
|
||||||
run: |
|
run: |
|
||||||
@@ -59,16 +59,16 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: AppImage
|
name: AppImage
|
||||||
path: './*.AppImage*'
|
path: './*.AppImage*'
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
LICENSE.txt
|
LICENSE.txt
|
||||||
*.AppImage*
|
*.AppImage*
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-13, macos-14 ]
|
os: [ macos-15-intel, macos-14 ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -36,7 +38,7 @@ jobs:
|
|||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
- name: Install python dependencies
|
- name: Install python dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
|
python -m pip install --upgrade pip pyinstaller certifi
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
- name: Install the Apple certificate and provisioning profile
|
- name: Install the Apple certificate and provisioning profile
|
||||||
# TODO signing
|
# TODO signing
|
||||||
@@ -78,18 +80,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python setup.py build_binary
|
python setup.py build_binary
|
||||||
- name: upload build
|
- name: upload build
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: mac-os-build-${{ runner.arch }}
|
name: mac-os-build-${{ runner.arch }}
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
LICENSE.txt
|
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
- name: Clean up keychain and provisioning profile
|
- name: Clean up keychain and provisioning profile
|
||||||
# TODO signing
|
# TODO signing
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-13 ]
|
os: [ macos-15-intel ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
# We need the official Python, because the GA ones only support newer macOS versions
|
# We need the official Python, because the GA ones only support newer macOS versions
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
PYTHON_VERSION: 3.11.9
|
PYTHON_VERSION: 3.11.9
|
||||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Get Python
|
- name: Get Python
|
||||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Install Python dependencies
|
- name: Install Python dependencies
|
||||||
run: |
|
run: |
|
||||||
python3 --version
|
python3 --version
|
||||||
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
|
pip3 install --upgrade pip pyinstaller certifi
|
||||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||||
./gen_ui_files.sh
|
./gen_ui_files.sh
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
@@ -51,16 +51,16 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python3 setup.py build_binary
|
python3 setup.py build_binary
|
||||||
- name: upload build
|
- name: upload build
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: osx-build-${{ runner.arch }}
|
name: osx-build-${{ runner.arch }}
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
LICENSE.txt
|
LICENSE.txt
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
|
||||||
|
|
||||||
name: build KCC for windows with docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
entry: [ kcc-c2e, kcc-c2p ]
|
|
||||||
include:
|
|
||||||
- entry: kcc-c2e
|
|
||||||
capital: KCC_c2e
|
|
||||||
- entry: kcc-c2p
|
|
||||||
capital: KCC_c2p
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Package Application
|
|
||||||
uses: JackMcKew/pyinstaller-action-windows@main
|
|
||||||
with:
|
|
||||||
path: .
|
|
||||||
spec: ./${{ matrix.entry }}.spec
|
|
||||||
- name: rename binaries
|
|
||||||
run: |
|
|
||||||
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
|
|
||||||
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
|
|
||||||
|
|
||||||
- name: upload-unsigned-artifact
|
|
||||||
id: upload-unsigned-artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-build-${{ matrix.entry }}
|
|
||||||
path: dist/windows/*.exe
|
|
||||||
|
|
||||||
- id: optional_step_id
|
|
||||||
uses: signpath/github-action-submit-signing-request@v1.3
|
|
||||||
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/windows/'
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
LICENSE.txt
|
|
||||||
dist/windows/*.exe
|
|
||||||
@@ -23,9 +23,19 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
entry: [ kcc, kcc-c2e, kcc-c2p ]
|
||||||
|
include:
|
||||||
|
- entry: kcc
|
||||||
|
command: build_binary
|
||||||
|
- entry: kcc-c2e
|
||||||
|
command: build_c2e
|
||||||
|
- entry: kcc-c2p
|
||||||
|
command: build_c2p
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -35,20 +45,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install certifi pyinstaller --no-binary pyinstaller
|
pip install certifi pyinstaller --no-binary pyinstaller
|
||||||
- name: build binary
|
- name: build binary
|
||||||
run: |
|
run: |
|
||||||
python setup.py build_binary
|
python setup.py ${{ matrix.command }}
|
||||||
- name: upload-unsigned-artifact
|
- name: upload-unsigned-artifact
|
||||||
id: upload-unsigned-artifact
|
id: upload-unsigned-artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: windows-build
|
name: windows-build-${{ matrix.entry }}
|
||||||
path: dist/*.exe
|
path: dist/*.exe
|
||||||
- id: optional_step_id
|
- id: optional_step_id
|
||||||
uses: signpath/github-action-submit-signing-request@v1.3
|
uses: signpath/github-action-submit-signing-request@v2.2
|
||||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||||
with:
|
with:
|
||||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
@@ -59,11 +69,10 @@ jobs:
|
|||||||
wait-for-completion: true
|
wait-for-completion: true
|
||||||
output-artifact-directory: 'dist/'
|
output-artifact-directory: 'dist/'
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
LICENSE.txt
|
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
WINDOWS_7: 1
|
WINDOWS_7: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements-win7.txt
|
pip install -r requirements-win7.txt
|
||||||
pip install certifi pyinstaller --no-binary pyinstaller
|
pip install certifi pyinstaller --no-binary pyinstaller
|
||||||
.\gen_ui_files.bat
|
.\gen_ui_files.bat
|
||||||
@@ -46,15 +46,26 @@ jobs:
|
|||||||
python setup.py build_binary
|
python setup.py build_binary
|
||||||
- name: upload-unsigned-artifact
|
- name: upload-unsigned-artifact
|
||||||
id: upload-unsigned-artifact
|
id: upload-unsigned-artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: windows7-build
|
name: windows7-build
|
||||||
path: dist/*.exe
|
path: dist/*.exe
|
||||||
|
- id: optional_step_id
|
||||||
|
uses: signpath/github-action-submit-signing-request@v2.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
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
files: |
|
files: |
|
||||||
dist/*.exe
|
dist/*.exe
|
||||||
|
|||||||
+1
-1
@@ -2,12 +2,12 @@
|
|||||||
Pipfile
|
Pipfile
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
setup.bat
|
setup.bat
|
||||||
kindlecomicconverter/sentry.py
|
|
||||||
other/windows/kindlegen.exe
|
other/windows/kindlegen.exe
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
KindleComicConverter*.egg-info/
|
KindleComicConverter*.egg-info/
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
win7
|
win7
|
||||||
osx10.11
|
osx10.11
|
||||||
/venv/
|
/venv/
|
||||||
|
|||||||
+74
-16
@@ -1,19 +1,77 @@
|
|||||||
# Select final stage based on TARGETARCH ARG
|
# STAGE 1: BUILDER
|
||||||
FROM ghcr.io/ciromattia/kcc:docker-base-20241116
|
# Contains all build tools and dev dependencies, will be discarded
|
||||||
LABEL com.kcc.name="Kindle Comic Converter"
|
FROM python:3.13-slim-bullseye AS builder
|
||||||
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
|
|
||||||
LABEL org.opencontainers.image.description='Kindle Comic Converter'
|
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.authors='darodi'
|
|
||||||
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.vendor='ciromattia'
|
|
||||||
LABEL org.opencontainers.image.licenses='ISC'
|
|
||||||
LABEL org.opencontainers.image.title="Kindle Comic Converter"
|
|
||||||
|
|
||||||
COPY . /opt/kcc
|
# Install system dependencies
|
||||||
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
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" && \
|
||||||
|
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}
|
||||||
|
|
||||||
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
|
RUN \
|
||||||
|
set -x && \
|
||||||
|
python -m venv /opt/venv && \
|
||||||
|
. /opt/venv/bin/activate && \
|
||||||
|
pip install --upgrade pip
|
||||||
|
|
||||||
|
# Install numpy first, as it is unlikely to change and takes too long to compile
|
||||||
|
RUN \
|
||||||
|
set -x && \
|
||||||
|
. /opt/venv/bin/activate && \
|
||||||
|
pip install --no-cache-dir numpy==2.3.4
|
||||||
|
|
||||||
|
# Install PyMuPDF separately, as it is likely to change but still takes too long to compile
|
||||||
|
RUN \
|
||||||
|
set -x && \
|
||||||
|
. /opt/venv/bin/activate && \
|
||||||
|
pip install --no-cache-dir PyMuPDF==1.26.6
|
||||||
|
|
||||||
|
# Install Python dependencies using virtual environment
|
||||||
|
COPY requirements-docker.txt .
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
set -x && \
|
||||||
|
. /opt/venv/bin/activate && \
|
||||||
|
pip install --no-cache-dir -r requirements-docker.txt
|
||||||
|
|
||||||
|
# STAGE 2: FINAL
|
||||||
|
# Clean, small and secure image with only runtime dependencies
|
||||||
|
FROM python:3.13-slim-bullseye
|
||||||
|
|
||||||
|
# Install runtime dependencies only
|
||||||
|
RUN \
|
||||||
|
set -x && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
|
||||||
|
apt-get install -y --no-install-recommends p7zip-full && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy artifacts from builder
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
COPY . /opt/kcc/
|
||||||
|
|
||||||
|
WORKDIR /opt/kcc
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Setup executable and version file
|
||||||
|
RUN \
|
||||||
|
chmod +x /opt/kcc/entrypoint.sh && \
|
||||||
|
ln -s /opt/kcc/kcc-c2e.py /usr/local/bin/c2e && \
|
||||||
|
ln -s /opt/kcc/kcc-c2p.py /usr/local/bin/c2p && \
|
||||||
|
ln -s /opt/kcc/entrypoint.sh /usr/local/bin/entrypoint && \
|
||||||
|
ln -s /opt/kcc/kindlegen/kindlegen /usr/local/bin/kindlegen && \
|
||||||
|
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
||||||
|
|
||||||
|
LABEL com.kcc.name="Kindle Comic Converter" \
|
||||||
|
com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" \
|
||||||
|
org.opencontainers.image.title="Kindle Comic Converter" \
|
||||||
|
org.opencontainers.image.description='Kindle Comic Converter' \
|
||||||
|
org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' \
|
||||||
|
org.opencontainers.image.source='https://github.com/ciromattia/kcc' \
|
||||||
|
org.opencontainers.image.authors='Darodi and José Cerezo' \
|
||||||
|
org.opencontainers.image.url='https://github.com/ciromattia/kcc' \
|
||||||
|
org.opencontainers.image.vendor='ciromattia' \
|
||||||
|
org.opencontainers.image.licenses='ISC'
|
||||||
|
|
||||||
|
ENTRYPOINT ["entrypoint"]
|
||||||
CMD ["-h"]
|
CMD ["-h"]
|
||||||
|
|||||||
-164
@@ -1,164 +0,0 @@
|
|||||||
FROM --platform=linux/amd64 python:3.13-slim-bullseye as compile-amd64
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG TARGETVARIANT
|
|
||||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
|
||||||
|
|
||||||
|
|
||||||
COPY requirements.txt /opt/kcc/
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
|
||||||
apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 && \
|
|
||||||
python -m pip install --upgrade pip && \
|
|
||||||
python -m venv /opt/venv && \
|
|
||||||
python -m pip install -r /opt/kcc/requirements.txt
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG TARGETVARIANT
|
|
||||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
|
||||||
|
|
||||||
ENV LC_ALL=C.UTF-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LANGUAGE=en_US:en
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
|
|
||||||
COPY requirements.txt /opt/kcc/
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
|
|
||||||
RUN set -x && \
|
|
||||||
TEMP_PACKAGES=() && \
|
|
||||||
KEPT_PACKAGES=() && \
|
|
||||||
# Packages only required during build
|
|
||||||
TEMP_PACKAGES+=(build-essential) && \
|
|
||||||
TEMP_PACKAGES+=(cmake) && \
|
|
||||||
TEMP_PACKAGES+=(libfreetype6-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libfontconfig1-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libpng-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libjpeg-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libssl-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libxft-dev) && \
|
|
||||||
TEMP_PACKAGES+=(make) && \
|
|
||||||
TEMP_PACKAGES+=(python3-dev) && \
|
|
||||||
TEMP_PACKAGES+=(python3-setuptools) && \
|
|
||||||
TEMP_PACKAGES+=(python3-wheel) && \
|
|
||||||
# Packages kept in the image
|
|
||||||
KEPT_PACKAGES+=(bash) && \
|
|
||||||
KEPT_PACKAGES+=(ca-certificates) && \
|
|
||||||
KEPT_PACKAGES+=(chrpath) && \
|
|
||||||
KEPT_PACKAGES+=(locales) && \
|
|
||||||
KEPT_PACKAGES+=(locales-all) && \
|
|
||||||
KEPT_PACKAGES+=(libfreetype6) && \
|
|
||||||
KEPT_PACKAGES+=(libfontconfig1) && \
|
|
||||||
KEPT_PACKAGES+=(p7zip-full) && \
|
|
||||||
KEPT_PACKAGES+=(python3) && \
|
|
||||||
KEPT_PACKAGES+=(python3-pip) && \
|
|
||||||
KEPT_PACKAGES+=(unrar-free) && \
|
|
||||||
# Install packages
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
||||||
${KEPT_PACKAGES[@]} \
|
|
||||||
${TEMP_PACKAGES[@]} \
|
|
||||||
&& \
|
|
||||||
# Install required python modules
|
|
||||||
python -m pip install --upgrade pip && \
|
|
||||||
python -m venv /opt/venv && \
|
|
||||||
python -m pip install -r /opt/kcc/requirements.txt
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
|
||||||
|
|
||||||
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG TARGETVARIANT
|
|
||||||
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
|
|
||||||
|
|
||||||
ENV LC_ALL=C.UTF-8 \
|
|
||||||
LANG=C.UTF-8 \
|
|
||||||
LANGUAGE=en_US:en
|
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
||||||
|
|
||||||
COPY requirements.txt /opt/kcc/
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
|
|
||||||
RUN set -x && \
|
|
||||||
TEMP_PACKAGES=() && \
|
|
||||||
KEPT_PACKAGES=() && \
|
|
||||||
# Packages only required during build
|
|
||||||
TEMP_PACKAGES+=(build-essential) && \
|
|
||||||
TEMP_PACKAGES+=(cmake) && \
|
|
||||||
TEMP_PACKAGES+=(libffi-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libfreetype6-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libfontconfig1-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libpng-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libjpeg-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libssl-dev) && \
|
|
||||||
TEMP_PACKAGES+=(libxft-dev) && \
|
|
||||||
TEMP_PACKAGES+=(make) && \
|
|
||||||
TEMP_PACKAGES+=(python3-dev) && \
|
|
||||||
TEMP_PACKAGES+=(python3-setuptools) && \
|
|
||||||
TEMP_PACKAGES+=(python3-wheel) && \
|
|
||||||
# Packages kept in the image
|
|
||||||
KEPT_PACKAGES+=(bash) && \
|
|
||||||
KEPT_PACKAGES+=(ca-certificates) && \
|
|
||||||
KEPT_PACKAGES+=(chrpath) && \
|
|
||||||
KEPT_PACKAGES+=(locales) && \
|
|
||||||
KEPT_PACKAGES+=(locales-all) && \
|
|
||||||
KEPT_PACKAGES+=(libfreetype6) && \
|
|
||||||
KEPT_PACKAGES+=(libfontconfig1) && \
|
|
||||||
KEPT_PACKAGES+=(p7zip-full) && \
|
|
||||||
KEPT_PACKAGES+=(python3) && \
|
|
||||||
KEPT_PACKAGES+=(python3-pip) && \
|
|
||||||
KEPT_PACKAGES+=(unrar-free) && \
|
|
||||||
# Install packages
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
||||||
${KEPT_PACKAGES[@]} \
|
|
||||||
${TEMP_PACKAGES[@]} \
|
|
||||||
&& \
|
|
||||||
# Install required python modules
|
|
||||||
python -m pip install --upgrade pip && \
|
|
||||||
python -m venv /opt/venv && \
|
|
||||||
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy pymupdf
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
|
||||||
FROM --platform=linux/amd64 python:3.13-slim-bullseye as build-amd64
|
|
||||||
COPY --from=compile-amd64 /opt/venv /opt/venv
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 python:3.13-slim-bullseye as build-arm64
|
|
||||||
COPY --from=compile-arm64 /opt/venv /opt/venv
|
|
||||||
|
|
||||||
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as build-armv7
|
|
||||||
COPY --from=compile-armv7 /opt/venv /opt/venv
|
|
||||||
######################################################################################
|
|
||||||
|
|
||||||
# Select final stage based on TARGETARCH ARG
|
|
||||||
FROM build-${TARGETARCH}${TARGETVARIANT}
|
|
||||||
LABEL com.kcc.name="Kindle Comic Converter base image"
|
|
||||||
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
|
|
||||||
LABEL org.opencontainers.image.description='Kindle Comic Converter base image'
|
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.authors='darodi'
|
|
||||||
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
|
|
||||||
LABEL org.opencontainers.image.vendor='ciromattia'
|
|
||||||
LABEL org.opencontainers.image.licenses='ISC'
|
|
||||||
LABEL org.opencontainers.image.title="Kindle Comic Converter"
|
|
||||||
|
|
||||||
|
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
|
||||||
WORKDIR /app
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
|
|
||||||
apt-get install -y p7zip-full unrar-free && \
|
|
||||||
ln -s /app/kindlegen /bin/kindlegen && \
|
|
||||||
echo docker-base-20241116 > /IMAGE_VERSION
|
|
||||||
|
|
||||||
@@ -7,12 +7,19 @@
|
|||||||
[](https://github.com/ciromattia/kcc/releases)
|
[](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.
|
like Kindle, Kobo, ReMarkable, and more.
|
||||||
Pages display in fullscreen without margins,
|
Pages display in fullscreen without margins,
|
||||||
with proper fixed layout support.
|
with proper fixed layout support.
|
||||||
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
|
||||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||||
|
KCC runs on Windows, macOS, and Linux.
|
||||||
|
|
||||||
|
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!
|
**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
|
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
||||||
@@ -26,7 +33,7 @@ which have different requirements than normal LCD screens.
|
|||||||
Combining that with downscaling to your specific device's screen resolution
|
Combining that with downscaling to your specific device's screen resolution
|
||||||
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
|
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
|
||||||
This can also improve battery life, page turn speed, and general performance
|
This can also improve battery life, page turn speed, and general performance
|
||||||
on underpowered ereaders with small storage capacities.
|
on underpowered ereaders with small memory and storage capacities.
|
||||||
|
|
||||||
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
|
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
|
||||||
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
|
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
|
||||||
@@ -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
|
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
|
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
|
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:
|
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.
|
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.
|
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
|
### 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.
|
**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,30 +102,31 @@ Click on **Assets** of the latest release.
|
|||||||
You probably want either
|
You probably want either
|
||||||
- `KCC_*.*.*.exe` (Windows)
|
- `KCC_*.*.*.exe` (Windows)
|
||||||
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
|
- `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.
|
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.
|
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
|
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
- Should I use Calibre?
|
- Should I use Calibre?
|
||||||
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre will break the formatting.
|
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
|
||||||
|
Additionally, it will break page numbers.
|
||||||
Viewing KCC output in Calibre will also not work properly.
|
Viewing KCC output in Calibre will also not work properly.
|
||||||
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
|
Direct USB dropping is reccomended.
|
||||||
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
|
|
||||||
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
|
|
||||||
- Blank pages?
|
- Blank pages?
|
||||||
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
|
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. You can try PDF output.
|
||||||
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
||||||
- What output format should I use?
|
- What output format should I use?
|
||||||
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
|
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
|
||||||
- All options have additional information in tooltips if you hover over the option.
|
- All options have additional information in tooltips if you hover over the option.
|
||||||
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
||||||
|
- Kindle panel view not working?
|
||||||
|
- Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
|
||||||
- Right to left mode not working?
|
- Right to left mode not working?
|
||||||
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
|
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
|
||||||
- Colors inverted?
|
- Colors inverted?
|
||||||
@@ -125,9 +136,6 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
|||||||
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
||||||
- How to make AZW3 instead of MOBI?
|
- How to make AZW3 instead of MOBI?
|
||||||
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
|
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
|
||||||
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
|
|
||||||
- Image too dark?
|
|
||||||
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
|
|
||||||
- Huge margins / slow page turns?
|
- Huge margins / slow page turns?
|
||||||
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
||||||
|
|
||||||
@@ -175,38 +183,47 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
|||||||
### Profiles:
|
### Profiles:
|
||||||
|
|
||||||
```
|
```
|
||||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
|
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
|
||||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||||
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
|
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
|
||||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
|
||||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
|
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
|
||||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
|
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
|
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
|
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
|
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
|
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
|
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
|
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
|
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
|
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
|
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
|
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
|
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
|
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
|
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
'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:
|
### Standalone `kcc-c2e.py` usage:
|
||||||
@@ -230,6 +247,8 @@ MAIN:
|
|||||||
|
|
||||||
PROCESSING:
|
PROCESSING:
|
||||||
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||||
|
--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
|
-u, --upscale Resize images smaller than device's resolution
|
||||||
-s, --stretch Stretch images to device's resolution
|
-s, --stretch Stretch images to device's resolution
|
||||||
-r SPLITTER, --splitter SPLITTER
|
-r SPLITTER, --splitter SPLITTER
|
||||||
@@ -250,11 +269,19 @@ PROCESSING:
|
|||||||
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
|
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
|
||||||
--blackborders Disable autodetection and force black borders
|
--blackborders Disable autodetection and force black borders
|
||||||
--whiteborders Disable autodetection and force white borders
|
--whiteborders Disable autodetection and force white borders
|
||||||
|
--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
|
--forcecolor Don't convert images to grayscale
|
||||||
--forcepng Create PNG files instead JPEG
|
--forcepng Create PNG files instead JPEG for black and white images
|
||||||
|
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
|
||||||
|
--force-png-rgb Force color images to be saved as PNG
|
||||||
|
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
|
||||||
|
--noquantize Don't quantize PNG images to 16 colors
|
||||||
--mozjpeg Create JPEG files using mozJpeg
|
--mozjpeg Create JPEG files using mozJpeg
|
||||||
|
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
|
||||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||||
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
||||||
|
--tempdir Create temporary files directory on source file drive.
|
||||||
|
|
||||||
OUTPUT SETTINGS:
|
OUTPUT SETTINGS:
|
||||||
-o OUTPUT, --output OUTPUT
|
-o OUTPUT, --output OUTPUT
|
||||||
@@ -270,8 +297,11 @@ OUTPUT SETTINGS:
|
|||||||
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
-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]
|
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||||
|
--onepagelandscape Show a single centered page in landscape
|
||||||
--norotate Do not rotate double page spreads in spread splitter option.
|
--norotate Do not rotate double page spreads in spread splitter option.
|
||||||
|
--rotateright Rotate double page spreads in opposite direction.
|
||||||
--rotatefirst Put rotated spread first in spread splitter option.
|
--rotatefirst Put rotated spread first in spread splitter option.
|
||||||
|
--filefusion Combines all input files into a single file.
|
||||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||||
|
|
||||||
CUSTOM PROFILE:
|
CUSTOM PROFILE:
|
||||||
@@ -315,6 +345,7 @@ Depending on your system [Python](https://www.python.org) may be called either `
|
|||||||
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
|
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
|
||||||
|
|
||||||
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
|
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
|
||||||
|
If new objects have been added, verify that correct tab order has been applied by using [Tab Order Editing Mode](https://doc.qt.io/qt-6/designer-tab-order.html).
|
||||||
Then use the `gen_ui_files` scripts to autogenerate the python UI.
|
Then use the `gen_ui_files` scripts to autogenerate the python UI.
|
||||||
|
|
||||||
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
||||||
@@ -334,6 +365,7 @@ One time setup and running for the first time:
|
|||||||
```
|
```
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
venv\Scripts\activate.bat
|
venv\Scripts\activate.bat
|
||||||
|
pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python kcc.py
|
python kcc.py
|
||||||
```
|
```
|
||||||
@@ -359,6 +391,7 @@ One time setup and running for the first time:
|
|||||||
```
|
```
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python kcc.py
|
python kcc.py
|
||||||
```
|
```
|
||||||
@@ -412,7 +445,6 @@ Older links (dead):
|
|||||||
## PRIVACY
|
## PRIVACY
|
||||||
**KCC** is initiating internet connections in two cases:
|
**KCC** is initiating internet connections in two cases:
|
||||||
* During startup - Version check and announcement check.
|
* During startup - Version check and announcement check.
|
||||||
* When error occurs - Automatic reporting on Windows and macOS.
|
|
||||||
|
|
||||||
## KNOWN ISSUES
|
## KNOWN ISSUES
|
||||||
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MODE=${KCC_MODE:-c2e}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
"c2e")
|
||||||
|
echo "Starting C2E..."
|
||||||
|
exec c2e "$@"
|
||||||
|
;;
|
||||||
|
|
||||||
|
"c2p")
|
||||||
|
echo "Starting C2P..."
|
||||||
|
exec c2p "$@"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Error: Unknown mode '$MODE'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -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
|
|
||||||
+796
-518
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,19 @@
|
|||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QLineEdit" name="volumeLine"/>
|
<widget class="QLineEdit" name="volumeLine"/>
|
||||||
</item>
|
</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">
|
<item row="3" column="0">
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@@ -192,6 +205,19 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<tabstops>
|
||||||
|
<tabstop>seriesLine</tabstop>
|
||||||
|
<tabstop>volumeLine</tabstop>
|
||||||
|
<tabstop>bulkVolumeCheck</tabstop>
|
||||||
|
<tabstop>titleLine</tabstop>
|
||||||
|
<tabstop>numberLine</tabstop>
|
||||||
|
<tabstop>writerLine</tabstop>
|
||||||
|
<tabstop>pencillerLine</tabstop>
|
||||||
|
<tabstop>inkerLine</tabstop>
|
||||||
|
<tabstop>coloristLine</tabstop>
|
||||||
|
<tabstop>okButton</tabstop>
|
||||||
|
<tabstop>cancelButton</tabstop>
|
||||||
|
</tabstops>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="KCC.qrc"/>
|
<include location="KCC.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+463
-87
@@ -22,7 +22,7 @@ import itertools
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
|
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
|
||||||
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
|
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, QAbstractItemView, QListView, QTreeView)
|
||||||
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -38,11 +38,10 @@ from xml.sax.saxutils import escape
|
|||||||
from psutil import Process
|
from psutil import Process
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
from raven import Client
|
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
|
||||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
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 __version__
|
||||||
from . import comic2ebook
|
from . import comic2ebook
|
||||||
from . import metadata
|
from . import metadata
|
||||||
@@ -195,7 +194,7 @@ class VersionThread(QThread):
|
|||||||
icon = 'bindle'
|
icon = 'bindle'
|
||||||
if category == 'kofi':
|
if category == 'kofi':
|
||||||
icon = 'kofi'
|
icon = 'kofi'
|
||||||
message = f"<b>{payload.get('name')}</b>"
|
message = f"{payload.get('name')}"
|
||||||
if payload.get('link'):
|
if payload.get('link'):
|
||||||
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
|
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
|
||||||
if payload.get('showDeadline'):
|
if payload.get('showDeadline'):
|
||||||
@@ -327,26 +326,50 @@ class WorkerThread(QThread):
|
|||||||
options.maximizestrips = True
|
options.maximizestrips = True
|
||||||
if GUI.disableProcessingBox.isChecked():
|
if GUI.disableProcessingBox.isChecked():
|
||||||
options.noprocessing = True
|
options.noprocessing = True
|
||||||
|
if GUI.legacyExtractBox.isChecked():
|
||||||
|
options.legacyextract = True
|
||||||
|
if GUI.pdfWidthBox.isChecked():
|
||||||
|
options.pdfwidth = True
|
||||||
|
if GUI.smartCoverCropBox.isChecked():
|
||||||
|
options.smartcovercrop = True
|
||||||
|
if GUI.coverFillBox.isChecked():
|
||||||
|
options.coverfill = True
|
||||||
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.metadatatitle = 1
|
options.metadatatitle = 1
|
||||||
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
|
||||||
options.metadatatitle = 2
|
options.metadatatitle = 2
|
||||||
if GUI.deleteBox.isChecked():
|
if GUI.deleteBox.isChecked():
|
||||||
options.delete = True
|
options.delete = True
|
||||||
|
if GUI.tempDirBox.isChecked():
|
||||||
|
options.tempdir = True
|
||||||
if GUI.spreadShiftBox.isChecked():
|
if GUI.spreadShiftBox.isChecked():
|
||||||
options.spreadshift = True
|
options.spreadshift = True
|
||||||
|
if GUI.onePageLandscapeBox.isChecked():
|
||||||
|
options.onepagelandscape = True
|
||||||
if GUI.fileFusionBox.isChecked():
|
if GUI.fileFusionBox.isChecked():
|
||||||
options.filefusion = True
|
options.filefusion = True
|
||||||
else:
|
else:
|
||||||
options.filefusion = False
|
options.filefusion = False
|
||||||
if GUI.noRotateBox.isChecked():
|
if GUI.noRotateBox.isChecked():
|
||||||
options.norotate = True
|
options.norotate = True
|
||||||
|
if GUI.rotateRightBox.isChecked():
|
||||||
|
options.rotateright = True
|
||||||
if GUI.rotateFirstBox.isChecked():
|
if GUI.rotateFirstBox.isChecked():
|
||||||
options.rotatefirst = True
|
options.rotatefirst = True
|
||||||
|
if GUI.forcePngRgbBox.isChecked():
|
||||||
|
options.force_png_rgb = True
|
||||||
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.forcepng = True
|
options.forcepng = True
|
||||||
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
||||||
options.mozjpeg = True
|
options.mozjpeg = True
|
||||||
|
if GUI.webpBox.isChecked():
|
||||||
|
options.webp = True
|
||||||
|
if GUI.pngLegacyBox.isChecked():
|
||||||
|
options.pnglegacy = True
|
||||||
|
if GUI.noQuantizeBox.isChecked():
|
||||||
|
options.noquantize = True
|
||||||
|
if GUI.jpegQualityBox.isChecked():
|
||||||
|
options.jpegquality = GUI.jpegQualitySpinBox.value()
|
||||||
if GUI.currentMode > 2:
|
if GUI.currentMode > 2:
|
||||||
options.customwidth = str(GUI.widthBox.value())
|
options.customwidth = str(GUI.widthBox.value())
|
||||||
options.customheight = str(GUI.heightBox.value())
|
options.customheight = str(GUI.heightBox.value())
|
||||||
@@ -370,7 +393,10 @@ class WorkerThread(QThread):
|
|||||||
for job in currentJobs:
|
for job in currentJobs:
|
||||||
bookDir.append(job)
|
bookDir.append(job)
|
||||||
try:
|
try:
|
||||||
|
fusion_source_parent = str(Path(bookDir[0]).parent)
|
||||||
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
||||||
|
if options.output is None:
|
||||||
|
options.output = fusion_source_parent
|
||||||
currentJobs.clear()
|
currentJobs.clear()
|
||||||
currentJobs.append(comic2ebook.makeFusion(bookDir))
|
currentJobs.append(comic2ebook.makeFusion(bookDir))
|
||||||
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
|
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
|
||||||
@@ -382,13 +408,14 @@ class WorkerThread(QThread):
|
|||||||
error_message = 'Process Failed. Custom title can\'t be set when processing more than 1 source.\nDid you forget to check fusion?'
|
error_message = 'Process Failed. Custom title can\'t be set when processing more than 1 source.\nDid you forget to check fusion?'
|
||||||
print(error_message)
|
print(error_message)
|
||||||
MW.addMessage.emit(error_message, 'error', True)
|
MW.addMessage.emit(error_message, 'error', True)
|
||||||
for job in currentJobs:
|
for i, job in enumerate(currentJobs, start=1):
|
||||||
|
job_progress_number = f'[{i}/{len(currentJobs)}] '
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
if not self.conversionAlive:
|
if not self.conversionAlive:
|
||||||
self.clean()
|
self.clean()
|
||||||
return
|
return
|
||||||
self.errors = False
|
self.errors = False
|
||||||
MW.addMessage.emit('<b>Source:</b> ' + job, 'info', False)
|
MW.addMessage.emit(f'<b>{job_progress_number}Source:</b> ' + job, 'info', False)
|
||||||
if gui_current_format == 'CBZ':
|
if gui_current_format == 'CBZ':
|
||||||
MW.addMessage.emit('Creating CBZ files', 'info', False)
|
MW.addMessage.emit('Creating CBZ files', 'info', False)
|
||||||
GUI.progress.content = 'Creating CBZ files'
|
GUI.progress.content = 'Creating CBZ files'
|
||||||
@@ -402,7 +429,7 @@ class WorkerThread(QThread):
|
|||||||
jobargv.append(job)
|
jobargv.append(job)
|
||||||
try:
|
try:
|
||||||
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
||||||
outputPath = comic2ebook.makeBook(job, self)
|
outputPath = comic2ebook.makeBook(job, self, job_progress_number)
|
||||||
MW.hideProgressBar.emit()
|
MW.hideProgressBar.emit()
|
||||||
except UserWarning as warn:
|
except UserWarning as warn:
|
||||||
if not self.conversionAlive:
|
if not self.conversionAlive:
|
||||||
@@ -422,8 +449,6 @@ class WorkerThread(QThread):
|
|||||||
_, _, traceback = sys.exc_info()
|
_, _, traceback = sys.exc_info()
|
||||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||||
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
||||||
if ' is corrupted.' not in str(err):
|
|
||||||
GUI.sentry.captureException()
|
|
||||||
MW.addMessage.emit('Error during conversion! Please consult '
|
MW.addMessage.emit('Error during conversion! Please consult '
|
||||||
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
|
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
|
||||||
'for more details.', 'error', False)
|
'for more details.', 'error', False)
|
||||||
@@ -444,7 +469,7 @@ class WorkerThread(QThread):
|
|||||||
else:
|
else:
|
||||||
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
|
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
|
||||||
if 'MOBI' in gui_current_format:
|
if 'MOBI' in gui_current_format:
|
||||||
MW.progressBarTick.emit('Creating MOBI files')
|
MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
|
||||||
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
|
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
|
||||||
MW.progressBarTick.emit('tick')
|
MW.progressBarTick.emit('tick')
|
||||||
MW.addMessage.emit('Creating MOBI files', 'info', False)
|
MW.addMessage.emit('Creating MOBI files', 'info', False)
|
||||||
@@ -518,11 +543,12 @@ class WorkerThread(QThread):
|
|||||||
if os.path.exists(item.replace('.epub', '.mobi')):
|
if os.path.exists(item.replace('.epub', '.mobi')):
|
||||||
os.remove(item.replace('.epub', '.mobi'))
|
os.remove(item.replace('.epub', '.mobi'))
|
||||||
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
|
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')
|
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
|
||||||
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
|
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
|
||||||
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
|
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
|
||||||
if self.kindlegenErrorCode[0] == 23026:
|
if self.kindlegenErrorCode[0] == 23026:
|
||||||
MW.addMessage.emit('Created EPUB file was too big.', 'error', False)
|
MW.addMessage.emit('Created EPUB file was too big. Weird file structure?', 'error', False)
|
||||||
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
|
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
|
||||||
False)
|
False)
|
||||||
if self.kindlegenErrorCode[0] == 3221226505:
|
if self.kindlegenErrorCode[0] == 3221226505:
|
||||||
@@ -534,12 +560,6 @@ class WorkerThread(QThread):
|
|||||||
move(item, GUI.targetDirectory)
|
move(item, GUI.targetDirectory)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if options.filefusion:
|
|
||||||
for path in currentJobs:
|
|
||||||
if os.path.isfile(path):
|
|
||||||
os.remove(path)
|
|
||||||
elif os.path.isdir(path):
|
|
||||||
rmtree(path, True)
|
|
||||||
GUI.progress.content = ''
|
GUI.progress.content = ''
|
||||||
GUI.progress.stop()
|
GUI.progress.stop()
|
||||||
MW.hideProgressBar.emit()
|
MW.hideProgressBar.emit()
|
||||||
@@ -603,43 +623,78 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.jobList.clear()
|
GUI.jobList.clear()
|
||||||
if self.tar or self.sevenzip:
|
if self.tar or self.sevenzip:
|
||||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
|
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
|
||||||
else:
|
else:
|
||||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||||
'Comic (*.pdf);;All (*.*)')
|
'Comic (*.pdf);;All (*.*)')
|
||||||
for fname in fnames[0]:
|
for fname in fnames[0]:
|
||||||
if fname != '':
|
if fname != '':
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
fname = fname.replace('/', '\\')
|
|
||||||
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
|
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
|
||||||
GUI.jobList.addItem(fname)
|
GUI.jobList.addItem(fname)
|
||||||
GUI.jobList.scrollToBottom()
|
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):
|
def selectFileMetaEditor(self, sname):
|
||||||
|
files = []
|
||||||
if not sname:
|
if not sname:
|
||||||
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
||||||
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
# Multi-directory selection for bulk editing ComicInfo.xml
|
||||||
if dname != '':
|
dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
|
||||||
sname = os.path.join(dname, 'ComicInfo.xml')
|
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
||||||
self.lastPath = os.path.dirname(sname)
|
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
||||||
|
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||||||
|
|
||||||
|
# Enable multi-selection in the dialog (may not work with native dialog on all platforms)
|
||||||
|
file_view = dialog.findChild(QListView, 'listView')
|
||||||
|
if file_view:
|
||||||
|
file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
file_tree = dialog.findChild(QTreeView)
|
||||||
|
if file_tree:
|
||||||
|
file_tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
|
|
||||||
|
if dialog.exec():
|
||||||
|
selected_dirs = dialog.selectedFiles()
|
||||||
|
if selected_dirs:
|
||||||
|
files = [os.path.join(d, 'ComicInfo.xml') for d in selected_dirs]
|
||||||
|
self.lastPath = os.path.dirname(selected_dirs[0])
|
||||||
else:
|
else:
|
||||||
if self.sevenzip:
|
if self.sevenzip:
|
||||||
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
|
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
|
||||||
'Comic (*.cbz *.cbr *.cb7)')
|
'Comic (*.cbz *.cbr *.cb7)')
|
||||||
|
files = fnames[0]
|
||||||
|
if files:
|
||||||
|
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
|
||||||
else:
|
else:
|
||||||
fname = ['']
|
|
||||||
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
|
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
|
||||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||||
' to enable metadata editing.', 'warning')
|
' to enable metadata editing.', 'warning')
|
||||||
if fname[0] != '':
|
else:
|
||||||
sname = fname[0]
|
files = [sname]
|
||||||
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
|
|
||||||
if sname:
|
if files:
|
||||||
try:
|
try:
|
||||||
self.editor.loadData(sname)
|
self.editor.loadData(files)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_, _, traceback = sys.exc_info()
|
_, _, traceback = sys.exc_info()
|
||||||
GUI.sentry.captureException()
|
|
||||||
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
|
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
|
||||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||||
else:
|
else:
|
||||||
@@ -656,6 +711,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
# noinspection PyCallByClass
|
# noinspection PyCallByClass
|
||||||
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
|
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
|
||||||
|
|
||||||
|
def openHumble(self):
|
||||||
|
# noinspection PyCallByClass
|
||||||
|
QDesktopServices.openUrl(QUrl('https://humblebundleinc.sjv.io/3JaR3A'))
|
||||||
|
|
||||||
|
def openYouTube(self):
|
||||||
|
# noinspection PyCallByClass
|
||||||
|
QDesktopServices.openUrl(QUrl('https://www.youtube.com/@eink-dude'))
|
||||||
|
|
||||||
|
def openDiscord(self):
|
||||||
|
# noinspection PyCallByClass
|
||||||
|
QDesktopServices.openUrl(QUrl('https://discord.gg/um5JRKwmGT'))
|
||||||
|
|
||||||
def modeChange(self, mode):
|
def modeChange(self, mode):
|
||||||
if mode == 1:
|
if mode == 1:
|
||||||
self.currentMode = 1
|
self.currentMode = 1
|
||||||
@@ -723,6 +790,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.croppingWidget.setVisible(False)
|
GUI.croppingWidget.setVisible(False)
|
||||||
self.changeCroppingPower(100) # 1.0
|
self.changeCroppingPower(100) # 1.0
|
||||||
|
|
||||||
|
def togglejpegqualityBox(self, value):
|
||||||
|
if value:
|
||||||
|
GUI.jpegQualityWidget.setVisible(True)
|
||||||
|
else:
|
||||||
|
GUI.jpegQualityWidget.setVisible(False)
|
||||||
|
|
||||||
def togglewebtoonBox(self, value):
|
def togglewebtoonBox(self, value):
|
||||||
if value:
|
if value:
|
||||||
self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
|
self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
|
||||||
@@ -735,8 +808,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.rotateBox.setChecked(False)
|
GUI.rotateBox.setChecked(False)
|
||||||
GUI.borderBox.setEnabled(False)
|
GUI.borderBox.setEnabled(False)
|
||||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
GUI.upscaleBox.setEnabled(False)
|
# GUI.upscaleBox.setEnabled(False)
|
||||||
GUI.upscaleBox.setChecked(False)
|
# GUI.upscaleBox.setChecked(False)
|
||||||
GUI.croppingBox.setEnabled(False)
|
GUI.croppingBox.setEnabled(False)
|
||||||
GUI.croppingBox.setChecked(False)
|
GUI.croppingBox.setChecked(False)
|
||||||
GUI.interPanelCropBox.setEnabled(False)
|
GUI.interPanelCropBox.setEnabled(False)
|
||||||
@@ -753,7 +826,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.rotateBox.setEnabled(True)
|
GUI.rotateBox.setEnabled(True)
|
||||||
GUI.borderBox.setEnabled(True)
|
GUI.borderBox.setEnabled(True)
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
if profile['Label'] != 'KS':
|
if not profile['Label'].startswith('KS') or True:
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
GUI.croppingBox.setEnabled(True)
|
GUI.croppingBox.setEnabled(True)
|
||||||
GUI.interPanelCropBox.setEnabled(True)
|
GUI.interPanelCropBox.setEnabled(True)
|
||||||
@@ -778,12 +851,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
def toggleImageFormatBox(self, value):
|
def toggleImageFormatBox(self, value):
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
if value == 1:
|
if value == 1:
|
||||||
if profile['Label'] == 'KS':
|
if profile['Label'].startswith('KS'):
|
||||||
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||||
for bad_format in ('MOBI', 'EPUB'):
|
for bad_format in ('MOBI', 'EPUB'):
|
||||||
if bad_format in current_format:
|
if bad_format in current_format:
|
||||||
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
|
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
|
||||||
break
|
break
|
||||||
|
GUI.pngLegacyBox.setEnabled(True)
|
||||||
|
GUI.noQuantizeBox.setEnabled(True)
|
||||||
|
GUI.forcePngRgbBox.setEnabled(True)
|
||||||
|
else:
|
||||||
|
GUI.pngLegacyBox.setEnabled(False)
|
||||||
|
GUI.noQuantizeBox.setEnabled(False)
|
||||||
|
GUI.forcePngRgbBox.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
def togglechunkSizeCheckBox(self, value):
|
def togglechunkSizeCheckBox(self, value):
|
||||||
GUI.chunkSizeWidget.setVisible(value)
|
GUI.chunkSizeWidget.setVisible(value)
|
||||||
@@ -840,10 +921,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
if not GUI.webtoonBox.isChecked():
|
if not GUI.webtoonBox.isChecked():
|
||||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||||
if profile['Label'] == 'KS':
|
if profile['Label'].startswith('KS') and False:
|
||||||
GUI.upscaleBox.setDisabled(True)
|
GUI.upscaleBox.setDisabled(True)
|
||||||
else:
|
else:
|
||||||
if not GUI.webtoonBox.isChecked():
|
if not GUI.webtoonBox.isChecked() or True:
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
if profile['Label'] == 'KCS':
|
if profile['Label'] == 'KCS':
|
||||||
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||||
@@ -851,6 +932,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
if bad_format in current_format:
|
if bad_format in current_format:
|
||||||
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
|
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
|
||||||
break
|
break
|
||||||
|
elif profile['Label'] == 'KDX':
|
||||||
|
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
GUI.pngLegacyBox.setChecked(True)
|
||||||
if not profile['PVOptions']:
|
if not profile['PVOptions']:
|
||||||
GUI.qualityBox.setChecked(False)
|
GUI.qualityBox.setChecked(False)
|
||||||
if str(GUI.deviceBox.currentText()) == 'Other':
|
if str(GUI.deviceBox.currentText()) == 'Other':
|
||||||
@@ -874,8 +959,16 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
|
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
|
||||||
GUI.chunkSizeCheckBox.setEnabled(False)
|
GUI.chunkSizeCheckBox.setEnabled(False)
|
||||||
GUI.chunkSizeCheckBox.setChecked(False)
|
GUI.chunkSizeCheckBox.setChecked(False)
|
||||||
|
elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
|
||||||
|
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
GUI.upscaleBox.setChecked(True)
|
||||||
elif not GUI.webtoonBox.isChecked():
|
elif not GUI.webtoonBox.isChecked():
|
||||||
GUI.chunkSizeCheckBox.setEnabled(True)
|
GUI.chunkSizeCheckBox.setEnabled(True)
|
||||||
|
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
|
||||||
|
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||||
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
else:
|
||||||
|
GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
|
||||||
|
|
||||||
def stripTags(self, html):
|
def stripTags(self, html):
|
||||||
s = HTMLStripper()
|
s = HTMLStripper()
|
||||||
@@ -1003,15 +1096,28 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'colorBox': GUI.colorBox.checkState(),
|
'colorBox': GUI.colorBox.checkState(),
|
||||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||||
|
'legacyExtractBox': GUI.legacyExtractBox.checkState(),
|
||||||
|
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||||
|
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
||||||
|
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||||
|
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
|
||||||
|
'webpBox': GUI.webpBox.checkState(),
|
||||||
|
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
|
||||||
|
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
|
||||||
|
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
|
||||||
|
'jpegQuality': GUI.jpegQualitySpinBox.value(),
|
||||||
'widthBox': GUI.widthBox.value(),
|
'widthBox': GUI.widthBox.value(),
|
||||||
'heightBox': GUI.heightBox.value(),
|
'heightBox': GUI.heightBox.value(),
|
||||||
'deleteBox': GUI.deleteBox.checkState(),
|
'deleteBox': GUI.deleteBox.checkState(),
|
||||||
|
'tempDirBox': GUI.tempDirBox.checkState(),
|
||||||
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
||||||
|
'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
|
||||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||||
|
'rotateRightBox': GUI.rotateRightBox.checkState(),
|
||||||
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
||||||
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
||||||
'gammaSlider': float(self.gammaValue) * 100,
|
'gammaSlider': float(self.gammaValue) * 100,
|
||||||
@@ -1025,13 +1131,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
MW.activateWindow()
|
MW.activateWindow()
|
||||||
if type(message) is bytes:
|
if type(message) is bytes:
|
||||||
message = message.decode('UTF-8')
|
message = message.decode('UTF-8')
|
||||||
if not self.conversionAlive and message != 'ARISE':
|
if not self.conversionAlive and message != 'ARISE' and not GUI.jobList.findItems(message, Qt.MatchFlag.MatchExactly):
|
||||||
if self.needClean:
|
if self.needClean:
|
||||||
self.needClean = False
|
self.needClean = False
|
||||||
GUI.jobList.clear()
|
GUI.jobList.clear()
|
||||||
formats = ['.pdf']
|
formats = ['.pdf']
|
||||||
if self.tar or self.sevenzip:
|
if self.tar or self.sevenzip:
|
||||||
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
|
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
|
||||||
if os.path.isdir(message):
|
if os.path.isdir(message):
|
||||||
GUI.jobList.addItem(message)
|
GUI.jobList.addItem(message)
|
||||||
GUI.jobList.scrollToBottom()
|
GUI.jobList.scrollToBottom()
|
||||||
@@ -1056,6 +1162,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
if message[-1] == '/':
|
if message[-1] == '/':
|
||||||
message = message[:-1]
|
message = message[:-1]
|
||||||
self.handleMessage(message)
|
self.handleMessage(message)
|
||||||
|
# sorting may conflict with manual file fusion order
|
||||||
|
# GUI.jobList.sortItems()
|
||||||
|
|
||||||
def forceShutdown(self):
|
def forceShutdown(self):
|
||||||
self.saveSettings(None)
|
self.saveSettings(None)
|
||||||
@@ -1116,7 +1224,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.croppingPowerValue = 1.0
|
self.croppingPowerValue = 1.0
|
||||||
self.currentMode = 1
|
self.currentMode = 1
|
||||||
self.targetDirectory = ''
|
self.targetDirectory = ''
|
||||||
self.sentry = Client(release=__version__)
|
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from psutil import BELOW_NORMAL_PRIORITY_CLASS
|
from psutil import BELOW_NORMAL_PRIORITY_CLASS
|
||||||
@@ -1132,7 +1239,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'convertButton', 'formatBox']:
|
'convertButton', 'formatBox']:
|
||||||
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
||||||
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
||||||
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
|
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'gridLayout_6', 'horizontalLayout_2']:
|
||||||
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
|
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
|
||||||
if self.windowSize == '0x0':
|
if self.windowSize == '0x0':
|
||||||
MW.resize(500, 500)
|
MW.resize(500, 500)
|
||||||
@@ -1142,7 +1249,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
||||||
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
||||||
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
||||||
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
||||||
|
"KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
|
||||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||||
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
|
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
|
||||||
@@ -1158,9 +1266,27 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
||||||
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
'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 1324x1986": {
|
||||||
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1324',
|
||||||
|
},
|
||||||
|
"Kindle Scribe 1/2": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
||||||
},
|
},
|
||||||
|
"Kindle Scribe 3": {
|
||||||
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
|
||||||
|
},
|
||||||
|
"Kindle Scribe Colorsoft": {
|
||||||
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
|
||||||
|
},
|
||||||
"Kindle 11": {
|
"Kindle 11": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
|
||||||
},
|
},
|
||||||
@@ -1168,7 +1294,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
|
||||||
},
|
},
|
||||||
"Kindle Paperwhite 12": {
|
"Kindle Paperwhite 12": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW6',
|
||||||
},
|
},
|
||||||
"Kindle Colorsoft": {
|
"Kindle Colorsoft": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
|
||||||
@@ -1235,9 +1361,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'Label': 'OTHER'},
|
'Label': 'OTHER'},
|
||||||
}
|
}
|
||||||
profilesGUI = [
|
profilesGUI = [
|
||||||
|
"Kindle Scribe Colorsoft",
|
||||||
|
"Kindle Scribe 3",
|
||||||
"Kindle Colorsoft",
|
"Kindle Colorsoft",
|
||||||
"Kindle Paperwhite 12",
|
"Kindle Paperwhite 12",
|
||||||
"Kindle Scribe",
|
"Kindle Scribe 1/2",
|
||||||
"Kindle Paperwhite 11",
|
"Kindle Paperwhite 11",
|
||||||
"Kindle 11",
|
"Kindle 11",
|
||||||
"Kindle Oasis 9/10",
|
"Kindle Oasis 9/10",
|
||||||
@@ -1257,6 +1385,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"Separator",
|
"Separator",
|
||||||
"Other",
|
"Other",
|
||||||
"Separator",
|
"Separator",
|
||||||
|
"Kindle 1324x1986",
|
||||||
|
"Kindle 1920x1920",
|
||||||
|
"Kindle 1860x1920",
|
||||||
|
"Kindle 1240x1860",
|
||||||
"Kindle 8/10",
|
"Kindle 8/10",
|
||||||
"Kindle Oasis 8",
|
"Kindle Oasis 8",
|
||||||
"Kindle Paperwhite 7/10",
|
"Kindle Paperwhite 7/10",
|
||||||
@@ -1304,7 +1436,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
||||||
'info')
|
'info')
|
||||||
|
|
||||||
self.tar = 'tar' in available_archive_tools()
|
self.tar = TAR in available_archive_tools()
|
||||||
self.sevenzip = SEVENZIP in available_archive_tools()
|
self.sevenzip = SEVENZIP in available_archive_tools()
|
||||||
if not any([self.tar, self.sevenzip]):
|
if not any([self.tar, self.sevenzip]):
|
||||||
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
|
||||||
@@ -1315,14 +1447,19 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
|
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
|
||||||
GUI.clearButton.clicked.connect(self.clearJobs)
|
GUI.clearButton.clicked.connect(self.clearJobs)
|
||||||
GUI.fileButton.clicked.connect(self.selectFile)
|
GUI.fileButton.clicked.connect(self.selectFile)
|
||||||
|
GUI.directoryButton.clicked.connect(self.selectDir)
|
||||||
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
||||||
GUI.wikiButton.clicked.connect(self.openWiki)
|
GUI.wikiButton.clicked.connect(self.openWiki)
|
||||||
GUI.kofiButton.clicked.connect(self.openKofi)
|
GUI.kofiButton.clicked.connect(self.openKofi)
|
||||||
|
GUI.humbleButton.clicked.connect(self.openHumble)
|
||||||
|
GUI.youtubeButton.clicked.connect(self.openYouTube)
|
||||||
|
GUI.discordButton.clicked.connect(self.openDiscord)
|
||||||
GUI.convertButton.clicked.connect(self.convertStart)
|
GUI.convertButton.clicked.connect(self.convertStart)
|
||||||
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
||||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||||
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
|
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
|
||||||
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
|
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
|
||||||
|
GUI.jpegQualityBox.stateChanged.connect(self.togglejpegqualityBox)
|
||||||
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
|
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
|
||||||
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
|
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
|
||||||
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
|
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
|
||||||
@@ -1384,6 +1521,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.croppingPowerSlider.setValue(int(self.options[option]))
|
GUI.croppingPowerSlider.setValue(int(self.options[option]))
|
||||||
self.changeCroppingPower(int(self.options[option]))
|
self.changeCroppingPower(int(self.options[option]))
|
||||||
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
|
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
|
||||||
|
elif str(option) == "jpegQuality":
|
||||||
|
GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
|
||||||
elif str(option) == "chunkSizeBox":
|
elif str(option) == "chunkSizeBox":
|
||||||
GUI.chunkSizeBox.setValue(int(self.options[option]))
|
GUI.chunkSizeBox.setValue(int(self.options[option]))
|
||||||
else:
|
else:
|
||||||
@@ -1411,62 +1550,299 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
|
|
||||||
|
|
||||||
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
||||||
def loadData(self, file):
|
def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
|
||||||
self.parser = metadata.MetadataParser(file)
|
note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
|
||||||
if self.parser.format in ['RAR', 'RAR5']:
|
|
||||||
self.editorWidget.setEnabled(False)
|
if len(valuesByFile) <= 20:
|
||||||
self.okButton.setEnabled(False)
|
rows = ''.join(
|
||||||
self.statusLabel.setText('CBR metadata are read-only.')
|
'<tr>'
|
||||||
|
f'<td style="padding:2px 6px; white-space:nowrap;">{escape(os.path.basename(f))}</td>'
|
||||||
|
f'<td style="padding:2px 6px;">{escape(v)}</td>'
|
||||||
|
'</tr>'
|
||||||
|
for f, v in valuesByFile
|
||||||
|
)
|
||||||
|
|
||||||
|
table = (
|
||||||
|
'<table border="1" cellspacing="0" cellpadding="0">'
|
||||||
|
'<tr>'
|
||||||
|
'<th style="padding:2px 6px; text-align:left;">File</th>'
|
||||||
|
'<th style="padding:2px 6px; text-align:left;">Value</th>'
|
||||||
|
'</tr>'
|
||||||
|
f'{rows}'
|
||||||
|
'</table>'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
counts = {}
|
||||||
|
for _, v in valuesByFile:
|
||||||
|
counts[v] = counts.get(v, 0) + 1
|
||||||
|
|
||||||
|
rows = ''.join(
|
||||||
|
'<tr>'
|
||||||
|
f'<td style="padding:2px 6px;">{escape(v)}</td>'
|
||||||
|
f'<td style="padding:2px 6px; text-align:right;">{c}</td>'
|
||||||
|
'</tr>'
|
||||||
|
for v, c in sorted(counts.items(), key=lambda t: (-t[1], t[0]))
|
||||||
|
)
|
||||||
|
|
||||||
|
table = (
|
||||||
|
'<table border="1" cellspacing="0" cellpadding="0">'
|
||||||
|
'<tr>'
|
||||||
|
'<th style="padding:2px 6px; text-align:left;">Value</th>'
|
||||||
|
'<th style="padding:2px 6px; text-align:right;">Count</th>'
|
||||||
|
'</tr>'
|
||||||
|
f'{rows}'
|
||||||
|
'</table>'
|
||||||
|
)
|
||||||
|
|
||||||
|
tooltipHTML = f'\
|
||||||
|
<b>{escape(fieldLabel)}</b>\
|
||||||
|
{note}\
|
||||||
|
{table}\
|
||||||
|
'
|
||||||
|
|
||||||
|
return tooltipHTML
|
||||||
|
|
||||||
|
def loadData(self, files):
|
||||||
|
self.files = files if isinstance(files, list) else [files]
|
||||||
|
self.bulkMode = len(self.files) > 1
|
||||||
|
|
||||||
|
# Sort files by name for consistent volume assignment
|
||||||
|
self.files.sort()
|
||||||
|
|
||||||
|
# Unified CBR check for all files (both single and bulk mode)
|
||||||
|
for file in self.files:
|
||||||
|
parser = metadata.MetadataParser(file)
|
||||||
|
if parser.format in ['RAR', 'RAR5']:
|
||||||
|
self.editorWidget.setEnabled(False)
|
||||||
|
self.okButton.setEnabled(False)
|
||||||
|
self.statusLabel.setText('CBR files in selection are read-only.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.bulkMode:
|
||||||
|
firstFile = self.files[0]
|
||||||
|
self.parser = metadata.MetadataParser(firstFile)
|
||||||
|
self.editorWidget.setEnabled(True)
|
||||||
|
self.okButton.setEnabled(True)
|
||||||
|
self.statusLabel.setText(f'Editing {len(self.files)} files.')
|
||||||
|
|
||||||
|
# Show bulk volume checkbox
|
||||||
|
self.bulkVolumeCheck.setVisible(True)
|
||||||
|
self.bulkVolumeCheck.setChecked(False)
|
||||||
|
|
||||||
|
for field in (self.volumeLine, self.numberLine, self.titleLine):
|
||||||
|
field.setEnabled(False)
|
||||||
|
field.setText('')
|
||||||
|
field.setPlaceholderText('(multiple files)')
|
||||||
|
field.setToolTip('')
|
||||||
|
|
||||||
|
# Load metadata for all files and show common values, or “(multiple values)” + tooltip.
|
||||||
|
parsed = []
|
||||||
|
for file in self.files:
|
||||||
|
parsed.append((file, metadata.MetadataParser(file)))
|
||||||
|
|
||||||
|
field_specs = [
|
||||||
|
(self.seriesLine, 'Series', lambda p: (p.data.get('Series', '') or '')),
|
||||||
|
(self.writerLine, 'Writer', lambda p: ', '.join(p.data.get('Writers', []) or [])),
|
||||||
|
(self.pencillerLine, 'Penciller', lambda p: ', '.join(p.data.get('Pencillers', []) or [])),
|
||||||
|
(self.inkerLine, 'Inker', lambda p: ', '.join(p.data.get('Inkers', []) or [])),
|
||||||
|
(self.coloristLine, 'Colorist', lambda p: ', '.join(p.data.get('Colorists', []) or [])),
|
||||||
|
]
|
||||||
|
|
||||||
|
for line, label, extractor in field_specs:
|
||||||
|
line.setEnabled(True)
|
||||||
|
valuesByFile = [(f, extractor(p)) for f, p in parsed]
|
||||||
|
uniqueValues = {v for _, v in valuesByFile}
|
||||||
|
|
||||||
|
if len(uniqueValues) == 1:
|
||||||
|
common_value = valuesByFile[0][1] if valuesByFile else ''
|
||||||
|
line.setPlaceholderText('')
|
||||||
|
line.setToolTip('')
|
||||||
|
line.setText(common_value)
|
||||||
|
else:
|
||||||
|
line.setText('')
|
||||||
|
line.setPlaceholderText('(multiple values)')
|
||||||
|
line.setToolTip(self._buildBulkFieldToolTip(label, valuesByFile))
|
||||||
|
else:
|
||||||
|
file = self.files[0]
|
||||||
|
self.parser = metadata.MetadataParser(file)
|
||||||
|
|
||||||
|
# Hide bulk volume checkbox in single file mode
|
||||||
|
self.bulkVolumeCheck.setVisible(False)
|
||||||
|
|
||||||
|
for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine,
|
||||||
|
self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
field.setEnabled(True)
|
||||||
|
field.setPlaceholderText('')
|
||||||
|
|
||||||
self.editorWidget.setEnabled(True)
|
self.editorWidget.setEnabled(True)
|
||||||
self.okButton.setEnabled(True)
|
self.okButton.setEnabled(True)
|
||||||
self.statusLabel.setText('Separate authors with a comma.')
|
self.statusLabel.setText('Separate authors with a comma.')
|
||||||
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
|
||||||
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
||||||
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
for field in (self.seriesLine, self.titleLine):
|
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
||||||
if field.text() == '':
|
for field in (self.seriesLine, self.titleLine):
|
||||||
path = Path(file)
|
if field.text() == '':
|
||||||
if file.endswith('.xml'):
|
path = Path(file)
|
||||||
field.setText(path.parent.name)
|
if file.endswith('.xml'):
|
||||||
else:
|
field.setText(path.parent.name)
|
||||||
field.setText(path.stem)
|
else:
|
||||||
|
field.setText(path.stem)
|
||||||
|
|
||||||
def saveData(self):
|
def saveData(self):
|
||||||
for field in (self.volumeLine, self.numberLine):
|
if self.bulkMode:
|
||||||
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
bulkData = {}
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
if self.cleanData(self.seriesLine.text()):
|
||||||
else:
|
bulkData['Series'] = self.cleanData(self.seriesLine.text())
|
||||||
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
for field in (self.seriesLine, self.titleLine):
|
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
fieldName = field.objectName().capitalize()[:-4] + 's'
|
||||||
values = self.cleanData(field.text()).split(',')
|
values = self.cleanData(field.text()).split(',')
|
||||||
tmpData = []
|
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
|
||||||
for value in values:
|
if tmpData:
|
||||||
if self.cleanData(value) != '':
|
bulkData[fieldName] = tmpData
|
||||||
tmpData.append(self.cleanData(value))
|
# Handle bulk volume editing
|
||||||
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
volumes = None
|
||||||
try:
|
if self.bulkVolumeCheck.isChecked():
|
||||||
self.parser.saveXML()
|
volumeText = self.volumeLine.text()
|
||||||
except Exception as err:
|
volumes, error = self.parseVolumeInput(volumeText, len(self.files))
|
||||||
_, _, traceback = sys.exc_info()
|
if error:
|
||||||
GUI.sentry.captureException()
|
self.statusLabel.setText(error)
|
||||||
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
return
|
||||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
|
||||||
self.ui.close()
|
if not bulkData and volumes is None:
|
||||||
|
self.statusLabel.setText('No changes to apply.')
|
||||||
|
return
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
total = len(self.files)
|
||||||
|
self.okButton.setEnabled(False)
|
||||||
|
self.cancelButton.setEnabled(False)
|
||||||
|
|
||||||
|
for i, file in enumerate(self.files, 1):
|
||||||
|
self.statusLabel.setText(f'Processing {i}/{total}: {os.path.basename(file)}')
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
try:
|
||||||
|
parser = metadata.MetadataParser(file)
|
||||||
|
if parser.format in ['RAR', 'RAR5']:
|
||||||
|
errors.append(f'{os.path.basename(file)}: CBR is read-only')
|
||||||
|
continue
|
||||||
|
for key, value in bulkData.items():
|
||||||
|
parser.data[key] = value
|
||||||
|
# Set volume if bulk volume editing is enabled
|
||||||
|
if volumes is not None:
|
||||||
|
parser.data['Volume'] = str(volumes[i - 1])
|
||||||
|
parser.saveXML()
|
||||||
|
except Exception as err:
|
||||||
|
errors.append(f'{os.path.basename(file)}: {str(err)}')
|
||||||
|
|
||||||
|
self.okButton.setEnabled(True)
|
||||||
|
self.cancelButton.setEnabled(True)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) +
|
||||||
|
(f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error')
|
||||||
|
self.statusLabel.setText('Errors occurred.')
|
||||||
|
else:
|
||||||
|
self.statusLabel.setText(f'Successfully updated {total} files.')
|
||||||
|
self.ui.close()
|
||||||
|
else:
|
||||||
|
for field in (self.volumeLine, self.numberLine):
|
||||||
|
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
||||||
|
else:
|
||||||
|
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for field in (self.seriesLine, self.titleLine):
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
||||||
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
|
values = self.cleanData(field.text()).split(',')
|
||||||
|
tmpData = []
|
||||||
|
for value in values:
|
||||||
|
if self.cleanData(value) != '':
|
||||||
|
tmpData.append(self.cleanData(value))
|
||||||
|
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
||||||
|
try:
|
||||||
|
self.parser.saveXML()
|
||||||
|
except Exception as err:
|
||||||
|
_, _, traceback = sys.exc_info()
|
||||||
|
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||||
|
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||||
|
self.ui.close()
|
||||||
|
|
||||||
def cleanData(self, s):
|
def cleanData(self, s):
|
||||||
return escape(s.strip())
|
return escape(s.strip())
|
||||||
|
|
||||||
|
def parseVolumeInput(self, text, fileCount):
|
||||||
|
text = text.strip()
|
||||||
|
if not text:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
volumes = []
|
||||||
|
|
||||||
|
# Check if it's a range (e.g., "5-10")
|
||||||
|
if '-' in text and ',' not in text:
|
||||||
|
parts = text.split('-')
|
||||||
|
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
|
||||||
|
return None, 'Invalid range format (use start-end)'
|
||||||
|
try:
|
||||||
|
start = int(parts[0].strip())
|
||||||
|
end = int(parts[1].strip())
|
||||||
|
if start < 0 or end < 0:
|
||||||
|
return None, 'Volume numbers must be positive'
|
||||||
|
if start > end:
|
||||||
|
return None, 'Invalid range: start > end'
|
||||||
|
volumes = list(range(start, end + 1))
|
||||||
|
except ValueError:
|
||||||
|
return None, 'Invalid range format'
|
||||||
|
# Check if it's a comma-separated list (e.g., "1,3,5")
|
||||||
|
elif ',' in text:
|
||||||
|
try:
|
||||||
|
volumes = [int(v.strip()) for v in text.split(',') if v.strip()]
|
||||||
|
if any(v < 0 for v in volumes):
|
||||||
|
return None, 'Volume numbers must be positive'
|
||||||
|
except ValueError:
|
||||||
|
return None, 'Invalid list format'
|
||||||
|
# Single number - generate sequence starting from that number
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
start = int(text)
|
||||||
|
if start < 0:
|
||||||
|
return None, 'Volume number must be positive'
|
||||||
|
volumes = list(range(start, start + fileCount))
|
||||||
|
except ValueError:
|
||||||
|
return None, 'Invalid number'
|
||||||
|
|
||||||
|
# Validate count
|
||||||
|
if not volumes:
|
||||||
|
return None, 'No valid volume numbers parsed'
|
||||||
|
if len(volumes) != fileCount:
|
||||||
|
return None, f'Volume count ({len(volumes)}) != file count ({fileCount})'
|
||||||
|
|
||||||
|
return volumes, None
|
||||||
|
|
||||||
|
def toggleBulkVolume(self, checked):
|
||||||
|
self.volumeLine.setEnabled(checked)
|
||||||
|
if checked:
|
||||||
|
self.volumeLine.setText('')
|
||||||
|
self.volumeLine.setPlaceholderText('e.g., 5 or 1-10 or 1,3,5')
|
||||||
|
else:
|
||||||
|
self.volumeLine.setText('')
|
||||||
|
self.volumeLine.setPlaceholderText('(multiple files)')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ui = QDialog()
|
self.ui = QDialog()
|
||||||
self.parser = None
|
self.parser = None
|
||||||
|
self.files = []
|
||||||
|
self.bulkMode = False
|
||||||
self.setupUi(self.ui)
|
self.setupUi(self.ui)
|
||||||
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
|
||||||
|
|
||||||
|
self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume)
|
||||||
|
|
||||||
self.okButton.clicked.connect(self.saveData)
|
self.okButton.clicked.connect(self.saveData)
|
||||||
self.cancelButton.clicked.connect(self.ui.close)
|
self.cancelButton.clicked.connect(self.ui.close)
|
||||||
if sys.platform.startswith('linux'):
|
if sys.platform.startswith('linux'):
|
||||||
|
|||||||
+664
-460
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,
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
|
||||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
||||||
QVBoxLayout, QWidget)
|
QSizePolicy, QVBoxLayout, QWidget)
|
||||||
from . import KCC_rc
|
from . import KCC_rc
|
||||||
|
|
||||||
class Ui_editorDialog(object):
|
class Ui_editorDialog(object):
|
||||||
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
|
|||||||
|
|
||||||
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
|
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 = QLabel(self.editorWidget)
|
||||||
self.label_3.setObjectName(u"label_3")
|
self.label_3.setObjectName(u"label_3")
|
||||||
|
|
||||||
@@ -156,6 +162,16 @@ class Ui_editorDialog(object):
|
|||||||
|
|
||||||
self.verticalLayout.addWidget(self.optionWidget)
|
self.verticalLayout.addWidget(self.optionWidget)
|
||||||
|
|
||||||
|
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
|
||||||
|
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)
|
||||||
|
QWidget.setTabOrder(self.pencillerLine, self.inkerLine)
|
||||||
|
QWidget.setTabOrder(self.inkerLine, self.coloristLine)
|
||||||
|
QWidget.setTabOrder(self.coloristLine, self.okButton)
|
||||||
|
QWidget.setTabOrder(self.okButton, self.cancelButton)
|
||||||
|
|
||||||
self.retranslateUi(editorDialog)
|
self.retranslateUi(editorDialog)
|
||||||
|
|
||||||
@@ -166,6 +182,10 @@ class Ui_editorDialog(object):
|
|||||||
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
||||||
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
||||||
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", 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_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
|
||||||
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
||||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '9.2.0'
|
__version__ = '10.2.0'
|
||||||
__license__ = 'ISC'
|
__license__ = 'ISC'
|
||||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|||||||
+398
-148
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,12 @@ import sys
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter
|
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
|
||||||
from PIL.Image import Dither
|
from PIL.Image import Dither
|
||||||
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
|
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
def mergeDirectoryTick(output):
|
def mergeDirectoryTick(output):
|
||||||
if output:
|
if output:
|
||||||
@@ -60,18 +62,19 @@ def mergeDirectory(work):
|
|||||||
imagesValid.append(i[0])
|
imagesValid.append(i[0])
|
||||||
# Silently drop directories that contain too many images
|
# Silently drop directories that contain too many images
|
||||||
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
|
||||||
if targetHeight > 131072 * 2:
|
if targetHeight > 131072 * 8:
|
||||||
raise RuntimeError(f'Image too tall at {targetHeight} pixels.')
|
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))
|
result = Image.new('RGB', (targetWidth, targetHeight))
|
||||||
y = 0
|
y = 0
|
||||||
for i in imagesValid:
|
for i in imagesValid:
|
||||||
img = Image.open(i).convert('RGB')
|
with Image.open(i) as img:
|
||||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
img = img.convert('RGB')
|
||||||
widthPercent = (targetWidth / float(img.size[0]))
|
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
widthPercent = (targetWidth / float(img.size[0]))
|
||||||
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||||
result.paste(img, (0, y))
|
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||||
y += img.size[1]
|
result.paste(img, (0, y))
|
||||||
|
y += img.size[1]
|
||||||
os.remove(i)
|
os.remove(i)
|
||||||
savePath = os.path.split(imagesValid[0])
|
savePath = os.path.split(imagesValid[0])
|
||||||
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
|
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
|
||||||
@@ -219,7 +222,7 @@ def splitImage(work):
|
|||||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None, qtgui=None):
|
def main(argv=None, job_progress='', qtgui=None):
|
||||||
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
||||||
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
|
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
|
||||||
|
|
||||||
@@ -251,16 +254,14 @@ def main(argv=None, qtgui=None):
|
|||||||
return 1
|
return 1
|
||||||
if args.height > 0:
|
if args.height > 0:
|
||||||
for sourceDir in args.input:
|
for sourceDir in args.input:
|
||||||
targetDir = sourceDir + "-Splitted"
|
targetDir = sourceDir
|
||||||
if os.path.isdir(sourceDir):
|
if os.path.isdir(sourceDir):
|
||||||
rmtree(targetDir, True)
|
|
||||||
os.renames(sourceDir, targetDir)
|
|
||||||
work = []
|
work = []
|
||||||
pagenumber = 1
|
pagenumber = 1
|
||||||
splitWorkerOutput = []
|
splitWorkerOutput = []
|
||||||
splitWorkerPool = Pool(maxtasksperchild=10)
|
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||||
if args.merge:
|
if args.merge:
|
||||||
print("Merging images...")
|
print(f"{job_progress}Merging images...")
|
||||||
directoryNumer = 1
|
directoryNumer = 1
|
||||||
mergeWork = []
|
mergeWork = []
|
||||||
mergeWorkerOutput = []
|
mergeWorkerOutput = []
|
||||||
@@ -272,7 +273,7 @@ def main(argv=None, qtgui=None):
|
|||||||
directoryNumer += 1
|
directoryNumer += 1
|
||||||
mergeWork.append([os.path.join(root, directory)])
|
mergeWork.append([os.path.join(root, directory)])
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('Combining images')
|
GUI.progressBarTick.emit(f'{job_progress}Combining images')
|
||||||
GUI.progressBarTick.emit(str(directoryNumer))
|
GUI.progressBarTick.emit(str(directoryNumer))
|
||||||
for i in mergeWork:
|
for i in mergeWork:
|
||||||
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
|
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
|
||||||
@@ -285,7 +286,7 @@ def main(argv=None, qtgui=None):
|
|||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||||
mergeWorkerOutput[0][1])
|
mergeWorkerOutput[0][1])
|
||||||
print("Splitting images...")
|
print(f"{job_progress}Splitting images...")
|
||||||
dot_clean(targetDir)
|
dot_clean(targetDir)
|
||||||
for root, _, files in os.walk(targetDir, False):
|
for root, _, files in os.walk(targetDir, False):
|
||||||
for name in files:
|
for name in files:
|
||||||
@@ -295,7 +296,7 @@ def main(argv=None, qtgui=None):
|
|||||||
else:
|
else:
|
||||||
os.remove(os.path.join(root, name))
|
os.remove(os.path.join(root, name))
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('Splitting images')
|
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
|
||||||
GUI.progressBarTick.emit(str(pagenumber))
|
GUI.progressBarTick.emit(str(pagenumber))
|
||||||
GUI.progressBarTick.emit('tick')
|
GUI.progressBarTick.emit('tick')
|
||||||
if len(work) > 0:
|
if len(work) > 0:
|
||||||
@@ -311,8 +312,6 @@ def main(argv=None, qtgui=None):
|
|||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||||
splitWorkerOutput[0][1])
|
splitWorkerOutput[0][1])
|
||||||
if args.inPlace:
|
|
||||||
os.renames(targetDir, sourceDir)
|
|
||||||
else:
|
else:
|
||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
raise UserWarning("C2P: Source directory is empty.")
|
raise UserWarning("C2P: Source directory is empty.")
|
||||||
|
|||||||
@@ -20,15 +20,17 @@
|
|||||||
|
|
||||||
from functools import cached_property, lru_cache
|
from functools import cached_property, lru_cache
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
import distro
|
import distro
|
||||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
from xml.parsers.expat import ExpatError
|
from xml.parsers.expat import ExpatError
|
||||||
from .shared import subprocess_run
|
from .shared import IMAGE_TYPES, subprocess_run
|
||||||
|
|
||||||
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
||||||
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
||||||
|
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
|
||||||
|
|
||||||
|
|
||||||
class ComicArchive:
|
class ComicArchive:
|
||||||
@@ -65,11 +67,14 @@ class ComicArchive:
|
|||||||
def extract(self, targetdir):
|
def extract(self, targetdir):
|
||||||
if not os.path.isdir(targetdir):
|
if not os.path.isdir(targetdir):
|
||||||
raise OSError('Target directory doesn\'t exist.')
|
raise OSError('Target directory doesn\'t exist.')
|
||||||
|
|
||||||
|
if Path(self.basename).suffix.lower() in IMAGE_TYPES:
|
||||||
|
raise UserWarning('Put images into folder and drag and drop folder into KCC window.')
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
|
|
||||||
extraction_commands = [
|
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],
|
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -121,7 +126,7 @@ class ComicArchive:
|
|||||||
def available_archive_tools():
|
def available_archive_tools():
|
||||||
available = []
|
available = []
|
||||||
|
|
||||||
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
|
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
|
||||||
try:
|
try:
|
||||||
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
||||||
available.append(tool)
|
available.append(tool)
|
||||||
|
|||||||
+171
-101
@@ -24,13 +24,15 @@ import numpy as np
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
import mozjpeg_lossless_optimization
|
import mozjpeg_lossless_optimization
|
||||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
|
from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
|
||||||
|
|
||||||
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
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 .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 .inter_panel_crop_alg import crop_empty_inter_panel
|
||||||
|
from .shared import get_contain_resolution
|
||||||
|
|
||||||
AUTO_CROP_THRESHOLD = 0.015
|
AUTO_CROP_THRESHOLD = 0.015
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
class ProfileData:
|
class ProfileData:
|
||||||
@@ -85,6 +87,9 @@ class ProfileData:
|
|||||||
]
|
]
|
||||||
|
|
||||||
ProfilesKindleEBOK = {
|
ProfilesKindleEBOK = {
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfilesKindlePDOC = {
|
||||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||||
@@ -92,16 +97,20 @@ class ProfileData:
|
|||||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||||
}
|
|
||||||
|
|
||||||
ProfilesKindlePDOC = {
|
|
||||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
|
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
|
||||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
|
||||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.0),
|
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), 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),
|
||||||
|
'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 = {
|
ProfilesKindle = {
|
||||||
@@ -152,11 +161,14 @@ class ComicPageParser:
|
|||||||
|
|
||||||
# Detect corruption in source image, let caller catch any exceptions triggered.
|
# Detect corruption in source image, let caller catch any exceptions triggered.
|
||||||
srcImgPath = os.path.join(source[0], source[1])
|
srcImgPath = os.path.join(source[0], source[1])
|
||||||
Image.open(srcImgPath).verify()
|
# Image.open(srcImgPath).verify()
|
||||||
with Image.open(srcImgPath) as im:
|
with Image.open(srcImgPath) as im:
|
||||||
self.image = im.copy()
|
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
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
Image.Resampling = Image
|
Image.Resampling = Image
|
||||||
@@ -186,15 +198,23 @@ class ComicPageParser:
|
|||||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||||
new_image.paste(pageone, (0, 0))
|
new_image.paste(pageone, (0, 0))
|
||||||
new_image.paste(pagetwo, (0, height))
|
new_image.paste(pagetwo, (0, height))
|
||||||
self.payload.append(['N', self.source, new_image, self.fill])
|
self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
|
||||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
elif self.opt.webtoon:
|
||||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
self.payload.append(['N', self.source, self.image, self.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
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
if not self.opt.rotateright:
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
else:
|
||||||
if self.opt.splitter != 1:
|
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:
|
if width > height:
|
||||||
leftbox = (0, 0, int(width / 2), height)
|
leftbox = (0, 0, int(width / 2), height)
|
||||||
rightbox = (int(width / 2), 0, width, height)
|
rightbox = (int(width / 2), 0, width, height)
|
||||||
@@ -207,18 +227,23 @@ class ComicPageParser:
|
|||||||
else:
|
else:
|
||||||
pageone = self.image.crop(leftbox)
|
pageone = self.image.crop(leftbox)
|
||||||
pagetwo = self.image.crop(rightbox)
|
pagetwo = self.image.crop(rightbox)
|
||||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
|
||||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
|
||||||
if self.opt.splitter > 0:
|
|
||||||
|
# if (rotate) or (split and rotate)
|
||||||
|
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
|
||||||
spread = self.image
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
if not self.opt.rotateright:
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
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:
|
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):
|
def fillCheck(self):
|
||||||
if self.opt.bordersColor:
|
if False:
|
||||||
return self.opt.bordersColor
|
return self.opt.bordersColor
|
||||||
else:
|
else:
|
||||||
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
||||||
@@ -257,15 +282,17 @@ class ComicPageParser:
|
|||||||
|
|
||||||
|
|
||||||
class ComicPage:
|
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.opt = options
|
||||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||||
if self.opt.hq:
|
if self.opt.hq:
|
||||||
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
||||||
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
|
|
||||||
self.original_color_mode = image.mode
|
self.original_color_mode = image.mode
|
||||||
# TODO: color check earlier
|
# TODO: color check earlier
|
||||||
self.image = image.convert("RGB")
|
self.image = image.convert("RGB")
|
||||||
|
self.color = self.colorCheck()
|
||||||
|
self.colorOutput = self.color and self.opt.forcecolor
|
||||||
|
self.page_background_color = page_background_color
|
||||||
self.fill = fill
|
self.fill = fill
|
||||||
self.rotated = False
|
self.rotated = False
|
||||||
self.orgPath = os.path.join(path[0], path[1])
|
self.orgPath = os.path.join(path[0], path[1])
|
||||||
@@ -284,8 +311,7 @@ class ComicPage:
|
|||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
Image.Resampling = Image
|
Image.Resampling = Image
|
||||||
|
|
||||||
@cached_property
|
def colorCheck(self):
|
||||||
def color(self):
|
|
||||||
if self.original_color_mode in ("L", "1"):
|
if self.original_color_mode in ("L", "1"):
|
||||||
return False
|
return False
|
||||||
if self.opt.webtoon:
|
if self.opt.webtoon:
|
||||||
@@ -296,9 +322,9 @@ class ComicPage:
|
|||||||
|
|
||||||
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
|
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
|
||||||
# for better accuracy, you could split the image in half and analyze each half separately
|
# for better accuracy, you could split the image in half and analyze each half separately
|
||||||
def histograms_cutoff(self, cb, cr, cutoff=(2, 2)):
|
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
|
||||||
cb_hist = cb.histogram()
|
if cutoff == (0, 0):
|
||||||
cr_hist = cr.histogram()
|
return cb_hist, cr_hist
|
||||||
|
|
||||||
for h in cb_hist, cr_hist:
|
for h in cb_hist, cr_hist:
|
||||||
# get number of pixels
|
# get number of pixels
|
||||||
@@ -327,60 +353,50 @@ class ComicPage:
|
|||||||
break
|
break
|
||||||
return cb_hist, cr_hist
|
return cb_hist, cr_hist
|
||||||
|
|
||||||
|
def color_precision(self, cb_hist_original, cr_hist_original, cutoff, diff_threshold):
|
||||||
|
cb_hist, cr_hist = self.histograms_cutoff(cb_hist_original.copy(), cr_hist_original.copy(), cutoff)
|
||||||
|
|
||||||
|
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
||||||
|
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
||||||
|
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
|
||||||
|
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
|
||||||
|
|
||||||
|
# bias adjustment, don't go lower than 7
|
||||||
|
SPREAD_THRESHOLD = 7
|
||||||
|
if self.opt.forcecolor:
|
||||||
|
if any([
|
||||||
|
cb_nonzero[0] > 128,
|
||||||
|
cr_nonzero[0] > 128,
|
||||||
|
cb_nonzero[-1] < 128,
|
||||||
|
cr_nonzero[-1] < 128,
|
||||||
|
]):
|
||||||
|
return True, True
|
||||||
|
elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
||||||
|
return True, False
|
||||||
|
|
||||||
|
DIFF_THRESHOLD = diff_threshold
|
||||||
|
if any([
|
||||||
|
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
||||||
|
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
||||||
|
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
||||||
|
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
||||||
|
]):
|
||||||
|
return True, True
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
def calculate_color(self):
|
def calculate_color(self):
|
||||||
img = self.image.convert("YCbCr")
|
img = self.image.convert("YCbCr")
|
||||||
_, cb, cr = img.split()
|
_, cb, cr = img.split()
|
||||||
|
cb_hist_original = cb.histogram()
|
||||||
|
cr_hist_original = cr.histogram()
|
||||||
|
|
||||||
# get rid of some jpg compression
|
# you can increase 22 but don't increase 10. 4 maybe can go higher
|
||||||
cutoff = (.2, .2)
|
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
|
||||||
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
|
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
|
||||||
|
if done:
|
||||||
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
return decision
|
||||||
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
return False
|
||||||
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
|
|
||||||
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
|
|
||||||
|
|
||||||
# bias adjustment
|
|
||||||
SPREAD_THRESHOLD = 5
|
|
||||||
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# check for large amount of extreme colors
|
|
||||||
# 11 if too high. 10 is barely enough. If needed make it magnitude of both
|
|
||||||
DIFF_THRESHOLD = 10
|
|
||||||
if any([
|
|
||||||
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
|
||||||
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
|
||||||
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
|
||||||
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
|
||||||
]):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# get ride of most jpg compression
|
|
||||||
cutoff = (2, 2)
|
|
||||||
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
|
|
||||||
|
|
||||||
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
|
||||||
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
|
||||||
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
|
|
||||||
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
|
|
||||||
|
|
||||||
# bias adjustment
|
|
||||||
SPREAD_THRESHOLD = 5
|
|
||||||
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# check for any amount of mild colors still remaining
|
|
||||||
DIFF_THRESHOLD = 6
|
|
||||||
if any([
|
|
||||||
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
|
||||||
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
|
|
||||||
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
|
||||||
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
|
|
||||||
]):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def saveToDir(self):
|
def saveToDir(self):
|
||||||
try:
|
try:
|
||||||
@@ -404,25 +420,32 @@ class ComicPage:
|
|||||||
raise RuntimeError('Cannot save image. ' + str(err))
|
raise RuntimeError('Cannot save image. ' + str(err))
|
||||||
|
|
||||||
def save_with_codec(self, image, targetPath):
|
def save_with_codec(self, image, targetPath):
|
||||||
if self.opt.forcepng:
|
if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
|
||||||
image.info["transparency"] = None
|
image.info.pop('transparency', None)
|
||||||
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
|
if self.opt.webp_output:
|
||||||
|
targetPath += '.webp'
|
||||||
|
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
|
||||||
|
elif self.opt.kindle_azw3:
|
||||||
targetPath += '.gif'
|
targetPath += '.gif'
|
||||||
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
||||||
else:
|
else:
|
||||||
targetPath += '.png'
|
targetPath += '.png'
|
||||||
image.save(targetPath, 'PNG', optimize=1)
|
image.save(targetPath, 'PNG', optimize=1)
|
||||||
else:
|
else:
|
||||||
targetPath += '.jpg'
|
if self.opt.webp_output:
|
||||||
if self.opt.mozjpeg:
|
targetPath += '.webp'
|
||||||
|
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
|
||||||
|
elif self.opt.mozjpeg:
|
||||||
|
targetPath += '.jpg'
|
||||||
with io.BytesIO() as output:
|
with io.BytesIO() as output:
|
||||||
image.save(output, format="JPEG", optimize=1, quality=85)
|
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
|
||||||
input_jpeg_bytes = output.getvalue()
|
input_jpeg_bytes = output.getvalue()
|
||||||
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
||||||
with open(targetPath, "wb") as output_jpeg_file:
|
with open(targetPath, "wb") as output_jpeg_file:
|
||||||
output_jpeg_file.write(output_jpeg_bytes)
|
output_jpeg_file.write(output_jpeg_bytes)
|
||||||
else:
|
else:
|
||||||
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
targetPath += '.jpg'
|
||||||
|
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
|
||||||
return targetPath
|
return targetPath
|
||||||
|
|
||||||
def gammaCorrectImage(self):
|
def gammaCorrectImage(self):
|
||||||
@@ -490,23 +513,43 @@ class ComicPage:
|
|||||||
self.image = erase_rainbow_artifacts(self.image, is_color)
|
self.image = erase_rainbow_artifacts(self.image, is_color)
|
||||||
|
|
||||||
def resizeImage(self):
|
def resizeImage(self):
|
||||||
|
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
|
||||||
|
# TODO: Kindle Scribe case
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
|
||||||
ratio_device = float(self.size[1]) / float(self.size[0])
|
ratio_device = float(self.size[1]) / float(self.size[0])
|
||||||
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
||||||
method = self.resize_method()
|
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)
|
self.image = self.image.resize(self.size, method)
|
||||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||||
pass
|
pass
|
||||||
else: # if image bigger than device resolution or smaller with upscaling
|
else: # if image bigger than device resolution or smaller with upscaling
|
||||||
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
|
||||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
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)
|
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||||
else:
|
else:
|
||||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||||
|
|
||||||
def resize_method(self):
|
def resize_method(self):
|
||||||
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
|
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||||
return Image.Resampling.BICUBIC
|
return Image.Resampling.BICUBIC
|
||||||
else:
|
else:
|
||||||
return Image.Resampling.LANCZOS
|
return Image.Resampling.LANCZOS
|
||||||
@@ -523,7 +566,7 @@ class ComicPage:
|
|||||||
self.image = self.image.crop(box)
|
self.image = self.image.crop(box)
|
||||||
|
|
||||||
def cropPageNumber(self, power, minimum):
|
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:
|
if bbox:
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
@@ -533,7 +576,7 @@ class ComicPage:
|
|||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
def cropMargin(self, power, 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:
|
if bbox:
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
@@ -543,13 +586,14 @@ class ComicPage:
|
|||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
def cropInterPanelEmptySections(self, direction):
|
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:
|
class Cover:
|
||||||
def __init__(self, source, opt):
|
def __init__(self, source, opt):
|
||||||
self.options = opt
|
self.options = opt
|
||||||
self.source = source
|
self.source = source
|
||||||
self.image = Image.open(source)
|
self.image = Image.open(source)
|
||||||
|
self.smartcover = False
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
Image.Resampling = Image
|
Image.Resampling = Image
|
||||||
@@ -557,33 +601,59 @@ class Cover:
|
|||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
self.image = self.image.convert('RGB')
|
self.image = self.image.convert('RGB')
|
||||||
self.image = ImageOps.autocontrast(self.image)
|
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||||
if not self.options.forcecolor:
|
if not self.options.forcecolor:
|
||||||
self.image = self.image.convert('L')
|
self.image = self.image.convert('L')
|
||||||
self.crop_main_cover()
|
if self.options.smartcovercrop:
|
||||||
|
self.crop_main_cover()
|
||||||
|
|
||||||
size = list(self.options.profileData[1])
|
size = list(self.options.profileData[1])
|
||||||
if self.options.kindle_scribe_azw3:
|
if self.options.kindle_scribe_azw3:
|
||||||
|
size[0] = min(size[0], 1920)
|
||||||
size[1] = min(size[1], 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):
|
def crop_main_cover(self):
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
if w / h > 2:
|
if w / h > 2:
|
||||||
|
self.smartcover = True
|
||||||
if self.options.righttoleft:
|
if self.options.righttoleft:
|
||||||
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
||||||
else:
|
else:
|
||||||
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
||||||
|
elif w / h > 1.83:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .19, 0, w * .575, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w * .425, 0, .81 * w, h))
|
||||||
|
elif w / h > 1.7:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .2, 0, w * .583, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w * .417, 0, .8 * w, h))
|
||||||
elif w / h > 1.34:
|
elif w / h > 1.34:
|
||||||
|
self.smartcover = True
|
||||||
if self.options.righttoleft:
|
if self.options.righttoleft:
|
||||||
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
||||||
else:
|
else:
|
||||||
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
||||||
|
elif w / h > 1.0:
|
||||||
|
self.smartcover = True
|
||||||
|
if self.options.righttoleft:
|
||||||
|
self.image = self.image.crop((w * .36, 0, w, h))
|
||||||
|
else:
|
||||||
|
self.image = self.image.crop((w, 0, .64 * w, h))
|
||||||
|
|
||||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
def save_to_folder(self, target, tomeid, len_tomes=0):
|
||||||
try:
|
try:
|
||||||
if tomeid == 0:
|
if tomeid == 0:
|
||||||
self.image.save(target, "JPEG", optimize=1, quality=85)
|
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||||
else:
|
else:
|
||||||
copy = self.image.copy()
|
copy = self.image.copy()
|
||||||
draw = ImageDraw.Draw(copy)
|
draw = ImageDraw.Draw(copy)
|
||||||
@@ -597,7 +667,7 @@ class Cover:
|
|||||||
stroke_fill=0,
|
stroke_fill=0,
|
||||||
stroke_width=25
|
stroke_width=25
|
||||||
)
|
)
|
||||||
copy.save(target, "JPEG", optimize=1, quality=85)
|
copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||||
except IOError:
|
except IOError:
|
||||||
raise RuntimeError('Failed to save cover.')
|
raise RuntimeError('Failed to save cover.')
|
||||||
|
|
||||||
@@ -605,6 +675,6 @@ class Cover:
|
|||||||
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
||||||
try:
|
try:
|
||||||
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
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:
|
except IOError:
|
||||||
raise RuntimeError('Failed to upload cover.')
|
raise RuntimeError('Failed to upload cover.')
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from PIL import Image, ImageFilter, ImageOps
|
from PIL import Image, ImageFilter, ImageOps, ImageFile
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from .common_crop import threshold_from_power, group_close_values
|
from .common_crop import threshold_from_power, group_close_values
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
|
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from PIL import ImageOps, ImageFilter
|
from PIL import ImageOps, ImageFilter, ImageFile
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .common_crop import threshold_from_power, group_close_values
|
from .common_crop import threshold_from_power, group_close_values
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Some assupmptions on the page number sizes
|
Some assupmptions on the page number sizes
|
||||||
@@ -158,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
|
|||||||
for box in edge_bbox:
|
for box in edge_bbox:
|
||||||
edge = bw_img.crop(box)
|
edge = bw_img.crop(box)
|
||||||
h = edge.histogram()
|
h = edge.histogram()
|
||||||
|
if not edge.height or not edge.width:
|
||||||
|
continue
|
||||||
imperfections = h[255] / (edge.height * edge.width)
|
imperfections = h[255] / (edge.height * edge.width)
|
||||||
if imperfections > 0 and imperfections < .02:
|
if imperfections > 0 and imperfections < .02:
|
||||||
bw_img.paste(im=0, box=box)
|
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
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image, ImageFile
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
def fourier_transform_image(img):
|
def fourier_transform_image(img):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import sys
|
|||||||
from traceback import format_tb
|
from traceback import format_tb
|
||||||
|
|
||||||
|
|
||||||
|
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
|
||||||
|
|
||||||
|
|
||||||
class HTMLStripper(HTMLParser):
|
class HTMLStripper(HTMLParser):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
HTMLParser.__init__(self)
|
HTMLParser.__init__(self)
|
||||||
@@ -58,6 +61,23 @@ def getImageFileName(imgfile):
|
|||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
return [name, ext]
|
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):
|
def walkSort(dirnames, filenames):
|
||||||
convert = lambda text: int(text) if text.isdigit() else text
|
convert = lambda text: int(text) if text.isdigit() else text
|
||||||
@@ -102,10 +122,6 @@ def dependencyCheck(level):
|
|||||||
missing.append('PySide 6.0.0')
|
missing.append('PySide 6.0.0')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('PySide 6.0.0+')
|
missing.append('PySide 6.0.0+')
|
||||||
try:
|
|
||||||
import raven
|
|
||||||
except ImportError:
|
|
||||||
missing.append('raven 6.0.0+')
|
|
||||||
if level > 1:
|
if level > 1:
|
||||||
try:
|
try:
|
||||||
from psutil import __version__ as psutilVersion
|
from psutil import __version__ as psutilVersion
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Pillow>=11.3.0
|
||||||
|
psutil>=5.9.5
|
||||||
|
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,11 @@
|
|||||||
PySide6==6.5.2
|
PySide6==6.4.3
|
||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.31.0
|
requests>=2.34.2
|
||||||
python-slugify>=1.2.1
|
python-slugify>=8.0.4
|
||||||
raven>=6.0.0
|
packaging>=26.2
|
||||||
packaging>=23.2
|
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
natsort>=8.4.0
|
natsort>=8.4.0
|
||||||
distro>=1.8.0
|
distro>=1.8.0
|
||||||
numpy<2
|
numpy<2
|
||||||
PyMuPDF>=1.26.1
|
PyMuPDF==1.25.5
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
PySide6==6.1.3
|
PySide6==6.1.3
|
||||||
Pillow>=9
|
Pillow>=9
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.31.0
|
requests>=2.32.4
|
||||||
python-slugify>=1.2.1
|
python-slugify>=8.0.4
|
||||||
raven>=6.0.0
|
packaging>=26.2
|
||||||
packaging>=23.2
|
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
natsort>=8.4.0
|
natsort>=8.4.0
|
||||||
distro>=1.8.0
|
distro>=1.8.0
|
||||||
|
|||||||
+3
-4
@@ -1,10 +1,9 @@
|
|||||||
PySide6<6.10
|
PySide6<6.10
|
||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.31.0
|
requests>=2.34.2
|
||||||
python-slugify>=1.2.1
|
python-slugify>=8.0.4,<9.0.0
|
||||||
raven>=6.0.0
|
packaging>=26.2
|
||||||
packaging>=23.2
|
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
natsort>=8.4.0
|
natsort>=8.4.0
|
||||||
distro>=1.8.0
|
distro>=1.8.0
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Install as Python package:
|
|||||||
|
|
||||||
Create EXE/APP:
|
Create EXE/APP:
|
||||||
python3 setup.py build_binary
|
python3 setup.py build_binary
|
||||||
|
python3 setup.py build_c2e
|
||||||
|
python3 setup.py build_c2p
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -38,8 +40,8 @@ class BuildBinaryCommand(setuptools.Command):
|
|||||||
if sys.platform == 'darwin':
|
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 --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
|
# 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')
|
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
|
||||||
if min_os:
|
if min_os.startswith('10.1'):
|
||||||
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
||||||
else:
|
else:
|
||||||
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
||||||
@@ -57,10 +59,75 @@ class BuildBinaryCommand(setuptools.Command):
|
|||||||
else:
|
else:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
class BuildC2ECommand(setuptools.Command):
|
||||||
|
description = 'build binary c2e release'
|
||||||
|
user_options = []
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
def run(self):
|
||||||
|
VERSION = __version__
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "KCC C2E" -c -s kcc-c2e.py')
|
||||||
|
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||||
|
sys.exit(0)
|
||||||
|
elif sys.platform == 'win32':
|
||||||
|
if os.getenv('WINDOWS_7'):
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_win7_legacy_' + VERSION + ' -c --noupx kcc-c2e.py')
|
||||||
|
else:
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_' + VERSION + ' -c --noupx kcc-c2e.py')
|
||||||
|
sys.exit(0)
|
||||||
|
elif sys.platform == 'linux':
|
||||||
|
os.system(
|
||||||
|
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2e_linux_' + VERSION + ' kcc-c2e.py')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
class BuildC2PCommand(setuptools.Command):
|
||||||
|
description = 'build binary c2p release'
|
||||||
|
user_options = []
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# noinspection PyShadowingNames
|
||||||
|
def run(self):
|
||||||
|
VERSION = __version__
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -n "KCC C2P" -c -s kcc-c2p.py')
|
||||||
|
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||||
|
sys.exit(0)
|
||||||
|
elif sys.platform == 'win32':
|
||||||
|
if os.getenv('WINDOWS_7'):
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_win7_legacy_' + VERSION + ' -c --noupx kcc-c2p.py')
|
||||||
|
else:
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_' + VERSION + ' -c --noupx kcc-c2p.py')
|
||||||
|
sys.exit(0)
|
||||||
|
elif sys.platform == 'linux':
|
||||||
|
os.system(
|
||||||
|
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2p_linux_' + VERSION + ' kcc-c2p.py')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
cmdclass={
|
cmdclass={
|
||||||
'build_binary': BuildBinaryCommand,
|
'build_binary': BuildBinaryCommand,
|
||||||
|
'build_c2e': BuildC2ECommand,
|
||||||
|
'build_c2p': BuildC2PCommand,
|
||||||
},
|
},
|
||||||
name=NAME,
|
name=NAME,
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
@@ -81,17 +148,16 @@ setuptools.setup(
|
|||||||
},
|
},
|
||||||
packages=['kindlecomicconverter'],
|
packages=['kindlecomicconverter'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyside6>=6.0.0',
|
'PySide6>=6.0.0',
|
||||||
'Pillow>=9.3.0',
|
'Pillow>=9.3.0',
|
||||||
'PyMuPDF>=1.18.0',
|
|
||||||
'psutil>=5.9.5',
|
'psutil>=5.9.5',
|
||||||
'python-slugify>=1.2.1,<9.0.0',
|
|
||||||
'raven>=6.0.0',
|
|
||||||
'requests>=2.31.0',
|
'requests>=2.31.0',
|
||||||
'mozjpeg-lossless-optimization>=1.1.2',
|
'python-slugify>=1.2.1,<9.0.0',
|
||||||
|
'mozjpeg-lossless-optimization>=1.2.0',
|
||||||
'natsort>=8.4.0',
|
'natsort>=8.4.0',
|
||||||
'distro',
|
'distro>=1.8.0',
|
||||||
'numpy>=1.22.4',
|
'numpy>=1.22.4',
|
||||||
|
'packaging>=23.2',
|
||||||
'PyMuPDF>=1.16.1',
|
'PyMuPDF>=1.16.1',
|
||||||
],
|
],
|
||||||
classifiers=[],
|
classifiers=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user