1
0
mirror of https://github.com/ciromattia/kcc synced 2026-06-29 01:25:41 +00:00

Compare commits

...

60 Commits

Author SHA1 Message Date
Alex Xu cab3703217 bump to 10.3.0 2026-06-26 10:23:51 -07:00
Alex Xu 3723bf9d52 add light novel mode (#1361)
* add a novel mode where it only resizes images

* add GUI

* cleanup

* fix typo

* fully cooked

* remove print

* clean up

* cook

* cook

* handle LA case

* cleanup
2026-06-26 10:23:28 -07:00
Alex Xu 2a6d61530f epub input: fix fusion (#1375) 2026-06-26 10:23:09 -07:00
Alex Xu 5396f1f9c4 name output epub/cbz directly (#1374) 2026-06-24 21:22:49 -07:00
Alex Xu f1b58c83d6 Replace copyfile + remove with move (#1373)
* remove mobiPath toclean

* remove copyfile _comic.zip
2026-06-24 18:58:27 -07:00
Alex Xu 0f009755b1 store settings separately 2026-06-23 17:37:46 -07:00
Alex Xu ee5bd150e5 add force ebok option 2026-06-23 17:37:46 -07:00
Alex Xu a55c0ddb08 add epub language option (#1371) 2026-06-23 13:29:10 -07:00
Alex Xu 799961407e re-arrange options into 4 columns (#1370)
* re-arrange options into 4 columns

* put delete input at the top
2026-06-22 22:27:00 -07:00
Alex Xu bc28df1f53 add invert direction and vertical 4 panel options (#1369) 2026-06-22 17:31:12 -07:00
Alex Xu a8b2c055bf epub input: invert spread shift (#1368) 2026-06-22 17:17:22 -07:00
dependabot[bot] afa9c7e7e6 Update psutil requirement from >=5.9.5 to >=7.2.2 (#1358)
Updates the requirements on [psutil](https://github.com/giampaolo/psutil) to permit the latest version.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/v5.9.5...v7.2.2)

---
updated-dependencies:
- dependency-name: psutil
  dependency-version: 7.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:31 -07:00
dependabot[bot] 1f1b9a37fa Update distro requirement from >=1.8.0 to >=1.9.0 (#1357)
Updates the requirements on [distro](https://github.com/python-distro/distro) to permit the latest version.
- [Release notes](https://github.com/python-distro/distro/releases)
- [Changelog](https://github.com/python-distro/distro/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python-distro/distro/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: distro
  dependency-version: 1.9.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:15 -07:00
dependabot[bot] c4d9512cbc Bump actions/checkout from 6 to 7 (#1367)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:02:58 -07:00
Alex Xu 89fc6437dd don't crash if rosetta is not available on macOS (#1366)
* don't crash if rosetta is not available on macOS

* make error a dialog

* add newlines

* fix format

* modify strings
2026-06-20 17:55:04 -07:00
Alex Xu 1e57da08a9 time kindlegen (#1365) 2026-06-18 16:34:52 -07:00
Alex Xu b4ef37dbb9 bump Windows 7 numpy (#1362) 2026-05-31 18:43:10 -07:00
Alex Xu b95cf6e179 ignore exceptions when epub parsing (#1360)
* ignore exceptions when epub parsing

* Update comic2ebook.py

* Update comic2ebook.py
2026-05-26 09:13:22 -07:00
フィルターペーパー 08070cdd97 Fix fusion output location (#1355)
* Fix fusion output location

Use source folder when output folder is not specified

* undelete things
2026-05-25 16:41:36 -07:00
Alex Xu df3d174437 fusion temp file cleanup improvements (#1359) 2026-05-25 14:52:08 -07:00
Alex Xu dc4475bcb0 bump to 10.2.0 2026-05-22 15:15:02 -07:00
Alex Xu b2e7fd3f5a fix macOS 10.14 PyMuPDF (#1354) 2026-05-22 15:02:13 -07:00
Alex Xu 4102643110 disk usage warning reccomends using temp directory option (#1353) 2026-05-22 11:55:42 -07:00
Alex Xu ca6b3b7611 fix file fusion when output folder unchecked (beta regression) (#1352) 2026-05-22 11:41:47 -07:00
Alex Xu 6d5db71b5b add pip install --upgrade pip to README.md 2026-05-21 21:07:57 -07:00
Alex Xu 87c6b7143a fix cropping with border options (#1351) 2026-05-21 13:28:12 -07:00
Alex Xu a5bd995a6b remove sentry 2026-05-20 07:55:58 -07:00
Alex Xu bcc69b0f05 Update requirements-win7.txt 2026-05-19 08:21:56 -07:00
Jarosław Janas f96adc5dc3 Metadata editor in bulk v2 (#1349)
* Add bulk metadata editing support

* Enable multi-file selection in Metadata Editor using getOpenFileNames()

* Disable Volume, Number, and Title fields in bulk mode

* Pre-fill Series and author fields from first selected file in bulk mode

* Show status "Editing Y files" in bulk mode

* Add progress display during bulk save ("Processing X/Y: filename")

* Disable buttons during processing

* Skip read-only CBR files with error collection

* Show error summary dialog if any files fail to save

* Keep single file metadata editing the same

* Add bulk volume editing with rich tooltip

* Add checkbox next to Volume field for enabling bulk volume editing (only in bulk mode)

* Support single number (5), range (1-10), or comma list (1, 3, 5) input formats

* Sort files alphabetically before volume assignment

* Validate that volume count matches file count

* Add rich HTML tooltip explaining the feature

* Move bulkVolumeCheck widget definition to MetaEditor.ui

* Define bulkVolumeCheck QCheckBox in MetaEditor.ui instead of creating it programmatically in KCC_gui.py

* Update tab order to include bulkVolumeCheck after volumeLine

* Add multi-directory selection for bulk metadata editing

* Shift+click on editor button now opens multi-directory selection dialog
* Multiple directories enable bulk mode with volume number assignment
* Uses Qt's built-in dialog with multi-selection enabled for native look and feel

* Fix volume input validation in bulk metadata editor

* Handle invalid range formats like "1-2-3" or "--" that previously fell through, now returns an error message

* Add explicit check for empty/malformed range parts before attempting to parse

* Add positive number validation for all input types (range, comma-list, single number) to be consistent with single file mode which uses isnumeric()

* Add explicit validation for empty volumes list after parsing

* Keep metadata editor dialog open when error occur during bulks save

* Fix CBR read-only check in bulk metadata editor

* Check all files for CBR format during load instead of during save

* Unify the CBR check for single and bulk mode

* Handle mixed metadata values in bulk editing

* In bulk mode, compare Series + Writer/Penciller/Inker/Colorist across all selected files (instead of using only the first file)

* When values differ, show “(multiple values)” placeholder for that field

* Add hover tooltip with overwrite warning and a File|Value table (or Value|Count summary when there are >20 files)

* Code comments cleanup

* Use ExtendedSelection for multi-directory file dialog

* use !=
2026-05-19 08:17:13 -07:00
Alex Xu f54b06e058 increase bisect threhold aspect ratio from 1.75 to 1.80 (#1348) 2026-05-18 21:42:44 -07:00
Alex Xu 7ceeb29fae add 1 page landscape option (#1344) 2026-05-17 11:55:46 -07:00
Alex Xu c385ef7ae0 epub input: extract biggest image per page (#1342)
* extract biggest image on page

* fix largest_size location
2026-05-17 09:12:44 -07:00
Alex Xu 9827f11944 experimental epub input (#1090)
* experimental epub input

* fix missing spine items

* only extract first image on page

* re-organize

* fallback if naive spine extraction fails

* apply legacy extract option for epub too
2026-05-15 13:24:35 -07:00
dependabot[bot] 8030884148 Update python-slugify requirement from >=1.2.1 to >=8.0.4 (#1314)
Updates the requirements on [python-slugify](https://github.com/un33k/python-slugify) to permit the latest version.
- [Changelog](https://github.com/un33k/python-slugify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un33k/python-slugify/compare/1.2.1...v8.0.4)

---
updated-dependencies:
- dependency-name: python-slugify
  dependency-version: 8.0.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 22:15:29 -07:00
dependabot[bot] 949fb0acb4 Update requests requirement from >=2.31.0 to >=2.34.2 (#1313)
Updates the requirements on [requests](https://github.com/psf/requests) to permit the latest version.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.31.0...v2.34.2)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 22:13:14 -07:00
dependabot[bot] 84f69a0950 Update packaging requirement from >=23.2 to >=26.2 (#1315)
Updates the requirements on [packaging](https://github.com/pypa/packaging) to permit the latest version.
- [Release notes](https://github.com/pypa/packaging/releases)
- [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/packaging/compare/23.2...26.2)

---
updated-dependencies:
- dependency-name: packaging
  dependency-version: '26.2'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 22:12:12 -07:00
Flavio Mello 868a31cb00 Update setup.py (#1339)
Include packaging in setup.py install_requires
2026-05-14 22:07:38 -07:00
Alex Xu 1af24b394f remove raven (#1341)
* remove raven

* remove raven README
2026-05-14 22:04:20 -07:00
Alex Xu 401876da22 Merge pull request #1338 from axu2/temp
Temporary File improvements
2026-05-11 16:40:46 -07:00
Alex Xu ffeaaeca19 clean up temp files better 2026-05-11 16:34:11 -07:00
Alex Xu b95bb12393 honor temp directory option in all locations 2026-05-11 11:53:20 -07:00
フィルターペーパー 4a6e4622ed Use tempdir option for fusion path
* Update makeFusion to use the same temporary directory location
* Avoid creating an orphan "KCC-" in TMPDIR when --tempdir is set
2026-05-11 10:33:50 -07:00
dependabot[bot] a491810810 Bump signpath/github-action-submit-signing-request from 2.0 to 2.2 (#1337)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2.0 to 2.2.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v2.0...v2.2)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.2'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 19:00:05 -07:00
dependabot[bot] 75d0342fe1 Bump docker/build-push-action from 6 to 7 (#1336)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 18:59:54 -07:00
dependabot[bot] 60d41b25e4 Bump softprops/action-gh-release from 2 to 3 (#1335)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 18:59:42 -07:00
dependabot[bot] 0db788589d Bump docker/setup-qemu-action from 3 to 4 (#1263)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 12:05:27 -07:00
dependabot[bot] f42e6aea5c Bump docker/login-action from 3 to 4 (#1262)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 12:05:16 -07:00
dependabot[bot] 53ae057cbb Bump docker/metadata-action from 5 to 6 (#1261)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 12:05:05 -07:00
dependabot[bot] d729839976 Bump docker/setup-buildx-action from 3 to 4 (#1260)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 12:04:51 -07:00
dependabot[bot] 42a50ed670 Bump actions/upload-artifact from 6 to 7 (#1256)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 12:04:38 -07:00
Alex Xu e6ef7c1732 Bump actions/upload-artifact from 5 to 6 (#1332)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:59:01 -07:00
Alex Xu f2a806a42a bumpy to 10.1.3 2026-05-06 08:39:27 -07:00
Alex Xu 8798d71bfa smart cover crop is default off (#1331) 2026-05-06 08:12:19 -07:00
Alex Xu 19ce14eeee KFX default upscale (#1329) 2026-05-04 16:40:40 -07:00
Alex Xu 5a1e2dafcb fix thin horizontal line in landscape in certain situations (#1326) 2026-05-04 15:22:37 -07:00
Alex Xu f149ae23f3 add Kindle Scribe 2025 landscape 1324x1986 profile (#1325) 2026-05-03 16:48:05 -07:00
Alex Xu 2878e5d41b bump to 10.1.2 2026-05-02 15:17:36 -07:00
Alex Xu bd691989a9 fix KFX on Windows 7 (#1323) 2026-05-02 15:13:21 -07:00
Alex Xu d4aeb798c7 bump to 10.1.1 2026-05-02 14:43:12 -07:00
Alex Xu 997a514e2a fix list index out of range for kfx (#1321) 2026-05-02 14:42:46 -07:00
24 changed files with 2096 additions and 1554 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+6 -6
View File
@@ -21,20 +21,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Set Release Date - name: Set Release Date
id: release_date id: release_date
@@ -43,7 +43,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: ghcr.io/${{ github.repository_owner }}/kcc images: ghcr.io/${{ github.repository_owner }}/kcc
# Always creates the "latest" tag # Always creates the "latest" tag
@@ -54,7 +54,7 @@ jobs:
type=raw,value=${{ steps.release_date.outputs.release_date }} type=raw,value=${{ steps.release_date.outputs.release_date }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
context: . context: .
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -59,12 +59,12 @@ jobs:
env: env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: AppImage name: AppImage
path: './*.AppImage*' path: './*.AppImage*'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
prerelease: true prerelease: true
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
env: env:
MACOSX_DEPLOYMENT_TARGET: '14.0' MACOSX_DEPLOYMENT_TARGET: '14.0'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -80,12 +80,12 @@ jobs:
run: | run: |
python setup.py build_binary python setup.py build_binary
- name: upload build - name: upload build
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: mac-os-build-${{ runner.arch }} name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg path: dist/*.dmg
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
prerelease: true prerelease: true
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9 PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14' MACOSX_DEPLOYMENT_TARGET: '10.14'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Get Python - name: Get Python
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg" run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
- name: Install Python - name: Install Python
@@ -51,12 +51,12 @@ jobs:
run: | run: |
python3 setup.py build_binary python3 setup.py build_binary
- name: upload build - name: upload build
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: osx-build-${{ runner.arch }} name: osx-build-${{ runner.arch }}
path: dist/*.dmg path: dist/*.dmg
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
prerelease: true prerelease: true
+4 -4
View File
@@ -35,7 +35,7 @@ jobs:
command: build_c2p command: build_c2p
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -53,12 +53,12 @@ jobs:
python setup.py ${{ matrix.command }} python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact - name: upload-unsigned-artifact
id: upload-unsigned-artifact id: upload-unsigned-artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: windows-build-${{ matrix.entry }} name: windows-build-${{ matrix.entry }}
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0 uses: signpath/github-action-submit-signing-request@v2.2
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -69,7 +69,7 @@ jobs:
wait-for-completion: true wait-for-completion: true
output-artifact-directory: 'dist/' output-artifact-directory: 'dist/'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
prerelease: true prerelease: true
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
env: env:
WINDOWS_7: 1 WINDOWS_7: 1
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
@@ -46,12 +46,12 @@ jobs:
python setup.py build_binary python setup.py build_binary
- name: upload-unsigned-artifact - name: upload-unsigned-artifact
id: upload-unsigned-artifact id: upload-unsigned-artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v7
with: with:
name: windows7-build name: windows7-build
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0 uses: signpath/github-action-submit-signing-request@v2.2
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -62,7 +62,7 @@ jobs:
wait-for-completion: true wait-for-completion: true
output-artifact-directory: 'dist/' output-artifact-directory: 'dist/'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
prerelease: true prerelease: true
-1
View File
@@ -2,7 +2,6 @@
Pipfile Pipfile
Pipfile.lock Pipfile.lock
setup.bat setup.bat
kindlecomicconverter/sentry.py
other/windows/kindlegen.exe other/windows/kindlegen.exe
dist/ dist/
build/ build/
+12 -3
View File
@@ -199,6 +199,8 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0), 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0), 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0), 'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
@@ -237,15 +239,19 @@ MAIN:
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) 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)
[Default=KV] [Default=KV]
-m, --manga-style Manga style (right-to-left reading and splitting) -m, --manga-style Manga style (right-to-left reading and splitting)
--lightnovel Only resize images and preserve original file structure.
--ebok Force EBOK tag instead of PDOC for MOBI
--invertdirection Invert page turn direction
-q, --hq Try to increase the quality of magnification -q, --hq Try to increase the quality of magnification
-2, --two-panel Display two not four panels in Panel View mode -2, --two-panel Display two not four panels in Panel View mode
--vertical4panel Show side panels first in virtual panel view
-w, --webtoon Webtoon processing mode -w, --webtoon Webtoon processing mode
--ts TARGETSIZE, --targetsize TARGETSIZE --ts TARGETSIZE, --targetsize TARGETSIZE
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others] the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
PROCESSING: PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option -n, --noprocessing Do not modify image and ignore any profile or processing option
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier. --legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
--pdfwidth Render vector PDFs based on device width instead of height. --pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution -u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution -s, --stretch Stretch images to device's resolution
@@ -267,7 +273,7 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0] Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--nosmartcovercrop Disable attempt to crop main cover from wide image --smartcovercrop Attempt to crop main cover from wide image
--coverfill Center-crop only the cover to fill target device screen --coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale --forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG for black and white images --forcepng Create PNG files instead JPEG for black and white images
@@ -289,12 +295,14 @@ OUTPUT SETTINGS:
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0] --metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
--language EPUB language [Default=en-US]
-f FORMAT, --format FORMAT -f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto] Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
-b BATCHSPLIT, --batchsplit BATCHSPLIT -b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment --spreadshift Shift first page to opposite side in landscape for two page spread alignment
--onepagelandscape Show a single centered page in landscape
--norotate Do not rotate double page spreads in spread splitter option. --norotate Do not rotate double page spreads in spread splitter option.
--rotateright Rotate double page spreads in opposite direction. --rotateright Rotate double page spreads in opposite direction.
--rotatefirst Put rotated spread first in spread splitter option. --rotatefirst Put rotated spread first in spread splitter option.
@@ -362,6 +370,7 @@ One time setup and running for the first time:
``` ```
python -m venv venv python -m venv venv
venv\Scripts\activate.bat venv\Scripts\activate.bat
pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
python kcc.py python kcc.py
``` ```
@@ -387,6 +396,7 @@ One time setup and running for the first time:
``` ```
python3 -m venv venv python3 -m venv venv
source venv/bin/activate source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
python kcc.py python kcc.py
``` ```
@@ -440,7 +450,6 @@ Older links (dead):
## PRIVACY ## PRIVACY
**KCC** is initiating internet connections in two cases: **KCC** is initiating internet connections in two cases:
* During startup - Version check and announcement check. * During startup - Version check and announcement check.
* When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES ## KNOWN ISSUES
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues). Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
+787 -726
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -62,6 +62,19 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="volumeLine"/> <widget class="QLineEdit" name="volumeLine"/>
</item> </item>
<item row="1" column="2">
<widget class="QCheckBox" name="bulkVolumeCheck">
<property name="visible">
<bool>false</bool>
</property>
<property name="toolTip">
<string>&lt;b&gt;Bulk Volume Editing&lt;/b&gt;&lt;br&gt;Check this box to assign volume numbers to multiple files.&lt;br&gt;&lt;br&gt;&lt;b&gt;Input formats:&lt;/b&gt;&lt;br&gt;&lt;code&gt;5&lt;/code&gt; → sequence starting from 5 (5, 6, 7...)&lt;br&gt;&lt;code&gt;1-10&lt;/code&gt; → range from 1 to 10&lt;br&gt;&lt;code&gt;1, 3, 5&lt;/code&gt; → specific values&lt;br&gt;&lt;br&gt;&lt;i&gt;Note: Files are sorted alphabetically before assignment.&lt;/i&gt;</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
@@ -195,6 +208,7 @@
<tabstops> <tabstops>
<tabstop>seriesLine</tabstop> <tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop> <tabstop>volumeLine</tabstop>
<tabstop>bulkVolumeCheck</tabstop>
<tabstop>titleLine</tabstop> <tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop> <tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop> <tabstop>writerLine</tabstop>
+350 -76
View File
@@ -22,7 +22,7 @@ import itertools
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView) from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer) from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os import os
@@ -38,7 +38,6 @@ from xml.sax.saxutils import escape
from psutil import Process from psutil import Process
from copy import copy from copy import copy
from packaging.version import Version from packaging.version import Version
from raven import Client
from tempfile import gettempdir from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
@@ -274,6 +273,12 @@ class WorkerThread(QThread):
options.format = gui_current_format options.format = gui_current_format
if GUI.mangaBox.isChecked(): if GUI.mangaBox.isChecked():
options.righttoleft = True options.righttoleft = True
if GUI.lightnovelBox.isChecked():
options.lightnovel = True
if GUI.ebokBox.isChecked():
options.ebok = True
if GUI.invertDirectionBox.isChecked():
options.invertdirection = True
if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2 options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked: elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
@@ -282,6 +287,8 @@ class WorkerThread(QThread):
options.autoscale = True options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked: elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True options.hq = True
if GUI.vertical4PanelBox.isChecked():
options.vertical4panel = True
if GUI.webtoonBox.isChecked(): if GUI.webtoonBox.isChecked():
options.webtoon = True options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -327,12 +334,12 @@ class WorkerThread(QThread):
options.maximizestrips = True options.maximizestrips = True
if GUI.disableProcessingBox.isChecked(): if GUI.disableProcessingBox.isChecked():
options.noprocessing = True options.noprocessing = True
if GUI.pdfExtractBox.isChecked(): if GUI.legacyExtractBox.isChecked():
options.pdfextract = True options.legacyextract = True
if GUI.pdfWidthBox.isChecked(): if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True options.pdfwidth = True
if GUI.noSmartCoverCropBox.isChecked(): if GUI.smartCoverCropBox.isChecked():
options.nosmartcovercrop = True options.smartcovercrop = True
if GUI.coverFillBox.isChecked(): if GUI.coverFillBox.isChecked():
options.coverfill = True options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -345,6 +352,8 @@ class WorkerThread(QThread):
options.tempdir = True options.tempdir = True
if GUI.spreadShiftBox.isChecked(): if GUI.spreadShiftBox.isChecked():
options.spreadshift = True options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked(): if GUI.fileFusionBox.isChecked():
options.filefusion = True options.filefusion = True
else: else:
@@ -378,6 +387,8 @@ class WorkerThread(QThread):
options.title = str(GUI.titleEdit.text()) options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text(): if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text()) options.author = str(GUI.authorEdit.text())
if GUI.languageEdit.text():
options.language = str(GUI.languageEdit.text())
if GUI.chunkSizeCheckBox.isChecked(): if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value()) options.targetsize = int(GUI.chunkSizeBox.value())
@@ -392,7 +403,10 @@ class WorkerThread(QThread):
for job in currentJobs: for job in currentJobs:
bookDir.append(job) bookDir.append(job)
try: try:
fusion_source_parent = str(Path(bookDir[0]).parent)
comic2ebook.options = comic2ebook.checkOptions(copy(options)) comic2ebook.options = comic2ebook.checkOptions(copy(options))
if options.output is None:
options.output = fusion_source_parent
currentJobs.clear() currentJobs.clear()
currentJobs.append(comic2ebook.makeFusion(bookDir)) currentJobs.append(comic2ebook.makeFusion(bookDir))
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False) MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
@@ -445,8 +459,6 @@ class WorkerThread(QThread):
_, _, traceback = sys.exc_info() _, _, traceback = sys.exc_info()
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error') % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
if ' is corrupted.' not in str(err):
GUI.sentry.captureException()
MW.addMessage.emit('Error during conversion! Please consult ' MW.addMessage.emit('Error during conversion! Please consult '
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> ' '<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
'for more details.', 'error', False) 'for more details.', 'error', False)
@@ -466,7 +478,7 @@ class WorkerThread(QThread):
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else: else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format: if 'MOBI' in gui_current_format and not options.lightnovel:
MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files') MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1)) MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
MW.progressBarTick.emit('tick') MW.progressBarTick.emit('tick')
@@ -507,7 +519,6 @@ class WorkerThread(QThread):
for item in outputPath: for item in outputPath:
GUI.progress.content = '' GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
os.remove(mobiPath + '_toclean')
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath): if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath):
try: try:
move(mobiPath, GUI.targetDirectory) move(mobiPath, GUI.targetDirectory)
@@ -528,8 +539,6 @@ class WorkerThread(QThread):
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
if os.path.exists(mobiPath): if os.path.exists(mobiPath):
os.remove(mobiPath) os.remove(mobiPath)
if os.path.exists(mobiPath + '_toclean'):
os.remove(mobiPath + '_toclean')
MW.addMessage.emit('Failed to process MOBI file!', 'error', False) MW.addMessage.emit('Failed to process MOBI file!', 'error', False)
MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical') MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
else: else:
@@ -558,12 +567,6 @@ class WorkerThread(QThread):
move(item, GUI.targetDirectory) move(item, GUI.targetDirectory)
except Exception: except Exception:
pass pass
if options.filefusion:
for path in currentJobs:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
GUI.progress.content = '' GUI.progress.content = ''
GUI.progress.stop() GUI.progress.stop()
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
@@ -627,7 +630,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.clear() GUI.jobList.clear()
if self.tar or self.sevenzip: if self.tar or self.sevenzip:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)') 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
else: else:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)') 'Comic (*.pdf);;All (*.*)')
@@ -658,30 +661,47 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self, sname): def selectFileMetaEditor(self, sname):
files = []
if not sname: if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier: if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) # Multi-directory selection for bulk editing ComicInfo.xml
if dname != '': dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
sname = os.path.join(dname, 'ComicInfo.xml') dialog.setFileMode(QFileDialog.FileMode.Directory)
self.lastPath = os.path.dirname(sname) dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
# Enable multi-selection in the dialog (may not work with native dialog on all platforms)
file_view = dialog.findChild(QListView, 'listView')
if file_view:
file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
file_tree = dialog.findChild(QTreeView)
if file_tree:
file_tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
if dialog.exec():
selected_dirs = dialog.selectedFiles()
if selected_dirs:
files = [os.path.join(d, 'ComicInfo.xml') for d in selected_dirs]
self.lastPath = os.path.dirname(selected_dirs[0])
else: else:
if self.sevenzip: if self.sevenzip:
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath, fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)') 'Comic (*.cbz *.cbr *.cb7)')
files = fnames[0]
if files:
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
else: else:
fname = ['']
self.showDialog("Editor is disabled due to a lack of 7z.", 'error') self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>' self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable metadata editing.', 'warning') ' to enable metadata editing.', 'warning')
if fname[0] != '': else:
sname = fname[0] files = [sname]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname: if files:
try: try:
self.editor.loadData(sname) self.editor.loadData(files)
except Exception as err: except Exception as err:
_, _, traceback = sys.exc_info() _, _, traceback = sys.exc_info()
GUI.sentry.captureException()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s" self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error') % (str(err), sanitizeTrace(traceback)), 'error')
else: else:
@@ -948,6 +968,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.chunkSizeCheckBox.setChecked(False) GUI.chunkSizeCheckBox.setChecked(False)
elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX': elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked) GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.upscaleBox.setChecked(True)
elif not GUI.webtoonBox.isChecked(): elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True) GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked(): if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
@@ -1066,8 +1087,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('startNumber', self.startNumber + 1) self.settings.setValue('startNumber', self.startNumber + 1)
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height())) self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(), self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'lightnovelBox': GUI.lightnovelBox.checkState(),
'ebokBox': GUI.ebokBox.checkState(),
'invertDirectionBox': GUI.invertDirectionBox.checkState(),
'languageEdit': GUI.languageEdit.text(),
'rotateBox': GUI.rotateBox.checkState(), 'rotateBox': GUI.rotateBox.checkState(),
'qualityBox': GUI.qualityBox.checkState(), 'qualityBox': GUI.qualityBox.checkState(),
'vertical4PanelBox': GUI.vertical4PanelBox.checkState(),
'gammaBox': GUI.gammaBox.checkState(), 'gammaBox': GUI.gammaBox.checkState(),
'autoLevelBox': GUI.autoLevelBox.checkState(), 'autoLevelBox': GUI.autoLevelBox.checkState(),
'autocontrastBox': GUI.autocontrastBox.checkState(), 'autocontrastBox': GUI.autocontrastBox.checkState(),
@@ -1082,9 +1108,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState(), 'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(), 'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(), 'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(), 'legacyExtractBox': GUI.legacyExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(), 'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(), 'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(), 'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(), 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(), 'mozJpegBox': GUI.mozJpegBox.checkState(),
@@ -1099,6 +1125,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'deleteBox': GUI.deleteBox.checkState(), 'deleteBox': GUI.deleteBox.checkState(),
'tempDirBox': GUI.tempDirBox.checkState(), 'tempDirBox': GUI.tempDirBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState(), 'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState(), 'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(), 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(), 'noRotateBox': GUI.noRotateBox.checkState(),
@@ -1122,7 +1149,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.clear() GUI.jobList.clear()
formats = ['.pdf'] formats = ['.pdf']
if self.tar or self.sevenzip: if self.tar or self.sevenzip:
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar']) formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
if os.path.isdir(message): if os.path.isdir(message):
GUI.jobList.addItem(message) GUI.jobList.addItem(message)
GUI.jobList.scrollToBottom() GUI.jobList.scrollToBottom()
@@ -1174,6 +1201,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.kindleGen = False self.kindleGen = False
if startup: if startup:
self.display_kindlegen_missing() self.display_kindlegen_missing()
except OSError as e:
self.kindleGen = False
if startup:
error = f"kindlegen: {e.strerror}\n\n Re-install Rosetta/Kindle Previewer/other Intel app?\n\nPlease email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help"
self.showDialog(error, 'error')
def __init__(self, kccapp, kccwindow): def __init__(self, kccapp, kccwindow):
global APP, MW, GUI global APP, MW, GUI
@@ -1183,7 +1215,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW) self.setupUi(MW)
self.editor = KCCGUI_MetaEditor() self.editor = KCCGUI_MetaEditor()
self.icons = Icons() self.icons = Icons()
self.settings = QSettings('ciromattia', 'kcc9') self.settings = QSettings('ciromattia', 'kcc10')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str) self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str)) self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
@@ -1209,7 +1241,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.croppingPowerValue = 1.0 self.croppingPowerValue = 1.0
self.currentMode = 1 self.currentMode = 1
self.targetDirectory = '' self.targetDirectory = ''
self.sentry = Client(release=__version__)
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from psutil import BELOW_NORMAL_PRIORITY_CLASS from psutil import BELOW_NORMAL_PRIORITY_CLASS
@@ -1261,6 +1292,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 1240x1860": { "Kindle 1240x1860": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
}, },
"Kindle 1324x1986": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1324',
},
"Kindle Scribe 1/2": { "Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
}, },
@@ -1368,6 +1402,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator", "Separator",
"Other", "Other",
"Separator", "Separator",
"Kindle 1324x1986",
"Kindle 1920x1920", "Kindle 1920x1920",
"Kindle 1860x1920", "Kindle 1860x1920",
"Kindle 1240x1860", "Kindle 1240x1860",
@@ -1494,6 +1529,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.widthBox.setValue(int(self.options[option])) GUI.widthBox.setValue(int(self.options[option]))
elif str(option) == "heightBox": elif str(option) == "heightBox":
GUI.heightBox.setValue(int(self.options[option])) GUI.heightBox.setValue(int(self.options[option]))
elif str(option) == "languageEdit":
GUI.languageEdit.setText(str(self.options[option]))
elif str(option) == "gammaSlider": elif str(option) == "gammaSlider":
if GUI.gammaSlider.isEnabled(): if GUI.gammaSlider.isEnabled():
GUI.gammaSlider.setValue(int(self.options[option])) GUI.gammaSlider.setValue(int(self.options[option]))
@@ -1532,62 +1569,299 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog): class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
def loadData(self, file): def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
self.parser = metadata.MetadataParser(file) note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
if self.parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False) if len(valuesByFile) <= 20:
self.okButton.setEnabled(False) rows = ''.join(
self.statusLabel.setText('CBR metadata are read-only.') '<tr>'
f'<td style="padding:2px 6px; white-space:nowrap;">{escape(os.path.basename(f))}</td>'
f'<td style="padding:2px 6px;">{escape(v)}</td>'
'</tr>'
for f, v in valuesByFile
)
table = (
'<table border="1" cellspacing="0" cellpadding="0">'
'<tr>'
'<th style="padding:2px 6px; text-align:left;">File</th>'
'<th style="padding:2px 6px; text-align:left;">Value</th>'
'</tr>'
f'{rows}'
'</table>'
)
else: else:
counts = {}
for _, v in valuesByFile:
counts[v] = counts.get(v, 0) + 1
rows = ''.join(
'<tr>'
f'<td style="padding:2px 6px;">{escape(v)}</td>'
f'<td style="padding:2px 6px; text-align:right;">{c}</td>'
'</tr>'
for v, c in sorted(counts.items(), key=lambda t: (-t[1], t[0]))
)
table = (
'<table border="1" cellspacing="0" cellpadding="0">'
'<tr>'
'<th style="padding:2px 6px; text-align:left;">Value</th>'
'<th style="padding:2px 6px; text-align:right;">Count</th>'
'</tr>'
f'{rows}'
'</table>'
)
tooltipHTML = f'\
<b>{escape(fieldLabel)}</b>\
{note}\
{table}\
'
return tooltipHTML
def loadData(self, files):
self.files = files if isinstance(files, list) else [files]
self.bulkMode = len(self.files) > 1
# Sort files by name for consistent volume assignment
self.files.sort()
# Unified CBR check for all files (both single and bulk mode)
for file in self.files:
parser = metadata.MetadataParser(file)
if parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False)
self.okButton.setEnabled(False)
self.statusLabel.setText('CBR files in selection are read-only.')
return
if self.bulkMode:
firstFile = self.files[0]
self.parser = metadata.MetadataParser(firstFile)
self.editorWidget.setEnabled(True)
self.okButton.setEnabled(True)
self.statusLabel.setText(f'Editing {len(self.files)} files.')
# Show bulk volume checkbox
self.bulkVolumeCheck.setVisible(True)
self.bulkVolumeCheck.setChecked(False)
for field in (self.volumeLine, self.numberLine, self.titleLine):
field.setEnabled(False)
field.setText('')
field.setPlaceholderText('(multiple files)')
field.setToolTip('')
# Load metadata for all files and show common values, or “(multiple values)” + tooltip.
parsed = []
for file in self.files:
parsed.append((file, metadata.MetadataParser(file)))
field_specs = [
(self.seriesLine, 'Series', lambda p: (p.data.get('Series', '') or '')),
(self.writerLine, 'Writer', lambda p: ', '.join(p.data.get('Writers', []) or [])),
(self.pencillerLine, 'Penciller', lambda p: ', '.join(p.data.get('Pencillers', []) or [])),
(self.inkerLine, 'Inker', lambda p: ', '.join(p.data.get('Inkers', []) or [])),
(self.coloristLine, 'Colorist', lambda p: ', '.join(p.data.get('Colorists', []) or [])),
]
for line, label, extractor in field_specs:
line.setEnabled(True)
valuesByFile = [(f, extractor(p)) for f, p in parsed]
uniqueValues = {v for _, v in valuesByFile}
if len(uniqueValues) == 1:
common_value = valuesByFile[0][1] if valuesByFile else ''
line.setPlaceholderText('')
line.setToolTip('')
line.setText(common_value)
else:
line.setText('')
line.setPlaceholderText('(multiple values)')
line.setToolTip(self._buildBulkFieldToolTip(label, valuesByFile))
else:
file = self.files[0]
self.parser = metadata.MetadataParser(file)
# Hide bulk volume checkbox in single file mode
self.bulkVolumeCheck.setVisible(False)
for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine,
self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setEnabled(True)
field.setPlaceholderText('')
self.editorWidget.setEnabled(True) self.editorWidget.setEnabled(True)
self.okButton.setEnabled(True) self.okButton.setEnabled(True)
self.statusLabel.setText('Separate authors with a comma.') self.statusLabel.setText('Separate authors with a comma.')
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
for field in (self.seriesLine, self.titleLine): field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
if field.text() == '': for field in (self.seriesLine, self.titleLine):
path = Path(file) if field.text() == '':
if file.endswith('.xml'): path = Path(file)
field.setText(path.parent.name) if file.endswith('.xml'):
else: field.setText(path.parent.name)
field.setText(path.stem) else:
field.setText(path.stem)
def saveData(self): def saveData(self):
for field in (self.volumeLine, self.numberLine): if self.bulkMode:
if field.text().isnumeric() or self.cleanData(field.text()) == '': bulkData = {}
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text()) if self.cleanData(self.seriesLine.text()):
else: bulkData['Series'] = self.cleanData(self.seriesLine.text())
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break
else:
for field in (self.seriesLine, self.titleLine):
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
fieldName = field.objectName().capitalize()[:-4] + 's'
values = self.cleanData(field.text()).split(',') values = self.cleanData(field.text()).split(',')
tmpData = [] tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
for value in values: if tmpData:
if self.cleanData(value) != '': bulkData[fieldName] = tmpData
tmpData.append(self.cleanData(value)) # Handle bulk volume editing
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData volumes = None
try: if self.bulkVolumeCheck.isChecked():
self.parser.saveXML() volumeText = self.volumeLine.text()
except Exception as err: volumes, error = self.parseVolumeInput(volumeText, len(self.files))
_, _, traceback = sys.exc_info() if error:
GUI.sentry.captureException() self.statusLabel.setText(error)
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s" return
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close() if not bulkData and volumes is None:
self.statusLabel.setText('No changes to apply.')
return
errors = []
total = len(self.files)
self.okButton.setEnabled(False)
self.cancelButton.setEnabled(False)
for i, file in enumerate(self.files, 1):
self.statusLabel.setText(f'Processing {i}/{total}: {os.path.basename(file)}')
QApplication.processEvents()
try:
parser = metadata.MetadataParser(file)
if parser.format in ['RAR', 'RAR5']:
errors.append(f'{os.path.basename(file)}: CBR is read-only')
continue
for key, value in bulkData.items():
parser.data[key] = value
# Set volume if bulk volume editing is enabled
if volumes is not None:
parser.data['Volume'] = str(volumes[i - 1])
parser.saveXML()
except Exception as err:
errors.append(f'{os.path.basename(file)}: {str(err)}')
self.okButton.setEnabled(True)
self.cancelButton.setEnabled(True)
if errors:
GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) +
(f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error')
self.statusLabel.setText('Errors occurred.')
else:
self.statusLabel.setText(f'Successfully updated {total} files.')
self.ui.close()
else:
for field in (self.volumeLine, self.numberLine):
if field.text().isnumeric() or self.cleanData(field.text()) == '':
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
else:
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break
else:
for field in (self.seriesLine, self.titleLine):
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
values = self.cleanData(field.text()).split(',')
tmpData = []
for value in values:
if self.cleanData(value) != '':
tmpData.append(self.cleanData(value))
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
try:
self.parser.saveXML()
except Exception as err:
_, _, traceback = sys.exc_info()
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close()
def cleanData(self, s): def cleanData(self, s):
return escape(s.strip()) return escape(s.strip())
def parseVolumeInput(self, text, fileCount):
text = text.strip()
if not text:
return None, None
volumes = []
# Check if it's a range (e.g., "5-10")
if '-' in text and ',' not in text:
parts = text.split('-')
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
return None, 'Invalid range format (use start-end)'
try:
start = int(parts[0].strip())
end = int(parts[1].strip())
if start < 0 or end < 0:
return None, 'Volume numbers must be positive'
if start > end:
return None, 'Invalid range: start > end'
volumes = list(range(start, end + 1))
except ValueError:
return None, 'Invalid range format'
# Check if it's a comma-separated list (e.g., "1,3,5")
elif ',' in text:
try:
volumes = [int(v.strip()) for v in text.split(',') if v.strip()]
if any(v < 0 for v in volumes):
return None, 'Volume numbers must be positive'
except ValueError:
return None, 'Invalid list format'
# Single number - generate sequence starting from that number
else:
try:
start = int(text)
if start < 0:
return None, 'Volume number must be positive'
volumes = list(range(start, start + fileCount))
except ValueError:
return None, 'Invalid number'
# Validate count
if not volumes:
return None, 'No valid volume numbers parsed'
if len(volumes) != fileCount:
return None, f'Volume count ({len(volumes)}) != file count ({fileCount})'
return volumes, None
def toggleBulkVolume(self, checked):
self.volumeLine.setEnabled(checked)
if checked:
self.volumeLine.setText('')
self.volumeLine.setPlaceholderText('e.g., 5 or 1-10 or 1,3,5')
else:
self.volumeLine.setText('')
self.volumeLine.setPlaceholderText('(multiple files)')
def __init__(self): def __init__(self):
self.ui = QDialog() self.ui = QDialog()
self.parser = None self.parser = None
self.files = []
self.bulkMode = False
self.setupUi(self.ui) self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint) self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume)
self.okButton.clicked.connect(self.saveData) self.okButton.clicked.connect(self.saveData)
self.cancelButton.clicked.connect(self.ui.close) self.cancelButton.clicked.connect(self.ui.close)
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
File diff suppressed because it is too large Load Diff
+15 -4
View File
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout, from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
QLabel, QLineEdit, QPushButton, QSizePolicy, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QVBoxLayout, QWidget) QSizePolicy, QVBoxLayout, QWidget)
from . import KCC_rc from . import KCC_rc
class Ui_editorDialog(object): class Ui_editorDialog(object):
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1) self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
self.bulkVolumeCheck = QCheckBox(self.editorWidget)
self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck")
self.bulkVolumeCheck.setVisible(False)
self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1)
self.label_3 = QLabel(self.editorWidget) self.label_3 = QLabel(self.editorWidget)
self.label_3.setObjectName(u"label_3") self.label_3.setObjectName(u"label_3")
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget) self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine) QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine) QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine) QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine) QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine) QWidget.setTabOrder(self.writerLine, self.pencillerLine)
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None)) editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None)) self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None)) self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
#if QT_CONFIG(tooltip)
self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"<b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> \u2192 sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> \u2192 range from 1 to 10<br><code>1, 3, 5</code> \u2192 specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i>", None))
#endif // QT_CONFIG(tooltip)
self.bulkVolumeCheck.setText("")
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None)) self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None)) self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None)) self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.1.0' __version__ = '10.3.0'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi' __copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
+222 -93
View File
@@ -22,7 +22,9 @@ from collections import Counter
import os import os
import pathlib import pathlib
import re import re
import shutil
import sys import sys
import xml.etree.ElementTree as ET
from argparse import ArgumentParser from argparse import ArgumentParser
from time import perf_counter, strftime, gmtime from time import perf_counter, strftime, gmtime
from copy import copy from copy import copy
@@ -30,14 +32,14 @@ from glob import glob, escape
from re import sub from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipFile, ZIP_STORED
from tempfile import mkdtemp, gettempdir, TemporaryFile from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from uuid import uuid4 from uuid import uuid4
from natsort import os_sort_keygen, os_sorted from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext from slugify import slugify as slugify_ext
from PIL import Image, ImageFile from PIL import Image, ImageFile, ImageOps
from pathlib import Path from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage from psutil import virtual_memory, disk_usage
@@ -74,22 +76,19 @@ def main(argv=None):
print('No matching files found.') print('No matching files found.')
return 1 return 1
if options.filefusion: if options.filefusion:
fusion_source_parent = str(Path(sources[0]).parent)
fusion_path = makeFusion(list(sources)) fusion_path = makeFusion(list(sources))
sources.clear() sources.clear()
sources.append(fusion_path) sources.append(fusion_path)
for source in sources: for source in sources:
source = source.rstrip('\\').rstrip('/') source = source.rstrip('\\').rstrip('/')
options = copy(args) options = copy(args)
if options.filefusion and options.output is None:
options.output = fusion_source_parent
options = checkOptions(options) options = checkOptions(options)
print('Working on ' + source + '...') print('Working on ' + source + '...')
makeBook(source) makeBook(source)
if options.filefusion:
for path in sources:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
return 0 return 0
@@ -144,7 +143,7 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
f.write('<div style="display:none;">.</div>\n') f.write('<div style="display:none;">.</div>\n')
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n') f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
if imgfile2: if imgfile2:
f.write(f'<img style="bottom: 0" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n') f.write(f'<img style="top: 1920px" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
f.write("</div>\n") f.write("</div>\n")
if options.iskindle and options.panelview: if options.iskindle and options.panelview:
if options.autoscale: if options.autoscale:
@@ -227,7 +226,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx') ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
f = open(ncxfile, "w", encoding='UTF-8') f = open(ncxfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n", f"<ncx version=\"2005-1\" xml:lang=\"{options.language}\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
"<head>\n", "<head>\n",
"<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n", "<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
"<meta name=\"dtb:depth\" content=\"1\"/>\n", "<meta name=\"dtb:depth\" content=\"1\"/>\n",
@@ -293,10 +292,22 @@ def buildNAV(dstdir, title, chapters, chapternames):
def buildOPF(dstdir, title, filelist, originalpath, cover=None): def buildOPF(dstdir, title, filelist, originalpath, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
deviceres = options.profileData[1] deviceres = options.profileData[1]
if options.righttoleft:
writingmode = "horizontal-rl" if options.vertical4panel:
writingmode = "vertical"
else: else:
writingmode = "horizontal-lr" writingmode = "horizontal"
if options.invertdirection:
if options.righttoleft:
writingmode += "-lr"
else:
writingmode += "-rl"
else:
if options.righttoleft:
writingmode += "-rl"
else:
writingmode += "-lr"
f = open(opffile, "w", encoding='UTF-8') f = open(opffile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<package version=\"3.0\" unique-identifier=\"BookID\" ", "<package version=\"3.0\" unique-identifier=\"BookID\" ",
@@ -304,7 +315,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ", "<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n", "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
"<dc:title>", hescape(title), "</dc:title>\n", "<dc:title>", hescape(title), "</dc:title>\n",
"<dc:language>en-US</dc:language>\n", f"<dc:language>{options.language}</dc:language>\n",
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n", "<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"]) "<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
if len(options.summary) > 0: if len(options.summary) > 0:
@@ -395,13 +406,22 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
else: else:
return "" return ""
if options.righttoleft: if options.invertdirection:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n") if options.righttoleft:
pageside = "right" f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
else:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
pageside = "right"
else: else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n") if options.righttoleft:
pageside = "left" f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
if originalpath.lower().endswith('.pdf'): pageside = "right"
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
if originalpath.lower().endswith('.pdf') or originalpath.lower().endswith('.epub'):
if pageside == "right": if pageside == "right":
pageside = "left" pageside = "left"
else: else:
@@ -466,6 +486,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
pageside = "right" pageside = "right"
for entry, prop in zip(reflist, page_spread_property_list): for entry, prop in zip(reflist, page_spread_property_list):
if options.onepagelandscape:
prop = 'center'
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n') f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
f.write("</spine>\n</package>\n") f.write("</spine>\n</package>\n")
@@ -892,12 +914,17 @@ def getWorkFolder(afile, workdir=None):
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else: else:
fullPath = workdir fullPath = workdir
check_path = gettempdir()
if options.tempdir: if options.tempdir:
check_path = os.path.dirname(afile) check_path = os.path.dirname(afile)
else:
check_path = gettempdir()
DISK_WARNING = "Not enough disk space to perform conversion. Try Temp Directory option."
if os.path.isdir(afile): if os.path.isdir(afile):
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5: if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.") raise UserWarning(DISK_WARNING)
try: try:
copytree(afile, fullPath) copytree(afile, fullPath)
sanitizePermissions(fullPath) sanitizePermissions(fullPath)
@@ -907,13 +934,13 @@ def getWorkFolder(afile, workdir=None):
raise UserWarning("Failed to prepare a workspace.") raise UserWarning("Failed to prepare a workspace.")
elif os.path.isfile(afile): elif os.path.isfile(afile):
if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5: if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.") raise UserWarning(DISK_WARNING)
if afile.lower().endswith('.pdf'): if afile.lower().endswith('.pdf'):
if not os.path.exists(fullPath): if not os.path.exists(fullPath):
os.makedirs(fullPath) os.makedirs(fullPath)
path = workdir path = workdir
sanitizePermissions(path) sanitizePermissions(path)
if options.pdfextract: if options.legacyextract:
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath) pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
njpg = pdf.extract() njpg = pdf.extract()
if njpg == 0: if njpg == 0:
@@ -938,6 +965,8 @@ def getWorkFolder(afile, workdir=None):
try: try:
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(fullPath) path = cbx.extract(fullPath)
if options.lightnovel:
return workdir
sanitizePermissions(path) sanitizePermissions(path)
tdir = os.listdir(fullPath) tdir = os.listdir(fullPath)
@@ -952,11 +981,69 @@ def getWorkFolder(afile, workdir=None):
for file in os.listdir(os.path.join(fullPath, tdir[0])): for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath) move(os.path.join(fullPath, tdir[0], file), fullPath)
os.rmdir(os.path.join(fullPath, tdir[0])) os.rmdir(os.path.join(fullPath, tdir[0]))
if options.legacyextract:
return workdir
if afile.lower().endswith('.epub'):
container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
opf_path = os.path.join(path, opf_path)
opf = ET.parse(opf_path)
spine = []
for spine_item in opf.findall(r'.//{*}itemref'):
spine.append(spine_item.attrib.get('idref'))
manifest_dict = {}
for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
ordered_image_paths = []
for i, spine_item in enumerate(spine):
try:
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
except Exception:
continue
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
largest_size = 0
img_path = None
for img in imgs:
for key in img.attrib:
if 'src' in key or 'href' in key:
temp_img_path = img.attrib[key]
if temp_img_path.startswith('..'):
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
else:
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
try:
temp_size = os.path.getsize(temp_img_path)
if temp_size > largest_size:
largest_size = temp_size
img_path = temp_img_path
except OSError:
pass
# TODO empty image
if img_path:
ordered_image_paths.append(img_path)
# fallback if naive spine extraction fails
if not ordered_image_paths:
return workdir
if options.tempdir:
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
else:
workdir2 = mkdtemp('', 'KCC-')
for i, img_path in enumerate(ordered_image_paths):
_, ext = os.path.splitext(img_path)
fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
os.makedirs(fullpath2, exist_ok=True)
shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
rmtree(workdir, True)
return workdir2
return workdir return workdir
finally:
except OSError as e: pass
rmtree(workdir, True)
raise UserWarning(e)
else: else:
raise UserWarning("Failed to open source file/directory.") raise UserWarning("Failed to open source file/directory.")
@@ -1321,7 +1408,6 @@ def slugify(value, is_natural_sorted):
def makeZIP(zipfilename, basedir, job_progress='', isepub=False): def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools(): if SEVENZIP in available_archive_tools():
if isepub: if isepub:
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
@@ -1364,10 +1450,18 @@ def makeParser():
" [Default=KV]") " [Default=KV]")
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False, main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (right-to-left reading and splitting)") help="Manga style (right-to-left reading and splitting)")
main_options.add_argument("--lightnovel", action="store_true", dest="lightnovel", default=False,
help="Only resize images and preserve original file structure.")
main_options.add_argument("--ebok", action="store_true", dest="ebok", default=False,
help="Force EBOK tag instead of PDOC for MOBI")
main_options.add_argument("--invertdirection", action="store_true", dest="invertdirection", default=False,
help="Invert page turn direction")
main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False, main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
help="Try to increase the quality of magnification") help="Try to increase the quality of magnification")
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False, main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
help="Display two not four panels in Panel View mode") help="Display two not four panels in Panel View mode")
main_options.add_argument("--vertical4panel", action="store_true", dest="vertical4panel", default=False,
help="Display side panels first in virtual panel view")
main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False, main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
help="Webtoon processing mode"), help="Webtoon processing mode"),
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None, main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
@@ -1383,6 +1477,8 @@ def makeParser():
"2: Use Title only") "2: Use Title only")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor", output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]") help="Author name [Default=KCC]")
output_options.add_argument("--language", action="store", dest="language", default="en-US",
help="EPUB language [Default=en-US]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto", output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) " help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]") "[Default=Auto]")
@@ -1393,6 +1489,8 @@ def makeParser():
"2: Consider every subdirectory as separate volume [Default=0]") "2: Consider every subdirectory as separate volume [Default=0]")
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False, output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
help="Shift first page to opposite side in landscape for spread alignment") help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
help="Show a single centered page in landscape")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False, output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.") help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False, output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
@@ -1402,12 +1500,12 @@ def makeParser():
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False, processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option") help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False, processing_options.add_argument("--legacyextract", action="store_true", dest="legacyextract", default=False,
help="Use the legacy PDF image extraction method from KCC 8 and earlier") help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False, processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
help="Render vector PDFs to device width instead of height.") help="Render vector PDFs to device width instead of height.")
processing_options.add_argument("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False, processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
help="Disable attempt to crop main cover from wide image") help="Attempt to crop main cover from wide image")
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False, processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen") help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False, processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
@@ -1488,6 +1586,9 @@ def checkOptions(options):
else: else:
options.isKobo = True options.isKobo = True
if options.lightnovel:
options.noKepub = True
if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format): if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles') raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
@@ -1607,35 +1708,37 @@ def checkTools(source):
except (FileNotFoundError, CalledProcessError): except (FileNotFoundError, CalledProcessError):
print('ERROR: KindleGen is missing!') print('ERROR: KindleGen is missing!')
sys.exit(1) sys.exit(1)
except OSError as e:
print(f"kindlegen: {e.strerror}")
print('Re-install Rosetta/Kindle Previewer/other Intel app?')
print('Please email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help')
sys.exit(1)
def checkPre(source): def checkPre(source='KCC-'):
# Make sure that all temporary files are gone # Make sure that all temporary files are gone
for root, dirs, _ in walkLevel(gettempdir(), 0): for root, dirs, _ in walkLevel(gettempdir(), 0):
for tempdir in dirs: for tempdir in dirs:
if tempdir.startswith('KCC-'): if tempdir.startswith(source):
rmtree(os.path.join(root, tempdir), True) rmtree(os.path.join(root, tempdir), True)
# Make sure that target directory is writable
if os.path.isdir(source):
src = os.path.abspath(os.path.join(source, '..'))
else:
src = os.path.dirname(source)
try:
with TemporaryFile(prefix='KCC-', dir=src):
pass
except Exception:
raise UserWarning("Target directory is not writable.")
def makeFusion(sources: List[str]): def makeFusion(sources: List[str]):
if len(sources) < 2: if len(sources) < 2:
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?') raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter() start = perf_counter()
first_path = Path(sources[0]) first_path = Path(sources[0])
if first_path.is_file():
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]') if options.tempdir:
fusion_parent = first_path.parent
else: else:
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]') # LLL is after KCC
checkPre('LLL-')
fusion_parent = Path(mkdtemp('', 'LLL-'))
if first_path.is_file():
fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
else:
fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
print("Running Fusion") print("Running Fusion")
# Check if prefix is needed when user-specified ordering differs from OS natural sorting # Check if prefix is needed when user-specified ordering differs from OS natural sorting
@@ -1644,7 +1747,6 @@ def makeFusion(sources: List[str]):
for index, source in enumerate(sources, start=1): for index, source in enumerate(sources, start=1):
print(f"Processing {source}...") print(f"Processing {source}...")
checkPre(source)
print("Checking images...") print("Checking images...")
source_path = Path(source) source_path = Path(source)
# Add the fusion_0001_ prefix to maintain user-specified order if needed # Add the fusion_0001_ prefix to maintain user-specified order if needed
@@ -1656,7 +1758,9 @@ def makeFusion(sources: List[str]):
else: else:
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}') targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
getWorkFolder(source, str(targetpath)) path = getWorkFolder(source, str(targetpath))
if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
sanitizeTree(targetpath, prefix='fusion') sanitizeTree(targetpath, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported # TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath) flattenTree(targetpath)
@@ -1676,10 +1780,41 @@ def makeBook(source, qtgui=None, job_progress=''):
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
else: else:
checkTools(source) checkTools(source)
checkPre(source) checkPre()
if not options.filefusion:
checkPre('LLL-')
print(f"{job_progress}Preparing source images...") print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source)
print(f"{job_progress}Checking images...") print(f"{job_progress}Checking images...")
if options.lightnovel:
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for file in files:
_, ext = os.path.splitext(file)
if ext.lower() in ('.jpg', '.jpeg', '.png', '.webp', '.gif'):
with Image.open(os.path.join(root, file)) as img:
# TODO: detect BW images saved as RGB
if not options.forcecolor:
if img.mode == 'RGB':
img = img.convert('L')
elif img.mode == 'RGBA':
img = img.convert('LA')
x, y = image.ProfileData.Profiles[options.profile][1]
if options.iskindle:
x = min(x, 1920)
y = min(y, 1920)
if img.size[0] > x or img.size[1] > y:
img = ImageOps.contain(img, (x, y))
img.save(os.path.join(root, file), quality=options.jpegquality)
_, ext = os.path.splitext(source)
if ext != '.epub':
ext = '.cbz'
output_file = getOutputFilename(source, options.output, ext, '')
makeZIP(output_file, os.path.join(path, 'OEBPS', 'Images'), job_progress)
rmtree(path, True)
return [output_file]
getMetadata(os.path.join(path, "OEBPS", "Images"), source) getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images")) removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
@@ -1706,27 +1841,27 @@ def makeBook(source, qtgui=None, job_progress=''):
size = get_contain_resolution(imagef, (x, y)) size = get_contain_resolution(imagef, (x, y))
normalized_resolutions.append(size) normalized_resolutions.append(size)
counter = Counter(normalized_resolutions) counter = Counter(normalized_resolutions)
aspect_ratios = [] aspect_ratios = []
filtered_resolutions = [] filtered_resolutions = []
for w, h in normalized_resolutions: for w, h in normalized_resolutions:
aspect_ratio = h / w aspect_ratio = h / w
# page-like aspect ratios, could be improved # page-like aspect ratios, could be improved
if aspect_ratio > 1.3 and aspect_ratio < 1.7: if aspect_ratio > 1.3 and aspect_ratio < 1.7:
aspect_ratios.append(aspect_ratio) aspect_ratios.append(aspect_ratio)
filtered_resolutions.append((w, h)) filtered_resolutions.append((w, h))
most_common_res, most_common_count = counter.most_common(1)[0] most_common_res, most_common_count = counter.most_common(1)[0]
options.kfx_resolution = most_common_res options.kfx_resolution = most_common_res
if most_common_count / counter.total() > .6: if most_common_count / sum(counter.values()) > .6:
pass pass
#elif max(aspect_ratios) - min(aspect_ratios) < .2: #elif max(aspect_ratios) - min(aspect_ratios) < .2:
else: else:
# get the widest resolution # get the widest resolution
options.kfx_resolution = max(filtered_resolutions) options.kfx_resolution = max(filtered_resolutions)
# else: # else:
# raise UserWarning('Aspect ratio of pages too different for KFX conversion') # raise UserWarning('Aspect ratio of pages too different for KFX conversion')
if options.noprocessing: if options.noprocessing:
print(f"{job_progress}Do not process image, ignore any profile or processing option") print(f"{job_progress}Do not process image, ignore any profile or processing option")
@@ -1770,7 +1905,7 @@ def makeBook(source, qtgui=None, job_progress=''):
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if cover and cover.smartcover: if cover and cover.smartcover:
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes)) cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress) makeZIP(filepath[-1], os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF': elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...") print(f"{job_progress}Creating PDF file with PyMuPDF...")
# determine output filename based on source and tome count # determine output filename based on source and tome count
@@ -1789,19 +1924,11 @@ def makeBook(source, qtgui=None, job_progress=''):
else: else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress) buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, job_progress, True) makeZIP(filepath[-1], tome, job_progress, True)
# Copy files to final destination (PDF files are already saved directly)
if options.format != 'PDF':
copyfile(tome + '_comic.zip', filepath[-1])
try:
os.remove(tome + '_comic.zip')
except FileNotFoundError:
# newly temporary created file is not found. It might have been already deleted
pass
rmtree(tome, True) rmtree(tome, True)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI': if not GUI and options.format == 'MOBI' and not options.lightnovel:
print(f"{job_progress}Creating MOBI files...") print(f"{job_progress}Creating MOBI files...")
work = [] work = []
for i in filepath: for i in filepath:
@@ -1820,8 +1947,6 @@ def makeBook(source, qtgui=None, job_progress=''):
if not output[0]: if not output[0]:
print(f'{job_progress}Error: Failed to tweak KindleGen output!') print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath return filepath
else:
os.remove(i.replace('.epub', '.mobi') + '_toclean')
if cover and k.path and k.coverSupport: if cover and k.path and k.coverSupport:
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1]) options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
if options.delete: if options.delete:
@@ -1832,22 +1957,23 @@ def makeBook(source, qtgui=None, job_progress=''):
end = perf_counter() end = perf_counter()
print(f"{job_progress}makeBook: {end - start} seconds") print(f"{job_progress}makeBook: {end - start} seconds")
# Clean up temporary workspace
try: if options.filefusion:
rmtree(path, True) rmtree(source, True)
except Exception: checkPre('LLL-')
pass
return filepath return filepath
def makeMOBIFix(item, uuid): def makeMOBIFix(item, uuid):
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys() is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
if options.ebok:
is_pdoc = False
if not options.keep_epub: if not options.keep_epub:
os.remove(item) os.remove(item)
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean')
try: try:
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc) dualmetafix.DualMobiMetaFix(mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
return [True] return [True]
except Exception as err: except Exception as err:
return [False, format(err)] return [False, format(err)]
@@ -1869,8 +1995,11 @@ def makeMOBIWorker(item):
kindlegenError = '' kindlegenError = ''
try: try:
if os.path.getsize(item) < 629145600: if os.path.getsize(item) < 629145600:
start = perf_counter()
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item], output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True) stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
end = perf_counter()
print(f"kindlegen: {end - start} sec")
else: else:
# ERROR: EPUB too big # ERROR: EPUB too big
kindlegenErrorCode = 23026 kindlegenErrorCode = 23026
+1 -2
View File
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
class DualMobiMetaFix: class DualMobiMetaFix:
def __init__(self, infile, outfile, asin, is_pdoc): def __init__(self, outfile, asin, is_pdoc):
cdetype = b'EBOK' cdetype = b'EBOK'
if is_pdoc: if is_pdoc:
cdetype = b'PDOC' cdetype = b'PDOC'
shutil.copyfile(infile, outfile)
f = open(outfile, "r+b") f = open(outfile, "r+b")
self.datain = mmap.mmap(f.fileno(), 0) self.datain = mmap.mmap(f.fileno(), 0)
self.datain_rec0 = readsection(self.datain, 0) self.datain_rec0 = readsection(self.datain, 0)
+22 -16
View File
@@ -106,6 +106,7 @@ class ProfileData:
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0), 'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0), 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0), 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
@@ -164,7 +165,10 @@ class ComicPageParser:
with Image.open(srcImgPath) as im: with Image.open(srcImgPath) as im:
self.image = im.copy() self.image = im.copy()
self.fill = self.fillCheck() self.page_background_color = self.fillCheck()
self.fill = self.page_background_color
if self.opt.bordersColor:
self.fill = self.opt.bordersColor
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@@ -194,9 +198,9 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2))) new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0)) new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height)) new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.fill]) self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
elif self.opt.webtoon: elif self.opt.webtoon:
self.payload.append(['N', self.source, self.image, self.fill]) self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
# rotate only TODO dead code? # rotate only TODO dead code?
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1: elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
spread = self.image spread = self.image
@@ -205,11 +209,12 @@ class ComicPageParser:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else: else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True) spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill]) self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
# elif wide enough to split # elif wide enough to split
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16: elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
# if (split) or (split and rotate) # if (split) or (split and rotate)
if self.opt.splitter != 1 and width / height < 1.75: BISECT_THRESHOLD = 1.8
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
if width > height: if width > height:
leftbox = (0, 0, int(width / 2), height) leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height) rightbox = (int(width / 2), 0, width, height)
@@ -222,23 +227,23 @@ class ComicPageParser:
else: else:
pageone = self.image.crop(leftbox) pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox) pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill]) self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill]) self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
# if (rotate) or (split and rotate) # if (rotate) or (split and rotate)
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75): if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
if not self.opt.rotateright: if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True) spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else: else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True) spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill]) self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
else: else:
self.payload.append(['N', self.source, self.image, self.fill]) self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
def fillCheck(self): def fillCheck(self):
if self.opt.bordersColor: if False:
return self.opt.bordersColor return self.opt.bordersColor
else: else:
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1') bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
@@ -277,7 +282,7 @@ class ComicPageParser:
class ComicPage: class ComicPage:
def __init__(self, options, mode, path, image, fill): def __init__(self, options, mode, path, image, page_background_color, fill):
self.opt = options self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData _, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq: if self.opt.hq:
@@ -287,6 +292,7 @@ class ComicPage:
self.image = image.convert("RGB") self.image = image.convert("RGB")
self.color = self.colorCheck() self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor self.colorOutput = self.color and self.opt.forcecolor
self.page_background_color = page_background_color
self.fill = fill self.fill = fill
self.rotated = False self.rotated = False
self.orgPath = os.path.join(path[0], path[1]) self.orgPath = os.path.join(path[0], path[1])
@@ -560,7 +566,7 @@ class ComicPage:
self.image = self.image.crop(box) self.image = self.image.crop(box)
def cropPageNumber(self, power, minimum): def cropPageNumber(self, power, minimum):
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill) bbox = get_bbox_crop_margin_page_number(self.image, power, self.page_background_color)
if bbox: if bbox:
w, h = self.image.size w, h = self.image.size
@@ -570,7 +576,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum): def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.fill) bbox = get_bbox_crop_margin(self.image, power, self.page_background_color)
if bbox: if bbox:
w, h = self.image.size w, h = self.image.size
@@ -580,7 +586,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction): def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill) self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
class Cover: class Cover:
def __init__(self, source, opt): def __init__(self, source, opt):
@@ -598,7 +604,7 @@ class Cover:
self.image = ImageOps.autocontrast(self.image, preserve_tone=True) self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor: if not self.options.forcecolor:
self.image = self.image.convert('L') self.image = self.image.convert('L')
if not self.options.nosmartcovercrop: if self.options.smartcovercrop:
self.crop_main_cover() self.crop_main_cover()
size = list(self.options.profileData[1]) size = list(self.options.profileData[1])
-4
View File
@@ -122,10 +122,6 @@ def dependencyCheck(level):
missing.append('PySide 6.0.0') missing.append('PySide 6.0.0')
except ImportError: except ImportError:
missing.append('PySide 6.0.0+') missing.append('PySide 6.0.0+')
try:
import raven
except ImportError:
missing.append('raven 6.0.0+')
if level > 1: if level > 1:
try: try:
from psutil import __version__ as psutilVersion from psutil import __version__ as psutilVersion
+5 -5
View File
@@ -1,11 +1,11 @@
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.31.0 requests>=2.34.2
python-slugify>=1.2.1 python-slugify>=8.0.4
packaging>=23.2 packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.9.0
# Below requirements are compiled in Dockefile # Below requirements are compiled in Dockefile
# numpy==2.3.4 # numpy==2.3.4
# PyMuPDF==1.26.6 # PyMuPDF==1.26.6
+6 -7
View File
@@ -1,12 +1,11 @@
PySide6==6.4.3 PySide6==6.4.3
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.31.0 requests>=2.34.2
python-slugify>=1.2.1 python-slugify>=8.0.4
raven>=6.0.0 packaging>=26.2
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.9.0
numpy<2 numpy<2
PyMuPDF>=1.26.1 PyMuPDF==1.25.5
+6 -7
View File
@@ -1,12 +1,11 @@
PySide6==6.1.3 PySide6==6.1.3
Pillow>=9 Pillow>=9
psutil>=5.9.5 psutil>=7.2.2
requests>=2.31.0 requests>=2.32.4
python-slugify>=1.2.1 python-slugify>=8.0.4
raven>=6.0.0 packaging>=26.2
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.9.0
numpy==1.23.0 numpy==1.23.5
PyMuPDF>=1.16 PyMuPDF>=1.16
+5 -6
View File
@@ -1,12 +1,11 @@
PySide6<6.10 PySide6<6.10
Pillow>=11.3.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=7.2.2
requests>=2.31.0 requests>=2.34.2
python-slugify>=1.2.1,<9.0.0 python-slugify>=8.0.4,<9.0.0
raven>=6.0.0 packaging>=26.2
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0 mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.9.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.18.0 PyMuPDF>=1.18.0
+1 -1
View File
@@ -153,11 +153,11 @@ setuptools.setup(
'psutil>=5.9.5', 'psutil>=5.9.5',
'requests>=2.31.0', 'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'mozjpeg-lossless-optimization>=1.2.0', 'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro>=1.8.0', 'distro>=1.8.0',
'numpy>=1.22.4', 'numpy>=1.22.4',
'packaging>=23.2',
'PyMuPDF>=1.16.1', 'PyMuPDF>=1.16.1',
], ],
classifiers=[], classifiers=[],