mirror of
https://github.com/ciromattia/kcc
synced 2026-06-22 14:30:58 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10037a3c9d |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -21,20 +21,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
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@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- 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@v6
|
uses: docker/metadata-action@v5
|
||||||
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@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: AppImage
|
name: AppImage
|
||||||
path: './*.AppImage*'
|
path: './*.AppImage*'
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
uses: actions/upload-artifact@v6
|
||||||
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@v3
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -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@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
uses: actions/upload-artifact@v6
|
||||||
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@v3
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
command: build_c2p
|
command: build_c2p
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
uses: actions/upload-artifact@v6
|
||||||
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.2
|
uses: signpath/github-action-submit-signing-request@v2.0
|
||||||
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@v3
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
WINDOWS_7: 1
|
WINDOWS_7: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
uses: actions/upload-artifact@v6
|
||||||
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.2
|
uses: signpath/github-action-submit-signing-request@v2.0
|
||||||
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@v3
|
uses: softprops/action-gh-release@v2
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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/
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
like Kindle, Kobo, ReMarkable, and more.
|
like Kindle, Kobo, ReMarkable, and more.
|
||||||
Pages display in fullscreen without margins,
|
Pages display in fullscreen without margins,
|
||||||
with proper fixed layout support.
|
with proper fixed layout support.
|
||||||
Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
|
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
|
||||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||||
KCC runs on Windows, macOS, and Linux.
|
KCC runs on Windows, macOS, and Linux.
|
||||||
|
|
||||||
@@ -115,14 +115,15 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
|||||||
## FAQ
|
## FAQ
|
||||||
- Should I use Calibre?
|
- Should I use Calibre?
|
||||||
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
|
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
|
||||||
Additionally, it will break page numbers.
|
|
||||||
Viewing KCC output in Calibre will also not work properly.
|
Viewing KCC output in Calibre will also not work properly.
|
||||||
Direct USB dropping is reccomended.
|
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
|
||||||
|
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
|
||||||
|
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
|
||||||
- Blank pages?
|
- Blank pages?
|
||||||
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. You can try PDF output.
|
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
|
||||||
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
||||||
- What output format should I use?
|
- What output format should I use?
|
||||||
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
|
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
|
||||||
- All options have additional information in tooltips if you hover over the option.
|
- All options have additional information in tooltips if you hover over the option.
|
||||||
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
||||||
- Kindle panel view not working?
|
- Kindle panel view not working?
|
||||||
@@ -136,6 +137,9 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
|||||||
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
||||||
- How to make AZW3 instead of MOBI?
|
- How to make AZW3 instead of MOBI?
|
||||||
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
|
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
|
||||||
|
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
|
||||||
|
- Image too dark?
|
||||||
|
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
|
||||||
- Huge margins / slow page turns?
|
- Huge margins / slow page turns?
|
||||||
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
||||||
|
|
||||||
@@ -194,13 +198,10 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
|||||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
|
||||||
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
|
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
|
||||||
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
|
||||||
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
|
'KCS': ("Kindle Colorsoft", (1264, 1680), 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),
|
||||||
@@ -247,7 +248,7 @@ MAIN:
|
|||||||
|
|
||||||
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
|
||||||
--legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
|
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
|
||||||
--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
|
||||||
@@ -269,7 +270,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
|
||||||
--smartcovercrop Attempt to crop main cover from wide image
|
--nosmartcovercrop Disable 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
|
||||||
@@ -297,7 +298,6 @@ OUTPUT SETTINGS:
|
|||||||
-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.
|
||||||
@@ -365,7 +365,6 @@ 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
|
||||||
```
|
```
|
||||||
@@ -391,7 +390,6 @@ 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
|
||||||
```
|
```
|
||||||
@@ -445,6 +443,7 @@ 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).
|
||||||
|
|||||||
+10
-18
@@ -655,12 +655,12 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="11" column="1">
|
<item row="11" column="1">
|
||||||
<widget class="QCheckBox" name="smartCoverCropBox">
|
<widget class="QCheckBox" name="noSmartCoverCropBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html></string>
|
<string>Disable attempt to crop main cover from wide image.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Smart Cover Crop</string>
|
<string>No Smart Cover Crop</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -750,12 +750,14 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0">
|
<item row="9" column="0">
|
||||||
<widget class="QCheckBox" name="legacyExtractBox">
|
<widget class="QCheckBox" name="pdfExtractBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><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></string>
|
<string>Use the PDF image extraction method from KCC 8 and earlier.
|
||||||
|
|
||||||
|
Useful for really weird PDFs.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Legacy Extract</string>
|
<string>PDF Legacy Extract</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -864,7 +866,7 @@ Useful if you plan to crop a little off the top and bottom to fill screen.</stri
|
|||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html></string>
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Custom Autocontrast</string>
|
<string>Autocontrast</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="tristate">
|
<property name="tristate">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@@ -883,7 +885,7 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="2">
|
<item row="12" column="1">
|
||||||
<widget class="QCheckBox" name="tempDirBox">
|
<widget class="QCheckBox" name="tempDirBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><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></string>
|
<string><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></string>
|
||||||
@@ -893,16 +895,6 @@ Ignored for Kindle EPUB/MOBI and all PDF.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="1">
|
|
||||||
<widget class="QCheckBox" name="onePageLandscapeBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>1 Page Landscape</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -62,19 +62,6 @@
|
|||||||
<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><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> → sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> → range from 1 to 10<br><code>1, 3, 5</code> → specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i></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">
|
||||||
@@ -208,7 +195,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
+77
-342
@@ -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, QAbstractItemView, QListView, QTreeView)
|
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
|
||||||
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -38,6 +38,7 @@ 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
|
||||||
@@ -326,12 +327,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.legacyExtractBox.isChecked():
|
if GUI.pdfExtractBox.isChecked():
|
||||||
options.legacyextract = True
|
options.pdfextract = True
|
||||||
if GUI.pdfWidthBox.isChecked():
|
if GUI.pdfWidthBox.isChecked():
|
||||||
options.pdfwidth = True
|
options.pdfwidth = True
|
||||||
if GUI.smartCoverCropBox.isChecked():
|
if GUI.noSmartCoverCropBox.isChecked():
|
||||||
options.smartcovercrop = True
|
options.nosmartcovercrop = 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:
|
||||||
@@ -344,8 +345,6 @@ 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:
|
||||||
@@ -393,10 +392,7 @@ 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)
|
||||||
@@ -449,6 +445,8 @@ 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)
|
||||||
@@ -560,6 +558,12 @@ 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()
|
||||||
@@ -623,7 +627,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 *.epub *.pdf);;All (*.*)')
|
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.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 (*.*)')
|
||||||
@@ -654,47 +658,30 @@ 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:
|
||||||
# Multi-directory selection for bulk editing ComicInfo.xml
|
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
||||||
dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
|
if dname != '':
|
||||||
dialog.setFileMode(QFileDialog.FileMode.Directory)
|
sname = os.path.join(dname, 'ComicInfo.xml')
|
||||||
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
|
self.lastPath = os.path.dirname(sname)
|
||||||
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:
|
||||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
|
fname = QFileDialog.getOpenFileName(MW, 'Select file', 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')
|
||||||
else:
|
if fname[0] != '':
|
||||||
files = [sname]
|
sname = fname[0]
|
||||||
|
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
|
||||||
if files:
|
if sname:
|
||||||
try:
|
try:
|
||||||
self.editor.loadData(files)
|
self.editor.loadData(sname)
|
||||||
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:
|
||||||
@@ -808,8 +795,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.rotateBox.setChecked(False)
|
GUI.rotateBox.setChecked(False)
|
||||||
GUI.borderBox.setEnabled(False)
|
GUI.borderBox.setEnabled(False)
|
||||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||||
# GUI.upscaleBox.setEnabled(False)
|
GUI.upscaleBox.setEnabled(False)
|
||||||
# GUI.upscaleBox.setChecked(False)
|
GUI.upscaleBox.setChecked(False)
|
||||||
GUI.croppingBox.setEnabled(False)
|
GUI.croppingBox.setEnabled(False)
|
||||||
GUI.croppingBox.setChecked(False)
|
GUI.croppingBox.setChecked(False)
|
||||||
GUI.interPanelCropBox.setEnabled(False)
|
GUI.interPanelCropBox.setEnabled(False)
|
||||||
@@ -826,7 +813,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.rotateBox.setEnabled(True)
|
GUI.rotateBox.setEnabled(True)
|
||||||
GUI.borderBox.setEnabled(True)
|
GUI.borderBox.setEnabled(True)
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
if not profile['Label'].startswith('KS') or True:
|
if not profile['Label'].startswith('KS'):
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
GUI.croppingBox.setEnabled(True)
|
GUI.croppingBox.setEnabled(True)
|
||||||
GUI.interPanelCropBox.setEnabled(True)
|
GUI.interPanelCropBox.setEnabled(True)
|
||||||
@@ -921,10 +908,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
if not GUI.webtoonBox.isChecked():
|
if not GUI.webtoonBox.isChecked():
|
||||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||||
if profile['Label'].startswith('KS') and False:
|
if profile['Label'].startswith('KS'):
|
||||||
GUI.upscaleBox.setDisabled(True)
|
GUI.upscaleBox.setDisabled(True)
|
||||||
else:
|
else:
|
||||||
if not GUI.webtoonBox.isChecked() or True:
|
if not GUI.webtoonBox.isChecked():
|
||||||
GUI.upscaleBox.setEnabled(True)
|
GUI.upscaleBox.setEnabled(True)
|
||||||
if profile['Label'] == 'KCS':
|
if profile['Label'] == 'KCS':
|
||||||
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
|
||||||
@@ -959,16 +946,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
|
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
|
||||||
GUI.chunkSizeCheckBox.setEnabled(False)
|
GUI.chunkSizeCheckBox.setEnabled(False)
|
||||||
GUI.chunkSizeCheckBox.setChecked(False)
|
GUI.chunkSizeCheckBox.setChecked(False)
|
||||||
elif GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'KFX':
|
|
||||||
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
|
||||||
GUI.upscaleBox.setChecked(True)
|
|
||||||
elif not GUI.webtoonBox.isChecked():
|
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():
|
||||||
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
|
||||||
else:
|
|
||||||
GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
|
|
||||||
|
|
||||||
def stripTags(self, html):
|
def stripTags(self, html):
|
||||||
s = HTMLStripper()
|
s = HTMLStripper()
|
||||||
@@ -1096,9 +1077,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(),
|
||||||
'legacyExtractBox': GUI.legacyExtractBox.checkState(),
|
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||||
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||||
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.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(),
|
||||||
@@ -1113,7 +1094,6 @@ 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(),
|
||||||
@@ -1137,7 +1117,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', '.epub'])
|
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
|
||||||
if os.path.isdir(message):
|
if os.path.isdir(message):
|
||||||
GUI.jobList.addItem(message)
|
GUI.jobList.addItem(message)
|
||||||
GUI.jobList.scrollToBottom()
|
GUI.jobList.scrollToBottom()
|
||||||
@@ -1189,11 +1169,6 @@ 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
|
||||||
@@ -1229,6 +1204,7 @@ 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
|
||||||
@@ -1255,7 +1231,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
||||||
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
||||||
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
||||||
"KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
|
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
||||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||||
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
|
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
|
||||||
@@ -1280,9 +1256,6 @@ 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',
|
||||||
},
|
},
|
||||||
@@ -1390,7 +1363,6 @@ 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",
|
||||||
@@ -1555,299 +1527,62 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
|
|
||||||
|
|
||||||
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
||||||
def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
|
def loadData(self, file):
|
||||||
note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
|
self.parser = metadata.MetadataParser(file)
|
||||||
|
if self.parser.format in ['RAR', 'RAR5']:
|
||||||
if len(valuesByFile) <= 20:
|
self.editorWidget.setEnabled(False)
|
||||||
rows = ''.join(
|
self.okButton.setEnabled(False)
|
||||||
'<tr>'
|
self.statusLabel.setText('CBR metadata are read-only.')
|
||||||
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):
|
||||||
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
|
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
||||||
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
||||||
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
|
for field in (self.seriesLine, self.titleLine):
|
||||||
for field in (self.seriesLine, self.titleLine):
|
if field.text() == '':
|
||||||
if field.text() == '':
|
path = Path(file)
|
||||||
path = Path(file)
|
if file.endswith('.xml'):
|
||||||
if file.endswith('.xml'):
|
field.setText(path.parent.name)
|
||||||
field.setText(path.parent.name)
|
else:
|
||||||
else:
|
field.setText(path.stem)
|
||||||
field.setText(path.stem)
|
|
||||||
|
|
||||||
def saveData(self):
|
def saveData(self):
|
||||||
if self.bulkMode:
|
for field in (self.volumeLine, self.numberLine):
|
||||||
bulkData = {}
|
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
||||||
if self.cleanData(self.seriesLine.text()):
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.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 = [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:
|
else:
|
||||||
self.statusLabel.setText(f'Successfully updated {total} files.')
|
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
||||||
self.ui.close()
|
break
|
||||||
else:
|
else:
|
||||||
for field in (self.volumeLine, self.numberLine):
|
for field in (self.seriesLine, self.titleLine):
|
||||||
if field.text().isnumeric() or self.cleanData(field.text()) == '':
|
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
||||||
else:
|
values = self.cleanData(field.text()).split(',')
|
||||||
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
|
tmpData = []
|
||||||
break
|
for value in values:
|
||||||
else:
|
if self.cleanData(value) != '':
|
||||||
for field in (self.seriesLine, self.titleLine):
|
tmpData.append(self.cleanData(value))
|
||||||
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
|
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
||||||
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
|
try:
|
||||||
values = self.cleanData(field.text()).split(',')
|
self.parser.saveXML()
|
||||||
tmpData = []
|
except Exception as err:
|
||||||
for value in values:
|
_, _, traceback = sys.exc_info()
|
||||||
if self.cleanData(value) != '':
|
GUI.sentry.captureException()
|
||||||
tmpData.append(self.cleanData(value))
|
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||||
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
|
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||||
try:
|
self.ui.close()
|
||||||
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'):
|
||||||
|
|||||||
@@ -344,10 +344,10 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
|
||||||
|
|
||||||
self.smartCoverCropBox = QCheckBox(self.optionWidget)
|
self.noSmartCoverCropBox = QCheckBox(self.optionWidget)
|
||||||
self.smartCoverCropBox.setObjectName(u"smartCoverCropBox")
|
self.noSmartCoverCropBox.setObjectName(u"noSmartCoverCropBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.smartCoverCropBox, 11, 1, 1, 1)
|
self.gridLayout_2.addWidget(self.noSmartCoverCropBox, 11, 1, 1, 1)
|
||||||
|
|
||||||
self.rotateFirstBox = QCheckBox(self.optionWidget)
|
self.rotateFirstBox = QCheckBox(self.optionWidget)
|
||||||
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
||||||
@@ -389,10 +389,10 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
||||||
|
|
||||||
self.legacyExtractBox = QCheckBox(self.optionWidget)
|
self.pdfExtractBox = QCheckBox(self.optionWidget)
|
||||||
self.legacyExtractBox.setObjectName(u"legacyExtractBox")
|
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.legacyExtractBox, 9, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
|
||||||
|
|
||||||
self.colorBox = QCheckBox(self.optionWidget)
|
self.colorBox = QCheckBox(self.optionWidget)
|
||||||
self.colorBox.setObjectName(u"colorBox")
|
self.colorBox.setObjectName(u"colorBox")
|
||||||
@@ -453,12 +453,7 @@ class Ui_mainWindow(object):
|
|||||||
self.tempDirBox = QCheckBox(self.optionWidget)
|
self.tempDirBox = QCheckBox(self.optionWidget)
|
||||||
self.tempDirBox.setObjectName(u"tempDirBox")
|
self.tempDirBox.setObjectName(u"tempDirBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.tempDirBox, 12, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.tempDirBox, 12, 1, 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)
|
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||||
@@ -758,9 +753,9 @@ class Ui_mainWindow(object):
|
|||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
|
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.smartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Attempt to crop main cover from wide image.</p></body></html>", None))
|
self.noSmartCoverCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"Disable attempt to crop main cover from wide image.", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.smartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"Smart Cover Crop", None))
|
self.noSmartCoverCropBox.setText(QCoreApplication.translate("mainWindow", u"No Smart Cover Crop", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#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))
|
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)
|
#endif // QT_CONFIG(tooltip)
|
||||||
@@ -790,9 +785,11 @@ class Ui_mainWindow(object):
|
|||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
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))
|
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))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.legacyExtractBox.setText(QCoreApplication.translate("mainWindow", u"Legacy Extract", None))
|
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#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))
|
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)
|
#endif // QT_CONFIG(tooltip)
|
||||||
@@ -822,7 +819,7 @@ class Ui_mainWindow(object):
|
|||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>", None))
|
self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Custom Autocontrast", None))
|
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Autocontrast", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.webpBox.setToolTip(QCoreApplication.translate("mainWindow", u"Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.\n"
|
self.webpBox.setToolTip(QCoreApplication.translate("mainWindow", u"Replace JPG with lossy WebP and PNG with lossless WebP. This includes the JPG Quality.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -833,10 +830,6 @@ 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))
|
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)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
|
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)
|
#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))
|
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)
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
|||||||
@@ -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, QCheckBox, QDialog, QGridLayout,
|
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
||||||
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||||
QSizePolicy, QVBoxLayout, QWidget)
|
QVBoxLayout, QWidget)
|
||||||
from . import KCC_rc
|
from . import KCC_rc
|
||||||
|
|
||||||
class Ui_editorDialog(object):
|
class Ui_editorDialog(object):
|
||||||
@@ -57,12 +57,6 @@ 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")
|
||||||
|
|
||||||
@@ -163,8 +157,7 @@ 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.bulkVolumeCheck)
|
QWidget.setTabOrder(self.volumeLine, self.titleLine)
|
||||||
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)
|
||||||
@@ -182,10 +175,6 @@ 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,4 +1,4 @@
|
|||||||
__version__ = '10.2.0'
|
__version__ = '9.7.2'
|
||||||
__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'
|
||||||
|
|||||||
@@ -18,13 +18,10 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
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
|
||||||
@@ -32,8 +29,8 @@ 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
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from tempfile import mkdtemp, gettempdir
|
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||||
from shutil import move, copytree, rmtree, copyfile
|
from shutil import move, copytree, rmtree, copyfile
|
||||||
from multiprocessing import Pool, cpu_count
|
from multiprocessing import Pool, cpu_count
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -46,7 +43,7 @@ from psutil import virtual_memory, disk_usage
|
|||||||
from html import escape as hescape
|
from html import escape as hescape
|
||||||
import pymupdf
|
import pymupdf
|
||||||
|
|
||||||
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean, get_contain_resolution
|
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
|
||||||
from .comicarchive import SEVENZIP, available_archive_tools
|
from .comicarchive import SEVENZIP, available_archive_tools
|
||||||
from . import comic2panel
|
from . import comic2panel
|
||||||
from . import image
|
from . import image
|
||||||
@@ -76,19 +73,22 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -136,14 +136,14 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
|||||||
"content=\"width=" + str(imgsizeframe[0]) + ", height=" + str(imgsizeframe[1]) + "\"/>\n"
|
"content=\"width=" + str(imgsizeframe[0]) + ", height=" + str(imgsizeframe[1]) + "\"/>\n"
|
||||||
"</head>\n",
|
"</head>\n",
|
||||||
"<body style=\"" + additionalStyle + "\">\n",
|
"<body style=\"" + additionalStyle + "\">\n",
|
||||||
"<div style=\"text-align:center;\">\n",
|
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
|
||||||
])
|
])
|
||||||
if options.iskindle:
|
if options.iskindle:
|
||||||
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
||||||
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="top: 1920px" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
f.write(f'<img 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:
|
||||||
@@ -324,19 +324,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
|||||||
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
|
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
|
||||||
if options.iskindle and options.profile != 'Custom':
|
if options.iskindle and options.profile != 'Custom':
|
||||||
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
||||||
])
|
# "<meta name=\"original-resolution\" content=\"",
|
||||||
if not options.kfx_resolution:
|
# str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
|
||||||
f.writelines([
|
|
||||||
"<meta name=\"original-resolution\" content=\"",
|
|
||||||
str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
x, y = options.kfx_resolution
|
|
||||||
f.writelines([
|
|
||||||
"<meta name=\"original-resolution\" content=\"",
|
|
||||||
str(x) + "x" + str(y) + "\"/>\n",
|
|
||||||
])
|
|
||||||
f.writelines([
|
|
||||||
"<meta name=\"book-type\" content=\"comic\"/>\n",
|
"<meta name=\"book-type\" content=\"comic\"/>\n",
|
||||||
"<meta name=\"primary-writing-mode\" content=\"" + writingmode + "\"/>\n",
|
"<meta name=\"primary-writing-mode\" content=\"" + writingmode + "\"/>\n",
|
||||||
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
||||||
@@ -465,8 +454,6 @@ 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")
|
||||||
@@ -886,24 +873,15 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
|||||||
|
|
||||||
def getWorkFolder(afile, workdir=None):
|
def getWorkFolder(afile, workdir=None):
|
||||||
if not workdir:
|
if not workdir:
|
||||||
|
workdir = mkdtemp('', 'KCC-')
|
||||||
if options.tempdir:
|
if options.tempdir:
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||||
else:
|
|
||||||
workdir = mkdtemp('', 'KCC-')
|
|
||||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||||
else:
|
else:
|
||||||
fullPath = workdir
|
fullPath = workdir
|
||||||
|
|
||||||
if options.tempdir:
|
|
||||||
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(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
||||||
raise UserWarning(DISK_WARNING)
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
try:
|
try:
|
||||||
copytree(afile, fullPath)
|
copytree(afile, fullPath)
|
||||||
sanitizePermissions(fullPath)
|
sanitizePermissions(fullPath)
|
||||||
@@ -912,14 +890,14 @@ def getWorkFolder(afile, workdir=None):
|
|||||||
rmtree(workdir, True)
|
rmtree(workdir, True)
|
||||||
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(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
||||||
raise UserWarning(DISK_WARNING)
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
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.legacyextract:
|
if options.pdfextract:
|
||||||
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||||
njpg = pdf.extract()
|
njpg = pdf.extract()
|
||||||
if njpg == 0:
|
if njpg == 0:
|
||||||
@@ -958,69 +936,11 @@ 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:
|
|
||||||
pass
|
except OSError as e:
|
||||||
|
rmtree(workdir, True)
|
||||||
|
raise UserWarning(e)
|
||||||
else:
|
else:
|
||||||
raise UserWarning("Failed to open source file/directory.")
|
raise UserWarning("Failed to open source file/directory.")
|
||||||
|
|
||||||
@@ -1155,6 +1075,11 @@ def getDirectorySize(start_path='.'):
|
|||||||
return total_size
|
return total_size
|
||||||
|
|
||||||
|
|
||||||
|
def getTopMargin(deviceres, size):
|
||||||
|
y = int((deviceres[1] - size[1]) / 2) / deviceres[1] * 100
|
||||||
|
return str(round(y, 1))
|
||||||
|
|
||||||
|
|
||||||
def getPanelViewResolution(imagesize, deviceres):
|
def getPanelViewResolution(imagesize, deviceres):
|
||||||
scale = float(deviceres[0]) / float(imagesize[0])
|
scale = float(deviceres[0]) / float(imagesize[0])
|
||||||
return int(deviceres[0]), int(scale * imagesize[1])
|
return int(deviceres[0]), int(scale * imagesize[1])
|
||||||
@@ -1457,8 +1382,6 @@ 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,
|
||||||
@@ -1468,12 +1391,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("--legacyextract", action="store_true", dest="legacyextract", default=False,
|
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
|
||||||
help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
|
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
|
||||||
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("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
|
processing_options.add_argument("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False,
|
||||||
help="Attempt to crop main cover from wide image")
|
help="Disable 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,
|
||||||
@@ -1548,15 +1471,6 @@ def checkOptions(options):
|
|||||||
options.isKobo = False
|
options.isKobo = False
|
||||||
options.bordersColor = None
|
options.bordersColor = None
|
||||||
options.keep_epub = False
|
options.keep_epub = False
|
||||||
|
|
||||||
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
|
||||||
options.iskindle = True
|
|
||||||
else:
|
|
||||||
options.isKobo = True
|
|
||||||
|
|
||||||
if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
|
|
||||||
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
|
|
||||||
|
|
||||||
if options.format == 'PDF-200MB':
|
if options.format == 'PDF-200MB':
|
||||||
options.targetsize = 195
|
options.targetsize = 195
|
||||||
options.format = 'PDF'
|
options.format = 'PDF'
|
||||||
@@ -1588,7 +1502,10 @@ def checkOptions(options):
|
|||||||
options.format = 'PDF'
|
options.format = 'PDF'
|
||||||
else:
|
else:
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
|
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||||
|
options.iskindle = True
|
||||||
|
else:
|
||||||
|
options.isKobo = True
|
||||||
if options.white_borders:
|
if options.white_borders:
|
||||||
options.bordersColor = 'white'
|
options.bordersColor = 'white'
|
||||||
if options.black_borders:
|
if options.black_borders:
|
||||||
@@ -1619,7 +1536,6 @@ def checkOptions(options):
|
|||||||
options.hq = False
|
options.hq = False
|
||||||
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
||||||
if options.format == 'KFX':
|
if options.format == 'KFX':
|
||||||
options.targetsize = 195
|
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
options.kfx = True
|
options.kfx = True
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
@@ -1641,7 +1557,6 @@ def checkOptions(options):
|
|||||||
options.jpegquality = 90
|
options.jpegquality = 90
|
||||||
else:
|
else:
|
||||||
options.jpegquality = 85
|
options.jpegquality = 85
|
||||||
|
|
||||||
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||||
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
|
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
|
||||||
|
|
||||||
@@ -1673,37 +1588,35 @@ 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='KCC-'):
|
def checkPre(source):
|
||||||
# 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(source):
|
if tempdir.startswith('KCC-'):
|
||||||
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 options.tempdir:
|
|
||||||
fusion_parent = first_path.parent
|
|
||||||
else:
|
|
||||||
# LLL is after KCC
|
|
||||||
checkPre('LLL-')
|
|
||||||
fusion_parent = Path(mkdtemp('', 'LLL-'))
|
|
||||||
|
|
||||||
if first_path.is_file():
|
if first_path.is_file():
|
||||||
fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
|
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
|
||||||
else:
|
else:
|
||||||
fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
|
fusion_path = first_path.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
|
||||||
@@ -1712,6 +1625,7 @@ 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
|
||||||
@@ -1743,9 +1657,7 @@ def makeBook(source, qtgui=None, job_progress=''):
|
|||||||
GUI.progressBarTick.emit('1')
|
GUI.progressBarTick.emit('1')
|
||||||
else:
|
else:
|
||||||
checkTools(source)
|
checkTools(source)
|
||||||
checkPre()
|
checkPre(source)
|
||||||
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...")
|
||||||
@@ -1760,43 +1672,9 @@ def makeBook(source, qtgui=None, job_progress=''):
|
|||||||
if not options.webtoon:
|
if not options.webtoon:
|
||||||
cover = image.Cover(cover_path, options)
|
cover = image.Cover(cover_path, options)
|
||||||
|
|
||||||
x, y = image.ProfileData.Profiles[options.profile][1]
|
|
||||||
if options.webtoon:
|
if options.webtoon:
|
||||||
|
x, y = image.ProfileData.Profiles[options.profile][1]
|
||||||
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
|
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
|
||||||
|
|
||||||
options.kfx_resolution = None
|
|
||||||
if options.kfx:
|
|
||||||
original_resolutions = []
|
|
||||||
normalized_resolutions = []
|
|
||||||
for root, _, files in os.walk(os.path.join(path, "OEBPS", "Images")):
|
|
||||||
for file in files:
|
|
||||||
with Image.open(os.path.join(root, file)) as imagef:
|
|
||||||
original_resolutions.append(imagef.size)
|
|
||||||
size = get_contain_resolution(imagef, (x, y))
|
|
||||||
normalized_resolutions.append(size)
|
|
||||||
|
|
||||||
counter = Counter(normalized_resolutions)
|
|
||||||
|
|
||||||
aspect_ratios = []
|
|
||||||
filtered_resolutions = []
|
|
||||||
for w, h in normalized_resolutions:
|
|
||||||
aspect_ratio = h / w
|
|
||||||
# page-like aspect ratios, could be improved
|
|
||||||
if aspect_ratio > 1.3 and aspect_ratio < 1.7:
|
|
||||||
aspect_ratios.append(aspect_ratio)
|
|
||||||
filtered_resolutions.append((w, h))
|
|
||||||
|
|
||||||
most_common_res, most_common_count = counter.most_common(1)[0]
|
|
||||||
options.kfx_resolution = most_common_res
|
|
||||||
if most_common_count / sum(counter.values()) > .6:
|
|
||||||
pass
|
|
||||||
#elif max(aspect_ratios) - min(aspect_ratios) < .2:
|
|
||||||
else:
|
|
||||||
# get the widest resolution
|
|
||||||
options.kfx_resolution = max(filtered_resolutions)
|
|
||||||
# else:
|
|
||||||
# raise UserWarning('Aspect ratio of pages too different for KFX conversion')
|
|
||||||
|
|
||||||
if options.noprocessing:
|
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")
|
||||||
else:
|
else:
|
||||||
@@ -1837,7 +1715,7 @@ def makeBook(source, qtgui=None, job_progress=''):
|
|||||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
||||||
else:
|
else:
|
||||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||||
if cover and cover.smartcover:
|
if 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(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
|
||||||
elif options.format == 'PDF':
|
elif options.format == 'PDF':
|
||||||
@@ -1845,7 +1723,7 @@ def makeBook(source, qtgui=None, job_progress=''):
|
|||||||
# determine output filename based on source and tome count
|
# determine output filename based on source and tome count
|
||||||
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
|
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
|
||||||
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
|
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
|
||||||
if cover and cover.smartcover:
|
if 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))
|
||||||
# use optimized buildPDF logic with streaming and compression
|
# use optimized buildPDF logic with streaming and compression
|
||||||
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
|
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
|
||||||
@@ -1901,11 +1779,11 @@ 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
|
||||||
if options.filefusion:
|
try:
|
||||||
rmtree(source, True)
|
rmtree(path, True)
|
||||||
checkPre('LLL-')
|
except Exception:
|
||||||
|
pass
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
@@ -1938,11 +1816,8 @@ 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
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
|
|||||||
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
||||||
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
||||||
from .inter_panel_crop_alg import crop_empty_inter_panel
|
from .inter_panel_crop_alg import crop_empty_inter_panel
|
||||||
from .shared import get_contain_resolution
|
|
||||||
|
|
||||||
AUTO_CROP_THRESHOLD = 0.015
|
AUTO_CROP_THRESHOLD = 0.015
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
@@ -106,7 +105,6 @@ 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),
|
||||||
@@ -165,10 +163,7 @@ class ComicPageParser:
|
|||||||
with Image.open(srcImgPath) as im:
|
with Image.open(srcImgPath) as im:
|
||||||
self.image = im.copy()
|
self.image = im.copy()
|
||||||
|
|
||||||
self.page_background_color = self.fillCheck()
|
self.fill = 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
|
||||||
@@ -198,9 +193,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.page_background_color, self.fill])
|
self.payload.append(['N', self.source, new_image, self.fill])
|
||||||
elif self.opt.webtoon:
|
elif self.opt.webtoon:
|
||||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
self.payload.append(['N', self.source, self.image, 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
|
||||||
@@ -209,12 +204,11 @@ 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.page_background_color, self.fill])
|
self.payload.append(['R', self.source, spread, 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)
|
||||||
BISECT_THRESHOLD = 1.8
|
if self.opt.splitter != 1 and width / height < 1.75:
|
||||||
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)
|
||||||
@@ -227,23 +221,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.page_background_color, self.fill])
|
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||||
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
|
self.payload.append(['S2', self.source, pagetwo, 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 >= BISECT_THRESHOLD):
|
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
|
||||||
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.page_background_color, self.fill])
|
self.payload.append(['R', self.source, spread, self.fill])
|
||||||
else:
|
else:
|
||||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
self.payload.append(['N', self.source, self.image, self.fill])
|
||||||
|
|
||||||
def fillCheck(self):
|
def fillCheck(self):
|
||||||
if False:
|
if self.opt.bordersColor:
|
||||||
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')
|
||||||
@@ -282,7 +276,7 @@ class ComicPageParser:
|
|||||||
|
|
||||||
|
|
||||||
class ComicPage:
|
class ComicPage:
|
||||||
def __init__(self, options, mode, path, image, page_background_color, fill):
|
def __init__(self, options, mode, path, image, 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:
|
||||||
@@ -292,7 +286,6 @@ 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])
|
||||||
@@ -524,17 +517,7 @@ class ComicPage:
|
|||||||
ratio_device = float(self.size[1]) / float(self.size[0])
|
ratio_device = float(self.size[1]) / float(self.size[0])
|
||||||
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
||||||
method = self.resize_method()
|
method = self.resize_method()
|
||||||
if self.opt.kfx:
|
if self.opt.stretch:
|
||||||
ratio_kfx = self.opt.kfx_resolution[1] / self.opt.kfx_resolution[0]
|
|
||||||
contain_size = get_contain_resolution(self.image, self.size)
|
|
||||||
if abs(ratio_image - ratio_kfx) < AUTO_CROP_THRESHOLD:
|
|
||||||
if contain_size[0] > self.opt.kfx_resolution[0] or contain_size[1] > self.opt.kfx_resolution[1]:
|
|
||||||
self.image = ImageOps.fit(self.image, self.opt.kfx_resolution, method=method)
|
|
||||||
else:
|
|
||||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
|
||||||
else:
|
|
||||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
|
||||||
elif self.opt.stretch:
|
|
||||||
self.image = self.image.resize(self.size, method)
|
self.image = self.image.resize(self.size, method)
|
||||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||||
pass
|
pass
|
||||||
@@ -543,7 +526,7 @@ class ComicPage:
|
|||||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||||
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||||
elif (self.opt.format in ('CBZ', 'PDF')) and not self.opt.white_borders:
|
elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
|
||||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||||
else:
|
else:
|
||||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||||
@@ -566,7 +549,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.page_background_color)
|
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
@@ -576,7 +559,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.page_background_color)
|
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
@@ -586,7 +569,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.page_background_color)
|
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||||
|
|
||||||
class Cover:
|
class Cover:
|
||||||
def __init__(self, source, opt):
|
def __init__(self, source, opt):
|
||||||
@@ -604,7 +587,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 self.options.smartcovercrop:
|
if not self.options.nosmartcovercrop:
|
||||||
self.crop_main_cover()
|
self.crop_main_cover()
|
||||||
|
|
||||||
size = list(self.options.profileData[1])
|
size = list(self.options.profileData[1])
|
||||||
|
|||||||
@@ -61,23 +61,6 @@ def getImageFileName(imgfile):
|
|||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
return [name, ext]
|
return [name, ext]
|
||||||
|
|
||||||
def get_contain_resolution(image, size):
|
|
||||||
'''same code as Pillow ImageOps.contain()'''
|
|
||||||
im_ratio = image.width / image.height
|
|
||||||
dest_ratio = size[0] / size[1]
|
|
||||||
|
|
||||||
if im_ratio != dest_ratio:
|
|
||||||
if im_ratio > dest_ratio:
|
|
||||||
new_height = round(image.height / image.width * size[0])
|
|
||||||
if new_height != size[1]:
|
|
||||||
size = (size[0], new_height)
|
|
||||||
else:
|
|
||||||
new_width = round(image.width / image.height * size[1])
|
|
||||||
if new_width != size[0]:
|
|
||||||
size = (new_width, size[1])
|
|
||||||
|
|
||||||
return size
|
|
||||||
|
|
||||||
|
|
||||||
def walkSort(dirnames, filenames):
|
def walkSort(dirnames, filenames):
|
||||||
convert = lambda text: int(text) if text.isdigit() else text
|
convert = lambda text: int(text) if text.isdigit() else text
|
||||||
@@ -122,6 +105,10 @@ 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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.34.2
|
requests>=2.31.0
|
||||||
python-slugify>=8.0.4
|
python-slugify>=1.2.1
|
||||||
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.8.0
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
PySide6==6.4.3
|
PySide6==6.4.3
|
||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.34.2
|
requests>=2.31.0
|
||||||
python-slugify>=8.0.4
|
python-slugify>=1.2.1
|
||||||
packaging>=26.2
|
raven>=6.0.0
|
||||||
|
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.8.0
|
||||||
numpy<2
|
numpy<2
|
||||||
PyMuPDF==1.25.5
|
PyMuPDF>=1.26.1
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
PySide6==6.1.3
|
PySide6==6.1.3
|
||||||
Pillow>=9
|
Pillow>=9
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.32.4
|
requests>=2.31.0
|
||||||
python-slugify>=8.0.4
|
python-slugify>=1.2.1
|
||||||
packaging>=26.2
|
raven>=6.0.0
|
||||||
|
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.8.0
|
||||||
numpy==1.23.5
|
numpy==1.23.0
|
||||||
PyMuPDF>=1.16
|
PyMuPDF>=1.16
|
||||||
|
|||||||
+4
-3
@@ -1,9 +1,10 @@
|
|||||||
PySide6<6.10
|
PySide6<6.10
|
||||||
Pillow>=11.3.0
|
Pillow>=11.3.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.34.2
|
requests>=2.31.0
|
||||||
python-slugify>=8.0.4,<9.0.0
|
python-slugify>=1.2.1,<9.0.0
|
||||||
packaging>=26.2
|
raven>=6.0.0
|
||||||
|
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.8.0
|
||||||
|
|||||||
@@ -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=[],
|
||||||
|
|||||||
Reference in New Issue
Block a user