1
0
mirror of https://github.com/ciromattia/kcc synced 2026-06-22 14:30:58 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Xu 10037a3c9d remove original res 2026-04-20 12:20:22 -07:00
23 changed files with 246 additions and 703 deletions
+1 -1
View File
@@ -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
+6 -6
View File
@@ -21,20 +21,20 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@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: .
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@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
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
env: env:
MACOSX_DEPLOYMENT_TARGET: '14.0' MACOSX_DEPLOYMENT_TARGET: '14.0'
steps: steps:
- uses: actions/checkout@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
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9 PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14' MACOSX_DEPLOYMENT_TARGET: '10.14'
steps: steps:
- uses: actions/checkout@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
+4 -4
View File
@@ -35,7 +35,7 @@ jobs:
command: build_c2p command: build_c2p
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@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
+4 -4
View File
@@ -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
+1
View File
@@ -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/
+14 -15
View File
@@ -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
View File
@@ -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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Attempt to crop main cover from wide image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use the PDF/EPUB image extraction method from older KCC versions.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Use if standard extraction fails for whatever reason.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - BW only&lt;br/&gt;&lt;/span&gt;Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Disabled&lt;br/&gt;&lt;/span&gt;Disable autocontrast&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - BW and Color&lt;br/&gt;&lt;/span&gt;BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - BW only&lt;br/&gt;&lt;/span&gt;Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Disabled&lt;br/&gt;&lt;/span&gt;Disable autocontrast&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - BW and Color&lt;br/&gt;&lt;/span&gt;BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Main Drive&lt;br/&gt;&lt;/span&gt;Use dedicated temporary directory on main OS drive.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Source File Drive&lt;br/&gt;&lt;/span&gt;Create temporary file directory on source file drive.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Main Drive&lt;br/&gt;&lt;/span&gt;Use dedicated temporary directory on main OS drive.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Source File Drive&lt;br/&gt;&lt;/span&gt;Create temporary file directory on source file drive.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@@ -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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 2 page landscape&lt;br/&gt;&lt;/span&gt;2 viewports for left and right pages&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 1 page landscape&lt;br/&gt;&lt;/span&gt;A single centered viewport for 1 page&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1 Page Landscape</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
-14
View File
@@ -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>&lt;b&gt;Bulk Volume Editing&lt;/b&gt;&lt;br&gt;Check this box to assign volume numbers to multiple files.&lt;br&gt;&lt;br&gt;&lt;b&gt;Input formats:&lt;/b&gt;&lt;br&gt;&lt;code&gt;5&lt;/code&gt; → sequence starting from 5 (5, 6, 7...)&lt;br&gt;&lt;code&gt;1-10&lt;/code&gt; → range from 1 to 10&lt;br&gt;&lt;code&gt;1, 3, 5&lt;/code&gt; → specific values&lt;br&gt;&lt;br&gt;&lt;i&gt;Note: Files are sorted alphabetically before assignment.&lt;/i&gt;</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
@@ -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
View File
@@ -22,7 +22,7 @@ import itertools
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, 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'):
+14 -21
View File
@@ -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)
+4 -15
View File
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, 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 -1
View File
@@ -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'
+61 -186
View File
@@ -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
+18 -35
View File
@@ -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])
+4 -17
View File
@@ -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
+3 -3
View File
@@ -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
+5 -4
View File
@@ -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
+5 -4
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -153,11 +153,11 @@ setuptools.setup(
'psutil>=5.9.5', 'psutil>=5.9.5',
'requests>=2.31.0', 'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'mozjpeg-lossless-optimization>=1.2.0', 'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro>=1.8.0', 'distro>=1.8.0',
'numpy>=1.22.4', 'numpy>=1.22.4',
'packaging>=23.2',
'PyMuPDF>=1.16.1', 'PyMuPDF>=1.16.1',
], ],
classifiers=[], classifiers=[],