mirror of
https://github.com/ciromattia/kcc
synced 2026-04-15 13:38:46 +00:00
Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
0e56cde791 | ||
|
|
9a021ad5d4 | ||
|
|
d648081086 | ||
|
|
b2a8b364d9 | ||
|
|
1f935a0635 | ||
|
|
58b9651ff3 | ||
|
|
11a56dd892 | ||
|
|
c23af709cf | ||
|
|
87a7197c16 | ||
|
|
5813f914fc | ||
|
|
530ae410d4 | ||
|
|
a7428f18b6 | ||
|
|
06194b33ad | ||
|
|
80ea17ff02 | ||
|
|
e0466ad040 | ||
|
|
df924824aa | ||
|
|
82f1c1bb0a | ||
|
|
6f801a3334 | ||
|
|
ee8fc77ca9 | ||
|
|
1da06b43e2 | ||
|
|
d28740491e | ||
|
|
e2b98db1a2 | ||
|
|
4c5ec95a9b | ||
|
|
e56612228c | ||
|
|
3470d01367 | ||
|
|
a795a84899 | ||
|
|
ef44f6f285 | ||
|
|
6acdebae3c | ||
|
|
dc8136f6fd | ||
|
|
28ec2d23fc | ||
|
|
108363ce2f | ||
|
|
543fb81027 | ||
|
|
e6c6c05d40 | ||
|
|
18cd55f439 | ||
|
|
7ce4438886 | ||
|
|
841ec517ac | ||
|
|
4614fdd07a | ||
|
|
d0b72e1c83 | ||
|
|
e11755b118 | ||
|
|
a9bfc152bf | ||
|
|
f228ed261e | ||
|
|
b8020758a6 | ||
|
|
6540638e03 | ||
|
|
565e4aec45 | ||
|
|
0453fcfc90 | ||
|
|
a6ee867da2 | ||
|
|
1e4434fc7c | ||
|
|
7122945fa2 | ||
|
|
788163f3df | ||
|
|
3fa90735d5 | ||
|
|
e6cd26c773 | ||
|
|
e92b5c74de | ||
|
|
a031e4622e | ||
|
|
ae1989335e | ||
|
|
a7cb3565aa | ||
|
|
ec8098291f | ||
|
|
297e8757f0 | ||
|
|
2cfd6d9728 | ||
|
|
5733786dc6 | ||
|
|
8a862f11ac | ||
|
|
c32620cfeb | ||
|
|
512cac7f8a | ||
|
|
5e86acc740 | ||
|
|
20388304e8 | ||
|
|
420bed995b | ||
|
|
bc92c2dd85 | ||
|
|
a1f4e040ba | ||
|
|
137d53672a | ||
|
|
0cc75ab1e7 | ||
|
|
4cecf6fc4d | ||
|
|
2f0c9ae95d | ||
|
|
b856a176b0 | ||
|
|
e50163fb59 | ||
|
|
0ab20a5ce3 | ||
|
|
9c69a6fdcc | ||
|
|
26f0bf9989 | ||
|
|
b3db3256d6 | ||
|
|
96a92fb9bb | ||
|
|
18f02df3a1 | ||
|
|
235db43fa6 | ||
|
|
cab7cae714 | ||
|
|
a41f947030 | ||
|
|
d2828877af | ||
|
|
704dcd6dbe | ||
|
|
6a7500441d | ||
|
|
753eeb64eb | ||
|
|
a63d9e3ad0 | ||
|
|
cc01dc611a | ||
|
|
9a605c2d8a | ||
|
|
a7005748c7 | ||
|
|
55193119fb | ||
|
|
741cab68f3 | ||
|
|
7f8f3e67b7 | ||
|
|
be12661f38 | ||
|
|
06e2ee2968 | ||
|
|
27296565a3 | ||
|
|
bb70337a35 | ||
|
|
cf0586ae70 | ||
|
|
4100141b46 | ||
|
|
8eb81b7d67 | ||
|
|
e2dbc05a83 | ||
|
|
50b82786a1 | ||
|
|
88d1643f64 | ||
|
|
ddf2fa360f | ||
|
|
43e974f20d | ||
|
|
e3a3e9b3c2 | ||
|
|
380dc5c42c | ||
|
|
8ab7520754 | ||
|
|
3cd6e09bcb | ||
|
|
cb5f4db5c4 | ||
|
|
f1ffb2c4e8 | ||
|
|
26327728d0 | ||
|
|
61bfb0a51f | ||
|
|
2f03119926 | ||
|
|
8f08c63f4e | ||
|
|
eb24a400b4 | ||
|
|
cc2eb9dcf3 | ||
|
|
d1a443b3d8 | ||
|
|
f8c35ce634 | ||
|
|
580a800d25 | ||
|
|
023797f012 | ||
|
|
63a18627d3 | ||
|
|
0d967084a0 | ||
|
|
2da5b11858 | ||
|
|
f6d10337d8 | ||
|
|
cf047ecf6f | ||
|
|
e7ee8bed9d | ||
|
|
9c8a1759cf | ||
|
|
6299754964 | ||
|
|
a3db86a29b | ||
|
|
67714a9b06 | ||
|
|
95f9a3cda9 | ||
|
|
0e12fc30c6 |
@@ -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
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -69,6 +69,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
34
.github/workflows/docker-base-publish.yml
vendored
34
.github/workflows/docker-base-publish.yml
vendored
@@ -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 }}
|
|
||||||
66
.github/workflows/docker-publish.yml
vendored
66
.github/workflows/docker-publish.yml
vendored
@@ -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
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
platform_linux_arm32v7_enabled: true
|
registry: ghcr.io
|
||||||
platform_linux_arm64v8_enabled: true
|
username: ${{ github.repository_owner }}
|
||||||
platform_linux_amd64_enabled: true
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
push_enabled: true
|
|
||||||
build_nohealthcheck: false
|
- name: Set up QEMU
|
||||||
ghcr_repo_owner: ${{ github.repository_owner }}
|
uses: docker/setup-qemu-action@v3
|
||||||
ghcr_repo: ${{ github.repository }}
|
|
||||||
secrets:
|
- name: Set up Docker Buildx
|
||||||
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- 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@v5
|
||||||
|
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@v6
|
||||||
|
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
|
||||||
|
|||||||
10
.github/workflows/package-linux.yml
vendored
10
.github/workflows/package-linux.yml
vendored
@@ -25,9 +25,9 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
@@ -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,7 +59,7 @@ 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@v6
|
||||||
with:
|
with:
|
||||||
name: AppImage
|
name: AppImage
|
||||||
path: './*.AppImage*'
|
path: './*.AppImage*'
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
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*
|
||||||
|
|||||||
17
.github/workflows/package-macos.yml
vendored
17
.github/workflows/package-macos.yml
vendored
@@ -25,18 +25,20 @@ 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@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
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
|
||||||
@@ -69,7 +71,7 @@ jobs:
|
|||||||
# apply provisioning profile
|
# apply provisioning profile
|
||||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- run: npm install -g appdmg
|
- run: npm install -g appdmg
|
||||||
@@ -78,7 +80,7 @@ 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@v6
|
||||||
with:
|
with:
|
||||||
name: mac-os-build-${{ runner.arch }}
|
name: mac-os-build-${{ runner.arch }}
|
||||||
path: dist/*.dmg
|
path: dist/*.dmg
|
||||||
@@ -87,9 +89,8 @@ jobs:
|
|||||||
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
|
||||||
|
|||||||
66
.github/workflows/package-osx-legacy.yml
vendored
Normal file
66
.github/workflows/package-osx-legacy.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: build KCC for osx legacy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
# Don't trigger if it's just a documentation update
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '**.MD'
|
||||||
|
- '**.yml'
|
||||||
|
- '**.sh'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'LICENSE'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.dockerignore'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-15-intel ]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
# We need the official Python, because the GA ones only support newer macOS versions
|
||||||
|
# The deployment target is picked up by the Python build tools automatically
|
||||||
|
PYTHON_VERSION: 3.11.9
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Get Python
|
||||||
|
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||||
|
- name: Install Python
|
||||||
|
run: |
|
||||||
|
sudo installer -pkg python.pkg -target /
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
python3 --version
|
||||||
|
pip3 install --upgrade pip pyinstaller certifi
|
||||||
|
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||||
|
./gen_ui_files.sh
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- run: npm install -g appdmg
|
||||||
|
- name: build binary
|
||||||
|
run: |
|
||||||
|
python3 setup.py build_binary
|
||||||
|
- name: upload build
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: osx-build-${{ runner.arch }}
|
||||||
|
path: dist/*.dmg
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: false
|
||||||
|
files: |
|
||||||
|
LICENSE.txt
|
||||||
|
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@v4
|
|
||||||
|
|
||||||
- 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.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/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
|
|
||||||
27
.github/workflows/package-windows.yml
vendored
27
.github/workflows/package-windows.yml
vendored
@@ -23,11 +23,21 @@ 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@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
@@ -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@v6
|
||||||
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.2
|
uses: signpath/github-action-submit-signing-request@v2.0
|
||||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||||
with:
|
with:
|
||||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
@@ -63,7 +73,6 @@ jobs:
|
|||||||
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
|
||||||
|
|||||||
71
.github/workflows/package-windows7.yml
vendored
Normal file
71
.github/workflows/package-windows7.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: build KCC for windows 7
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
# Don't trigger if it's just a documentation update
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
- '**.MD'
|
||||||
|
- '**.yml'
|
||||||
|
- '**.sh'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'LICENSE'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.dockerignore'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: windows-2022
|
||||||
|
env:
|
||||||
|
WINDOWS_7: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
cache: 'pip'
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-win7.txt
|
||||||
|
pip install certifi pyinstaller --no-binary pyinstaller
|
||||||
|
.\gen_ui_files.bat
|
||||||
|
- name: build binary
|
||||||
|
run: |
|
||||||
|
python setup.py build_binary
|
||||||
|
- name: upload-unsigned-artifact
|
||||||
|
id: upload-unsigned-artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: windows7-build
|
||||||
|
path: dist/*.exe
|
||||||
|
- id: optional_step_id
|
||||||
|
uses: signpath/github-action-submit-signing-request@v2.0
|
||||||
|
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||||
|
with:
|
||||||
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
|
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
|
||||||
|
project-slug: 'kcc'
|
||||||
|
signing-policy-slug: 'release-signing'
|
||||||
|
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
|
||||||
|
wait-for-completion: true
|
||||||
|
output-artifact-directory: 'dist/'
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: false
|
||||||
|
files: |
|
||||||
|
dist/*.exe
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,9 @@ dist/
|
|||||||
build/
|
build/
|
||||||
KindleComicConverter*.egg-info/
|
KindleComicConverter*.egg-info/
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
win7
|
||||||
|
osx10.11
|
||||||
/venv/
|
/venv/
|
||||||
/kindlegen*
|
/kindlegen*
|
||||||
/kcc.bat
|
/kcc.bat
|
||||||
|
|||||||
90
Dockerfile
90
Dockerfile
@@ -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
Dockerfile-base
164
Dockerfile-base
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
|
||||||
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
|
|
||||||
|
|
||||||
152
README.md
152
README.md
@@ -7,22 +7,33 @@
|
|||||||
[](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/GIF image files in folders, archives, or PDFs.
|
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
||||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ.
|
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||||
|
KCC runs on Windows, macOS, and Linux.
|
||||||
|
|
||||||
If your source are super high resolution DRM-free PDFs from Kodansha/Humble Bundle/Fanatical,
|
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
|
||||||
you'll need to first [convert the PDFs to CBZ](https://github.com/ciromattia/kcc/issues/680) for use in KCC.
|
|
||||||
|
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
|
||||||
|
|
||||||
|
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
|
||||||
|
|
||||||
|
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
|
||||||
|
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
||||||
|
for optimal compatibility with your device's native PDF reader.
|
||||||
|
|
||||||
|
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
|
||||||
|
which can be directly converted by KCC.
|
||||||
|
|
||||||
Its main feature is various optional image processing steps to look good on eink screens,
|
Its main feature is various optional image processing steps to look good on eink screens,
|
||||||
which have different requirements than normal LCD screens.
|
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.
|
||||||
@@ -30,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:
|
||||||
|
|
||||||
@@ -42,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.
|
||||||
@@ -88,19 +102,32 @@ 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)
|
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
|
||||||
|
|
||||||
|
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
|
||||||
|
|
||||||
The `c2e` and `c2p` versions are command line tools for power users.
|
The `c2e` and `c2p` versions are command line tools for power users.
|
||||||
|
|
||||||
On Windows 11, you may need to run in compatibility mode for an older Windows version.
|
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
|
||||||
|
|
||||||
On Mac, right click open to get past the security warning.
|
|
||||||
|
|
||||||
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?
|
||||||
|
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
|
||||||
|
Viewing KCC output in Calibre will also not work properly.
|
||||||
|
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
|
||||||
|
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
|
||||||
|
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
|
||||||
|
- 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.
|
||||||
|
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
||||||
|
- What output format should I use?
|
||||||
|
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
|
||||||
- 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?
|
||||||
@@ -111,11 +138,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
|||||||
- 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)
|
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
|
||||||
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
|
|
||||||
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
|
|
||||||
- Image too dark?
|
- 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
|
- 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
|
||||||
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
|
|
||||||
- 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.
|
||||||
|
|
||||||
@@ -163,38 +187,44 @@ 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),
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
|
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
|
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
|
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
|
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
|
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
|
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
|
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
|
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
|
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
|
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
|
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
|
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
|
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
|
||||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
|
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
|
||||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
|
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
|
||||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
'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:
|
||||||
@@ -217,13 +247,18 @@ MAIN:
|
|||||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||||
|
|
||||||
PROCESSING:
|
PROCESSING:
|
||||||
-n, --noprocessing Do not modify image and ignore any profil or processing option
|
-n, --noprocessing Do not modify image and ignore any profile or processing option
|
||||||
|
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
|
||||||
|
--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
|
||||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||||
-g GAMMA, --gamma GAMMA
|
-g GAMMA, --gamma GAMMA
|
||||||
Apply gamma correction to linearize the image [Default=Auto]
|
Apply gamma correction to linearize the image [Default=Auto]
|
||||||
|
--autolevel Set most common dark pixel value to be black point for leveling.
|
||||||
|
--noautocontrast Disable autocontrast
|
||||||
|
--colorautocontrast Force autocontrast for all pages. Skipped when near blacks and whites don't exist
|
||||||
-c CROPPING, --cropping CROPPING
|
-c CROPPING, --cropping CROPPING
|
||||||
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
||||||
--cp CROPPINGP, --croppingpower CROPPINGP
|
--cp CROPPINGP, --croppingpower CROPPINGP
|
||||||
@@ -235,9 +270,16 @@ 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.
|
||||||
|
|
||||||
@@ -246,18 +288,20 @@ OUTPUT SETTINGS:
|
|||||||
Output generated file to specified directory or file
|
Output generated file to specified directory or file
|
||||||
-t TITLE, --title TITLE
|
-t TITLE, --title TITLE
|
||||||
Comic title [Default=filename or directory name]
|
Comic title [Default=filename or directory name]
|
||||||
--comicinfotitle Write title from ComicInfo.xml
|
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
|
||||||
-a AUTHOR, --author AUTHOR
|
-a AUTHOR, --author AUTHOR
|
||||||
Author name [Default=KCC]
|
Author name [Default=KCC]
|
||||||
-f FORMAT, --format FORMAT
|
-f FORMAT, --format FORMAT
|
||||||
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto]
|
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
|
||||||
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
||||||
-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
|
||||||
--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.
|
||||||
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
|
--filefusion Combines all input files into a single file.
|
||||||
|
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||||
|
|
||||||
CUSTOM PROFILE:
|
CUSTOM PROFILE:
|
||||||
--customwidth CUSTOMWIDTH
|
--customwidth CUSTOMWIDTH
|
||||||
@@ -300,6 +344,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
|
||||||
@@ -396,7 +441,7 @@ 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.
|
* During startup - Version check and announcement check.
|
||||||
* When error occurs - Automatic reporting on Windows and macOS.
|
* When error occurs - Automatic reporting on Windows and macOS.
|
||||||
|
|
||||||
## KNOWN ISSUES
|
## KNOWN ISSUES
|
||||||
@@ -405,3 +450,6 @@ Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
|||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
|
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
|
||||||
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
|
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180
|
||||||
|
|||||||
22
entrypoint.sh
Normal file
22
entrypoint.sh
Normal file
@@ -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
|
|
||||||
@@ -27,6 +27,10 @@
|
|||||||
<file>../icons/convert.png</file>
|
<file>../icons/convert.png</file>
|
||||||
<file>../icons/document_new.png</file>
|
<file>../icons/document_new.png</file>
|
||||||
<file>../icons/folder_new.png</file>
|
<file>../icons/folder_new.png</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="Brand">
|
||||||
<file>../icons/kofi_symbol.png</file>
|
<file>../icons/kofi_symbol.png</file>
|
||||||
|
<file>../icons/Humble_H-Red.png</file>
|
||||||
|
<file>../icons/Bindle_Red.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
1365
gui/KCC.ui
1365
gui/KCC.ui
File diff suppressed because it is too large
Load Diff
@@ -62,56 +62,66 @@
|
|||||||
<item row="1" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QLineEdit" name="volumeLine"/>
|
<widget class="QLineEdit" name="volumeLine"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" 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">
|
||||||
<string>Number:</string>
|
<string>Number:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QLineEdit" name="numberLine"/>
|
<widget class="QLineEdit" name="numberLine"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Writer:</string>
|
<string>Writer:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QLineEdit" name="writerLine"/>
|
<widget class="QLineEdit" name="writerLine"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="label_5">
|
<widget class="QLabel" name="label_5">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Penciller:</string>
|
<string>Penciller:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QLineEdit" name="pencillerLine"/>
|
<widget class="QLineEdit" name="pencillerLine"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="label_6">
|
<widget class="QLabel" name="label_6">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Inker:</string>
|
<string>Inker:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QLineEdit" name="inkerLine"/>
|
<widget class="QLineEdit" name="inkerLine"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLabel" name="label_7">
|
<widget class="QLabel" name="label_7">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Colorist:</string>
|
<string>Colorist:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="QLineEdit" name="coloristLine"/>
|
<widget class="QLineEdit" name="coloristLine"/>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>Title:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="titleLine"/>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -182,6 +192,18 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<tabstops>
|
||||||
|
<tabstop>seriesLine</tabstop>
|
||||||
|
<tabstop>volumeLine</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>
|
||||||
|
|||||||
BIN
icons/Bindle_Red.png
Normal file
BIN
icons/Bindle_Red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
BIN
icons/Humble_H-Red.png
Normal file
BIN
icons/Humble_H-Red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -17,11 +17,12 @@
|
|||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
import itertools
|
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, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
|
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
|
||||||
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -41,7 +42,7 @@ 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
|
||||||
@@ -124,7 +125,7 @@ class Icons:
|
|||||||
self.EPUBFormat = QIcon()
|
self.EPUBFormat = QIcon()
|
||||||
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.KFXFormat = QIcon()
|
self.KFXFormat = QIcon()
|
||||||
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off)
|
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
self.info = QIcon()
|
self.info = QIcon()
|
||||||
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
@@ -136,6 +137,15 @@ class Icons:
|
|||||||
self.programIcon = QIcon()
|
self.programIcon = QIcon()
|
||||||
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
|
self.kofi = QIcon()
|
||||||
|
self.kofi.addPixmap(QPixmap(":/Brand/icons/kofi_symbol.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
|
self.humble = QIcon()
|
||||||
|
self.humble.addPixmap(QPixmap(":/Brand/icons/Humble_H-Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
|
self.bindle = QIcon()
|
||||||
|
self.bindle.addPixmap(QPixmap(":/Brand/icons/Bindle_Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
|
|
||||||
class VersionThread(QThread):
|
class VersionThread(QThread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -150,6 +160,8 @@ class VersionThread(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
# unauthenticated API requests limit is 60 req/hour
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
|
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
|
||||||
|
|
||||||
html_url = json_parser["html_url"]
|
html_url = json_parser["html_url"]
|
||||||
@@ -162,7 +174,38 @@ class VersionThread(QThread):
|
|||||||
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
|
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
|
||||||
False)
|
False)
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
announcements = requests.get('https://api.github.com/repos/axu2/kcc-messages/contents/links.json',
|
||||||
|
headers={
|
||||||
|
'Accept': 'application/vnd.github.raw+json',
|
||||||
|
'X-GitHub-Api-Version': '2022-11-28'}).json()
|
||||||
|
for category, payloads in announcements.items():
|
||||||
|
for payload in payloads:
|
||||||
|
expiration = datetime.fromisoformat(payload['expiration'])
|
||||||
|
if expiration < datetime.now(timezone.utc):
|
||||||
|
continue
|
||||||
|
delta = expiration - datetime.now(timezone.utc)
|
||||||
|
time_left = f"{delta.days} day(s) left"
|
||||||
|
icon = 'info'
|
||||||
|
if category == 'humbleMangaBundles':
|
||||||
|
icon = 'humble'
|
||||||
|
if category == 'humbleComicBundles':
|
||||||
|
icon = 'bindle'
|
||||||
|
if category == 'kofi':
|
||||||
|
icon = 'kofi'
|
||||||
|
message = f"{payload.get('name')}"
|
||||||
|
if payload.get('link'):
|
||||||
|
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
|
||||||
|
if payload.get('showDeadline'):
|
||||||
|
message += f': {time_left}'
|
||||||
|
if category == 'humbleBundles':
|
||||||
|
message += ' [referral]'
|
||||||
|
MW.addMessage.emit(message, icon , False)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
def setAnswer(self, dialoganswer):
|
def setAnswer(self, dialoganswer):
|
||||||
self.answer = dialoganswer
|
self.answer = dialoganswer
|
||||||
@@ -247,11 +290,29 @@ class WorkerThread(QThread):
|
|||||||
options.upscale = True
|
options.upscale = True
|
||||||
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
||||||
options.gamma = float(GUI.gammaValue)
|
options.gamma = float(GUI.gammaValue)
|
||||||
options.cropping = GUI.croppingBox.checkState().value
|
if GUI.autoLevelBox.isChecked():
|
||||||
|
options.autolevel = True
|
||||||
|
if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
|
options.noautocontrast = True
|
||||||
|
elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
|
||||||
|
options.colorautocontrast = True
|
||||||
|
if GUI.croppingBox.isChecked():
|
||||||
|
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
|
options.cropping = 1
|
||||||
|
else:
|
||||||
|
options.cropping = 2
|
||||||
|
else:
|
||||||
|
options.cropping = 0
|
||||||
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
||||||
options.croppingp = float(GUI.croppingPowerValue)
|
options.croppingp = float(GUI.croppingPowerValue)
|
||||||
options.preservemargin = GUI.preserveMarginBox.value()
|
options.preservemargin = GUI.preserveMarginBox.value()
|
||||||
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
|
if GUI.interPanelCropBox.isChecked():
|
||||||
|
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
|
options.interpanelcrop = 1
|
||||||
|
else:
|
||||||
|
options.interpanelcrop = 2
|
||||||
|
else:
|
||||||
|
options.interpanelcrop = 0
|
||||||
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.white_borders = True
|
options.white_borders = True
|
||||||
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
||||||
@@ -260,14 +321,24 @@ class WorkerThread(QThread):
|
|||||||
options.batchsplit = 2
|
options.batchsplit = 2
|
||||||
if GUI.colorBox.isChecked():
|
if GUI.colorBox.isChecked():
|
||||||
options.forcecolor = True
|
options.forcecolor = True
|
||||||
if GUI.reduceRainbowBox.isChecked():
|
if GUI.eraseRainbowBox.isChecked():
|
||||||
options.reducerainbow = True
|
options.eraserainbow = True
|
||||||
if GUI.maximizeStrips.isChecked():
|
if GUI.maximizeStrips.isChecked():
|
||||||
options.maximizestrips = True
|
options.maximizestrips = True
|
||||||
if GUI.disableProcessingBox.isChecked():
|
if GUI.disableProcessingBox.isChecked():
|
||||||
options.noprocessing = True
|
options.noprocessing = True
|
||||||
if GUI.comicinfoTitleBox.isChecked():
|
if GUI.pdfExtractBox.isChecked():
|
||||||
options.comicinfotitle = True
|
options.pdfextract = 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:
|
||||||
|
options.metadatatitle = 1
|
||||||
|
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
|
||||||
|
options.metadatatitle = 2
|
||||||
if GUI.deleteBox.isChecked():
|
if GUI.deleteBox.isChecked():
|
||||||
options.delete = True
|
options.delete = True
|
||||||
if GUI.spreadShiftBox.isChecked():
|
if GUI.spreadShiftBox.isChecked():
|
||||||
@@ -278,17 +349,31 @@ class WorkerThread(QThread):
|
|||||||
options.filefusion = False
|
options.filefusion = False
|
||||||
if GUI.noRotateBox.isChecked():
|
if GUI.noRotateBox.isChecked():
|
||||||
options.norotate = True
|
options.norotate = True
|
||||||
|
if GUI.rotateRightBox.isChecked():
|
||||||
|
options.rotateright = True
|
||||||
if GUI.rotateFirstBox.isChecked():
|
if GUI.rotateFirstBox.isChecked():
|
||||||
options.rotatefirst = True
|
options.rotatefirst = True
|
||||||
|
if GUI.forcePngRgbBox.isChecked():
|
||||||
|
options.force_png_rgb = True
|
||||||
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.forcepng = True
|
options.forcepng = True
|
||||||
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
||||||
options.mozjpeg = True
|
options.mozjpeg = True
|
||||||
|
if GUI.webpBox.isChecked():
|
||||||
|
options.webp = True
|
||||||
|
if GUI.pngLegacyBox.isChecked():
|
||||||
|
options.pnglegacy = True
|
||||||
|
if GUI.noQuantizeBox.isChecked():
|
||||||
|
options.noquantize = True
|
||||||
|
if GUI.jpegQualityBox.isChecked():
|
||||||
|
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())
|
||||||
if GUI.targetDirectory != '':
|
if GUI.targetDirectory != '':
|
||||||
options.output = GUI.targetDirectory
|
options.output = GUI.targetDirectory
|
||||||
|
if GUI.titleEdit.text():
|
||||||
|
options.title = str(GUI.titleEdit.text())
|
||||||
if GUI.authorEdit.text():
|
if GUI.authorEdit.text():
|
||||||
options.author = str(GUI.authorEdit.text())
|
options.author = str(GUI.authorEdit.text())
|
||||||
if GUI.chunkSizeCheckBox.isChecked():
|
if GUI.chunkSizeCheckBox.isChecked():
|
||||||
@@ -312,16 +397,25 @@ class WorkerThread(QThread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Fusion Failed. ' + str(e))
|
print('Fusion Failed. ' + str(e))
|
||||||
MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True)
|
MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True)
|
||||||
for job in currentJobs:
|
elif len(currentJobs) > 1 and options.title != 'defaulttitle':
|
||||||
|
currentJobs.clear()
|
||||||
|
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)
|
||||||
|
MW.addMessage.emit(error_message, 'error', True)
|
||||||
|
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'
|
||||||
|
elif gui_current_format == 'PDF':
|
||||||
|
MW.addMessage.emit('Creating PDF files', 'info', False)
|
||||||
|
GUI.progress.content = 'Creating PDF files'
|
||||||
else:
|
else:
|
||||||
MW.addMessage.emit('Creating EPUB files', 'info', False)
|
MW.addMessage.emit('Creating EPUB files', 'info', False)
|
||||||
GUI.progress.content = 'Creating EPUB files'
|
GUI.progress.content = 'Creating EPUB files'
|
||||||
@@ -329,7 +423,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:
|
||||||
@@ -366,10 +460,12 @@ class WorkerThread(QThread):
|
|||||||
GUI.progress.content = ''
|
GUI.progress.content = ''
|
||||||
if gui_current_format == 'CBZ':
|
if gui_current_format == 'CBZ':
|
||||||
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
|
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
|
||||||
|
elif gui_current_format == 'PDF':
|
||||||
|
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
|
||||||
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)
|
||||||
@@ -419,7 +515,9 @@ class WorkerThread(QThread):
|
|||||||
k = kindle.Kindle(options.profile)
|
k = kindle.Kindle(options.profile)
|
||||||
if k.path and k.coverSupport:
|
if k.path and k.coverSupport:
|
||||||
for item in outputPath:
|
for item in outputPath:
|
||||||
comic2ebook.options.covers[outputPath.index(item)][0].saveToKindle(
|
cover = comic2ebook.options.covers[outputPath.index(item)][0]
|
||||||
|
if cover:
|
||||||
|
cover.saveToKindle(
|
||||||
k, comic2ebook.options.covers[outputPath.index(item)][1])
|
k, comic2ebook.options.covers[outputPath.index(item)][1])
|
||||||
MW.addMessage.emit('Kindle detected. Uploading covers... <b>Done!</b>', 'info', False)
|
MW.addMessage.emit('Kindle detected. Uploading covers... <b>Done!</b>', 'info', False)
|
||||||
else:
|
else:
|
||||||
@@ -441,11 +539,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:
|
||||||
@@ -462,7 +561,7 @@ class WorkerThread(QThread):
|
|||||||
if os.path.isfile(path):
|
if os.path.isfile(path):
|
||||||
os.remove(path)
|
os.remove(path)
|
||||||
elif os.path.isdir(path):
|
elif os.path.isdir(path):
|
||||||
rmtree(path)
|
rmtree(path, True)
|
||||||
GUI.progress.content = ''
|
GUI.progress.content = ''
|
||||||
GUI.progress.stop()
|
GUI.progress.stop()
|
||||||
MW.hideProgressBar.emit()
|
MW.hideProgressBar.emit()
|
||||||
@@ -532,21 +631,37 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'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 selectFileMetaEditor(self):
|
def selectDir(self):
|
||||||
sname = ''
|
if self.needClean:
|
||||||
|
self.needClean = False
|
||||||
|
GUI.jobList.clear()
|
||||||
|
|
||||||
|
dialog = QFileDialog(MW, 'Select input folder(s)', self.lastPath)
|
||||||
|
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
||||||
|
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
||||||
|
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
|
||||||
|
dialog.findChild(QTreeView).setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||||
|
|
||||||
|
if dialog.exec():
|
||||||
|
dnames = dialog.selectedFiles()
|
||||||
|
for dname in dnames:
|
||||||
|
if dname != '':
|
||||||
|
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
|
||||||
|
GUI.jobList.addItem(dname)
|
||||||
|
GUI.jobList.scrollToBottom()
|
||||||
|
|
||||||
|
|
||||||
|
def selectFileMetaEditor(self, sname):
|
||||||
|
if not sname:
|
||||||
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
|
||||||
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
||||||
if dname != '':
|
if dname != '':
|
||||||
sname = os.path.join(dname, 'ComicInfo.xml')
|
sname = os.path.join(dname, 'ComicInfo.xml')
|
||||||
if sys.platform.startswith('win'):
|
self.lastPath = os.path.dirname(sname)
|
||||||
sname = sname.replace('/', '\\')
|
|
||||||
self.lastPath = os.path.abspath(sname)
|
|
||||||
else:
|
else:
|
||||||
if self.sevenzip:
|
if self.sevenzip:
|
||||||
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
|
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
|
||||||
@@ -557,12 +672,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
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] != '':
|
if fname[0] != '':
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
sname = fname[0].replace('/', '\\')
|
|
||||||
else:
|
|
||||||
sname = fname[0]
|
sname = fname[0]
|
||||||
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
|
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
|
||||||
if sname != '':
|
if sname:
|
||||||
try:
|
try:
|
||||||
self.editor.loadData(sname)
|
self.editor.loadData(sname)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -584,6 +696,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
|
||||||
@@ -651,26 +775,50 @@ 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('Try reading webtoon panels side by side in landscape!', 'info')
|
||||||
GUI.qualityBox.setEnabled(False)
|
GUI.qualityBox.setEnabled(False)
|
||||||
GUI.qualityBox.setChecked(False)
|
GUI.qualityBox.setChecked(False)
|
||||||
GUI.mangaBox.setEnabled(False)
|
GUI.mangaBox.setEnabled(False)
|
||||||
GUI.mangaBox.setChecked(False)
|
GUI.mangaBox.setChecked(False)
|
||||||
GUI.rotateBox.setEnabled(False)
|
GUI.rotateBox.setEnabled(False)
|
||||||
GUI.rotateBox.setChecked(False)
|
GUI.rotateBox.setChecked(False)
|
||||||
|
GUI.borderBox.setEnabled(False)
|
||||||
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
GUI.upscaleBox.setEnabled(False)
|
GUI.upscaleBox.setEnabled(False)
|
||||||
GUI.upscaleBox.setChecked(True)
|
GUI.upscaleBox.setChecked(False)
|
||||||
GUI.chunkSizeCheckBox.setEnabled(False)
|
GUI.croppingBox.setEnabled(False)
|
||||||
GUI.chunkSizeCheckBox.setChecked(False)
|
GUI.croppingBox.setChecked(False)
|
||||||
|
GUI.interPanelCropBox.setEnabled(False)
|
||||||
|
GUI.interPanelCropBox.setChecked(False)
|
||||||
|
GUI.autoLevelBox.setEnabled(False)
|
||||||
|
GUI.autoLevelBox.setChecked(False)
|
||||||
|
GUI.autocontrastBox.setEnabled(False)
|
||||||
|
GUI.autocontrastBox.setChecked(False)
|
||||||
else:
|
else:
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
if profile['PVOptions']:
|
if profile['PVOptions']:
|
||||||
GUI.qualityBox.setEnabled(True)
|
GUI.qualityBox.setEnabled(True)
|
||||||
GUI.mangaBox.setEnabled(True)
|
GUI.mangaBox.setEnabled(True)
|
||||||
GUI.rotateBox.setEnabled(True)
|
GUI.rotateBox.setEnabled(True)
|
||||||
|
GUI.borderBox.setEnabled(True)
|
||||||
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
|
if not profile['Label'].startswith('KS'):
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
GUI.chunkSizeCheckBox.setEnabled(True)
|
GUI.croppingBox.setEnabled(True)
|
||||||
|
GUI.interPanelCropBox.setEnabled(True)
|
||||||
|
GUI.autoLevelBox.setEnabled(True)
|
||||||
|
GUI.autocontrastBox.setEnabled(True)
|
||||||
|
GUI.autocontrastBox.setChecked(True)
|
||||||
|
|
||||||
|
|
||||||
def togglequalityBox(self, value):
|
def togglequalityBox(self, value):
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
@@ -685,9 +833,49 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||||
|
|
||||||
|
def toggleImageFormatBox(self, value):
|
||||||
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
|
if value == 1:
|
||||||
|
if profile['Label'].startswith('KS'):
|
||||||
|
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||||
|
for bad_format in ('MOBI', 'EPUB'):
|
||||||
|
if bad_format in current_format:
|
||||||
|
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
|
||||||
|
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)
|
||||||
|
|
||||||
|
def toggletitleEdit(self, value):
|
||||||
|
if value:
|
||||||
|
self.metadataTitleBox.setChecked(False)
|
||||||
|
|
||||||
|
def togglefileFusionBox(self, value):
|
||||||
|
if value:
|
||||||
|
GUI.metadataTitleBox.setChecked(False)
|
||||||
|
GUI.metadataTitleBox.setEnabled(False)
|
||||||
|
else:
|
||||||
|
GUI.metadataTitleBox.setEnabled(True)
|
||||||
|
|
||||||
|
def togglemetadataTitleBox(self, value):
|
||||||
|
if value:
|
||||||
|
GUI.titleEdit.setText(None)
|
||||||
|
|
||||||
|
def editSourceMetadata(self, item):
|
||||||
|
if item.icon().isNull():
|
||||||
|
sname = item.text()
|
||||||
|
if os.path.isdir(sname):
|
||||||
|
sname = os.path.join(sname, "ComicInfo.xml")
|
||||||
|
self.selectFileMetaEditor(sname)
|
||||||
|
|
||||||
def changeGamma(self, value):
|
def changeGamma(self, value):
|
||||||
valueRaw = int(5 * round(float(value) / 5))
|
valueRaw = int(5 * round(float(value) / 5))
|
||||||
value = '%.2f' % (float(valueRaw) / 100)
|
value = '%.2f' % (float(valueRaw) / 100)
|
||||||
@@ -715,15 +903,24 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.modeChange(1)
|
self.modeChange(1)
|
||||||
GUI.colorBox.setChecked(profile['ForceColor'])
|
GUI.colorBox.setChecked(profile['ForceColor'])
|
||||||
self.changeFormat()
|
self.changeFormat()
|
||||||
GUI.gammaSlider.setValue(0)
|
|
||||||
self.changeGamma(0)
|
|
||||||
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'):
|
||||||
GUI.upscaleBox.setDisabled(True)
|
GUI.upscaleBox.setDisabled(True)
|
||||||
else:
|
else:
|
||||||
|
if not GUI.webtoonBox.isChecked():
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
|
if profile['Label'] == 'KCS':
|
||||||
|
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||||
|
for bad_format in ('MOBI', 'EPUB'):
|
||||||
|
if bad_format in current_format:
|
||||||
|
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
|
||||||
|
break
|
||||||
|
elif profile['Label'] == 'KDX':
|
||||||
|
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
|
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':
|
||||||
@@ -749,6 +946,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.chunkSizeCheckBox.setChecked(False)
|
GUI.chunkSizeCheckBox.setChecked(False)
|
||||||
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')
|
||||||
|
|
||||||
def stripTags(self, html):
|
def stripTags(self, html):
|
||||||
s = HTMLStripper()
|
s = HTMLStripper()
|
||||||
@@ -859,34 +1058,47 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
|
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
|
||||||
self.settings.setValue('startNumber', self.startNumber + 1)
|
self.settings.setValue('startNumber', self.startNumber + 1)
|
||||||
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
|
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
|
||||||
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState().value,
|
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
|
||||||
'rotateBox': GUI.rotateBox.checkState().value,
|
'rotateBox': GUI.rotateBox.checkState(),
|
||||||
'qualityBox': GUI.qualityBox.checkState().value,
|
'qualityBox': GUI.qualityBox.checkState(),
|
||||||
'gammaBox': GUI.gammaBox.checkState().value,
|
'gammaBox': GUI.gammaBox.checkState(),
|
||||||
'croppingBox': GUI.croppingBox.checkState().value,
|
'autoLevelBox': GUI.autoLevelBox.checkState(),
|
||||||
|
'autocontrastBox': GUI.autocontrastBox.checkState(),
|
||||||
|
'croppingBox': GUI.croppingBox.checkState(),
|
||||||
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
||||||
'preserveMarginBox': self.preserveMarginBox.value(),
|
'preserveMarginBox': self.preserveMarginBox.value(),
|
||||||
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
|
'interPanelCropBox': GUI.interPanelCropBox.checkState(),
|
||||||
'upscaleBox': GUI.upscaleBox.checkState().value,
|
'upscaleBox': GUI.upscaleBox.checkState(),
|
||||||
'borderBox': GUI.borderBox.checkState().value,
|
'borderBox': GUI.borderBox.checkState(),
|
||||||
'webtoonBox': GUI.webtoonBox.checkState().value,
|
'webtoonBox': GUI.webtoonBox.checkState(),
|
||||||
'outputSplit': GUI.outputSplit.checkState().value,
|
'outputSplit': GUI.outputSplit.checkState(),
|
||||||
'colorBox': GUI.colorBox.checkState().value,
|
'colorBox': GUI.colorBox.checkState(),
|
||||||
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
|
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||||
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
|
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||||
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
|
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||||
'mozJpegBox': GUI.mozJpegBox.checkState().value,
|
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||||
|
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
||||||
|
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||||
|
'metadataTitleBox': GUI.metadataTitleBox.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().value,
|
'deleteBox': GUI.deleteBox.checkState(),
|
||||||
'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
|
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
||||||
'fileFusionBox': GUI.fileFusionBox.checkState().value,
|
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value,
|
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||||
'noRotateBox': GUI.noRotateBox.checkState().value,
|
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||||
'rotateFirstBox': GUI.rotateFirstBox.checkState().value,
|
'rotateRightBox': GUI.rotateRightBox.checkState(),
|
||||||
'maximizeStrips': GUI.maximizeStrips.checkState().value,
|
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
||||||
|
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
||||||
'gammaSlider': float(self.gammaValue) * 100,
|
'gammaSlider': float(self.gammaValue) * 100,
|
||||||
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
|
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState(),
|
||||||
'chunkSizeBox': GUI.chunkSizeBox.value()})
|
'chunkSizeBox': GUI.chunkSizeBox.value()})
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.tray.hide()
|
self.tray.hide()
|
||||||
@@ -896,7 +1108,7 @@ 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()
|
||||||
@@ -927,6 +1139,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)
|
||||||
@@ -961,7 +1175,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.setupUi(MW)
|
self.setupUi(MW)
|
||||||
self.editor = KCCGUI_MetaEditor()
|
self.editor = KCCGUI_MetaEditor()
|
||||||
self.icons = Icons()
|
self.icons = Icons()
|
||||||
self.settings = QSettings('ciromattia', 'kcc')
|
self.settings = QSettings('ciromattia', 'kcc9')
|
||||||
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
|
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
|
||||||
self.lastPath = self.settings.value('lastPath', '', type=str)
|
self.lastPath = self.settings.value('lastPath', '', type=str)
|
||||||
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
|
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
|
||||||
@@ -971,7 +1185,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
|
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
|
||||||
self.startNumber = self.settings.value('startNumber', 0, type=int)
|
self.startNumber = self.settings.value('startNumber', 0, type=int)
|
||||||
self.windowSize = self.settings.value('windowSize', '0x0', type=str)
|
self.windowSize = self.settings.value('windowSize', '0x0', type=str)
|
||||||
self.options = self.settings.value('options', {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100})
|
default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}
|
||||||
|
try:
|
||||||
|
self.options = self.settings.value('options', default_options)
|
||||||
|
except Exception:
|
||||||
|
self.options = default_options
|
||||||
self.worker = WorkerThread()
|
self.worker = WorkerThread()
|
||||||
self.versionCheck = VersionThread()
|
self.versionCheck = VersionThread()
|
||||||
self.progress = ProgressThread()
|
self.progress = ProgressThread()
|
||||||
@@ -999,7 +1217,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)
|
||||||
@@ -1008,6 +1226,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
|
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
|
||||||
"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 (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
||||||
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
||||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||||
@@ -1024,9 +1244,24 @@ 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 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': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
|
||||||
|
},
|
||||||
|
"Kindle Scribe Colorsoft": {
|
||||||
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, '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',
|
||||||
},
|
},
|
||||||
@@ -1037,7 +1272,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
|
||||||
},
|
},
|
||||||
"Kindle Colorsoft": {
|
"Kindle Colorsoft": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
|
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
|
||||||
},
|
},
|
||||||
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
||||||
@@ -1089,19 +1324,23 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'Label': 'KoS'},
|
'Label': 'KoS'},
|
||||||
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'KoE'},
|
'Label': 'KoE'},
|
||||||
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'Rmk1'},
|
'Label': 'Rmk1'},
|
||||||
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'Rmk2'},
|
'Label': 'Rmk2'},
|
||||||
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True,
|
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
|
||||||
'Label': 'RmkPP'},
|
'Label': 'RmkPP'},
|
||||||
|
"reMarkable Paper Pro Move": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
|
||||||
|
'Label': 'RmkPPMove'},
|
||||||
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
|
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
|
||||||
'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",
|
||||||
@@ -1117,9 +1356,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"reMarkable 1",
|
"reMarkable 1",
|
||||||
"reMarkable 2",
|
"reMarkable 2",
|
||||||
"reMarkable Paper Pro",
|
"reMarkable Paper Pro",
|
||||||
|
"reMarkable Paper Pro Move",
|
||||||
"Separator",
|
"Separator",
|
||||||
"Other",
|
"Other",
|
||||||
"Separator",
|
"Separator",
|
||||||
|
"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",
|
||||||
@@ -1160,7 +1403,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
statusBarLabel.setOpenExternalLinks(True)
|
statusBarLabel.setOpenExternalLinks(True)
|
||||||
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
|
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
|
||||||
|
|
||||||
self.addMessage('<b>Welcome!</b>', 'info')
|
|
||||||
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
|
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
|
||||||
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
|
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
|
||||||
if self.startNumber < 5:
|
if self.startNumber < 5:
|
||||||
@@ -1168,7 +1410,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>'
|
||||||
@@ -1179,19 +1421,29 @@ 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.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
|
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
|
||||||
GUI.deviceBox.activated.connect(self.changeDevice)
|
GUI.deviceBox.activated.connect(self.changeDevice)
|
||||||
GUI.formatBox.activated.connect(self.changeFormat)
|
GUI.formatBox.activated.connect(self.changeFormat)
|
||||||
|
GUI.titleEdit.textChanged.connect(self.toggletitleEdit)
|
||||||
|
GUI.fileFusionBox.stateChanged.connect(self.togglefileFusionBox)
|
||||||
|
GUI.metadataTitleBox.stateChanged.connect(self.togglemetadataTitleBox)
|
||||||
|
GUI.jobList.itemDoubleClicked.connect(self.editSourceMetadata)
|
||||||
MW.progressBarTick.connect(self.updateProgressbar)
|
MW.progressBarTick.connect(self.updateProgressbar)
|
||||||
MW.modeConvert.connect(self.modeConvert)
|
MW.modeConvert.connect(self.modeConvert)
|
||||||
MW.addMessage.connect(self.addMessage)
|
MW.addMessage.connect(self.addMessage)
|
||||||
@@ -1243,6 +1495,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:
|
||||||
@@ -1280,15 +1534,17 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
|||||||
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):
|
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
||||||
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
||||||
if self.seriesLine.text() == '':
|
for field in (self.seriesLine, self.titleLine):
|
||||||
|
if field.text() == '':
|
||||||
|
path = Path(file)
|
||||||
if file.endswith('.xml'):
|
if file.endswith('.xml'):
|
||||||
self.seriesLine.setText(file.split('\\')[-2])
|
field.setText(path.parent.name)
|
||||||
else:
|
else:
|
||||||
self.seriesLine.setText(file.split('\\')[-1].split('/')[-1].split('.')[0])
|
field.setText(path.stem)
|
||||||
|
|
||||||
def saveData(self):
|
def saveData(self):
|
||||||
for field in (self.volumeLine, self.numberLine):
|
for field in (self.volumeLine, self.numberLine):
|
||||||
@@ -1298,7 +1554,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
|||||||
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
self.parser.data['Series'] = self.cleanData(self.seriesLine.text())
|
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):
|
||||||
values = self.cleanData(field.text()).split(',')
|
values = self.cleanData(field.text()).split(',')
|
||||||
tmpData = []
|
tmpData = []
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'MetaEditor.ui'
|
## Form generated from reading UI file 'MetaEditor.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.9.1
|
## Created by: Qt User Interface Compiler version 6.9.3
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -60,52 +60,62 @@ class Ui_editorDialog(object):
|
|||||||
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")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
|
self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
|
||||||
|
|
||||||
self.numberLine = QLineEdit(self.editorWidget)
|
self.numberLine = QLineEdit(self.editorWidget)
|
||||||
self.numberLine.setObjectName(u"numberLine")
|
self.numberLine.setObjectName(u"numberLine")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.numberLine, 2, 1, 1, 1)
|
self.gridLayout.addWidget(self.numberLine, 3, 1, 1, 1)
|
||||||
|
|
||||||
self.label_4 = QLabel(self.editorWidget)
|
self.label_4 = QLabel(self.editorWidget)
|
||||||
self.label_4.setObjectName(u"label_4")
|
self.label_4.setObjectName(u"label_4")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1)
|
self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1)
|
||||||
|
|
||||||
self.writerLine = QLineEdit(self.editorWidget)
|
self.writerLine = QLineEdit(self.editorWidget)
|
||||||
self.writerLine.setObjectName(u"writerLine")
|
self.writerLine.setObjectName(u"writerLine")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.writerLine, 3, 1, 1, 1)
|
self.gridLayout.addWidget(self.writerLine, 4, 1, 1, 1)
|
||||||
|
|
||||||
self.label_5 = QLabel(self.editorWidget)
|
self.label_5 = QLabel(self.editorWidget)
|
||||||
self.label_5.setObjectName(u"label_5")
|
self.label_5.setObjectName(u"label_5")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.label_5, 4, 0, 1, 1)
|
self.gridLayout.addWidget(self.label_5, 5, 0, 1, 1)
|
||||||
|
|
||||||
self.pencillerLine = QLineEdit(self.editorWidget)
|
self.pencillerLine = QLineEdit(self.editorWidget)
|
||||||
self.pencillerLine.setObjectName(u"pencillerLine")
|
self.pencillerLine.setObjectName(u"pencillerLine")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.pencillerLine, 4, 1, 1, 1)
|
self.gridLayout.addWidget(self.pencillerLine, 5, 1, 1, 1)
|
||||||
|
|
||||||
self.label_6 = QLabel(self.editorWidget)
|
self.label_6 = QLabel(self.editorWidget)
|
||||||
self.label_6.setObjectName(u"label_6")
|
self.label_6.setObjectName(u"label_6")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1)
|
self.gridLayout.addWidget(self.label_6, 6, 0, 1, 1)
|
||||||
|
|
||||||
self.inkerLine = QLineEdit(self.editorWidget)
|
self.inkerLine = QLineEdit(self.editorWidget)
|
||||||
self.inkerLine.setObjectName(u"inkerLine")
|
self.inkerLine.setObjectName(u"inkerLine")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.inkerLine, 5, 1, 1, 1)
|
self.gridLayout.addWidget(self.inkerLine, 6, 1, 1, 1)
|
||||||
|
|
||||||
self.label_7 = QLabel(self.editorWidget)
|
self.label_7 = QLabel(self.editorWidget)
|
||||||
self.label_7.setObjectName(u"label_7")
|
self.label_7.setObjectName(u"label_7")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1)
|
self.gridLayout.addWidget(self.label_7, 7, 0, 1, 1)
|
||||||
|
|
||||||
self.coloristLine = QLineEdit(self.editorWidget)
|
self.coloristLine = QLineEdit(self.editorWidget)
|
||||||
self.coloristLine.setObjectName(u"coloristLine")
|
self.coloristLine.setObjectName(u"coloristLine")
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.coloristLine, 6, 1, 1, 1)
|
self.gridLayout.addWidget(self.coloristLine, 7, 1, 1, 1)
|
||||||
|
|
||||||
|
self.label_8 = QLabel(self.editorWidget)
|
||||||
|
self.label_8.setObjectName(u"label_8")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1)
|
||||||
|
|
||||||
|
self.titleLine = QLineEdit(self.editorWidget)
|
||||||
|
self.titleLine.setObjectName(u"titleLine")
|
||||||
|
|
||||||
|
self.gridLayout.addWidget(self.titleLine, 2, 1, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
self.verticalLayout.addWidget(self.editorWidget)
|
self.verticalLayout.addWidget(self.editorWidget)
|
||||||
@@ -146,6 +156,15 @@ 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.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)
|
||||||
|
|
||||||
@@ -161,6 +180,7 @@ class Ui_editorDialog(object):
|
|||||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||||
self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None))
|
self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None))
|
||||||
self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None))
|
self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None))
|
||||||
|
self.label_8.setText(QCoreApplication.translate("editorDialog", u"Title:", None))
|
||||||
self.statusLabel.setText("")
|
self.statusLabel.setText("")
|
||||||
self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None))
|
self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None))
|
||||||
self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None))
|
self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '8.0.4'
|
__version__ = '9.7.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'
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from typing import List
|
|||||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||||
from shutil import move, copytree, rmtree, copyfile
|
from shutil import move, copytree, rmtree, copyfile
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool, cpu_count
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from natsort import os_sort_keygen, os_sorted
|
from natsort import os_sort_keygen, os_sorted
|
||||||
from slugify import slugify as slugify_ext
|
from slugify import slugify as slugify_ext
|
||||||
@@ -41,8 +41,9 @@ from pathlib import Path
|
|||||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||||
from psutil import virtual_memory, disk_usage
|
from psutil import virtual_memory, disk_usage
|
||||||
from html import escape as hescape
|
from html import escape as hescape
|
||||||
|
import pymupdf
|
||||||
|
|
||||||
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
|
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
|
||||||
from .comicarchive import SEVENZIP, available_archive_tools
|
from .comicarchive import SEVENZIP, available_archive_tools
|
||||||
from . import comic2panel
|
from . import comic2panel
|
||||||
from . import image
|
from . import image
|
||||||
@@ -65,18 +66,29 @@ def main(argv=None):
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 0
|
return 0
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
sources = set([source for option in options.input for source in glob(escape(option))])
|
sources = [source for option in options.input for source in glob(escape(option))]
|
||||||
else:
|
else:
|
||||||
sources = set(options.input)
|
sources = options.input
|
||||||
if len(sources) == 0:
|
if len(sources) == 0:
|
||||||
print('No matching files found.')
|
print('No matching files found.')
|
||||||
return 1
|
return 1
|
||||||
|
if options.filefusion:
|
||||||
|
fusion_path = makeFusion(list(sources))
|
||||||
|
sources.clear()
|
||||||
|
sources.append(fusion_path)
|
||||||
for source in sources:
|
for source in sources:
|
||||||
source = source.rstrip('\\').rstrip('/')
|
source = source.rstrip('\\').rstrip('/')
|
||||||
options = copy(args)
|
options = copy(args)
|
||||||
options = checkOptions(options)
|
options = checkOptions(options)
|
||||||
print('Working on ' + source + '...')
|
print('Working on ' + source + '...')
|
||||||
makeBook(source)
|
makeBook(source)
|
||||||
|
|
||||||
|
if options.filefusion:
|
||||||
|
for path in sources:
|
||||||
|
if os.path.isfile(path):
|
||||||
|
os.remove(path)
|
||||||
|
elif os.path.isdir(path):
|
||||||
|
rmtree(path, True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -125,9 +137,10 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
|||||||
"</head>\n",
|
"</head>\n",
|
||||||
"<body style=\"" + additionalStyle + "\">\n",
|
"<body style=\"" + additionalStyle + "\">\n",
|
||||||
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
|
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
|
||||||
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
|
||||||
'<div style="display:none;">.</div>\n',
|
|
||||||
])
|
])
|
||||||
|
if options.iskindle:
|
||||||
|
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
||||||
|
f.write('<div style="display:none;">.</div>\n')
|
||||||
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
|
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
|
||||||
if imgfile2:
|
if imgfile2:
|
||||||
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
||||||
@@ -276,7 +289,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
def buildOPF(dstdir, title, filelist, cover=None):
|
def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||||
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
||||||
deviceres = options.profileData[1]
|
deviceres = options.profileData[1]
|
||||||
if options.righttoleft:
|
if options.righttoleft:
|
||||||
@@ -297,8 +310,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
|
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
|
||||||
for author in options.authors:
|
for author in options.authors:
|
||||||
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
|
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
|
||||||
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
|
f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n")
|
||||||
"<meta name=\"cover\" content=\"cover\"/>\n"])
|
if cover:
|
||||||
|
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
|
||||||
if options.iskindle and options.profile != 'Custom':
|
if options.iskindle and options.profile != 'Custom':
|
||||||
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
||||||
"<meta name=\"original-resolution\" content=\"",
|
"<meta name=\"original-resolution\" content=\"",
|
||||||
@@ -339,6 +353,8 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
mt = 'image/png'
|
mt = 'image/png'
|
||||||
elif '.gif' == filename[1]:
|
elif '.gif' == filename[1]:
|
||||||
mt = 'image/gif'
|
mt = 'image/gif'
|
||||||
|
elif '.webp' == filename[1]:
|
||||||
|
mt = 'image/webp'
|
||||||
else:
|
else:
|
||||||
mt = 'image/jpeg'
|
mt = 'image/jpeg'
|
||||||
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
||||||
@@ -364,6 +380,11 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
else:
|
else:
|
||||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
|
if originalpath.lower().endswith('.pdf'):
|
||||||
|
if pageside == "right":
|
||||||
|
pageside = "left"
|
||||||
|
else:
|
||||||
|
pageside = "right"
|
||||||
if options.spreadshift:
|
if options.spreadshift:
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
@@ -438,7 +459,7 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
|||||||
"</container>"])
|
"</container>"])
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len_tomes=0):
|
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, job_progress='', len_tomes=0):
|
||||||
filelist = []
|
filelist = []
|
||||||
chapterlist = []
|
chapterlist = []
|
||||||
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
||||||
@@ -525,7 +546,9 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
|
|||||||
"}\n"])
|
"}\n"])
|
||||||
f.close()
|
f.close()
|
||||||
build_html_start = perf_counter()
|
build_html_start = perf_counter()
|
||||||
|
if cover:
|
||||||
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
|
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
|
||||||
|
dot_clean(path)
|
||||||
options.covers.append((cover, options.uuid))
|
options.covers.append((cover, options.uuid))
|
||||||
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||||
chapter = False
|
chapter = False
|
||||||
@@ -544,12 +567,12 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
|
|||||||
else:
|
else:
|
||||||
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
|
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
|
||||||
build_html_end = perf_counter()
|
build_html_end = perf_counter()
|
||||||
print(f"buildHTML: {build_html_end - build_html_start} seconds")
|
print(f"{job_progress}buildHTML: {build_html_end - build_html_start} seconds")
|
||||||
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
|
# Overwrite chapternames if ComicInfo.xml has bookmarks
|
||||||
if ischunked:
|
if ischunked:
|
||||||
options.comicinfo_chapters = []
|
options.comicinfo_chapters = []
|
||||||
|
|
||||||
if not chapternames and options.comicinfo_chapters:
|
if options.comicinfo_chapters:
|
||||||
chapterlist = []
|
chapterlist = []
|
||||||
|
|
||||||
global_diff = 0
|
global_diff = 0
|
||||||
@@ -577,10 +600,39 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
|
|||||||
chapternames[filename] = aChapter[1]
|
chapternames[filename] = aChapter[1]
|
||||||
buildNCX(path, options.title, chapterlist, chapternames)
|
buildNCX(path, options.title, chapterlist, chapternames)
|
||||||
buildNAV(path, options.title, chapterlist, chapternames)
|
buildNAV(path, options.title, chapterlist, chapternames)
|
||||||
buildOPF(path, options.title, filelist, cover)
|
buildOPF(path, options.title, filelist, originalpath, cover)
|
||||||
|
|
||||||
|
|
||||||
def imgDirectoryProcessing(path):
|
def buildPDF(path, title, job_progress='', cover=None, output_file=None):
|
||||||
|
"""
|
||||||
|
Build a PDF file from processed comic images.
|
||||||
|
Images are combined into a single PDF optimized for e-readers.
|
||||||
|
"""
|
||||||
|
start = perf_counter()
|
||||||
|
# open empty PDF
|
||||||
|
with pymupdf.open() as doc:
|
||||||
|
doc.set_metadata({'title': title, 'author': options.authors[0]})
|
||||||
|
# Stream images to PDF
|
||||||
|
for root, dirs, files in os.walk(os.path.join(path, "OEBPS", "Images")):
|
||||||
|
files.sort(key=OS_SORT_KEY)
|
||||||
|
dirs.sort(key=OS_SORT_KEY)
|
||||||
|
for file in files:
|
||||||
|
w, h = Image.open(os.path.join(root, file)).size
|
||||||
|
page = doc.new_page(width=w, height=h)
|
||||||
|
page.insert_image(page.rect, filename=os.path.join(root, file))
|
||||||
|
|
||||||
|
# determine output filename if not provided
|
||||||
|
if output_file is None:
|
||||||
|
output_file = getOutputFilename(path, None, '.pdf', '')
|
||||||
|
|
||||||
|
# Save with optimizations for smaller file size
|
||||||
|
doc.save(output_file, deflate=True, garbage=4, clean=True)
|
||||||
|
end = perf_counter()
|
||||||
|
print(f"{job_progress}MuPDF output: {end-start} sec")
|
||||||
|
return output_file
|
||||||
|
|
||||||
|
|
||||||
|
def imgDirectoryProcessing(path, job_progress=''):
|
||||||
global workerPool, workerOutput
|
global workerPool, workerOutput
|
||||||
workerPool = Pool(maxtasksperchild=100)
|
workerPool = Pool(maxtasksperchild=100)
|
||||||
workerOutput = []
|
workerOutput = []
|
||||||
@@ -600,7 +652,7 @@ def imgDirectoryProcessing(path):
|
|||||||
workerPool.close()
|
workerPool.close()
|
||||||
workerPool.join()
|
workerPool.join()
|
||||||
img_processing_end = perf_counter()
|
img_processing_end = perf_counter()
|
||||||
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
|
print(f"{job_progress}imgFileProcessing: {img_processing_end - img_processing_start} seconds")
|
||||||
|
|
||||||
# macOS 15 likes to add ._ files after multiprocessing
|
# macOS 15 likes to add ._ files after multiprocessing
|
||||||
dot_clean(path)
|
dot_clean(path)
|
||||||
@@ -610,10 +662,10 @@ def imgDirectoryProcessing(path):
|
|||||||
raise UserWarning("Conversion interrupted.")
|
raise UserWarning("Conversion interrupted.")
|
||||||
if len(workerOutput) > 0:
|
if len(workerOutput) > 0:
|
||||||
rmtree(os.path.join(path, '..', '..'), True)
|
rmtree(os.path.join(path, '..', '..'), True)
|
||||||
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
|
||||||
else:
|
else:
|
||||||
rmtree(os.path.join(path, '..', '..'), True)
|
rmtree(os.path.join(path, '..', '..'), True)
|
||||||
raise UserWarning("Source directory is empty.")
|
raise UserWarning("C2E: Source directory is empty.")
|
||||||
|
|
||||||
|
|
||||||
def imgFileProcessingTick(output):
|
def imgFileProcessingTick(output):
|
||||||
@@ -639,36 +691,188 @@ def imgFileProcessing(work):
|
|||||||
workImg = image.ComicPageParser((dirpath, afile), opt)
|
workImg = image.ComicPageParser((dirpath, afile), opt)
|
||||||
for i in workImg.payload:
|
for i in workImg.payload:
|
||||||
img = image.ComicPage(opt, *i)
|
img = image.ComicPage(opt, *i)
|
||||||
is_not_color = not opt.forcecolor or not img.color
|
|
||||||
if is_not_color:
|
|
||||||
img.convertToGrayscale()
|
|
||||||
if opt.cropping == 2 and not opt.webtoon:
|
if opt.cropping == 2 and not opt.webtoon:
|
||||||
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
||||||
if opt.cropping == 1 and not opt.webtoon:
|
if opt.cropping == 1 and not opt.webtoon:
|
||||||
img.cropMargin(opt.croppingp, opt.croppingm)
|
img.cropMargin(opt.croppingp, opt.croppingm)
|
||||||
if opt.interpanelcrop > 0:
|
if opt.interpanelcrop > 0:
|
||||||
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
|
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
|
||||||
|
|
||||||
img.gammaCorrectImage()
|
img.gammaCorrectImage()
|
||||||
if is_not_color:
|
|
||||||
|
if not img.colorOutput:
|
||||||
|
img.convertToGrayscale()
|
||||||
|
|
||||||
img.autocontrastImage()
|
img.autocontrastImage()
|
||||||
img.resizeImage()
|
img.resizeImage()
|
||||||
img.optimizeForDisplay(opt.reducerainbow)
|
img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
|
||||||
if is_not_color and opt.forcepng:
|
|
||||||
|
if img.colorOutput:
|
||||||
|
pass
|
||||||
|
elif opt.forcepng:
|
||||||
|
if not opt.noquantize:
|
||||||
img.quantizeImage()
|
img.quantizeImage()
|
||||||
|
if opt.format == 'PDF':
|
||||||
|
img.convertToGrayscale()
|
||||||
|
elif opt.profile == 'KDX' and opt.format == 'CBZ':
|
||||||
|
img.convertToGrayscale()
|
||||||
|
elif opt.pnglegacy:
|
||||||
|
img.convertToGrayscale()
|
||||||
output.append(img.saveToDir())
|
output.append(img.saveToDir())
|
||||||
return output
|
return output
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||||
|
|
||||||
|
|
||||||
def getWorkFolder(afile):
|
def render_page(vector):
|
||||||
|
"""Render a page range of a document.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The PyMuPDF document cannot be part of the argument, because that
|
||||||
|
cannot be pickled. So we are being passed in just its filename.
|
||||||
|
This is no performance issue, because we are a separate process and
|
||||||
|
need to open the document anyway.
|
||||||
|
Any page-specific function can be processed here - rendering is just
|
||||||
|
an example - text extraction might be another.
|
||||||
|
The work must however be self-contained: no inter-process communication
|
||||||
|
or synchronization is possible with this design.
|
||||||
|
Care must also be taken with which parameters are contained in the
|
||||||
|
argument, because it will be passed in via pickling by the Pool class.
|
||||||
|
So any large objects will increase the overall duration.
|
||||||
|
Args:
|
||||||
|
vector: a list containing required parameters.
|
||||||
|
"""
|
||||||
|
# recreate the arguments
|
||||||
|
idx = vector[0] # this is the segment number we have to process
|
||||||
|
cpu = vector[1] # number of CPUs
|
||||||
|
filename = vector[2] # document filename
|
||||||
|
output_dir = vector[3]
|
||||||
|
target_width = vector[4]
|
||||||
|
target_height = vector[5]
|
||||||
|
pdf_width = vector[6]
|
||||||
|
with pymupdf.open(filename) as doc: # open the document
|
||||||
|
num_pages = doc.page_count # get number of pages
|
||||||
|
|
||||||
|
# pages per segment: make sure that cpu * seg_size >= num_pages!
|
||||||
|
seg_size = int(num_pages / cpu + 1)
|
||||||
|
seg_from = idx * seg_size # our first page number
|
||||||
|
seg_to = min(seg_from + seg_size, num_pages) # last page number
|
||||||
|
|
||||||
|
for i in range(seg_from, seg_to): # work through our page segment
|
||||||
|
page = doc[i]
|
||||||
|
if not pdf_width or page.rect.width > page.rect.height:
|
||||||
|
zoom = target_height / page.rect.height
|
||||||
|
else:
|
||||||
|
zoom = target_width / page.rect.width
|
||||||
|
mat = pymupdf.Matrix(zoom, zoom)
|
||||||
|
# TODO: decide colorspace earlier so later color check is cheaper.
|
||||||
|
# This is actually pretty hard when you have to deal with color vector text
|
||||||
|
pix = page.get_pixmap(matrix=mat, colorspace='RGB', alpha=False)
|
||||||
|
pix.save(os.path.join(output_dir, "p-%i.png" % i))
|
||||||
|
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_page(vector):
|
||||||
|
"""For pages with single image (and no text). Otherwise it's recommended to use render_page()
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The PyMuPDF document cannot be part of the argument, because that
|
||||||
|
cannot be pickled. So we are being passed in just its filename.
|
||||||
|
This is no performance issue, because we are a separate process and
|
||||||
|
need to open the document anyway.
|
||||||
|
Any page-specific function can be processed here - rendering is just
|
||||||
|
an example - text extraction might be another.
|
||||||
|
The work must however be self-contained: no inter-process communication
|
||||||
|
or synchronization is possible with this design.
|
||||||
|
Care must also be taken with which parameters are contained in the
|
||||||
|
argument, because it will be passed in via pickling by the Pool class.
|
||||||
|
So any large objects will increase the overall duration.
|
||||||
|
Args:
|
||||||
|
vector: a list containing required parameters.
|
||||||
|
"""
|
||||||
|
# recreate the arguments
|
||||||
|
idx = vector[0] # this is the segment number we have to process
|
||||||
|
cpu = vector[1] # number of CPUs
|
||||||
|
filename = vector[2] # document filename
|
||||||
|
output_dir = vector[3]
|
||||||
|
|
||||||
|
|
||||||
|
with pymupdf.open(filename) as doc: # open the document
|
||||||
|
num_pages = doc.page_count # get number of pages
|
||||||
|
|
||||||
|
# pages per segment: make sure that cpu * seg_size >= num_pages!
|
||||||
|
seg_size = int(num_pages / cpu + 1)
|
||||||
|
seg_from = idx * seg_size # our first page number
|
||||||
|
seg_to = min(seg_from + seg_size, num_pages) # last page number
|
||||||
|
|
||||||
|
for i in range(seg_from, seg_to): # work through our page segment
|
||||||
|
output_path = os.path.join(output_dir, "p-%i.png" % i)
|
||||||
|
page = doc.load_page(i)
|
||||||
|
image_list = page.get_images()
|
||||||
|
if len(image_list) > 1:
|
||||||
|
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
|
||||||
|
if not image_list:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
xref = image_list[0][0]
|
||||||
|
d = doc.extract_image(xref)
|
||||||
|
if d['cs-name'] == 'DeviceCMYK':
|
||||||
|
pix = pymupdf.Pixmap(doc, xref)
|
||||||
|
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
|
||||||
|
pix.save(output_path)
|
||||||
|
else:
|
||||||
|
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout:
|
||||||
|
imgout.write(d["image"])
|
||||||
|
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
|
||||||
|
render = False
|
||||||
|
with pymupdf.open(filename) as doc:
|
||||||
|
for page in doc:
|
||||||
|
page_text = page.get_text().strip()
|
||||||
|
if page_text != "":
|
||||||
|
render = True
|
||||||
|
break
|
||||||
|
if len(page.get_images()) > 1:
|
||||||
|
render = True
|
||||||
|
break
|
||||||
|
if len(page.get_images()) == 1:
|
||||||
|
image = page.get_images()[0]
|
||||||
|
if not image[5] or image[8] == 'CCITTFaxDecode':
|
||||||
|
render = True
|
||||||
|
break
|
||||||
|
|
||||||
|
cpu = cpu_count()
|
||||||
|
|
||||||
|
# make vectors of arguments for the processes
|
||||||
|
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
|
||||||
|
print("Starting %i processes for '%s'." % (cpu, filename))
|
||||||
|
|
||||||
|
|
||||||
|
start = perf_counter()
|
||||||
|
with Pool() as pool:
|
||||||
|
results = pool.map(
|
||||||
|
render_page if render else extract_page, vectors
|
||||||
|
)
|
||||||
|
end = perf_counter()
|
||||||
|
print(f"MuPDF: {end - start} sec")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getWorkFolder(afile, workdir=None):
|
||||||
|
if not workdir:
|
||||||
|
workdir = mkdtemp('', 'KCC-')
|
||||||
|
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||||
|
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||||
|
else:
|
||||||
|
fullPath = workdir
|
||||||
if os.path.isdir(afile):
|
if os.path.isdir(afile):
|
||||||
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
||||||
raise UserWarning("Not enough disk space to perform conversion.")
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
|
||||||
try:
|
try:
|
||||||
os.rmdir(workdir)
|
|
||||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
|
||||||
copytree(afile, fullPath)
|
copytree(afile, fullPath)
|
||||||
sanitizePermissions(fullPath)
|
sanitizePermissions(fullPath)
|
||||||
return workdir
|
return workdir
|
||||||
@@ -679,30 +883,60 @@ def getWorkFolder(afile):
|
|||||||
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
||||||
raise UserWarning("Not enough disk space to perform conversion.")
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
if afile.lower().endswith('.pdf'):
|
if afile.lower().endswith('.pdf'):
|
||||||
pdf = pdfjpgextract.PdfJpgExtract(afile)
|
if not os.path.exists(fullPath):
|
||||||
path, njpg = pdf.extract()
|
os.makedirs(fullPath)
|
||||||
workdir = path
|
path = workdir
|
||||||
sanitizePermissions(path)
|
sanitizePermissions(path)
|
||||||
|
if options.pdfextract:
|
||||||
|
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||||
|
njpg = pdf.extract()
|
||||||
if njpg == 0:
|
if njpg == 0:
|
||||||
rmtree(path, True)
|
|
||||||
raise UserWarning("Failed to extract images from PDF file.")
|
raise UserWarning("Failed to extract images from PDF file.")
|
||||||
|
return workdir
|
||||||
|
target_width, target_height = options.profileData[1]
|
||||||
|
if options.cropping == 1:
|
||||||
|
target_height *= 1.2 #Account for possible margin at the top and bottom
|
||||||
|
target_width *= 1.2
|
||||||
|
elif options.cropping == 2:
|
||||||
|
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
|
||||||
|
target_width *= 1.25
|
||||||
|
try:
|
||||||
|
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
|
||||||
|
except Exception as e:
|
||||||
|
rmtree(path, True)
|
||||||
|
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
||||||
|
return workdir
|
||||||
else:
|
else:
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
if not os.path.exists(fullPath):
|
||||||
|
os.makedirs(fullPath)
|
||||||
try:
|
try:
|
||||||
cbx = comicarchive.ComicArchive(afile)
|
cbx = comicarchive.ComicArchive(afile)
|
||||||
path = cbx.extract(workdir)
|
path = cbx.extract(fullPath)
|
||||||
sanitizePermissions(path)
|
sanitizePermissions(path)
|
||||||
|
|
||||||
|
tdir = os.listdir(fullPath)
|
||||||
|
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
|
||||||
|
tdir.remove('ComicInfo.xml')
|
||||||
|
if os.path.isdir(os.path.join(fullPath, tdir[0])):
|
||||||
|
os.replace(
|
||||||
|
os.path.join(fullPath, 'ComicInfo.xml'),
|
||||||
|
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
|
||||||
|
)
|
||||||
|
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
|
||||||
|
for file in os.listdir(os.path.join(fullPath, tdir[0])):
|
||||||
|
move(os.path.join(fullPath, tdir[0], file), fullPath)
|
||||||
|
os.rmdir(os.path.join(fullPath, tdir[0]))
|
||||||
|
return workdir
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
rmtree(workdir, True)
|
rmtree(workdir, True)
|
||||||
raise UserWarning(e)
|
raise UserWarning(e)
|
||||||
else:
|
else:
|
||||||
raise UserWarning("Failed to open source file/directory.")
|
raise UserWarning("Failed to open source file/directory.")
|
||||||
newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
|
||||||
os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
|
|
||||||
return newpath
|
|
||||||
|
|
||||||
|
|
||||||
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||||
|
source_path = Path(srcpath)
|
||||||
if srcpath[-1] == os.path.sep:
|
if srcpath[-1] == os.path.sep:
|
||||||
srcpath = srcpath[:-1]
|
srcpath = srcpath[:-1]
|
||||||
if 'Ko' in options.profile and options.format == 'EPUB':
|
if 'Ko' in options.profile and options.format == 'EPUB':
|
||||||
@@ -712,20 +946,29 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
|||||||
else:
|
else:
|
||||||
ext = '.kepub.epub'
|
ext = '.kepub.epub'
|
||||||
if wantedname is not None:
|
if wantedname is not None:
|
||||||
|
wanted_root, wanted_ext = os.path.splitext(wantedname)
|
||||||
if wantedname.endswith(ext):
|
if wantedname.endswith(ext):
|
||||||
filename = os.path.abspath(wantedname)
|
filename = os.path.abspath(wantedname)
|
||||||
elif os.path.isdir(srcpath):
|
elif wanted_ext == '.mobi' and ext == '.epub':
|
||||||
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
|
filename = os.path.abspath(wanted_root + ext)
|
||||||
|
# output directory
|
||||||
else:
|
else:
|
||||||
filename = os.path.join(os.path.abspath(options.output),
|
abs_path = os.path.abspath(options.output)
|
||||||
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
|
if not os.path.exists(abs_path):
|
||||||
|
os.mkdir(abs_path)
|
||||||
|
if source_path.is_file():
|
||||||
|
filename = os.path.join(os.path.abspath(options.output), source_path.stem + tomenumber + ext)
|
||||||
|
else:
|
||||||
|
filename = os.path.join(os.path.abspath(options.output), source_path.name + tomenumber + ext)
|
||||||
elif os.path.isdir(srcpath):
|
elif os.path.isdir(srcpath):
|
||||||
filename = srcpath + tomenumber + ext
|
filename = srcpath + tomenumber + ext
|
||||||
else:
|
else:
|
||||||
if 'Ko' in options.profile and options.format == 'EPUB':
|
if 'Ko' in options.profile and options.format == 'EPUB':
|
||||||
src = pathlib.Path(srcpath)
|
if source_path.is_file():
|
||||||
name = re.sub(r'\W+', '_', src.stem) + tomenumber + ext
|
name = re.sub(r'\W+', '_', source_path.stem) + tomenumber + ext
|
||||||
filename = src.with_name(name)
|
else:
|
||||||
|
name = re.sub(r'\W+', '_', source_path.name) + tomenumber + ext
|
||||||
|
filename = source_path.with_name(name)
|
||||||
else:
|
else:
|
||||||
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
|
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
@@ -734,10 +977,17 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
|||||||
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
|
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
|
||||||
counter += 1
|
counter += 1
|
||||||
filename = basename + '_kcc' + str(counter) + ext
|
filename = basename + '_kcc' + str(counter) + ext
|
||||||
|
elif options.format == 'MOBI' and ext == '.epub':
|
||||||
|
counter = 0
|
||||||
|
basename = os.path.splitext(filename)[0]
|
||||||
|
if os.path.isfile(basename + '.mobi'):
|
||||||
|
while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
|
||||||
|
counter += 1
|
||||||
|
filename = basename + '_kcc' + str(counter) + ext
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def getComicInfo(path, originalpath):
|
def getMetadata(path, originalpath):
|
||||||
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
||||||
options.comicinfo_chapters = []
|
options.comicinfo_chapters = []
|
||||||
options.summary = ''
|
options.summary = ''
|
||||||
@@ -756,21 +1006,24 @@ def getComicInfo(path, originalpath):
|
|||||||
else:
|
else:
|
||||||
defaultAuthor = False
|
defaultAuthor = False
|
||||||
options.authors = [options.author]
|
options.authors = [options.author]
|
||||||
|
|
||||||
if os.path.exists(xmlPath):
|
if os.path.exists(xmlPath):
|
||||||
try:
|
try:
|
||||||
xml = metadata.MetadataParser(xmlPath)
|
xml = metadata.MetadataParser(xmlPath)
|
||||||
except Exception:
|
except Exception:
|
||||||
os.remove(xmlPath)
|
os.remove(xmlPath)
|
||||||
return
|
return
|
||||||
if options.comicinfotitle:
|
if options.metadatatitle == 2:
|
||||||
options.title = xml.data['Title']
|
options.title = xml.data['Title']
|
||||||
elif defaultTitle:
|
elif defaultTitle:
|
||||||
if xml.data['Series']:
|
if xml.data['Series']:
|
||||||
options.title = xml.data['Series']
|
options.title = xml.data['Series']
|
||||||
if xml.data['Volume']:
|
if xml.data['Volume']:
|
||||||
titleSuffix += ' V' + xml.data['Volume'].zfill(2)
|
titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
|
||||||
if xml.data['Number']:
|
if xml.data['Number']:
|
||||||
titleSuffix += ' #' + xml.data['Number'].zfill(3)
|
titleSuffix += ' #' + xml.data['Number'].zfill(3)
|
||||||
|
if options.metadatatitle == 1 and xml.data['Title']:
|
||||||
|
titleSuffix += ': ' + xml.data['Title']
|
||||||
options.title += titleSuffix
|
options.title += titleSuffix
|
||||||
if defaultAuthor:
|
if defaultAuthor:
|
||||||
options.authors = []
|
options.authors = []
|
||||||
@@ -788,6 +1041,13 @@ def getComicInfo(path, originalpath):
|
|||||||
options.summary = xml.data['Summary']
|
options.summary = xml.data['Summary']
|
||||||
os.remove(xmlPath)
|
os.remove(xmlPath)
|
||||||
|
|
||||||
|
if originalpath.lower().endswith('.pdf'):
|
||||||
|
with pymupdf.open(originalpath) as doc:
|
||||||
|
if options.metadatatitle and doc.metadata['title']:
|
||||||
|
options.title = doc.metadata['title']
|
||||||
|
if defaultAuthor and doc.metadata['author']:
|
||||||
|
options.authors = [doc.metadata['author']]
|
||||||
|
|
||||||
|
|
||||||
def getDirectorySize(start_path='.'):
|
def getDirectorySize(start_path='.'):
|
||||||
total_size = 0
|
total_size = 0
|
||||||
@@ -821,7 +1081,7 @@ def removeNonImages(filetree):
|
|||||||
for root, dirs, files in os.walk(filetree):
|
for root, dirs, files in os.walk(filetree):
|
||||||
for name in files:
|
for name in files:
|
||||||
_, ext = getImageFileName(name)
|
_, ext = getImageFileName(name)
|
||||||
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
|
if ext not in IMAGE_TYPES:
|
||||||
if os.path.exists(os.path.join(root, name)):
|
if os.path.exists(os.path.join(root, name)):
|
||||||
os.remove(os.path.join(root, name))
|
os.remove(os.path.join(root, name))
|
||||||
# remove empty nested folders
|
# remove empty nested folders
|
||||||
@@ -829,8 +1089,11 @@ def removeNonImages(filetree):
|
|||||||
if not files and not dirs:
|
if not files and not dirs:
|
||||||
os.rmdir(root)
|
os.rmdir(root)
|
||||||
|
|
||||||
|
if not os.listdir(Path(filetree).parent):
|
||||||
|
raise UserWarning('No images detected, nested archives are not supported.')
|
||||||
|
|
||||||
def sanitizeTree(filetree):
|
|
||||||
|
def sanitizeTree(filetree, prefix='kcc'):
|
||||||
chapterNames = {}
|
chapterNames = {}
|
||||||
page = 1
|
page = 1
|
||||||
cover_path = None
|
cover_path = None
|
||||||
@@ -840,7 +1103,7 @@ def sanitizeTree(filetree):
|
|||||||
_, ext = getImageFileName(name)
|
_, ext = getImageFileName(name)
|
||||||
|
|
||||||
# 9999 page limit
|
# 9999 page limit
|
||||||
unique_name = f'kcc-{page:04}'
|
unique_name = f'{prefix}-{page:04}'
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
newKey = os.path.join(root, unique_name + ext)
|
newKey = os.path.join(root, unique_name + ext)
|
||||||
@@ -881,21 +1144,15 @@ def sanitizePermissions(filetree):
|
|||||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
||||||
for name in dirs:
|
for name in dirs:
|
||||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
|
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
|
||||||
|
dot_clean(filetree)
|
||||||
|
|
||||||
def dot_clean(filetree):
|
|
||||||
for root, _, files in os.walk(filetree, topdown=False):
|
|
||||||
for name in files:
|
|
||||||
if name.startswith('._'):
|
|
||||||
os.remove(os.path.join(root, name))
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_directory(path):
|
def chunk_directory(path):
|
||||||
level = -1
|
level = -1
|
||||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||||
for f in files:
|
for f in files:
|
||||||
# Windows MAX_LENGTH = 260 plus some buffer
|
# Windows MAX_LEN = 260 plus some buffer
|
||||||
if len(os.path.join(root, f)) > 180:
|
if os.name == 'nt' and len(os.path.join(root, f)) > 220:
|
||||||
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||||
level = 1
|
level = 1
|
||||||
break
|
break
|
||||||
@@ -986,6 +1243,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
|||||||
try:
|
try:
|
||||||
img = Image.open(path)
|
img = Image.open(path)
|
||||||
imageNumber += 1
|
imageNumber += 1
|
||||||
|
# count images smaller than device resolution
|
||||||
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
|
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
|
||||||
imageSmaller += 1
|
imageSmaller += 1
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -1006,7 +1264,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
|||||||
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
|
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
|
||||||
, 'warning', False)
|
, 'warning', False)
|
||||||
GUI.addMessage.emit('', '', False)
|
GUI.addMessage.emit('', '', False)
|
||||||
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and options.profile != 'KS':
|
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and not options.profile.startswith('KS'):
|
||||||
print("WARNING: More than 25% of images are smaller than target device resolution. "
|
print("WARNING: More than 25% of images are smaller than target device resolution. "
|
||||||
"Consider enabling stretching or upscaling to improve readability.")
|
"Consider enabling stretching or upscaling to improve readability.")
|
||||||
if GUI:
|
if GUI:
|
||||||
@@ -1033,18 +1291,20 @@ def slugify(value, is_natural_sorted):
|
|||||||
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
|
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||||
def makeZIP(zipfilename, basedir, isepub=False):
|
|
||||||
start = perf_counter()
|
start = perf_counter()
|
||||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||||
if SEVENZIP in available_archive_tools():
|
if SEVENZIP in available_archive_tools():
|
||||||
if isepub:
|
if isepub:
|
||||||
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||||
mimetypeFile.write('application/epub+zip')
|
mimetypeFile.write('application/epub+zip')
|
||||||
mimetypeFile.close()
|
mimetypeFile.close()
|
||||||
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
|
subprocess_run([SEVENZIP, 'a', '-mx0', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
|
||||||
|
# crazy hack to ensure mimetype is first when using 7zip
|
||||||
|
if isepub:
|
||||||
|
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
|
||||||
else:
|
else:
|
||||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
|
||||||
if isepub:
|
if isepub:
|
||||||
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
|
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
|
||||||
for dirpath, _, filenames in os.walk(basedir):
|
for dirpath, _, filenames in os.walk(basedir):
|
||||||
@@ -1055,10 +1315,9 @@ def makeZIP(zipfilename, basedir, isepub=False):
|
|||||||
zipOutput.write(path, aPath)
|
zipOutput.write(path, aPath)
|
||||||
zipOutput.close()
|
zipOutput.close()
|
||||||
end = perf_counter()
|
end = perf_counter()
|
||||||
print(f"makeZIP time: {end - start} seconds")
|
print(f"{job_progress}makeZIP time: {end - start} seconds")
|
||||||
return zipfilename
|
return zipfilename
|
||||||
|
|
||||||
|
|
||||||
def makeParser():
|
def makeParser():
|
||||||
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
|
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
|
||||||
|
|
||||||
@@ -1091,12 +1350,13 @@ def makeParser():
|
|||||||
help="Output generated file to specified directory or file")
|
help="Output generated file to specified directory or file")
|
||||||
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
|
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||||
help="Comic title [Default=filename or directory name]")
|
help="Comic title [Default=filename or directory name]")
|
||||||
output_options.add_argument("--comicinfotitle", action="store_true", dest="comicinfotitle", default=False,
|
output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0,
|
||||||
help="Write Title from ComicInfo.xml")
|
help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema "
|
||||||
|
"2: Use Title only")
|
||||||
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
|
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
|
||||||
help="Author name [Default=KCC]")
|
help="Author name [Default=KCC]")
|
||||||
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
||||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
|
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
|
||||||
"[Default=Auto]")
|
"[Default=Auto]")
|
||||||
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
|
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
|
||||||
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
|
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
|
||||||
@@ -1107,11 +1367,21 @@ def makeParser():
|
|||||||
help="Shift first page to opposite side in landscape for spread alignment")
|
help="Shift first page to opposite side in landscape for spread alignment")
|
||||||
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
|
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
|
||||||
help="Do not rotate double page spreads in spread splitter option.")
|
help="Do not rotate double page spreads in spread splitter option.")
|
||||||
|
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
|
||||||
|
help="Rotate double page spreads in opposite direction.")
|
||||||
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
|
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
|
||||||
help="Put rotated 2 page spread first in spread splitter option.")
|
help="Put rotated 2 page spread first in spread splitter option.")
|
||||||
|
|
||||||
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
|
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
|
||||||
help="Do not modify image and ignore any profil or processing option")
|
help="Do not modify image and ignore any profile or processing option")
|
||||||
|
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
|
||||||
|
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
|
||||||
|
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
|
||||||
|
help="Render vector PDFs to device width instead of height.")
|
||||||
|
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
|
||||||
|
help="Attempt to crop main cover from wide image")
|
||||||
|
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
|
||||||
|
help="Crop cover to fill screen")
|
||||||
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||||
help="Resize images smaller than device's resolution")
|
help="Resize images smaller than device's resolution")
|
||||||
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
||||||
@@ -1120,6 +1390,14 @@ def makeParser():
|
|||||||
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
||||||
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
|
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
|
||||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
help="Apply gamma correction to linearize the image [Default=Auto]")
|
||||||
|
output_options.add_argument("--autolevel", action="store_true", dest="autolevel", default=False,
|
||||||
|
help="Set most common dark pixel value to be black point for leveling.")
|
||||||
|
output_options.add_argument("--noautocontrast", action="store_true", dest="noautocontrast", default=False,
|
||||||
|
help="Disable autocontrast.")
|
||||||
|
output_options.add_argument("--colorautocontrast", action="store_true", dest="colorautocontrast", default=False,
|
||||||
|
help="Autocontrast color pages too. Skipped for pages without near blacks or whites.")
|
||||||
|
output_options.add_argument("--filefusion", action="store_true", dest="filefusion", default=False,
|
||||||
|
help="Combines all input files into a single file.")
|
||||||
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
|
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
|
||||||
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
||||||
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
|
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
|
||||||
@@ -1136,12 +1414,22 @@ def makeParser():
|
|||||||
help="Disable autodetection and force white borders")
|
help="Disable autodetection and force white borders")
|
||||||
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
||||||
help="Don't convert images to grayscale")
|
help="Don't convert images to grayscale")
|
||||||
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
|
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
|
||||||
help="Reduce rainbow effect on color eink by slightly blurring images.")
|
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
|
||||||
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||||
help="Create PNG files instead JPEG")
|
help="Create PNG files instead JPEG for black and white images")
|
||||||
|
processing_options.add_argument("--force-png-rgb", action="store_true", dest="force_png_rgb", default=False,
|
||||||
|
help="Force color images to be saved as PNG")
|
||||||
|
processing_options.add_argument("--webp", action="store_true", dest="webp", default=False,
|
||||||
|
help="Replace JPG with lossy WEBP and PNG with lossless WEBP")
|
||||||
|
processing_options.add_argument("--pnglegacy", action="store_true", dest="pnglegacy", default=False,
|
||||||
|
help="Use a more compatible 8 bit png instead of 4 bit")
|
||||||
|
processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
|
||||||
|
help="Don't quantize to 16 color PNG")
|
||||||
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
||||||
help="Create JPEG files using mozJpeg")
|
help="Create JPEG files using mozJpeg")
|
||||||
|
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
|
||||||
|
help="The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.")
|
||||||
processing_options.add_argument("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
|
processing_options.add_argument("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
|
||||||
help="Turn 1x4 strips to 2x2 strips")
|
help="Turn 1x4 strips to 2x2 strips")
|
||||||
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
|
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
|
||||||
@@ -1164,6 +1452,11 @@ def checkOptions(options):
|
|||||||
options.isKobo = False
|
options.isKobo = False
|
||||||
options.bordersColor = None
|
options.bordersColor = None
|
||||||
options.keep_epub = False
|
options.keep_epub = False
|
||||||
|
if options.format == 'PDF-200MB':
|
||||||
|
options.targetsize = 195
|
||||||
|
options.format = 'PDF'
|
||||||
|
if options.batchsplit != 2:
|
||||||
|
options.batchsplit = 1
|
||||||
if options.format == 'EPUB-200MB':
|
if options.format == 'EPUB-200MB':
|
||||||
options.targetsize = 195
|
options.targetsize = 195
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
@@ -1175,6 +1468,8 @@ def checkOptions(options):
|
|||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
if options.batchsplit != 2:
|
if options.batchsplit != 2:
|
||||||
options.batchsplit = 1
|
options.batchsplit = 1
|
||||||
|
if not options.targetsize and options.profile.startswith('Rmk'):
|
||||||
|
options.targetsize = 95
|
||||||
if options.format == 'MOBI+EPUB':
|
if options.format == 'MOBI+EPUB':
|
||||||
options.keep_epub = True
|
options.keep_epub = True
|
||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
@@ -1184,6 +1479,8 @@ def checkOptions(options):
|
|||||||
options.format = 'CBZ'
|
options.format = 'CBZ'
|
||||||
elif options.profile in image.ProfileData.ProfilesKindle.keys():
|
elif options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
|
elif options.profile in image.ProfileData.ProfilesRemarkable.keys():
|
||||||
|
options.format = 'PDF'
|
||||||
else:
|
else:
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||||
@@ -1207,8 +1504,10 @@ def checkOptions(options):
|
|||||||
if options.webtoon:
|
if options.webtoon:
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
options.righttoleft = False
|
options.righttoleft = False
|
||||||
options.upscale = True
|
options.upscale = False
|
||||||
options.hq = False
|
options.hq = False
|
||||||
|
options.white_borders = True
|
||||||
|
options.bordersColor = 'white'
|
||||||
# Disable all Kindle features for other e-readers
|
# Disable all Kindle features for other e-readers
|
||||||
if options.profile == 'OTHER':
|
if options.profile == 'OTHER':
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
@@ -1216,9 +1515,6 @@ def checkOptions(options):
|
|||||||
if 'Ko' in options.profile:
|
if 'Ko' in options.profile:
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
options.hq = False
|
options.hq = False
|
||||||
# CBZ files on Kindle DX/DXG support higher resolution
|
|
||||||
if options.profile == 'KDX' and options.format == 'CBZ':
|
|
||||||
options.customheight = 1200
|
|
||||||
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
||||||
if options.format == 'KFX':
|
if options.format == 'KFX':
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
@@ -1237,6 +1533,26 @@ def checkOptions(options):
|
|||||||
image.ProfileData.Profiles["Custom"] = newProfile
|
image.ProfileData.Profiles["Custom"] = newProfile
|
||||||
options.profile = "Custom"
|
options.profile = "Custom"
|
||||||
options.profileData = image.ProfileData.Profiles[options.profile]
|
options.profileData = image.ProfileData.Profiles[options.profile]
|
||||||
|
if not options.jpegquality:
|
||||||
|
if options.profile.startswith('KS') or options.profile == 'KCS':
|
||||||
|
options.jpegquality = 90
|
||||||
|
else:
|
||||||
|
options.jpegquality = 85
|
||||||
|
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||||
|
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
|
||||||
|
|
||||||
|
options.webp_output = options.format != 'PDF' and not options.kindle_azw3 and options.webp
|
||||||
|
|
||||||
|
# CBZ files on Kindle DX/DXG support higher resolution
|
||||||
|
if options.profile == 'KDX' and options.format == 'CBZ':
|
||||||
|
options.profileData = list(image.ProfileData.Profiles[options.profile])
|
||||||
|
options.profileData[1] = list(options.profileData[1])
|
||||||
|
options.profileData[1][1] = 1200
|
||||||
|
|
||||||
|
if options.kindle_scribe_azw3:
|
||||||
|
options.profileData = list(image.ProfileData.Profiles[options.profile])
|
||||||
|
options.profileData[1] = list(options.profileData[1])
|
||||||
|
options.profileData[1][0] = min(1920, options.profileData[1][0])
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
@@ -1284,21 +1600,28 @@ def makeFusion(sources: List[str]):
|
|||||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||||
print("Running Fusion")
|
print("Running Fusion")
|
||||||
|
|
||||||
for source in sources:
|
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||||
|
path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
|
||||||
|
needs_prefix = os_sorted(path_names) != path_names
|
||||||
|
|
||||||
|
for index, source in enumerate(sources, start=1):
|
||||||
print(f"Processing {source}...")
|
print(f"Processing {source}...")
|
||||||
checkPre(source)
|
checkPre(source)
|
||||||
print("Checking images...")
|
print("Checking images...")
|
||||||
path = getWorkFolder(source)
|
|
||||||
pathfinder = os.path.join(path, "OEBPS", "Images")
|
|
||||||
sanitizeTree(pathfinder)
|
|
||||||
# TODO: remove flattenTree when subchapters are supported
|
|
||||||
flattenTree(pathfinder)
|
|
||||||
source_path = Path(source)
|
source_path = Path(source)
|
||||||
|
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||||
|
prefix = ''
|
||||||
|
if needs_prefix:
|
||||||
|
prefix = f'fusion_{index:04d}_'
|
||||||
if source_path.is_file():
|
if source_path.is_file():
|
||||||
os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
|
targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
|
||||||
else:
|
else:
|
||||||
os.renames(pathfinder, fusion_path.joinpath(source_path.name))
|
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
|
||||||
|
|
||||||
|
getWorkFolder(source, str(targetpath))
|
||||||
|
sanitizeTree(targetpath, prefix='fusion')
|
||||||
|
# TODO: remove flattenTree when subchapters are supported
|
||||||
|
flattenTree(targetpath)
|
||||||
|
|
||||||
end = perf_counter()
|
end = perf_counter()
|
||||||
print(f"makefusion: {end - start} seconds")
|
print(f"makefusion: {end - start} seconds")
|
||||||
@@ -1307,7 +1630,7 @@ def makeFusion(sources: List[str]):
|
|||||||
return str(fusion_path)
|
return str(fusion_path)
|
||||||
|
|
||||||
|
|
||||||
def makeBook(source, qtgui=None):
|
def makeBook(source, qtgui=None, job_progress=''):
|
||||||
start = perf_counter()
|
start = perf_counter()
|
||||||
global GUI
|
global GUI
|
||||||
GUI = qtgui
|
GUI = qtgui
|
||||||
@@ -1315,30 +1638,34 @@ def makeBook(source, qtgui=None):
|
|||||||
GUI.progressBarTick.emit('1')
|
GUI.progressBarTick.emit('1')
|
||||||
else:
|
else:
|
||||||
checkTools(source)
|
checkTools(source)
|
||||||
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
|
|
||||||
checkPre(source)
|
checkPre(source)
|
||||||
print("Preparing source images...")
|
print(f"{job_progress}Preparing source images...")
|
||||||
path = getWorkFolder(source)
|
path = getWorkFolder(source)
|
||||||
print("Checking images...")
|
print(f"{job_progress}Checking images...")
|
||||||
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
|
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
|
||||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
||||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||||
|
if options.filefusion:
|
||||||
|
# Strip the fusion_0001_ sort prefix from makeFusion if present
|
||||||
|
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
|
||||||
|
cover = None
|
||||||
|
if not options.webtoon:
|
||||||
cover = image.Cover(cover_path, options)
|
cover = image.Cover(cover_path, options)
|
||||||
|
|
||||||
if options.webtoon:
|
if options.webtoon:
|
||||||
y = image.ProfileData.Profiles[options.profile][1][1]
|
x, y = image.ProfileData.Profiles[options.profile][1]
|
||||||
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
|
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
|
||||||
if options.noprocessing:
|
if options.noprocessing:
|
||||||
print("Do not process image, ignore any profile or processing option")
|
print(f"{job_progress}Do not process image, ignore any profile or processing option")
|
||||||
else:
|
else:
|
||||||
print("Processing images...")
|
print(f"{job_progress}Processing images...")
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('Processing images')
|
GUI.progressBarTick.emit(f'{job_progress}Processing images')
|
||||||
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
|
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"), job_progress)
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('1')
|
GUI.progressBarTick.emit('1')
|
||||||
if options.batchsplit > 0:
|
if options.batchsplit > 0 or options.targetsize:
|
||||||
tomes = chunk_directory(path)
|
tomes = chunk_directory(path)
|
||||||
else:
|
else:
|
||||||
tomes = [path]
|
tomes = [path]
|
||||||
@@ -1346,9 +1673,11 @@ def makeBook(source, qtgui=None):
|
|||||||
tomeNumber = 0
|
tomeNumber = 0
|
||||||
if GUI:
|
if GUI:
|
||||||
if options.format == 'CBZ':
|
if options.format == 'CBZ':
|
||||||
GUI.progressBarTick.emit('Compressing CBZ files')
|
GUI.progressBarTick.emit(f'{job_progress}Compressing CBZ files')
|
||||||
|
elif options.format == 'PDF':
|
||||||
|
GUI.progressBarTick.emit(f'{job_progress}Creating PDF files')
|
||||||
else:
|
else:
|
||||||
GUI.progressBarTick.emit('Compressing EPUB files')
|
GUI.progressBarTick.emit(f'{job_progress}Compressing EPUB files')
|
||||||
GUI.progressBarTick.emit(str(len(tomes) + 1))
|
GUI.progressBarTick.emit(str(len(tomes) + 1))
|
||||||
GUI.progressBarTick.emit('tick')
|
GUI.progressBarTick.emit('tick')
|
||||||
options.baseTitle = options.title
|
options.baseTitle = options.title
|
||||||
@@ -1362,21 +1691,31 @@ def makeBook(source, qtgui=None):
|
|||||||
tomeNumber += 1
|
tomeNumber += 1
|
||||||
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
|
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
|
||||||
if options.format == 'CBZ':
|
if options.format == 'CBZ':
|
||||||
print("Creating CBZ file...")
|
print(f"{job_progress}Creating CBZ file...")
|
||||||
if len(tomes) > 1:
|
if len(tomes) > 1:
|
||||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
||||||
else:
|
else:
|
||||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||||
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
|
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
|
||||||
|
elif options.format == 'PDF':
|
||||||
|
print(f"{job_progress}Creating PDF file with PyMuPDF...")
|
||||||
|
# determine output filename based on source and tome count
|
||||||
|
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
|
||||||
|
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
|
||||||
|
# use optimized buildPDF logic with streaming and compression
|
||||||
|
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
|
||||||
|
filepath.append(output_pdf)
|
||||||
else:
|
else:
|
||||||
print("Creating EPUB file...")
|
print(f"{job_progress}Creating EPUB file...")
|
||||||
if len(tomes) > 1:
|
if len(tomes) > 1:
|
||||||
buildEPUB(tome, chapterNames, tomeNumber, True, cover, len(tomes))
|
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, job_progress, len(tomes))
|
||||||
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
|
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
|
||||||
else:
|
else:
|
||||||
buildEPUB(tome, chapterNames, tomeNumber, False, cover)
|
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
|
||||||
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
||||||
makeZIP(tome + '_comic', tome, True)
|
makeZIP(tome + '_comic', tome, job_progress, True)
|
||||||
|
# Copy files to final destination (PDF files are already saved directly)
|
||||||
|
if options.format != 'PDF':
|
||||||
copyfile(tome + '_comic.zip', filepath[-1])
|
copyfile(tome + '_comic.zip', filepath[-1])
|
||||||
try:
|
try:
|
||||||
os.remove(tome + '_comic.zip')
|
os.remove(tome + '_comic.zip')
|
||||||
@@ -1387,36 +1726,41 @@ def makeBook(source, qtgui=None):
|
|||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('tick')
|
GUI.progressBarTick.emit('tick')
|
||||||
if not GUI and options.format == 'MOBI':
|
if not GUI and options.format == 'MOBI':
|
||||||
print("Creating MOBI files...")
|
print(f"{job_progress}Creating MOBI files...")
|
||||||
work = []
|
work = []
|
||||||
for i in filepath:
|
for i in filepath:
|
||||||
work.append([i])
|
work.append([i])
|
||||||
output = makeMOBI(work, GUI)
|
output = makeMOBI(work, GUI)
|
||||||
for errors in output:
|
for errors in output:
|
||||||
if errors[0] != 0:
|
if errors[0] != 0:
|
||||||
print('Error: KindleGen failed to create MOBI!')
|
print(f"{job_progress}Error: KindleGen failed to create MOBI!")
|
||||||
print(errors)
|
print(errors)
|
||||||
return filepath
|
return filepath
|
||||||
k = kindle.Kindle(options.profile)
|
k = kindle.Kindle(options.profile)
|
||||||
if k.path and k.coverSupport:
|
if k.path and k.coverSupport:
|
||||||
print("Kindle detected. Uploading covers...")
|
print(f"{job_progress}Kindle detected. Uploading covers...")
|
||||||
for i in filepath:
|
for i in filepath:
|
||||||
output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
|
output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
|
||||||
if not output[0]:
|
if not output[0]:
|
||||||
print('Error: Failed to tweak KindleGen output!')
|
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
|
||||||
return filepath
|
return filepath
|
||||||
else:
|
else:
|
||||||
os.remove(i.replace('.epub', '.mobi') + '_toclean')
|
os.remove(i.replace('.epub', '.mobi') + '_toclean')
|
||||||
if k.path and k.coverSupport:
|
if cover and k.path and k.coverSupport:
|
||||||
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
|
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
|
||||||
if options.delete:
|
if options.delete:
|
||||||
if os.path.isfile(source):
|
if os.path.isfile(source):
|
||||||
os.remove(source)
|
os.remove(source)
|
||||||
elif os.path.isdir(source):
|
elif os.path.isdir(source):
|
||||||
rmtree(source)
|
rmtree(source, True)
|
||||||
|
|
||||||
end = perf_counter()
|
end = perf_counter()
|
||||||
print(f"makeBook: {end - start} seconds")
|
print(f"{job_progress}makeBook: {end - start} seconds")
|
||||||
|
# Clean up temporary workspace
|
||||||
|
try:
|
||||||
|
rmtree(path, True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,17 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from shutil import rmtree, copytree, move
|
from shutil import rmtree
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
|
||||||
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
|
from PIL.Image import Dither
|
||||||
|
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
def mergeDirectoryTick(output):
|
def mergeDirectoryTick(output):
|
||||||
@@ -44,6 +48,7 @@ def mergeDirectory(work):
|
|||||||
imagesValid = []
|
imagesValid = []
|
||||||
sizes = []
|
sizes = []
|
||||||
targetHeight = 0
|
targetHeight = 0
|
||||||
|
dot_clean(directory)
|
||||||
for root, _, files in walkLevel(directory, 0):
|
for root, _, files in walkLevel(directory, 0):
|
||||||
for name in files:
|
for name in files:
|
||||||
if getImageFileName(name) is not None:
|
if getImageFileName(name) is not None:
|
||||||
@@ -57,12 +62,13 @@ 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:
|
if targetHeight > 131072 * 4:
|
||||||
return None
|
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:
|
||||||
|
img = img.convert('RGB')
|
||||||
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
if img.size[0] < targetWidth or img.size[0] > targetWidth:
|
||||||
widthPercent = (targetWidth / float(img.size[0]))
|
widthPercent = (targetWidth / float(img.size[0]))
|
||||||
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
heightSize = int((float(img.size[1]) * float(widthPercent)))
|
||||||
@@ -100,7 +106,11 @@ def splitImage(work):
|
|||||||
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
|
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
|
||||||
Image.MAX_IMAGE_PIXELS = 1000000000
|
Image.MAX_IMAGE_PIXELS = 1000000000
|
||||||
imgOrg = Image.open(filePath).convert('RGB')
|
imgOrg = Image.open(filePath).convert('RGB')
|
||||||
imgProcess = Image.open(filePath).convert('1')
|
# I experimented with custom vertical edge kernel [-1, 2, -1] but got poor results
|
||||||
|
imgEdges = Image.open(filePath).convert('L').filter(ImageFilter.FIND_EDGES)
|
||||||
|
# threshold of 8 is too high. 5 is too low.
|
||||||
|
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
|
||||||
|
|
||||||
widthImg, heightImg = imgOrg.size
|
widthImg, heightImg = imgOrg.size
|
||||||
if heightImg > opt.height:
|
if heightImg > opt.height:
|
||||||
if opt.debug:
|
if opt.debug:
|
||||||
@@ -111,47 +121,71 @@ def splitImage(work):
|
|||||||
yWork = 0
|
yWork = 0
|
||||||
panelDetected = False
|
panelDetected = False
|
||||||
panels = []
|
panels = []
|
||||||
|
# check git history for how these constant values changed
|
||||||
|
h_pad = int(widthImg / 20)
|
||||||
|
v_pad = int(widthImg / 80)
|
||||||
|
if v_pad % 2:
|
||||||
|
v_pad += 1
|
||||||
while yWork < heightImg:
|
while yWork < heightImg:
|
||||||
tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4))
|
tmpImg = imgProcess.crop((h_pad, yWork, widthImg - h_pad, yWork + v_pad))
|
||||||
solid = detectSolid(tmpImg)
|
solid = detectSolid(tmpImg)
|
||||||
if not solid and not panelDetected:
|
if not solid and not panelDetected:
|
||||||
panelDetected = True
|
panelDetected = True
|
||||||
panelY1 = yWork - 2
|
panelY1 = yWork
|
||||||
if heightImg - yWork <= 5:
|
if heightImg - yWork <= (v_pad // 2):
|
||||||
if not solid and panelDetected:
|
if not solid and panelDetected:
|
||||||
panelY2 = heightImg
|
panelY2 = heightImg
|
||||||
panelDetected = False
|
panelDetected = False
|
||||||
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||||
if solid and panelDetected:
|
if solid and panelDetected:
|
||||||
panelDetected = False
|
panelDetected = False
|
||||||
panelY2 = yWork + 6
|
panelY2 = yWork
|
||||||
|
# skip short panel at start
|
||||||
|
if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
|
||||||
|
continue
|
||||||
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||||
yWork += 5
|
yWork += v_pad // 2
|
||||||
|
|
||||||
|
max_width = 1072
|
||||||
|
virtual_width = min((max_width, opt.width, widthImg))
|
||||||
|
if opt.width > max_width:
|
||||||
|
virtual_height = int(opt.height/max_width*virtual_width)
|
||||||
|
else:
|
||||||
|
virtual_height = int(opt.height/opt.width*virtual_width)
|
||||||
|
opt.height = virtual_height
|
||||||
|
|
||||||
# Split too big panels
|
# Split too big panels
|
||||||
panelsProcessed = []
|
panelsProcessed = []
|
||||||
for panel in panels:
|
for panel in panels:
|
||||||
|
# 1.52 too high
|
||||||
if panel[2] <= opt.height * 1.5:
|
if panel[2] <= opt.height * 1.5:
|
||||||
panelsProcessed.append(panel)
|
panelsProcessed.append(panel)
|
||||||
elif panel[2] < opt.height * 2:
|
elif panel[2] <= opt.height * 2:
|
||||||
diff = panel[2] - opt.height
|
diff = panel[2] - opt.height
|
||||||
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
|
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
|
||||||
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
||||||
else:
|
else:
|
||||||
parts = round(panel[2] / opt.height)
|
# split super long panels with overlap
|
||||||
|
parts = math.ceil(panel[2] / opt.height)
|
||||||
diff = panel[2] // parts
|
diff = panel[2] // parts
|
||||||
for x in range(0, parts):
|
panelsProcessed.append((panel[0], panel[0] + opt.height, opt.height))
|
||||||
panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff))
|
for x in range(1, parts - 1):
|
||||||
|
start = panel[0] + (x * diff)
|
||||||
|
panelsProcessed.append((start, start + opt.height, opt.height))
|
||||||
|
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
||||||
|
|
||||||
if opt.debug:
|
if opt.debug:
|
||||||
for panel in panelsProcessed:
|
for panel in panelsProcessed:
|
||||||
draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255))
|
draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255))
|
||||||
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
|
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
|
||||||
|
# debugImage.show()
|
||||||
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
|
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
|
||||||
|
|
||||||
# Create virtual pages
|
# Create virtual pages
|
||||||
pages = []
|
pages = []
|
||||||
currentPage = []
|
currentPage = []
|
||||||
|
# TODO: 1.25 way too high, 1.1 too high, 1.05 slightly too high(?), optimized for 2 page landscape reading
|
||||||
|
# opt.height = max_height = virtual_height * 1.00
|
||||||
pageLeft = opt.height
|
pageLeft = opt.height
|
||||||
panelNumber = 0
|
panelNumber = 0
|
||||||
for panel in panelsProcessed:
|
for panel in panelsProcessed:
|
||||||
@@ -188,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)
|
||||||
|
|
||||||
@@ -200,6 +234,8 @@ def main(argv=None, qtgui=None):
|
|||||||
" with spaces.")
|
" with spaces.")
|
||||||
main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
|
main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
|
||||||
help="Height of the target device screen")
|
help="Height of the target device screen")
|
||||||
|
main_options.add_argument("-x", "--width", type=int, dest="width", default=0,
|
||||||
|
help="Width of the target device screen")
|
||||||
main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
|
main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
|
||||||
help="Overwrite source directory")
|
help="Overwrite source directory")
|
||||||
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
|
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
|
||||||
@@ -218,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)
|
|
||||||
copytree(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 = []
|
||||||
@@ -239,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)
|
||||||
@@ -252,7 +286,8 @@ 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)
|
||||||
for root, _, files in os.walk(targetDir, False):
|
for root, _, files in os.walk(targetDir, False):
|
||||||
for name in files:
|
for name in files:
|
||||||
if getImageFileName(name) is not None:
|
if getImageFileName(name) is not None:
|
||||||
@@ -261,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:
|
||||||
@@ -269,6 +304,7 @@ def main(argv=None, qtgui=None):
|
|||||||
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
|
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
|
||||||
splitWorkerPool.close()
|
splitWorkerPool.close()
|
||||||
splitWorkerPool.join()
|
splitWorkerPool.join()
|
||||||
|
dot_clean(targetDir)
|
||||||
if GUI and not GUI.conversionAlive:
|
if GUI and not GUI.conversionAlive:
|
||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
raise UserWarning("Conversion interrupted.")
|
raise UserWarning("Conversion interrupted.")
|
||||||
@@ -276,12 +312,9 @@ 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:
|
|
||||||
rmtree(sourceDir)
|
|
||||||
move(targetDir, sourceDir)
|
|
||||||
else:
|
else:
|
||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
raise UserWarning("Source directory is empty.")
|
raise UserWarning("C2P: Source directory is empty.")
|
||||||
else:
|
else:
|
||||||
raise UserWarning("Provided input is not a directory.")
|
raise UserWarning("Provided input is not a directory.")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -36,21 +38,22 @@ class ComicArchive:
|
|||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
if not os.path.isfile(self.filepath):
|
if not os.path.isfile(self.filepath):
|
||||||
raise OSError('File not found.')
|
raise OSError('File not found.')
|
||||||
|
self.dirname, self.basename = os.path.split(filepath)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def type(self):
|
def type(self):
|
||||||
extraction_commands = [
|
extraction_commands = [
|
||||||
[SEVENZIP, 'l', '-y', '-p1', self.filepath],
|
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||||
]
|
]
|
||||||
|
|
||||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
extraction_commands.append(
|
extraction_commands.append(
|
||||||
['unrar', 'l', '-y', '-p1', self.filepath],
|
['unrar', 'l', '-y', '-p1', self.basename],
|
||||||
)
|
)
|
||||||
|
|
||||||
for cmd in extraction_commands:
|
for cmd in extraction_commands:
|
||||||
try:
|
try:
|
||||||
process = subprocess_run(cmd, capture_output=True, check=True)
|
process = subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||||
for line in process.stdout.splitlines():
|
for line in process.stdout.splitlines():
|
||||||
if b'Type =' in line:
|
if b'Type =' in line:
|
||||||
return line.rstrip().decode().split(' = ')[1].upper()
|
return line.rstrip().decode().split(' = ')[1].upper()
|
||||||
@@ -65,28 +68,31 @@ class ComicArchive:
|
|||||||
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.filepath, '-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.filepath],
|
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
|
||||||
]
|
]
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
extraction_commands.append(
|
extraction_commands.append(
|
||||||
['unar', self.filepath, '-D', '-f', '-o', targetdir]
|
['unar', self.basename, '-D', '-f', '-o', targetdir]
|
||||||
)
|
)
|
||||||
|
|
||||||
extraction_commands.reverse()
|
extraction_commands.reverse()
|
||||||
|
|
||||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
extraction_commands.append(
|
extraction_commands.append(
|
||||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]
|
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||||
)
|
)
|
||||||
|
|
||||||
for cmd in extraction_commands:
|
for cmd in extraction_commands:
|
||||||
try:
|
try:
|
||||||
subprocess_run(cmd, capture_output=True, check=True)
|
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||||
return targetdir
|
return targetdir
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
missing.append(cmd[0])
|
missing.append(cmd[0])
|
||||||
@@ -101,14 +107,14 @@ class ComicArchive:
|
|||||||
def addFile(self, sourcefile):
|
def addFile(self, sourcefile):
|
||||||
if self.type in ['RAR', 'RAR5']:
|
if self.type in ['RAR', 'RAR5']:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
process = subprocess_run([SEVENZIP, 'a', '-y', self.filepath, sourcefile],
|
process = subprocess_run([SEVENZIP, 'a', '-y', self.basename, sourcefile],
|
||||||
stdout=PIPE, stderr=STDOUT)
|
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError('Failed to add the file.')
|
raise OSError('Failed to add the file.')
|
||||||
|
|
||||||
def extractMetadata(self):
|
def extractMetadata(self):
|
||||||
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
|
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.basename, 'ComicInfo.xml'],
|
||||||
stdout=PIPE, stderr=STDOUT)
|
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError(EXTRACTION_ERROR)
|
raise OSError(EXTRACTION_ERROR)
|
||||||
try:
|
try:
|
||||||
@@ -120,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)
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ 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 .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
|
||||||
|
|
||||||
AUTO_CROP_THRESHOLD = 0.015
|
AUTO_CROP_THRESHOLD = 0.015
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
class ProfileData:
|
class ProfileData:
|
||||||
@@ -83,22 +86,28 @@ class ProfileData:
|
|||||||
]
|
]
|
||||||
|
|
||||||
ProfilesKindleEBOK = {
|
ProfilesKindleEBOK = {
|
||||||
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
|
|
||||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
|
||||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
|
||||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
|
||||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
|
||||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
|
||||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesKindlePDOC = {
|
ProfilesKindlePDOC = {
|
||||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
|
||||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
|
||||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
|
||||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
|
||||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
|
||||||
|
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||||
|
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
|
||||||
|
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
|
||||||
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||||
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
|
||||||
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||||
|
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||||
|
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||||
|
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||||
|
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||||
|
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
|
||||||
|
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||||
|
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesKindle = {
|
ProfilesKindle = {
|
||||||
@@ -107,34 +116,35 @@ class ProfileData:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProfilesKobo = {
|
ProfilesKobo = {
|
||||||
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
|
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
|
||||||
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
|
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
|
||||||
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
|
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
|
||||||
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
|
||||||
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
|
||||||
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
|
||||||
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
|
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
|
||||||
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
|
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
|
||||||
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
|
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
|
||||||
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
|
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
|
||||||
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
|
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
|
||||||
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
|
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
|
||||||
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
|
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
|
||||||
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
|
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
|
||||||
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
|
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesRemarkable = {
|
ProfilesRemarkable = {
|
||||||
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
|
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
|
||||||
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
|
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
|
||||||
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
|
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
|
||||||
|
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
Profiles = {
|
Profiles = {
|
||||||
**ProfilesKindle,
|
**ProfilesKindle,
|
||||||
**ProfilesKobo,
|
**ProfilesKobo,
|
||||||
**ProfilesRemarkable,
|
**ProfilesRemarkable,
|
||||||
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
'OTHER': ("Other", (0, 0), Palette16, 1.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -148,8 +158,9 @@ 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()
|
||||||
self.image = Image.open(srcImgPath)
|
with Image.open(srcImgPath) as im:
|
||||||
|
self.image = im.copy()
|
||||||
|
|
||||||
self.fill = self.fillCheck()
|
self.fill = self.fillCheck()
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
@@ -186,10 +197,13 @@ class ComicPageParser:
|
|||||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||||
spread = self.image
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
|
if not self.opt.rotateright:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
|
else:
|
||||||
|
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
self.payload.append(['R', self.source, spread, self.fill])
|
||||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||||
if self.opt.splitter != 1:
|
if self.opt.splitter != 1 and width / height < 2:
|
||||||
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)
|
||||||
@@ -204,10 +218,13 @@ class ComicPageParser:
|
|||||||
pagetwo = self.image.crop(rightbox)
|
pagetwo = self.image.crop(rightbox)
|
||||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||||
if self.opt.splitter > 0:
|
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 2):
|
||||||
spread = self.image
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
|
if not self.opt.rotateright:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
|
else:
|
||||||
|
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
self.payload.append(['R', self.source, spread, self.fill])
|
||||||
else:
|
else:
|
||||||
self.payload.append(['N', self.source, self.image, self.fill])
|
self.payload.append(['N', self.source, self.image, self.fill])
|
||||||
@@ -257,9 +274,11 @@ class ComicPage:
|
|||||||
_, 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
|
||||||
self.image = image.convert("RGB")
|
self.image = image.convert("RGB")
|
||||||
|
self.color = self.colorCheck()
|
||||||
|
self.colorOutput = self.color and self.opt.forcecolor
|
||||||
self.fill = fill
|
self.fill = fill
|
||||||
self.rotated = False
|
self.rotated = False
|
||||||
self.orgPath = os.path.join(path[0], path[1])
|
self.orgPath = os.path.join(path[0], path[1])
|
||||||
@@ -278,25 +297,92 @@ 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
|
||||||
img = self.image.convert("YCbCr")
|
if self.opt.webtoon:
|
||||||
_, cb, cr = img.split()
|
return True
|
||||||
|
if self.calculate_color():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 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
|
||||||
|
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
|
||||||
|
if cutoff == (0, 0):
|
||||||
|
return cb_hist, cr_hist
|
||||||
|
|
||||||
|
for h in cb_hist, cr_hist:
|
||||||
|
# get number of pixels
|
||||||
|
n = sum(h)
|
||||||
|
# remove cutoff% pixels from the low end
|
||||||
|
cut = int(n * cutoff[0] // 100)
|
||||||
|
for lo in range(256):
|
||||||
|
if cut > h[lo]:
|
||||||
|
cut = cut - h[lo]
|
||||||
|
h[lo] = 0
|
||||||
|
else:
|
||||||
|
h[lo] -= cut
|
||||||
|
cut = 0
|
||||||
|
if cut <= 0:
|
||||||
|
break
|
||||||
|
# remove cutoff% samples from the high end
|
||||||
|
cut = int(n * cutoff[1] // 100)
|
||||||
|
for hi in range(255, -1, -1):
|
||||||
|
if cut > h[hi]:
|
||||||
|
cut = cut - h[hi]
|
||||||
|
h[hi] = 0
|
||||||
|
else:
|
||||||
|
h[hi] -= cut
|
||||||
|
cut = 0
|
||||||
|
if cut <= 0:
|
||||||
|
break
|
||||||
|
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_hist = cb.histogram()
|
|
||||||
cr_hist = cr.histogram()
|
|
||||||
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
||||||
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
||||||
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0
|
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
|
||||||
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0
|
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
|
||||||
|
|
||||||
SPREAD_THRESHOLD=20
|
# bias adjustment, don't go lower than 7
|
||||||
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
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):
|
||||||
|
img = self.image.convert("YCbCr")
|
||||||
|
_, cb, cr = img.split()
|
||||||
|
cb_hist_original = cb.histogram()
|
||||||
|
cr_hist_original = cr.histogram()
|
||||||
|
|
||||||
|
# you can increase 22 but don't increase 10. 4 maybe can go higher
|
||||||
|
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
|
||||||
|
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
|
||||||
|
if done:
|
||||||
|
return decision
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def saveToDir(self):
|
def saveToDir(self):
|
||||||
try:
|
try:
|
||||||
@@ -320,25 +406,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:
|
||||||
|
if self.opt.webp_output:
|
||||||
|
targetPath += '.webp'
|
||||||
|
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
|
||||||
|
elif self.opt.mozjpeg:
|
||||||
targetPath += '.jpg'
|
targetPath += '.jpg'
|
||||||
if self.opt.mozjpeg:
|
|
||||||
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):
|
||||||
@@ -353,9 +446,40 @@ class ComicPage:
|
|||||||
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
||||||
|
|
||||||
def autocontrastImage(self):
|
def autocontrastImage(self):
|
||||||
# autocontrast on non grayscale images has unexpected results
|
if self.opt.webtoon:
|
||||||
# since it autocontrasts each color channel separately
|
return
|
||||||
self.image = ImageOps.autocontrast(self.image)
|
if self.opt.noautocontrast:
|
||||||
|
return
|
||||||
|
if self.color and not self.opt.colorautocontrast:
|
||||||
|
return
|
||||||
|
|
||||||
|
# if image is extremely low contrast, that was probably intentional
|
||||||
|
extrema = self.image.convert('L').getextrema()
|
||||||
|
if extrema[1] - extrema[0] < (255 - 32 * 3):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.opt.autolevel:
|
||||||
|
self.autolevelImage()
|
||||||
|
|
||||||
|
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||||
|
|
||||||
|
def autolevelImage(self):
|
||||||
|
img = self.image
|
||||||
|
if self.color:
|
||||||
|
img = self.image.convert("YCbCr")
|
||||||
|
y, cb, cr = img.split()
|
||||||
|
img = y
|
||||||
|
else:
|
||||||
|
img = img.convert('L')
|
||||||
|
h = img.histogram()
|
||||||
|
most_common_dark_pixel_count = max(h[:64])
|
||||||
|
black_point = h.index(most_common_dark_pixel_count)
|
||||||
|
bp = black_point
|
||||||
|
img = img.point(lambda p: p if p > bp else bp)
|
||||||
|
if self.color:
|
||||||
|
self.image = Image.merge(mode='YCbCr', bands=[img, cb, cr]).convert('RGB')
|
||||||
|
else:
|
||||||
|
self.image = img
|
||||||
|
|
||||||
def convertToGrayscale(self):
|
def convertToGrayscale(self):
|
||||||
self.image = self.image.convert('L')
|
self.image = self.image.convert('L')
|
||||||
@@ -369,15 +493,20 @@ class ComicPage:
|
|||||||
palImg.putpalette(self.palette)
|
palImg.putpalette(self.palette)
|
||||||
self.image = self.image.quantize(palette=palImg)
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
def optimizeForDisplay(self, reducerainbow):
|
def optimizeForDisplay(self, eraserainbow, is_color):
|
||||||
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
|
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
|
||||||
if reducerainbow and not self.color:
|
if eraserainbow and all(dim > 1 for dim in self.image.size):
|
||||||
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100)
|
self.image = erase_rainbow_artifacts(self.image, is_color)
|
||||||
self.image = self.image.filter(unsharpFilter)
|
|
||||||
self.image = self.image.filter(ImageFilter.BoxBlur(1.0))
|
|
||||||
self.image = self.image.filter(unsharpFilter)
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -386,15 +515,17 @@ class ComicPage:
|
|||||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||||
pass
|
pass
|
||||||
else: # if image bigger than device resolution or smaller with upscaling
|
else: # if image bigger than device resolution or smaller with upscaling
|
||||||
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
|
||||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||||
elif (self.opt.format == 'CBZ' 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') or self.opt.kfx) and not self.opt.white_borders:
|
||||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||||
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
|
||||||
@@ -414,12 +545,20 @@ class ComicPage:
|
|||||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
|
w, h = self.image.size
|
||||||
|
left, upper, right, lower = bbox
|
||||||
|
# don't crop more than 10% of image
|
||||||
|
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
||||||
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.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
|
w, h = self.image.size
|
||||||
|
left, upper, right, lower = bbox
|
||||||
|
# don't crop more than 10% of image
|
||||||
|
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
def cropInterPanelEmptySections(self, direction):
|
def cropInterPanelEmptySections(self, direction):
|
||||||
@@ -437,14 +576,20 @@ 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')
|
||||||
|
if self.options.smartcovercrop:
|
||||||
self.crop_main_cover()
|
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)
|
||||||
|
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)
|
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
def crop_main_cover(self):
|
def crop_main_cover(self):
|
||||||
@@ -454,7 +599,7 @@ class Cover:
|
|||||||
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.3:
|
elif w / h > 1.34:
|
||||||
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:
|
||||||
@@ -463,7 +608,7 @@ class Cover:
|
|||||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
def save_to_epub(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)
|
||||||
@@ -477,10 +622,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)
|
||||||
dot_cover = Path(target).with_stem('._' + Path(target).stem)
|
|
||||||
if os.path.exists(dot_cover):
|
|
||||||
os.remove(dot_cover)
|
|
||||||
except IOError:
|
except IOError:
|
||||||
raise RuntimeError('Failed to save cover.')
|
raise RuntimeError('Failed to save cover.')
|
||||||
|
|
||||||
@@ -488,6 +630,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).
|
||||||
@@ -19,10 +21,10 @@ def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "bo
|
|||||||
img_temp = img
|
img_temp = img
|
||||||
|
|
||||||
if img.mode != 'L':
|
if img.mode != 'L':
|
||||||
img_temp = ImageOps.grayscale(img)
|
img_temp = ImageOps.grayscale(img_temp)
|
||||||
|
|
||||||
if background_color != 'white':
|
if background_color != 'white':
|
||||||
img_temp = ImageOps.invert(img)
|
img_temp = ImageOps.invert(img_temp)
|
||||||
|
|
||||||
img_mat = np.array(img)
|
img_mat = np.array(img)
|
||||||
|
|
||||||
|
|||||||
@@ -123,4 +123,4 @@ class MetadataParser:
|
|||||||
cbx.addFile(tmpXML)
|
cbx.addFile(tmpXML)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UserWarning(e)
|
raise UserWarning(e)
|
||||||
rmtree(workdir)
|
rmtree(workdir, True)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -52,6 +54,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
|
|||||||
'''
|
'''
|
||||||
threshold = threshold_from_power(power)
|
threshold = threshold_from_power(power)
|
||||||
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
||||||
|
ignore_pixels_near_edge(bw_img)
|
||||||
bw_bbox = bw_img.getbbox()
|
bw_bbox = bw_img.getbbox()
|
||||||
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
||||||
return None
|
return None
|
||||||
@@ -142,8 +145,27 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
|
|||||||
threshold = threshold_from_power(power)
|
threshold = threshold_from_power(power)
|
||||||
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
||||||
|
|
||||||
|
ignore_pixels_near_edge(bw_img)
|
||||||
|
|
||||||
return bw_img.getbbox()
|
return bw_img.getbbox()
|
||||||
|
|
||||||
|
def ignore_pixels_near_edge(bw_img):
|
||||||
|
w, h = bw_img.size
|
||||||
|
edge_bbox = [
|
||||||
|
(0, 0, w, int(0.02 * h)),
|
||||||
|
(0, int(0.98 * h), w, h),
|
||||||
|
(0, 0, int(0.02 * w), h),
|
||||||
|
(int(0.98 * w), 0, w, h)
|
||||||
|
]
|
||||||
|
for box in edge_bbox:
|
||||||
|
edge = bw_img.crop(box)
|
||||||
|
h = edge.histogram()
|
||||||
|
if not edge.height or not edge.width:
|
||||||
|
continue
|
||||||
|
imperfections = h[255] / (edge.height * edge.width)
|
||||||
|
if imperfections > 0 and imperfections < .02:
|
||||||
|
bw_img.paste(im=0, box=box)
|
||||||
|
|
||||||
|
|
||||||
def box_intersect(box1, box2, max_dist):
|
def box_intersect(box1, box2, max_dist):
|
||||||
return not (box2[0]-max_dist[0] > box1[1]
|
return not (box2[0]-max_dist[0] > box1[1]
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from random import choice
|
|
||||||
from string import ascii_uppercase, digits
|
|
||||||
|
|
||||||
# skip stray images a few pixels in size in some PDFs
|
# skip stray images a few pixels in size in some PDFs
|
||||||
# typical images are many thousands in length
|
# typical images are many thousands in length
|
||||||
@@ -32,10 +30,9 @@ STRAY_IMAGE_LENGTH_THRESHOLD = 300
|
|||||||
|
|
||||||
|
|
||||||
class PdfJpgExtract:
|
class PdfJpgExtract:
|
||||||
def __init__(self, fname):
|
def __init__(self, fname, fullPath):
|
||||||
self.fname = fname
|
self.fname = fname
|
||||||
self.filename = os.path.splitext(fname)
|
self.path = fullPath
|
||||||
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
|
|
||||||
|
|
||||||
def getPath(self):
|
def getPath(self):
|
||||||
return self.path
|
return self.path
|
||||||
@@ -48,7 +45,6 @@ class PdfJpgExtract:
|
|||||||
endfix = 2
|
endfix = 2
|
||||||
i = 0
|
i = 0
|
||||||
njpg = 0
|
njpg = 0
|
||||||
os.makedirs(self.path)
|
|
||||||
while True:
|
while True:
|
||||||
istream = pdf.find(b"stream", i)
|
istream = pdf.find(b"stream", i)
|
||||||
if istream < 0:
|
if istream < 0:
|
||||||
@@ -71,9 +67,9 @@ class PdfJpgExtract:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
jpg = pdf[istart:iend]
|
jpg = pdf[istart:iend]
|
||||||
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
|
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
|
||||||
jpgfile.write(jpg)
|
jpgfile.write(jpg)
|
||||||
jpgfile.close()
|
jpgfile.close()
|
||||||
njpg += 1
|
njpg += 1
|
||||||
|
|
||||||
return self.path, njpg
|
return njpg
|
||||||
|
|||||||
249
kindlecomicconverter/rainbow_artifacts_eraser.py
Normal file
249
kindlecomicconverter/rainbow_artifacts_eraser.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFile
|
||||||
|
|
||||||
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
|
|
||||||
|
def fourier_transform_image(img):
|
||||||
|
"""
|
||||||
|
Memory-optimized version that modifies the array in place when possible.
|
||||||
|
"""
|
||||||
|
# Convert with minimal copy
|
||||||
|
img_array = np.asarray(img, dtype=np.float32)
|
||||||
|
|
||||||
|
# Use rfft2 if the image is real to save memory
|
||||||
|
# and computation time (approximately 2x faster)
|
||||||
|
fft_result = np.fft.rfft2(img_array)
|
||||||
|
|
||||||
|
return fft_result
|
||||||
|
|
||||||
|
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
|
||||||
|
angle_tolerance=10, attenuation_factor=0.10):
|
||||||
|
"""
|
||||||
|
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
|
||||||
|
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
|
||||||
|
target_angle: Target angle in degrees (default: 135)
|
||||||
|
angle_tolerance: Angular tolerance in degrees (default: 15)
|
||||||
|
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: Modified FFT with applied attenuation (same format as input)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get dimensions of the rfft2 result
|
||||||
|
if fft_spectrum.ndim == 2:
|
||||||
|
height, width_rfft = fft_spectrum.shape
|
||||||
|
else: # 3D array (color channels)
|
||||||
|
height, width_rfft = fft_spectrum.shape[:2]
|
||||||
|
|
||||||
|
# For rfft2, the original width is (width_rfft - 1) * 2
|
||||||
|
width_original = (width_rfft - 1) * 2
|
||||||
|
|
||||||
|
# Create frequency grids for rfft2 format
|
||||||
|
freq_y = np.fft.fftfreq(height, d=1.0)
|
||||||
|
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
|
||||||
|
|
||||||
|
|
||||||
|
# Use broadcasting to create grids without meshgrid (more efficient)
|
||||||
|
freq_y_grid = freq_y.reshape(-1, 1) # Column
|
||||||
|
freq_x_grid = freq_x.reshape(1, -1) # Row
|
||||||
|
|
||||||
|
# Calculate squared radial frequencies (avoid sqrt)
|
||||||
|
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
|
||||||
|
freq_threshold_sq = freq_threshold**2
|
||||||
|
|
||||||
|
# Frequency condition
|
||||||
|
freq_condition = freq_radial_sq >= freq_threshold_sq
|
||||||
|
|
||||||
|
# Early exit if no frequency satisfies the condition
|
||||||
|
if not np.any(freq_condition):
|
||||||
|
return fft_spectrum
|
||||||
|
|
||||||
|
# Calculate angles only where necessary
|
||||||
|
# Use atan2 directly with broadcasting
|
||||||
|
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
|
||||||
|
|
||||||
|
# Convert to degrees and normalize in a single operation
|
||||||
|
angles_deg = np.rad2deg(angles_rad) % 360
|
||||||
|
|
||||||
|
# Calculation of complementary angle
|
||||||
|
target_angle_2 = (target_angle + 180) % 360
|
||||||
|
|
||||||
|
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
|
||||||
|
target_angle_3 = (target_angle + 90) % 360
|
||||||
|
target_angle_4 = (target_angle_3 + 180) % 360
|
||||||
|
|
||||||
|
# Create angular conditions in a vectorized way
|
||||||
|
angle_condition = np.zeros_like(angles_deg, dtype=bool)
|
||||||
|
|
||||||
|
# Process both angles simultaneously
|
||||||
|
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
|
||||||
|
min_angle = (angle - angle_tolerance) % 360
|
||||||
|
max_angle = (angle + angle_tolerance) % 360
|
||||||
|
|
||||||
|
if min_angle > max_angle: # Interval crosses 0°
|
||||||
|
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
|
||||||
|
else: # Normal interval
|
||||||
|
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
|
||||||
|
|
||||||
|
# Combine conditions
|
||||||
|
combined_condition = freq_condition & angle_condition
|
||||||
|
|
||||||
|
# Apply attenuation directly (avoid creating a full mask)
|
||||||
|
if attenuation_factor == 0:
|
||||||
|
# Special case: complete suppression
|
||||||
|
if fft_spectrum.ndim == 2:
|
||||||
|
fft_spectrum[combined_condition] = 0
|
||||||
|
else: # 3D array
|
||||||
|
fft_spectrum[combined_condition, :] = 0
|
||||||
|
return fft_spectrum
|
||||||
|
elif attenuation_factor == 1:
|
||||||
|
# Special case: no attenuation
|
||||||
|
return fft_spectrum
|
||||||
|
else:
|
||||||
|
# General case: partial attenuation
|
||||||
|
if fft_spectrum.ndim == 2:
|
||||||
|
fft_spectrum[combined_condition] *= attenuation_factor
|
||||||
|
else: # 3D array
|
||||||
|
fft_spectrum[combined_condition, :] *= attenuation_factor
|
||||||
|
return fft_spectrum
|
||||||
|
|
||||||
|
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
|
||||||
|
"""
|
||||||
|
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fft_spectrum: Fourier transform result (complex array from rfft2)
|
||||||
|
is_color: Boolean indicating if the image is to be treated as color
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: Reconstructed image
|
||||||
|
"""
|
||||||
|
# Perform inverse Fourier transform with original shape if provided
|
||||||
|
if original_shape is not None:
|
||||||
|
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
|
||||||
|
else:
|
||||||
|
img_reconstructed = np.fft.irfft2(fft_spectrum)
|
||||||
|
|
||||||
|
# Normalize values between 0 and 255
|
||||||
|
img_reconstructed = np.clip(img_reconstructed, 0, 255)
|
||||||
|
img_reconstructed = img_reconstructed.astype(np.uint8)
|
||||||
|
|
||||||
|
# Convert to PIL image
|
||||||
|
if is_color and img_reconstructed.ndim == 3:
|
||||||
|
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
|
||||||
|
else:
|
||||||
|
pil_image = Image.fromarray(img_reconstructed, mode='L')
|
||||||
|
|
||||||
|
return pil_image
|
||||||
|
|
||||||
|
def rgb_to_yuv(rgb_array):
|
||||||
|
"""
|
||||||
|
Convert RGB to YUV color space.
|
||||||
|
Y = luminance, U and V = chrominance
|
||||||
|
"""
|
||||||
|
# Coefficients for RGB to YUV conversion
|
||||||
|
rgb_to_yuv_matrix = np.array([
|
||||||
|
[0.299, 0.587, 0.114], # Y
|
||||||
|
[-0.14713, -0.28886, 0.436], # U
|
||||||
|
[0.615, -0.51499, -0.10001] # V
|
||||||
|
])
|
||||||
|
|
||||||
|
# Reshape for matrix multiplication
|
||||||
|
original_shape = rgb_array.shape
|
||||||
|
rgb_flat = rgb_array.reshape(-1, 3)
|
||||||
|
|
||||||
|
# Apply transformation
|
||||||
|
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
|
||||||
|
|
||||||
|
# Reshape back
|
||||||
|
yuv_array = yuv_flat.reshape(original_shape)
|
||||||
|
|
||||||
|
return yuv_array
|
||||||
|
|
||||||
|
def yuv_to_rgb(yuv_array):
|
||||||
|
"""
|
||||||
|
Convert YUV to RGB color space.
|
||||||
|
"""
|
||||||
|
# Coefficients for YUV to RGB conversion
|
||||||
|
yuv_to_rgb_matrix = np.array([
|
||||||
|
[1.0, 0.0, 1.13983], # R
|
||||||
|
[1.0, -0.39465, -0.58060], # G
|
||||||
|
[1.0, 2.03211, 0.0] # B
|
||||||
|
])
|
||||||
|
|
||||||
|
# Reshape for matrix multiplication
|
||||||
|
original_shape = yuv_array.shape
|
||||||
|
yuv_flat = yuv_array.reshape(-1, 3)
|
||||||
|
|
||||||
|
# Apply transformation
|
||||||
|
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
|
||||||
|
|
||||||
|
# Reshape back
|
||||||
|
rgb_array = rgb_flat.reshape(original_shape)
|
||||||
|
|
||||||
|
return rgb_array
|
||||||
|
|
||||||
|
def erase_rainbow_artifacts(img, is_color):
|
||||||
|
"""
|
||||||
|
Remove rainbow artifacts from grayscale or color images.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
img: PIL Image (grayscale or RGB)
|
||||||
|
is_color: Boolean indicating if the image is to be treated as color
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image: Cleaned image
|
||||||
|
"""
|
||||||
|
# Auto-detect color mode if not specified
|
||||||
|
if is_color is None:
|
||||||
|
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
|
||||||
|
|
||||||
|
if is_color and img.mode in ('RGB', 'RGBA'):
|
||||||
|
# Convert to RGB if needed
|
||||||
|
if img.mode == 'RGBA':
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
|
# Convert to numpy array
|
||||||
|
img_array = np.array(img, dtype=np.float32)
|
||||||
|
|
||||||
|
# Convert to YUV color space
|
||||||
|
yuv_array = rgb_to_yuv(img_array)
|
||||||
|
|
||||||
|
# Extract luminance channel (Y)
|
||||||
|
luminance = yuv_array[:, :, 0]
|
||||||
|
|
||||||
|
# Process only the luminance channel
|
||||||
|
fft_spectrum = fourier_transform_image(luminance)
|
||||||
|
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
||||||
|
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
|
||||||
|
|
||||||
|
# Normalize and clip luminance
|
||||||
|
clean_luminance = np.clip(clean_luminance, 0, 255)
|
||||||
|
|
||||||
|
# Replace luminance in YUV array
|
||||||
|
yuv_array[:, :, 0] = clean_luminance
|
||||||
|
|
||||||
|
# Convert back to RGB
|
||||||
|
rgb_array = yuv_to_rgb(yuv_array)
|
||||||
|
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
# Convert back to PIL image
|
||||||
|
clean_image = Image.fromarray(rgb_array, mode='RGB')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Grayscale processing (original behavior)
|
||||||
|
if img.mode != 'L':
|
||||||
|
img = img.convert('L')
|
||||||
|
|
||||||
|
# Get original image dimensions
|
||||||
|
original_shape = (img.height, img.width)
|
||||||
|
|
||||||
|
fft_spectrum = fourier_transform_image(img)
|
||||||
|
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
||||||
|
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
|
||||||
|
|
||||||
|
return clean_image
|
||||||
@@ -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)
|
||||||
@@ -45,6 +48,14 @@ class HTMLStripper(HTMLParser):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def dot_clean(filetree):
|
||||||
|
for root, _, files in os.walk(filetree, topdown=False):
|
||||||
|
for name in files:
|
||||||
|
if name.startswith('._') or name == '.DS_Store':
|
||||||
|
if os.path.exists(os.path.join(root, name)):
|
||||||
|
os.remove(os.path.join(root, name))
|
||||||
|
|
||||||
|
|
||||||
def getImageFileName(imgfile):
|
def getImageFileName(imgfile):
|
||||||
name, ext = os.path.splitext(imgfile)
|
name, ext = os.path.splitext(imgfile)
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
@@ -90,10 +101,10 @@ def dependencyCheck(level):
|
|||||||
if level > 2:
|
if level > 2:
|
||||||
try:
|
try:
|
||||||
from PySide6.QtCore import qVersion as qtVersion
|
from PySide6.QtCore import qVersion as qtVersion
|
||||||
if Version('6.5.1') > Version(qtVersion()):
|
if Version('6.0.0') > Version(qtVersion()):
|
||||||
missing.append('PySide 6.5.1+')
|
missing.append('PySide 6.0.0')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('PySide 6.5.1+')
|
missing.append('PySide 6.0.0+')
|
||||||
try:
|
try:
|
||||||
import raven
|
import raven
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -116,10 +127,16 @@ def dependencyCheck(level):
|
|||||||
missing.append('python-slugify 1.2.1+')
|
missing.append('python-slugify 1.2.1+')
|
||||||
try:
|
try:
|
||||||
from PIL import __version__ as pillowVersion
|
from PIL import __version__ as pillowVersion
|
||||||
if Version('11.3.0') > Version(pillowVersion):
|
if Version('8.3.0') > Version(pillowVersion):
|
||||||
missing.append('Pillow 11.3.0+')
|
missing.append('Pillow 8.3.0+')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('Pillow 11.3.0+')
|
missing.append('Pillow 8.3.0+')
|
||||||
|
try:
|
||||||
|
from pymupdf import __version__ as pymupdfVersion
|
||||||
|
if Version('1.16.1') > Version(pymupdfVersion):
|
||||||
|
missing.append('PyMuPDF 1.16.1+')
|
||||||
|
except ImportError:
|
||||||
|
missing.append('PyMuPDF 1.16.1+')
|
||||||
if len(missing) > 0:
|
if len(missing) > 0:
|
||||||
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
11
requirements-docker.txt
Normal file
11
requirements-docker.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Pillow>=11.3.0
|
||||||
|
psutil>=5.9.5
|
||||||
|
requests>=2.31.0
|
||||||
|
python-slugify>=1.2.1
|
||||||
|
packaging>=23.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
|
||||||
12
requirements-osx-legacy.txt
Normal file
12
requirements-osx-legacy.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PySide6==6.4.3
|
||||||
|
Pillow>=11.3.0
|
||||||
|
psutil>=5.9.5
|
||||||
|
requests>=2.31.0
|
||||||
|
python-slugify>=1.2.1
|
||||||
|
raven>=6.0.0
|
||||||
|
packaging>=23.2
|
||||||
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
|
natsort>=8.4.0
|
||||||
|
distro>=1.8.0
|
||||||
|
numpy<2
|
||||||
|
PyMuPDF>=1.26.1
|
||||||
12
requirements-win7.txt
Normal file
12
requirements-win7.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PySide6==6.1.3
|
||||||
|
Pillow>=9
|
||||||
|
psutil>=5.9.5
|
||||||
|
requests>=2.31.0
|
||||||
|
python-slugify>=1.2.1
|
||||||
|
raven>=6.0.0
|
||||||
|
packaging>=23.2
|
||||||
|
mozjpeg-lossless-optimization>=1.2.0
|
||||||
|
natsort>=8.4.0
|
||||||
|
distro>=1.8.0
|
||||||
|
numpy==1.23.0
|
||||||
|
PyMuPDF>=1.16
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
PySide6>=6.5.1
|
PySide6<6.10
|
||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
python-slugify>=1.2.1
|
python-slugify>=1.2.1,<9.0.0
|
||||||
raven>=6.0.0
|
raven>=6.0.0
|
||||||
packaging>=23.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>=1.22.4
|
numpy>=1.22.4
|
||||||
|
PyMuPDF>=1.18.0
|
||||||
|
|||||||
87
setup.py
87
setup.py
@@ -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,9 +40,16 @@ 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', '')
|
||||||
|
if min_os.startswith('10.1'):
|
||||||
|
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
||||||
|
else:
|
||||||
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif sys.platform == 'win32':
|
elif sys.platform == 'win32':
|
||||||
|
if os.getenv('WINDOWS_7'):
|
||||||
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_legacy_' + VERSION + ' -w --noupx kcc.py')
|
||||||
|
else:
|
||||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif sys.platform == 'linux':
|
elif sys.platform == 'linux':
|
||||||
@@ -50,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,
|
||||||
@@ -74,16 +148,17 @@ setuptools.setup(
|
|||||||
},
|
},
|
||||||
packages=['kindlecomicconverter'],
|
packages=['kindlecomicconverter'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyside6>=6.5.1',
|
'PySide6>=6.0.0',
|
||||||
'Pillow>=11.3.0',
|
'Pillow>=9.3.0',
|
||||||
'psutil>=5.9.5',
|
'psutil>=5.9.5',
|
||||||
|
'requests>=2.31.0',
|
||||||
'python-slugify>=1.2.1,<9.0.0',
|
'python-slugify>=1.2.1,<9.0.0',
|
||||||
'raven>=6.0.0',
|
'raven>=6.0.0',
|
||||||
'requests>=2.31.0',
|
'mozjpeg-lossless-optimization>=1.2.0',
|
||||||
'mozjpeg-lossless-optimization>=1.1.2',
|
|
||||||
'natsort>=8.4.0',
|
'natsort>=8.4.0',
|
||||||
'distro',
|
'distro>=1.8.0',
|
||||||
'numpy>=1.22.4'
|
'numpy>=1.22.4',
|
||||||
|
'PyMuPDF>=1.16.1',
|
||||||
],
|
],
|
||||||
classifiers=[],
|
classifiers=[],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user