├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── docker-base-publish.yml │ ├── docker-publish.yml │ ├── package-linux.yml │ ├── package-macos.yml │ ├── package-windows-with-docker.yml │ └── package-windows.yml ├── .gitignore ├── AppImageBuilder.yml ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile-base ├── LICENSE.txt ├── README.md ├── application-vnd.appimage.svg ├── environment.yml ├── gen_ui_files.bat ├── gen_ui_files.sh ├── gui ├── KCC.qrc ├── KCC.ui └── MetaEditor.ui ├── header.jpg ├── icons ├── CBZ.png ├── EPUB.png ├── KFX.png ├── Kindle.png ├── Kobo.png ├── MOBI.png ├── Other.png ├── Rmk.png ├── Wizard-Small.bmp ├── Wizard.bmp ├── WizardOSX.png ├── clear.png ├── comic2ebook.icns ├── comic2ebook.ico ├── comic2ebook.png ├── convert.png ├── document_new.png ├── editor.png ├── error.png ├── folder_new.png ├── info.png ├── list_background.png ├── list_background.xcf ├── warning.png └── wiki.png ├── kcc-c2e.py ├── kcc-c2e.spec ├── kcc-c2p.py ├── kcc-c2p.spec ├── kcc.json ├── kcc.py ├── kcc.spec ├── kindlecomicconverter ├── KCC_gui.py ├── KCC_rc.py ├── KCC_ui.py ├── KCC_ui_editor.py ├── __init__.py ├── comic2ebook.py ├── comic2panel.py ├── comicarchive.py ├── common_crop.py ├── dualmetafix.py ├── image.py ├── inter_panel_crop_alg.py ├── kindle.py ├── metadata.py ├── page_number_crop_alg.py ├── pdfjpgextract.py ├── shared.py └── startup.py ├── requirements.txt └── setup.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | build 4 | dist 5 | KindleComicConverter.egg-info 6 | .dockerignore 7 | .gitignore 8 | .travis.yml 9 | Dockerfile 10 | venv 11 | *.md 12 | LICENSE.txt 13 | MANIFEST.in 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | Add a screenshot of your KCC settings. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. macOS, Linux, Windows 11] 28 | - Device [e.g. Kindle Paperwhite 3rd gen, Kobo Libra 2] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | # Enable version updates for Docker 14 | - package-ecosystem: "docker" 15 | # Look for a `Dockerfile` in the `root` directory 16 | directory: "/" 17 | # Check for updates once a week 18 | schedule: 19 | interval: "weekly" 20 | 21 | # Maintain dependencies for GitHub Actions 22 | - package-ecosystem: "github-actions" 23 | directory: "/" 24 | schedule: 25 | interval: "weekly" 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "beta_release" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "beta_release" ] 20 | schedule: 21 | - cron: '42 22 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/docker-base-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker base 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: [ 'docker-base-*' ] 7 | 8 | # Don't trigger if it's just a documentation update 9 | paths-ignore: 10 | - '**.md' 11 | - '**.MD' 12 | - '**.yml' 13 | - 'docs/**' 14 | - 'LICENSE' 15 | - '.gitattributes' 16 | - '.gitignore' 17 | - '.dockerignore' 18 | 19 | 20 | jobs: 21 | build_and_push: 22 | uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main 23 | with: 24 | docker_build_file: ./Dockerfile-base 25 | platform_linux_arm32v7_enabled: true 26 | platform_linux_arm64v8_enabled: true 27 | platform_linux_amd64_enabled: true 28 | push_enabled: true 29 | build_nohealthcheck: false 30 | ghcr_repo_owner: ${{ github.repository_owner }} 31 | ghcr_repo: ${{ github.repository }} 32 | build_latest: false 33 | secrets: 34 | ghcr_token: ${{ secrets.GITHUB_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | # Publish semver tags as releases. 7 | tags: [ 'v*.*.*' ] 8 | 9 | # Don't trigger if it's just a documentation update 10 | paths-ignore: 11 | - '**.md' 12 | - '**.MD' 13 | - '**.yml' 14 | - 'docs/**' 15 | - 'LICENSE' 16 | - '.gitattributes' 17 | - '.gitignore' 18 | - '.dockerignore' 19 | 20 | 21 | jobs: 22 | build_and_push: 23 | uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main 24 | with: 25 | platform_linux_arm32v7_enabled: true 26 | platform_linux_arm64v8_enabled: true 27 | platform_linux_amd64_enabled: true 28 | push_enabled: true 29 | build_nohealthcheck: false 30 | ghcr_repo_owner: ${{ github.repository_owner }} 31 | ghcr_repo: ${{ github.repository }} 32 | secrets: 33 | ghcr_token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/package-linux.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: build KCC for Linux 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | tags: 9 | - "v*.*.*" 10 | 11 | # Don't trigger if it's just a documentation update 12 | paths-ignore: 13 | - '**.md' 14 | - '**.MD' 15 | - '**.yml' 16 | - '**.sh' 17 | - 'docs/**' 18 | - 'Dockerfile' 19 | - 'LICENSE' 20 | - '.gitattributes' 21 | - '.gitignore' 22 | - '.dockerignore' 23 | 24 | jobs: 25 | build: 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.11 33 | cache: 'pip' 34 | - name: Install python dependencies 35 | run: | 36 | sudo apt-get update 37 | sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0 38 | python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller 39 | python -m pip install -r requirements.txt 40 | - name: build binary 41 | run: | 42 | python setup.py build_binary 43 | chmod +x dist/kcc_linux* 44 | # issue with this action, disabled and commented out 45 | # see https://github.com/AppImageCrafters/build-appimage/issues/5 46 | # see https://appimage-builder.readthedocs.io/en/latest/intro/install.html#install-appimagetool 47 | # - name: Build AppImage 48 | # uses: AppImageCrafters/build-appimage-action@master 49 | # env: 50 | # UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync 51 | # with: 52 | # recipe: AppImageBuilder.yml 53 | - name: Build AppImage 54 | run: | 55 | wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage 56 | chmod +x appimage-builder-x86_64.AppImage 57 | sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder 58 | appimage-builder --recipe AppImageBuilder.yml --skip-test 59 | env: 60 | UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync 61 | - name: upload artifact 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: AppImage 65 | path: './*.AppImage*' 66 | - name: Release 67 | uses: softprops/action-gh-release@v2 68 | if: startsWith(github.ref, 'refs/tags/') 69 | with: 70 | prerelease: true 71 | generate_release_notes: true 72 | files: | 73 | LICENSE.txt 74 | *.AppImage* 75 | -------------------------------------------------------------------------------- /.github/workflows/package-macos.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: build KCC for mac os 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | tags: 9 | - "v*.*.*" 10 | 11 | # Don't trigger if it's just a documentation update 12 | paths-ignore: 13 | - '**.md' 14 | - '**.MD' 15 | - '**.yml' 16 | - '**.sh' 17 | - 'docs/**' 18 | - 'Dockerfile' 19 | - 'LICENSE' 20 | - '.gitattributes' 21 | - '.gitignore' 22 | - '.dockerignore' 23 | 24 | jobs: 25 | build: 26 | strategy: 27 | matrix: 28 | os: [ macos-13, macos-14 ] 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: 3.11 36 | cache: 'pip' 37 | - name: Install python dependencies 38 | run: | 39 | python -m pip install --upgrade pip setuptools wheel pyinstaller certifi 40 | pip install -r requirements.txt 41 | - name: Install the Apple certificate and provisioning profile 42 | # TODO signing 43 | # https://federicoterzi.com/blog/automatic-code-signing-and-notarization-for-macos-apps-using-github-actions/ 44 | if: ${{ false }} 45 | env: 46 | BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} 47 | P12_PASSWORD: ${{ secrets.P12_PASSWORD }} 48 | BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }} 49 | KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} 50 | run: | 51 | # create variables 52 | CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 53 | PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision 54 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 55 | 56 | # import certificate and provisioning profile from secrets 57 | echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH 58 | echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH 59 | 60 | # create temporary keychain 61 | security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 62 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 63 | security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 64 | 65 | # import certificate to keychain 66 | security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH 67 | security list-keychain -d user -s $KEYCHAIN_PATH 68 | 69 | # apply provisioning profile 70 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 71 | cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles 72 | - uses: actions/setup-node@v4 73 | with: 74 | node-version: 16 75 | - run: npm install -g appdmg 76 | - name: build binary 77 | # TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v 78 | run: | 79 | python setup.py build_binary 80 | - name: upload build 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: mac-os-build-${{ runner.arch }} 84 | path: dist/*.dmg 85 | - name: Release 86 | uses: softprops/action-gh-release@v2 87 | if: startsWith(github.ref, 'refs/tags/') 88 | with: 89 | prerelease: true 90 | generate_release_notes: true 91 | files: | 92 | LICENSE.txt 93 | dist/*.dmg 94 | - name: Clean up keychain and provisioning profile 95 | # TODO signing 96 | if: ${{ false }} 97 | # if: ${{ always() }} 98 | run: | 99 | security delete-keychain $RUNNER_TEMP/app-signing.keychain-db 100 | rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision 101 | -------------------------------------------------------------------------------- /.github/workflows/package-windows-with-docker.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: build KCC for windows with docker 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | tags: 9 | - "v*.*.*" 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | entry: [ kcc-c2e, kcc-c2p ] 16 | include: 17 | - entry: kcc-c2e 18 | capital: KCC_c2e 19 | - entry: kcc-c2p 20 | capital: KCC_c2p 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Package Application 26 | uses: JackMcKew/pyinstaller-action-windows@main 27 | with: 28 | path: . 29 | spec: ./${{ matrix.entry }}.spec 30 | - name: rename binaries 31 | run: | 32 | version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g") 33 | mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe 34 | 35 | - name: upload-unsigned-artifact 36 | id: upload-unsigned-artifact 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: windows-build-${{ matrix.entry }} 40 | path: dist/windows/*.exe 41 | 42 | - id: optional_step_id 43 | uses: signpath/github-action-submit-signing-request@v1.2 44 | if: ${{ github.repository == 'ciromattia/kcc' }} 45 | with: 46 | api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' 47 | organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6' 48 | project-slug: 'kcc' 49 | signing-policy-slug: 'release-signing' 50 | github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}' 51 | wait-for-completion: true 52 | output-artifact-directory: 'dist/windows/' 53 | 54 | - name: Release 55 | uses: softprops/action-gh-release@v2 56 | if: startsWith(github.ref, 'refs/tags/') 57 | with: 58 | prerelease: true 59 | generate_release_notes: true 60 | files: | 61 | LICENSE.txt 62 | dist/windows/*.exe 63 | -------------------------------------------------------------------------------- /.github/workflows/package-windows.yml: -------------------------------------------------------------------------------- 1 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 2 | 3 | name: build KCC for windows 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | tags: 9 | - "v*.*.*" 10 | 11 | # Don't trigger if it's just a documentation update 12 | paths-ignore: 13 | - '**.md' 14 | - '**.MD' 15 | - '**.yml' 16 | - '**.sh' 17 | - 'docs/**' 18 | - 'Dockerfile' 19 | - 'LICENSE' 20 | - '.gitattributes' 21 | - '.gitignore' 22 | - '.dockerignore' 23 | 24 | jobs: 25 | build: 26 | runs-on: windows-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.11 33 | cache: 'pip' 34 | - name: Install dependencies 35 | env: 36 | PYINSTALLER_COMPILE_BOOTLOADER: 1 37 | run: | 38 | python -m pip install --upgrade pip setuptools wheel 39 | pip install -r requirements.txt 40 | pip install certifi pyinstaller --no-binary pyinstaller 41 | - name: build binary 42 | run: | 43 | python setup.py build_binary 44 | - name: upload-unsigned-artifact 45 | id: upload-unsigned-artifact 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: windows-build 49 | path: dist/*.exe 50 | - id: optional_step_id 51 | uses: signpath/github-action-submit-signing-request@v1.2 52 | if: ${{ github.repository == 'ciromattia/kcc' }} 53 | with: 54 | api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' 55 | organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6' 56 | project-slug: 'kcc' 57 | signing-policy-slug: 'release-signing' 58 | github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}' 59 | wait-for-completion: true 60 | output-artifact-directory: 'dist/' 61 | - name: Release 62 | uses: softprops/action-gh-release@v2 63 | if: startsWith(github.ref, 'refs/tags/') 64 | with: 65 | prerelease: true 66 | generate_release_notes: true 67 | files: | 68 | LICENSE.txt 69 | dist/*.exe 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | Pipfile 3 | Pipfile.lock 4 | setup.bat 5 | kindlecomicconverter/sentry.py 6 | other/windows/kindlegen.exe 7 | dist/ 8 | build/ 9 | KindleComicConverter*.egg-info/ 10 | .idea/ 11 | /venv/ 12 | /kindlegen* 13 | /kcc.bat 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /AppImageBuilder.yml: -------------------------------------------------------------------------------- 1 | # appimage-builder recipe see https://appimage-builder.readthedocs.io for details 2 | version: 1 3 | script: 4 | - rm -rf AppDir || true 5 | - mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/ 6 | - cp -a dist/kcc_linux* AppDir/ && mv AppDir/kcc_linux* AppDir/kcc_linux 7 | - cp icons/comic2ebook.png AppDir/usr/share/icons/hicolor/64x64/apps/ 8 | AppDir: 9 | path: AppDir 10 | app_info: 11 | id: com.github.ciromattia.kcc 12 | name: kindleComicConverter 13 | icon: comic2ebook 14 | version: latest 15 | exec: ./kcc_linux 16 | exec_args: $@ 17 | apt: 18 | arch: 19 | - amd64 20 | allow_unauthenticated: true 21 | sources: 22 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy main restricted 23 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates main restricted 24 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy universe 25 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates universe 26 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy multiverse 27 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy-updates multiverse 28 | - sourceline: deb http://archive.ubuntu.com/ubuntu jammy-backports main restricted 29 | universe multiverse 30 | - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security main restricted 31 | - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security universe 32 | - sourceline: deb http://security.ubuntu.com/ubuntu jammy-security multiverse 33 | include: 34 | - libc6:amd64 35 | files: 36 | include: [] 37 | exclude: 38 | - usr/share/man 39 | - usr/share/doc/*/README.* 40 | - usr/share/doc/*/changelog.* 41 | - usr/share/doc/*/NEWS.* 42 | - usr/share/doc/*/TODO.* 43 | test: 44 | fedora-30: 45 | image: appimagecrafters/tests-env:fedora-30 46 | command: ./AppRun 47 | use_host_x: true 48 | debian-stable: 49 | image: appimagecrafters/tests-env:debian-stable 50 | command: ./AppRun 51 | use_host_x: true 52 | archlinux-latest: 53 | image: appimagecrafters/tests-env:archlinux-latest 54 | command: ./AppRun 55 | use_host_x: true 56 | centos-7: 57 | image: appimagecrafters/tests-env:centos-7 58 | command: ./AppRun 59 | use_host_x: true 60 | ubuntu-xenial: 61 | image: appimagecrafters/tests-env:ubuntu-xenial 62 | command: ./AppRun 63 | use_host_x: true 64 | AppImage: 65 | arch: x86_64 66 | update-information: !ENV ${UPDATE_INFO} 67 | sign-key: None -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | #### 5.6.2: 5 | * build pipeline : drop pypi by @darodi in [#465](https://github.com/ciromattia/kcc/pull/465) 6 | * supporting Kindle Previewer by @darodi in [#466](https://github.com/ciromattia/kcc/pull/466) 7 | * Bump actions/upload-artifact from 2 to 3 by @dependabot in [#468](https://github.com/ciromattia/kcc/pull/468) 8 | * new appImage by @darodi in [#483](https://github.com/ciromattia/kcc/pull/483) 9 | * Bump actions/checkout from 2 to 3 by @dependabot in [#484](https://github.com/ciromattia/kcc/pull/484) 10 | * supporting Kindle Previewer by @darodi in [#486](https://github.com/ciromattia/kcc/pull/486) 11 | * comic2ebook/func: Add a delete option (closes #458) by @Constantin1489 in [#485](https://github.com/ciromattia/kcc/pull/485) 12 | * Add command line executables to CI/pipelines by @darodi in [#487](https://github.com/ciromattia/kcc/pull/487) 13 | * gui/func: Add a 'delete after conversion' button (closes #458) by @Constantin1489 in [#488](https://github.com/ciromattia/kcc/pull/488) 14 | * fix crashes on png transparency by @axu2 in [#494](https://github.com/ciromattia/kcc/pull/494) 15 | * Update python-slugify requirement from <8.0.0,>=1.2.1 to >=1.2.1,<9.0.0 by @dependabot in [#473](https://github.com/ciromattia/kcc/pull/473) 16 | * Updates GUI text for new Homebrew version by @thatrobotdev in [#491](https://github.com/ciromattia/kcc/pull/491) 17 | * Even with EPUB-200MB option selected, created file is above 200MB by @darodi in [#503](https://github.com/ciromattia/kcc/pull/503) 18 | * limit kindle scribe image size to (1440, 1920) when using kindlegen by @darodi in [#514](https://github.com/ciromattia/kcc/pull/514) 19 | * add 7z to PATH by @axu2 in [#513](https://github.com/ciromattia/kcc/pull/513) 20 | * use unrar for fedora only by @darodi in [#515](https://github.com/ciromattia/kcc/pull/515) 21 | 22 | 23 | #### 5.6.1: 24 | * Fix pillow backwards compatibility, add mozjpeg-lossless-optimization to setup.py by @corylk in #461 25 | * fix in fedora: 7z doesn't support rar archives, use unrar by @AlicesReflexion in #370 26 | * Using communicate instead of terminate by @catsout in #459 27 | * use copyfile and delete instead of shutil.move fix #386 by @StudioEtrange in #387 28 | 29 | 30 | #### 5.6.0: 31 | * Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi) 32 | * update to python 3.11, thanks [@darodi](https://github.com/darodi) 33 | * Bump python from 3.8-slim-buster to 3.11-slim-buster dependabot[bot] 34 | * More precise type in slugify dependency check, thanks [@bamless](https://github.com/bamless) 35 | * Fix 'slugify' dependency check, thanks [@bamless](https://github.com/bamless) 36 | * Update python-slugify requirement from <3.0.0,>=1.2.1 to >=1.2.1,<8.0.0 dependabot[bot] 37 | * Bump actions/setup-python from 3 to 4 [darodi/kcc#32](https://github.com/darodi/kcc/issues/32) dependabot[bot] 38 | * Bump actions/setup-node from 2 to 3 [darodi/kcc#34](https://github.com/darodi/kcc/issues/34) dependabot[bot] 39 | * Fix Docker 7z missing [darodi/kcc#31](https://github.com/darodi/kcc/issues/31), thanks [@darodi](https://github.com/darodi) 40 | * Spread splitter not keeping aspect ratio [darodi/kcc#27](https://github.com/darodi/kcc/issues/27), thanks [@darodi](https://github.com/darodi) 41 | * activate batchsplit only for EPUB-200 [darodi/kcc#24](https://github.com/darodi/kcc/issues/24), thanks [@darodi](https://github.com/darodi) 42 | * Feature Request: allow split for epub and set target (email, web upload) [darodi/kcc#21](https://github.com/darodi/kcc/issues/21), thanks [@darodi](https://github.com/darodi) 43 | * Adding a switch on GUI interface in order to control cropping options [darodi/kcc#18](https://github.com/darodi/kcc/issues/18), thanks [@darodi](https://github.com/darodi) 44 | * profiles: add Kindle11 and Kindle Scribe [darodi/kcc#16](https://github.com/darodi/kcc/issues/16), thanks [@darodi](https://github.com/darodi) 45 | * Update with new Kobo models [darodi/kcc#15](https://github.com/darodi/kcc/issues/15), thanks [@lennie420](https://github.com/lennie420) 46 | * Keep epub file when selecting another type of output file [darodi/kcc#12](https://github.com/darodi/kcc/issues/12), thanks [@darodi](https://github.com/darodi) 47 | * fix Using 'Disable processing' option using my processed image get an error [darodi/kcc#1](https://github.com/darodi/kcc/issues/1), thanks [@darodi](https://github.com/darodi) 48 | * KFX Output in GUI [darodi/kcc#9](https://github.com/darodi/kcc/issues/9), thanks [@darodi](https://github.com/darodi) 49 | * Linux version and appImage [darodi/kcc#6](https://github.com/darodi/kcc/issues/6), thanks [@darodi](https://github.com/darodi) 50 | * docker image for command line [darodi/kcc#5](https://github.com/darodi/kcc/issues/5), thanks [@darodi](https://github.com/darodi) 51 | * Fix type error in autocontrastImage [ciromattia/kcc#432](https://github.com/ciromattia/kcc/issues/432), thanks [@darodi](https://github.com/darodi) 52 | * Option to turn 1x4 strips into 2x2 strips [ciromattia/kcc#439](https://github.com/ciromattia/kcc/issues/439), thanks [@darodi](https://github.com/darodi) 53 | * Option in GUI to have PNG instead of jpg images [darodi/kcc#3](https://github.com/darodi/kcc/issues/3), thanks [@darodi](https://github.com/darodi) 54 | * Use MozJPEG as the JPEG encoder : cli and GUI option [ciromattia/kcc#416](https://github.com/ciromattia/kcc/pull/416), thanks [@darodi](https://github.com/darodi) 55 | * Disable all image transformation : cli and GUI option [ciromattia/kcc#388](https://github.com/ciromattia/kcc/pull/388), thanks [@StudioEtrange](https://github.com/StudioEtrange) 56 | * file selector add All `*.*` [ciromattia/kcc#412](https://github.com/ciromattia/kcc/pull/412), thanks [@StudioEtrange](https://github.com/StudioEtrange) 57 | * Clarify Pillow version requirement [ciromattia/kcc#366](https://github.com/ciromattia/kcc/pull/366), thanks [@clach04](https://github.com/clach04) 58 | * sync requirements between setup.py and requirements.txt [ciromattia/kcc#411](https://github.com/ciromattia/kcc/pull/411), thanks [@StudioEtrange](https://github.com/StudioEtrange) 59 | * Add profile for Kindle PW5/Signature [ciromattia/kcc#405](https://github.com/ciromattia/kcc/pull/405), thanks [@Einlar](https://github.com/Einlar), [@darodi](https://github.com/darodi) 60 | * Fixed the skipped/missed images and/or panels [ciromattia/kcc#393](https://github.com/ciromattia/kcc/pull/393), thanks [@FulyaDemirkan](https://github.com/FulyaDemirkan) 61 | * Add profiles for the Kobo Clara HD and Libra H2O [ciromattia/kcc#331](https://github.com/ciromattia/kcc/pull/331), thanks [@fbriere](https://github.com/fbriere) 62 | 63 | #### 5.5.2: 64 | * Fixed KindleGen detection on macOS 10.15 65 | * Fixed multiple smaller issues 66 | 67 | #### 5.5.1: 68 | * Fixes some stability issues 69 | 70 | #### 5.5.0: 71 | * Added support for WebP format 72 | * Added profiles for Kindle Paperwhite 4 and Kobo Forma 73 | * All archives are now handled by 7z 74 | * Removed MCD support 75 | * Fixed multiple smaller issues 76 | 77 | #### 5.4.5: 78 | * Fixed EPUB output for non-Kindle devices 79 | 80 | #### 5.4.4: 81 | * Minor bug fixes 82 | 83 | #### 5.4.3: 84 | * Fixed conversion crash on Windows 85 | 86 | #### 5.4.2: 87 | * Added Kindle Oasis 2 profile 88 | * Allowed metadata editor to edit directories 89 | * Fixed image stretching when HQ Panel View option was enabled 90 | * Fixed possible problem with directory sort order 91 | 92 | #### 5.4.1: 93 | * Minor bug fixes and tweaks 94 | * Implemented new binary build pipeline 95 | 96 | #### 5.4: 97 | * Reimplemented high quality Panel View option 98 | * Improved webtoon splitter 99 | * Fixed page splitter 100 | 101 | #### 5.3.1: 102 | * Small increase of output quality 103 | * Improved error reporting 104 | * Internal changes and tweaks 105 | 106 | #### 5.3: 107 | * Vastly improved output compatibility for non-Kindle devices 108 | * Enabled old pinch zoom for Kindle devices 109 | * Re-enabled Panel View support for Kindle Keyboard 110 | * Partially re-enabled OS X file association mechanism 111 | * Fixed multiple smaller issues 112 | 113 | #### 5.2.1: 114 | * Improved directory parsing 115 | * Tweaked margin detection algorithm 116 | * Improved error reporting 117 | 118 | #### 5.2: 119 | * Added new Panel View options 120 | * Implemented new margin detection algorithm 121 | * Removed HQ Panel View mode 122 | * Fixed multiple smaller issues 123 | 124 | #### 5.1.3: 125 | * Added Kobo Aura ONE profile 126 | * Fixed few small bugs 127 | 128 | #### 5.1.2: 129 | * Fixed error reporting 130 | 131 | #### 5.1.1: 132 | * Fixed multiple GUI bugs 133 | 134 | #### 5.1: 135 | * GUI now can be resized and high DPI support was somewhat improved 136 | * Added profile for Kindle Oasis 137 | * Implemented new error reporting mechanism 138 | * CLI version now support additional cropping options 139 | * Fixed permission issues on Windows 140 | * Fixed multiple smaller issues 141 | 142 | #### 5.0.1: 143 | * Fixed Panel View placement issues 144 | * Decreased application startup time 145 | * Fixed multiple smaller issues 146 | 147 | #### 5.0: 148 | * Major overhaul of internal mechanisms and GUI 149 | * Added cover upload feature 150 | * Tweaked Webtoon parsing mode 151 | * Fixed multiple smaller issues 152 | * Migrated build enviroment to PyInstaller 153 | 154 | #### 4.6.5: 155 | * Fixed multiple Windows and OS X issues 156 | * Allowed Linux release to use older PyQT5 version 157 | 158 | #### 4.6.4: 159 | * Fixed multiple Windows specific problems 160 | * Improved error handling 161 | * Improved color detection algorithm 162 | * New, slimmer OS X release 163 | 164 | #### 4.6.3: 165 | * Implemented remote bug reporting 166 | * Minor bug fixes and GUI tweaks 167 | 168 | #### 4.6.2: 169 | * Fixed critical MOBI header bug 170 | * Fixed metadata encoding error 171 | 172 | #### 4.6.1: 173 | * Fixed KEPUB TOC generator 174 | * Added warning about too small input files 175 | * ComicRack Summary metadata field is now parsed 176 | * Small tweaks of KEPUB output 177 | 178 | #### 4.6: 179 | * KEPUB is now default output for all Kobo profiles 180 | * EPUB output now produce fully valid EPUB 3.0.1 181 | * Added profile for Kindle Paperwhite 3 182 | * Dropped official support of all Kindle Fire models and Kindle for Android 183 | * Other minor tweaks 184 | 185 | #### 4.5.1: 186 | * Added Kobo Glo HD profile 187 | * Fixed RAR/CBR parsing anomalies 188 | * Minor bug fixes and tweaks 189 | 190 | #### 4.5: 191 | * Added simple ComicRack metadata editor 192 | * Re-enabled Manga Cover Database support 193 | * ComicRack bookmarks are now parsed 194 | * Fixed glitches in Kindle Voyage profile 195 | * Fixed problems with directory locks on Windows 196 | * Fixed sorting anomalies 197 | * Improved conversion speed 198 | 199 | #### 4.4.1: 200 | * Fixed problems with OSX GUI 201 | * Added one missing DLL to Windows installer 202 | 203 | #### 4.4: 204 | * Improved speed and quality of conversion 205 | * Added RAR5 support 206 | * Dropped BMP and TIFF support 207 | * Fixed some WebToon mode bugs 208 | * Fixed CBR parsing on OSX 209 | 210 | #### 4.3.1: 211 | * Fixed Kindle Voyage profile 212 | * Fixed some bugs in OS X release 213 | * CLI version now support multiple input files at once 214 | * Disabled MCB support 215 | * Other minor tweaks 216 | 217 | #### 4.3: 218 | * Added profiles for Kindle Voyage and Kobo Aura H2O 219 | * Added missing features to CLI version 220 | * Other minor bug fixes 221 | 222 | #### 4.2.1: 223 | * Improved margin color detection 224 | * Fixed random crashes of MOBI processing step 225 | * Fixed resizing problems in high quality mode 226 | * Fixed some MCD support bugs 227 | * Default output format for Kindle DX is now CBZ 228 | 229 | #### 4.2: 230 | * Added [Manga Cover Database](http://manga.joentjuh.nl/) support 231 | * Officially dropped Windows XP support 232 | * Fixed _Other_ profile 233 | * Fixed problems with page order on stock KOBO CBZ reader 234 | * Many other small bug fixes and tweaks 235 | 236 | #### 4.1: 237 | * Thanks to code contributed by Kevin Hendricks speed of MOBI creation was greatly increased 238 | * Improved performance on Windows 239 | * Improved MOBI splitting and changed maximal size of output file 240 | * Fixed _No optimization_ mode 241 | * Multiple small tweaks nad minor bug fixes 242 | 243 | #### 4.0.2: 244 | * Fixed some Windows and OSX specific bugs 245 | * Fixed problem with marigns when using HQ mode 246 | 247 | #### 4.0.1: 248 | * Fixed file lock problems that plagued some Windows users 249 | * Fixed content server failing to start on Windows 250 | * Improved performance of WebToon splitter 251 | * Tweaked margin color detection 252 | 253 | #### 4.0: 254 | * KCC now use Python 3.3 and Qt 5.2 255 | * Full UTF-8 awareness 256 | * CBZ output now support Manga mode 257 | * Improved Panel View support and margin color detection 258 | * Added drag&drop support 259 | * Output directory can be now selected 260 | * Windows release now have auto-updater 261 | * Names of chapters on Kindle should be now more user friendly 262 | * Fixed OSX file association support 263 | * Many extensive internal changes and tweaks 264 | 265 | #### 3.7.2: 266 | * Fixed problems with HQ mode 267 | 268 | #### 3.7.1: 269 | * Hotfixed Kobo profiles 270 | 271 | #### 3.7: 272 | * Added profiles for KOBO devices 273 | * Improved Panel View support 274 | * Improved WebToon splitter 275 | * Improved margin color autodetection 276 | * Tweaked EPUB output 277 | * Fixed stretching option 278 | * GUI tweaks and minor bugfixes 279 | 280 | #### 3.6.2: 281 | * Fixed previous PNG output fix 282 | * Fixed Panel View anomalies 283 | 284 | #### 3.6.1: 285 | * Fixed PNG output 286 | 287 | #### 3.6: 288 | * Increased quality of Panel View zoom 289 | * Creation of multipart MOBI output is now faster on machines with 4GB+ RAM 290 | * Automatic gamma correction now distinguishes color and grayscale images 291 | * Added ComicRack metadata parser 292 | * Implemented new method to detect border color in non-webtoon comics 293 | * Upscaling is now enabled by default for Kindle Fire HD/HDX 294 | * Windows nad Linux releases now have tray icon 295 | * Fixed Kindle Fire HDX 7" output 296 | * Increased target resolution for Kindle DX/DXG CBZ output 297 | 298 | #### 3.5: 299 | * Added simple content server - Converted files can be now delivered wireless 300 | * Added proper Windows installer 301 | * Improved multiprocessing speed 302 | * GUI tweaks and minor bug fixes 303 | 304 | #### 3.4: 305 | * Improved PNG output 306 | * Increased quality of upscaling 307 | * Added support of file association - KCC can now open CBZ, CBR, CB7, ZIP, RAR, 7Z and PDF files directly 308 | * Paths that contain UTF-8 characters are now supported 309 | * Migrated to new version of Pillow library 310 | * Merged DX and DXG profiles 311 | * Many other minor bug fixes and GUI tweaks 312 | 313 | #### 3.3: 314 | * Margins are now automatically omitted in Panel View mode 315 | * Margin color fill is now autodetected 316 | * Created MOBI files are not longer marked as _Personal_ on newer Kindle models 317 | * Layout of panels in Panel View mode is now automatically adjusted to content 318 | * Fixed Kindle 2/DX/DXG profiles - no more blank pages 319 | * All Kindle Fire profiles now support hiqh quality Panel View 320 | * Added support of 7z/CB7 files 321 | * Added Kindle Fire HDX profile 322 | * Support for Virtual Panel View was removed 323 | * Profiles for Kindle Keyboard, Touch and Non-Touch are now merged 324 | * Windows release is now bundled with UnRAR and 7za 325 | * Small GUI tweaks 326 | 327 | #### 3.2: 328 | * Too big EPUB files are now splitted before conversion to MOBI 329 | * Added experimental parser of manga webtoons 330 | * Improved error handling 331 | 332 | #### 3.2.1: 333 | * Hotfixed crash occurring on OS with Russian locale 334 | 335 | #### 3.1: 336 | * Added profile: Kindle for Android 337 | * Add file/directory dialogs now support multiselect 338 | * Many small fixes and tweaks 339 | 340 | #### 3.0: 341 | * New QT GUI 342 | * Merge with AWKCC 343 | * Added ultra quality mode 344 | * Added support for custom width/height 345 | * Added option to disable color conversion 346 | 347 | #### 2.10: 348 | * Multiprocessing support 349 | * Kindle Fire support (color EPUB/MOBI) 350 | * Panel View support for horizontal content 351 | * Fixed panel order for horizontal pages when --rotate is enabled 352 | * Disabled cropping and page number cutting for blank pages 353 | * Fixed some slugify issues with specific file naming conventions (#50, #51) 354 | 355 | #### 2.9 356 | * Added support for generating a plain CBZ (skipping all the EPUB/MOBI generation) (#45) 357 | * Prevent output file overwriting the source one: if a duplicate name is detected, append _kcc to the name 358 | * Rarfile library updated to 2.6 359 | * Added GIF, TIFF and BMP to supported formats (#42) 360 | * Filenames slugifications (#28, #31, #9, #8) 361 | 362 | #### 2.8 363 | * Updated rarfile library 364 | * Panel View support + HQ support (#36) - new option: --nopanelviewhq 365 | * Split profiles for K4NT and K4T 366 | * Rewrite of Landscape Mode support (huge readability improvement for KPW) 367 | * Upscale use now BILINEAR method 368 | * Added generic CSS file 369 | * Optimized archive extraction for zip/rar files (#40) 370 | 371 | #### 2.7 372 | * Lots of GUI improvements (#27, #13) 373 | * Added gamma support within --gamma option (defaults to profile-specified gamma) (#26, #27) 374 | * Added --nodithering option to prevent dithering optimizations (#27) 375 | * EPUB margins support (#30) 376 | * Fixed no file added if file has no spaces on Windows (#25) 377 | * Gracefully exit if unrar missing (#15) 378 | * Do not call kindlegen if source EPUB is bigger than 320MB (#17) 379 | * Get filetype from magic number (#14) 380 | * PDF conversion works again 381 | 382 | #### 2.6 383 | * Added --rotate option to rotate landscape images instead of splitting them (#16, #24) 384 | * Added --output option to customize EPUB output dir/file (#22) 385 | * Add rendition:layout and rendition:orientation EPUB meta tags (supported by new kindlegen 2.8) 386 | * Fixed natural sorting for files (#18) 387 | 388 | #### 2.5 389 | * Added --black-borders option to set added borders black when page's ratio is not the device's one (#11). 390 | * Fixes EPUB containing zipped itself (#10) 391 | 392 | #### 2.4 393 | * Use temporary directory as workdir (fixes converting from external volumes and zipfiles renaming) 394 | * Fixed "add folders" from GUI. 395 | 396 | #### 2.3 397 | * Fixed win32 EPUB generation, folder handling, filenames with spaces and subfolders 398 | 399 | #### 2.2: 400 | * Added (valid!) EPUB 2.0 output 401 | * Rename .zip files to .cbz to avoid overwriting 402 | 403 | #### 2.1 404 | * Added basic error reporting 405 | 406 | #### 2.0 407 | * GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support. 408 | 409 | #### 1.5 410 | * Added subfolder support for multiple chapters. 411 | 412 | #### 1.4.1 413 | * Fixed a serious bug on resizing when img ratio was bigger than device one 414 | 415 | #### 1.4 416 | * Added some options for controlling image optimization 417 | * Further optimization (ImageOps, page numbering cut, autocontrast) 418 | 419 | #### 1.3 420 | * Fixed an issue in OPF generation for device resolution 421 | * Reworked options system (call with -h option to get the inline help) 422 | 423 | #### 1.2 424 | * Comic optimizations! Split pages not target-oriented (landscape with portrait target or portrait with landscape target), add palette and other image optimizations from Mangle. WARNING: PIL is required for all image mangling! 425 | 426 | #### 1.1.1 427 | * Added support for CBZ/CBR files in Kindle Comic Converter 428 | 429 | #### 1.1 430 | * Added support for CBZ/CBR files in comic2ebook.py 431 | 432 | #### 1.0 433 | * Initial version 434 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Select final stage based on TARGETARCH ARG 2 | FROM ghcr.io/ciromattia/kcc:docker-base-20241116 3 | LABEL com.kcc.name="Kindle Comic Converter" 4 | LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" 5 | LABEL org.opencontainers.image.description='Kindle Comic Converter' 6 | LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' 7 | LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc' 8 | LABEL org.opencontainers.image.authors='darodi' 9 | LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc' 10 | LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' 11 | LABEL org.opencontainers.image.vendor='ciromattia' 12 | LABEL org.opencontainers.image.licenses='ISC' 13 | LABEL org.opencontainers.image.title="Kindle Comic Converter" 14 | 15 | COPY . /opt/kcc 16 | RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION 17 | 18 | ENTRYPOINT ["/opt/kcc/kcc-c2e.py"] 19 | CMD ["-h"] 20 | -------------------------------------------------------------------------------- /Dockerfile-base: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 python:3.13-slim-bullseye as compile-amd64 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | ARG TARGETVARIANT 5 | RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT" 6 | 7 | 8 | COPY requirements.txt /opt/kcc/ 9 | ENV PATH="/opt/venv/bin:$PATH" 10 | RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \ 11 | apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 && \ 12 | python -m pip install --upgrade pip && \ 13 | python -m venv /opt/venv && \ 14 | python -m pip install -r /opt/kcc/requirements.txt 15 | 16 | 17 | ###################################################################################### 18 | 19 | FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64 20 | ARG TARGETOS 21 | ARG TARGETARCH 22 | ARG TARGETVARIANT 23 | RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT" 24 | 25 | ENV LC_ALL=C.UTF-8 \ 26 | LANG=C.UTF-8 \ 27 | LANGUAGE=en_US:en 28 | 29 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 30 | 31 | COPY requirements.txt /opt/kcc/ 32 | ENV PATH="/opt/venv/bin:$PATH" 33 | 34 | RUN set -x && \ 35 | TEMP_PACKAGES=() && \ 36 | KEPT_PACKAGES=() && \ 37 | # Packages only required during build 38 | TEMP_PACKAGES+=(build-essential) && \ 39 | TEMP_PACKAGES+=(cmake) && \ 40 | TEMP_PACKAGES+=(libfreetype6-dev) && \ 41 | TEMP_PACKAGES+=(libfontconfig1-dev) && \ 42 | TEMP_PACKAGES+=(libpng-dev) && \ 43 | TEMP_PACKAGES+=(libjpeg-dev) && \ 44 | TEMP_PACKAGES+=(libssl-dev) && \ 45 | TEMP_PACKAGES+=(libxft-dev) && \ 46 | TEMP_PACKAGES+=(make) && \ 47 | TEMP_PACKAGES+=(python3-dev) && \ 48 | TEMP_PACKAGES+=(python3-setuptools) && \ 49 | TEMP_PACKAGES+=(python3-wheel) && \ 50 | # Packages kept in the image 51 | KEPT_PACKAGES+=(bash) && \ 52 | KEPT_PACKAGES+=(ca-certificates) && \ 53 | KEPT_PACKAGES+=(chrpath) && \ 54 | KEPT_PACKAGES+=(locales) && \ 55 | KEPT_PACKAGES+=(locales-all) && \ 56 | KEPT_PACKAGES+=(libfreetype6) && \ 57 | KEPT_PACKAGES+=(libfontconfig1) && \ 58 | KEPT_PACKAGES+=(p7zip-full) && \ 59 | KEPT_PACKAGES+=(python3) && \ 60 | KEPT_PACKAGES+=(python3-pip) && \ 61 | KEPT_PACKAGES+=(unrar-free) && \ 62 | # Install packages 63 | DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \ 64 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 65 | ${KEPT_PACKAGES[@]} \ 66 | ${TEMP_PACKAGES[@]} \ 67 | && \ 68 | # Install required python modules 69 | python -m pip install --upgrade pip && \ 70 | python -m venv /opt/venv && \ 71 | python -m pip install -r /opt/kcc/requirements.txt 72 | 73 | 74 | ###################################################################################### 75 | 76 | FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7 77 | ARG TARGETOS 78 | ARG TARGETARCH 79 | ARG TARGETVARIANT 80 | RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT" 81 | 82 | ENV LC_ALL=C.UTF-8 \ 83 | LANG=C.UTF-8 \ 84 | LANGUAGE=en_US:en 85 | 86 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 87 | 88 | COPY requirements.txt /opt/kcc/ 89 | ENV PATH="/opt/venv/bin:$PATH" 90 | 91 | RUN set -x && \ 92 | TEMP_PACKAGES=() && \ 93 | KEPT_PACKAGES=() && \ 94 | # Packages only required during build 95 | TEMP_PACKAGES+=(build-essential) && \ 96 | TEMP_PACKAGES+=(cmake) && \ 97 | TEMP_PACKAGES+=(libffi-dev) && \ 98 | TEMP_PACKAGES+=(libfreetype6-dev) && \ 99 | TEMP_PACKAGES+=(libfontconfig1-dev) && \ 100 | TEMP_PACKAGES+=(libpng-dev) && \ 101 | TEMP_PACKAGES+=(libjpeg-dev) && \ 102 | TEMP_PACKAGES+=(libssl-dev) && \ 103 | TEMP_PACKAGES+=(libxft-dev) && \ 104 | TEMP_PACKAGES+=(make) && \ 105 | TEMP_PACKAGES+=(python3-dev) && \ 106 | TEMP_PACKAGES+=(python3-setuptools) && \ 107 | TEMP_PACKAGES+=(python3-wheel) && \ 108 | # Packages kept in the image 109 | KEPT_PACKAGES+=(bash) && \ 110 | KEPT_PACKAGES+=(ca-certificates) && \ 111 | KEPT_PACKAGES+=(chrpath) && \ 112 | KEPT_PACKAGES+=(locales) && \ 113 | KEPT_PACKAGES+=(locales-all) && \ 114 | KEPT_PACKAGES+=(libfreetype6) && \ 115 | KEPT_PACKAGES+=(libfontconfig1) && \ 116 | KEPT_PACKAGES+=(p7zip-full) && \ 117 | KEPT_PACKAGES+=(python3) && \ 118 | KEPT_PACKAGES+=(python3-pip) && \ 119 | KEPT_PACKAGES+=(unrar-free) && \ 120 | # Install packages 121 | DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \ 122 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 123 | ${KEPT_PACKAGES[@]} \ 124 | ${TEMP_PACKAGES[@]} \ 125 | && \ 126 | # Install required python modules 127 | python -m pip install --upgrade pip && \ 128 | python -m venv /opt/venv && \ 129 | python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy 130 | 131 | 132 | ###################################################################################### 133 | FROM --platform=linux/amd64 python:3.13-slim-bullseye as build-amd64 134 | COPY --from=compile-amd64 /opt/venv /opt/venv 135 | 136 | FROM --platform=linux/arm64 python:3.13-slim-bullseye as build-arm64 137 | COPY --from=compile-arm64 /opt/venv /opt/venv 138 | 139 | FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as build-armv7 140 | COPY --from=compile-armv7 /opt/venv /opt/venv 141 | ###################################################################################### 142 | 143 | # Select final stage based on TARGETARCH ARG 144 | FROM build-${TARGETARCH}${TARGETVARIANT} 145 | LABEL com.kcc.name="Kindle Comic Converter base image" 146 | LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" 147 | LABEL org.opencontainers.image.description='Kindle Comic Converter base image' 148 | LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' 149 | LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc' 150 | LABEL org.opencontainers.image.authors='darodi' 151 | LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc' 152 | LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' 153 | LABEL org.opencontainers.image.vendor='ciromattia' 154 | LABEL org.opencontainers.image.licenses='ISC' 155 | LABEL org.opencontainers.image.title="Kindle Comic Converter" 156 | 157 | 158 | ENV PATH="/opt/venv/bin:$PATH" 159 | WORKDIR /app 160 | RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \ 161 | apt-get install -y p7zip-full unrar-free && \ 162 | ln -s /app/kindlegen /bin/kindlegen && \ 163 | echo docker-base-20241116 > /IMAGE_VERSION 164 | 165 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC LICENSE 2 | 3 | Copyright (c) 2012-2025 Ciro Mattia Gonano 4 | Copyright (c) 2013-2019 Paweł Jastrzębski 5 | Copyright (c) 2021-2023 Darodi (https://github.com/darodi) 6 | Copyright (c) 2023-2025 Alex Xu (https://github.com/axu2) 7 | 8 | Permission to use, copy, modify, and/or distribute this software for 9 | any purpose with or without fee is hereby granted, provided that the 10 | above copyright notice and this permission notice appear in all 11 | copies. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 14 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 16 | AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 17 | DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 18 | OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 19 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 20 | PERFORMANCE OF THIS SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Header Image 2 | 3 | # KCC 4 | 5 | [![GitHub release](https://img.shields.io/github/release/ciromattia/kcc.svg)](https://github.com/ciromattia/kcc/releases) 6 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ciromattia/kcc/docker-publish.yml?label=docker%20build)](https://github.com/ciromattia/kcc/pkgs/container/kcc) 7 | [![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](https://github.com/ciromattia/kcc/releases) 8 | 9 | 10 | **Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders 11 | like Kindle, Kobo, ReMarkable, and more. 12 | Pages display in fullscreen without margins, 13 | with proper fixed layout support. 14 | Supported input formats include JPG/PNG/GIF/JP2/WEBP image files in folders, archives, or PDFs. 15 | Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ. 16 | 17 | Its main feature is various optional image processing steps to look good on eink screens, 18 | which have different requirements than normal LCD screens. 19 | Combining that with downscaling to your specific device's screen resolution 20 | can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink. 21 | This can also improve battery life, page turn speed, and general performance 22 | on underpowered ereaders with small storage capacities. 23 | 24 | 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: 25 | 1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain. 26 | 2) unneccessary margins at the bottom of the screen 27 | 3) incorrect page turn direction for manga that's read right to left 28 | 4) unaligned two page spreads in landscape, where pages are shifted over by 1 29 | 30 | The GUI looks like this, built in Qt6, with my most commonly used settings: 31 | 32 | ![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218) 33 | 34 | Simply drag and drop your files/folders into the KCC window, 35 | adjust your settings (hover over each option to see details in a tooltip), 36 | and hit convert to create ereader optimized files. 37 | You can change the default output directory by holding `Shift` while clicking the convert button. 38 | Then just drag and drop the generated output files onto your device's documents folder via USB. 39 | If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac. 40 | 41 | YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658 42 | 43 | ### A word of warning 44 | **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. 45 | Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers. 46 | _KC2_ in no way is a replacement for **KCC** so you can be quite confident we are going to carry on developing our little monster ;-) 47 | 48 | ### Issues / new features / donations 49 | If you have general questions about usage, feedback etc. please [post it here](http://www.mobileread.com/forums/showthread.php?t=207461). 50 | If you have some **technical** problems using KCC please [file an issue here](https://github.com/ciromattia/kcc/issues/new). 51 | If you can fix an open issue, fork & make a pull request. 52 | 53 | If you find **KCC** valuable you can consider donating to the authors: 54 | - Ciro Mattia Gonano (founder, active 2012-2014): 55 | 56 | [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2) 57 | 58 | - Paweł Jastrzębski (active 2013-2019): 59 | 60 | [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS) 61 | [![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/) 62 | 63 | - Alex Xu (active 2023-Present) 64 | 65 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q41BW8HS) 66 | 67 | ## Sponsors 68 | 69 | - Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) 70 | 71 | ## DOWNLOADS 72 | 73 | - **https://github.com/ciromattia/kcc/releases** 74 | 75 | Click on **Assets** of the latest release. 76 | 77 | You probably want either 78 | - `KCC_*.*.*.exe` (Windows) 79 | - `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later) 80 | - `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip) 81 | 82 | The `c2e` and `c2p` versions are command line tools for power users. 83 | 84 | On Windows 11, you may need to run in compatibility mode for an older Windows version. 85 | 86 | On Mac, right click open to get past the security warning. 87 | 88 | For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation 89 | 90 | ## FAQ 91 | - How to make AZW3 instead of MOBI? 92 | - The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons. 93 | - [Windows 7 support](https://github.com/ciromattia/kcc/issues/678) 94 | - [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011) 95 | - [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux) 96 | - Image too dark? 97 | - 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 98 | - [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680) 99 | - Cannot connect Kindle Scribe or 2024+ Kindle to macOS 100 | - Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu) 101 | (no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps. 102 | - Huge margins / slow page turns? 103 | - 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. 104 | 105 | ## PREREQUISITES 106 | 107 | You'll need to install various tools to access important but optional features. Close and re-open KCC to get KCC to detect them. 108 | 109 | ### KindleGen 110 | 111 | On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it. 112 | 113 | If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen 114 | 115 | ### 7-Zip 116 | 117 | This is optional but will make conversions much faster. 118 | 119 | This is required for certain files and advanced features. 120 | 121 | KCC will ask you to install if needed. 122 | 123 | Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip 124 | 125 | ## INPUT FORMATS 126 | **KCC** can understand and convert, at the moment, the following input types: 127 | - Folders containing: PNG, JPG, GIF or WebP files 128 | - CBZ, ZIP *(With `7z` executable)* 129 | - CBR, RAR *(With `7z` executable)* 130 | - CB7, 7Z *(With `7z` executable)* 131 | - PDF *(Only extracting JPG images)* 132 | 133 | ## USAGE 134 | 135 | Should be pretty self-explanatory. All options have detailed information in tooltips. 136 | After completed conversion, you should find ready file alongside the original input file (same directory). 137 | 138 | Please check [our wiki](https://github.com/ciromattia/kcc/wiki/) for more details. 139 | 140 | CLI version of **KCC** is intended for power users. It allows using options that might not be compatible and decrease the quality of output. 141 | CLI version has reduced dependencies, on Debian based distributions this commands should install all needed dependencies: 142 | ``` 143 | sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugify 144 | ``` 145 | 146 | ### Profiles: 147 | 148 | ``` 149 | 'K1': ("Kindle 1", (600, 670), Palette4, 1.8), 150 | 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8), 151 | 'K2': ("Kindle 2", (600, 670), Palette15, 1.8), 152 | 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8), 153 | 'K578': ("Kindle", (600, 800), Palette16, 1.8), 154 | 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8), 155 | 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8), 156 | 'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8), 157 | 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8), 158 | 'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8), 159 | 'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8), 160 | 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8), 161 | 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8), 162 | 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8), 163 | 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8), 164 | 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8), 165 | 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8), 166 | 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8), 167 | 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8), 168 | 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8), 169 | 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8), 170 | 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8), 171 | 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8), 172 | 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8), 173 | 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8), 174 | 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8), 175 | 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8), 176 | 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8), 177 | 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8), 178 | 'OTHER': ("Other", (0, 0), Palette16, 1.8), 179 | ``` 180 | 181 | ### Standalone `kcc-c2e.py` usage: 182 | 183 | ``` 184 | usage: kcc-c2e [options] [input] 185 | 186 | MANDATORY: 187 | input Full path to comic folder or file(s) to be processed. 188 | 189 | MAIN: 190 | -p PROFILE, --profile PROFILE 191 | Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE) 192 | [Default=KV] 193 | -m, --manga-style Manga style (right-to-left reading and splitting) 194 | -q, --hq Try to increase the quality of magnification 195 | -2, --two-panel Display two not four panels in Panel View mode 196 | -w, --webtoon Webtoon processing mode 197 | --ts TARGETSIZE, --targetsize TARGETSIZE 198 | the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others] 199 | 200 | PROCESSING: 201 | -n, --noprocessing Do not modify image and ignore any profil or processing option 202 | -u, --upscale Resize images smaller than device's resolution 203 | -s, --stretch Stretch images to device's resolution 204 | -r SPLITTER, --splitter SPLITTER 205 | Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0] 206 | -g GAMMA, --gamma GAMMA 207 | Apply gamma correction to linearize the image [Default=Auto] 208 | -c CROPPING, --cropping CROPPING 209 | Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2] 210 | --cp CROPPINGP, --croppingpower CROPPINGP 211 | Set cropping power [Default=1.0] 212 | --preservemargin After calculating crop, "back up" a specified percentage amount [Default=0] 213 | --cm CROPPINGM, --croppingminimum CROPPINGM 214 | Set cropping minimum area ratio [Default=0.0] 215 | --ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP 216 | Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0] 217 | --blackborders Disable autodetection and force black borders 218 | --whiteborders Disable autodetection and force white borders 219 | --forcecolor Don't convert images to grayscale 220 | --forcepng Create PNG files instead JPEG 221 | --mozjpeg Create JPEG files using mozJpeg 222 | --maximizestrips Turn 1x4 strips to 2x2 strips 223 | -d, --delete Delete source file(s) or a directory. It's not recoverable. 224 | 225 | OUTPUT SETTINGS: 226 | -o OUTPUT, --output OUTPUT 227 | Output generated file to specified directory or file 228 | -t TITLE, --title TITLE 229 | Comic title [Default=filename or directory name] 230 | -a AUTHOR, --author AUTHOR 231 | Author name [Default=KCC] 232 | -f FORMAT, --format FORMAT 233 | Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto] 234 | --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' 235 | -b BATCHSPLIT, --batchsplit BATCHSPLIT 236 | Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] 237 | --spreadshift Shift first page to opposite side in landscape for two page spread alignment 238 | --norotate Do not rotate double page spreads in spread splitter option. 239 | --reducerainbow Reduce rainbow effect on color eink by slightly blurring images 240 | 241 | CUSTOM PROFILE: 242 | --customwidth CUSTOMWIDTH 243 | Replace screen width provided by device profile 244 | --customheight CUSTOMHEIGHT 245 | Replace screen height provided by device profile 246 | 247 | OTHER: 248 | -h, --help Show this help message and exit 249 | 250 | ``` 251 | 252 | ### Standalone `kcc-c2p.py` usage: 253 | 254 | ``` 255 | usage: kcc-c2p [options] [input] 256 | 257 | MANDATORY: 258 | input Full path to comic folder(s) to be processed. Separate multiple inputs with spaces. 259 | 260 | MAIN: 261 | -y HEIGHT, --height HEIGHT 262 | Height of the target device screen 263 | -i, --in-place Overwrite source directory 264 | -m, --merge Combine every directory into a single image before splitting 265 | 266 | OTHER: 267 | -d, --debug Create debug file for every split image 268 | -h, --help Show this help message and exit 269 | ``` 270 | 271 | ## INSTALL FROM SOURCE 272 | 273 | This section is for developers who want to contribute to KCC or power users who want to run the latest code without waiting for an official release. 274 | 275 | Easiest to use [GitHub Desktop](https://desktop.github.com) to clone your fork of the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo. 276 | 277 | Depending on your system [Python](https://www.python.org) may be called either `python` or `python3`. We use virtual environments (venv) to manage dependencies. 278 | 279 | If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com). 280 | 281 | If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`. 282 | Then use the `gen_ui_files` scripts to autogenerate the python UI. 283 | 284 | An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785 285 | 286 | Do not use `git merge` to merge master from upstream, 287 | use the "Sync fork" button on your fork on GitHub in your branch 288 | to avoid weird looking merges in pull requests. 289 | 290 | ### Windows install from source 291 | 292 | One time setup and running for the first time: 293 | ``` 294 | python -m venv venv 295 | venv\Scripts\activate.bat 296 | pip install -r requirements.txt 297 | python kcc.py 298 | ``` 299 | 300 | Every time you close Command Prompt, you will need to re-activate the virtual environment and re-run: 301 | 302 | ``` 303 | venv\Scripts\activate.bat 304 | python kcc.py 305 | ``` 306 | 307 | You can build a `.exe` of KCC like the downloads we offer with 308 | 309 | ``` 310 | python setup.py build_binary 311 | ``` 312 | 313 | ### macOS install from source 314 | 315 | If the system installed Python gives you issues, please install the latest Python from either brew or the official website. 316 | 317 | One time setup and running for the first time: 318 | ``` 319 | python3 -m venv venv 320 | source venv/bin/activate 321 | pip install -r requirements.txt 322 | python kcc.py 323 | ``` 324 | 325 | Every time you close Terminal, you will need to reactivate the virtual environment and re-run: 326 | 327 | ``` 328 | source venv/bin/activate 329 | python kcc.py 330 | ``` 331 | 332 | You can build a `.app` of KCC like the downloads we offer with 333 | 334 | ``` 335 | python setup.py build_binary 336 | ``` 337 | 338 | ## CREDITS 339 | **KCC** is made by 340 | 341 | - [Ciro Mattia Gonano](http://github.com/ciromattia) 342 | - [Paweł Jastrzębski](http://github.com/AcidWeb) 343 | - [Darodi](http://github.com/darodi) 344 | - [Alex Xu](http://github.com/axu2) 345 | 346 | This script born as a cross-platform alternative to `KindleComicParser` by **Dc5e** (published [here](http://www.mobileread.com/forums/showthread.php?t=192783)). 347 | 348 | The app relies and includes the following scripts: 349 | 350 | - `DualMetaFix` script by **K. Hendricks**. Released with GPL-3 License. 351 | - `image.py` class from **Alex Yatskov**'s [Mangle](https://github.com/FooSoft/mangle/) with subsequent [proDOOMman](https://github.com/proDOOMman/Mangle)'s and [Birua](https://github.com/Birua/Mangle)'s patches. 352 | - Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License. 353 | 354 | ## SAMPLE FILES CREATED BY KCC 355 | 356 | https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples 357 | 358 | Older links (dead): 359 | 360 | 361 | * [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi) 362 | * [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi) 363 | * [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi) 364 | * [Kindle](http://kcc.iosphe.re/Samples/Ubunchu!-K578.mobi) 365 | * [Kobo Aura](http://kcc.iosphe.re/Samples/Ubunchu-KoA.kepub.epub) 366 | * [Kobo Aura HD](http://kcc.iosphe.re/Samples/Ubunchu-KoAHD.kepub.epub) 367 | * [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub) 368 | * [Kobo Aura ONE](http://kcc.iosphe.re/Samples/Ubunchu-KoAO.kepub.epub) 369 | * [Kobo Forma](http://kcc.iosphe.re/Samples/Ubunchu-KoF.kepub.epub) 370 | 371 | ## PRIVACY 372 | **KCC** is initiating internet connections in two cases: 373 | * During startup - Version check. 374 | * When error occurs - Automatic reporting on Windows and macOS. 375 | 376 | ## KNOWN ISSUES 377 | Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues). 378 | 379 | ## COPYRIGHT 380 | Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu. 381 | **KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details. 382 | -------------------------------------------------------------------------------- /application-vnd.appimage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 30 | 32 | 36 | 40 | 41 | 51 | 53 | 57 | 61 | 62 | 72 | 74 | 78 | 82 | 83 | 94 | 96 | 100 | 104 | 105 | 116 | 118 | 122 | 126 | 127 | 137 | 139 | 143 | 147 | 148 | 158 | 160 | 164 | 168 | 169 | 179 | 181 | 185 | 189 | 190 | 198 | 200 | 204 | 208 | 212 | 213 | 214 | 233 | 235 | 236 | 238 | image/svg+xml 239 | 241 | 242 | 243 | 244 | 245 | 249 | 253 | 257 | 264 | 272 | 279 | 280 | 281 | 290 | 294 | 298 | 302 | 311 | 315 | 319 | 320 | 321 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: kcc 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.11 7 | - Pillow>=5.2.0 8 | - psutil>=5.9.5 9 | - python-slugify>=1.2.1 10 | - raven>=6.0.0 11 | - distro 12 | - natsort>=8.4.0 13 | - pip 14 | - pip: 15 | - mozjpeg-lossless-optimization>=1.1.2 16 | - pyside6>=6.5.1 17 | -------------------------------------------------------------------------------- /gen_ui_files.bat: -------------------------------------------------------------------------------- 1 | pyside6-uic gui/KCC.ui --from-imports > kindlecomicconverter/KCC_ui.py 2 | pyside6-uic gui/MetaEditor.ui --from-imports > kindlecomicconverter/KCC_ui_editor.py 3 | pyside6-rcc gui/KCC.qrc > kindlecomicconverter/KCC_rc.py 4 | -------------------------------------------------------------------------------- /gen_ui_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | pyside6-uic gui/KCC.ui --from-imports > kindlecomicconverter/KCC_ui.py 4 | pyside6-uic gui/MetaEditor.ui --from-imports > kindlecomicconverter/KCC_ui_editor.py 5 | pyside6-rcc gui/KCC.qrc > kindlecomicconverter/KCC_rc.py 6 | -------------------------------------------------------------------------------- /gui/KCC.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../icons/comic2ebook.png 4 | 5 | 6 | ../icons/Kobo.png 7 | ../icons/Other.png 8 | ../icons/Kindle.png 9 | ../icons/Rmk.png 10 | 11 | 12 | ../icons/CBZ.png 13 | ../icons/EPUB.png 14 | ../icons/MOBI.png 15 | ../icons/KFX.png 16 | 17 | 18 | ../icons/error.png 19 | ../icons/info.png 20 | ../icons/warning.png 21 | 22 | 23 | ../icons/wiki.png 24 | ../icons/editor.png 25 | ../icons/list_background.png 26 | ../icons/clear.png 27 | ../icons/convert.png 28 | ../icons/document_new.png 29 | ../icons/folder_new.png 30 | 31 | 32 | -------------------------------------------------------------------------------- /gui/MetaEditor.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | editorDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 260 11 | 12 | 13 | 14 | 15 | 400 16 | 260 17 | 18 | 19 | 20 | Metadata editor 21 | 22 | 23 | 24 | :/Icon/icons/comic2ebook.png:/Icon/icons/comic2ebook.png 25 | 26 | 27 | 28 | 5 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 36 | 37 | 0 38 | 39 | 40 | 0 41 | 42 | 43 | 0 44 | 45 | 46 | 47 | 48 | Series: 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Volume: 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Number: 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Writer: 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Penciller: 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Inker: 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | Colorist: 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 124 | 125 | 0 126 | 127 | 128 | 0 129 | 130 | 131 | 0 132 | 133 | 134 | 135 | 136 | 137 | 0 138 | 0 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 0 151 | 30 152 | 153 | 154 | 155 | Save 156 | 157 | 158 | 159 | :/Other/icons/convert.png:/Other/icons/convert.png 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 0 168 | 30 169 | 170 | 171 | 172 | Cancel 173 | 174 | 175 | 176 | :/Other/icons/clear.png:/Other/icons/clear.png 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/header.jpg -------------------------------------------------------------------------------- /icons/CBZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/CBZ.png -------------------------------------------------------------------------------- /icons/EPUB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/EPUB.png -------------------------------------------------------------------------------- /icons/KFX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/KFX.png -------------------------------------------------------------------------------- /icons/Kindle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Kindle.png -------------------------------------------------------------------------------- /icons/Kobo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Kobo.png -------------------------------------------------------------------------------- /icons/MOBI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/MOBI.png -------------------------------------------------------------------------------- /icons/Other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Other.png -------------------------------------------------------------------------------- /icons/Rmk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Rmk.png -------------------------------------------------------------------------------- /icons/Wizard-Small.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Wizard-Small.bmp -------------------------------------------------------------------------------- /icons/Wizard.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/Wizard.bmp -------------------------------------------------------------------------------- /icons/WizardOSX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/WizardOSX.png -------------------------------------------------------------------------------- /icons/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/clear.png -------------------------------------------------------------------------------- /icons/comic2ebook.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/comic2ebook.icns -------------------------------------------------------------------------------- /icons/comic2ebook.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/comic2ebook.ico -------------------------------------------------------------------------------- /icons/comic2ebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/comic2ebook.png -------------------------------------------------------------------------------- /icons/convert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/convert.png -------------------------------------------------------------------------------- /icons/document_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/document_new.png -------------------------------------------------------------------------------- /icons/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/editor.png -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/error.png -------------------------------------------------------------------------------- /icons/folder_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/folder_new.png -------------------------------------------------------------------------------- /icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/info.png -------------------------------------------------------------------------------- /icons/list_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/list_background.png -------------------------------------------------------------------------------- /icons/list_background.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/list_background.xcf -------------------------------------------------------------------------------- /icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/warning.png -------------------------------------------------------------------------------- /icons/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciromattia/kcc/51d0be4379b105cfab12d662fa97df71be965b3f/icons/wiki.png -------------------------------------------------------------------------------- /kcc-c2e.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 5 | # Copyright (c) 2013-2019 Pawel Jastrzebski 6 | # 7 | # Permission to use, copy, modify, and/or distribute this software for 8 | # any purpose with or without fee is hereby granted, provided that the 9 | # above copyright notice and this permission notice appear in all 10 | # copies. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 13 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 14 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 15 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 16 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 17 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 18 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | # PERFORMANCE OF THIS SOFTWARE. 20 | 21 | import sys 22 | 23 | from kcc import modify_path 24 | 25 | if sys.version_info < (3, 8, 0): 26 | print('ERROR: This is a Python 3.8+ script!') 27 | sys.exit(1) 28 | 29 | from multiprocessing import freeze_support, set_start_method 30 | from kindlecomicconverter.startup import startC2E 31 | 32 | if __name__ == "__main__": 33 | modify_path() 34 | set_start_method('spawn') 35 | freeze_support() 36 | startC2E() 37 | -------------------------------------------------------------------------------- /kcc-c2e.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['kcc-c2e.py'], 8 | pathex=['.'], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=['_cffi_backend'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | [], 28 | name='kcc-c2e', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=False, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=True, 36 | disable_windowed_traceback=False, 37 | target_arch=None, 38 | codesign_identity=None, 39 | entitlements_file=None , icon='icons\\comic2ebook.ico') 40 | -------------------------------------------------------------------------------- /kcc-c2p.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 5 | # Copyright (c) 2013-2019 Pawel Jastrzebski 6 | # 7 | # Permission to use, copy, modify, and/or distribute this software for 8 | # any purpose with or without fee is hereby granted, provided that the 9 | # above copyright notice and this permission notice appear in all 10 | # copies. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 13 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 14 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 15 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 16 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 17 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 18 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | # PERFORMANCE OF THIS SOFTWARE. 20 | 21 | import sys 22 | 23 | from kcc import modify_path 24 | 25 | if sys.version_info < (3, 8, 0): 26 | print('ERROR: This is a Python 3.8+ script!') 27 | sys.exit(1) 28 | 29 | from multiprocessing import freeze_support, set_start_method 30 | from kindlecomicconverter.startup import startC2P 31 | 32 | if __name__ == "__main__": 33 | modify_path() 34 | set_start_method('spawn') 35 | freeze_support() 36 | startC2P() 37 | -------------------------------------------------------------------------------- /kcc-c2p.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['kcc-c2p.py'], 8 | pathex=['.'], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=['_cffi_backend'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | [], 28 | name='kcc-c2p', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=False, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=True, 36 | disable_windowed_traceback=False, 37 | target_arch=None, 38 | codesign_identity=None, 39 | entitlements_file=None , icon='icons\\comic2ebook.ico') 40 | -------------------------------------------------------------------------------- /kcc.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Kindle Comic Converter", 3 | "icon": "icons/comic2ebook.icns", 4 | "background": "icons/WizardOSX.png", 5 | "icon-size": 160, 6 | "contents": [ 7 | { "x": 180, "y": 300, "type": "file", "path": "dist/Kindle Comic Converter.app" }, 8 | { "x": 520, "y": 300, "type": "link", "path": "/Applications" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /kcc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 5 | # Copyright (c) 2013-2019 Pawel Jastrzebski 6 | # 7 | # Permission to use, copy, modify, and/or distribute this software for 8 | # any purpose with or without fee is hereby granted, provided that the 9 | # above copyright notice and this permission notice appear in all 10 | # copies. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 13 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 14 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 15 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 16 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 17 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 18 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | # PERFORMANCE OF THIS SOFTWARE. 20 | 21 | import os 22 | import sys 23 | import platform 24 | 25 | from pathlib import Path 26 | 27 | if sys.version_info < (3, 8, 0): 28 | print('ERROR: This is a Python 3.8+ script!') 29 | sys.exit(1) 30 | 31 | def modify_path(): 32 | if platform.system() == 'Darwin': 33 | mac_paths = [ 34 | '/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS', 35 | '/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/', 36 | ] 37 | if getattr(sys, 'frozen', False): 38 | os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths + 39 | [ 40 | '/opt/homebrew/bin', 41 | '/usr/local/bin', 42 | '/usr/bin', 43 | '/bin', 44 | ] 45 | ) 46 | os.chdir(os.path.dirname(os.path.abspath(sys.executable))) 47 | else: 48 | os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths) 49 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 50 | 51 | elif platform.system() == 'Linux': 52 | if getattr(sys, 'frozen', False): 53 | os.environ['PATH'] += os.pathsep + os.pathsep.join( 54 | [ 55 | str(Path.home() / ".local" / "bin"), 56 | '/opt/homebrew/bin', 57 | '/usr/local/bin', 58 | '/usr/bin', 59 | '/bin', 60 | ] 61 | ) 62 | os.chdir(os.path.dirname(os.path.abspath(sys.executable))) 63 | else: 64 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 65 | 66 | elif platform.system() == 'Windows': 67 | win_paths = [ 68 | os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'), 69 | os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'), 70 | os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'), 71 | 'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin', 72 | 'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin', 73 | 'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin', 74 | 'C:\\Program Files\\7-Zip', 75 | 'D:\\Program Files\\7-Zip', 76 | 'E:\\Program Files\\7-Zip', 77 | ] 78 | if getattr(sys, 'frozen', False): 79 | os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths) 80 | os.chdir(os.path.dirname(os.path.abspath(sys.executable))) 81 | else: 82 | os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths) 83 | os.chdir(os.path.dirname(os.path.abspath(__file__))) 84 | 85 | 86 | from multiprocessing import freeze_support, set_start_method 87 | from kindlecomicconverter.startup import start 88 | 89 | if __name__ == "__main__": 90 | modify_path() 91 | set_start_method('spawn') 92 | freeze_support() 93 | start() 94 | 95 | -------------------------------------------------------------------------------- /kcc.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis(['kcc.py'], 8 | pathex=['.'], 9 | binaries=[], 10 | datas=[], 11 | hiddenimports=['_cffi_backend'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | 22 | exe = EXE(pyz, 23 | a.scripts, 24 | a.binaries, 25 | a.zipfiles, 26 | a.datas, 27 | [], 28 | name='kcc', 29 | debug=False, 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=False, 33 | upx_exclude=[], 34 | runtime_tmpdir=None, 35 | console=False, 36 | disable_windowed_traceback=False, 37 | target_arch=None, 38 | codesign_identity=None, 39 | entitlements_file=None , icon='icons\\comic2ebook.ico') 40 | -------------------------------------------------------------------------------- /kindlecomicconverter/KCC_ui_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'MetaEditor.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout, 19 | QLabel, QLineEdit, QPushButton, QSizePolicy, 20 | QVBoxLayout, QWidget) 21 | from . import KCC_rc 22 | 23 | class Ui_editorDialog(object): 24 | def setupUi(self, editorDialog): 25 | if not editorDialog.objectName(): 26 | editorDialog.setObjectName(u"editorDialog") 27 | editorDialog.resize(400, 260) 28 | editorDialog.setMinimumSize(QSize(400, 260)) 29 | icon = QIcon() 30 | icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) 31 | editorDialog.setWindowIcon(icon) 32 | self.verticalLayout = QVBoxLayout(editorDialog) 33 | self.verticalLayout.setObjectName(u"verticalLayout") 34 | self.verticalLayout.setContentsMargins(-1, -1, -1, 5) 35 | self.editorWidget = QWidget(editorDialog) 36 | self.editorWidget.setObjectName(u"editorWidget") 37 | self.gridLayout = QGridLayout(self.editorWidget) 38 | self.gridLayout.setObjectName(u"gridLayout") 39 | self.gridLayout.setContentsMargins(0, 0, 0, 0) 40 | self.label_1 = QLabel(self.editorWidget) 41 | self.label_1.setObjectName(u"label_1") 42 | 43 | self.gridLayout.addWidget(self.label_1, 0, 0, 1, 1) 44 | 45 | self.seriesLine = QLineEdit(self.editorWidget) 46 | self.seriesLine.setObjectName(u"seriesLine") 47 | 48 | self.gridLayout.addWidget(self.seriesLine, 0, 1, 1, 1) 49 | 50 | self.label_2 = QLabel(self.editorWidget) 51 | self.label_2.setObjectName(u"label_2") 52 | 53 | self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) 54 | 55 | self.volumeLine = QLineEdit(self.editorWidget) 56 | self.volumeLine.setObjectName(u"volumeLine") 57 | 58 | self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1) 59 | 60 | self.label_3 = QLabel(self.editorWidget) 61 | self.label_3.setObjectName(u"label_3") 62 | 63 | self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) 64 | 65 | self.numberLine = QLineEdit(self.editorWidget) 66 | self.numberLine.setObjectName(u"numberLine") 67 | 68 | self.gridLayout.addWidget(self.numberLine, 2, 1, 1, 1) 69 | 70 | self.label_4 = QLabel(self.editorWidget) 71 | self.label_4.setObjectName(u"label_4") 72 | 73 | self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1) 74 | 75 | self.writerLine = QLineEdit(self.editorWidget) 76 | self.writerLine.setObjectName(u"writerLine") 77 | 78 | self.gridLayout.addWidget(self.writerLine, 3, 1, 1, 1) 79 | 80 | self.label_5 = QLabel(self.editorWidget) 81 | self.label_5.setObjectName(u"label_5") 82 | 83 | self.gridLayout.addWidget(self.label_5, 4, 0, 1, 1) 84 | 85 | self.pencillerLine = QLineEdit(self.editorWidget) 86 | self.pencillerLine.setObjectName(u"pencillerLine") 87 | 88 | self.gridLayout.addWidget(self.pencillerLine, 4, 1, 1, 1) 89 | 90 | self.label_6 = QLabel(self.editorWidget) 91 | self.label_6.setObjectName(u"label_6") 92 | 93 | self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) 94 | 95 | self.inkerLine = QLineEdit(self.editorWidget) 96 | self.inkerLine.setObjectName(u"inkerLine") 97 | 98 | self.gridLayout.addWidget(self.inkerLine, 5, 1, 1, 1) 99 | 100 | self.label_7 = QLabel(self.editorWidget) 101 | self.label_7.setObjectName(u"label_7") 102 | 103 | self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1) 104 | 105 | self.coloristLine = QLineEdit(self.editorWidget) 106 | self.coloristLine.setObjectName(u"coloristLine") 107 | 108 | self.gridLayout.addWidget(self.coloristLine, 6, 1, 1, 1) 109 | 110 | 111 | self.verticalLayout.addWidget(self.editorWidget) 112 | 113 | self.optionWidget = QWidget(editorDialog) 114 | self.optionWidget.setObjectName(u"optionWidget") 115 | self.horizontalLayout = QHBoxLayout(self.optionWidget) 116 | self.horizontalLayout.setObjectName(u"horizontalLayout") 117 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 118 | self.statusLabel = QLabel(self.optionWidget) 119 | self.statusLabel.setObjectName(u"statusLabel") 120 | sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) 121 | sizePolicy.setHorizontalStretch(0) 122 | sizePolicy.setVerticalStretch(0) 123 | sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth()) 124 | self.statusLabel.setSizePolicy(sizePolicy) 125 | 126 | self.horizontalLayout.addWidget(self.statusLabel) 127 | 128 | self.okButton = QPushButton(self.optionWidget) 129 | self.okButton.setObjectName(u"okButton") 130 | self.okButton.setMinimumSize(QSize(0, 30)) 131 | icon1 = QIcon() 132 | icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) 133 | self.okButton.setIcon(icon1) 134 | 135 | self.horizontalLayout.addWidget(self.okButton) 136 | 137 | self.cancelButton = QPushButton(self.optionWidget) 138 | self.cancelButton.setObjectName(u"cancelButton") 139 | self.cancelButton.setMinimumSize(QSize(0, 30)) 140 | icon2 = QIcon() 141 | icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) 142 | self.cancelButton.setIcon(icon2) 143 | 144 | self.horizontalLayout.addWidget(self.cancelButton) 145 | 146 | 147 | self.verticalLayout.addWidget(self.optionWidget) 148 | 149 | 150 | self.retranslateUi(editorDialog) 151 | 152 | QMetaObject.connectSlotsByName(editorDialog) 153 | # setupUi 154 | 155 | def retranslateUi(self, editorDialog): 156 | editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None)) 157 | self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None)) 158 | self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None)) 159 | self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None)) 160 | self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None)) 161 | self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None)) 162 | self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None)) 163 | self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None)) 164 | self.statusLabel.setText("") 165 | self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None)) 166 | self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None)) 167 | # retranslateUi 168 | 169 | -------------------------------------------------------------------------------- /kindlecomicconverter/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '7.4.1' 2 | __license__ = 'ISC' 3 | __copyright__ = '2012-2022, Ciro Mattia Gonano , Pawel Jastrzebski , darodi' 4 | __docformat__ = 'restructuredtext en' 5 | -------------------------------------------------------------------------------- /kindlecomicconverter/comic2panel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 4 | # Copyright (c) 2013-2019 Pawel Jastrzebski 5 | # 6 | # Permission to use, copy, modify, and/or distribute this software for 7 | # any purpose with or without fee is hereby granted, provided that the 8 | # above copyright notice and this permission notice appear in all 9 | # copies. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 16 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | # PERFORMANCE OF THIS SOFTWARE. 19 | # 20 | 21 | import os 22 | import sys 23 | from argparse import ArgumentParser 24 | from shutil import rmtree, copytree, move 25 | from multiprocessing import Pool 26 | from PIL import Image, ImageChops, ImageOps, ImageDraw 27 | from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace 28 | 29 | 30 | def mergeDirectoryTick(output): 31 | if output: 32 | mergeWorkerOutput.append(output) 33 | mergeWorkerPool.terminate() 34 | if GUI: 35 | GUI.progressBarTick.emit('tick') 36 | if not GUI.conversionAlive: 37 | mergeWorkerPool.terminate() 38 | 39 | 40 | def mergeDirectory(work): 41 | try: 42 | directory = work[0] 43 | images = [] 44 | imagesValid = [] 45 | sizes = [] 46 | targetHeight = 0 47 | for root, _, files in walkLevel(directory, 0): 48 | for name in files: 49 | if getImageFileName(name) is not None: 50 | i = Image.open(os.path.join(root, name)) 51 | images.append([os.path.join(root, name), i.size[0], i.size[1]]) 52 | sizes.append(i.size[0]) 53 | if len(images) > 0: 54 | targetWidth = max(set(sizes), key=sizes.count) 55 | for i in images: 56 | targetHeight += i[2] 57 | imagesValid.append(i[0]) 58 | # Silently drop directories that contain too many images 59 | # 131072 = GIMP_MAX_IMAGE_SIZE / 4 60 | if targetHeight > 131072: 61 | return None 62 | result = Image.new('RGB', (targetWidth, targetHeight)) 63 | y = 0 64 | for i in imagesValid: 65 | img = Image.open(i).convert('RGB') 66 | if img.size[0] < targetWidth or img.size[0] > targetWidth: 67 | widthPercent = (targetWidth / float(img.size[0])) 68 | heightSize = int((float(img.size[1]) * float(widthPercent))) 69 | img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5)) 70 | result.paste(img, (0, y)) 71 | y += img.size[1] 72 | os.remove(i) 73 | savePath = os.path.split(imagesValid[0]) 74 | result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG') 75 | except Exception: 76 | return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) 77 | 78 | 79 | def detectSolid(img): 80 | return not ImageChops.invert(img).getbbox() or not img.getbbox() 81 | 82 | 83 | def splitImageTick(output): 84 | if output: 85 | splitWorkerOutput.append(output) 86 | splitWorkerPool.terminate() 87 | if GUI: 88 | GUI.progressBarTick.emit('tick') 89 | if not GUI.conversionAlive: 90 | splitWorkerPool.terminate() 91 | 92 | 93 | # noinspection PyUnboundLocalVariable 94 | def splitImage(work): 95 | try: 96 | path = work[0] 97 | name = work[1] 98 | opt = work[2] 99 | filePath = os.path.join(path, name) 100 | Image.warnings.simplefilter('error', Image.DecompressionBombWarning) 101 | Image.MAX_IMAGE_PIXELS = 1000000000 102 | imgOrg = Image.open(filePath).convert('RGB') 103 | imgProcess = Image.open(filePath).convert('1') 104 | widthImg, heightImg = imgOrg.size 105 | if heightImg > opt.height: 106 | if opt.debug: 107 | drawImg = Image.open(filePath).convert(mode='RGBA') 108 | draw = ImageDraw.Draw(drawImg) 109 | 110 | # Find panels 111 | yWork = 0 112 | panelDetected = False 113 | panels = [] 114 | while yWork < heightImg: 115 | tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4)) 116 | solid = detectSolid(tmpImg) 117 | if not solid and not panelDetected: 118 | panelDetected = True 119 | panelY1 = yWork - 2 120 | if heightImg - yWork <= 5: 121 | if not solid and panelDetected: 122 | panelY2 = heightImg 123 | panelDetected = False 124 | panels.append((panelY1, panelY2, panelY2 - panelY1)) 125 | if solid and panelDetected: 126 | panelDetected = False 127 | panelY2 = yWork + 6 128 | panels.append((panelY1, panelY2, panelY2 - panelY1)) 129 | yWork += 5 130 | 131 | # Split too big panels 132 | panelsProcessed = [] 133 | for panel in panels: 134 | if panel[2] <= opt.height * 1.5: 135 | panelsProcessed.append(panel) 136 | elif panel[2] < opt.height * 2: 137 | diff = panel[2] - opt.height 138 | panelsProcessed.append((panel[0], panel[1] - diff, opt.height)) 139 | panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height)) 140 | else: 141 | parts = round(panel[2] / opt.height) 142 | diff = panel[2] // parts 143 | for x in range(0, parts): 144 | panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff)) 145 | 146 | if opt.debug: 147 | for panel in panelsProcessed: 148 | draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255)) 149 | debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg) 150 | debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG') 151 | 152 | # Create virtual pages 153 | pages = [] 154 | currentPage = [] 155 | pageLeft = opt.height 156 | panelNumber = 0 157 | for panel in panelsProcessed: 158 | if pageLeft - panel[2] > 0: 159 | pageLeft -= panel[2] 160 | currentPage.append(panelNumber) 161 | panelNumber += 1 162 | else: 163 | if len(currentPage) > 0: 164 | pages.append(currentPage) 165 | pageLeft = opt.height - panel[2] 166 | currentPage = [panelNumber] 167 | panelNumber += 1 168 | if len(currentPage) > 0: 169 | pages.append(currentPage) 170 | 171 | # Create pages 172 | pageNumber = 1 173 | for page in pages: 174 | pageHeight = 0 175 | targetHeight = 0 176 | for panel in page: 177 | pageHeight += panelsProcessed[panel][2] 178 | if pageHeight > 15: 179 | newPage = Image.new('RGB', (widthImg, pageHeight)) 180 | for panel in page: 181 | panelImg = imgOrg.crop((0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1])) 182 | newPage.paste(panelImg, (0, targetHeight)) 183 | targetHeight += panelsProcessed[panel][2] 184 | newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber).zfill(4) + '.png'), 'PNG') 185 | pageNumber += 1 186 | os.remove(filePath) 187 | except Exception: 188 | return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) 189 | 190 | 191 | def main(argv=None, qtgui=None): 192 | global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput 193 | parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False) 194 | 195 | mandatory_options = parser.add_argument_group("MANDATORY") 196 | main_options = parser.add_argument_group("MAIN") 197 | other_options = parser.add_argument_group("OTHER") 198 | mandatory_options.add_argument("input", action="extend", nargs="*", default=None, 199 | help="Full path to comic folder(s) to be processed. Separate multiple inputs" 200 | " with spaces.") 201 | main_options.add_argument("-y", "--height", type=int, dest="height", default=0, 202 | help="Height of the target device screen") 203 | main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False, 204 | help="Overwrite source directory") 205 | main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False, 206 | help="Combine every directory into a single image before splitting") 207 | other_options.add_argument("-d", "--debug", action="store_true", dest="debug", default=False, 208 | help="Create debug file for every split image") 209 | other_options.add_argument("-h", "--help", action="help", 210 | help="Show this help message and exit") 211 | args = parser.parse_args(argv) 212 | if qtgui: 213 | GUI = qtgui 214 | else: 215 | GUI = None 216 | if not argv or args.input == []: 217 | parser.print_help() 218 | return 1 219 | if args.height > 0: 220 | for sourceDir in args.input: 221 | targetDir = sourceDir + "-Splitted" 222 | if os.path.isdir(sourceDir): 223 | rmtree(targetDir, True) 224 | copytree(sourceDir, targetDir) 225 | work = [] 226 | pagenumber = 1 227 | splitWorkerOutput = [] 228 | splitWorkerPool = Pool(maxtasksperchild=10) 229 | if args.merge: 230 | print("Merging images...") 231 | directoryNumer = 1 232 | mergeWork = [] 233 | mergeWorkerOutput = [] 234 | mergeWorkerPool = Pool(maxtasksperchild=10) 235 | mergeWork.append([targetDir]) 236 | for root, dirs, files in os.walk(targetDir, False): 237 | dirs, files = walkSort(dirs, files) 238 | for directory in dirs: 239 | directoryNumer += 1 240 | mergeWork.append([os.path.join(root, directory)]) 241 | if GUI: 242 | GUI.progressBarTick.emit('Combining images') 243 | GUI.progressBarTick.emit(str(directoryNumer)) 244 | for i in mergeWork: 245 | mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick) 246 | mergeWorkerPool.close() 247 | mergeWorkerPool.join() 248 | if GUI and not GUI.conversionAlive: 249 | rmtree(targetDir, True) 250 | raise UserWarning("Conversion interrupted.") 251 | if len(mergeWorkerOutput) > 0: 252 | rmtree(targetDir, True) 253 | raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], 254 | mergeWorkerOutput[0][1]) 255 | print("Splitting images...") 256 | for root, _, files in os.walk(targetDir, False): 257 | for name in files: 258 | if getImageFileName(name) is not None: 259 | pagenumber += 1 260 | work.append([root, name, args]) 261 | else: 262 | os.remove(os.path.join(root, name)) 263 | if GUI: 264 | GUI.progressBarTick.emit('Splitting images') 265 | GUI.progressBarTick.emit(str(pagenumber)) 266 | GUI.progressBarTick.emit('tick') 267 | if len(work) > 0: 268 | for i in work: 269 | splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick) 270 | splitWorkerPool.close() 271 | splitWorkerPool.join() 272 | if GUI and not GUI.conversionAlive: 273 | rmtree(targetDir, True) 274 | raise UserWarning("Conversion interrupted.") 275 | if len(splitWorkerOutput) > 0: 276 | rmtree(targetDir, True) 277 | raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0], 278 | splitWorkerOutput[0][1]) 279 | if args.inPlace: 280 | rmtree(sourceDir) 281 | move(targetDir, sourceDir) 282 | else: 283 | rmtree(targetDir, True) 284 | raise UserWarning("Source directory is empty.") 285 | else: 286 | raise UserWarning("Provided input is not a directory.") 287 | else: 288 | raise UserWarning("Target height is not set.") 289 | -------------------------------------------------------------------------------- /kindlecomicconverter/comicarchive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 4 | # Copyright (c) 2013-2019 Pawel Jastrzebski 5 | # 6 | # Permission to use, copy, modify, and/or distribute this software for 7 | # any purpose with or without fee is hereby granted, provided that the 8 | # above copyright notice and this permission notice appear in all 9 | # copies. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 16 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | # PERFORMANCE OF THIS SOFTWARE. 19 | # 20 | 21 | from functools import cached_property 22 | import os 23 | import platform 24 | import distro 25 | from subprocess import STDOUT, PIPE, CalledProcessError 26 | from xml.dom.minidom import parseString 27 | from xml.parsers.expat import ExpatError 28 | from .shared import subprocess_run 29 | 30 | EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.' 31 | 32 | 33 | class ComicArchive: 34 | def __init__(self, filepath): 35 | self.filepath = filepath 36 | if not os.path.isfile(self.filepath): 37 | raise OSError('File not found.') 38 | 39 | @cached_property 40 | def type(self): 41 | extraction_commands = [ 42 | ['7z', 'l', '-y', '-p1', self.filepath], 43 | ] 44 | 45 | if distro.id() == 'fedora' or distro.like() == 'fedora': 46 | extraction_commands.append( 47 | ['unrar', 'l', '-y', '-p1', self.filepath], 48 | ) 49 | 50 | for cmd in extraction_commands: 51 | try: 52 | process = subprocess_run(cmd, capture_output=True, check=True) 53 | for line in process.stdout.splitlines(): 54 | if b'Type =' in line: 55 | return line.rstrip().decode().split(' = ')[1].upper() 56 | except FileNotFoundError: 57 | pass 58 | except CalledProcessError: 59 | pass 60 | 61 | raise OSError(EXTRACTION_ERROR) 62 | 63 | def extract(self, targetdir): 64 | if not os.path.isdir(targetdir): 65 | raise OSError('Target directory doesn\'t exist.') 66 | 67 | missing = [] 68 | 69 | extraction_commands = [ 70 | ['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir], 71 | ['7z', 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath], 72 | ] 73 | 74 | if platform.system() == 'Darwin': 75 | extraction_commands.append( 76 | ['unar', self.filepath, '-f', '-o', targetdir] 77 | ) 78 | 79 | extraction_commands.reverse() 80 | 81 | if distro.id() == 'fedora' or distro.like() == 'fedora': 82 | extraction_commands.append( 83 | ['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir] 84 | ) 85 | 86 | for cmd in extraction_commands: 87 | try: 88 | subprocess_run(cmd, capture_output=True, check=True) 89 | return targetdir 90 | except FileNotFoundError: 91 | missing.append(cmd[0]) 92 | except CalledProcessError: 93 | pass 94 | 95 | if missing: 96 | raise OSError(f'Extraction failed, install specialized extraction software. ') 97 | else: 98 | raise OSError(EXTRACTION_ERROR) 99 | 100 | def addFile(self, sourcefile): 101 | if self.type in ['RAR', 'RAR5']: 102 | raise NotImplementedError 103 | process = subprocess_run(['7z', 'a', '-y', self.filepath, sourcefile], 104 | stdout=PIPE, stderr=STDOUT) 105 | if process.returncode != 0: 106 | raise OSError('Failed to add the file.') 107 | 108 | def extractMetadata(self): 109 | process = subprocess_run(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'], 110 | stdout=PIPE, stderr=STDOUT) 111 | if process.returncode != 0: 112 | raise OSError(EXTRACTION_ERROR) 113 | try: 114 | return parseString(process.stdout) 115 | except ExpatError: 116 | return None 117 | -------------------------------------------------------------------------------- /kindlecomicconverter/common_crop.py: -------------------------------------------------------------------------------- 1 | def threshold_from_power(power): 2 | return 240-(power*64) 3 | 4 | 5 | ''' 6 | Groups close values together 7 | ''' 8 | def group_close_values(vals, max_dist_tolerated): 9 | groups = [] 10 | 11 | group_start = -1 12 | group_end = 0 13 | for i in range(len(vals)): 14 | dist = vals[i] - group_end 15 | if group_start == -1: 16 | group_start = vals[i] 17 | group_end = vals[i] 18 | elif dist <= max_dist_tolerated: 19 | group_end = vals[i] 20 | else: 21 | groups.append((group_start, group_end)) 22 | group_start = -1 23 | group_end = -1 24 | 25 | if group_start != -1: 26 | groups.append((group_start, group_end)) 27 | 28 | return groups 29 | -------------------------------------------------------------------------------- /kindlecomicconverter/dualmetafix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Based on initial version of DualMetaFix. Copyright (C) 2013 Kevin Hendricks 4 | # Changes for KCC Copyright (C) 2014-2019 Pawel Jastrzebski 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | import struct 20 | import mmap 21 | import shutil 22 | 23 | 24 | class DualMetaFixException(Exception): 25 | pass 26 | 27 | 28 | # palm database offset constants 29 | number_of_pdb_records = 76 30 | first_pdb_record = 78 31 | 32 | # important rec0 offsets 33 | mobi_header_base = 16 34 | mobi_header_length = 20 35 | mobi_version = 36 36 | title_offset = 84 37 | 38 | 39 | def getint(data, ofs, sz='L'): 40 | i, = struct.unpack_from('>' + sz, data, ofs) 41 | return i 42 | 43 | 44 | def writeint(data, ofs, n, slen='L'): 45 | if slen == 'L': 46 | return data[:ofs] + struct.pack('>L', n) + data[ofs + 4:] 47 | else: 48 | return data[:ofs] + struct.pack('>H', n) + data[ofs + 2:] 49 | 50 | 51 | def getsecaddr(datain, secno): 52 | nsec = getint(datain, number_of_pdb_records, 'H') 53 | if (secno < 0) | (secno >= nsec): 54 | emsg = 'requested section number %d out of range (nsec=%d)' % (secno, nsec) 55 | raise DualMetaFixException(emsg) 56 | secstart = getint(datain, first_pdb_record + secno * 8) 57 | if secno == nsec - 1: 58 | secend = len(datain) 59 | else: 60 | secend = getint(datain, first_pdb_record + (secno + 1) * 8) 61 | return secstart, secend 62 | 63 | 64 | def readsection(datain, secno): 65 | secstart, secend = getsecaddr(datain, secno) 66 | return datain[secstart:secend] 67 | 68 | 69 | # overwrite section - must be exact same length as original 70 | def replacesection(datain, secno, secdata): 71 | secstart, secend = getsecaddr(datain, secno) 72 | seclen = secend - secstart 73 | if len(secdata) != seclen: 74 | raise DualMetaFixException('section length change in replacesection') 75 | datain[secstart:secstart + seclen] = secdata 76 | 77 | 78 | def get_exth_params(rec0): 79 | ebase = mobi_header_base + getint(rec0, mobi_header_length) 80 | if rec0[ebase:ebase + 4] != b'EXTH': 81 | raise DualMetaFixException('EXTH tag not found where expected') 82 | elen = getint(rec0, ebase + 4) 83 | enum = getint(rec0, ebase + 8) 84 | rlen = len(rec0) 85 | return ebase, elen, enum, rlen 86 | 87 | 88 | def add_exth(rec0, exth_num, exth_bytes): 89 | ebase, elen, enum, rlen = get_exth_params(rec0) 90 | newrecsize = 8 + len(exth_bytes) 91 | newrec0 = rec0[0:ebase + 4] + struct.pack('>L', elen + newrecsize) + struct.pack('>L', enum + 1) + \ 92 | struct.pack('>L', exth_num) + struct.pack('>L', newrecsize) + exth_bytes + rec0[ebase + 12:] 93 | newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) + newrecsize) 94 | # keep constant record length by removing newrecsize null bytes from end 95 | sectail = newrec0[-newrecsize:] 96 | if sectail != b'\0' * newrecsize: 97 | raise DualMetaFixException('add_exth: trimmed non-null bytes at end of section') 98 | newrec0 = newrec0[0:rlen] 99 | return newrec0 100 | 101 | 102 | def read_exth(rec0, exth_num): 103 | exth_values = [] 104 | ebase, elen, enum, rlen = get_exth_params(rec0) 105 | ebase += 12 106 | while enum > 0: 107 | exth_id = getint(rec0, ebase) 108 | if exth_id == exth_num: 109 | # We might have multiple exths, so build a list. 110 | exth_values.append(rec0[ebase + 8:ebase + getint(rec0, ebase + 4)]) 111 | enum -= 1 112 | ebase = ebase + getint(rec0, ebase + 4) 113 | return exth_values 114 | 115 | 116 | def del_exth(rec0, exth_num): 117 | ebase, elen, enum, rlen = get_exth_params(rec0) 118 | ebase_idx = ebase + 12 119 | enum_idx = 0 120 | while enum_idx < enum: 121 | exth_id = getint(rec0, ebase_idx) 122 | exth_size = getint(rec0, ebase_idx + 4) 123 | if exth_id == exth_num: 124 | newrec0 = rec0 125 | newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) - exth_size) 126 | newrec0 = newrec0[:ebase_idx] + newrec0[ebase_idx + exth_size:] 127 | newrec0 = newrec0[0:ebase + 4] + struct.pack('>L', elen - exth_size) + \ 128 | struct.pack('>L', enum - 1) + newrec0[ebase + 12:] 129 | newrec0 += b'\0' * exth_size 130 | if rlen != len(newrec0): 131 | raise DualMetaFixException('del_exth: incorrect section size change') 132 | return newrec0 133 | enum_idx += 1 134 | ebase_idx = ebase_idx + exth_size 135 | return rec0 136 | 137 | 138 | class DualMobiMetaFix: 139 | def __init__(self, infile, outfile, asin, is_pdoc): 140 | cdetype = b'EBOK' 141 | if is_pdoc: 142 | cdetype = b'PDOC' 143 | 144 | shutil.copyfile(infile, outfile) 145 | f = open(outfile, "r+b") 146 | self.datain = mmap.mmap(f.fileno(), 0) 147 | self.datain_rec0 = readsection(self.datain, 0) 148 | 149 | # in the first mobi header 150 | # add 501 to "EBOK", add 113 as asin 151 | rec0 = self.datain_rec0 152 | rec0 = del_exth(rec0, 501) 153 | rec0 = del_exth(rec0, 113) 154 | rec0 = add_exth(rec0, 501, cdetype) 155 | rec0 = add_exth(rec0, 113, asin) 156 | replacesection(self.datain, 0, rec0) 157 | 158 | ver = getint(self.datain_rec0, mobi_version) 159 | self.combo = (ver != 8) 160 | if not self.combo: 161 | return 162 | 163 | exth121 = read_exth(self.datain_rec0, 121) 164 | if len(exth121) == 0: 165 | self.combo = False 166 | return 167 | else: 168 | # only pay attention to first exth121 169 | # (there should only be one) 170 | datain_kf8, = struct.unpack_from('>L', exth121[0], 0) 171 | if datain_kf8 == 0xffffffff: 172 | self.combo = False 173 | return 174 | self.datain_kfrec0 = readsection(self.datain, datain_kf8) 175 | 176 | # in the second header 177 | # add 501 to "EBOK", add 113 as asin 178 | rec0 = self.datain_kfrec0 179 | rec0 = del_exth(rec0, 501) 180 | rec0 = del_exth(rec0, 113) 181 | rec0 = add_exth(rec0, 501, cdetype) 182 | rec0 = add_exth(rec0, 113, asin) 183 | replacesection(self.datain, datain_kf8, rec0) 184 | 185 | self.datain.flush() 186 | self.datain.close() 187 | -------------------------------------------------------------------------------- /kindlecomicconverter/image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2010 Alex Yatskov 4 | # Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov 5 | # Copyright (c) 2016 Alberto Planas 6 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 7 | # Copyright (c) 2013-2019 Pawel Jastrzebski 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | import io 22 | import os 23 | from pathlib import Path 24 | import mozjpeg_lossless_optimization 25 | from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter 26 | from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin 27 | from .inter_panel_crop_alg import crop_empty_inter_panel 28 | 29 | AUTO_CROP_THRESHOLD = 0.015 30 | 31 | 32 | class ProfileData: 33 | def __init__(self): 34 | pass 35 | 36 | Palette4 = [ 37 | 0x00, 0x00, 0x00, 38 | 0x55, 0x55, 0x55, 39 | 0xaa, 0xaa, 0xaa, 40 | 0xff, 0xff, 0xff 41 | ] 42 | 43 | Palette15 = [ 44 | 0x00, 0x00, 0x00, 45 | 0x11, 0x11, 0x11, 46 | 0x22, 0x22, 0x22, 47 | 0x33, 0x33, 0x33, 48 | 0x44, 0x44, 0x44, 49 | 0x55, 0x55, 0x55, 50 | 0x66, 0x66, 0x66, 51 | 0x77, 0x77, 0x77, 52 | 0x88, 0x88, 0x88, 53 | 0x99, 0x99, 0x99, 54 | 0xaa, 0xaa, 0xaa, 55 | 0xbb, 0xbb, 0xbb, 56 | 0xcc, 0xcc, 0xcc, 57 | 0xdd, 0xdd, 0xdd, 58 | 0xff, 0xff, 0xff, 59 | ] 60 | 61 | Palette16 = [ 62 | 0x00, 0x00, 0x00, 63 | 0x11, 0x11, 0x11, 64 | 0x22, 0x22, 0x22, 65 | 0x33, 0x33, 0x33, 66 | 0x44, 0x44, 0x44, 67 | 0x55, 0x55, 0x55, 68 | 0x66, 0x66, 0x66, 69 | 0x77, 0x77, 0x77, 70 | 0x88, 0x88, 0x88, 71 | 0x99, 0x99, 0x99, 72 | 0xaa, 0xaa, 0xaa, 73 | 0xbb, 0xbb, 0xbb, 74 | 0xcc, 0xcc, 0xcc, 75 | 0xdd, 0xdd, 0xdd, 76 | 0xee, 0xee, 0xee, 77 | 0xff, 0xff, 0xff, 78 | ] 79 | 80 | PalleteNull = [ 81 | ] 82 | 83 | ProfilesKindleEBOK = { 84 | 'K1': ("Kindle 1", (600, 670), Palette4, 1.8), 85 | 'K2': ("Kindle 2", (600, 670), Palette15, 1.8), 86 | 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8), 87 | 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8), 88 | 'K578': ("Kindle", (600, 800), Palette16, 1.8), 89 | 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8), 90 | 'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8), 91 | } 92 | 93 | ProfilesKindlePDOC = { 94 | 'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8), 95 | 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8), 96 | 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8), 97 | 'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8), 98 | } 99 | 100 | ProfilesKindle = { 101 | **ProfilesKindleEBOK, 102 | **ProfilesKindlePDOC 103 | } 104 | 105 | ProfilesKobo = { 106 | 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8), 107 | 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8), 108 | 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8), 109 | 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8), 110 | 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8), 111 | 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8), 112 | 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8), 113 | 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8), 114 | 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8), 115 | 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8), 116 | 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8), 117 | 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8), 118 | 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8), 119 | 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8), 120 | 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8), 121 | } 122 | 123 | ProfilesRemarkable = { 124 | 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8), 125 | 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8), 126 | 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8), 127 | } 128 | 129 | Profiles = { 130 | **ProfilesKindle, 131 | **ProfilesKobo, 132 | **ProfilesRemarkable, 133 | 'OTHER': ("Other", (0, 0), Palette16, 1.8), 134 | } 135 | 136 | 137 | class ComicPageParser: 138 | def __init__(self, source, options): 139 | Image.MAX_IMAGE_PIXELS = int(2048 * 2048 * 2048 // 4 // 3) 140 | self.opt = options 141 | self.source = source 142 | self.size = self.opt.profileData[1] 143 | self.payload = [] 144 | 145 | # Detect corruption in source image, let caller catch any exceptions triggered. 146 | srcImgPath = os.path.join(source[0], source[1]) 147 | self.image = Image.open(srcImgPath) 148 | self.image.verify() 149 | self.image = Image.open(srcImgPath).convert('RGB') 150 | 151 | self.color = self.colorCheck() 152 | self.fill = self.fillCheck() 153 | # backwards compatibility for Pillow >9.1.0 154 | if not hasattr(Image, 'Resampling'): 155 | Image.Resampling = Image 156 | self.splitCheck() 157 | 158 | def getImageHistogram(self, image): 159 | histogram = image.histogram() 160 | if histogram[0] == 0: 161 | return -1 162 | elif histogram[255] == 0: 163 | return 1 164 | else: 165 | return 0 166 | 167 | def splitCheck(self): 168 | width, height = self.image.size 169 | dstwidth, dstheight = self.size 170 | if self.opt.maximizestrips: 171 | leftbox = (0, 0, int(width / 2), height) 172 | rightbox = (int(width / 2), 0, width, height) 173 | if self.opt.righttoleft: 174 | pageone = self.image.crop(rightbox) 175 | pagetwo = self.image.crop(leftbox) 176 | else: 177 | pageone = self.image.crop(leftbox) 178 | pagetwo = self.image.crop(rightbox) 179 | new_image = Image.new("RGB", (int(width / 2), int(height*2))) 180 | new_image.paste(pageone, (0, 0)) 181 | new_image.paste(pagetwo, (0, height)) 182 | self.payload.append(['N', self.source, new_image, self.color, self.fill]) 183 | elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \ 184 | and not self.opt.webtoon and self.opt.splitter == 1: 185 | spread = self.image 186 | if not self.opt.norotate: 187 | spread = spread.rotate(90, Image.Resampling.BICUBIC, True) 188 | self.payload.append(['R', self.source, spread, self.color, self.fill]) 189 | elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon: 190 | if self.opt.splitter != 1: 191 | if width > height: 192 | leftbox = (0, 0, int(width / 2), height) 193 | rightbox = (int(width / 2), 0, width, height) 194 | else: 195 | leftbox = (0, 0, width, int(height / 2)) 196 | rightbox = (0, int(height / 2), width, height) 197 | if self.opt.righttoleft: 198 | pageone = self.image.crop(rightbox) 199 | pagetwo = self.image.crop(leftbox) 200 | else: 201 | pageone = self.image.crop(leftbox) 202 | pagetwo = self.image.crop(rightbox) 203 | self.payload.append(['S1', self.source, pageone, self.color, self.fill]) 204 | self.payload.append(['S2', self.source, pagetwo, self.color, self.fill]) 205 | if self.opt.splitter > 0: 206 | spread = self.image 207 | if not self.opt.norotate: 208 | spread = spread.rotate(90, Image.Resampling.BICUBIC, True) 209 | self.payload.append(['R', self.source, spread, 210 | self.color, self.fill]) 211 | else: 212 | self.payload.append(['N', self.source, self.image, self.color, self.fill]) 213 | 214 | def colorCheck(self): 215 | if self.opt.webtoon: 216 | return True 217 | else: 218 | img = self.image.copy() 219 | bands = img.getbands() 220 | if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'): 221 | thumb = img.resize((40, 40)) 222 | SSE, bias = 0, [0, 0, 0] 223 | bias = ImageStat.Stat(thumb).mean[:3] 224 | bias = [b - sum(bias) / 3 for b in bias] 225 | for pixel in thumb.getdata(): 226 | mu = sum(pixel) / 3 227 | SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2]) 228 | MSE = float(SSE) / (40 * 40) 229 | if MSE > 22: 230 | return True 231 | else: 232 | return False 233 | else: 234 | return False 235 | 236 | def fillCheck(self): 237 | if self.opt.bordersColor: 238 | return self.opt.bordersColor 239 | else: 240 | bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1') 241 | imageBoxA = bw.getbbox() 242 | imageBoxB = ImageChops.invert(bw).getbbox() 243 | if imageBoxA is None or imageBoxB is None: 244 | surfaceB, surfaceW = 0, 0 245 | diff = 0 246 | else: 247 | surfaceB = (imageBoxA[2] - imageBoxA[0]) * (imageBoxA[3] - imageBoxA[1]) 248 | surfaceW = (imageBoxB[2] - imageBoxB[0]) * (imageBoxB[3] - imageBoxB[1]) 249 | diff = ((max(surfaceB, surfaceW) - min(surfaceB, surfaceW)) / min(surfaceB, surfaceW)) * 100 250 | if diff > 0.5: 251 | if surfaceW < surfaceB: 252 | return 'white' 253 | elif surfaceW > surfaceB: 254 | return 'black' 255 | else: 256 | fill = 0 257 | startY = 0 258 | while startY < bw.size[1]: 259 | if startY + 5 > bw.size[1]: 260 | startY = bw.size[1] - 5 261 | fill += self.getImageHistogram(bw.crop((0, startY, bw.size[0], startY + 5))) 262 | startY += 5 263 | startX = 0 264 | while startX < bw.size[0]: 265 | if startX + 5 > bw.size[0]: 266 | startX = bw.size[0] - 5 267 | fill += self.getImageHistogram(bw.crop((startX, 0, startX + 5, bw.size[1]))) 268 | startX += 5 269 | if fill > 0: 270 | return 'black' 271 | else: 272 | return 'white' 273 | 274 | 275 | class ComicPage: 276 | def __init__(self, options, mode, path, image, color, fill): 277 | self.opt = options 278 | _, self.size, self.palette, self.gamma = self.opt.profileData 279 | if self.opt.hq: 280 | self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5)) 281 | self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB')) 282 | self.image = image 283 | self.color = color 284 | self.fill = fill 285 | self.rotated = False 286 | self.orgPath = os.path.join(path[0], path[1]) 287 | if 'N' in mode: 288 | self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc' 289 | elif 'R' in mode: 290 | self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-a' 291 | if not options.norotate: 292 | self.rotated = True 293 | elif 'S1' in mode: 294 | self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-b' 295 | elif 'S2' in mode: 296 | self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-c' 297 | # backwards compatibility for Pillow >9.1.0 298 | if not hasattr(Image, 'Resampling'): 299 | Image.Resampling = Image 300 | 301 | def saveToDir(self): 302 | try: 303 | flags = [] 304 | if not self.opt.forcecolor and not self.opt.forcepng: 305 | self.image = self.image.convert('L') 306 | if self.rotated: 307 | flags.append('Rotated') 308 | if self.fill != 'white': 309 | flags.append('BlackBackground') 310 | if self.opt.forcepng: 311 | self.image.info["transparency"] = None 312 | self.targetPath += '.png' 313 | self.image.save(self.targetPath, 'PNG', optimize=1) 314 | else: 315 | self.targetPath += '.jpg' 316 | if self.opt.mozjpeg: 317 | with io.BytesIO() as output: 318 | self.image.save(output, format="JPEG", optimize=1, quality=85) 319 | input_jpeg_bytes = output.getvalue() 320 | output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes) 321 | with open(self.targetPath, "wb") as output_jpeg_file: 322 | output_jpeg_file.write(output_jpeg_bytes) 323 | else: 324 | self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85) 325 | if os.path.isfile(self.orgPath): 326 | os.remove(self.orgPath) 327 | return [Path(self.targetPath).name, flags] 328 | except IOError as err: 329 | raise RuntimeError('Cannot save image. ' + str(err)) 330 | 331 | def autocontrastImage(self): 332 | gamma = self.opt.gamma 333 | if gamma < 0.1: 334 | gamma = self.gamma 335 | if self.gamma != 1.0 and self.color: 336 | gamma = 1.0 337 | if gamma == 1.0: 338 | self.image = ImageOps.autocontrast(self.image) 339 | else: 340 | self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))) 341 | 342 | def quantizeImage(self): 343 | colors = len(self.palette) // 3 344 | if colors < 256: 345 | self.palette += self.palette[:3] * (256 - colors) 346 | palImg = Image.new('P', (1, 1)) 347 | palImg.putpalette(self.palette) 348 | self.image = self.image.convert('L') 349 | self.image = self.image.convert('RGB') 350 | # Quantize is deprecated but new function call it internally anyway... 351 | self.image = self.image.quantize(palette=palImg) 352 | 353 | def optimizeForDisplay(self, reducerainbow): 354 | # Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array 355 | if reducerainbow and not self.color: 356 | unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100) 357 | self.image = self.image.filter(unsharpFilter) 358 | self.image = self.image.filter(ImageFilter.BoxBlur(1.0)) 359 | self.image = self.image.filter(unsharpFilter) 360 | 361 | def resizeImage(self): 362 | # kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub 363 | if self.kindle_scribe_azw3: 364 | self.size = (1440, 1920) 365 | ratio_device = float(self.size[1]) / float(self.size[0]) 366 | ratio_image = float(self.image.size[1]) / float(self.image.size[0]) 367 | method = self.resize_method() 368 | if self.opt.stretch: 369 | self.image = self.image.resize(self.size, method) 370 | elif method == Image.Resampling.BICUBIC and not self.opt.upscale: 371 | pass 372 | else: # if image bigger than device resolution or smaller with upscaling 373 | if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD: 374 | self.image = ImageOps.fit(self.image, self.size, method=method) 375 | elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders: 376 | self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill) 377 | else: 378 | if self.kindle_scribe_azw3: 379 | self.size = (1860, 1920) 380 | self.image = ImageOps.contain(self.image, self.size, method=method) 381 | 382 | def resize_method(self): 383 | if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]: 384 | return Image.Resampling.BICUBIC 385 | else: 386 | return Image.Resampling.LANCZOS 387 | 388 | def maybeCrop(self, box, minimum): 389 | w, h = self.image.size 390 | left, upper, right, lower = box 391 | if self.opt.preservemargin: 392 | ratio = 1 - self.opt.preservemargin / 100 393 | box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio) 394 | box_area = (box[2] - box[0]) * (box[3] - box[1]) 395 | image_area = self.image.size[0] * self.image.size[1] 396 | if (box_area / image_area) >= minimum: 397 | self.image = self.image.crop(box) 398 | 399 | def cropPageNumber(self, power, minimum): 400 | bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill) 401 | 402 | if bbox: 403 | self.maybeCrop(bbox, minimum) 404 | 405 | def cropMargin(self, power, minimum): 406 | bbox = get_bbox_crop_margin(self.image, power, self.fill) 407 | 408 | if bbox: 409 | self.maybeCrop(bbox, minimum) 410 | 411 | def cropInterPanelEmptySections(self, direction): 412 | self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill) 413 | 414 | class Cover: 415 | def __init__(self, source, target, opt, tomeid): 416 | self.options = opt 417 | self.source = source 418 | self.target = target 419 | if tomeid == 0: 420 | self.tomeid = 1 421 | else: 422 | self.tomeid = tomeid 423 | self.image = Image.open(source) 424 | # backwards compatibility for Pillow >9.1.0 425 | if not hasattr(Image, 'Resampling'): 426 | Image.Resampling = Image 427 | self.process() 428 | 429 | def process(self): 430 | self.image = self.image.convert('RGB') 431 | self.image = ImageOps.autocontrast(self.image) 432 | if not self.options.forcecolor: 433 | self.image = self.image.convert('L') 434 | w, h = self.image.size 435 | if w / h > 2: 436 | if self.options.righttoleft: 437 | self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h)) 438 | else: 439 | self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h)) 440 | elif w / h > 1.3: 441 | if self.options.righttoleft: 442 | self.image = self.image.crop((0, 0, w/2 - w * 0.03, h)) 443 | else: 444 | self.image = self.image.crop((w/2 + w * 0.03, 0, w, h)) 445 | self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS) 446 | self.save() 447 | 448 | def save(self): 449 | try: 450 | self.image.save(self.target, "JPEG", optimize=1, quality=85) 451 | except IOError: 452 | raise RuntimeError('Failed to save cover.') 453 | 454 | def saveToKindle(self, kindle, asin): 455 | self.image = self.image.resize((300, 470), Image.Resampling.LANCZOS) 456 | try: 457 | self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails', 458 | 'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85) 459 | except IOError: 460 | raise RuntimeError('Failed to upload cover.') 461 | -------------------------------------------------------------------------------- /kindlecomicconverter/inter_panel_crop_alg.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageFilter, ImageOps 2 | import numpy as np 3 | from typing import Literal 4 | from .common_crop import threshold_from_power, group_close_values 5 | 6 | 7 | ''' 8 | Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins). 9 | 10 | Parameters: 11 | img (PIL image): A PIL image. 12 | direction (horizontal or vertical or both): To crop rows (horizontal), cols (vertical) or both. 13 | keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance). 14 | background_color (string): 'white' for white background, anything else for black. 15 | Returns: 16 | img (PIL image): A PIL image after cropping empty sections. 17 | ''' 18 | def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "both"], keep=0.04, background_color='white'): 19 | img_temp = img 20 | 21 | if img.mode != 'L': 22 | img_temp = ImageOps.grayscale(img) 23 | 24 | if background_color != 'white': 25 | img_temp = ImageOps.invert(img) 26 | 27 | img_mat = np.array(img) 28 | 29 | power = 1 30 | img_temp = ImageOps.autocontrast(img_temp, 1).filter(ImageFilter.BoxBlur(1)) 31 | img_temp = img_temp.point(lambda p: 255 if p <= threshold_from_power(power) else 0) 32 | 33 | if direction in ["horizontal", "both"]: 34 | rows_idx_to_remove = empty_sections(img_temp, keep, horizontal=True) 35 | img_mat = np.delete(img_mat, rows_idx_to_remove, 0) 36 | 37 | if direction in ["vertical", "both"]: 38 | cols_idx_to_remove = empty_sections(img_temp, keep, horizontal=False) 39 | img_mat = np.delete(img_mat, cols_idx_to_remove, 1) 40 | 41 | return Image.fromarray(img_mat) 42 | 43 | 44 | ''' 45 | Finds empty sections (excluding near borders). 46 | 47 | Parameters: 48 | img (PIL image): A PIL image. 49 | keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance). 50 | horizontal (boolean): True to find empty rows, False to find empty columns. 51 | Returns: 52 | Itertable (list or NumPy array): indices of rows or columns to remove. 53 | ''' 54 | def empty_sections(img, keep, horizontal=True): 55 | axis = 1 if horizontal else 0 56 | 57 | img_mat = np.array(img) 58 | img_mat_max = np.max(img_mat, axis=axis) 59 | img_mat_empty_idx = np.where(img_mat_max == 0)[0] 60 | 61 | empty_sections = group_close_values(img_mat_empty_idx, 1) 62 | sections_to_remove = [] 63 | for section in empty_sections: 64 | if section[1] < img.size[1] * 0.99 and section[0] > img.size[1] * 0.01: # if not near borders 65 | sections_to_remove.append(section) 66 | 67 | if len(sections_to_remove) != 0: 68 | sections_to_remove_after_keep = [(int(x1+(keep/2)*(x2-x1)), int(x2-(keep/2)*(x2-x1))) for x1,x2 in sections_to_remove] 69 | idx_to_remove = np.concatenate([np.arange(x1, x2) for x1,x2 in sections_to_remove_after_keep]) 70 | 71 | return idx_to_remove 72 | 73 | return [] 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /kindlecomicconverter/kindle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2013-2019 Pawel Jastrzebski 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for 6 | # any purpose with or without fee is hereby granted, provided that the 7 | # above copyright notice and this permission notice appear in all 8 | # copies. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 11 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 12 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 13 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 14 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 15 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | # PERFORMANCE OF THIS SOFTWARE. 18 | 19 | import os.path 20 | import psutil 21 | 22 | from . import image 23 | 24 | 25 | class Kindle: 26 | def __init__(self, profile): 27 | self.profile = profile 28 | self.path = self.findDevice() 29 | if self.path: 30 | self.coverSupport = self.checkThumbnails() 31 | else: 32 | self.coverSupport = False 33 | 34 | def findDevice(self): 35 | if self.profile in image.ProfileData.ProfilesKindlePDOC.keys(): 36 | return False 37 | for drive in reversed(psutil.disk_partitions(False)): 38 | if (drive[2] == 'FAT32' and drive[3] == 'rw,removable') or \ 39 | (drive[2] in ('vfat', 'msdos', 'FAT', 'apfs') and 'rw' in drive[3]): 40 | if os.path.isdir(os.path.join(drive[1], 'system')) and \ 41 | os.path.isdir(os.path.join(drive[1], 'documents')): 42 | return drive[1] 43 | return False 44 | 45 | def checkThumbnails(self): 46 | if os.path.isdir(os.path.join(self.path, 'system', 'thumbnails')): 47 | return True 48 | return False 49 | -------------------------------------------------------------------------------- /kindlecomicconverter/metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2013-2019 Pawel Jastrzebski 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for 6 | # any purpose with or without fee is hereby granted, provided that the 7 | # above copyright notice and this permission notice appear in all 8 | # copies. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 11 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 12 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 13 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 14 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 15 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 16 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 17 | # PERFORMANCE OF THIS SOFTWARE. 18 | 19 | import os 20 | from xml.dom.minidom import parse, Document 21 | from tempfile import mkdtemp 22 | from shutil import rmtree 23 | from xml.sax.saxutils import unescape 24 | from . import comicarchive 25 | 26 | 27 | class MetadataParser: 28 | def __init__(self, source): 29 | self.source = source 30 | self.data = {'Series': '', 31 | 'Volume': '', 32 | 'Number': '', 33 | 'Writers': [], 34 | 'Pencillers': [], 35 | 'Inkers': [], 36 | 'Colorists': [], 37 | 'Summary': '', 38 | 'Bookmarks': [], 39 | 'Title': ''} 40 | self.rawdata = None 41 | self.format = None 42 | if self.source.endswith('.xml') and os.path.exists(self.source): 43 | self.rawdata = parse(self.source) 44 | elif not self.source.endswith('.xml'): 45 | try: 46 | cbx = comicarchive.ComicArchive(self.source) 47 | self.rawdata = cbx.extractMetadata() 48 | self.format = cbx.type 49 | except OSError as e: 50 | raise UserWarning(e) 51 | if self.rawdata: 52 | self.parseXML() 53 | 54 | def parseXML(self): 55 | if len(self.rawdata.getElementsByTagName('Series')) != 0: 56 | self.data['Series'] = unescape(self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue) 57 | if len(self.rawdata.getElementsByTagName('Volume')) != 0: 58 | self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue 59 | if len(self.rawdata.getElementsByTagName('Number')) != 0: 60 | self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue 61 | if len(self.rawdata.getElementsByTagName('Summary')) != 0: 62 | self.data['Summary'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue) 63 | if len(self.rawdata.getElementsByTagName('Title')) != 0: 64 | self.data['Title'] = unescape(self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue) 65 | for field in ['Writer', 'Penciller', 'Inker', 'Colorist']: 66 | if len(self.rawdata.getElementsByTagName(field)) != 0: 67 | for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '): 68 | self.data[field + 's'].append(unescape(person)) 69 | self.data[field + 's'] = list(set(self.data[field + 's'])) 70 | self.data[field + 's'].sort() 71 | if len(self.rawdata.getElementsByTagName('Page')) != 0: 72 | for page in self.rawdata.getElementsByTagName('Page'): 73 | if 'Bookmark' in page.attributes and 'Image' in page.attributes: 74 | self.data['Bookmarks'].append((int(page.attributes['Image'].value), 75 | page.attributes['Bookmark'].value)) 76 | 77 | def saveXML(self): 78 | if self.rawdata: 79 | root = self.rawdata.getElementsByTagName('ComicInfo')[0] 80 | for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']], 81 | ['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])], 82 | ['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])], 83 | ['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']], 84 | ['Title', self.data['Title']]): 85 | if self.rawdata.getElementsByTagName(row[0]): 86 | node = self.rawdata.getElementsByTagName(row[0])[0] 87 | if row[1]: 88 | node.firstChild.replaceWholeText(row[1]) 89 | else: 90 | root.removeChild(node) 91 | elif row[1]: 92 | main = self.rawdata.createElement(row[0]) 93 | root.appendChild(main) 94 | text = self.rawdata.createTextNode(row[1]) 95 | main.appendChild(text) 96 | else: 97 | doc = Document() 98 | root = doc.createElement('ComicInfo') 99 | root.setAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema') 100 | root.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance') 101 | doc.appendChild(root) 102 | for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']], 103 | ['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])], 104 | ['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])], 105 | ['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']], 106 | ['Title', self.data['Title']]): 107 | if row[1]: 108 | main = doc.createElement(row[0]) 109 | root.appendChild(main) 110 | text = doc.createTextNode(row[1]) 111 | main.appendChild(text) 112 | self.rawdata = doc 113 | if self.source.endswith('.xml'): 114 | with open(self.source, 'w', encoding='utf-8') as f: 115 | self.rawdata.writexml(f, encoding='utf-8') 116 | else: 117 | workdir = mkdtemp('', 'KCC-') 118 | tmpXML = os.path.join(workdir, 'ComicInfo.xml') 119 | with open(tmpXML, 'w', encoding='utf-8') as f: 120 | self.rawdata.writexml(f, encoding='utf-8') 121 | try: 122 | cbx = comicarchive.ComicArchive(self.source) 123 | cbx.addFile(tmpXML) 124 | except OSError as e: 125 | raise UserWarning(e) 126 | rmtree(workdir) 127 | -------------------------------------------------------------------------------- /kindlecomicconverter/page_number_crop_alg.py: -------------------------------------------------------------------------------- 1 | from PIL import ImageOps, ImageFilter 2 | import numpy as np 3 | from .common_crop import threshold_from_power, group_close_values 4 | 5 | 6 | ''' 7 | Some assupmptions on the page number sizes 8 | We assume that the size of the number (including all digits) is between 9 | 'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size' relative to the image size. 10 | We assume the distance between the digit is no more than 'max_dist_size' (x,y), and no more than 3 digits. 11 | ''' 12 | max_shape_size_tolerated_size = (0.015*3, 0.02) # percent 13 | min_shape_size_tolerated_size = (0.003, 0.006) # percent 14 | window_h_size = max_shape_size_tolerated_size[1]*1.25 # percent 15 | max_dist_size = (0.01, 0.002) # percent 16 | 17 | 18 | ''' 19 | E-reader screen real-estate is an important resource. 20 | More available screensize means more details can be better seen, especially text. 21 | Text is one of the most important elements that need to be clearly readable on e-readers, 22 | which mostly are smaller devices where the need to zoom is unwanted. 23 | 24 | By cropping the page number on the bottom of the page, 2%-5% of the page height can be regained 25 | that allows us to upscale the image even more. 26 | - Most of the times the screen height is the limiting factor in upscaling, rather than its width. 27 | 28 | Parameters: 29 | img (PIL image): A PIL image. 30 | power (float): The power to 'chop' through pixels matching the background. Values in range[0,3]. 31 | background_color (string): 'white' for white background, anything else for black. 32 | Returns: 33 | bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory 34 | ''' 35 | def get_bbox_crop_margin_page_number(img, power=1, background_color='white'): 36 | if img.mode != 'L': 37 | img = ImageOps.grayscale(img) 38 | 39 | if background_color != 'white': 40 | img = ImageOps.invert(img) 41 | 42 | ''' 43 | Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites. 44 | Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts. 45 | Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform. 46 | ''' 47 | img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1)) 48 | 49 | ''' 50 | The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background) 51 | and the lower the power, more sensitive to black pixels. 52 | ''' 53 | threshold = threshold_from_power(power) 54 | bw_img = img.point(lambda p: 255 if p <= threshold else 0) 55 | bw_bbox = bw_img.getbbox() 56 | if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. 57 | return None 58 | 59 | left, top_y_pos, right, bot_y_pos = bw_bbox 60 | 61 | ''' 62 | We inspect the lower bottom part of the image where we suspect might be a page number. 63 | We assume that page number consist of 1 to 3 digits and the total min and max size of the number 64 | is between 'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size'. 65 | ''' 66 | window_h = int(img.size[1] * window_h_size) 67 | img_part = img.crop((left,bot_y_pos-window_h, right, bot_y_pos)) 68 | 69 | ''' 70 | We detect related-pixels by proximity, with max distance defined in 'max_dist_size'. 71 | Related pixels (in x axis) for each image-row are then merged to boxes with adjacent rows (in y axis) 72 | to form bounding boxes of the detected objects (which one of them could be the page number). 73 | ''' 74 | img_part_mat = np.array(img_part) 75 | window_groups = [] 76 | for i in range(img_part.size[1]): 77 | row_groups = [(g[0], g[1], i, i) for g in group_close_values(np.where(img_part_mat[i] <= threshold)[0], img.size[0]*max_dist_size[0])] 78 | window_groups.extend(row_groups) 79 | 80 | window_groups = np.array(window_groups) 81 | 82 | boxes = merge_boxes(window_groups, (img.size[0]*max_dist_size[0], img.size[1]*max_dist_size[1])) 83 | ''' 84 | We assume that the lowest part of the image that has black pixels on is the page number. 85 | In case that there are more than one detected object in the loewst part, we assume that one of them is probably 86 | manga-content and shouldn't be cropped. 87 | ''' 88 | # filter all small objects 89 | boxes = list(filter(lambda box: box[1]-box[0] >= img.size[0]*min_shape_size_tolerated_size[0] 90 | and box[3]-box[2] >= img.size[1]*min_shape_size_tolerated_size[1], boxes)) 91 | lowest_boxes = list(filter(lambda box: box[3] == window_h-1, boxes)) 92 | 93 | min_y_of_lowest_boxes = 0 94 | if len(lowest_boxes) > 0: 95 | min_y_of_lowest_boxes = np.min(np.array(lowest_boxes)[:,2]) 96 | 97 | boxes_in_same_y_range = list(filter(lambda box: box[3] >= min_y_of_lowest_boxes, boxes)) 98 | 99 | max_shape_size_tolerated = (img.size[0] * max_shape_size_tolerated_size[0], 100 | max(img.size[1] *max_shape_size_tolerated_size[1], 3)) 101 | 102 | should_force_crop = ( 103 | len(boxes_in_same_y_range) == 1 104 | and (boxes_in_same_y_range[0][1] - boxes_in_same_y_range[0][0] <= max_shape_size_tolerated[0]) 105 | and (boxes_in_same_y_range[0][3] - boxes_in_same_y_range[0][2] <= max_shape_size_tolerated[1]) 106 | ) 107 | 108 | cropped_bbox = (0, 0, img.size[0], img.size[1]) 109 | if should_force_crop: 110 | cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1)) 111 | 112 | cropped_bbox = bw_img.crop(cropped_bbox).getbbox() 113 | return cropped_bbox 114 | 115 | 116 | ''' 117 | Parameters: 118 | img (PIL image): A PIL image. 119 | power (float): The power to 'chop' through pixels matching the background. Values in range[0,3]. 120 | background_color (string): 'white' for white background, anything else for black. 121 | Returns: 122 | bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory 123 | ''' 124 | def get_bbox_crop_margin(img, power=1, background_color='white'): 125 | if img.mode != 'L': 126 | img = ImageOps.grayscale(img) 127 | 128 | if background_color != 'white': 129 | img = ImageOps.invert(img) 130 | 131 | ''' 132 | Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites. 133 | Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts. 134 | Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform. 135 | ''' 136 | img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1)) 137 | 138 | ''' 139 | The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background) 140 | and the lower the power, more sensitive to black pixels. 141 | ''' 142 | threshold = threshold_from_power(power) 143 | bw_img = img.point(lambda p: 255 if p <= threshold else 0) 144 | 145 | return bw_img.getbbox() 146 | 147 | 148 | def box_intersect(box1, box2, max_dist): 149 | return not (box2[0]-max_dist[0] > box1[1] 150 | or box2[1]+max_dist[0] < box1[0] 151 | or box2[2]-max_dist[1] > box1[3] 152 | or box2[3]+max_dist[1] < box1[2]) 153 | 154 | ''' 155 | Merge close bounding boxes (left,right, top,bot) (x axis) with distance threshold defined in 156 | 'max_dist_tolerated'. Boxes with less 'max_dist_tolerated' distance (Chebyshev distance). 157 | ''' 158 | def merge_boxes(boxes, max_dist_tolerated): 159 | j = 0 160 | while j < len(boxes)-1: 161 | g1 = boxes[j] 162 | intersecting_boxes = [] 163 | other_boxes = [] 164 | for i in range(j+1,len(boxes)): 165 | g2 = boxes[i] 166 | if box_intersect(g1,g2, max_dist_tolerated): 167 | intersecting_boxes.append(g2) 168 | else: 169 | other_boxes.append(g2) 170 | 171 | if len(intersecting_boxes) > 0: 172 | intersecting_boxes = np.array([g1, *intersecting_boxes]) 173 | merged_box = np.array([ 174 | np.min(intersecting_boxes[:,0]), 175 | np.max(intersecting_boxes[:,1]), 176 | np.min(intersecting_boxes[:,2]), 177 | np.max(intersecting_boxes[:,3]) 178 | ]) 179 | other_boxes.append(merged_box) 180 | boxes = np.concatenate([boxes[:j], other_boxes]) 181 | j = 0 182 | else: 183 | j += 1 184 | return boxes 185 | -------------------------------------------------------------------------------- /kindlecomicconverter/pdfjpgextract.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 4 | # Copyright (c) 2013-2019 Pawel Jastrzebski 5 | # 6 | # Based upon the code snippet by Ned Batchelder 7 | # (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html) 8 | # 9 | # Permission to use, copy, modify, and/or distribute this software for 10 | # any purpose with or without fee is hereby granted, provided that the 11 | # above copyright notice and this permission notice appear in all 12 | # copies. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 15 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 16 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 17 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 18 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 19 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 20 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 21 | # PERFORMANCE OF THIS SOFTWARE. 22 | # 23 | 24 | import os 25 | from random import choice 26 | from string import ascii_uppercase, digits 27 | 28 | # skip stray images a few pixels in size in some PDFs 29 | # typical images are many thousands in length 30 | # https://github.com/ciromattia/kcc/pull/546 31 | STRAY_IMAGE_LENGTH_THRESHOLD = 300 32 | 33 | 34 | class PdfJpgExtract: 35 | def __init__(self, fname): 36 | self.fname = fname 37 | self.filename = os.path.splitext(fname) 38 | self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3)) 39 | 40 | def getPath(self): 41 | return self.path 42 | 43 | def extract(self): 44 | pdf = open(self.fname, "rb").read() 45 | startmark = b"\xff\xd8" 46 | startfix = 0 47 | endmark = b"\xff\xd9" 48 | endfix = 2 49 | i = 0 50 | njpg = 0 51 | os.makedirs(self.path) 52 | while True: 53 | istream = pdf.find(b"stream", i) 54 | if istream < 0: 55 | break 56 | istart = pdf.find(startmark, istream, istream + 20) 57 | if istart < 0: 58 | i = istream + 20 59 | continue 60 | iend = pdf.find(b"endstream", istart) 61 | if iend < 0: 62 | raise Exception("Didn't find end of stream!") 63 | iend = pdf.find(endmark, iend - 20) 64 | if iend < 0: 65 | raise Exception("Didn't find end of JPG!") 66 | istart += startfix 67 | iend += endfix 68 | i = iend 69 | 70 | if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD: 71 | continue 72 | 73 | jpg = pdf[istart:iend] 74 | jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb") 75 | jpgfile.write(jpg) 76 | jpgfile.close() 77 | njpg += 1 78 | 79 | return self.path, njpg 80 | -------------------------------------------------------------------------------- /kindlecomicconverter/shared.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 4 | # Copyright (c) 2013-2019 Pawel Jastrzebski 5 | # 6 | # Permission to use, copy, modify, and/or distribute this software for 7 | # any purpose with or without fee is hereby granted, provided that the 8 | # above copyright notice and this permission notice appear in all 9 | # copies. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 16 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | # PERFORMANCE OF THIS SOFTWARE. 19 | # 20 | 21 | from functools import lru_cache 22 | import os 23 | from hashlib import md5 24 | from html.parser import HTMLParser 25 | import subprocess 26 | from packaging.version import Version 27 | from re import split 28 | import sys 29 | from traceback import format_tb 30 | 31 | 32 | class HTMLStripper(HTMLParser): 33 | def __init__(self): 34 | HTMLParser.__init__(self) 35 | self.reset() 36 | self.strict = False 37 | self.convert_charrefs = True 38 | self.fed = [] 39 | 40 | def handle_data(self, d): 41 | self.fed.append(d) 42 | 43 | def get_data(self): 44 | return ''.join(self.fed) 45 | 46 | def error(self, message): 47 | pass 48 | 49 | 50 | def getImageFileName(imgfile): 51 | name, ext = os.path.splitext(imgfile) 52 | ext = ext.lower() 53 | if (name.startswith('.') and len(name) == 1): 54 | return None 55 | if name.startswith('._'): 56 | return None 57 | if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']: 58 | return None 59 | return [name, ext] 60 | 61 | 62 | def walkSort(dirnames, filenames): 63 | convert = lambda text: int(text) if text.isdigit() else text 64 | alphanum_key = lambda key: [convert(c) for c in split('([0-9]+)', key)] 65 | dirnames.sort(key=lambda name: alphanum_key(name.lower())) 66 | filenames.sort(key=lambda name: alphanum_key(name.lower())) 67 | return dirnames, filenames 68 | 69 | 70 | def walkLevel(some_dir, level=1): 71 | some_dir = some_dir.rstrip(os.path.sep) 72 | assert os.path.isdir(some_dir) 73 | num_sep = some_dir.count(os.path.sep) 74 | for root, dirs, files in os.walk(some_dir): 75 | dirs, files = walkSort(dirs, files) 76 | yield root, dirs, files 77 | num_sep_this = root.count(os.path.sep) 78 | if num_sep + level <= num_sep_this: 79 | del dirs[:] 80 | 81 | 82 | 83 | def sanitizeTrace(traceback): 84 | return ''.join(format_tb(traceback))\ 85 | .replace('C:/projects/kcc/', '')\ 86 | .replace('c:/projects/kcc/', '')\ 87 | .replace('C:/python37-x64/', '')\ 88 | .replace('c:/python37-x64/', '')\ 89 | .replace('C:\\projects\\kcc\\', '')\ 90 | .replace('c:\\projects\\kcc\\', '')\ 91 | .replace('C:\\python37-x64\\', '')\ 92 | .replace('c:\\python37-x64\\', '') 93 | 94 | 95 | # noinspection PyUnresolvedReferences 96 | def dependencyCheck(level): 97 | missing = [] 98 | if level > 2: 99 | try: 100 | from PySide6.QtCore import qVersion as qtVersion 101 | if Version('6.5.1') > Version(qtVersion()): 102 | missing.append('PySide 6.5.1+') 103 | except ImportError: 104 | missing.append('PySide 6.5.1+') 105 | try: 106 | import raven 107 | except ImportError: 108 | missing.append('raven 6.0.0+') 109 | if level > 1: 110 | try: 111 | from psutil import __version__ as psutilVersion 112 | if Version('5.0.0') > Version(psutilVersion): 113 | missing.append('psutil 5.0.0+') 114 | except ImportError: 115 | missing.append('psutil 5.0.0+') 116 | try: 117 | from types import ModuleType 118 | from slugify import __version__ as slugifyVersion 119 | if isinstance(slugifyVersion, ModuleType): 120 | slugifyVersion = slugifyVersion.__version__ 121 | if Version('1.2.1') > Version(slugifyVersion): 122 | missing.append('python-slugify 1.2.1+') 123 | except ImportError: 124 | missing.append('python-slugify 1.2.1+') 125 | try: 126 | from PIL import __version__ as pillowVersion 127 | if Version('5.2.0') > Version(pillowVersion): 128 | missing.append('Pillow 5.2.0+') 129 | except ImportError: 130 | missing.append('Pillow 5.2.0+') 131 | if len(missing) > 0: 132 | print('ERROR: ' + ', '.join(missing) + ' is not installed!') 133 | sys.exit(1) 134 | 135 | @lru_cache 136 | def available_archive_tools(): 137 | available = [] 138 | 139 | for tool in ['tar', '7z', 'unar', 'unrar']: 140 | try: 141 | subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 142 | available.append(tool) 143 | except FileNotFoundError: 144 | pass 145 | 146 | return available 147 | 148 | def subprocess_run(command, **kwargs): 149 | if (os.name == 'nt'): 150 | kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW) 151 | return subprocess.run(command, **kwargs) 152 | -------------------------------------------------------------------------------- /kindlecomicconverter/startup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (c) 2012-2014 Ciro Mattia Gonano 4 | # Copyright (c) 2013-2019 Pawel Jastrzebski 5 | # 6 | # Permission to use, copy, modify, and/or distribute this software for 7 | # any purpose with or without fee is hereby granted, provided that the 8 | # above copyright notice and this permission notice appear in all 9 | # copies. 10 | # 11 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 12 | # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 13 | # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE 14 | # AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 15 | # DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 16 | # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 17 | # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | # PERFORMANCE OF THIS SOFTWARE. 19 | # 20 | 21 | import os 22 | import sys 23 | from . import __version__ 24 | from .shared import dependencyCheck 25 | 26 | 27 | def start(): 28 | dependencyCheck(3) 29 | from . import KCC_gui 30 | os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = "1" 31 | KCCAplication = KCC_gui.QApplicationMessaging(sys.argv) 32 | if KCCAplication.isRunning(): 33 | for i in range(1, len(sys.argv)): 34 | KCCAplication.sendMessage(sys.argv[i]) 35 | else: 36 | KCCAplication.sendMessage('ARISE') 37 | else: 38 | KCCWindow = KCC_gui.QMainWindowKCC() 39 | KCCUI = KCC_gui.KCCGUI(KCCAplication, KCCWindow) 40 | for i in range(1, len(sys.argv)): 41 | KCCUI.handleMessage(sys.argv[i]) 42 | sys.exit(KCCAplication.exec_()) 43 | 44 | 45 | def startC2E(): 46 | dependencyCheck(2) 47 | from .comic2ebook import main 48 | print('comic2ebook v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.') 49 | sys.exit(main(sys.argv[1:])) 50 | 51 | 52 | def startC2P(): 53 | dependencyCheck(1) 54 | from .comic2panel import main 55 | print('comic2panel v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.') 56 | sys.exit(main(sys.argv[1:])) 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6>=6.5.1 2 | Pillow>=5.2.0 3 | psutil>=5.9.5 4 | requests>=2.31.0 5 | python-slugify>=1.2.1 6 | raven>=6.0.0 7 | packaging>=23.2 8 | mozjpeg-lossless-optimization>=1.1.2 9 | natsort>=8.4.0 10 | distro>=1.8.0 11 | numpy>=1.22.4,<2.0.0 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | pip/pyinstaller build script for KCC. 5 | 6 | Install as Python package: 7 | python3 setup.py install 8 | 9 | Create EXE/APP: 10 | python3 setup.py build_binary 11 | """ 12 | 13 | import os 14 | import platform 15 | import sys 16 | import setuptools 17 | from kindlecomicconverter import __version__ 18 | 19 | NAME = 'KindleComicConverter' 20 | MAIN = 'kcc.py' 21 | VERSION = __version__ 22 | 23 | 24 | # noinspection PyUnresolvedReferences 25 | class BuildBinaryCommand(setuptools.Command): 26 | description = 'build binary release' 27 | user_options = [] 28 | 29 | def initialize_options(self): 30 | pass 31 | 32 | def finalize_options(self): 33 | pass 34 | 35 | # noinspection PyShadowingNames 36 | def run(self): 37 | VERSION = __version__ 38 | if sys.platform == 'darwin': 39 | os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py') 40 | # TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v 41 | os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg') 42 | sys.exit(0) 43 | elif sys.platform == 'win32': 44 | os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py') 45 | sys.exit(0) 46 | elif sys.platform == 'linux': 47 | os.system( 48 | 'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py') 49 | sys.exit(0) 50 | else: 51 | sys.exit(0) 52 | 53 | 54 | setuptools.setup( 55 | cmdclass={ 56 | 'build_binary': BuildBinaryCommand, 57 | }, 58 | name=NAME, 59 | version=VERSION, 60 | author='Ciro Mattia Gonano, Pawel Jastrzebski', 61 | author_email='ciromattia@gmail.com, pawelj@iosphe.re', 62 | description='Comic and Manga converter for e-book readers.', 63 | license='ISC License (ISCL)', 64 | keywords=['kindle', 'kobo', 'comic', 'manga', 'mobi', 'epub', 'cbz'], 65 | url='http://github.com/ciromattia/kcc', 66 | entry_points={ 67 | 'console_scripts': [ 68 | 'kcc-c2e = kindlecomicconverter.startup:startC2E', 69 | 'kcc-c2p = kindlecomicconverter.startup:startC2P', 70 | ], 71 | 'gui_scripts': [ 72 | 'kcc = kindlecomicconverter.startup:start', 73 | ], 74 | }, 75 | packages=['kindlecomicconverter'], 76 | install_requires=[ 77 | 'pyside6>=6.5.1', 78 | 'Pillow>=5.2.0', 79 | 'psutil>=5.9.5', 80 | 'python-slugify>=1.2.1,<9.0.0', 81 | 'raven>=6.0.0', 82 | 'requests>=2.31.0', 83 | 'mozjpeg-lossless-optimization>=1.1.2', 84 | 'natsort>=8.4.0', 85 | 'distro', 86 | 'numpy>=1.22.4,<2.0.0' 87 | ], 88 | classifiers=[], 89 | zip_safe=False, 90 | ) 91 | --------------------------------------------------------------------------------