diff --git a/.dockerignore b/.dockerignore
index 590e76611..a72e77b28 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,13 +1,40 @@
.git
.github
+
build
dist
KindleComicConverter.egg-info
+
.dockerignore
.gitignore
.travis.yml
+
Dockerfile
venv
+.venv
+__pycache__/
+*/__pycache__/
+*.pyc
+
*.md
-LICENSE.txt
+*.txt
+!requirements-docker.txt
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
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index fb4f6d107..44bb5351c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/docker-base-publish.yml b/.github/workflows/docker-base-publish.yml
deleted file mode 100644
index 7c692a46d..000000000
--- a/.github/workflows/docker-base-publish.yml
+++ /dev/null
@@ -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 }}
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index be58fd235..2d2a8015b 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -1,10 +1,10 @@
-name: Docker
+name: Build and Publish Docker Image
on:
workflow_dispatch:
push:
- # Publish semver tags as releases.
- tags: [ 'v*.*.*' ]
+ tags:
+ - 'v*.*.*'
# Don't trigger if it's just a documentation update
paths-ignore:
@@ -15,19 +15,53 @@ on:
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- - '.dockerignore'
-
jobs:
- build_and_push:
- uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
- with:
- 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 }}
- secrets:
- ghcr_token: ${{ secrets.GITHUB_TOKEN }}
+ build_and_publish_base_image:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Set Release Date
+ id: release_date
+ run: |
+ echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ images: ghcr.io/${{ github.repository_owner }}/kcc
+ # Always creates the "latest" tag
+ flavor: |
+ latest=true
+ tags: |
+ type=ref,event=tag
+ type=raw,value=${{ steps.release_date.outputs.release_date }}
+
+ - name: Build and push
+ uses: docker/build-push-action@v7
+ with:
+ platforms: linux/amd64,linux/arm64,linux/arm/v7
+ context: .
+ push: true
+ tags: |
+ ${{ steps.meta.outputs.tags }}
+ cache-from: |
+ type=registry,ref=ghcr.io/ciromattia/kcc:cache
+ type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
+ cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max
diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml
index dd8a96ec4..a862ee285 100644
--- a/.github/workflows/package-linux.yml
+++ b/.github/workflows/package-linux.yml
@@ -25,7 +25,7 @@ jobs:
build:
runs-on: ubuntu-22.04
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -35,7 +35,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
- python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
+ python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt
- name: build binary
run: |
@@ -59,16 +59,16 @@ jobs:
env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: AppImage
path: './*.AppImage*'
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
- generate_release_notes: true
+ generate_release_notes: false
files: |
LICENSE.txt
*.AppImage*
diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml
index 92412877b..7bdf9ea66 100644
--- a/.github/workflows/package-macos.yml
+++ b/.github/workflows/package-macos.yml
@@ -25,10 +25,12 @@ jobs:
build:
strategy:
matrix:
- os: [ macos-13, macos-14 ]
+ os: [ macos-15-intel, macos-14 ]
runs-on: ${{ matrix.os }}
+ env:
+ MACOSX_DEPLOYMENT_TARGET: '14.0'
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -36,7 +38,7 @@ jobs:
cache: 'pip'
- name: Install python dependencies
run: |
- python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
+ python -m pip install --upgrade pip pyinstaller certifi
pip install -r requirements.txt
- name: Install the Apple certificate and provisioning profile
# TODO signing
@@ -69,7 +71,7 @@ jobs:
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- - uses: actions/setup-node@v5
+ - uses: actions/setup-node@v6
with:
node-version: 16
- run: npm install -g appdmg
@@ -78,18 +80,17 @@ jobs:
run: |
python setup.py build_binary
- name: upload build
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
- generate_release_notes: true
+ generate_release_notes: false
files: |
- LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile
# TODO signing
diff --git a/.github/workflows/package-osx-legacy.yml b/.github/workflows/package-osx-legacy.yml
index 5833349a7..57c01181f 100644
--- a/.github/workflows/package-osx-legacy.yml
+++ b/.github/workflows/package-osx-legacy.yml
@@ -23,7 +23,7 @@ jobs:
build:
strategy:
matrix:
- os: [ macos-13 ]
+ os: [ macos-15-intel ]
runs-on: ${{ matrix.os }}
env:
# We need the official Python, because the GA ones only support newer macOS versions
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- - uses: actions/checkout@v5
+ - 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
@@ -40,10 +40,10 @@ jobs:
- name: Install Python dependencies
run: |
python3 --version
- pip3 install --upgrade pip setuptools wheel pyinstaller certifi
+ pip3 install --upgrade pip pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- - uses: actions/setup-node@v5
+ - uses: actions/setup-node@v6
with:
node-version: 16
- run: npm install -g appdmg
@@ -51,16 +51,16 @@ jobs:
run: |
python3 setup.py build_binary
- name: upload build
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
- generate_release_notes: true
+ generate_release_notes: false
files: |
LICENSE.txt
dist/*.dmg
diff --git a/.github/workflows/package-windows-with-docker.yml b/.github/workflows/package-windows-with-docker.yml
deleted file mode 100644
index 15254731b..000000000
--- a/.github/workflows/package-windows-with-docker.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
-
-name: build KCC for windows with docker
-
-on:
- workflow_dispatch:
- push:
- tags:
- - "v*.*.*"
-
-jobs:
- build:
- strategy:
- matrix:
- entry: [ kcc-c2e, kcc-c2p ]
- include:
- - entry: kcc-c2e
- capital: KCC_c2e
- - entry: kcc-c2p
- capital: KCC_c2p
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
-
- - name: Package Application
- uses: JackMcKew/pyinstaller-action-windows@main
- with:
- path: .
- spec: ./${{ matrix.entry }}.spec
- - name: rename binaries
- run: |
- version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
- mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
-
- - name: upload-unsigned-artifact
- id: upload-unsigned-artifact
- uses: actions/upload-artifact@v4
- with:
- name: windows-build-${{ matrix.entry }}
- path: dist/windows/*.exe
-
- - id: optional_step_id
- uses: signpath/github-action-submit-signing-request@v1.3
- if: ${{ github.repository == 'ciromattia/kcc' }}
- with:
- api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
- organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
- project-slug: 'kcc'
- signing-policy-slug: 'release-signing'
- github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
- wait-for-completion: true
- output-artifact-directory: 'dist/windows/'
-
- - name: Release
- uses: softprops/action-gh-release@v2
- if: startsWith(github.ref, 'refs/tags/')
- with:
- prerelease: true
- generate_release_notes: true
- files: |
- LICENSE.txt
- dist/windows/*.exe
diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml
index f9ca320fb..6dfbd734c 100644
--- a/.github/workflows/package-windows.yml
+++ b/.github/workflows/package-windows.yml
@@ -23,9 +23,19 @@ on:
jobs:
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
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -35,20 +45,20 @@ jobs:
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
- python -m pip install --upgrade pip setuptools wheel
+ python -m pip install --upgrade pip
pip install -r requirements.txt
pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary
run: |
- python setup.py build_binary
+ python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
- name: windows-build
+ name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- id: optional_step_id
- uses: signpath/github-action-submit-signing-request@v1.3
+ uses: signpath/github-action-submit-signing-request@v2.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -59,11 +69,10 @@ jobs:
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
- generate_release_notes: true
+ generate_release_notes: false
files: |
- LICENSE.txt
dist/*.exe
diff --git a/.github/workflows/package-windows7.yml b/.github/workflows/package-windows7.yml
index 0f9a36bfb..adbb4762e 100644
--- a/.github/workflows/package-windows7.yml
+++ b/.github/workflows/package-windows7.yml
@@ -27,7 +27,7 @@ jobs:
env:
WINDOWS_7: 1
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -37,7 +37,7 @@ jobs:
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
- python -m pip install --upgrade pip setuptools wheel
+ python -m pip install --upgrade pip
pip install -r requirements-win7.txt
pip install certifi pyinstaller --no-binary pyinstaller
.\gen_ui_files.bat
@@ -46,16 +46,26 @@ jobs:
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: windows7-build
path: dist/*.exe
+ - id: optional_step_id
+ uses: signpath/github-action-submit-signing-request@v2.2
+ if: ${{ github.repository == 'ciromattia/kcc' }}
+ with:
+ api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+ organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
+ project-slug: 'kcc'
+ signing-policy-slug: 'release-signing'
+ github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
+ wait-for-completion: true
+ output-artifact-directory: 'dist/'
- name: Release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
- generate_release_notes: true
+ generate_release_notes: false
files: |
- LICENSE.txt
dist/*.exe
diff --git a/.gitignore b/.gitignore
index d6094c9c3..a27db6f72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,12 +2,12 @@
Pipfile
Pipfile.lock
setup.bat
-kindlecomicconverter/sentry.py
other/windows/kindlegen.exe
dist/
build/
KindleComicConverter*.egg-info/
.idea/
+.vscode/
win7
osx10.11
/venv/
diff --git a/Dockerfile b/Dockerfile
index e9581fe2b..398d417fc 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,77 @@
-# Select final stage based on TARGETARCH ARG
-FROM ghcr.io/ciromattia/kcc:docker-base-20241116
-LABEL com.kcc.name="Kindle Comic Converter"
-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
-RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
-
-ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
+# STAGE 1: BUILDER
+# Contains all build tools and dev dependencies, will be discarded
+FROM python:3.13-slim-bullseye AS builder
+
+# Install system dependencies
+RUN set -x && \
+ BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev" && \
+ 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}
+
+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"]
diff --git a/Dockerfile-base b/Dockerfile-base
deleted file mode 100644
index cb73e60ad..000000000
--- a/Dockerfile-base
+++ /dev/null
@@ -1,164 +0,0 @@
-FROM --platform=linux/amd64 python:3.13-slim-bullseye as compile-amd64
-ARG TARGETOS
-ARG TARGETARCH
-ARG TARGETVARIANT
-RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
-
-
-COPY requirements.txt /opt/kcc/
-ENV PATH="/opt/venv/bin:$PATH"
-RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
- apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 && \
- python -m pip install --upgrade pip && \
- python -m venv /opt/venv && \
- python -m pip install -r /opt/kcc/requirements.txt
-
-
-######################################################################################
-
-FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64
-ARG TARGETOS
-ARG TARGETARCH
-ARG TARGETVARIANT
-RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
-
-ENV LC_ALL=C.UTF-8 \
- LANG=C.UTF-8 \
- LANGUAGE=en_US:en
-
-SHELL ["/bin/bash", "-o", "pipefail", "-c"]
-
-COPY requirements.txt /opt/kcc/
-ENV PATH="/opt/venv/bin:$PATH"
-
-RUN set -x && \
- TEMP_PACKAGES=() && \
- KEPT_PACKAGES=() && \
- # Packages only required during build
- TEMP_PACKAGES+=(build-essential) && \
- TEMP_PACKAGES+=(cmake) && \
- TEMP_PACKAGES+=(libfreetype6-dev) && \
- TEMP_PACKAGES+=(libfontconfig1-dev) && \
- TEMP_PACKAGES+=(libpng-dev) && \
- TEMP_PACKAGES+=(libjpeg-dev) && \
- TEMP_PACKAGES+=(libssl-dev) && \
- TEMP_PACKAGES+=(libxft-dev) && \
- TEMP_PACKAGES+=(make) && \
- TEMP_PACKAGES+=(python3-dev) && \
- TEMP_PACKAGES+=(python3-setuptools) && \
- TEMP_PACKAGES+=(python3-wheel) && \
- # Packages kept in the image
- KEPT_PACKAGES+=(bash) && \
- KEPT_PACKAGES+=(ca-certificates) && \
- KEPT_PACKAGES+=(chrpath) && \
- KEPT_PACKAGES+=(locales) && \
- KEPT_PACKAGES+=(locales-all) && \
- KEPT_PACKAGES+=(libfreetype6) && \
- KEPT_PACKAGES+=(libfontconfig1) && \
- KEPT_PACKAGES+=(p7zip-full) && \
- KEPT_PACKAGES+=(python3) && \
- KEPT_PACKAGES+=(python3-pip) && \
- KEPT_PACKAGES+=(unrar-free) && \
- # Install packages
- DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
- ${KEPT_PACKAGES[@]} \
- ${TEMP_PACKAGES[@]} \
- && \
- # Install required python modules
- python -m pip install --upgrade pip && \
- python -m venv /opt/venv && \
- python -m pip install -r /opt/kcc/requirements.txt
-
-
-######################################################################################
-
-FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7
-ARG TARGETOS
-ARG TARGETARCH
-ARG TARGETVARIANT
-RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
-
-ENV LC_ALL=C.UTF-8 \
- LANG=C.UTF-8 \
- LANGUAGE=en_US:en
-
-SHELL ["/bin/bash", "-o", "pipefail", "-c"]
-
-COPY requirements.txt /opt/kcc/
-ENV PATH="/opt/venv/bin:$PATH"
-
-RUN set -x && \
- TEMP_PACKAGES=() && \
- KEPT_PACKAGES=() && \
- # Packages only required during build
- TEMP_PACKAGES+=(build-essential) && \
- TEMP_PACKAGES+=(cmake) && \
- TEMP_PACKAGES+=(libffi-dev) && \
- TEMP_PACKAGES+=(libfreetype6-dev) && \
- TEMP_PACKAGES+=(libfontconfig1-dev) && \
- TEMP_PACKAGES+=(libpng-dev) && \
- TEMP_PACKAGES+=(libjpeg-dev) && \
- TEMP_PACKAGES+=(libssl-dev) && \
- TEMP_PACKAGES+=(libxft-dev) && \
- TEMP_PACKAGES+=(make) && \
- TEMP_PACKAGES+=(python3-dev) && \
- TEMP_PACKAGES+=(python3-setuptools) && \
- TEMP_PACKAGES+=(python3-wheel) && \
- # Packages kept in the image
- KEPT_PACKAGES+=(bash) && \
- KEPT_PACKAGES+=(ca-certificates) && \
- KEPT_PACKAGES+=(chrpath) && \
- KEPT_PACKAGES+=(locales) && \
- KEPT_PACKAGES+=(locales-all) && \
- KEPT_PACKAGES+=(libfreetype6) && \
- KEPT_PACKAGES+=(libfontconfig1) && \
- KEPT_PACKAGES+=(p7zip-full) && \
- KEPT_PACKAGES+=(python3) && \
- KEPT_PACKAGES+=(python3-pip) && \
- KEPT_PACKAGES+=(unrar-free) && \
- # Install packages
- DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
- DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
- ${KEPT_PACKAGES[@]} \
- ${TEMP_PACKAGES[@]} \
- && \
- # Install required python modules
- python -m pip install --upgrade pip && \
- python -m venv /opt/venv && \
- python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy pymupdf
-
-
-######################################################################################
-FROM --platform=linux/amd64 python:3.13-slim-bullseye as build-amd64
-COPY --from=compile-amd64 /opt/venv /opt/venv
-
-FROM --platform=linux/arm64 python:3.13-slim-bullseye as build-arm64
-COPY --from=compile-arm64 /opt/venv /opt/venv
-
-FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as build-armv7
-COPY --from=compile-armv7 /opt/venv /opt/venv
-######################################################################################
-
-# Select final stage based on TARGETARCH ARG
-FROM build-${TARGETARCH}${TARGETVARIANT}
-LABEL com.kcc.name="Kindle Comic Converter base image"
-LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
-LABEL org.opencontainers.image.description='Kindle Comic Converter base image'
-LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
-LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
-LABEL org.opencontainers.image.authors='darodi'
-LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
-LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
-LABEL org.opencontainers.image.vendor='ciromattia'
-LABEL org.opencontainers.image.licenses='ISC'
-LABEL org.opencontainers.image.title="Kindle Comic Converter"
-
-
-ENV PATH="/opt/venv/bin:$PATH"
-WORKDIR /app
-RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
- apt-get install -y p7zip-full unrar-free && \
- ln -s /app/kindlegen /bin/kindlegen && \
- echo docker-base-20241116 > /IMAGE_VERSION
-
diff --git a/README.md b/README.md
index 16d16debc..6ff51975c 100644
--- a/README.md
+++ b/README.md
@@ -7,12 +7,19 @@
[](https://github.com/ciromattia/kcc/releases)
-**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
+**Kindle Comic Converter** optimizes black & white (or color) comics and manga for E-ink ereaders
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
-Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
+Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
+KCC runs on Windows, macOS, and Linux.
+
+Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
+
+https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
+
+**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
@@ -26,7 +33,7 @@ which have different requirements than normal LCD screens.
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.
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:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
@@ -34,6 +41,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
+6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
The GUI looks like this, built in Qt6, with my most commonly used settings:
@@ -46,7 +54,9 @@ You can change the default output directory by holding `Shift` while clicking th
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
-YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
+YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
+
+Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
@@ -92,30 +102,31 @@ Click on **Assets** of the latest release.
You probably want either
- `KCC_*.*.*.exe` (Windows)
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
-- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 12+)
+- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
The `c2e` and `c2p` versions are command line tools for power users.
-On Mac, right click open to get past the security warning.
+On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
## FAQ
- Should I use Calibre?
- - No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre will break the formatting.
+ - No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
+ Additionally, it will break page numbers.
Viewing KCC output in Calibre will also not work properly.
- On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
- On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
- If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
+ Direct USB dropping is reccomended.
- Blank pages?
- - May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
+ - May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. You can try PDF output.
Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use?
- - MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
+ - MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
- All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
+- Kindle panel view not working?
+ - Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted?
@@ -125,9 +136,6 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
-- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
-- Image too dark?
- - The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -175,38 +183,47 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
### Profiles:
```
- 'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
- 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
- 'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
- 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
- 'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
- 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
- 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
- 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
- 'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
- 'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
- 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
- 'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
- 'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
- 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
- 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
- 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
- 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
- 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
- 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
- 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
- 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
- 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
- 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
- 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
- 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
- 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
- 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
- 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
- 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
- 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
- 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
- 'OTHER': ("Other", (0, 0), Palette16, 1.8),
+ 'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
+ 'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
+ 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
+ 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
+ 'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
+ 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
+ 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
+ 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
+ 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
+ 'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
+ 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
+ 'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
+ 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
+ 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
+ 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
+ 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
+ 'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
+ 'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
+ 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
+ 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
+ 'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
+ 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
+ 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
+ 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
+ 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
+ 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
+ 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
+ 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
+ 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
+ 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
+ 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
+ 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
+ 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
+ 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
+ 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
+ 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
+ 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
+ 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
+ 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
+ 'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
+ 'OTHER': ("Other", (0, 0), Palette16, 1.0),
```
### Standalone `kcc-c2e.py` usage:
@@ -230,6 +247,8 @@ MAIN:
PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option
+ --legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
+ --pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
-r SPLITTER, --splitter SPLITTER
@@ -237,6 +256,8 @@ PROCESSING:
-g GAMMA, --gamma GAMMA
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
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
--cp CROPPINGP, --croppingpower CROPPINGP
@@ -248,11 +269,19 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
+ --smartcovercrop Attempt to crop main cover from wide image
+ --coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale
- --forcepng Create PNG files instead JPEG
+ --forcepng Create PNG files instead JPEG for black and white images
+ --webp Replace JPG with lossy WEBP and PNG with lossless WEBP
+ --force-png-rgb Force color images to be saved as PNG
+ --pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
+ --noquantize Don't quantize PNG images to 16 colors
--mozjpeg Create JPEG files using mozJpeg
+ --jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
--maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not recoverable.
+ --tempdir Create temporary files directory on source file drive.
OUTPUT SETTINGS:
-o OUTPUT, --output OUTPUT
@@ -268,8 +297,11 @@ OUTPUT SETTINGS:
-b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
+ --onepagelandscape Show a single centered page in landscape
--norotate Do not rotate double page spreads in spread splitter option.
+ --rotateright Rotate double page spreads in opposite direction.
--rotatefirst Put rotated spread first in spread splitter option.
+ --filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
CUSTOM PROFILE:
@@ -313,6 +345,7 @@ Depending on your system [Python](https://www.python.org) may be called either `
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
If you want to edit the `.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.
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
@@ -410,7 +443,6 @@ Older links (dead):
## PRIVACY
**KCC** is initiating internet connections in two cases:
* During startup - Version check and announcement check.
-* When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100644
index 000000000..27aada191
--- /dev/null
+++ b/entrypoint.sh
@@ -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
\ No newline at end of file
diff --git a/environment.yml b/environment.yml
deleted file mode 100644
index 0a9ba527e..000000000
--- a/environment.yml
+++ /dev/null
@@ -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
diff --git a/gui/KCC.ui b/gui/KCC.ui
index 7af91e32a..17e749d39 100644
--- a/gui/KCC.ui
+++ b/gui/KCC.ui
@@ -7,7 +7,7 @@
0
0
566
- 573
+ 671
@@ -22,34 +22,12 @@
5
- -
-
-
-
- 0
- 150
-
-
-
- <html><head/><body><p>Double click on source to open it in metadata editor.</p></body></html>
-
-
-
-
-
- QAbstractItemView::SelectionMode::NoSelection
-
-
- QAbstractItemView::ScrollMode::ScrollPerPixel
-
-
- QAbstractItemView::ScrollMode::ScrollPerPixel
+
-
+
+
+ false
-
-
- -
-
-
+
0
@@ -62,78 +40,91 @@
0
-
-
-
-
-
- 0
- 30
-
-
-
- <html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>
+
-
+
+
+ 300
-
- Metadata Editor
+
+ 1
-
-
- :/Other/icons/editor.png:/Other/icons/editor.png
+
+ Qt::Orientation::Horizontal
- -
-
-
-
- 0
- 30
-
+
-
+
+
+
+ 0
+ 0
+
-
- Support me on Ko-fi
+
+ 99
-
-
- :/Brand/icons/kofi_symbol.png:/Other/icons/kofi_symbol.png
+
+ 5
-
-
- 19
- 16
-
+
+ 0
- -
-
-
-
- 0
- 30
-
+
-
+
+
+ <html><head/><body><p>After calculating the cropping boundaries, "back up" a specified percentage amount.</p></body></html>
- Wiki
+ Preserve Margin %
-
-
- :/Other/icons/wiki.png:/Other/icons/wiki.png
+
+
+ -
+
+
+ Cropping power:
- -
-
+
-
+
+
+
+ 0
+ 30
+
+
+
+
+ true
+
+
+
+ false
+
+
+ Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter
+
+
+
+ -
+
-
+
0
0
-
+
+ false
+
+
0
@@ -146,170 +137,47 @@
0
-
-
-
-
-
- 0
- 30
-
-
-
-
- true
-
-
-
- <html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>
-
-
- Convert
-
-
-
- :/Other/icons/convert.png:/Other/icons/convert.png
-
-
-
- -
-
-
-
- 0
- 30
-
-
-
- Clear list
-
-
-
- :/Other/icons/clear.png:/Other/icons/clear.png
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
- <html><head/><body><p style='white-space:pre'>Target device.</p></body></html>
-
-
-
- -
-
-
-
- 0
- 30
-
-
-
- <html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>
-
-
- Add file(s)
-
-
-
- :/Other/icons/document_new.png:/Other/icons/document_new.png
-
-
-
- -
-
-
-
- 0
- 30
-
-
-
- <html><head/><body><p>Use this to select the default output directory.</p></body></html>
-
+
-
+
-
-
-
-
- :/Other/icons/folder_new.png:/Other/icons/folder_new.png
+ JPEG Quality:
- -
-
-
-
- 0
- 0
-
-
-
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>
-
-
- Output Folder
-
-
- true
+
-
+
+
+ 95
-
-
- -
-
-
-
- 0
- 28
-
+
+ 5
-
- <html><head/><body><p style='white-space:pre'>Output format.</p></body></html>
+
+ 85
- clearButton
- deviceBox
- convertButton
- formatBox
- defaultOutputFolderButton
- fileButton
- defaultOutputFolderBox
- -
-
-
-
- 0
- 30
-
-
-
-
- true
-
+
-
+
+
+
+ 0
+ 0
+
false
-
- Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter
-
-
-
- -
-
-
- false
+
+ <html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>
-
+
+
+ 0
+
0
@@ -322,67 +190,76 @@
0
-
-
-
+
-
+
-
+
0
0
-
- <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
-
- Custom height:
+ Chunk size MB:
- -
-
-
- <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
+
-
+
+
+ 50
- 3200
+ 600
+
+
+ 400
- -
-
+
-
+
-
+
0
0
-
- <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
-
- Custom width:
-
-
-
- -
-
-
- <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
-
-
- 5120
+ Greater than default may cause performance issues on older ereaders.
- -
-
-
- false
+
-
+
+
+
+ 0
+ 150
+
-
+
+ <html><head/><body><p>Double click on source to open it in metadata editor.</p></body></html>
+
+
+
+
+
+ QAbstractItemView::SelectionMode::NoSelection
+
+
+ QAbstractItemView::ScrollMode::ScrollPerPixel
+
+
+ QAbstractItemView::ScrollMode::ScrollPerPixel
+
+
+
+ -
+
+
0
@@ -395,52 +272,106 @@
0
-
-
-
+
-
+
+
+
+ 0
+ 30
+
+
- <html><head/><body><p>After calculating the cropping boundaries, "back up" a specified percentage amount.</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>
+
+
+ Metadata Editor
+
+
+
+ :/Other/icons/editor.png:/Other/icons/editor.png
+
+
+
+ -
+
+
+
+ 0
+ 30
+
+
+
+ Support me on Ko-fi
+
+
+
+ :/Brand/icons/kofi_symbol.png:/Brand/icons/kofi_symbol.png
+
+
+
+ 19
+ 16
+
+
+
+
+ -
+
+
+
+ 0
+ 30
+
- Preserve Margin %
+ Wiki
+
+
+
+ :/Other/icons/wiki.png:/Other/icons/wiki.png
- -
-
+
-
+
+
+
+ 0
+ 30
+
+
- Cropping power:
+ YouTube
- -
-
-
- 300
+
-
+
+
+
+ 0
+ 30
+
-
- 1
+
+ Humble Bundle Referral
-
- Qt::Orientation::Horizontal
+
+
+ :/Brand/icons/Humble_H-Red.png:/Brand/icons/Humble_H-Red.png
- -
-
-
-
- 0
- 0
-
-
-
- 99
-
-
- 5
+
-
+
+
+
+ 0
+ 30
+
-
- 0
+
+ Discord
@@ -462,39 +393,52 @@
0
- -
-
+
-
+
- <html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>
+ Do not rotate double page spreads in spread splitter option.
- Custom gamma
+ No rotate
- -
-
+
-
+
- <html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>
- Right-to-left mode
+ 1x4 to 2x2 strips
- -
-
+
-
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>
- W/B margins
+ Spread splitter
true
+ -
+
+
+ false
+
+
+ Use a more compatible 8 bit PNG instead of 4 bit.
+
+
+ PNG Legacy Mode
+
+
+
-
@@ -508,16 +452,6 @@
- -
-
-
- <html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>
-
-
- File Fusion
-
-
-
-
@@ -562,13 +496,60 @@
- -
-
+
-
+
- <html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>
- Rotate First
+ Webtoon mode
+
+
+
+ -
+
+
+ <html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>
+
+
+ File Fusion
+
+
+
+ -
+
+
+ Delete input file(s) or directory. It's not recoverable!
+
+
+ Delete input
+
+
+
+ -
+
+
+ <html><head/><body><p>Set a custom gamma correction.</p><p>1.0 is default (disabled).<br/>< 1.0 makes the image brighter.<br/>> 1.0 makes the image darker. </p><p>1.8 was the default in KCC 9.1.0 and earlier.</p><p>Use if you want to make midtones darker.</p></body></html>
+
+
+ Custom gamma
+
+
+
+ -
+
+
+ false
+
+
+ Don't quantize PNG images to 16 colors (4 bit)
+
+This will double file size but preserve all 256 colors (8 bit).
+
+Eink only has 16 shades of gray so you probably don't want this.
+
+
+ No Quantize
@@ -582,29 +563,74 @@
- -
-
+
-
+
- <html><head/><body><p><span style=" font-weight:700; text-decoration: underline;">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=" font-weight:700; text-decoration: underline;">Checked</span><br/>Output file size specified in "Chunk size MB" before split occurs.</p></body></html>
+ Resize cover to exact device resolution by center-cropping to aspect ratio first.
+May crop top/bottom or left/right depending on source aspect ratio. Not implemented for Kindle Scribe.
- Chunk size
+ Cover Fill
- -
-
+
-
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>
+ Rotate 2 page spreads in opposite direction than normal.
- Spread splitter
+ Rotate Right
+
+
+
+ -
+
+
+ <html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>
+
+
+ Right-to-left (manga)
+
+
+
+ -
+
+
+ Shift first page to opposite side in landscape for two page spread alignment
+
+
+ Spread shift
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Margins<br/></span>Margins</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>
+
+
+ Cropping mode
true
+ -
+
+
+ The JPEG quality, on a scale from 0 (worst) to 95 (best).
+
+Default is 85 for most devices besides Kindle Scribe and Colorsoft, which are 90.
+
+Higher values are larger and higher quality, and may resolve blank page issues.
+
+
+ Custom JPEG Quality
+
+
+
-
@@ -618,10 +644,92 @@
-
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Don't use metadata Title<br/></span>Write default title.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Add metadata Title to the default schema<br/></span>Write default title with Title from ComicInfo.xml or other embedded metadata.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Use metadata Title only<br/></span>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Don't use metadata Title<br/></span>Write default title.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Add metadata Title to the default schema<br/></span>Write default title with Title from ComicInfo.xml or other embedded metadata.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Use metadata Title only<br/></span>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>
+
+
+ Metadata Title
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html>
+
+
+ Smart Cover Crop
+
+
+
+ -
+
+
+ <html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>
+
+
+ Rotate First
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG for black and white images</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>
+
+
+ JPEG/PNG/mozJpeg
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p>By default, KCC maps the darkest pixel value to pure black (the black point.)</p><p>Extreme black point sets the black point to be the most common dark pixel value.</p><p>Useful when text is black but artwork is gray.</p></body></html>
+
+
+ Extreme Black Point
+
+
+
+ -
+
+
+ false
+
+
+ Force full color images to be saved in lossless PNG format, dramatically increases the filesize.
+
+
+ Force PNG RGB
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>
+
+
+ Stretch/Upscale
+
+
+ true
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>
- Metadata Title
+ W/B margins
true
@@ -641,126 +749,299 @@
- -
-
+
-
+
- Shift first page to opposite side in landscape for two page spread alignment
+ <html><head/><body><p>Use the PDF/EPUB image extraction method from older KCC versions.</p><p><br/></p><p>Use if standard extraction fails for whatever reason.</p></body></html>
- Spread shift
+ Legacy Extract
- -
-
+
-
+
- <html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>
- Disable processing
+ Color mode
- -
-
+
-
+
- <html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>
+ Render vector PDFs to device width instead of height.
+
+Useful if you plan to crop a little off the top and bottom to fill screen.
- Webtoon mode
+ PDF Width Render
- -
-
+
-
+
- <html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>
- Color mode
+ Disable processing
- -
-
+
-
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>
+
+
+ Output Folder
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 30
+
+
+
+ <html><head/><body><p>Use this to select the default output directory.</p></body></html>
+
+
+
+
+
+
+ :/Other/icons/folder_new.png:/Other/icons/folder_new.png
+
+
+
+
+
+
+ -
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Margins<br/></span>Margins</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>
+ <html><head/><body><p><span style=" font-weight:700; text-decoration: underline;">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=" font-weight:700; text-decoration: underline;">Checked</span><br/>Output file size specified in "Chunk size MB" before split occurs.</p></body></html>
- Cropping mode
+ Chunk size
+
+
+
+ -
+
+
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>
+
+
+ Custom Autocontrast
true
- -
-
+
-
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>
+ Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.
+
+Ignored for Kindle EPUB/MOBI and all PDF.
- 1x4 to 2x2 strips
+ WebP (experimental)
- -
-
+
-
+
- Do not rotate double page spreads in spread splitter option.
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html>
- No rotate
+ Temp Directory
- -
-
+
-
+
- Delete input file(s) or directory. It's not recoverable!
+ <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 2 page landscape<br/></span>2 viewports for left and right pages</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 1 page landscape<br/></span>A single centered viewport for 1 page</p></body></html>
- Delete input
+ 1 Page Landscape
- -
-
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 30
+
+
+
+
+ true
+
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>
- Stretch/Upscale
+ Convert
-
- true
+
+
+ :/Other/icons/convert.png:/Other/icons/convert.png
- -
-
+
-
+
+
+
+ 0
+ 30
+
+
+
+ Clear list
+
+
+
+ :/Other/icons/clear.png:/Other/icons/clear.png
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+ <html><head/><body><p style='white-space:pre'>Target device.</p></body></html>
+
+
+
+ -
+
+
+
+ 0
+ 30
+
+
- <html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>
- JPEG/PNG/mozJpeg
+ Add input file(s)
-
- true
+
+
+ :/Other/icons/document_new.png:/Other/icons/document_new.png
- -
-
+
-
+
+
+
+ 0
+ 0
+
+
- <html><head/><body><p>Set the most common dark pixel value to be the black point for leveling on a page by page basis.</p><p>Skipped for any images that were originally color.</p><p>Use only if default autocontrast still results in very gray faded blacks. </p><p>Reccomended to use with Custom Gamma = 1.0 (Disabled).</p></body></html>
+ <html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=" font-weight:600;">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>
- Aggressive Black Point
+ Add input folder(s)
+
+
+
+ :/Other/icons/folder_new.png:/Other/icons/folder_new.png
+
+
+
+ -
+
+
+
+ 0
+ 28
+
+
+
+ <html><head/><body><p style='white-space:pre'>Output format.</p></body></html>
+ clearButton
+ deviceBox
+ convertButton
+ fileButton
+ directoryButton
+ formatBox
-
@@ -804,24 +1085,12 @@
- -
-
-
-
- 0
- 0
-
-
+
-
+
false
-
- <html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>
-
-
-
- 0
-
+
0
@@ -834,42 +1103,55 @@
0
-
-
-
+
-
+
-
+
0
0
+
+ <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
+
- Chunk size MB:
+ Custom height:
- -
-
-
- 100
+
-
+
+
+ <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
- 600
-
-
- 400
+ 6000
- -
-
+
-
+
-
+
0
0
+
+ <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
+
- Greater than default may cause performance issues on older ereaders.
+ Custom width:
+
+
+
+ -
+
+
+ <html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>
+
+
+ 8000
@@ -885,36 +1167,45 @@
- convertButton
+ jobList
+ fileButton
clearButton
deviceBox
- formatBox
+ widthBox
+ heightBox
+ convertButton
mangaBox
rotateBox
qualityBox
webtoonBox
upscaleBox
gammaBox
+ gammaSlider
borderBox
outputSplit
colorBox
mozJpegBox
maximizeStrips
croppingBox
+ croppingPowerSlider
+ preserveMarginBox
spreadShiftBox
deleteBox
disableProcessingBox
- chunkSizeBox
+ fileFusionBox
noRotateBox
interPanelCropBox
+ metadataTitleBox
+ coverFillBox
+ chunkSizeCheckBox
+ chunkSizeBox
eraseRainbowBox
- heightBox
- croppingPowerSlider
+ rotateFirstBox
+ autoLevelBox
+ autocontrastBox
editorButton
+ kofiButton
wikiButton
- jobList
- gammaSlider
- widthBox
diff --git a/gui/MetaEditor.ui b/gui/MetaEditor.ui
index 176eb2c67..3b453ad45 100644
--- a/gui/MetaEditor.ui
+++ b/gui/MetaEditor.ui
@@ -192,6 +192,18 @@
+
+ seriesLine
+ volumeLine
+ titleLine
+ numberLine
+ writerLine
+ pencillerLine
+ inkerLine
+ coloristLine
+ okButton
+ cancelButton
+
diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py
index c74384463..dde7f0d63 100644
--- a/kindlecomicconverter/KCC_gui.py
+++ b/kindlecomicconverter/KCC_gui.py
@@ -22,7 +22,7 @@
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
-from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
+from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -38,11 +38,10 @@
from psutil import Process
from copy import copy
from packaging.version import Version
-from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
-from .comicarchive import SEVENZIP, available_archive_tools
+from .comicarchive import SEVENZIP, TAR, available_archive_tools
from . import __version__
from . import comic2ebook
from . import metadata
@@ -189,11 +188,13 @@ def run(self):
delta = expiration - datetime.now(timezone.utc)
time_left = f"{delta.days} day(s) left"
icon = 'info'
- if category == 'humbleBundles':
+ if category == 'humbleMangaBundles':
+ icon = 'humble'
+ if category == 'humbleComicBundles':
icon = 'bindle'
if category == 'kofi':
icon = 'kofi'
- message = f"{payload.get('name')}"
+ message = f"{payload.get('name')}"
if payload.get('link'):
message = '{}'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'):
@@ -290,6 +291,10 @@ def run(self):
options.gamma = float(GUI.gammaValue)
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
@@ -321,26 +326,50 @@ def run(self):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
+ if GUI.legacyExtractBox.isChecked():
+ options.legacyextract = True
+ if GUI.pdfWidthBox.isChecked():
+ options.pdfwidth = True
+ if GUI.smartCoverCropBox.isChecked():
+ options.smartcovercrop = True
+ if GUI.coverFillBox.isChecked():
+ options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
options.metadatatitle = 2
if GUI.deleteBox.isChecked():
options.delete = True
+ if GUI.tempDirBox.isChecked():
+ options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
+ if GUI.onePageLandscapeBox.isChecked():
+ options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
+ if GUI.rotateRightBox.isChecked():
+ options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
+ if GUI.forcePngRgbBox.isChecked():
+ options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
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:
options.customwidth = str(GUI.widthBox.value())
options.customheight = str(GUI.heightBox.value())
@@ -376,13 +405,14 @@ def run(self):
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 job in currentJobs:
+ for i, job in enumerate(currentJobs, start=1):
+ job_progress_number = f'[{i}/{len(currentJobs)}] '
sleep(0.5)
if not self.conversionAlive:
self.clean()
return
self.errors = False
- MW.addMessage.emit('Source: ' + job, 'info', False)
+ MW.addMessage.emit(f'{job_progress_number}Source: ' + job, 'info', False)
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files'
@@ -396,7 +426,7 @@ def run(self):
jobargv.append(job)
try:
comic2ebook.options = comic2ebook.checkOptions(copy(options))
- outputPath = comic2ebook.makeBook(job, self)
+ outputPath = comic2ebook.makeBook(job, self, job_progress_number)
MW.hideProgressBar.emit()
except UserWarning as warn:
if not self.conversionAlive:
@@ -416,8 +446,6 @@ def run(self):
_, _, traceback = sys.exc_info()
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
- if ' is corrupted.' not in str(err):
- GUI.sentry.captureException()
MW.addMessage.emit('Error during conversion! Please consult '
'wiki '
'for more details.', 'error', False)
@@ -438,7 +466,7 @@ def run(self):
else:
MW.addMessage.emit('Creating EPUB files... Done!', 'info', True)
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('tick')
MW.addMessage.emit('Creating MOBI files', 'info', False)
@@ -488,8 +516,10 @@ def run(self):
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
for item in outputPath:
- comic2ebook.options.covers[outputPath.index(item)][0].saveToKindle(
- k, comic2ebook.options.covers[outputPath.index(item)][1])
+ cover = comic2ebook.options.covers[outputPath.index(item)][0]
+ if cover:
+ cover.saveToKindle(
+ k, comic2ebook.options.covers[outputPath.index(item)][1])
MW.addMessage.emit('Kindle detected. Uploading covers... Done!', 'info', False)
else:
GUI.progress.content = ''
@@ -510,11 +540,12 @@ def run(self):
if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi'))
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
+ MW.addMessage.emit(self.kindlegenErrorCode[1], 'error', False)
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
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',
False)
if self.kindlegenErrorCode[0] == 3221226505:
@@ -532,6 +563,7 @@ def run(self):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
+ comic2ebook.checkPre('LLL-')
GUI.progress.content = ''
GUI.progress.stop()
MW.hideProgressBar.emit()
@@ -595,18 +627,36 @@ def selectFile(self):
GUI.jobList.clear()
if self.tar or self.sevenzip:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
- 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
+ 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
else:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)')
for fname in fnames[0]:
if fname != '':
- if sys.platform.startswith('win'):
- fname = fname.replace('/', '\\')
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
GUI.jobList.addItem(fname)
GUI.jobList.scrollToBottom()
+ def selectDir(self):
+ if self.needClean:
+ self.needClean = False
+ GUI.jobList.clear()
+
+ dialog = QFileDialog(MW, 'Select input folder(s)', self.lastPath)
+ dialog.setFileMode(QFileDialog.FileMode.Directory)
+ dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
+ dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
+ dialog.findChild(QTreeView).setSelectionMode(QAbstractItemView.ExtendedSelection)
+
+ if dialog.exec():
+ dnames = dialog.selectedFiles()
+ for dname in dnames:
+ if dname != '':
+ self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
+ GUI.jobList.addItem(dname)
+ GUI.jobList.scrollToBottom()
+
+
def selectFileMetaEditor(self, sname):
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
@@ -631,7 +681,6 @@ def selectFileMetaEditor(self, sname):
self.editor.loadData(sname)
except Exception as err:
_, _, traceback = sys.exc_info()
- GUI.sentry.captureException()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
else:
@@ -648,6 +697,18 @@ def openKofi(self):
# noinspection PyCallByClass
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):
if mode == 1:
self.currentMode = 1
@@ -715,26 +776,50 @@ def togglecroppingBox(self, value):
GUI.croppingWidget.setVisible(False)
self.changeCroppingPower(100) # 1.0
+ def togglejpegqualityBox(self, value):
+ if value:
+ GUI.jpegQualityWidget.setVisible(True)
+ else:
+ GUI.jpegQualityWidget.setVisible(False)
+
def togglewebtoonBox(self, value):
if value:
+ self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
+ self.addMessage('Try reading webtoon panels side by side in landscape!', 'info')
GUI.qualityBox.setEnabled(False)
GUI.qualityBox.setChecked(False)
GUI.mangaBox.setEnabled(False)
GUI.mangaBox.setChecked(False)
GUI.rotateBox.setEnabled(False)
GUI.rotateBox.setChecked(False)
- GUI.upscaleBox.setEnabled(False)
- GUI.upscaleBox.setChecked(True)
- GUI.chunkSizeCheckBox.setEnabled(False)
- GUI.chunkSizeCheckBox.setChecked(False)
+ GUI.borderBox.setEnabled(False)
+ GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
+ # GUI.upscaleBox.setEnabled(False)
+ # GUI.upscaleBox.setChecked(False)
+ GUI.croppingBox.setEnabled(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:
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['PVOptions']:
GUI.qualityBox.setEnabled(True)
GUI.mangaBox.setEnabled(True)
GUI.rotateBox.setEnabled(True)
- GUI.upscaleBox.setEnabled(True)
- GUI.chunkSizeCheckBox.setEnabled(True)
+ GUI.borderBox.setEnabled(True)
+ profile = GUI.profiles[str(GUI.deviceBox.currentText())]
+ if not profile['Label'].startswith('KS') or True:
+ GUI.upscaleBox.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):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
@@ -748,7 +833,25 @@ def togglequalityBox(self, value):
else:
GUI.upscaleBox.setEnabled(True)
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):
GUI.chunkSizeWidget.setVisible(value)
@@ -801,15 +904,24 @@ def changeDevice(self):
self.modeChange(1)
GUI.colorBox.setChecked(profile['ForceColor'])
self.changeFormat()
- GUI.gammaSlider.setValue(0)
- self.changeGamma(0)
if not GUI.webtoonBox.isChecked():
GUI.qualityBox.setEnabled(profile['PVOptions'])
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
- if profile['Label'] == 'KS':
+ if profile['Label'].startswith('KS') and False:
GUI.upscaleBox.setDisabled(True)
else:
- GUI.upscaleBox.setEnabled(True)
+ if not GUI.webtoonBox.isChecked() or 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']:
GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other':
@@ -833,8 +945,16 @@ def changeFormat(self, outputformat=None):
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
GUI.chunkSizeCheckBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False)
+ elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
+ GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
+ GUI.upscaleBox.setChecked(True)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
+ if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
+ self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
+ GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
+ else:
+ GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
def stripTags(self, html):
s = HTMLStripper()
@@ -950,6 +1070,7 @@ def saveSettings(self, event):
'qualityBox': GUI.qualityBox.checkState(),
'gammaBox': GUI.gammaBox.checkState(),
'autoLevelBox': GUI.autoLevelBox.checkState(),
+ 'autocontrastBox': GUI.autocontrastBox.checkState(),
'croppingBox': GUI.croppingBox.checkState(),
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(),
@@ -961,15 +1082,28 @@ def saveSettings(self, event):
'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
+ 'legacyExtractBox': GUI.legacyExtractBox.checkState(),
+ 'pdfWidthBox': GUI.pdfWidthBox.checkState(),
+ 'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
+ 'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'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(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState(),
+ 'tempDirBox': GUI.tempDirBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
+ 'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(),
+ 'rotateRightBox': GUI.rotateRightBox.checkState(),
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100,
@@ -983,13 +1117,13 @@ def handleMessage(self, message):
MW.activateWindow()
if type(message) is bytes:
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:
self.needClean = False
GUI.jobList.clear()
formats = ['.pdf']
if self.tar or self.sevenzip:
- formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
+ formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
if os.path.isdir(message):
GUI.jobList.addItem(message)
GUI.jobList.scrollToBottom()
@@ -1014,6 +1148,8 @@ def dragAndDropAccepted(self, e):
if message[-1] == '/':
message = message[:-1]
self.handleMessage(message)
+ # sorting may conflict with manual file fusion order
+ # GUI.jobList.sortItems()
def forceShutdown(self):
self.saveSettings(None)
@@ -1074,7 +1210,6 @@ def __init__(self, kccapp, kccwindow):
self.croppingPowerValue = 1.0
self.currentMode = 1
self.targetDirectory = ''
- self.sentry = Client(release=__version__)
if sys.platform.startswith('win'):
# noinspection PyUnresolvedReferences
from psutil import BELOW_NORMAL_PRIORITY_CLASS
@@ -1090,7 +1225,7 @@ def __init__(self, kccapp, kccwindow):
'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
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)
if self.windowSize == '0x0':
MW.resize(500, 500)
@@ -1100,7 +1235,8 @@ def __init__(self, kccapp, kccwindow):
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
- "KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
+ "PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
+ "KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
@@ -1116,9 +1252,27 @@ def __init__(self, kccapp, kccwindow):
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
- "Kindle Scribe": {
+ "Kindle 1860x1920": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1860',
+ },
+ "Kindle 1920x1920": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1920',
+ },
+ "Kindle 1240x1860": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
+ },
+ "Kindle 1324x1986": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1324',
+ },
+ "Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
+ "Kindle Scribe 3": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
+ },
+ "Kindle Scribe Colorsoft": {
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
+ },
"Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
},
@@ -1126,10 +1280,10 @@ def __init__(self, kccapp, kccwindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
},
"Kindle Paperwhite 12": {
- 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
+ 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW6',
},
"Kindle Colorsoft": {
- '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,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
@@ -1193,9 +1347,11 @@ def __init__(self, kccapp, kccwindow):
'Label': 'OTHER'},
}
profilesGUI = [
+ "Kindle Scribe Colorsoft",
+ "Kindle Scribe 3",
"Kindle Colorsoft",
"Kindle Paperwhite 12",
- "Kindle Scribe",
+ "Kindle Scribe 1/2",
"Kindle Paperwhite 11",
"Kindle 11",
"Kindle Oasis 9/10",
@@ -1215,6 +1371,10 @@ def __init__(self, kccapp, kccwindow):
"Separator",
"Other",
"Separator",
+ "Kindle 1324x1986",
+ "Kindle 1920x1920",
+ "Kindle 1860x1920",
+ "Kindle 1240x1860",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle Paperwhite 7/10",
@@ -1262,7 +1422,7 @@ def __init__(self, kccapp, kccwindow):
'important tips.',
'info')
- self.tar = 'tar' in available_archive_tools()
+ self.tar = TAR in available_archive_tools()
self.sevenzip = SEVENZIP in available_archive_tools()
if not any([self.tar, self.sevenzip]):
self.addMessage('Install 7z (link)'
@@ -1273,16 +1433,22 @@ def __init__(self, kccapp, kccwindow):
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
GUI.clearButton.clicked.connect(self.clearJobs)
GUI.fileButton.clicked.connect(self.selectFile)
+ GUI.directoryButton.clicked.connect(self.selectDir)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi)
+ 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.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
+ GUI.jpegQualityBox.stateChanged.connect(self.togglejpegqualityBox)
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
+ GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat)
@@ -1341,6 +1507,8 @@ def __init__(self, kccapp, kccwindow):
GUI.croppingPowerSlider.setValue(int(self.options[option]))
self.changeCroppingPower(int(self.options[option]))
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
+ elif str(option) == "jpegQuality":
+ GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
elif str(option) == "chunkSizeBox":
GUI.chunkSizeBox.setValue(int(self.options[option]))
else:
@@ -1411,7 +1579,6 @@ def saveData(self):
self.parser.saveXML()
except Exception as err:
_, _, traceback = sys.exc_info()
- GUI.sentry.captureException()
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close()
diff --git a/kindlecomicconverter/KCC_rc.py b/kindlecomicconverter/KCC_rc.py
index 151c18f96..2bbc677a4 100644
--- a/kindlecomicconverter/KCC_rc.py
+++ b/kindlecomicconverter/KCC_rc.py
@@ -13767,59 +13767,59 @@
\x00\x00\x00h\x00\x02\x00\x00\x00\x04\x00\x00\x00\x08\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02<\x00\x00\x00\x00\x00\x01\x00\x02\xb4\x18\
-\x00\x00\x01\x99\xab\x9faO\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02z\x00\x00\x00\x00\x00\x01\x00\x03\x08\xb2\
-\x00\x00\x01\x99\xab\x9faM\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02f\x00\x00\x00\x00\x00\x01\x00\x02\xde\xb7\
-\x00\x00\x01\x99\xab\x9faM\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02R\x00\x00\x00\x00\x00\x01\x00\x02\xd3T\
-\x00\x00\x01\x99\xab\x9faN\
+\x00\x00\x01\x89\x89D9.\
\x00\x00\x00h\x00\x02\x00\x00\x00\x04\x00\x00\x00\x0d\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x22\x00\x00\x00\x00\x00\x01\x00\x01\xad\xc2\
-\x00\x00\x01\x99\xab\x9faO\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x01\xb7\x1a\
-\x00\x00\x01\x99\xab\x9faP\
+\x00\x00\x01\x98\x8a\xa4\xaa\xe2\
\x00\x00\x01\x08\x00\x00\x00\x00\x00\x01\x00\x01\xa2\xbb\
-\x00\x00\x01\x99\xab\x9faN\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x01\xbf0\
-\x00\x00\x01\x99\xab\x9faO\
+\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00h\x00\x02\x00\x00\x00\x03\x00\x00\x00\x12\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02\xbe\x00\x00\x00\x00\x00\x01\x00\x03:\xfe\
-\x00\x00\x01\x99\xab\x9fa\x5c\
+\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x02\x90\x00\x00\x00\x00\x00\x01\x00\x03%\x01\
-\x00\x00\x01\x99\xab\x9faZ\
+\x00\x00\x01\x88;p\xbcI\
\x00\x00\x02\xa6\x00\x00\x00\x00\x00\x01\x00\x03.^\
-\x00\x00\x01\x99\xab\x9faZ\
+\x00\x00\x01\x88;p\xbcI\
\x00\x00\x00h\x00\x02\x00\x00\x00\x07\x00\x00\x00\x16\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x98\x00\x00\x00\x00\x00\x01\x00\x01\xd5\xdc\
-\x00\x00\x01\x99\xab\x9fa]\
+\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x01\xae\x00\x00\x00\x00\x00\x01\x00\x01\xfe\x84\
-\x00\x00\x01\x99\xab\x9faZ\
+\x00\x00\x01\x88;p\xbcI\
\x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x02WX\
-\x00\x00\x01\x99\xab\x9fa[\
+\x00\x00\x01\x94\xb4\xd4\xf0a\
\x00\x00\x01\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x12\x11\
-\x00\x00\x01\x99\xab\x9faZ\
+\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x01\xd1}\
-\x00\x00\x01\x99\xab\x9faT\
+\x00\x00\x01\x88;p\xbcF\
\x00\x00\x01d\x00\x00\x00\x00\x00\x01\x00\x01\xc5\x14\
-\x00\x00\x01\x99\xab\x9faY\
+\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x07\xdb\
-\x00\x00\x01\x99\xab\x9faY\
+\x00\x00\x01\x88;p\xbcH\
\x00\x00\x00h\x00\x02\x00\x00\x00\x03\x00\x00\x00\x1e\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x01\x00\x01~z\
-\x00\x00\x01\x99\xab\x9fa[\
+\x00\x00\x01\x98\x8a\xa4\xaa\xe2\
\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
-\x00\x00\x01\x99\xab\x9faM\
+\x00\x00\x01\x9a\x02\xcf\x8cb\
\x00\x00\x00\xbe\x00\x00\x00\x00\x00\x01\x00\x01=\xd0\
-\x00\x00\x01\x99\xab\x9faN\
+\x00\x00\x01\x9a\x02\xcf\x8cc\
\x00\x00\x00h\x00\x02\x00\x00\x00\x01\x00\x00\x00\x22\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00x\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
-\x00\x00\x01\x99\xab\x9faX\
+\x00\x00\x01\x88;p\xbcH\
"
def qInitResources():
diff --git a/kindlecomicconverter/KCC_ui.py b/kindlecomicconverter/KCC_ui.py
index cb26aae43..525fdb217 100644
--- a/kindlecomicconverter/KCC_ui.py
+++ b/kindlecomicconverter/KCC_ui.py
@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
def setupUi(self, mainWindow):
if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow")
- mainWindow.resize(566, 573)
+ mainWindow.resize(566, 671)
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon)
@@ -35,6 +35,124 @@ def setupUi(self, mainWindow):
self.gridLayout = QGridLayout(self.centralWidget)
self.gridLayout.setObjectName(u"gridLayout")
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
+ self.croppingWidget = QWidget(self.centralWidget)
+ self.croppingWidget.setObjectName(u"croppingWidget")
+ self.croppingWidget.setVisible(False)
+ self.gridLayout_5 = QGridLayout(self.croppingWidget)
+ self.gridLayout_5.setObjectName(u"gridLayout_5")
+ self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
+ self.croppingPowerSlider = QSlider(self.croppingWidget)
+ self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
+ self.croppingPowerSlider.setMaximum(300)
+ self.croppingPowerSlider.setSingleStep(1)
+ self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
+
+ self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
+
+ self.preserveMarginBox = QSpinBox(self.croppingWidget)
+ self.preserveMarginBox.setObjectName(u"preserveMarginBox")
+ sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
+ self.preserveMarginBox.setSizePolicy(sizePolicy)
+ self.preserveMarginBox.setMaximum(99)
+ self.preserveMarginBox.setSingleStep(5)
+ self.preserveMarginBox.setValue(0)
+
+ self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
+
+ self.preserveMarginLabel = QLabel(self.croppingWidget)
+ self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
+
+ self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
+
+ self.croppingPowerLabel = QLabel(self.croppingWidget)
+ self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
+
+ self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
+
+
+ self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
+
+ self.progressBar = QProgressBar(self.centralWidget)
+ self.progressBar.setObjectName(u"progressBar")
+ self.progressBar.setMinimumSize(QSize(0, 30))
+ font = QFont()
+ font.setBold(True)
+ self.progressBar.setFont(font)
+ self.progressBar.setVisible(False)
+ self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
+
+ self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
+
+ self.jpegQualityWidget = QWidget(self.centralWidget)
+ self.jpegQualityWidget.setObjectName(u"jpegQualityWidget")
+ sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
+ sizePolicy1.setHorizontalStretch(0)
+ sizePolicy1.setVerticalStretch(0)
+ sizePolicy1.setHeightForWidth(self.jpegQualityWidget.sizePolicy().hasHeightForWidth())
+ self.jpegQualityWidget.setSizePolicy(sizePolicy1)
+ self.jpegQualityWidget.setVisible(False)
+ self.horizontalLayout_12 = QHBoxLayout(self.jpegQualityWidget)
+ self.horizontalLayout_12.setObjectName(u"horizontalLayout_12")
+ self.horizontalLayout_12.setContentsMargins(0, 0, 0, 0)
+ self.jpegQualityLabel = QLabel(self.jpegQualityWidget)
+ self.jpegQualityLabel.setObjectName(u"jpegQualityLabel")
+
+ self.horizontalLayout_12.addWidget(self.jpegQualityLabel)
+
+ self.jpegQualitySpinBox = QSpinBox(self.jpegQualityWidget)
+ self.jpegQualitySpinBox.setObjectName(u"jpegQualitySpinBox")
+ self.jpegQualitySpinBox.setMaximum(95)
+ self.jpegQualitySpinBox.setSingleStep(5)
+ self.jpegQualitySpinBox.setValue(85)
+
+ self.horizontalLayout_12.addWidget(self.jpegQualitySpinBox)
+
+
+ self.gridLayout.addWidget(self.jpegQualityWidget, 10, 0, 1, 1)
+
+ self.chunkSizeWidget = QWidget(self.centralWidget)
+ self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
+ sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
+ sizePolicy2.setHorizontalStretch(0)
+ sizePolicy2.setVerticalStretch(0)
+ sizePolicy2.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
+ self.chunkSizeWidget.setSizePolicy(sizePolicy2)
+ self.chunkSizeWidget.setVisible(False)
+ self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
+ self.horizontalLayout_4.setSpacing(0)
+ self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
+ self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
+ self.chunkSizeLabel = QLabel(self.chunkSizeWidget)
+ self.chunkSizeLabel.setObjectName(u"chunkSizeLabel")
+ sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
+ sizePolicy3.setHorizontalStretch(0)
+ sizePolicy3.setVerticalStretch(0)
+ sizePolicy3.setHeightForWidth(self.chunkSizeLabel.sizePolicy().hasHeightForWidth())
+ self.chunkSizeLabel.setSizePolicy(sizePolicy3)
+
+ self.horizontalLayout_4.addWidget(self.chunkSizeLabel)
+
+ self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
+ self.chunkSizeBox.setObjectName(u"chunkSizeBox")
+ self.chunkSizeBox.setMinimum(50)
+ self.chunkSizeBox.setMaximum(600)
+ self.chunkSizeBox.setValue(400)
+
+ self.horizontalLayout_4.addWidget(self.chunkSizeBox)
+
+ self.chunkSizeWarnLabel = QLabel(self.chunkSizeWidget)
+ self.chunkSizeWarnLabel.setObjectName(u"chunkSizeWarnLabel")
+ sizePolicy3.setHeightForWidth(self.chunkSizeWarnLabel.sizePolicy().hasHeightForWidth())
+ self.chunkSizeWarnLabel.setSizePolicy(sizePolicy3)
+
+ self.horizontalLayout_4.addWidget(self.chunkSizeWarnLabel)
+
+
+ self.gridLayout.addWidget(self.chunkSizeWidget, 6, 0, 1, 1)
+
self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList")
self.jobList.setMinimumSize(QSize(0, 150))
@@ -47,9 +165,9 @@ def setupUi(self, mainWindow):
self.toolWidget = QWidget(self.centralWidget)
self.toolWidget.setObjectName(u"toolWidget")
- self.horizontalLayout = QHBoxLayout(self.toolWidget)
- self.horizontalLayout.setObjectName(u"horizontalLayout")
- self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
+ self.gridLayout_6 = QGridLayout(self.toolWidget)
+ self.gridLayout_6.setObjectName(u"gridLayout_6")
+ self.gridLayout_6.setContentsMargins(0, 0, 0, 0)
self.editorButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton")
self.editorButton.setMinimumSize(QSize(0, 30))
@@ -57,7 +175,7 @@ def setupUi(self, mainWindow):
icon1.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon1)
- self.horizontalLayout.addWidget(self.editorButton)
+ self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1)
self.kofiButton = QPushButton(self.toolWidget)
self.kofiButton.setObjectName(u"kofiButton")
@@ -67,7 +185,7 @@ def setupUi(self, mainWindow):
self.kofiButton.setIcon(icon2)
self.kofiButton.setIconSize(QSize(19, 16))
- self.horizontalLayout.addWidget(self.kofiButton)
+ self.gridLayout_6.addWidget(self.kofiButton, 0, 1, 1, 1)
self.wikiButton = QPushButton(self.toolWidget)
self.wikiButton.setObjectName(u"wikiButton")
@@ -76,261 +194,194 @@ def setupUi(self, mainWindow):
icon3.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon3)
- self.horizontalLayout.addWidget(self.wikiButton)
+ self.gridLayout_6.addWidget(self.wikiButton, 0, 2, 1, 1)
+ self.youtubeButton = QPushButton(self.toolWidget)
+ self.youtubeButton.setObjectName(u"youtubeButton")
+ self.youtubeButton.setMinimumSize(QSize(0, 30))
- self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
+ self.gridLayout_6.addWidget(self.youtubeButton, 1, 0, 1, 1)
- self.buttonWidget = QWidget(self.centralWidget)
- self.buttonWidget.setObjectName(u"buttonWidget")
- sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
- self.buttonWidget.setSizePolicy(sizePolicy)
- self.gridLayout_4 = QGridLayout(self.buttonWidget)
- self.gridLayout_4.setObjectName(u"gridLayout_4")
- self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
- self.convertButton = QPushButton(self.buttonWidget)
- self.convertButton.setObjectName(u"convertButton")
- self.convertButton.setMinimumSize(QSize(0, 30))
- font = QFont()
- font.setBold(True)
- self.convertButton.setFont(font)
+ self.humbleButton = QPushButton(self.toolWidget)
+ self.humbleButton.setObjectName(u"humbleButton")
+ self.humbleButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
- icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.convertButton.setIcon(icon4)
+ icon4.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.humbleButton.setIcon(icon4)
- self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
+ self.gridLayout_6.addWidget(self.humbleButton, 1, 1, 1, 1)
- self.clearButton = QPushButton(self.buttonWidget)
- self.clearButton.setObjectName(u"clearButton")
- self.clearButton.setMinimumSize(QSize(0, 30))
- icon5 = QIcon()
- icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.clearButton.setIcon(icon5)
+ self.discordButton = QPushButton(self.toolWidget)
+ self.discordButton.setObjectName(u"discordButton")
+ self.discordButton.setMinimumSize(QSize(0, 30))
- self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
+ self.gridLayout_6.addWidget(self.discordButton, 1, 2, 1, 1)
- self.deviceBox = QComboBox(self.buttonWidget)
- self.deviceBox.setObjectName(u"deviceBox")
- self.deviceBox.setMinimumSize(QSize(0, 28))
-
- self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
-
- self.fileButton = QPushButton(self.buttonWidget)
- self.fileButton.setObjectName(u"fileButton")
- self.fileButton.setMinimumSize(QSize(0, 30))
- icon6 = QIcon()
- icon6.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.fileButton.setIcon(icon6)
-
- self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
- self.defaultOutputFolderButton = QPushButton(self.buttonWidget)
- self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
- self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
- icon7 = QIcon()
- icon7.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.defaultOutputFolderButton.setIcon(icon7)
-
- self.gridLayout_4.addWidget(self.defaultOutputFolderButton, 0, 5, 1, 1)
+ self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
- self.defaultOutputFolderBox = QCheckBox(self.buttonWidget)
- self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
- sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
- sizePolicy1.setHorizontalStretch(0)
- sizePolicy1.setVerticalStretch(0)
- sizePolicy1.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
- self.defaultOutputFolderBox.setSizePolicy(sizePolicy1)
- self.defaultOutputFolderBox.setTristate(True)
+ self.optionWidget = QWidget(self.centralWidget)
+ self.optionWidget.setObjectName(u"optionWidget")
+ self.gridLayout_2 = QGridLayout(self.optionWidget)
+ self.gridLayout_2.setObjectName(u"gridLayout_2")
+ self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
+ self.noRotateBox = QCheckBox(self.optionWidget)
+ self.noRotateBox.setObjectName(u"noRotateBox")
- self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1)
+ self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
- self.formatBox = QComboBox(self.buttonWidget)
- self.formatBox.setObjectName(u"formatBox")
- self.formatBox.setMinimumSize(QSize(0, 28))
+ self.maximizeStrips = QCheckBox(self.optionWidget)
+ self.maximizeStrips.setObjectName(u"maximizeStrips")
- self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 2)
+ self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
- self.clearButton.raise_()
- self.deviceBox.raise_()
- self.convertButton.raise_()
- self.formatBox.raise_()
- self.defaultOutputFolderButton.raise_()
- self.fileButton.raise_()
- self.defaultOutputFolderBox.raise_()
+ self.rotateBox = QCheckBox(self.optionWidget)
+ self.rotateBox.setObjectName(u"rotateBox")
+ self.rotateBox.setTristate(True)
- self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
+ self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
- self.progressBar = QProgressBar(self.centralWidget)
- self.progressBar.setObjectName(u"progressBar")
- self.progressBar.setMinimumSize(QSize(0, 30))
- self.progressBar.setFont(font)
- self.progressBar.setVisible(False)
- self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
+ self.pngLegacyBox = QCheckBox(self.optionWidget)
+ self.pngLegacyBox.setObjectName(u"pngLegacyBox")
+ self.pngLegacyBox.setEnabled(False)
- self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
+ self.gridLayout_2.addWidget(self.pngLegacyBox, 11, 0, 1, 1)
- self.customWidget = QWidget(self.centralWidget)
- self.customWidget.setObjectName(u"customWidget")
- self.customWidget.setVisible(False)
- self.gridLayout_3 = QGridLayout(self.customWidget)
- self.gridLayout_3.setObjectName(u"gridLayout_3")
- self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
- self.hLabel = QLabel(self.customWidget)
- self.hLabel.setObjectName(u"hLabel")
- sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
- sizePolicy2.setHorizontalStretch(0)
- sizePolicy2.setVerticalStretch(0)
- sizePolicy2.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
- self.hLabel.setSizePolicy(sizePolicy2)
+ self.interPanelCropBox = QCheckBox(self.optionWidget)
+ self.interPanelCropBox.setObjectName(u"interPanelCropBox")
+ self.interPanelCropBox.setTristate(True)
- self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
- self.widthBox = QSpinBox(self.customWidget)
- self.widthBox.setObjectName(u"widthBox")
- self.widthBox.setMaximum(3200)
+ self.titleEdit = QLineEdit(self.optionWidget)
+ self.titleEdit.setObjectName(u"titleEdit")
+ sizePolicy2.setHeightForWidth(self.titleEdit.sizePolicy().hasHeightForWidth())
+ self.titleEdit.setSizePolicy(sizePolicy2)
+ self.titleEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
+ self.titleEdit.setClearButtonEnabled(False)
- self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.titleEdit, 0, 0, 1, 1)
- self.wLabel = QLabel(self.customWidget)
- self.wLabel.setObjectName(u"wLabel")
- sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
- self.wLabel.setSizePolicy(sizePolicy2)
+ self.authorEdit = QLineEdit(self.optionWidget)
+ self.authorEdit.setObjectName(u"authorEdit")
+ sizePolicy2.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
+ self.authorEdit.setSizePolicy(sizePolicy2)
+ self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
+ self.authorEdit.setClearButtonEnabled(False)
- self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.authorEdit, 0, 1, 1, 1)
- self.heightBox = QSpinBox(self.customWidget)
- self.heightBox.setObjectName(u"heightBox")
- self.heightBox.setMaximum(5120)
+ self.webtoonBox = QCheckBox(self.optionWidget)
+ self.webtoonBox.setObjectName(u"webtoonBox")
- self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
+ self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
+ self.fileFusionBox = QCheckBox(self.optionWidget)
+ self.fileFusionBox.setObjectName(u"fileFusionBox")
- self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
+ self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
- self.croppingWidget = QWidget(self.centralWidget)
- self.croppingWidget.setObjectName(u"croppingWidget")
- self.croppingWidget.setVisible(False)
- self.gridLayout_5 = QGridLayout(self.croppingWidget)
- self.gridLayout_5.setObjectName(u"gridLayout_5")
- self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
- self.preserveMarginLabel = QLabel(self.croppingWidget)
- self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
+ self.deleteBox = QCheckBox(self.optionWidget)
+ self.deleteBox.setObjectName(u"deleteBox")
- self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
- self.croppingPowerLabel = QLabel(self.croppingWidget)
- self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
+ self.gammaBox = QCheckBox(self.optionWidget)
+ self.gammaBox.setObjectName(u"gammaBox")
- self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
- self.croppingPowerSlider = QSlider(self.croppingWidget)
- self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
- self.croppingPowerSlider.setMaximum(300)
- self.croppingPowerSlider.setSingleStep(1)
- self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
+ self.noQuantizeBox = QCheckBox(self.optionWidget)
+ self.noQuantizeBox.setObjectName(u"noQuantizeBox")
+ self.noQuantizeBox.setEnabled(False)
- self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.noQuantizeBox, 10, 2, 1, 1)
- self.preserveMarginBox = QSpinBox(self.croppingWidget)
- self.preserveMarginBox.setObjectName(u"preserveMarginBox")
- sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
- self.preserveMarginBox.setSizePolicy(sizePolicy1)
- self.preserveMarginBox.setMaximum(99)
- self.preserveMarginBox.setSingleStep(5)
- self.preserveMarginBox.setValue(0)
+ self.eraseRainbowBox = QCheckBox(self.optionWidget)
+ self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
- self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
+ self.coverFillBox = QCheckBox(self.optionWidget)
+ self.coverFillBox.setObjectName(u"coverFillBox")
- self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
+ self.gridLayout_2.addWidget(self.coverFillBox, 9, 1, 1, 1)
- self.optionWidget = QWidget(self.centralWidget)
- self.optionWidget.setObjectName(u"optionWidget")
- self.gridLayout_2 = QGridLayout(self.optionWidget)
- self.gridLayout_2.setObjectName(u"gridLayout_2")
- self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
- self.gammaBox = QCheckBox(self.optionWidget)
- self.gammaBox.setObjectName(u"gammaBox")
+ self.rotateRightBox = QCheckBox(self.optionWidget)
+ self.rotateRightBox.setObjectName(u"rotateRightBox")
- self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.rotateRightBox, 10, 1, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
- self.borderBox = QCheckBox(self.optionWidget)
- self.borderBox.setObjectName(u"borderBox")
- self.borderBox.setTristate(True)
+ self.spreadShiftBox = QCheckBox(self.optionWidget)
+ self.spreadShiftBox.setObjectName(u"spreadShiftBox")
- self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
- self.interPanelCropBox = QCheckBox(self.optionWidget)
- self.interPanelCropBox.setObjectName(u"interPanelCropBox")
- self.interPanelCropBox.setTristate(True)
+ self.croppingBox = QCheckBox(self.optionWidget)
+ self.croppingBox.setObjectName(u"croppingBox")
+ self.croppingBox.setTristate(True)
- self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
- self.fileFusionBox = QCheckBox(self.optionWidget)
- self.fileFusionBox.setObjectName(u"fileFusionBox")
+ self.jpegQualityBox = QCheckBox(self.optionWidget)
+ self.jpegQualityBox.setObjectName(u"jpegQualityBox")
- self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.jpegQualityBox, 8, 0, 1, 1)
- self.authorEdit = QLineEdit(self.optionWidget)
- self.authorEdit.setObjectName(u"authorEdit")
- sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
- sizePolicy3.setHorizontalStretch(0)
- sizePolicy3.setVerticalStretch(0)
- sizePolicy3.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
- self.authorEdit.setSizePolicy(sizePolicy3)
- self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
- self.authorEdit.setClearButtonEnabled(False)
+ self.outputSplit = QCheckBox(self.optionWidget)
+ self.outputSplit.setObjectName(u"outputSplit")
- self.gridLayout_2.addWidget(self.authorEdit, 0, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
- self.titleEdit = QLineEdit(self.optionWidget)
- self.titleEdit.setObjectName(u"titleEdit")
- sizePolicy3.setHeightForWidth(self.titleEdit.sizePolicy().hasHeightForWidth())
- self.titleEdit.setSizePolicy(sizePolicy3)
- self.titleEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
- self.titleEdit.setClearButtonEnabled(False)
+ self.metadataTitleBox = QCheckBox(self.optionWidget)
+ self.metadataTitleBox.setObjectName(u"metadataTitleBox")
+ self.metadataTitleBox.setTristate(True)
- self.gridLayout_2.addWidget(self.titleEdit, 0, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
+
+ self.smartCoverCropBox = QCheckBox(self.optionWidget)
+ self.smartCoverCropBox.setObjectName(u"smartCoverCropBox")
+
+ self.gridLayout_2.addWidget(self.smartCoverCropBox, 11, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
- self.eraseRainbowBox = QCheckBox(self.optionWidget)
- self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
+ self.mozJpegBox = QCheckBox(self.optionWidget)
+ self.mozJpegBox.setObjectName(u"mozJpegBox")
+ self.mozJpegBox.setTristate(True)
- self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
- self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
- self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
+ self.autoLevelBox = QCheckBox(self.optionWidget)
+ self.autoLevelBox.setObjectName(u"autoLevelBox")
- self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
- self.rotateBox = QCheckBox(self.optionWidget)
- self.rotateBox.setObjectName(u"rotateBox")
- self.rotateBox.setTristate(True)
+ self.forcePngRgbBox = QCheckBox(self.optionWidget)
+ self.forcePngRgbBox.setObjectName(u"forcePngRgbBox")
+ self.forcePngRgbBox.setEnabled(False)
- self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.forcePngRgbBox, 11, 2, 1, 1)
- self.outputSplit = QCheckBox(self.optionWidget)
- self.outputSplit.setObjectName(u"outputSplit")
+ self.upscaleBox = QCheckBox(self.optionWidget)
+ self.upscaleBox.setObjectName(u"upscaleBox")
+ self.upscaleBox.setTristate(True)
- self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
+ self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
- self.metadataTitleBox = QCheckBox(self.optionWidget)
- self.metadataTitleBox.setObjectName(u"metadataTitleBox")
- self.metadataTitleBox.setTristate(True)
+ self.borderBox = QCheckBox(self.optionWidget)
+ self.borderBox.setObjectName(u"borderBox")
+ self.borderBox.setTristate(True)
- self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
@@ -338,66 +389,149 @@ def setupUi(self, mainWindow):
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
- self.spreadShiftBox = QCheckBox(self.optionWidget)
- self.spreadShiftBox.setObjectName(u"spreadShiftBox")
+ self.legacyExtractBox = QCheckBox(self.optionWidget)
+ self.legacyExtractBox.setObjectName(u"legacyExtractBox")
- self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.legacyExtractBox, 9, 0, 1, 1)
+
+ self.colorBox = QCheckBox(self.optionWidget)
+ self.colorBox.setObjectName(u"colorBox")
+
+ self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
+
+ self.pdfWidthBox = QCheckBox(self.optionWidget)
+ self.pdfWidthBox.setObjectName(u"pdfWidthBox")
+
+ self.gridLayout_2.addWidget(self.pdfWidthBox, 10, 0, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
- self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
+
+ self.outputFolderWidget = QWidget(self.optionWidget)
+ self.outputFolderWidget.setObjectName(u"outputFolderWidget")
+ self.horizontalLayout_3 = QHBoxLayout(self.outputFolderWidget)
+ self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
+ self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
+ self.defaultOutputFolderBox = QCheckBox(self.outputFolderWidget)
+ self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
+ sizePolicy.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
+ self.defaultOutputFolderBox.setSizePolicy(sizePolicy)
+ self.defaultOutputFolderBox.setTristate(True)
+
+ self.horizontalLayout_3.addWidget(self.defaultOutputFolderBox)
+
+ self.defaultOutputFolderButton = QPushButton(self.outputFolderWidget)
+ self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
+ self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
+ icon5 = QIcon()
+ icon5.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.defaultOutputFolderButton.setIcon(icon5)
+
+ self.horizontalLayout_3.addWidget(self.defaultOutputFolderButton)
+
+
+ self.gridLayout_2.addWidget(self.outputFolderWidget, 0, 2, 1, 1)
+
+ self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
+ self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
+
+ self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
+
+ self.autocontrastBox = QCheckBox(self.optionWidget)
+ self.autocontrastBox.setObjectName(u"autocontrastBox")
+ self.autocontrastBox.setTristate(True)
+
+ self.gridLayout_2.addWidget(self.autocontrastBox, 9, 2, 1, 1)
+
+ self.webpBox = QCheckBox(self.optionWidget)
+ self.webpBox.setObjectName(u"webpBox")
+
+ self.gridLayout_2.addWidget(self.webpBox, 12, 0, 1, 1)
- self.webtoonBox = QCheckBox(self.optionWidget)
- self.webtoonBox.setObjectName(u"webtoonBox")
+ self.tempDirBox = QCheckBox(self.optionWidget)
+ self.tempDirBox.setObjectName(u"tempDirBox")
- self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
+ self.gridLayout_2.addWidget(self.tempDirBox, 12, 2, 1, 1)
- self.colorBox = QCheckBox(self.optionWidget)
- self.colorBox.setObjectName(u"colorBox")
+ self.onePageLandscapeBox = QCheckBox(self.optionWidget)
+ self.onePageLandscapeBox.setObjectName(u"onePageLandscapeBox")
- self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.onePageLandscapeBox, 12, 1, 1, 1)
- self.croppingBox = QCheckBox(self.optionWidget)
- self.croppingBox.setObjectName(u"croppingBox")
- self.croppingBox.setTristate(True)
- self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
+ self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
- self.maximizeStrips = QCheckBox(self.optionWidget)
- self.maximizeStrips.setObjectName(u"maximizeStrips")
+ self.buttonWidget = QWidget(self.centralWidget)
+ self.buttonWidget.setObjectName(u"buttonWidget")
+ sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
+ sizePolicy4.setHorizontalStretch(0)
+ sizePolicy4.setVerticalStretch(0)
+ sizePolicy4.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
+ self.buttonWidget.setSizePolicy(sizePolicy4)
+ self.gridLayout_4 = QGridLayout(self.buttonWidget)
+ self.gridLayout_4.setObjectName(u"gridLayout_4")
+ self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
+ self.convertButton = QPushButton(self.buttonWidget)
+ self.convertButton.setObjectName(u"convertButton")
+ self.convertButton.setMinimumSize(QSize(0, 30))
+ self.convertButton.setFont(font)
+ icon6 = QIcon()
+ icon6.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.convertButton.setIcon(icon6)
- self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
+ self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
- self.noRotateBox = QCheckBox(self.optionWidget)
- self.noRotateBox.setObjectName(u"noRotateBox")
+ self.clearButton = QPushButton(self.buttonWidget)
+ self.clearButton.setObjectName(u"clearButton")
+ self.clearButton.setMinimumSize(QSize(0, 30))
+ icon7 = QIcon()
+ icon7.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.clearButton.setIcon(icon7)
- self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
+ self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
- self.deleteBox = QCheckBox(self.optionWidget)
- self.deleteBox.setObjectName(u"deleteBox")
+ self.deviceBox = QComboBox(self.buttonWidget)
+ self.deviceBox.setObjectName(u"deviceBox")
+ self.deviceBox.setMinimumSize(QSize(0, 28))
- self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
+ self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
- self.upscaleBox = QCheckBox(self.optionWidget)
- self.upscaleBox.setObjectName(u"upscaleBox")
- self.upscaleBox.setTristate(True)
+ self.fileButton = QPushButton(self.buttonWidget)
+ self.fileButton.setObjectName(u"fileButton")
+ self.fileButton.setMinimumSize(QSize(0, 30))
+ icon8 = QIcon()
+ icon8.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.fileButton.setIcon(icon8)
- self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
+ self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
- self.mozJpegBox = QCheckBox(self.optionWidget)
- self.mozJpegBox.setObjectName(u"mozJpegBox")
- self.mozJpegBox.setTristate(True)
+ self.directoryButton = QPushButton(self.buttonWidget)
+ self.directoryButton.setObjectName(u"directoryButton")
+ sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
+ sizePolicy5.setHorizontalStretch(0)
+ sizePolicy5.setVerticalStretch(0)
+ sizePolicy5.setHeightForWidth(self.directoryButton.sizePolicy().hasHeightForWidth())
+ self.directoryButton.setSizePolicy(sizePolicy5)
+ self.directoryButton.setIcon(icon5)
- self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
+ self.gridLayout_4.addWidget(self.directoryButton, 0, 4, 1, 1)
- self.autoLevelBox = QCheckBox(self.optionWidget)
- self.autoLevelBox.setObjectName(u"autoLevelBox")
+ self.formatBox = QComboBox(self.buttonWidget)
+ self.formatBox.setObjectName(u"formatBox")
+ self.formatBox.setMinimumSize(QSize(0, 28))
- self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
+ self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 1)
+ self.clearButton.raise_()
+ self.deviceBox.raise_()
+ self.convertButton.raise_()
+ self.fileButton.raise_()
+ self.directoryButton.raise_()
+ self.formatBox.raise_()
- self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
+ self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
@@ -421,77 +555,84 @@ def setupUi(self, mainWindow):
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
- self.chunkSizeWidget = QWidget(self.centralWidget)
- self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
- sizePolicy3.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
- self.chunkSizeWidget.setSizePolicy(sizePolicy3)
- self.chunkSizeWidget.setVisible(False)
- self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
- self.horizontalLayout_4.setSpacing(0)
- self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
- self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
- self.chunkSizeLabel = QLabel(self.chunkSizeWidget)
- self.chunkSizeLabel.setObjectName(u"chunkSizeLabel")
- sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
- sizePolicy4.setHorizontalStretch(0)
- sizePolicy4.setVerticalStretch(0)
- sizePolicy4.setHeightForWidth(self.chunkSizeLabel.sizePolicy().hasHeightForWidth())
- self.chunkSizeLabel.setSizePolicy(sizePolicy4)
+ self.customWidget = QWidget(self.centralWidget)
+ self.customWidget.setObjectName(u"customWidget")
+ self.customWidget.setVisible(False)
+ self.gridLayout_3 = QGridLayout(self.customWidget)
+ self.gridLayout_3.setObjectName(u"gridLayout_3")
+ self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
+ self.hLabel = QLabel(self.customWidget)
+ self.hLabel.setObjectName(u"hLabel")
+ sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
+ self.hLabel.setSizePolicy(sizePolicy1)
- self.horizontalLayout_4.addWidget(self.chunkSizeLabel)
+ self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
- self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
- self.chunkSizeBox.setObjectName(u"chunkSizeBox")
- self.chunkSizeBox.setMinimum(100)
- self.chunkSizeBox.setMaximum(600)
- self.chunkSizeBox.setValue(400)
+ self.widthBox = QSpinBox(self.customWidget)
+ self.widthBox.setObjectName(u"widthBox")
+ self.widthBox.setMaximum(6000)
- self.horizontalLayout_4.addWidget(self.chunkSizeBox)
+ self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
- self.chunkSizeWarnLabel = QLabel(self.chunkSizeWidget)
- self.chunkSizeWarnLabel.setObjectName(u"chunkSizeWarnLabel")
- sizePolicy4.setHeightForWidth(self.chunkSizeWarnLabel.sizePolicy().hasHeightForWidth())
- self.chunkSizeWarnLabel.setSizePolicy(sizePolicy4)
+ self.wLabel = QLabel(self.customWidget)
+ self.wLabel.setObjectName(u"wLabel")
+ sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
+ self.wLabel.setSizePolicy(sizePolicy1)
- self.horizontalLayout_4.addWidget(self.chunkSizeWarnLabel)
+ self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
+
+ self.heightBox = QSpinBox(self.customWidget)
+ self.heightBox.setObjectName(u"heightBox")
+ self.heightBox.setMaximum(8000)
+ self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
- self.gridLayout.addWidget(self.chunkSizeWidget, 6, 0, 1, 1)
+
+ self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
mainWindow.setCentralWidget(self.centralWidget)
self.statusBar = QStatusBar(mainWindow)
self.statusBar.setObjectName(u"statusBar")
self.statusBar.setSizeGripEnabled(False)
mainWindow.setStatusBar(self.statusBar)
- QWidget.setTabOrder(self.convertButton, self.clearButton)
+ QWidget.setTabOrder(self.jobList, self.fileButton)
+ QWidget.setTabOrder(self.fileButton, self.clearButton)
QWidget.setTabOrder(self.clearButton, self.deviceBox)
- QWidget.setTabOrder(self.deviceBox, self.formatBox)
- QWidget.setTabOrder(self.formatBox, self.mangaBox)
+ QWidget.setTabOrder(self.deviceBox, self.widthBox)
+ QWidget.setTabOrder(self.widthBox, self.heightBox)
+ QWidget.setTabOrder(self.heightBox, self.convertButton)
+ QWidget.setTabOrder(self.convertButton, self.mangaBox)
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
QWidget.setTabOrder(self.rotateBox, self.qualityBox)
QWidget.setTabOrder(self.qualityBox, self.webtoonBox)
QWidget.setTabOrder(self.webtoonBox, self.upscaleBox)
QWidget.setTabOrder(self.upscaleBox, self.gammaBox)
- QWidget.setTabOrder(self.gammaBox, self.borderBox)
+ QWidget.setTabOrder(self.gammaBox, self.gammaSlider)
+ QWidget.setTabOrder(self.gammaSlider, self.borderBox)
QWidget.setTabOrder(self.borderBox, self.outputSplit)
QWidget.setTabOrder(self.outputSplit, self.colorBox)
QWidget.setTabOrder(self.colorBox, self.mozJpegBox)
QWidget.setTabOrder(self.mozJpegBox, self.maximizeStrips)
QWidget.setTabOrder(self.maximizeStrips, self.croppingBox)
- QWidget.setTabOrder(self.croppingBox, self.spreadShiftBox)
+ QWidget.setTabOrder(self.croppingBox, self.croppingPowerSlider)
+ QWidget.setTabOrder(self.croppingPowerSlider, self.preserveMarginBox)
+ QWidget.setTabOrder(self.preserveMarginBox, self.spreadShiftBox)
QWidget.setTabOrder(self.spreadShiftBox, self.deleteBox)
QWidget.setTabOrder(self.deleteBox, self.disableProcessingBox)
- QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
- QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
+ QWidget.setTabOrder(self.disableProcessingBox, self.fileFusionBox)
+ QWidget.setTabOrder(self.fileFusionBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
- QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
- QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
- QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
- QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
- QWidget.setTabOrder(self.editorButton, self.wikiButton)
- QWidget.setTabOrder(self.wikiButton, self.jobList)
- QWidget.setTabOrder(self.jobList, self.gammaSlider)
- QWidget.setTabOrder(self.gammaSlider, self.widthBox)
+ QWidget.setTabOrder(self.interPanelCropBox, self.metadataTitleBox)
+ QWidget.setTabOrder(self.metadataTitleBox, self.coverFillBox)
+ QWidget.setTabOrder(self.coverFillBox, self.chunkSizeCheckBox)
+ QWidget.setTabOrder(self.chunkSizeCheckBox, self.chunkSizeBox)
+ QWidget.setTabOrder(self.chunkSizeBox, self.eraseRainbowBox)
+ QWidget.setTabOrder(self.eraseRainbowBox, self.rotateFirstBox)
+ QWidget.setTabOrder(self.rotateFirstBox, self.autoLevelBox)
+ QWidget.setTabOrder(self.autoLevelBox, self.autocontrastBox)
+ QWidget.setTabOrder(self.autocontrastBox, self.editorButton)
+ QWidget.setTabOrder(self.editorButton, self.kofiButton)
+ QWidget.setTabOrder(self.kofiButton, self.wikiButton)
self.retranslateUi(mainWindow)
@@ -501,7 +642,18 @@ def setupUi(self, mainWindow):
def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
#if QT_CONFIG(tooltip)
- self.jobList.setToolTip(QCoreApplication.translate("mainWindow", u" Double click on source to open metadata editor.
", None))
+ self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"After calculating the cropping boundaries, "back up" a specified percentage amount.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
+ self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
+ self.jpegQualityLabel.setText(QCoreApplication.translate("mainWindow", u"JPEG Quality:", None))
+#if QT_CONFIG(tooltip)
+ self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u" Warning: chunk size greater than default may cause
performance/battery issues, especially on older devices.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.chunkSizeLabel.setText(QCoreApplication.translate("mainWindow", u"Chunk size MB:", None))
+ self.chunkSizeWarnLabel.setText(QCoreApplication.translate("mainWindow", u"Greater than default may cause performance issues on older ereaders.", None))
+#if QT_CONFIG(tooltip)
+ self.jobList.setToolTip(QCoreApplication.translate("mainWindow", u"Double click on source to open it in metadata editor.
", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"Shift+Click to edit directory.
", None))
@@ -509,153 +661,215 @@ def retranslateUi(self, mainWindow):
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
+ self.youtubeButton.setText(QCoreApplication.translate("mainWindow", u"YouTube", None))
+ self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
+ self.discordButton.setText(QCoreApplication.translate("mainWindow", u"Discord", None))
#if QT_CONFIG(tooltip)
- self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u" Shift+Click to select the output directory for this list.
", None))
+ self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
- self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
- self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
+ self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip)
- self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"Target device.
", None))
+ self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u" Unchecked - 1x4
Keep format 1x4 panels strips.
Checked - 2x2
Turn 1x4 strips to 2x2 to maximize screen usage.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
+#if QT_CONFIG(tooltip)
+ self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Split
Double page spreads will be cut into two separate pages.
Indeterminate - Split and rotate
Double page spreads will be displayed twice. First split and then rotated.
Checked - Rotate
Double page spreads will be rotated.
", None))
#endif // QT_CONFIG(tooltip)
+ self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
- self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"Add CBR, CBZ, CB7 or PDF file to queue.
", None))
+ self.pngLegacyBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use a more compatible 8 bit PNG instead of 4 bit.", None))
#endif // QT_CONFIG(tooltip)
- self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
+ self.pngLegacyBox.setText(QCoreApplication.translate("mainWindow", u"PNG Legacy Mode", None))
#if QT_CONFIG(tooltip)
- self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u" Use this to select the default output directory.
", None))
+ self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"
Unchecked - Disabled
Disabled
Indeterminate - Horizontal
Crop empty horizontal lines.
Checked - Both
Crop empty horizontal and vertical lines.
", None))
#endif // QT_CONFIG(tooltip)
- self.defaultOutputFolderButton.setText("")
+ self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
#if QT_CONFIG(tooltip)
- self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - next to source
Place output files next to source files
Indeterminate - folder next to source
Place output files in a folder next to source files
Checked - Custom
Place output files in custom directory specified by right button
", None))
+ self.titleEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Title
", None))
#endif // QT_CONFIG(tooltip)
- self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
+ self.titleEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Title", None))
#if QT_CONFIG(tooltip)
- self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"Output format.
", None))
+ self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip)
+ self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
#if QT_CONFIG(tooltip)
- self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+ self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"Enable special parsing mode for Korean Webtoons.
", None))
#endif // QT_CONFIG(tooltip)
- self.hLabel.setText(QCoreApplication.translate("mainWindow", u"Custom height:", None))
+ self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
- self.widthBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+ self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"Combines all selected files into a single file. (Helpful for combining chapters into volumes.)
", None))
#endif // QT_CONFIG(tooltip)
+ self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
#if QT_CONFIG(tooltip)
- self.wLabel.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+ self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
- self.wLabel.setText(QCoreApplication.translate("mainWindow", u"Custom width:", None))
+ self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
#if QT_CONFIG(tooltip)
- self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+ self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"Set a custom gamma correction.
1.0 is default (disabled).
< 1.0 makes the image brighter.
> 1.0 makes the image darker.
1.8 was the default in KCC 9.1.0 and earlier.
Use if you want to make midtones darker.
", None))
#endif // QT_CONFIG(tooltip)
+ self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
- self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"After calculating the cropping boundaries, "back up" a specified percentage amount.
", None))
+ self.noQuantizeBox.setToolTip(QCoreApplication.translate("mainWindow", u"Don't quantize PNG images to 16 colors (4 bit)\n"
+"\n"
+"This will double file size but preserve all 256 colors (8 bit).\n"
+"\n"
+"Eink only has 16 shades of gray so you probably don't want this.", None))
#endif // QT_CONFIG(tooltip)
- self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
- self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
+ self.noQuantizeBox.setText(QCoreApplication.translate("mainWindow", u"No Quantize", None))
+#if QT_CONFIG(tooltip)
+ self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
+#endif // QT_CONFIG(tooltip)
+ self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", None))
#if QT_CONFIG(tooltip)
- self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable automatic gamma correction.
", None))
+ self.coverFillBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resize cover to exact device resolution by center-cropping to aspect ratio first.\n"
+"May crop top/bottom or left/right depending on source aspect ratio. Not implemented for Kindle Scribe.", None))
#endif // QT_CONFIG(tooltip)
- self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
+ self.coverFillBox.setText(QCoreApplication.translate("mainWindow", u"Cover Fill", None))
+#if QT_CONFIG(tooltip)
+ self.rotateRightBox.setToolTip(QCoreApplication.translate("mainWindow", u"Rotate 2 page spreads in opposite direction than normal.", None))
+#endif // QT_CONFIG(tooltip)
+ self.rotateRightBox.setText(QCoreApplication.translate("mainWindow", u"Rotate Right", None))
#if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"Enable right-to-left reading.
", None))
#endif // QT_CONFIG(tooltip)
- self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
+ self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left (manga)", None))
#if QT_CONFIG(tooltip)
- self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Autodetection
The color of margins fill will be detected automatically.
Indeterminate - White
Margins will be untouched.
Checked - Black
Margins will be filled with black color.
", None))
+ self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
#endif // QT_CONFIG(tooltip)
- self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
+ self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
- self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Disabled
Disabled
Indeterminate - Horizontal
Crop empty horizontal lines.
Checked - Both
Crop empty horizontal and vertical lines.
", None))
+ self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Disabled
Disabled
Indeterminate - Margins
Margins
Checked - Margins + page numbers
Margins +page numbers
", None))
#endif // QT_CONFIG(tooltip)
- self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
+ self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
- self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"Combines all selected files into a single file. (Helpful for combining chapters into volumes.)
", None))
+ self.jpegQualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"The JPEG quality, on a scale from 0 (worst) to 95 (best). \n"
+"\n"
+"Default is 85 for most devices besides Kindle Scribe and Colorsoft, which are 90.\n"
+"\n"
+"Higher values are larger and higher quality, and may resolve blank page issues.", None))
#endif // QT_CONFIG(tooltip)
- self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
+ self.jpegQualityBox.setText(QCoreApplication.translate("mainWindow", u"Custom JPEG Quality", None))
#if QT_CONFIG(tooltip)
- self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
+ self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Automatic mode
The output will be split automatically.
Checked - Volume mode
Every subdirectory will be considered as a separate volume.
", None))
#endif // QT_CONFIG(tooltip)
- self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
+ self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip)
- self.titleEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Title is based on filename, directory name or metadata", None))
+ self.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Don't use metadata Title
Write default title.
Indeterminate - Add metadata Title to the default schema
Write default title with Title from ComicInfo.xml or other embedded metadata.
Checked - Use metadata Title only
Write Title from ComicInfo.xml or other embedded metadata.
", None))
#endif // QT_CONFIG(tooltip)
- self.titleEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Title", None))
+ self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
+#if QT_CONFIG(tooltip)
+ self.smartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Attempt to crop main cover from wide image.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.smartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"Smart Cover Crop", None))
#if QT_CONFIG(tooltip)
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"When the spread splitter option is partially checked,
Unchecked - Rotate Last
Put the rotated 2 page spread after the split spreads.
Checked - Rotate First
Put the rotated 2 page spread before the split spreads.
", None))
#endif // QT_CONFIG(tooltip)
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
#if QT_CONFIG(tooltip)
- self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
+ self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - JPEG
Use JPEG files
Indeterminate - force PNG
Create PNG files instead JPEG for black and white images
Checked - mozJpeg
10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2
", None))
#endif // QT_CONFIG(tooltip)
- self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", None))
+ self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
- self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked
Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.
Checked
Output file size specified in "Chunk size MB" before split occurs.
", None))
+ self.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"By default, KCC maps the darkest pixel value to pure black (the black point.)
Extreme black point sets the black point to be the most common dark pixel value.
Useful when text is black but artwork is gray.
", None))
#endif // QT_CONFIG(tooltip)
- self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
+ self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Extreme Black Point", None))
#if QT_CONFIG(tooltip)
- self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Split
Double page spreads will be cut into two separate pages.
Indeterminate - Split and rotate
Double page spreads will be displayed twice. First split and then rotated.
Checked - Rotate
Double page spreads will be rotated.
", None))
+ self.forcePngRgbBox.setToolTip(QCoreApplication.translate("mainWindow", u"Force full color images to be saved in lossless PNG format, dramatically increases the filesize.", None))
#endif // QT_CONFIG(tooltip)
- self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
+ self.forcePngRgbBox.setText(QCoreApplication.translate("mainWindow", u"Force PNG RGB", None))
#if QT_CONFIG(tooltip)
- self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Automatic mode
The output will be split automatically.
Checked - Volume mode
Every subdirectory will be considered as a separate volume.
", None))
+ self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Nothing
Images smaller than device resolution will not be resized.
Indeterminate - Stretching
Images smaller than device resolution will be resized. Aspect ratio will be not preserved.
Checked - Upscaling
Images smaller than device resolution will be resized. Aspect ratio will be preserved.
", None))
#endif // QT_CONFIG(tooltip)
- self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
+ self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
- self.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Don't use metadata Title
Write default title.
Indeterminate - Add metadata Title to the default schema
Write default title with Title from ComicInfo.xml or other embedded metadata.
Checked - Use metadata Title only
Write Title from ComicInfo.xml or other embedded metadata.
", None))
+ self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Autodetection
The color of margins fill will be detected automatically.
Indeterminate - White
Margins will be untouched.
Checked - Black
Margins will be filled with black color.
", None))
#endif // QT_CONFIG(tooltip)
- self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
+ self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - 4 panels
Zoom each corner separately.
Indeterminate - 2 panels
Zoom only the top and bottom of the page.
Checked - 4 high-quality panels
Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.
", None))
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
- self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
+ self.legacyExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF/EPUB image extraction method from older KCC versions.
Use if standard extraction fails for whatever reason.
", None))
#endif // QT_CONFIG(tooltip)
- self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
+ self.legacyExtractBox.setText(QCoreApplication.translate("mainWindow", u"Legacy Extract", None))
+#if QT_CONFIG(tooltip)
+ self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable conversion to grayscale.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
+#if QT_CONFIG(tooltip)
+ self.pdfWidthBox.setToolTip(QCoreApplication.translate("mainWindow", u"Render vector PDFs to device width instead of height.\n"
+"\n"
+"Useful if you plan to crop a little off the top and bottom to fill screen.", None))
+#endif // QT_CONFIG(tooltip)
+ self.pdfWidthBox.setText(QCoreApplication.translate("mainWindow", u"PDF Width Render", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not process any image, ignore profile and processing options.
", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
- self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"Enable special parsing mode for Korean Webtoons.
", None))
+ self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - next to source
Place output files next to source files
Indeterminate - folder next to source
Place output files in a folder next to source files
Checked - Custom
Place output files in custom directory specified by right button
", None))
#endif // QT_CONFIG(tooltip)
- self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
+ self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
#if QT_CONFIG(tooltip)
- self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable conversion to grayscale.
", None))
+ self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u"Use this to select the default output directory.
", None))
#endif // QT_CONFIG(tooltip)
- self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
+ self.defaultOutputFolderButton.setText("")
#if QT_CONFIG(tooltip)
- self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Disabled
Disabled
Indeterminate - Margins
Margins
Checked - Margins + page numbers
Margins +page numbers
", None))
+ self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked
Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.
Checked
Output file size specified in "Chunk size MB" before split occurs.
", None))
#endif // QT_CONFIG(tooltip)
- self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
+ self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip)
- self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - 1x4
Keep format 1x4 panels strips.
Checked - 2x2
Turn 1x4 strips to 2x2 to maximize screen usage.
", None))
+ self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - BW only
Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.
Indeterminate - Disabled
Disable autocontrast
Checked - BW and Color
BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.
", None))
#endif // QT_CONFIG(tooltip)
- self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
+ self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Custom Autocontrast", None))
#if QT_CONFIG(tooltip)
- self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
+ self.webpBox.setToolTip(QCoreApplication.translate("mainWindow", u"Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.\n"
+"\n"
+"Ignored for Kindle EPUB/MOBI and all PDF.", None))
#endif // QT_CONFIG(tooltip)
- self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
+ self.webpBox.setText(QCoreApplication.translate("mainWindow", u"WebP (experimental)", None))
#if QT_CONFIG(tooltip)
- self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
+ self.tempDirBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Main Drive
Use dedicated temporary directory on main OS drive.
Checked - Source File Drive
Create temporary file directory on source file drive.
", None))
#endif // QT_CONFIG(tooltip)
- self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
+ self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
#if QT_CONFIG(tooltip)
- self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - Nothing
Images smaller than device resolution will not be resized.
Indeterminate - Stretching
Images smaller than device resolution will be resized. Aspect ratio will be not preserved.
Checked - Upscaling
Images smaller than device resolution will be resized. Aspect ratio will be preserved.
", None))
+ self.onePageLandscapeBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - 2 page landscape
2 viewports for left and right pages
Checked - 1 page landscape
A single centered viewport for 1 page
", None))
#endif // QT_CONFIG(tooltip)
- self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
+ self.onePageLandscapeBox.setText(QCoreApplication.translate("mainWindow", u"1 Page Landscape", None))
#if QT_CONFIG(tooltip)
- self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"Unchecked - JPEG
Use JPEG files
Indeterminate - force PNG
Create PNG files instead JPEG
Checked - mozJpeg
10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2
", None))
+ self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"Shift+Click to select the output directory for this list.
", None))
#endif // QT_CONFIG(tooltip)
- self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
+ self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
+ self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
+#if QT_CONFIG(tooltip)
+ self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"Target device.
", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"Add CBR, CBZ, CB7 or PDF file to queue.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
+#if QT_CONFIG(tooltip)
+ self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"Add directory containing JPG, PNG or GIF files to queue.
CBR, CBZ and CB7 files inside will not be processed!
", None))
+#endif // QT_CONFIG(tooltip)
+ self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add input folder(s)", None))
#if QT_CONFIG(tooltip)
- self.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"Set the most common dark pixel value to be the black point for leveling on a page by page basis.
Skipped for any images that were originally color.
Use only if default autocontrast still results in very gray faded blacks.
Reccomended to use with Custom Gamma = 1.0 (Disabled).
", None))
+ self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"Output format.
", None))
#endif // QT_CONFIG(tooltip)
- self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Aggressive Black Point", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#if QT_CONFIG(tooltip)
- self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"Warning: chunk size greater than default may cause
performance/battery issues, especially on older devices.
", None))
+ self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.hLabel.setText(QCoreApplication.translate("mainWindow", u"Custom height:", None))
+#if QT_CONFIG(tooltip)
+ self.widthBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+#endif // QT_CONFIG(tooltip)
+#if QT_CONFIG(tooltip)
+ self.wLabel.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.wLabel.setText(QCoreApplication.translate("mainWindow", u"Custom width:", None))
+#if QT_CONFIG(tooltip)
+ self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"Resolution of the target device.
", None))
#endif // QT_CONFIG(tooltip)
- self.chunkSizeLabel.setText(QCoreApplication.translate("mainWindow", u"Chunk size MB:", None))
- self.chunkSizeWarnLabel.setText(QCoreApplication.translate("mainWindow", u"Greater than default may cause performance issues on older ereaders.", None))
# retranslateUi
diff --git a/kindlecomicconverter/KCC_ui_editor.py b/kindlecomicconverter/KCC_ui_editor.py
index 80a8f76e0..7bc559d4c 100644
--- a/kindlecomicconverter/KCC_ui_editor.py
+++ b/kindlecomicconverter/KCC_ui_editor.py
@@ -156,6 +156,15 @@ def setupUi(self, editorDialog):
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)
diff --git a/kindlecomicconverter/__init__.py b/kindlecomicconverter/__init__.py
index b3a1cc5d9..c8f615d4a 100644
--- a/kindlecomicconverter/__init__.py
+++ b/kindlecomicconverter/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '9.1.0'
+__version__ = '10.1.3'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano , Pawel Jastrzebski , darodi'
__docformat__ = 'restructuredtext en'
diff --git a/kindlecomicconverter/comic2ebook.py b/kindlecomicconverter/comic2ebook.py
index 2699f2572..28744d0c9 100755
--- a/kindlecomicconverter/comic2ebook.py
+++ b/kindlecomicconverter/comic2ebook.py
@@ -18,10 +18,13 @@
# PERFORMANCE OF THIS SOFTWARE.
#
+from collections import Counter
import os
import pathlib
import re
+import shutil
import sys
+import xml.etree.ElementTree as ET
from argparse import ArgumentParser
from time import perf_counter, strftime, gmtime
from copy import copy
@@ -29,8 +32,8 @@
from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List
-from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
-from tempfile import mkdtemp, gettempdir, TemporaryFile
+from zipfile import ZipFile, ZIP_STORED
+from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool, cpu_count
from uuid import uuid4
@@ -42,13 +45,13 @@
from psutil import virtual_memory, disk_usage
from html import escape as hescape
import pymupdf
-import numpy as np
-from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
+from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean, get_contain_resolution
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
+from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -66,18 +69,30 @@ def main(argv=None):
parser.print_help()
return 0
if sys.platform.startswith('win'):
- sources = set([source for option in options.input for source in glob(escape(option))])
+ sources = [source for option in options.input for source in glob(escape(option))]
else:
- sources = set(options.input)
+ sources = options.input
if len(sources) == 0:
print('No matching files found.')
return 1
+ if options.filefusion:
+ fusion_path = makeFusion(list(sources))
+ sources.clear()
+ sources.append(fusion_path)
for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(args)
options = checkOptions(options)
print('Working on ' + 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)
+ checkPre('LLL-')
return 0
@@ -125,14 +140,14 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
"content=\"width=" + str(imgsizeframe[0]) + ", height=" + str(imgsizeframe[1]) + "\"/>\n"
"\n",
"\n",
- "\n",
+ "
\n",
])
if options.iskindle:
# this display none div fixes formatting issues with virtual panel mode, for some reason
f.write('
.
\n')
f.write(f'

\n')
if imgfile2:
- f.write(f'

\n')
+ f.write(f'

\n')
f.write("
\n")
if options.iskindle and options.panelview:
if options.autoscale:
@@ -299,12 +314,33 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
f.writelines(["
", hescape(options.summary), "\n"])
for author in options.authors:
f.writelines(["
", hescape(author), "\n"])
- f.writelines(["
" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "\n",
- "
\n"])
+ if not options.iskindle and options.series:
+ f.writelines(['
', hescape(options.series), "\n"])
+ f.writelines(['
', "series", "\n"])
+ if options.volume and options.number:
+ f.writelines(['
', hescape(f"{options.volume}.{options.number}"), "\n"])
+ elif options.volume:
+ f.writelines(['
', hescape(options.volume), "\n"])
+ elif options.number:
+ f.writelines(['
', hescape(options.number), "\n"])
+ f.write("
" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "\n")
+ if cover:
+ f.write("
\n")
if options.iskindle and options.profile != 'Custom':
f.writelines(["
\n",
- "
\n",
+ ])
+ if not options.kfx_resolution:
+ f.writelines([
+ "
\n",
+ ])
+ else:
+ x, y = options.kfx_resolution
+ f.writelines([
+ "
\n",
+ ])
+ f.writelines([
"
\n",
"
\n",
"
\n",
@@ -341,6 +377,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
+ elif '.webp' == filename[1]:
+ mt = 'image/webp'
else:
mt = 'image/jpeg'
f.write("
\n')
f.write("\n\n")
@@ -445,7 +485,7 @@ def pageSpreadProperty(pageside):
""])
f.close()
-def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, len_tomes=0):
+def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, job_progress='', len_tomes=0):
filelist = []
chapterlist = []
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
@@ -532,7 +572,8 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
"}\n"])
f.close()
build_html_start = perf_counter()
- cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
+ if cover:
+ cover.save_to_folder(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
dot_clean(path)
options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
@@ -552,7 +593,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
else:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
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 ComicInfo.xml has bookmarks
if ischunked:
options.comicinfo_chapters = []
@@ -588,7 +629,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
buildOPF(path, options.title, filelist, originalpath, cover)
-def buildPDF(path, title, cover=None, output_file=None):
+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.
@@ -613,11 +654,11 @@ def buildPDF(path, title, cover=None, output_file=None):
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
- print(f"MuPDF output: {end-start} sec")
+ print(f"{job_progress}MuPDF output: {end-start} sec")
return output_file
-def imgDirectoryProcessing(path):
+def imgDirectoryProcessing(path, job_progress=''):
global workerPool, workerOutput
workerPool = Pool(maxtasksperchild=100)
workerOutput = []
@@ -637,7 +678,7 @@ def imgDirectoryProcessing(path):
workerPool.close()
workerPool.join()
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
dot_clean(path)
@@ -647,7 +688,7 @@ def imgDirectoryProcessing(path):
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
- raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
+ raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("C2E: Source directory is empty.")
@@ -676,7 +717,6 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
- is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
@@ -686,18 +726,24 @@ def imgFileProcessing(work):
img.gammaCorrectImage()
+ if not img.colorOutput:
+ img.convertToGrayscale()
+
img.autocontrastImage()
img.resizeImage()
- img.optimizeForDisplay(opt.eraserainbow, is_color)
+ img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
- if is_color:
+ if img.colorOutput:
pass
elif opt.forcepng:
- img.convertToGrayscale()
- if opt.format != 'PDF':
+ if not opt.noquantize:
img.quantizeImage()
- else:
- img.convertToGrayscale()
+ 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())
return output
except Exception:
@@ -727,7 +773,9 @@ def render_page(vector):
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
- target_height = vector[4]
+ target_width = vector[4]
+ target_height = vector[5]
+ pdf_width = vector[6]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
@@ -738,9 +786,13 @@ def render_page(vector):
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
- zoom = target_height / page.rect.height
+ if not pdf_width or page.rect.width > page.rect.height:
+ zoom = target_height / page.rect.height
+ else:
+ zoom = target_width / page.rect.width
mat = pymupdf.Matrix(zoom, zoom)
# TODO: decide colorspace earlier so later color check is cheaper.
+ # This is actually pretty hard when you have to deal with color vector text
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))
@@ -787,9 +839,7 @@ def extract_page(vector):
if len(image_list) > 1:
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
if not image_list:
- width, height = int(page.rect.width), int(page.rect.height)
- blank_page = Image.new("RGB", (width, height), "white")
- blank_page.save(output_path)
+ continue
else:
xref = image_list[0][0]
d = doc.extract_image(xref)
@@ -804,7 +854,7 @@ def extract_page(vector):
-def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
+def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
@@ -824,7 +874,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
cpu = cpu_count()
# make vectors of arguments for the processes
- vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
+ vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
@@ -838,14 +888,25 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
-def getWorkFolder(afile):
+def getWorkFolder(afile, workdir=None):
+ if not workdir:
+ if options.tempdir:
+ workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
+ else:
+ workdir = mkdtemp('', 'KCC-')
+ fullPath = os.path.join(workdir, 'OEBPS', 'Images')
+ else:
+ fullPath = workdir
+
+ if options.tempdir:
+ check_path = os.path.dirname(afile)
+ else:
+ check_path = gettempdir()
+
if os.path.isdir(afile):
- if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
+ if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
- workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
- os.rmdir(workdir)
- fullPath = os.path.join(workdir, 'OEBPS', 'Images')
copytree(afile, fullPath)
sanitizePermissions(fullPath)
return workdir
@@ -853,51 +914,120 @@ def getWorkFolder(afile):
rmtree(workdir, True)
raise UserWarning("Failed to prepare a workspace.")
elif os.path.isfile(afile):
- if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
+ if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
- workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
+ if not os.path.exists(fullPath):
+ os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
- target_height = options.profileData[1][1]
+ if options.legacyextract:
+ pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
+ njpg = pdf.extract()
+ if njpg == 0:
+ raise UserWarning("Failed to extract images from PDF file.")
+ return workdir
+ target_width, target_height = options.profileData[1]
if options.cropping == 1:
- target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
+ target_height *= 1.2 #Account for possible margin at the top and bottom
+ target_width *= 1.2
elif options.cropping == 2:
- target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
+ target_height *= 1.25 #Account for possible margin at the top and bottom with page number
+ target_width *= 1.25
try:
- mupdf_pdf_process_pages_parallel(afile, workdir, target_height)
+ mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
+ return workdir
else:
- workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
+ if not os.path.exists(fullPath):
+ os.makedirs(fullPath)
try:
cbx = comicarchive.ComicArchive(afile)
- path = cbx.extract(workdir)
+ path = cbx.extract(fullPath)
sanitizePermissions(path)
- tdir = os.listdir(workdir)
+ tdir = os.listdir(fullPath)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
- if os.path.isdir(os.path.join(workdir, tdir[0])):
+ if os.path.isdir(os.path.join(fullPath, tdir[0])):
os.replace(
- os.path.join(workdir, 'ComicInfo.xml'),
- os.path.join(workdir, tdir[0], 'ComicInfo.xml')
+ 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(workdir, tdir[0])):
- path = os.path.join(workdir, tdir[0])
-
- except OSError as e:
- rmtree(workdir, True)
- raise UserWarning(e)
+ 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]))
+
+ if options.legacyextract:
+ return workdir
+
+ if afile.lower().endswith('.epub'):
+ container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
+ opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
+ opf_path = os.path.join(path, opf_path)
+ opf = ET.parse(opf_path)
+ spine = []
+ for spine_item in opf.findall(r'.//{*}itemref'):
+ spine.append(spine_item.attrib.get('idref'))
+ manifest_dict = {}
+ for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
+ manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
+ ordered_image_paths = []
+ for i, spine_item in enumerate(spine):
+ if spine_item not in manifest_dict:
+ continue
+ page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
+ page = ET.parse(page_path)
+ imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
+
+ largest_size = 0
+ img_path = None
+ for img in imgs:
+ for key in img.attrib:
+ if 'src' in key or 'href' in key:
+ temp_img_path = img.attrib[key]
+ if temp_img_path.startswith('..'):
+ temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
+ else:
+ temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
+ try:
+ temp_size = os.path.getsize(temp_img_path)
+ if temp_size > largest_size:
+ largest_size = temp_size
+ img_path = temp_img_path
+ except OSError:
+ pass
+ # TODO empty image
+ if img_path:
+ ordered_image_paths.append(img_path)
+ # fallback if naive spine extraction fails
+ if not ordered_image_paths:
+ return workdir
+
+ if options.tempdir:
+ workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
+ else:
+ workdir2 = mkdtemp('', 'KCC-')
+ for i, img_path in enumerate(ordered_image_paths):
+ _, ext = os.path.splitext(img_path)
+ fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
+ os.makedirs(fullpath2, exist_ok=True)
+ shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
+ rmtree(workdir, True)
+ return workdir2
+
+ return workdir
+ finally:
+ pass
else:
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):
+ source_path = Path(srcpath)
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if 'Ko' in options.profile and options.format == 'EPUB':
@@ -907,20 +1037,29 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
else:
ext = '.kepub.epub'
if wantedname is not None:
+ wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
- elif os.path.isdir(srcpath):
- filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
+ elif wanted_ext == '.mobi' and ext == '.epub':
+ filename = os.path.abspath(wanted_root + ext)
+ # output directory
else:
- filename = os.path.join(os.path.abspath(options.output),
- os.path.basename(os.path.splitext(srcpath)[0]) + ext)
+ abs_path = os.path.abspath(options.output)
+ 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):
filename = srcpath + tomenumber + ext
else:
if 'Ko' in options.profile and options.format == 'EPUB':
- src = pathlib.Path(srcpath)
- name = re.sub(r'\W+', '_', src.stem) + tomenumber + ext
- filename = src.with_name(name)
+ if source_path.is_file():
+ name = re.sub(r'\W+', '_', source_path.stem) + tomenumber + ext
+ else:
+ name = re.sub(r'\W+', '_', source_path.name) + tomenumber + ext
+ filename = source_path.with_name(name)
else:
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
if os.path.isfile(filename):
@@ -929,6 +1068,13 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
+ elif options.format == 'MOBI' and ext == '.epub':
+ counter = 0
+ basename = os.path.splitext(filename)[0]
+ if os.path.isfile(basename + '.mobi'):
+ while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
+ counter += 1
+ filename = basename + '_kcc' + str(counter) + ext
return filename
@@ -937,6 +1083,9 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = []
options.summary = ''
titleSuffix = ''
+ options.volume = ''
+ options.number = ''
+ options.series = ''
if options.title == 'defaulttitle':
defaultTitle = True
if os.path.isdir(originalpath):
@@ -965,8 +1114,10 @@ def getMetadata(path, originalpath):
options.title = xml.data['Series']
if xml.data['Volume']:
titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
+ options.volume = xml.data['Volume']
if xml.data['Number']:
titleSuffix += ' #' + xml.data['Number'].zfill(3)
+ options.number = xml.data['Number']
if options.metadatatitle == 1 and xml.data['Title']:
titleSuffix += ': ' + xml.data['Title']
options.title += titleSuffix
@@ -984,6 +1135,8 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = xml.data['Bookmarks']
if xml.data['Summary']:
options.summary = xml.data['Summary']
+ if xml.data['Series']:
+ options.series = xml.data['Series']
os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'):
@@ -1003,11 +1156,6 @@ def getDirectorySize(start_path='.'):
return total_size
-def getTopMargin(deviceres, size):
- y = int((deviceres[1] - size[1]) / 2) / deviceres[1] * 100
- return str(round(y, 1))
-
-
def getPanelViewResolution(imagesize, deviceres):
scale = float(deviceres[0]) / float(imagesize[0])
return int(deviceres[0]), int(scale * imagesize[1])
@@ -1026,7 +1174,7 @@ def removeNonImages(filetree):
for root, dirs, files in os.walk(filetree):
for name in files:
_, 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)):
os.remove(os.path.join(root, name))
# remove empty nested folders
@@ -1038,7 +1186,7 @@ def removeNonImages(filetree):
raise UserWarning('No images detected, nested archives are not supported.')
-def sanitizeTree(filetree):
+def sanitizeTree(filetree, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1048,7 +1196,7 @@ def sanitizeTree(filetree):
_, ext = getImageFileName(name)
# 9999 page limit
- unique_name = f'kcc-{page:04}'
+ unique_name = f'{prefix}-{page:04}'
page += 1
newKey = os.path.join(root, unique_name + ext)
@@ -1097,7 +1245,7 @@ def chunk_directory(path):
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files:
# Windows MAX_LEN = 260 plus some buffer
- if os.name == 'nt' and len(os.path.join(root, f)) > 180:
+ if os.name == 'nt' and len(os.path.join(root, f)) > 220:
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
break
@@ -1209,7 +1357,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
, 'warning', False)
GUI.addMessage.emit('', '', False)
- if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and options.profile != 'KS':
+ if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and not options.profile.startswith('KS'):
print("WARNING: More than 25% of images are smaller than target device resolution. "
"Consider enabling stretching or upscaling to improve readability.")
if GUI:
@@ -1236,17 +1384,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))
return value
-def makeZIP(zipfilename, basedir, isepub=False):
+def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools():
if isepub:
- mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
+ mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
mimetypeFile.close()
- subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
+ 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:
- zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
+ zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
@@ -1257,7 +1408,7 @@ def makeZIP(zipfilename, basedir, isepub=False):
zipOutput.write(path, aPath)
zipOutput.close()
end = perf_counter()
- print(f"makeZIP time: {end - start} seconds")
+ print(f"{job_progress}makeZIP time: {end - start} seconds")
return zipfilename
def makeParser():
@@ -1307,13 +1458,25 @@ def makeParser():
"2: Consider every subdirectory as separate volume [Default=0]")
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
help="Shift first page to opposite side in landscape for spread alignment")
+ output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
+ help="Show a single centered page in landscape")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
+ output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
+ help="Rotate double page spreads in opposite direction.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option")
+ processing_options.add_argument("--legacyextract", action="store_true", dest="legacyextract", default=False,
+ help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
+ 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,
help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
@@ -1324,6 +1487,12 @@ def makeParser():
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",
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",
@@ -1343,13 +1512,25 @@ def makeParser():
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
- help="Create PNG files instead JPEG")
+ 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,
help="Create JPEG files using mozJpeg")
+ processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
+ help="The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.")
processing_options.add_argument("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
help="Turn 1x4 strips to 2x2 strips")
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
help="Delete source file(s) or a directory. It's not recoverable.")
+ processing_options.add_argument("--tempdir", action="store_true", dest="tempdir", default=False,
+ help="Create temporary files directory on source file drive.")
custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
help="Replace screen width provided by device profile")
@@ -1368,6 +1549,20 @@ def checkOptions(options):
options.isKobo = False
options.bordersColor = None
options.keep_epub = False
+
+ if options.profile in image.ProfileData.ProfilesKindle.keys():
+ options.iskindle = True
+ else:
+ options.isKobo = True
+
+ if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
+ raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
+
+ if options.format == 'PDF-200MB':
+ options.targetsize = 195
+ options.format = 'PDF'
+ if options.batchsplit != 2:
+ options.batchsplit = 1
if options.format == 'EPUB-200MB':
options.targetsize = 195
options.format = 'EPUB'
@@ -1379,6 +1574,8 @@ def checkOptions(options):
options.format = 'MOBI'
if options.batchsplit != 2:
options.batchsplit = 1
+ if not options.targetsize and options.profile.startswith('Rmk'):
+ options.targetsize = 95
if options.format == 'MOBI+EPUB':
options.keep_epub = True
options.format = 'MOBI'
@@ -1392,10 +1589,7 @@ def checkOptions(options):
options.format = 'PDF'
else:
options.format = 'EPUB'
- if options.profile in image.ProfileData.ProfilesKindle.keys():
- options.iskindle = True
- else:
- options.isKobo = True
+
if options.white_borders:
options.bordersColor = 'white'
if options.black_borders:
@@ -1413,8 +1607,10 @@ def checkOptions(options):
if options.webtoon:
options.panelview = False
options.righttoleft = False
- options.upscale = True
+ options.upscale = False
options.hq = False
+ options.white_borders = True
+ options.bordersColor = 'white'
# Disable all Kindle features for other e-readers
if options.profile == 'OTHER':
options.panelview = False
@@ -1422,11 +1618,9 @@ def checkOptions(options):
if 'Ko' in options.profile:
options.panelview = False
options.hq = False
- # CBZ files on Kindle DX/DXG support higher resolution
- if options.profile == 'KDX' and options.format == 'CBZ':
- options.customheight = 1200
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
if options.format == 'KFX':
+ options.targetsize = 195
options.format = 'EPUB'
options.kfx = True
options.panelview = False
@@ -1443,6 +1637,27 @@ def checkOptions(options):
image.ProfileData.Profiles["Custom"] = newProfile
options.profile = "Custom"
options.profileData = image.ProfileData.Profiles[options.profile]
+ if not options.jpegquality:
+ if options.profile.startswith('KS') or options.profile == 'KCS':
+ options.jpegquality = 90
+ else:
+ options.jpegquality = 85
+
+ options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
+ options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
+
+ 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
@@ -1461,50 +1676,53 @@ def checkTools(source):
sys.exit(1)
-def checkPre(source):
+def checkPre(source='KCC-'):
# Make sure that all temporary files are gone
for root, dirs, _ in walkLevel(gettempdir(), 0):
for tempdir in dirs:
- if tempdir.startswith('KCC-'):
+ if tempdir.startswith(source):
rmtree(os.path.join(root, tempdir), True)
- # Make sure that target directory is writable
- if os.path.isdir(source):
- src = os.path.abspath(os.path.join(source, '..'))
- else:
- src = os.path.dirname(source)
- try:
- with TemporaryFile(prefix='KCC-', dir=src):
- pass
- except Exception:
- raise UserWarning("Target directory is not writable.")
-
def makeFusion(sources: List[str]):
if len(sources) < 2:
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter()
first_path = Path(sources[0])
+
+ if options.tempdir:
+ fusion_parent = first_path.parent
+ else:
+ # LLL is after KCC
+ checkPre('LLL-')
+ fusion_parent = Path(mkdtemp('', 'LLL-'))
+
if first_path.is_file():
- fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
+ fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
else:
- fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
+ fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
print("Running Fusion")
- for source in sources:
+ # Check if prefix is needed when user-specified ordering differs from OS natural sorting
+ path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
+ needs_prefix = os_sorted(path_names) != path_names
+
+ for index, source in enumerate(sources, start=1):
print(f"Processing {source}...")
- checkPre(source)
print("Checking images...")
- path = getWorkFolder(source)
- pathfinder = os.path.join(path, "OEBPS", "Images")
- sanitizeTree(pathfinder)
- # TODO: remove flattenTree when subchapters are supported
- flattenTree(pathfinder)
source_path = Path(source)
+ # Add the fusion_0001_ prefix to maintain user-specified order if needed
+ prefix = ''
+ if needs_prefix:
+ prefix = f'fusion_{index:04d}_'
if source_path.is_file():
- os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
+ targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
else:
- os.renames(pathfinder, fusion_path.joinpath(source_path.name))
-
+ targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
+
+ getWorkFolder(source, str(targetpath))
+ sanitizeTree(targetpath, prefix='fusion')
+ # TODO: remove flattenTree when subchapters are supported
+ flattenTree(targetpath)
end = perf_counter()
print(f"makefusion: {end - start} seconds")
@@ -1513,7 +1731,7 @@ def makeFusion(sources: List[str]):
return str(fusion_path)
-def makeBook(source, qtgui=None):
+def makeBook(source, qtgui=None, job_progress=''):
start = perf_counter()
global GUI
GUI = qtgui
@@ -1521,27 +1739,67 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
- options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
- checkPre(source)
- print("Preparing source images...")
+ checkPre()
+ if not options.filefusion:
+ checkPre('LLL-')
+ print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source)
- print("Checking images...")
+ print(f"{job_progress}Checking images...")
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
- cover = image.Cover(cover_path, options)
-
+ 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)
+
+ x, y = image.ProfileData.Profiles[options.profile][1]
if options.webtoon:
- y = image.ProfileData.Profiles[options.profile][1][1]
- comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
+ comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
+
+ options.kfx_resolution = None
+ if options.kfx:
+ original_resolutions = []
+ normalized_resolutions = []
+ for root, _, files in os.walk(os.path.join(path, "OEBPS", "Images")):
+ for file in files:
+ with Image.open(os.path.join(root, file)) as imagef:
+ original_resolutions.append(imagef.size)
+ size = get_contain_resolution(imagef, (x, y))
+ normalized_resolutions.append(size)
+
+ counter = Counter(normalized_resolutions)
+
+ aspect_ratios = []
+ filtered_resolutions = []
+ for w, h in normalized_resolutions:
+ aspect_ratio = h / w
+ # page-like aspect ratios, could be improved
+ if aspect_ratio > 1.3 and aspect_ratio < 1.7:
+ aspect_ratios.append(aspect_ratio)
+ filtered_resolutions.append((w, h))
+
+ most_common_res, most_common_count = counter.most_common(1)[0]
+ options.kfx_resolution = most_common_res
+ if most_common_count / sum(counter.values()) > .6:
+ pass
+ #elif max(aspect_ratios) - min(aspect_ratios) < .2:
+ else:
+ # get the widest resolution
+ options.kfx_resolution = max(filtered_resolutions)
+ # else:
+ # raise UserWarning('Aspect ratio of pages too different for KFX conversion')
+
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:
- print("Processing images...")
+ print(f"{job_progress}Processing images...")
if GUI:
- GUI.progressBarTick.emit('Processing images')
- imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
+ GUI.progressBarTick.emit(f'{job_progress}Processing images')
+ imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"), job_progress)
if GUI:
GUI.progressBarTick.emit('1')
if options.batchsplit > 0 or options.targetsize:
@@ -1552,11 +1810,11 @@ def makeBook(source, qtgui=None):
tomeNumber = 0
if GUI:
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('Creating PDF files')
+ GUI.progressBarTick.emit(f'{job_progress}Creating PDF files')
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('tick')
options.baseTitle = options.title
@@ -1570,29 +1828,33 @@ def makeBook(source, qtgui=None):
tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.format == 'CBZ':
- print("Creating CBZ file...")
+ print(f"{job_progress}Creating CBZ file...")
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
- makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
+ if cover and cover.smartcover:
+ cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
+ makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF':
- print("Creating PDF file with PyMuPDF...")
+ 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)
+ if cover and cover.smartcover:
+ cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
# use optimized buildPDF logic with streaming and compression
- output_pdf = buildPDF(tome, options.title, None, output_file)
+ output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
filepath.append(output_pdf)
else:
- print("Creating EPUB file...")
+ print(f"{job_progress}Creating EPUB file...")
if len(tomes) > 1:
- buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, len(tomes))
+ buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, job_progress, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
- buildEPUB(tome, chapterNames, tomeNumber, False, cover, source)
+ buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
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])
@@ -1605,27 +1867,27 @@ def makeBook(source, qtgui=None):
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI':
- print("Creating MOBI files...")
+ print(f"{job_progress}Creating MOBI files...")
work = []
for i in filepath:
work.append([i])
output = makeMOBI(work, GUI)
for errors in output:
if errors[0] != 0:
- print('Error: KindleGen failed to create MOBI!')
+ print(f"{job_progress}Error: KindleGen failed to create MOBI!")
print(errors)
return filepath
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
- print("Kindle detected. Uploading covers...")
+ print(f"{job_progress}Kindle detected. Uploading covers...")
for i in filepath:
output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
if not output[0]:
- print('Error: Failed to tweak KindleGen output!')
+ print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath
else:
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])
if options.delete:
if os.path.isfile(source):
@@ -1634,7 +1896,7 @@ def makeBook(source, qtgui=None):
rmtree(source, True)
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)
@@ -1719,4 +1981,3 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput
-
diff --git a/kindlecomicconverter/comic2panel.py b/kindlecomicconverter/comic2panel.py
index 04a8c8858..3cb2e9fa8 100644
--- a/kindlecomicconverter/comic2panel.py
+++ b/kindlecomicconverter/comic2panel.py
@@ -18,15 +18,18 @@
# PERFORMANCE OF THIS SOFTWARE.
#
+import math
import os
import sys
from argparse import ArgumentParser
from shutil import rmtree
from multiprocessing import Pool
-from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter
+from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
from PIL.Image import Dither
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
+ImageFile.LOAD_TRUNCATED_IMAGES = True
+
def mergeDirectoryTick(output):
if output:
@@ -59,18 +62,19 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
- if targetHeight > 131072 * 2:
- raise RuntimeError(f'Image too tall at {targetHeight} pixels.')
+ if targetHeight > 131072 * 4:
+ raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
result = Image.new('RGB', (targetWidth, targetHeight))
y = 0
for i in imagesValid:
- img = Image.open(i).convert('RGB')
- if img.size[0] < targetWidth or img.size[0] > targetWidth:
- widthPercent = (targetWidth / float(img.size[0]))
- heightSize = int((float(img.size[1]) * float(widthPercent)))
- img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
- result.paste(img, (0, y))
- y += img.size[1]
+ with Image.open(i) as img:
+ img = img.convert('RGB')
+ if img.size[0] < targetWidth or img.size[0] > targetWidth:
+ widthPercent = (targetWidth / float(img.size[0]))
+ heightSize = int((float(img.size[1]) * float(widthPercent)))
+ img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
+ result.paste(img, (0, y))
+ y += img.size[1]
os.remove(i)
savePath = os.path.split(imagesValid[0])
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
@@ -137,35 +141,51 @@ def splitImage(work):
panelDetected = False
panelY2 = yWork
# skip short panel at start
- if not panels and panelY2 - panelY1 < v_pad * 2:
+ if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
continue
panels.append((panelY1, panelY2, panelY2 - panelY1))
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
panelsProcessed = []
for panel in panels:
+ # 1.52 too high
if panel[2] <= opt.height * 1.5:
panelsProcessed.append(panel)
- elif panel[2] < opt.height * 2:
+ elif panel[2] <= opt.height * 2:
diff = panel[2] - opt.height
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
else:
- parts = round(panel[2] / opt.height)
+ # split super long panels with overlap
+ parts = math.ceil(panel[2] / opt.height)
diff = panel[2] // parts
- for x in range(0, parts):
- panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff))
+ panelsProcessed.append((panel[0], panel[0] + opt.height, opt.height))
+ 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:
for panel in panelsProcessed:
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.show()
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
# Create virtual pages
pages = []
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
panelNumber = 0
for panel in panelsProcessed:
@@ -202,7 +222,7 @@ def splitImage(work):
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
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
@@ -214,6 +234,8 @@ def main(argv=None, qtgui=None):
" with spaces.")
main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
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,
help="Overwrite source directory")
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
@@ -232,16 +254,14 @@ def main(argv=None, qtgui=None):
return 1
if args.height > 0:
for sourceDir in args.input:
- targetDir = sourceDir + "-Splitted"
+ targetDir = sourceDir
if os.path.isdir(sourceDir):
- rmtree(targetDir, True)
- os.renames(sourceDir, targetDir)
work = []
pagenumber = 1
splitWorkerOutput = []
splitWorkerPool = Pool(maxtasksperchild=10)
if args.merge:
- print("Merging images...")
+ print(f"{job_progress}Merging images...")
directoryNumer = 1
mergeWork = []
mergeWorkerOutput = []
@@ -253,7 +273,7 @@ def main(argv=None, qtgui=None):
directoryNumer += 1
mergeWork.append([os.path.join(root, directory)])
if GUI:
- GUI.progressBarTick.emit('Combining images')
+ GUI.progressBarTick.emit(f'{job_progress}Combining images')
GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
@@ -266,7 +286,7 @@ def main(argv=None, qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1])
- print("Splitting images...")
+ print(f"{job_progress}Splitting images...")
dot_clean(targetDir)
for root, _, files in os.walk(targetDir, False):
for name in files:
@@ -276,7 +296,7 @@ def main(argv=None, qtgui=None):
else:
os.remove(os.path.join(root, name))
if GUI:
- GUI.progressBarTick.emit('Splitting images')
+ GUI.progressBarTick.emit(f'{job_progress}Splitting images')
GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick')
if len(work) > 0:
@@ -292,8 +312,6 @@ def main(argv=None, qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
splitWorkerOutput[0][1])
- if args.inPlace:
- os.renames(targetDir, sourceDir)
else:
rmtree(targetDir, True)
raise UserWarning("C2P: Source directory is empty.")
diff --git a/kindlecomicconverter/comicarchive.py b/kindlecomicconverter/comicarchive.py
index ba488a892..0acfc7697 100644
--- a/kindlecomicconverter/comicarchive.py
+++ b/kindlecomicconverter/comicarchive.py
@@ -20,15 +20,17 @@
from functools import cached_property, lru_cache
import os
+from pathlib import Path
import platform
import distro
from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError
-from .shared import subprocess_run
+from .shared import IMAGE_TYPES, subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
+TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
class ComicArchive:
@@ -65,11 +67,14 @@ def type(self):
def extract(self, targetdir):
if not os.path.isdir(targetdir):
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 = []
extraction_commands = [
- ['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
+ [TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
]
@@ -121,7 +126,7 @@ def extractMetadata(self):
def available_archive_tools():
available = []
- for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
+ for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
available.append(tool)
diff --git a/kindlecomicconverter/image.py b/kindlecomicconverter/image.py
index a8664a086..8b5486ed2 100755
--- a/kindlecomicconverter/image.py
+++ b/kindlecomicconverter/image.py
@@ -24,13 +24,15 @@
from pathlib import Path
from functools import cached_property
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 .inter_panel_crop_alg import crop_empty_inter_panel
+from .shared import get_contain_resolution
AUTO_CROP_THRESHOLD = 0.015
+ImageFile.LOAD_TRUNCATED_IMAGES = True
class ProfileData:
@@ -85,22 +87,30 @@ def __init__(self):
]
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 = {
- 'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
- 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
- 'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
- 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
- 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
- 'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
+ 'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
+ 'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
+ 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
+ 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
+ 'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
+ 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
+ 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
+ 'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
+ 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
+ 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
+ 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
+ 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
+ 'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
+ 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
+ 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
+ 'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
+ 'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
+ 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
+ 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
+ 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
+ 'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
}
ProfilesKindle = {
@@ -109,35 +119,35 @@ def __init__(self):
}
ProfilesKobo = {
- 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
- 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
- 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
- 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
- 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
- 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
- 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
- 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
- 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
- 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
- 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
- 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
- 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
- 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
- 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
+ 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
+ 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
+ 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
+ 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
+ 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
+ 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
+ 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
+ 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
+ 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
+ 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
+ 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
+ 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
+ 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
+ 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
+ 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
}
ProfilesRemarkable = {
- 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
- 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
- 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
- 'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.8),
+ '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),
}
Profiles = {
**ProfilesKindle,
**ProfilesKobo,
**ProfilesRemarkable,
- 'OTHER': ("Other", (0, 0), Palette16, 1.8),
+ 'OTHER': ("Other", (0, 0), Palette16, 1.0),
}
@@ -151,7 +161,7 @@ def __init__(self, source, options):
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
- Image.open(srcImgPath).verify()
+ # Image.open(srcImgPath).verify()
with Image.open(srcImgPath) as im:
self.image = im.copy()
@@ -186,14 +196,21 @@ def splitCheck(self):
new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.fill])
- elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
- and not self.opt.webtoon and self.opt.splitter == 1:
+ elif self.opt.webtoon:
+ self.payload.append(['N', self.source, self.image, self.fill])
+ # rotate only TODO dead code?
+ elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
- spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
+ if not self.opt.rotateright:
+ spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
+ else:
+ spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
- elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
- if self.opt.splitter != 1:
+ # elif wide enough to split
+ elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
+ # if (split) or (split and rotate)
+ if self.opt.splitter != 1 and width / height < 1.75:
if width > height:
leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height)
@@ -208,10 +225,15 @@ def splitCheck(self):
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
- if self.opt.splitter > 0:
+
+ # if (rotate) or (split and rotate)
+ if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
spread = self.image
if not self.opt.norotate:
- spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
+ if not self.opt.rotateright:
+ spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
+ else:
+ spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.fill])
@@ -261,9 +283,11 @@ def __init__(self, options, mode, path, image, fill):
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
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
+ # TODO: color check earlier
self.image = image.convert("RGB")
+ self.color = self.colorCheck()
+ self.colorOutput = self.color and self.opt.forcecolor
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
@@ -282,19 +306,22 @@ def __init__(self, options, mode, path, image, fill):
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
- @cached_property
- def color(self):
+ def colorCheck(self):
if self.original_color_mode in ("L", "1"):
return False
- img = self.image.convert("YCbCr")
- _, cb, cr = img.split()
-
- cb_hist = cb.histogram()
- cr_hist = cr.histogram()
+ if self.opt.webtoon:
+ 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:
- # cut off pixels from both ends of the histogram
- cutoff = (.1, .1)
# get number of pixels
n = sum(h)
# remove cutoff% pixels from the low end
@@ -319,28 +346,52 @@ def color(self):
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_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
- # bias adjustment
- SPREAD_THRESHOLD = 5
- if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
- return False
+ # bias adjustment, don't go lower than 7
+ SPREAD_THRESHOLD = 7
+ if self.opt.forcecolor:
+ if any([
+ cb_nonzero[0] > 128,
+ cr_nonzero[0] > 128,
+ cb_nonzero[-1] < 128,
+ cr_nonzero[-1] < 128,
+ ]):
+ return True, True
+ elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
+ return True, False
+
+ DIFF_THRESHOLD = diff_threshold
+ if any([
+ cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
+ cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
+ cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
+ cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
+ ]):
+ return True, True
- DIFF_THRESHOLD = 10
- if cb_nonzero[0] < 128 - DIFF_THRESHOLD:
- return True
- elif cb_nonzero[-1] > 128 + DIFF_THRESHOLD:
- return True
- elif cr_nonzero[0] < 128 - DIFF_THRESHOLD:
- return True
- elif cr_nonzero[-1] > 128 + DIFF_THRESHOLD:
- return True
- else:
- return False
+ 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
def saveToDir(self):
try:
@@ -364,25 +415,32 @@ def saveToDir(self):
raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath):
- if self.opt.forcepng:
- image.info["transparency"] = None
- if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
+ if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
+ image.info.pop('transparency', None)
+ if self.opt.webp_output:
+ targetPath += '.webp'
+ image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
+ elif self.opt.kindle_azw3:
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else:
- targetPath += '.jpg'
- if self.opt.mozjpeg:
+ if self.opt.webp_output:
+ targetPath += '.webp'
+ image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
+ elif self.opt.mozjpeg:
+ targetPath += '.jpg'
with io.BytesIO() as output:
- image.save(output, format="JPEG", optimize=1, quality=85)
+ image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
- image.save(targetPath, 'JPEG', optimize=1, quality=85)
+ targetPath += '.jpg'
+ image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath
def gammaCorrectImage(self):
@@ -397,20 +455,41 @@ def gammaCorrectImage(self):
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
def autocontrastImage(self):
- if self.opt.autolevel and not self.color:
- self.convertToGrayscale()
- h = self.image.histogram()
- most_common_dark_pixel_count = max(h[:64])
- black_point = h.index(most_common_dark_pixel_count)
- bp = black_point
- self.image = self.image.point(lambda p: p if p > bp else bp)
-
- # don't autocontrast grayscale pages that were originally color
- if not self.opt.forcecolor and self.color:
+ if self.opt.webtoon:
return
+ 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):
self.image = self.image.convert('L')
@@ -429,23 +508,43 @@ def optimizeForDisplay(self, eraserainbow, is_color):
self.image = erase_rainbow_artifacts(self.image, is_color)
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_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()
- if self.opt.stretch:
+ if self.opt.kfx:
+ ratio_kfx = self.opt.kfx_resolution[1] / self.opt.kfx_resolution[0]
+ contain_size = get_contain_resolution(self.image, self.size)
+ if abs(ratio_image - ratio_kfx) < AUTO_CROP_THRESHOLD:
+ if contain_size[0] > self.opt.kfx_resolution[0] or contain_size[1] > self.opt.kfx_resolution[1]:
+ self.image = ImageOps.fit(self.image, self.opt.kfx_resolution, method=method)
+ else:
+ self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
+ else:
+ self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
+ elif self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
else: # if image bigger than device resolution or smaller with upscaling
- if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
+ if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
+ self.image = ImageOps.fit(self.image, self.size, method=method)
+ elif 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:
+ elif (self.opt.format in ('CBZ', 'PDF')) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else:
self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self):
- if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
+ if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
return Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
@@ -489,6 +588,7 @@ def __init__(self, source, opt):
self.options = opt
self.source = source
self.image = Image.open(source)
+ self.smartcover = False
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@@ -496,33 +596,59 @@ def __init__(self, source, opt):
def process(self):
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:
self.image = self.image.convert('L')
- self.crop_main_cover()
+ if self.options.smartcovercrop:
+ self.crop_main_cover()
size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3:
+ size[0] = min(size[0], 1920)
size[1] = min(size[1], 1920)
- self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
+ if self.options.coverfill and not self.options.kindle_scribe_azw3:
+ # TODO: Kindle Scribe case
+ self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
+ else:
+ self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
def crop_main_cover(self):
w, h = self.image.size
if w / h > 2:
+ self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
+ elif w / h > 1.83:
+ self.smartcover = True
+ if self.options.righttoleft:
+ self.image = self.image.crop((w * .19, 0, w * .575, h))
+ else:
+ self.image = self.image.crop((w * .425, 0, .81 * w, h))
+ elif w / h > 1.7:
+ self.smartcover = True
+ if self.options.righttoleft:
+ self.image = self.image.crop((w * .2, 0, w * .583, h))
+ else:
+ self.image = self.image.crop((w * .417, 0, .8 * w, h))
elif w / h > 1.34:
+ self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
+ elif w / h > 1.0:
+ self.smartcover = True
+ if self.options.righttoleft:
+ self.image = self.image.crop((w * .36, 0, w, h))
+ else:
+ self.image = self.image.crop((w, 0, .64 * w, h))
- def save_to_epub(self, target, tomeid, len_tomes=0):
+ def save_to_folder(self, target, tomeid, len_tomes=0):
try:
if tomeid == 0:
- self.image.save(target, "JPEG", optimize=1, quality=85)
+ self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
else:
copy = self.image.copy()
draw = ImageDraw.Draw(copy)
@@ -536,7 +662,7 @@ def save_to_epub(self, target, tomeid, len_tomes=0):
stroke_fill=0,
stroke_width=25
)
- copy.save(target, "JPEG", optimize=1, quality=85)
+ copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
except IOError:
raise RuntimeError('Failed to save cover.')
@@ -544,6 +670,6 @@ def saveToKindle(self, kindle, asin):
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
try:
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
- 'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
+ 'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=self.options.jpegquality)
except IOError:
raise RuntimeError('Failed to upload cover.')
diff --git a/kindlecomicconverter/inter_panel_crop_alg.py b/kindlecomicconverter/inter_panel_crop_alg.py
index cbfd7a8f1..640e69a5b 100644
--- a/kindlecomicconverter/inter_panel_crop_alg.py
+++ b/kindlecomicconverter/inter_panel_crop_alg.py
@@ -1,8 +1,10 @@
-from PIL import Image, ImageFilter, ImageOps
+from PIL import Image, ImageFilter, ImageOps, ImageFile
import numpy as np
from typing import Literal
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).
diff --git a/kindlecomicconverter/page_number_crop_alg.py b/kindlecomicconverter/page_number_crop_alg.py
index 695b03b65..78f182c9e 100644
--- a/kindlecomicconverter/page_number_crop_alg.py
+++ b/kindlecomicconverter/page_number_crop_alg.py
@@ -1,7 +1,9 @@
-from PIL import ImageOps, ImageFilter
+from PIL import ImageOps, ImageFilter, ImageFile
import numpy as np
from .common_crop import threshold_from_power, group_close_values
+ImageFile.LOAD_TRUNCATED_IMAGES = True
+
'''
Some assupmptions on the page number sizes
@@ -158,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
+ if not edge.height or not edge.width:
+ continue
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)
diff --git a/kindlecomicconverter/pdfjpgextract.py b/kindlecomicconverter/pdfjpgextract.py
new file mode 100644
index 000000000..751a68e24
--- /dev/null
+++ b/kindlecomicconverter/pdfjpgextract.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012-2014 Ciro Mattia Gonano
+# Copyright (c) 2013-2019 Pawel Jastrzebski
+#
+# Based upon the code snippet by Ned Batchelder
+# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
+#
+# Permission to use, copy, modify, and/or distribute this software for
+# any purpose with or without fee is hereby granted, provided that the
+# above copyright notice and this permission notice appear in all
+# copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
+# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
+# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
+# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+#
+
+import os
+
+# skip stray images a few pixels in size in some PDFs
+# typical images are many thousands in length
+# https://github.com/ciromattia/kcc/pull/546
+STRAY_IMAGE_LENGTH_THRESHOLD = 300
+
+
+class PdfJpgExtract:
+ def __init__(self, fname, fullPath):
+ self.fname = fname
+ self.path = fullPath
+
+ def getPath(self):
+ return self.path
+
+ def extract(self):
+ pdf = open(self.fname, "rb").read()
+ startmark = b"\xff\xd8"
+ startfix = 0
+ endmark = b"\xff\xd9"
+ endfix = 2
+ i = 0
+ njpg = 0
+ while True:
+ istream = pdf.find(b"stream", i)
+ if istream < 0:
+ break
+ istart = pdf.find(startmark, istream, istream + 20)
+ if istart < 0:
+ i = istream + 20
+ continue
+ iend = pdf.find(b"endstream", istart)
+ if iend < 0:
+ raise Exception("Didn't find end of stream!")
+ iend = pdf.find(endmark, iend - 20)
+ if iend < 0:
+ raise Exception("Didn't find end of JPG!")
+ istart += startfix
+ iend += endfix
+ i = iend
+
+ if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
+ continue
+
+ jpg = pdf[istart:iend]
+ jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
+ jpgfile.write(jpg)
+ jpgfile.close()
+ njpg += 1
+
+ return njpg
diff --git a/kindlecomicconverter/rainbow_artifacts_eraser.py b/kindlecomicconverter/rainbow_artifacts_eraser.py
index 9825f640e..fcea70a17 100644
--- a/kindlecomicconverter/rainbow_artifacts_eraser.py
+++ b/kindlecomicconverter/rainbow_artifacts_eraser.py
@@ -1,5 +1,8 @@
import numpy as np
-from PIL import Image
+from PIL import Image, ImageFile
+
+ImageFile.LOAD_TRUNCATED_IMAGES = True
+
def fourier_transform_image(img):
"""
diff --git a/kindlecomicconverter/shared.py b/kindlecomicconverter/shared.py
index c6857d21c..12143170b 100644
--- a/kindlecomicconverter/shared.py
+++ b/kindlecomicconverter/shared.py
@@ -27,6 +27,9 @@
from traceback import format_tb
+IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
+
+
class HTMLStripper(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
@@ -58,6 +61,23 @@ def getImageFileName(imgfile):
ext = ext.lower()
return [name, ext]
+def get_contain_resolution(image, size):
+ '''same code as Pillow ImageOps.contain()'''
+ im_ratio = image.width / image.height
+ dest_ratio = size[0] / size[1]
+
+ if im_ratio != dest_ratio:
+ if im_ratio > dest_ratio:
+ new_height = round(image.height / image.width * size[0])
+ if new_height != size[1]:
+ size = (size[0], new_height)
+ else:
+ new_width = round(image.width / image.height * size[1])
+ if new_width != size[0]:
+ size = (new_width, size[1])
+
+ return size
+
def walkSort(dirnames, filenames):
convert = lambda text: int(text) if text.isdigit() else text
@@ -102,10 +122,6 @@ def dependencyCheck(level):
missing.append('PySide 6.0.0')
except ImportError:
missing.append('PySide 6.0.0+')
- try:
- import raven
- except ImportError:
- missing.append('raven 6.0.0+')
if level > 1:
try:
from psutil import __version__ as psutilVersion
diff --git a/requirements-docker.txt b/requirements-docker.txt
new file mode 100644
index 000000000..193bb24f9
--- /dev/null
+++ b/requirements-docker.txt
@@ -0,0 +1,11 @@
+Pillow>=11.3.0
+psutil>=5.9.5
+requests>=2.34.2
+python-slugify>=8.0.4
+packaging>=26.2
+mozjpeg-lossless-optimization>=1.2.0
+natsort>=8.4.0
+distro>=1.8.0
+# Below requirements are compiled in Dockefile
+# numpy==2.3.4
+# PyMuPDF==1.26.6
\ No newline at end of file
diff --git a/requirements-osx-legacy.txt b/requirements-osx-legacy.txt
index 018a60053..90eb63757 100644
--- a/requirements-osx-legacy.txt
+++ b/requirements-osx-legacy.txt
@@ -1,10 +1,9 @@
-PySide6==6.5.2
+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
+requests>=2.34.2
+python-slugify>=8.0.4
+packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
diff --git a/requirements-win7.txt b/requirements-win7.txt
index 47717d415..637f428bc 100644
--- a/requirements-win7.txt
+++ b/requirements-win7.txt
@@ -1,10 +1,9 @@
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
+requests>=2.34.2
+python-slugify>=8.0.4
+packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
diff --git a/requirements.txt b/requirements.txt
index 89abfdc4e..ca8358bde 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,9 @@
PySide6<6.10
Pillow>=11.3.0
psutil>=5.9.5
-requests>=2.31.0
-python-slugify>=1.2.1
-raven>=6.0.0
-packaging>=23.2
+requests>=2.34.2
+python-slugify>=8.0.4,<9.0.0
+packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
diff --git a/setup.py b/setup.py
index 0ab5f9d71..daf2276f6 100644
--- a/setup.py
+++ b/setup.py
@@ -8,6 +8,8 @@
Create EXE/APP:
python3 setup.py build_binary
+ python3 setup.py build_c2e
+ python3 setup.py build_c2p
"""
import os
@@ -38,8 +40,8 @@ def run(self):
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
- min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
- if min_os:
+ min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
+ if min_os.startswith('10.1'):
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
else:
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
@@ -57,10 +59,75 @@ def run(self):
else:
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(
cmdclass={
'build_binary': BuildBinaryCommand,
+ 'build_c2e': BuildC2ECommand,
+ 'build_c2p': BuildC2PCommand,
},
name=NAME,
version=VERSION,
@@ -81,17 +148,16 @@ def run(self):
},
packages=['kindlecomicconverter'],
install_requires=[
- 'pyside6>=6.0.0',
+ 'PySide6>=6.0.0',
'Pillow>=9.3.0',
- 'PyMuPDF>=1.18.0',
'psutil>=5.9.5',
- 'python-slugify>=1.2.1,<9.0.0',
- 'raven>=6.0.0',
'requests>=2.31.0',
- 'mozjpeg-lossless-optimization>=1.1.2',
+ 'python-slugify>=1.2.1,<9.0.0',
+ 'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0',
- 'distro',
+ 'distro>=1.8.0',
'numpy>=1.22.4',
+ 'packaging>=23.2',
'PyMuPDF>=1.16.1',
],
classifiers=[],