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 @@ [![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](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, &quot;back up&quot; 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, &quot;back up&quot; 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/>&lt; 1.0 makes the image brighter.<br/>&gt; 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 &quot;Chunk size MB&quot; 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 &quot;Chunk size MB&quot; 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=[],