1
0
mirror of https://github.com/ciromattia/kcc synced 2026-05-20 14:32:03 +00:00

Compare commits

...

31 Commits

Author SHA1 Message Date
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
22 changed files with 529 additions and 168 deletions

View File

@@ -24,17 +24,17 @@ jobs:
uses: actions/checkout@v6
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Set Release Date
id: release_date
@@ -43,7 +43,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/kcc
# Always creates the "latest" tag
@@ -54,7 +54,7 @@ jobs:
type=raw,value=${{ steps.release_date.outputs.release_date }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .

View File

@@ -59,12 +59,12 @@ jobs:
env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: AppImage
path: './*.AppImage*'
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true

View File

@@ -80,12 +80,12 @@ jobs:
run: |
python setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true

View File

@@ -51,12 +51,12 @@ jobs:
run: |
python3 setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true

View File

@@ -53,12 +53,12 @@ jobs:
python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- 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' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -69,7 +69,7 @@ jobs:
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true

View File

@@ -46,12 +46,12 @@ jobs:
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: windows7-build
path: dist/*.exe
- 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' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -62,7 +62,7 @@ jobs:
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true

1
.gitignore vendored
View File

@@ -2,7 +2,6 @@
Pipfile
Pipfile.lock
setup.bat
kindlecomicconverter/sentry.py
other/windows/kindlegen.exe
dist/
build/

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),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
@@ -245,7 +247,7 @@ MAIN:
PROCESSING:
-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.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
@@ -267,7 +269,7 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
--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
--forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG for black and white images
@@ -295,6 +297,7 @@ OUTPUT SETTINGS:
-b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--onepagelandscape Show a single centered page in landscape
--norotate Do not rotate double page spreads in spread splitter option.
--rotateright Rotate double page spreads in opposite direction.
--rotatefirst Put rotated spread first in spread splitter option.
@@ -440,7 +443,6 @@ Older links (dead):
## PRIVACY
**KCC** is initiating internet connections in two cases:
* During startup - Version check and announcement check.
* When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).

View File

@@ -655,12 +655,12 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
</widget>
</item>
<item row="11" column="1">
<widget class="QCheckBox" name="noSmartCoverCropBox">
<widget class="QCheckBox" name="smartCoverCropBox">
<property name="toolTip">
<string>Disable attempt to crop main cover from wide image.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Attempt to crop main cover from wide image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>No Smart Cover Crop</string>
<string>Smart Cover Crop</string>
</property>
</widget>
</item>
@@ -750,14 +750,12 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="pdfExtractBox">
<widget class="QCheckBox" name="legacyExtractBox">
<property name="toolTip">
<string>Use the PDF image extraction method from KCC 8 and earlier.
Useful for really weird PDFs.</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the PDF/EPUB image extraction method from older KCC versions.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Use if standard extraction fails for whatever reason.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>PDF Legacy Extract</string>
<string>Legacy Extract</string>
</property>
</widget>
</item>
@@ -885,7 +883,7 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
</property>
</widget>
</item>
<item row="12" column="1">
<item row="12" column="2">
<widget class="QCheckBox" name="tempDirBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Main Drive&lt;br/&gt;&lt;/span&gt;Use dedicated temporary directory on main OS drive.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Source File Drive&lt;br/&gt;&lt;/span&gt;Create temporary file directory on source file drive.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -895,6 +893,16 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="onePageLandscapeBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 2 page landscape&lt;br/&gt;&lt;/span&gt;2 viewports for left and right pages&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 1 page landscape&lt;br/&gt;&lt;/span&gt;A single centered viewport for 1 page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1 Page Landscape</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -62,6 +62,19 @@
<item row="1" column="1">
<widget class="QLineEdit" name="volumeLine"/>
</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">
<widget class="QLabel" name="label_3">
<property name="text">
@@ -195,6 +208,7 @@
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>bulkVolumeCheck</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>

View File

@@ -22,7 +22,7 @@ import itertools
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -38,7 +38,6 @@ from xml.sax.saxutils import escape
from psutil import Process
from copy import copy
from packaging.version import Version
from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
@@ -327,12 +326,12 @@ class WorkerThread(QThread):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.pdfExtractBox.isChecked():
options.pdfextract = True
if GUI.legacyExtractBox.isChecked():
options.legacyextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.noSmartCoverCropBox.isChecked():
options.nosmartcovercrop = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = True
if GUI.coverFillBox.isChecked():
options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -345,6 +344,8 @@ class WorkerThread(QThread):
options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
@@ -445,8 +446,6 @@ class WorkerThread(QThread):
_, _, traceback = sys.exc_info()
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
if ' is corrupted.' not in str(err):
GUI.sentry.captureException()
MW.addMessage.emit('Error during conversion! Please consult '
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
'for more details.', 'error', False)
@@ -564,6 +563,7 @@ class WorkerThread(QThread):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
comic2ebook.checkPre('LLL-')
GUI.progress.content = ''
GUI.progress.stop()
MW.hideProgressBar.emit()
@@ -627,7 +627,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.clear()
if self.tar or self.sevenzip:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
else:
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)')
@@ -658,30 +658,47 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self, sname):
files = []
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml')
self.lastPath = os.path.dirname(sname)
# Multi-directory selection for bulk editing ComicInfo.xml
dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
dialog.setFileMode(QFileDialog.FileMode.Directory)
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:
if self.sevenzip:
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
files = fnames[0]
if files:
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
else:
fname = ['']
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>'
' to enable metadata editing.', 'warning')
if fname[0] != '':
sname = fname[0]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname:
else:
files = [sname]
if files:
try:
self.editor.loadData(sname)
self.editor.loadData(files)
except Exception as err:
_, _, traceback = sys.exc_info()
GUI.sentry.captureException()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
else:
@@ -948,6 +965,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.chunkSizeCheckBox.setChecked(False)
elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.upscaleBox.setChecked(True)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
@@ -1082,9 +1100,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'legacyExtractBox': GUI.legacyExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(),
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(),
@@ -1099,6 +1117,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'deleteBox': GUI.deleteBox.checkState(),
'tempDirBox': GUI.tempDirBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(),
@@ -1122,7 +1141,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.clear()
formats = ['.pdf']
if self.tar or self.sevenzip:
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
if os.path.isdir(message):
GUI.jobList.addItem(message)
GUI.jobList.scrollToBottom()
@@ -1209,7 +1228,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.croppingPowerValue = 1.0
self.currentMode = 1
self.targetDirectory = ''
self.sentry = Client(release=__version__)
if sys.platform.startswith('win'):
# noinspection PyUnresolvedReferences
from psutil import BELOW_NORMAL_PRIORITY_CLASS
@@ -1261,6 +1279,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 1240x1860": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
},
"Kindle 1324x1986": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1324',
},
"Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
@@ -1368,6 +1389,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Other",
"Separator",
"Kindle 1324x1986",
"Kindle 1920x1920",
"Kindle 1860x1920",
"Kindle 1240x1860",
@@ -1532,62 +1554,300 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
def loadData(self, file):
self.parser = metadata.MetadataParser(file)
if self.parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False)
self.okButton.setEnabled(False)
self.statusLabel.setText('CBR metadata are read-only.')
def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
if len(valuesByFile) <= 20:
rows = ''.join(
'<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:
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.okButton.setEnabled(True)
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.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
for field in (self.seriesLine, self.titleLine):
if field.text() == '':
path = Path(file)
if file.endswith('.xml'):
field.setText(path.parent.name)
else:
field.setText(path.stem)
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
for field in (self.seriesLine, self.titleLine):
if field.text() == '':
path = Path(file)
if file.endswith('.xml'):
field.setText(path.parent.name)
else:
field.setText(path.stem)
def saveData(self):
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())
if self.bulkMode:
bulkData = {}
if self.cleanData(self.seriesLine.text()):
bulkData['Series'] = self.cleanData(self.seriesLine.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
fieldName = field.objectName().capitalize()[:-4] + 's'
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.sentry.captureException()
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close()
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
if tmpData:
bulkData[fieldName] = tmpData
# Handle bulk volume editing
volumes = None
if self.bulkVolumeCheck.isChecked():
volumeText = self.volumeLine.text()
volumes, error = self.parseVolumeInput(volumeText, len(self.files))
if error:
self.statusLabel.setText(error)
return
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.sentry.captureException()
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):
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):
self.ui = QDialog()
self.parser = None
self.files = []
self.bulkMode = False
self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume)
self.okButton.clicked.connect(self.saveData)
self.cancelButton.clicked.connect(self.ui.close)
if sys.platform.startswith('linux'):

View File

@@ -344,10 +344,10 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
self.noSmartCoverCropBox = QCheckBox(self.optionWidget)
self.noSmartCoverCropBox.setObjectName(u"noSmartCoverCropBox")
self.smartCoverCropBox = QCheckBox(self.optionWidget)
self.smartCoverCropBox.setObjectName(u"smartCoverCropBox")
self.gridLayout_2.addWidget(self.noSmartCoverCropBox, 11, 1, 1, 1)
self.gridLayout_2.addWidget(self.smartCoverCropBox, 11, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
@@ -389,10 +389,10 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.pdfExtractBox = QCheckBox(self.optionWidget)
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
self.legacyExtractBox = QCheckBox(self.optionWidget)
self.legacyExtractBox.setObjectName(u"legacyExtractBox")
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
self.gridLayout_2.addWidget(self.legacyExtractBox, 9, 0, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
@@ -453,7 +453,12 @@ class Ui_mainWindow(object):
self.tempDirBox = QCheckBox(self.optionWidget)
self.tempDirBox.setObjectName(u"tempDirBox")
self.gridLayout_2.addWidget(self.tempDirBox, 12, 1, 1, 1)
self.gridLayout_2.addWidget(self.tempDirBox, 12, 2, 1, 1)
self.onePageLandscapeBox = QCheckBox(self.optionWidget)
self.onePageLandscapeBox.setObjectName(u"onePageLandscapeBox")
self.gridLayout_2.addWidget(self.onePageLandscapeBox, 12, 1, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -753,9 +758,9 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip)
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
#if QT_CONFIG(tooltip)
self.noSmartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable attempt to crop main cover from wide image.", None))
self.smartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.noSmartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"No Smart Cover Crop", None))
self.smartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"Smart Cover Crop", None))
#if QT_CONFIG(tooltip)
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
@@ -785,11 +790,9 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.pdfExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF image extraction method from KCC 8 and earlier.\n"
"\n"
"Useful for really weird PDFs.", None))
self.legacyExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use the PDF/EPUB image extraction method from older KCC versions.</p><p><br/></p><p>Use if standard extraction fails for whatever reason.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
self.legacyExtractBox.setText(QCoreApplication.translate("mainWindow", u"Legacy Extract", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
@@ -830,6 +833,10 @@ class Ui_mainWindow(object):
self.tempDirBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
#if QT_CONFIG(tooltip)
self.onePageLandscapeBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 2 page landscape<br/></span>2 viewports for left and right pages</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 1 page landscape<br/></span>A single centered viewport for 1 page</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.onePageLandscapeBox.setText(QCoreApplication.translate("mainWindow", u"1 Page Landscape", None))
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)

View File

@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QSizePolicy,
QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QSizePolicy, QVBoxLayout, QWidget)
from . import KCC_rc
class Ui_editorDialog(object):
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
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.setObjectName(u"label_3")
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
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.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", 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_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))

View File

@@ -1,4 +1,4 @@
__version__ = '10.1.1'
__version__ = '10.1.3'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'

View File

@@ -22,7 +22,9 @@ from collections import Counter
import os
import pathlib
import re
import shutil
import sys
import xml.etree.ElementTree as ET
from argparse import ArgumentParser
from time import perf_counter, strftime, gmtime
from copy import copy
@@ -30,8 +32,8 @@ from glob import glob, escape
from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile
from zipfile import ZipFile, ZIP_STORED
from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool, cpu_count
from uuid import uuid4
@@ -90,6 +92,7 @@ def main(argv=None):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
checkPre('LLL-')
return 0
@@ -144,7 +147,7 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
f.write('<div style="display:none;">.</div>\n')
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
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")
if options.iskindle and options.panelview:
if options.autoscale:
@@ -466,6 +469,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
pageside = "right"
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("</spine>\n</package>\n")
@@ -892,9 +897,12 @@ def getWorkFolder(afile, workdir=None):
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else:
fullPath = workdir
check_path = gettempdir()
if options.tempdir:
check_path = os.path.dirname(afile)
else:
check_path = gettempdir()
if os.path.isdir(afile):
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
@@ -913,7 +921,7 @@ def getWorkFolder(afile, workdir=None):
os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
if options.pdfextract:
if options.legacyextract:
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
njpg = pdf.extract()
if njpg == 0:
@@ -952,11 +960,68 @@ def getWorkFolder(afile, workdir=None):
for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath)
os.rmdir(os.path.join(fullPath, tdir[0]))
return workdir
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
if options.legacyextract:
return workdir
if afile.lower().endswith('.epub'):
container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
opf_path = os.path.join(path, opf_path)
opf = ET.parse(opf_path)
spine = []
for spine_item in opf.findall(r'.//{*}itemref'):
spine.append(spine_item.attrib.get('idref'))
manifest_dict = {}
for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
ordered_image_paths = []
for i, spine_item in enumerate(spine):
if spine_item not in manifest_dict:
continue
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
largest_size = 0
img_path = None
for img in imgs:
for key in img.attrib:
if 'src' in key or 'href' in key:
temp_img_path = img.attrib[key]
if temp_img_path.startswith('..'):
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
else:
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
try:
temp_size = os.path.getsize(temp_img_path)
if temp_size > largest_size:
largest_size = temp_size
img_path = temp_img_path
except OSError:
pass
# TODO empty image
if img_path:
ordered_image_paths.append(img_path)
# fallback if naive spine extraction fails
if not ordered_image_paths:
return workdir
if options.tempdir:
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
else:
workdir2 = mkdtemp('', 'KCC-')
for i, img_path in enumerate(ordered_image_paths):
_, ext = os.path.splitext(img_path)
fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
os.makedirs(fullpath2, exist_ok=True)
shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
rmtree(workdir, True)
return workdir2
return workdir
finally:
pass
else:
raise UserWarning("Failed to open source file/directory.")
@@ -1393,6 +1458,8 @@ def makeParser():
"2: Consider every subdirectory as separate volume [Default=0]")
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
help="Show a single centered page in landscape")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
@@ -1402,12 +1469,12 @@ def makeParser():
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
processing_options.add_argument("--legacyextract", action="store_true", dest="legacyextract", default=False,
help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
help="Render vector PDFs to device width instead of height.")
processing_options.add_argument("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False,
help="Disable attempt to crop main cover from wide image")
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
help="Attempt to crop main cover from wide image")
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
@@ -1609,33 +1676,30 @@ def checkTools(source):
sys.exit(1)
def checkPre(source):
def checkPre(source='KCC-'):
# Make sure that all temporary files are gone
for root, dirs, _ in walkLevel(gettempdir(), 0):
for tempdir in dirs:
if tempdir.startswith('KCC-'):
if tempdir.startswith(source):
rmtree(os.path.join(root, tempdir), True)
# Make sure that target directory is writable
if os.path.isdir(source):
src = os.path.abspath(os.path.join(source, '..'))
else:
src = os.path.dirname(source)
try:
with TemporaryFile(prefix='KCC-', dir=src):
pass
except Exception:
raise UserWarning("Target directory is not writable.")
def makeFusion(sources: List[str]):
if len(sources) < 2:
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter()
first_path = Path(sources[0])
if first_path.is_file():
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
if options.tempdir:
fusion_parent = first_path.parent
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")
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
@@ -1644,7 +1708,6 @@ def makeFusion(sources: List[str]):
for index, source in enumerate(sources, start=1):
print(f"Processing {source}...")
checkPre(source)
print("Checking images...")
source_path = Path(source)
# Add the fusion_0001_ prefix to maintain user-specified order if needed
@@ -1676,7 +1739,9 @@ def makeBook(source, qtgui=None, job_progress=''):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
checkPre(source)
checkPre()
if not options.filefusion:
checkPre('LLL-')
print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source)
print(f"{job_progress}Checking images...")
@@ -1719,7 +1784,7 @@ def makeBook(source, qtgui=None, job_progress=''):
most_common_res, most_common_count = counter.most_common(1)[0]
options.kfx_resolution = most_common_res
if most_common_count / counter.total() > .6:
if most_common_count / sum(counter.values()) > .6:
pass
#elif max(aspect_ratios) - min(aspect_ratios) < .2:
else:

View File

@@ -106,6 +106,7 @@ class ProfileData:
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
@@ -209,7 +210,8 @@ class ComicPageParser:
# elif wide enough to split
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
# if (split) or (split and rotate)
if self.opt.splitter != 1 and width / height < 1.75:
BISECT_THRESHOLD = 1.8
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
if width > height:
leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height)
@@ -226,7 +228,7 @@ class ComicPageParser:
self.payload.append(['S2', self.source, pagetwo, self.fill])
# 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
if not self.opt.norotate:
if not self.opt.rotateright:
@@ -598,7 +600,7 @@ class Cover:
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor:
self.image = self.image.convert('L')
if not self.options.nosmartcovercrop:
if self.options.smartcovercrop:
self.crop_main_cover()
size = list(self.options.profileData[1])

View File

@@ -122,10 +122,6 @@ def dependencyCheck(level):
missing.append('PySide 6.0.0')
except ImportError:
missing.append('PySide 6.0.0+')
try:
import raven
except ImportError:
missing.append('raven 6.0.0+')
if level > 1:
try:
from psutil import __version__ as psutilVersion

View File

@@ -1,8 +1,8 @@
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
packaging>=23.2
requests>=2.34.2
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0

View File

@@ -1,10 +1,9 @@
PySide6==6.4.3
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
requests>=2.34.2
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0

View File

@@ -1,10 +1,9 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
requests>=2.32.4
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0

View File

@@ -1,10 +1,9 @@
PySide6<6.10
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1,<9.0.0
raven>=6.0.0
packaging>=23.2
requests>=2.34.2
python-slugify>=8.0.4,<9.0.0
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0

View File

@@ -153,11 +153,11 @@ setuptools.setup(
'psutil>=5.9.5',
'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0',
'distro>=1.8.0',
'numpy>=1.22.4',
'packaging>=23.2',
'PyMuPDF>=1.16.1',
],
classifiers=[],