mirror of
https://github.com/ciromattia/kcc
synced 2026-04-22 17:08:57 +00:00
Compare commits
1 Commits
v9.1.0
...
clone-fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9ee43278 |
15
.github/FUNDING.yml
vendored
15
.github/FUNDING.yml
vendored
@@ -1,15 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: # Replace with a single Open Collective username
|
|
||||||
ko_fi: eink_dude
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
|
||||||
liberapay: # Replace with a single Liberapay username
|
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
|
||||||
polar: # Replace with a single Polar username
|
|
||||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
|
||||||
thanks_dev: # Replace with a single thanks.dev username
|
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
4
.github/workflows/package-linux.yml
vendored
4
.github/workflows/package-linux.yml
vendored
@@ -25,9 +25,9 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
|
|||||||
6
.github/workflows/package-macos.yml
vendored
6
.github/workflows/package-macos.yml
vendored
@@ -28,9 +28,9 @@ jobs:
|
|||||||
os: [ macos-13, macos-14 ]
|
os: [ macos-13, macos-14 ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
# apply provisioning profile
|
# apply provisioning profile
|
||||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- run: npm install -g appdmg
|
- run: npm install -g appdmg
|
||||||
|
|||||||
66
.github/workflows/package-osx-legacy.yml
vendored
66
.github/workflows/package-osx-legacy.yml
vendored
@@ -1,66 +0,0 @@
|
|||||||
name: build KCC for osx legacy
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
# Don't trigger if it's just a documentation update
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- '**.MD'
|
|
||||||
- '**.yml'
|
|
||||||
- '**.sh'
|
|
||||||
- 'docs/**'
|
|
||||||
- 'Dockerfile'
|
|
||||||
- 'LICENSE'
|
|
||||||
- '.gitattributes'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.dockerignore'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ macos-13 ]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
env:
|
|
||||||
# We need the official Python, because the GA ones only support newer macOS versions
|
|
||||||
# The deployment target is picked up by the Python build tools automatically
|
|
||||||
PYTHON_VERSION: 3.11.9
|
|
||||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Get Python
|
|
||||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
|
||||||
- name: Install Python
|
|
||||||
run: |
|
|
||||||
sudo installer -pkg python.pkg -target /
|
|
||||||
- name: Install Python dependencies
|
|
||||||
run: |
|
|
||||||
python3 --version
|
|
||||||
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
|
|
||||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
|
||||||
./gen_ui_files.sh
|
|
||||||
- uses: actions/setup-node@v5
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
- run: npm install -g appdmg
|
|
||||||
- name: build binary
|
|
||||||
run: |
|
|
||||||
python3 setup.py build_binary
|
|
||||||
- name: upload build
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: osx-build-${{ runner.arch }}
|
|
||||||
path: dist/*.dmg
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
LICENSE.txt
|
|
||||||
dist/*.dmg
|
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
capital: KCC_c2p
|
capital: KCC_c2p
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Package Application
|
- name: Package Application
|
||||||
uses: JackMcKew/pyinstaller-action-windows@main
|
uses: JackMcKew/pyinstaller-action-windows@main
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
path: dist/windows/*.exe
|
path: dist/windows/*.exe
|
||||||
|
|
||||||
- id: optional_step_id
|
- id: optional_step_id
|
||||||
uses: signpath/github-action-submit-signing-request@v1.3
|
uses: signpath/github-action-submit-signing-request@v1.2
|
||||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||||
with:
|
with:
|
||||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
|
|||||||
6
.github/workflows/package-windows.yml
vendored
6
.github/workflows/package-windows.yml
vendored
@@ -25,9 +25,9 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
cache: 'pip'
|
cache: 'pip'
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
name: windows-build
|
name: windows-build
|
||||||
path: dist/*.exe
|
path: dist/*.exe
|
||||||
- id: optional_step_id
|
- id: optional_step_id
|
||||||
uses: signpath/github-action-submit-signing-request@v1.3
|
uses: signpath/github-action-submit-signing-request@v1.2
|
||||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||||
with:
|
with:
|
||||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
|
|||||||
61
.github/workflows/package-windows7.yml
vendored
61
.github/workflows/package-windows7.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
|
||||||
|
|
||||||
name: build KCC for windows 7
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
|
|
||||||
# Don't trigger if it's just a documentation update
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
- '**.MD'
|
|
||||||
- '**.yml'
|
|
||||||
- '**.sh'
|
|
||||||
- 'docs/**'
|
|
||||||
- 'Dockerfile'
|
|
||||||
- 'LICENSE'
|
|
||||||
- '.gitattributes'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.dockerignore'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-2022
|
|
||||||
env:
|
|
||||||
WINDOWS_7: 1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: 3.8
|
|
||||||
cache: 'pip'
|
|
||||||
- name: Install dependencies
|
|
||||||
env:
|
|
||||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
|
||||||
pip install -r requirements-win7.txt
|
|
||||||
pip install certifi pyinstaller --no-binary pyinstaller
|
|
||||||
.\gen_ui_files.bat
|
|
||||||
- name: build binary
|
|
||||||
run: |
|
|
||||||
python setup.py build_binary
|
|
||||||
- name: upload-unsigned-artifact
|
|
||||||
id: upload-unsigned-artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: windows-build
|
|
||||||
path: dist/*.exe
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
LICENSE.txt
|
|
||||||
dist/*.exe
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,8 +8,6 @@ dist/
|
|||||||
build/
|
build/
|
||||||
KindleComicConverter*.egg-info/
|
KindleComicConverter*.egg-info/
|
||||||
.idea/
|
.idea/
|
||||||
win7
|
|
||||||
osx10.11
|
|
||||||
/venv/
|
/venv/
|
||||||
/kindlegen*
|
/kindlegen*
|
||||||
/kcc.bat
|
/kcc.bat
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ RUN set -x && \
|
|||||||
# Install required python modules
|
# Install required python modules
|
||||||
python -m pip install --upgrade pip && \
|
python -m pip install --upgrade pip && \
|
||||||
python -m venv /opt/venv && \
|
python -m venv /opt/venv && \
|
||||||
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy pymupdf
|
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy
|
||||||
|
|
||||||
|
|
||||||
######################################################################################
|
######################################################################################
|
||||||
|
|||||||
103
README.md
103
README.md
@@ -2,50 +2,22 @@
|
|||||||
|
|
||||||
# KCC
|
# KCC
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/ciromattia/kcc/releases)
|
[](https://github.com/ciromattia/kcc/releases)
|
||||||
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
||||||
[](https://github.com/ciromattia/kcc/releases)
|
|
||||||
|
|
||||||
|
|
||||||
**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
|
|
||||||
like Kindle, Kobo, ReMarkable, and more.
|
|
||||||
Pages display in fullscreen without margins,
|
|
||||||
with proper fixed layout support.
|
|
||||||
Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs.
|
|
||||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
|
||||||
|
|
||||||
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
|
|
||||||
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
|
||||||
for optimal compatibility with your device's native PDF reader.
|
|
||||||
|
|
||||||
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
|
|
||||||
which can be directly converted by KCC.
|
|
||||||
|
|
||||||
|
**Kindle Comic Converter** optimizes comics and manga for eink readers like Kindle, Kobo, ReMarkable, and more.
|
||||||
|
Pages display in fullscreen without margins, with proper fixed layout support.
|
||||||
Its main feature is various optional image processing steps to look good on eink screens,
|
Its main feature is various optional image processing steps to look good on eink screens,
|
||||||
which have different requirements than normal LCD screens.
|
which have different requirements than normal LCD screens.
|
||||||
Combining that with downscaling to your specific device's screen resolution
|
It also does filesize optimization by downscaling to your specific device's screen resolution,
|
||||||
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
|
which can improve performance on underpowered ereaders.
|
||||||
This can also improve battery life, page turn speed, and general performance
|
Supported input formats include folders and archives of JPG/PNG files and more.
|
||||||
on underpowered ereaders with small storage capacities.
|
Supported output formats include virtual panel view MOBI/AZW3, EPUB, KEPUB, and CBZ.
|
||||||
|
|
||||||
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
|
|
||||||
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
|
|
||||||
2) unneccessary margins at the bottom of the screen
|
|
||||||
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
|
|
||||||
4) incorrect page turn direction for manga that's read right to left
|
|
||||||
5) unaligned two page spreads in landscape, where pages are shifted over by 1
|
|
||||||
|
|
||||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Simply drag and drop your files/folders into the KCC window,
|
|
||||||
adjust your settings (hover over each option to see details in a tooltip),
|
|
||||||
and hit convert to create ereader optimized files.
|
|
||||||
You can change the default output directory by holding `Shift` while clicking the convert button.
|
|
||||||
Then just drag and drop the generated output files onto your device's documents folder via USB.
|
|
||||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
|
|
||||||
|
|
||||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
|
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||||
|
|
||||||
### A word of warning
|
### A word of warning
|
||||||
@@ -72,13 +44,6 @@ If you find **KCC** valuable you can consider donating to the authors:
|
|||||||
|
|
||||||
[](https://ko-fi.com/Q5Q41BW8HS)
|
[](https://ko-fi.com/Q5Q41BW8HS)
|
||||||
|
|
||||||
## Commissions
|
|
||||||
|
|
||||||
This section is subject to change:
|
|
||||||
|
|
||||||
Email (for commisions and inquiries): `kindle.comic.converter` gmail
|
|
||||||
|
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
||||||
@@ -98,33 +63,20 @@ The `c2e` and `c2p` versions are command line tools for power users.
|
|||||||
|
|
||||||
On Windows 11, you may need to run in compatibility mode for an older Windows version.
|
On Windows 11, you may need to run in compatibility mode for an older Windows version.
|
||||||
|
|
||||||
On Mac, right click open to get past the security warning. macOS 12 Monterey or later is required, you can use https://dortania.github.io/OpenCore-Legacy-Patcher/ to get a newer macOS on unsupported hardware.
|
On Mac, right click open to get past the security warning.
|
||||||
|
|
||||||
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
- Should I use Calibre?
|
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
|
||||||
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output in Calibre will break the formatting.
|
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
|
||||||
Viewing KCC output in Calibre will also not work properly.
|
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
|
||||||
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
|
- Image too dark?
|
||||||
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
|
- 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
|
||||||
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
|
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
|
||||||
- What output format should I use?
|
|
||||||
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo.
|
|
||||||
- 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
|
|
||||||
- Right to left mode not working?
|
|
||||||
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
|
|
||||||
- Colors inverted?
|
|
||||||
- Disable Kindle dark mode
|
|
||||||
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
|
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
|
||||||
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
|
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
|
||||||
(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?
|
|
||||||
- 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.
|
||||||
|
|
||||||
@@ -176,12 +128,10 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
|||||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
||||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
||||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
||||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
'K578': ("Kindle", (600, 800), Palette16, 1.8),
|
||||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
|
||||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
||||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
||||||
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
|
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
|
||||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
||||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
||||||
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
||||||
@@ -233,7 +183,6 @@ PROCESSING:
|
|||||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||||
-g GAMMA, --gamma GAMMA
|
-g GAMMA, --gamma GAMMA
|
||||||
Apply gamma correction to linearize the image [Default=Auto]
|
Apply gamma correction to linearize the image [Default=Auto]
|
||||||
--autolevel Set most common dark pixel value to be black point for leveling.
|
|
||||||
-c CROPPING, --cropping CROPPING
|
-c CROPPING, --cropping CROPPING
|
||||||
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
|
||||||
--cp CROPPINGP, --croppingpower CROPPINGP
|
--cp CROPPINGP, --croppingpower CROPPINGP
|
||||||
@@ -256,18 +205,16 @@ OUTPUT SETTINGS:
|
|||||||
Output generated file to specified directory or file
|
Output generated file to specified directory or file
|
||||||
-t TITLE, --title TITLE
|
-t TITLE, --title TITLE
|
||||||
Comic title [Default=filename or directory name]
|
Comic title [Default=filename or directory name]
|
||||||
--metadatatitle Write title from ComicInfo.xml or other embedded metadata
|
|
||||||
-a AUTHOR, --author AUTHOR
|
-a AUTHOR, --author AUTHOR
|
||||||
Author name [Default=KCC]
|
Author name [Default=KCC]
|
||||||
-f FORMAT, --format FORMAT
|
-f FORMAT, --format FORMAT
|
||||||
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
|
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto]
|
||||||
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
||||||
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
-b BATCHSPLIT, --batchsplit BATCHSPLIT
|
||||||
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||||
--norotate Do not rotate double page spreads in spread splitter option.
|
--norotate Do not rotate double page spreads in spread splitter option.
|
||||||
--rotatefirst Put rotated spread first in spread splitter option.
|
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
|
||||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
|
||||||
|
|
||||||
CUSTOM PROFILE:
|
CUSTOM PROFILE:
|
||||||
--customwidth CUSTOMWIDTH
|
--customwidth CUSTOMWIDTH
|
||||||
@@ -314,15 +261,10 @@ Then use the `gen_ui_files` scripts to autogenerate the python UI.
|
|||||||
|
|
||||||
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
||||||
|
|
||||||
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
|
|
||||||
|
|
||||||
Do not use `git merge` to merge master from upstream,
|
Do not use `git merge` to merge master from upstream,
|
||||||
use the "Sync fork" button on your fork on GitHub in your branch
|
use the "Sync fork" button on your fork on GitHub in your branch
|
||||||
to avoid weird looking merges in pull requests.
|
to avoid weird looking merges in pull requests.
|
||||||
|
|
||||||
When making changes, be aware of how your change might affect file splitting/chunking
|
|
||||||
or chapter alignment.
|
|
||||||
|
|
||||||
### Windows install from source
|
### Windows install from source
|
||||||
|
|
||||||
One time setup and running for the first time:
|
One time setup and running for the first time:
|
||||||
@@ -348,8 +290,6 @@ python setup.py build_binary
|
|||||||
|
|
||||||
### macOS install from source
|
### macOS install from source
|
||||||
|
|
||||||
If the system installed Python gives you issues, please install the latest Python from either brew or the official website.
|
|
||||||
|
|
||||||
One time setup and running for the first time:
|
One time setup and running for the first time:
|
||||||
```
|
```
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
@@ -406,7 +346,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.
|
||||||
* When error occurs - Automatic reporting on Windows and macOS.
|
* When error occurs - Automatic reporting on Windows and macOS.
|
||||||
|
|
||||||
## KNOWN ISSUES
|
## KNOWN ISSUES
|
||||||
@@ -415,6 +355,3 @@ Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
|||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
|
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
|
||||||
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
|
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
|
||||||
|
|
||||||
## Verification
|
|
||||||
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ channels:
|
|||||||
- defaults
|
- defaults
|
||||||
dependencies:
|
dependencies:
|
||||||
- python=3.11
|
- python=3.11
|
||||||
- Pillow>=11.3.0
|
- Pillow>=5.2.0
|
||||||
- psutil>=5.9.5
|
- psutil>=5.9.5
|
||||||
- python-slugify>=1.2.1
|
- python-slugify>=1.2.1
|
||||||
- raven>=6.0.0
|
- raven>=6.0.0
|
||||||
|
|||||||
@@ -28,9 +28,4 @@
|
|||||||
<file>../icons/document_new.png</file>
|
<file>../icons/document_new.png</file>
|
||||||
<file>../icons/folder_new.png</file>
|
<file>../icons/folder_new.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="Brand">
|
|
||||||
<file>../icons/kofi_symbol.png</file>
|
|
||||||
<file>../icons/Humble_H-Red.png</file>
|
|
||||||
<file>../icons/Bindle_Red.png</file>
|
|
||||||
</qresource>
|
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
479
gui/KCC.ui
479
gui/KCC.ui
@@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>566</width>
|
<width>519</width>
|
||||||
<height>573</height>
|
<height>572</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@@ -24,12 +24,6 @@
|
|||||||
</property>
|
</property>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="2" column="0" colspan="2">
|
||||||
<widget class="QListWidget" name="jobList">
|
<widget class="QListWidget" name="jobList">
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>150</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="styleSheet">
|
<property name="styleSheet">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
</property>
|
</property>
|
||||||
@@ -79,29 +73,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="kofiButton">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>30</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Support me on Ko-fi</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="KCC.qrc">
|
|
||||||
<normaloff>:/Brand/icons/kofi_symbol.png</normaloff>:/Other/icons/kofi_symbol.png</iconset>
|
|
||||||
</property>
|
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>19</width>
|
|
||||||
<height>16</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="wikiButton">
|
<widget class="QPushButton" name="wikiButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
@@ -143,62 +114,27 @@
|
|||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="1" column="3">
|
<item row="0" column="0">
|
||||||
<widget class="QPushButton" name="convertButton">
|
<widget class="QPushButton" name="directoryButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
<height>30</height>
|
<height>30</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="font">
|
|
||||||
<font>
|
|
||||||
<bold>true</bold>
|
|
||||||
</font>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=" font-weight:600;">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Convert</string>
|
<string>Add image folder</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="KCC.qrc">
|
<iconset resource="KCC.qrc">
|
||||||
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
|
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="3">
|
<item row="0" column="3">
|
||||||
<widget class="QPushButton" name="clearButton">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>30</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Clear list</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="KCC.qrc">
|
|
||||||
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QComboBox" name="deviceBox">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>28</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Target device.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QPushButton" name="fileButton">
|
<widget class="QPushButton" name="fileButton">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
@@ -218,46 +154,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="5">
|
<item row="1" column="0">
|
||||||
<widget class="QPushButton" name="defaultOutputFolderButton">
|
<widget class="QComboBox" name="deviceBox">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
<width>0</width>
|
<width>0</width>
|
||||||
<height>30</height>
|
<height>28</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Use this to select the default output directory.</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Target device.</p></body></html></string>
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="KCC.qrc">
|
|
||||||
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="4">
|
<item row="1" column="3">
|
||||||
<widget class="QCheckBox" name="defaultOutputFolderBox">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Output Folder</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="4" colspan="2">
|
|
||||||
<widget class="QComboBox" name="formatBox">
|
<widget class="QComboBox" name="formatBox">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
<size>
|
<size>
|
||||||
@@ -270,14 +180,55 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QPushButton" name="convertButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Convert</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="KCC.qrc">
|
||||||
|
<normaloff>:/Other/icons/convert.png</normaloff>:/Other/icons/convert.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QPushButton" name="clearButton">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Clear list</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="KCC.qrc">
|
||||||
|
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
<zorder>directoryButton</zorder>
|
||||||
<zorder>clearButton</zorder>
|
<zorder>clearButton</zorder>
|
||||||
|
<zorder>fileButton</zorder>
|
||||||
<zorder>deviceBox</zorder>
|
<zorder>deviceBox</zorder>
|
||||||
<zorder>convertButton</zorder>
|
<zorder>convertButton</zorder>
|
||||||
<zorder>formatBox</zorder>
|
<zorder>formatBox</zorder>
|
||||||
<zorder>defaultOutputFolderButton</zorder>
|
|
||||||
<zorder>fileButton</zorder>
|
|
||||||
<zorder>defaultOutputFolderBox</zorder>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" colspan="2">
|
<item row="1" column="0" colspan="2">
|
||||||
@@ -341,7 +292,7 @@
|
|||||||
<string><html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>2400</number>
|
<number>2160</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -459,13 +410,16 @@
|
|||||||
<property name="bottomMargin">
|
<property name="bottomMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<item row="2" column="2">
|
<item row="4" column="2">
|
||||||
<widget class="QCheckBox" name="gammaBox">
|
<widget class="QCheckBox" name="croppingBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html></string>
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Margins<br/></span>Margins</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Custom gamma</string>
|
<string>Cropping mode</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -475,14 +429,37 @@
|
|||||||
<string><html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Right-to-left mode</string>
|
<string>Manga mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QCheckBox" name="webtoonBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Webtoon mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QCheckBox" name="rotateBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Spread splitter</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="borderBox">
|
<widget class="QCheckBox" name="borderBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html></string>
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>W/B margins</string>
|
<string>W/B margins</string>
|
||||||
@@ -492,6 +469,16 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QCheckBox" name="gammaBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Custom gamma</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="6" column="2">
|
||||||
<widget class="QCheckBox" name="interPanelCropBox">
|
<widget class="QCheckBox" name="interPanelCropBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -505,13 +492,46 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="3" column="2">
|
||||||
<widget class="QCheckBox" name="fileFusionBox">
|
<widget class="QCheckBox" name="colorBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html></string>
|
<string><html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html></string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>File Fusion</string>
|
<string>Color mode</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QCheckBox" name="qualityBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Panel View 4/2/HQ</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="2">
|
||||||
|
<widget class="QCheckBox" name="disableProcessingBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Disable processing</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QCheckBox" name="maximizeStrips">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>1x4 to 2x2 strips</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -537,155 +557,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
|
||||||
<widget class="QCheckBox" name="rotateFirstBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><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></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Rotate First</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="2">
|
|
||||||
<widget class="QCheckBox" name="eraseRainbowBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Erase rainbow effect on color eink screen by attenuating interfering frequencies</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Rainbow eraser</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="1">
|
|
||||||
<widget class="QCheckBox" name="chunkSizeCheckBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:700; text-decoration: underline;">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=" font-weight:700; text-decoration: underline;">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Chunk size</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QCheckBox" name="rotateBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Spread splitter</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
|
||||||
<widget class="QCheckBox" name="outputSplit">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Output split</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="0">
|
|
||||||
<widget class="QCheckBox" name="metadataTitleBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Metadata Title</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QCheckBox" name="qualityBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Panel View 4/2/HQ</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QCheckBox" name="spreadShiftBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Spread shift</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="2">
|
|
||||||
<widget class="QCheckBox" name="disableProcessingBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Disable processing</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QCheckBox" name="webtoonBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Webtoon mode</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="2">
|
|
||||||
<widget class="QCheckBox" name="colorBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Color mode</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="2">
|
|
||||||
<widget class="QCheckBox" name="croppingBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Margins<br/></span>Margins</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Cropping mode</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QCheckBox" name="maximizeStrips">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>1x4 to 2x2 strips</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="6" column="1">
|
|
||||||
<widget class="QCheckBox" name="noRotateBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Do not rotate double page spreads in spread splitter option.</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>No rotate</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QCheckBox" name="deleteBox">
|
<widget class="QCheckBox" name="deleteBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -696,19 +567,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QCheckBox" name="upscaleBox">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html></string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Stretch/Upscale</string>
|
|
||||||
</property>
|
|
||||||
<property name="tristate">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QCheckBox" name="mozJpegBox">
|
<widget class="QCheckBox" name="mozJpegBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
@@ -722,13 +580,66 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="2">
|
<item row="5" column="0">
|
||||||
<widget class="QCheckBox" name="autoLevelBox">
|
<widget class="QCheckBox" name="spreadShiftBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>Set the most common dark pixel value to be the black point for leveling on a page by page basis.</p><p>Skipped for any images that were originally color.</p><p>Use only if default autocontrast still results in very gray faded blacks. </p><p>Reccomended to use with Custom Gamma = 1.0 (Disabled).</p></body></html></string>
|
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Aggressive Black Point</string>
|
<string>Spread shift</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QCheckBox" name="upscaleBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p><span style=" font-weight:600; text-decoration: underline;">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=" font-weight:600; text-decoration: underline;">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Stretch/Upscale</string>
|
||||||
|
</property>
|
||||||
|
<property name="tristate">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QCheckBox" name="outputSplit">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Output split</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="1">
|
||||||
|
<widget class="QCheckBox" name="noRotateBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Do not rotate double page spreads in spread splitter option.</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>No rotate</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="2">
|
||||||
|
<widget class="QCheckBox" name="reduceRainbowBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Rainbow blur</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="1">
|
||||||
|
<widget class="QCheckBox" name="chunkSizeCheckBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p><span style=" font-weight:700; text-decoration: underline;">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=" font-weight:700; text-decoration: underline;">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Chunk size</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -859,6 +770,8 @@
|
|||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>convertButton</tabstop>
|
<tabstop>convertButton</tabstop>
|
||||||
<tabstop>clearButton</tabstop>
|
<tabstop>clearButton</tabstop>
|
||||||
|
<tabstop>directoryButton</tabstop>
|
||||||
|
<tabstop>fileButton</tabstop>
|
||||||
<tabstop>deviceBox</tabstop>
|
<tabstop>deviceBox</tabstop>
|
||||||
<tabstop>formatBox</tabstop>
|
<tabstop>formatBox</tabstop>
|
||||||
<tabstop>mangaBox</tabstop>
|
<tabstop>mangaBox</tabstop>
|
||||||
@@ -879,7 +792,7 @@
|
|||||||
<tabstop>chunkSizeBox</tabstop>
|
<tabstop>chunkSizeBox</tabstop>
|
||||||
<tabstop>noRotateBox</tabstop>
|
<tabstop>noRotateBox</tabstop>
|
||||||
<tabstop>interPanelCropBox</tabstop>
|
<tabstop>interPanelCropBox</tabstop>
|
||||||
<tabstop>eraseRainbowBox</tabstop>
|
<tabstop>reduceRainbowBox</tabstop>
|
||||||
<tabstop>heightBox</tabstop>
|
<tabstop>heightBox</tabstop>
|
||||||
<tabstop>croppingPowerSlider</tabstop>
|
<tabstop>croppingPowerSlider</tabstop>
|
||||||
<tabstop>editorButton</tabstop>
|
<tabstop>editorButton</tabstop>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.1 KiB |
@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2e.py'],
|
|||||||
hiddenimports=['_cffi_backend'],
|
hiddenimports=['_cffi_backend'],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=['pkg_resources'],
|
excludes=[],
|
||||||
win_no_prefer_redirects=False,
|
win_no_prefer_redirects=False,
|
||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2p.py'],
|
|||||||
hiddenimports=['_cffi_backend'],
|
hiddenimports=['_cffi_backend'],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=['pkg_resources'],
|
excludes=[],
|
||||||
win_no_prefer_redirects=False,
|
win_no_prefer_redirects=False,
|
||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
|
|||||||
2
kcc.spec
2
kcc.spec
@@ -11,7 +11,7 @@ a = Analysis(['kcc.py'],
|
|||||||
hiddenimports=['_cffi_backend'],
|
hiddenimports=['_cffi_backend'],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=['pkg_resources'],
|
excludes=[],
|
||||||
win_no_prefer_redirects=False,
|
win_no_prefer_redirects=False,
|
||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher,
|
cipher=block_cipher,
|
||||||
|
|||||||
@@ -16,10 +16,6 @@
|
|||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import itertools
|
|
||||||
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, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
|
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
|
||||||
@@ -31,7 +27,7 @@ import sys
|
|||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from shutil import move, rmtree
|
from shutil import move, rmtree
|
||||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
from subprocess import STDOUT, PIPE
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from xml.sax.saxutils import escape
|
from xml.sax.saxutils import escape
|
||||||
@@ -41,10 +37,10 @@ from packaging.version import Version
|
|||||||
from raven import Client
|
from raven import Client
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
|
||||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
|
||||||
from .comicarchive import SEVENZIP, available_archive_tools
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import comic2ebook
|
from . import comic2ebook
|
||||||
|
from . import image
|
||||||
from . import metadata
|
from . import metadata
|
||||||
from . import kindle
|
from . import kindle
|
||||||
from . import KCC_ui
|
from . import KCC_ui
|
||||||
@@ -125,7 +121,7 @@ class Icons:
|
|||||||
self.EPUBFormat = QIcon()
|
self.EPUBFormat = QIcon()
|
||||||
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.KFXFormat = QIcon()
|
self.KFXFormat = QIcon()
|
||||||
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off)
|
||||||
|
|
||||||
self.info = QIcon()
|
self.info = QIcon()
|
||||||
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
@@ -137,15 +133,6 @@ class Icons:
|
|||||||
self.programIcon = QIcon()
|
self.programIcon = QIcon()
|
||||||
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
|
||||||
self.kofi = QIcon()
|
|
||||||
self.kofi.addPixmap(QPixmap(":/Brand/icons/kofi_symbol.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
|
|
||||||
self.humble = QIcon()
|
|
||||||
self.humble.addPixmap(QPixmap(":/Brand/icons/Humble_H-Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
|
|
||||||
self.bindle = QIcon()
|
|
||||||
self.bindle.addPixmap(QPixmap(":/Brand/icons/Bindle_Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
|
|
||||||
|
|
||||||
class VersionThread(QThread):
|
class VersionThread(QThread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -160,50 +147,19 @@ class VersionThread(QThread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
# unauthenticated API requests limit is 60 req/hour
|
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
|
|
||||||
|
|
||||||
html_url = json_parser["html_url"]
|
html_url = json_parser["html_url"]
|
||||||
latest_version = json_parser["tag_name"]
|
latest_version = json_parser["tag_name"]
|
||||||
latest_version = re.sub(r'^v', "", latest_version)
|
latest_version = re.sub(r'^v', "", latest_version)
|
||||||
|
|
||||||
if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \
|
if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \
|
||||||
or ("b" in __version__
|
or ("b" in __version__
|
||||||
and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))):
|
and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))):
|
||||||
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
|
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
|
||||||
False)
|
False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
announcements = requests.get('https://api.github.com/repos/axu2/kcc-messages/contents/links.json',
|
|
||||||
headers={
|
|
||||||
'Accept': 'application/vnd.github.raw+json',
|
|
||||||
'X-GitHub-Api-Version': '2022-11-28'}).json()
|
|
||||||
for category, payloads in announcements.items():
|
|
||||||
for payload in payloads:
|
|
||||||
expiration = datetime.fromisoformat(payload['expiration'])
|
|
||||||
if expiration < datetime.now(timezone.utc):
|
|
||||||
continue
|
|
||||||
delta = expiration - datetime.now(timezone.utc)
|
|
||||||
time_left = f"{delta.days} day(s) left"
|
|
||||||
icon = 'info'
|
|
||||||
if category == 'humbleBundles':
|
|
||||||
icon = 'bindle'
|
|
||||||
if category == 'kofi':
|
|
||||||
icon = 'kofi'
|
|
||||||
message = f"<b>{payload.get('name')}</b>"
|
|
||||||
if payload.get('link'):
|
|
||||||
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
|
|
||||||
if payload.get('showDeadline'):
|
|
||||||
message += f': {time_left}'
|
|
||||||
if category == 'humbleBundles':
|
|
||||||
message += ' [referral]'
|
|
||||||
MW.addMessage.emit(message, icon , False)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
def setAnswer(self, dialoganswer):
|
def setAnswer(self, dialoganswer):
|
||||||
self.answer = dialoganswer
|
self.answer = dialoganswer
|
||||||
@@ -288,25 +244,11 @@ class WorkerThread(QThread):
|
|||||||
options.upscale = True
|
options.upscale = True
|
||||||
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
|
||||||
options.gamma = float(GUI.gammaValue)
|
options.gamma = float(GUI.gammaValue)
|
||||||
if GUI.autoLevelBox.isChecked():
|
options.cropping = GUI.croppingBox.checkState().value
|
||||||
options.autolevel = True
|
|
||||||
if GUI.croppingBox.isChecked():
|
|
||||||
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
|
|
||||||
options.cropping = 1
|
|
||||||
else:
|
|
||||||
options.cropping = 2
|
|
||||||
else:
|
|
||||||
options.cropping = 0
|
|
||||||
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
|
||||||
options.croppingp = float(GUI.croppingPowerValue)
|
options.croppingp = float(GUI.croppingPowerValue)
|
||||||
options.preservemargin = GUI.preserveMarginBox.value()
|
options.preservemargin = GUI.preserveMarginBox.value()
|
||||||
if GUI.interPanelCropBox.isChecked():
|
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
|
||||||
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
|
|
||||||
options.interpanelcrop = 1
|
|
||||||
else:
|
|
||||||
options.interpanelcrop = 2
|
|
||||||
else:
|
|
||||||
options.interpanelcrop = 0
|
|
||||||
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.white_borders = True
|
options.white_borders = True
|
||||||
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
|
||||||
@@ -315,26 +257,18 @@ class WorkerThread(QThread):
|
|||||||
options.batchsplit = 2
|
options.batchsplit = 2
|
||||||
if GUI.colorBox.isChecked():
|
if GUI.colorBox.isChecked():
|
||||||
options.forcecolor = True
|
options.forcecolor = True
|
||||||
if GUI.eraseRainbowBox.isChecked():
|
if GUI.reduceRainbowBox.isChecked():
|
||||||
options.eraserainbow = True
|
options.reducerainbow = True
|
||||||
if GUI.maximizeStrips.isChecked():
|
if GUI.maximizeStrips.isChecked():
|
||||||
options.maximizestrips = True
|
options.maximizestrips = True
|
||||||
if GUI.disableProcessingBox.isChecked():
|
if GUI.disableProcessingBox.isChecked():
|
||||||
options.noprocessing = True
|
options.noprocessing = True
|
||||||
if GUI.metadataTitleBox.isChecked():
|
|
||||||
options.metadatatitle = True
|
|
||||||
if GUI.deleteBox.isChecked():
|
if GUI.deleteBox.isChecked():
|
||||||
options.delete = True
|
options.delete = True
|
||||||
if GUI.spreadShiftBox.isChecked():
|
if GUI.spreadShiftBox.isChecked():
|
||||||
options.spreadshift = True
|
options.spreadshift = True
|
||||||
if GUI.fileFusionBox.isChecked():
|
|
||||||
options.filefusion = True
|
|
||||||
else:
|
|
||||||
options.filefusion = False
|
|
||||||
if GUI.noRotateBox.isChecked():
|
if GUI.noRotateBox.isChecked():
|
||||||
options.norotate = True
|
options.norotate = True
|
||||||
if GUI.rotateFirstBox.isChecked():
|
|
||||||
options.rotatefirst = True
|
|
||||||
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||||
options.forcepng = True
|
options.forcepng = True
|
||||||
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
||||||
@@ -354,19 +288,6 @@ class WorkerThread(QThread):
|
|||||||
if GUI.jobList.item(i).icon().isNull():
|
if GUI.jobList.item(i).icon().isNull():
|
||||||
currentJobs.append(str(GUI.jobList.item(i).text()))
|
currentJobs.append(str(GUI.jobList.item(i).text()))
|
||||||
GUI.jobList.clear()
|
GUI.jobList.clear()
|
||||||
if options.filefusion:
|
|
||||||
bookDir = []
|
|
||||||
MW.addMessage.emit('Attempting file fusion', 'info', False)
|
|
||||||
for job in currentJobs:
|
|
||||||
bookDir.append(job)
|
|
||||||
try:
|
|
||||||
comic2ebook.options = comic2ebook.checkOptions(copy(options))
|
|
||||||
currentJobs.clear()
|
|
||||||
currentJobs.append(comic2ebook.makeFusion(bookDir))
|
|
||||||
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
|
|
||||||
except Exception as e:
|
|
||||||
print('Fusion Failed. ' + str(e))
|
|
||||||
MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True)
|
|
||||||
for job in currentJobs:
|
for job in currentJobs:
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
if not self.conversionAlive:
|
if not self.conversionAlive:
|
||||||
@@ -377,9 +298,6 @@ class WorkerThread(QThread):
|
|||||||
if gui_current_format == 'CBZ':
|
if gui_current_format == 'CBZ':
|
||||||
MW.addMessage.emit('Creating CBZ files', 'info', False)
|
MW.addMessage.emit('Creating CBZ files', 'info', False)
|
||||||
GUI.progress.content = 'Creating CBZ files'
|
GUI.progress.content = 'Creating CBZ files'
|
||||||
elif gui_current_format == 'PDF':
|
|
||||||
MW.addMessage.emit('Creating PDF files', 'info', False)
|
|
||||||
GUI.progress.content = 'Creating PDF files'
|
|
||||||
else:
|
else:
|
||||||
MW.addMessage.emit('Creating EPUB files', 'info', False)
|
MW.addMessage.emit('Creating EPUB files', 'info', False)
|
||||||
GUI.progress.content = 'Creating EPUB files'
|
GUI.progress.content = 'Creating EPUB files'
|
||||||
@@ -424,8 +342,6 @@ class WorkerThread(QThread):
|
|||||||
GUI.progress.content = ''
|
GUI.progress.content = ''
|
||||||
if gui_current_format == 'CBZ':
|
if gui_current_format == 'CBZ':
|
||||||
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
|
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
|
||||||
elif gui_current_format == 'PDF':
|
|
||||||
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
|
|
||||||
else:
|
else:
|
||||||
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
|
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
|
||||||
if 'MOBI' in gui_current_format:
|
if 'MOBI' in gui_current_format:
|
||||||
@@ -517,12 +433,6 @@ class WorkerThread(QThread):
|
|||||||
move(item, GUI.targetDirectory)
|
move(item, GUI.targetDirectory)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if options.filefusion:
|
|
||||||
for path in currentJobs:
|
|
||||||
if os.path.isfile(path):
|
|
||||||
os.remove(path)
|
|
||||||
elif os.path.isdir(path):
|
|
||||||
rmtree(path, True)
|
|
||||||
GUI.progress.content = ''
|
GUI.progress.content = ''
|
||||||
GUI.progress.stop()
|
GUI.progress.stop()
|
||||||
MW.hideProgressBar.emit()
|
MW.hideProgressBar.emit()
|
||||||
@@ -552,33 +462,17 @@ class SystemTrayIcon(QSystemTrayIcon):
|
|||||||
|
|
||||||
|
|
||||||
class KCCGUI(KCC_ui.Ui_mainWindow):
|
class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||||
def selectDefaultOutputFolder(self):
|
def selectDir(self):
|
||||||
dname = QFileDialog.getExistingDirectory(MW, 'Select default output folder', self.defaultOutputFolder)
|
if self.needClean:
|
||||||
if self.is_directory_on_kindle(dname):
|
self.needClean = False
|
||||||
return
|
GUI.jobList.clear()
|
||||||
|
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
|
||||||
if dname != '':
|
if dname != '':
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
dname = dname.replace('/', '\\')
|
dname = dname.replace('/', '\\')
|
||||||
GUI.defaultOutputFolder = dname
|
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
|
||||||
|
GUI.jobList.addItem(dname)
|
||||||
def is_directory_on_kindle(self, dname):
|
GUI.jobList.scrollToBottom()
|
||||||
path = Path(dname)
|
|
||||||
for parent in itertools.chain([path], path.parents):
|
|
||||||
if parent.name == 'documents' and parent.parent.joinpath('system').joinpath('thumbnails').is_dir():
|
|
||||||
self.addMessage("Cannot select Kindle as output directory", 'error')
|
|
||||||
return True
|
|
||||||
|
|
||||||
def selectOutputFolder(self):
|
|
||||||
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
|
|
||||||
if self.is_directory_on_kindle(dname):
|
|
||||||
return
|
|
||||||
if dname != '':
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
dname = dname.replace('/', '\\')
|
|
||||||
GUI.targetDirectory = dname
|
|
||||||
else:
|
|
||||||
GUI.targetDirectory = ''
|
|
||||||
return GUI.targetDirectory
|
|
||||||
|
|
||||||
def selectFile(self):
|
def selectFile(self):
|
||||||
if self.needClean:
|
if self.needClean:
|
||||||
@@ -640,10 +534,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
# noinspection PyCallByClass
|
# noinspection PyCallByClass
|
||||||
QDesktopServices.openUrl(QUrl('https://github.com/ciromattia/kcc/wiki'))
|
QDesktopServices.openUrl(QUrl('https://github.com/ciromattia/kcc/wiki'))
|
||||||
|
|
||||||
def openKofi(self):
|
|
||||||
# noinspection PyCallByClass
|
|
||||||
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
|
|
||||||
|
|
||||||
def modeChange(self, mode):
|
def modeChange(self, mode):
|
||||||
if mode == 1:
|
if mode == 1:
|
||||||
self.currentMode = 1
|
self.currentMode = 1
|
||||||
@@ -666,7 +556,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
GUI.editorButton.setEnabled(status)
|
GUI.editorButton.setEnabled(status)
|
||||||
GUI.wikiButton.setEnabled(status)
|
GUI.wikiButton.setEnabled(status)
|
||||||
GUI.deviceBox.setEnabled(status)
|
GUI.deviceBox.setEnabled(status)
|
||||||
GUI.defaultOutputFolderButton.setEnabled(status)
|
GUI.directoryButton.setEnabled(status)
|
||||||
GUI.clearButton.setEnabled(status)
|
GUI.clearButton.setEnabled(status)
|
||||||
GUI.fileButton.setEnabled(status)
|
GUI.fileButton.setEnabled(status)
|
||||||
GUI.formatBox.setEnabled(status)
|
GUI.formatBox.setEnabled(status)
|
||||||
@@ -735,10 +625,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
def togglequalityBox(self, value):
|
def togglequalityBox(self, value):
|
||||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||||
if value == 2:
|
if value == 2:
|
||||||
if profile['Label'] not in ('K57', 'KPW', 'K810') :
|
if profile['Label'] == 'KV' or profile['Label'] in image.ProfileData.ProfilesKindlePDOC.keys():
|
||||||
self.addMessage('This option is intended for older Kindle models.', 'warning')
|
self.addMessage('This option is intended for older Kindle models.', 'warning')
|
||||||
self.addMessage('On this device, there will be conversion speed and quality issues.', 'warning')
|
self.addMessage('On this device, quality improvement will be negligible.', 'warning')
|
||||||
self.addMessage('Use the Kindle Scribe profile if you want higher resolution when zooming.', 'warning')
|
|
||||||
GUI.upscaleBox.setEnabled(False)
|
GUI.upscaleBox.setEnabled(False)
|
||||||
GUI.upscaleBox.setChecked(True)
|
GUI.upscaleBox.setChecked(True)
|
||||||
else:
|
else:
|
||||||
@@ -863,10 +752,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.worker.sync()
|
self.worker.sync()
|
||||||
else:
|
else:
|
||||||
if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
|
if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
|
||||||
if not self.selectOutputFolder():
|
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
|
||||||
return
|
if dname != '':
|
||||||
elif GUI.defaultOutputFolderBox.isChecked():
|
if sys.platform.startswith('win'):
|
||||||
self.targetDirectory = self.defaultOutputFolder
|
dname = dname.replace('/', '\\')
|
||||||
|
GUI.targetDirectory = dname
|
||||||
|
else:
|
||||||
|
GUI.targetDirectory = ''
|
||||||
else:
|
else:
|
||||||
GUI.targetDirectory = ''
|
GUI.targetDirectory = ''
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
@@ -877,12 +769,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.addMessage('No files selected! Please choose files to convert.', 'error')
|
self.addMessage('No files selected! Please choose files to convert.', 'error')
|
||||||
self.needClean = True
|
self.needClean = True
|
||||||
return
|
return
|
||||||
if GUI.defaultOutputFolderBox.checkState() == Qt.CheckState.PartiallyChecked:
|
|
||||||
parent = Path(self.jobList.item(0).text()).parent
|
|
||||||
target_path = parent.joinpath(f"{parent.name}")
|
|
||||||
if not target_path.exists():
|
|
||||||
target_path.mkdir()
|
|
||||||
self.targetDirectory = str(target_path)
|
|
||||||
if self.currentMode > 2 and (GUI.widthBox.value() == 0 or GUI.heightBox.value() == 0):
|
if self.currentMode > 2 and (GUI.widthBox.value() == 0 or GUI.heightBox.value() == 0):
|
||||||
GUI.jobList.clear()
|
GUI.jobList.clear()
|
||||||
self.addMessage('Target resolution is not set!', 'error')
|
self.addMessage('Target resolution is not set!', 'error')
|
||||||
@@ -914,40 +800,34 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
self.settings.setValue('settingsVersion', __version__)
|
self.settings.setValue('settingsVersion', __version__)
|
||||||
self.settings.setValue('lastPath', self.lastPath)
|
self.settings.setValue('lastPath', self.lastPath)
|
||||||
self.settings.setValue('defaultOutputFolder', self.defaultOutputFolder)
|
|
||||||
self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex())
|
self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex())
|
||||||
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
|
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
|
||||||
self.settings.setValue('startNumber', self.startNumber + 1)
|
self.settings.setValue('startNumber', self.startNumber + 1)
|
||||||
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
|
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
|
||||||
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
|
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState().value,
|
||||||
'rotateBox': GUI.rotateBox.checkState(),
|
'rotateBox': GUI.rotateBox.checkState().value,
|
||||||
'qualityBox': GUI.qualityBox.checkState(),
|
'qualityBox': GUI.qualityBox.checkState().value,
|
||||||
'gammaBox': GUI.gammaBox.checkState(),
|
'gammaBox': GUI.gammaBox.checkState().value,
|
||||||
'autoLevelBox': GUI.autoLevelBox.checkState(),
|
'croppingBox': GUI.croppingBox.checkState().value,
|
||||||
'croppingBox': GUI.croppingBox.checkState(),
|
|
||||||
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
|
||||||
'preserveMarginBox': self.preserveMarginBox.value(),
|
'preserveMarginBox': self.preserveMarginBox.value(),
|
||||||
'interPanelCropBox': GUI.interPanelCropBox.checkState(),
|
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
|
||||||
'upscaleBox': GUI.upscaleBox.checkState(),
|
'upscaleBox': GUI.upscaleBox.checkState().value,
|
||||||
'borderBox': GUI.borderBox.checkState(),
|
'borderBox': GUI.borderBox.checkState().value,
|
||||||
'webtoonBox': GUI.webtoonBox.checkState(),
|
'webtoonBox': GUI.webtoonBox.checkState().value,
|
||||||
'outputSplit': GUI.outputSplit.checkState(),
|
'outputSplit': GUI.outputSplit.checkState().value,
|
||||||
'colorBox': GUI.colorBox.checkState(),
|
'colorBox': GUI.colorBox.checkState().value,
|
||||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
|
||||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
|
||||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
'mozJpegBox': GUI.mozJpegBox.checkState().value,
|
||||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
|
||||||
'widthBox': GUI.widthBox.value(),
|
'widthBox': GUI.widthBox.value(),
|
||||||
'heightBox': GUI.heightBox.value(),
|
'heightBox': GUI.heightBox.value(),
|
||||||
'deleteBox': GUI.deleteBox.checkState(),
|
'deleteBox': GUI.deleteBox.checkState().value,
|
||||||
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
|
||||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
'noRotateBox': GUI.noRotateBox.checkState().value,
|
||||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
'maximizeStrips': GUI.maximizeStrips.checkState().value,
|
||||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
|
||||||
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
|
||||||
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
|
||||||
'gammaSlider': float(self.gammaValue) * 100,
|
'gammaSlider': float(self.gammaValue) * 100,
|
||||||
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState(),
|
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
|
||||||
'chunkSizeBox': GUI.chunkSizeBox.value()})
|
'chunkSizeBox': GUI.chunkSizeBox.value()})
|
||||||
self.settings.sync()
|
self.settings.sync()
|
||||||
self.tray.hide()
|
self.tray.hide()
|
||||||
@@ -1009,7 +889,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
|
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
|
||||||
' is outdated! MOBI conversion might fail.', 'warning')
|
' is outdated! MOBI conversion might fail.', 'warning')
|
||||||
break
|
break
|
||||||
except (FileNotFoundError, CalledProcessError):
|
except FileNotFoundError:
|
||||||
self.kindleGen = False
|
self.kindleGen = False
|
||||||
if startup:
|
if startup:
|
||||||
self.display_kindlegen_missing()
|
self.display_kindlegen_missing()
|
||||||
@@ -1022,21 +902,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
self.setupUi(MW)
|
self.setupUi(MW)
|
||||||
self.editor = KCCGUI_MetaEditor()
|
self.editor = KCCGUI_MetaEditor()
|
||||||
self.icons = Icons()
|
self.icons = Icons()
|
||||||
self.settings = QSettings('ciromattia', 'kcc9')
|
self.settings = QSettings('ciromattia', 'kcc')
|
||||||
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
|
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
|
||||||
self.lastPath = self.settings.value('lastPath', '', type=str)
|
self.lastPath = self.settings.value('lastPath', '', type=str)
|
||||||
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
|
|
||||||
if not os.path.exists(self.defaultOutputFolder):
|
|
||||||
self.defaultOutputFolder = ''
|
|
||||||
self.lastDevice = self.settings.value('lastDevice', 0, type=int)
|
self.lastDevice = self.settings.value('lastDevice', 0, type=int)
|
||||||
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
|
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
|
||||||
self.startNumber = self.settings.value('startNumber', 0, type=int)
|
self.startNumber = self.settings.value('startNumber', 0, type=int)
|
||||||
self.windowSize = self.settings.value('windowSize', '0x0', type=str)
|
self.windowSize = self.settings.value('windowSize', '0x0', type=str)
|
||||||
default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}
|
self.options = self.settings.value('options', {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100})
|
||||||
try:
|
|
||||||
self.options = self.settings.value('options', default_options)
|
|
||||||
except Exception:
|
|
||||||
self.options = default_options
|
|
||||||
self.worker = WorkerThread()
|
self.worker = WorkerThread()
|
||||||
self.versionCheck = VersionThread()
|
self.versionCheck = VersionThread()
|
||||||
self.progress = ProgressThread()
|
self.progress = ProgressThread()
|
||||||
@@ -1060,7 +933,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
if self.windowSize == '0x0':
|
if self.windowSize == '0x0':
|
||||||
MW.resize(500, 500)
|
MW.resize(500, 500)
|
||||||
elif sys.platform.startswith('darwin'):
|
elif sys.platform.startswith('darwin'):
|
||||||
for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
|
for element in ['editorButton', 'wikiButton', 'directoryButton', 'clearButton', 'fileButton', 'deviceBox',
|
||||||
'convertButton', 'formatBox']:
|
'convertButton', 'formatBox']:
|
||||||
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
||||||
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
||||||
@@ -1073,7 +946,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
|
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
|
||||||
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
||||||
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
||||||
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
|
||||||
"KFX (does not work)": {'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'},
|
||||||
@@ -1082,35 +954,33 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
|
|
||||||
|
|
||||||
self.profiles = {
|
self.profiles = {
|
||||||
"Kindle Oasis 9/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle Oasis 9/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
|
||||||
"Kindle 8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle Oasis 8": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K810'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
||||||
"Kindle Oasis 8": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle Voyage": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
|
||||||
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
||||||
"Kindle Scribe": {
|
"Kindle Scribe": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
|
||||||
},
|
},
|
||||||
"Kindle 11": {
|
"Kindle 11": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
|
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
|
||||||
},
|
},
|
||||||
"Kindle Paperwhite 11": {
|
"Kindle PW 11": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
|
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
|
||||||
},
|
},
|
||||||
"Kindle Paperwhite 12": {
|
"Kindle PW 12": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
|
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
|
||||||
},
|
},
|
||||||
"Kindle Colorsoft": {
|
"Kindle CS 12": {
|
||||||
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
|
'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO',
|
||||||
},
|
},
|
||||||
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle PW 7/10": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
|
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
|
||||||
"Kindle Paperwhite 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle PW 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KPW'},
|
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KPW'},
|
||||||
"Kindle 4/5/7": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
"Kindle 4/5/7/8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
|
||||||
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K57'},
|
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K578'},
|
||||||
"Kindle DX": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2,
|
"Kindle DX": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2,
|
||||||
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KDX'},
|
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KDX'},
|
||||||
"Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
|
"Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
|
||||||
@@ -1155,20 +1025,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
'Label': 'KoS'},
|
'Label': 'KoS'},
|
||||||
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'KoE'},
|
'Label': 'KoE'},
|
||||||
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
|
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'Rmk1'},
|
'Label': 'Rmk1'},
|
||||||
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
|
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
|
||||||
'Label': 'Rmk2'},
|
'Label': 'Rmk2'},
|
||||||
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
|
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True,
|
||||||
'Label': 'RmkPP'},
|
'Label': 'RmkPP'},
|
||||||
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
|
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
|
||||||
'Label': 'OTHER'},
|
'Label': 'OTHER'},
|
||||||
}
|
}
|
||||||
profilesGUI = [
|
profilesGUI = [
|
||||||
"Kindle Colorsoft",
|
"Kindle CS 12",
|
||||||
"Kindle Paperwhite 12",
|
"Kindle PW 12",
|
||||||
"Kindle Scribe",
|
"Kindle Scribe",
|
||||||
"Kindle Paperwhite 11",
|
"Kindle PW 11",
|
||||||
"Kindle 11",
|
"Kindle 11",
|
||||||
"Kindle Oasis 9/10",
|
"Kindle Oasis 9/10",
|
||||||
"Separator",
|
"Separator",
|
||||||
@@ -1186,12 +1056,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"Separator",
|
"Separator",
|
||||||
"Other",
|
"Other",
|
||||||
"Separator",
|
"Separator",
|
||||||
"Kindle 8/10",
|
|
||||||
"Kindle Oasis 8",
|
"Kindle Oasis 8",
|
||||||
"Kindle Paperwhite 7/10",
|
"Kindle PW 7/10",
|
||||||
"Kindle Voyage",
|
"Kindle Voyage",
|
||||||
"Kindle Paperwhite 5/6",
|
"Kindle PW 5/6",
|
||||||
"Kindle 4/5/7",
|
"Kindle 4/5/7/8/10",
|
||||||
"Kindle Touch",
|
"Kindle Touch",
|
||||||
"Kindle Keyboard",
|
"Kindle Keyboard",
|
||||||
"Kindle DX",
|
"Kindle DX",
|
||||||
@@ -1210,43 +1079,34 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
|||||||
"Kobo Mini/Touch",
|
"Kobo Mini/Touch",
|
||||||
]
|
]
|
||||||
|
|
||||||
link_dict = {
|
statusBarLabel = QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
|
||||||
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
|
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
|
||||||
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
|
'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461'
|
||||||
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj",
|
'">FORUM</a></b>')
|
||||||
'COMMISSIONS': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
|
|
||||||
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
|
|
||||||
'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461",
|
|
||||||
'DISCORD': "https://discord.com/invite/qj7wpnUHav",
|
|
||||||
}
|
|
||||||
|
|
||||||
link_html_list = [f'<a href="{v}">{k}</a>' for k, v in link_dict.items()]
|
|
||||||
statusBarLabel = QLabel(f'<b>{" - ".join(link_html_list)}</b>')
|
|
||||||
statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
statusBarLabel.setOpenExternalLinks(True)
|
statusBarLabel.setOpenExternalLinks(True)
|
||||||
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
|
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
|
||||||
|
|
||||||
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
|
self.addMessage('<b>Welcome!</b>', 'info')
|
||||||
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
|
self.addMessage('<b>Remember:</b> All options have additional information in tooltips.', 'info')
|
||||||
if self.startNumber < 5:
|
if self.startNumber < 5:
|
||||||
self.addMessage('Since you are a new user of <b>KCC</b> please see few '
|
self.addMessage('Since you are a new user of <b>KCC</b> please see few '
|
||||||
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
|
||||||
'info')
|
'info')
|
||||||
|
|
||||||
self.tar = 'tar' in available_archive_tools()
|
self.tar = 'tar' in available_archive_tools()
|
||||||
self.sevenzip = SEVENZIP in available_archive_tools()
|
self.sevenzip = '7z' in available_archive_tools()
|
||||||
if not any([self.tar, self.sevenzip]):
|
if not any([self.tar, self.sevenzip]):
|
||||||
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 CBZ/CBR/ZIP/etc processing.', 'warning')
|
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
|
||||||
self.detectKindleGen(True)
|
self.detectKindleGen(True)
|
||||||
|
|
||||||
APP.messageFromOtherInstance.connect(self.handleMessage)
|
APP.messageFromOtherInstance.connect(self.handleMessage)
|
||||||
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
|
GUI.directoryButton.clicked.connect(self.selectDir)
|
||||||
GUI.clearButton.clicked.connect(self.clearJobs)
|
GUI.clearButton.clicked.connect(self.clearJobs)
|
||||||
GUI.fileButton.clicked.connect(self.selectFile)
|
GUI.fileButton.clicked.connect(self.selectFile)
|
||||||
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
||||||
GUI.wikiButton.clicked.connect(self.openWiki)
|
GUI.wikiButton.clicked.connect(self.openWiki)
|
||||||
GUI.kofiButton.clicked.connect(self.openKofi)
|
|
||||||
GUI.convertButton.clicked.connect(self.convertStart)
|
GUI.convertButton.clicked.connect(self.convertStart)
|
||||||
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
||||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'KCC.ui'
|
## Form generated from reading UI file 'KCC.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.9.1
|
## Created by: Qt User Interface Compiler version 6.8.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
|
|||||||
def setupUi(self, mainWindow):
|
def setupUi(self, mainWindow):
|
||||||
if not mainWindow.objectName():
|
if not mainWindow.objectName():
|
||||||
mainWindow.setObjectName(u"mainWindow")
|
mainWindow.setObjectName(u"mainWindow")
|
||||||
mainWindow.resize(566, 573)
|
mainWindow.resize(519, 572)
|
||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
mainWindow.setWindowIcon(icon)
|
mainWindow.setWindowIcon(icon)
|
||||||
@@ -37,7 +37,6 @@ class Ui_mainWindow(object):
|
|||||||
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
|
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
|
||||||
self.jobList = QListWidget(self.centralWidget)
|
self.jobList = QListWidget(self.centralWidget)
|
||||||
self.jobList.setObjectName(u"jobList")
|
self.jobList.setObjectName(u"jobList")
|
||||||
self.jobList.setMinimumSize(QSize(0, 150))
|
|
||||||
self.jobList.setStyleSheet(u"")
|
self.jobList.setStyleSheet(u"")
|
||||||
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
||||||
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
@@ -59,22 +58,12 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.horizontalLayout.addWidget(self.editorButton)
|
self.horizontalLayout.addWidget(self.editorButton)
|
||||||
|
|
||||||
self.kofiButton = QPushButton(self.toolWidget)
|
|
||||||
self.kofiButton.setObjectName(u"kofiButton")
|
|
||||||
self.kofiButton.setMinimumSize(QSize(0, 30))
|
|
||||||
icon2 = QIcon()
|
|
||||||
icon2.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
self.kofiButton.setIcon(icon2)
|
|
||||||
self.kofiButton.setIconSize(QSize(19, 16))
|
|
||||||
|
|
||||||
self.horizontalLayout.addWidget(self.kofiButton)
|
|
||||||
|
|
||||||
self.wikiButton = QPushButton(self.toolWidget)
|
self.wikiButton = QPushButton(self.toolWidget)
|
||||||
self.wikiButton.setObjectName(u"wikiButton")
|
self.wikiButton.setObjectName(u"wikiButton")
|
||||||
self.wikiButton.setMinimumSize(QSize(0, 30))
|
self.wikiButton.setMinimumSize(QSize(0, 30))
|
||||||
icon3 = QIcon()
|
icon2 = QIcon()
|
||||||
icon3.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon2.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.wikiButton.setIcon(icon3)
|
self.wikiButton.setIcon(icon2)
|
||||||
|
|
||||||
self.horizontalLayout.addWidget(self.wikiButton)
|
self.horizontalLayout.addWidget(self.wikiButton)
|
||||||
|
|
||||||
@@ -91,75 +80,63 @@ class Ui_mainWindow(object):
|
|||||||
self.gridLayout_4 = QGridLayout(self.buttonWidget)
|
self.gridLayout_4 = QGridLayout(self.buttonWidget)
|
||||||
self.gridLayout_4.setObjectName(u"gridLayout_4")
|
self.gridLayout_4.setObjectName(u"gridLayout_4")
|
||||||
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
|
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.directoryButton = QPushButton(self.buttonWidget)
|
||||||
|
self.directoryButton.setObjectName(u"directoryButton")
|
||||||
|
self.directoryButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon3 = QIcon()
|
||||||
|
icon3.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.directoryButton.setIcon(icon3)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.directoryButton, 0, 0, 1, 1)
|
||||||
|
|
||||||
|
self.fileButton = QPushButton(self.buttonWidget)
|
||||||
|
self.fileButton.setObjectName(u"fileButton")
|
||||||
|
self.fileButton.setMinimumSize(QSize(0, 30))
|
||||||
|
icon4 = QIcon()
|
||||||
|
icon4.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.fileButton.setIcon(icon4)
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.fileButton, 0, 3, 1, 1)
|
||||||
|
|
||||||
|
self.deviceBox = QComboBox(self.buttonWidget)
|
||||||
|
self.deviceBox.setObjectName(u"deviceBox")
|
||||||
|
self.deviceBox.setMinimumSize(QSize(0, 28))
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.deviceBox, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.formatBox = QComboBox(self.buttonWidget)
|
||||||
|
self.formatBox.setObjectName(u"formatBox")
|
||||||
|
self.formatBox.setMinimumSize(QSize(0, 28))
|
||||||
|
|
||||||
|
self.gridLayout_4.addWidget(self.formatBox, 1, 3, 1, 1)
|
||||||
|
|
||||||
self.convertButton = QPushButton(self.buttonWidget)
|
self.convertButton = QPushButton(self.buttonWidget)
|
||||||
self.convertButton.setObjectName(u"convertButton")
|
self.convertButton.setObjectName(u"convertButton")
|
||||||
self.convertButton.setMinimumSize(QSize(0, 30))
|
self.convertButton.setMinimumSize(QSize(0, 30))
|
||||||
font = QFont()
|
font = QFont()
|
||||||
font.setBold(True)
|
font.setBold(True)
|
||||||
self.convertButton.setFont(font)
|
self.convertButton.setFont(font)
|
||||||
icon4 = QIcon()
|
icon5 = QIcon()
|
||||||
icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon5.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.convertButton.setIcon(icon4)
|
self.convertButton.setIcon(icon5)
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
|
self.gridLayout_4.addWidget(self.convertButton, 1, 2, 1, 1)
|
||||||
|
|
||||||
self.clearButton = QPushButton(self.buttonWidget)
|
self.clearButton = QPushButton(self.buttonWidget)
|
||||||
self.clearButton.setObjectName(u"clearButton")
|
self.clearButton.setObjectName(u"clearButton")
|
||||||
self.clearButton.setMinimumSize(QSize(0, 30))
|
self.clearButton.setMinimumSize(QSize(0, 30))
|
||||||
icon5 = QIcon()
|
|
||||||
icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
self.clearButton.setIcon(icon5)
|
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
|
|
||||||
|
|
||||||
self.deviceBox = QComboBox(self.buttonWidget)
|
|
||||||
self.deviceBox.setObjectName(u"deviceBox")
|
|
||||||
self.deviceBox.setMinimumSize(QSize(0, 28))
|
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
self.fileButton = QPushButton(self.buttonWidget)
|
|
||||||
self.fileButton.setObjectName(u"fileButton")
|
|
||||||
self.fileButton.setMinimumSize(QSize(0, 30))
|
|
||||||
icon6 = QIcon()
|
icon6 = QIcon()
|
||||||
icon6.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
icon6.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
self.fileButton.setIcon(icon6)
|
self.clearButton.setIcon(icon6)
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
|
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1)
|
||||||
|
|
||||||
self.defaultOutputFolderButton = QPushButton(self.buttonWidget)
|
|
||||||
self.defaultOutputFolderButton.setObjectName(u"defaultOutputFolderButton")
|
|
||||||
self.defaultOutputFolderButton.setMinimumSize(QSize(0, 30))
|
|
||||||
icon7 = QIcon()
|
|
||||||
icon7.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
|
||||||
self.defaultOutputFolderButton.setIcon(icon7)
|
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.defaultOutputFolderButton, 0, 5, 1, 1)
|
|
||||||
|
|
||||||
self.defaultOutputFolderBox = QCheckBox(self.buttonWidget)
|
|
||||||
self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
|
|
||||||
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
||||||
sizePolicy1.setHorizontalStretch(0)
|
|
||||||
sizePolicy1.setVerticalStretch(0)
|
|
||||||
sizePolicy1.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
|
|
||||||
self.defaultOutputFolderBox.setSizePolicy(sizePolicy1)
|
|
||||||
self.defaultOutputFolderBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1)
|
|
||||||
|
|
||||||
self.formatBox = QComboBox(self.buttonWidget)
|
|
||||||
self.formatBox.setObjectName(u"formatBox")
|
|
||||||
self.formatBox.setMinimumSize(QSize(0, 28))
|
|
||||||
|
|
||||||
self.gridLayout_4.addWidget(self.formatBox, 1, 4, 1, 2)
|
|
||||||
|
|
||||||
|
self.directoryButton.raise_()
|
||||||
self.clearButton.raise_()
|
self.clearButton.raise_()
|
||||||
|
self.fileButton.raise_()
|
||||||
self.deviceBox.raise_()
|
self.deviceBox.raise_()
|
||||||
self.convertButton.raise_()
|
self.convertButton.raise_()
|
||||||
self.formatBox.raise_()
|
self.formatBox.raise_()
|
||||||
self.defaultOutputFolderButton.raise_()
|
|
||||||
self.fileButton.raise_()
|
|
||||||
self.defaultOutputFolderBox.raise_()
|
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
|
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
|
||||||
|
|
||||||
@@ -180,24 +157,24 @@ class Ui_mainWindow(object):
|
|||||||
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
|
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
|
||||||
self.hLabel = QLabel(self.customWidget)
|
self.hLabel = QLabel(self.customWidget)
|
||||||
self.hLabel.setObjectName(u"hLabel")
|
self.hLabel.setObjectName(u"hLabel")
|
||||||
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
|
||||||
sizePolicy2.setHorizontalStretch(0)
|
sizePolicy1.setHorizontalStretch(0)
|
||||||
sizePolicy2.setVerticalStretch(0)
|
sizePolicy1.setVerticalStretch(0)
|
||||||
sizePolicy2.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
|
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
|
||||||
self.hLabel.setSizePolicy(sizePolicy2)
|
self.hLabel.setSizePolicy(sizePolicy1)
|
||||||
|
|
||||||
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
|
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
|
||||||
|
|
||||||
self.widthBox = QSpinBox(self.customWidget)
|
self.widthBox = QSpinBox(self.customWidget)
|
||||||
self.widthBox.setObjectName(u"widthBox")
|
self.widthBox.setObjectName(u"widthBox")
|
||||||
self.widthBox.setMaximum(2400)
|
self.widthBox.setMaximum(2160)
|
||||||
|
|
||||||
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
|
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
|
||||||
|
|
||||||
self.wLabel = QLabel(self.customWidget)
|
self.wLabel = QLabel(self.customWidget)
|
||||||
self.wLabel.setObjectName(u"wLabel")
|
self.wLabel.setObjectName(u"wLabel")
|
||||||
sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
|
sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
|
||||||
self.wLabel.setSizePolicy(sizePolicy2)
|
self.wLabel.setSizePolicy(sizePolicy1)
|
||||||
|
|
||||||
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
|
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
|
||||||
|
|
||||||
@@ -236,8 +213,11 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.preserveMarginBox = QSpinBox(self.croppingWidget)
|
self.preserveMarginBox = QSpinBox(self.croppingWidget)
|
||||||
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
|
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
|
||||||
sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
|
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||||
self.preserveMarginBox.setSizePolicy(sizePolicy1)
|
sizePolicy2.setHorizontalStretch(0)
|
||||||
|
sizePolicy2.setVerticalStretch(0)
|
||||||
|
sizePolicy2.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
|
||||||
|
self.preserveMarginBox.setSizePolicy(sizePolicy2)
|
||||||
self.preserveMarginBox.setMaximum(99)
|
self.preserveMarginBox.setMaximum(99)
|
||||||
self.preserveMarginBox.setSingleStep(5)
|
self.preserveMarginBox.setSingleStep(5)
|
||||||
self.preserveMarginBox.setValue(0)
|
self.preserveMarginBox.setValue(0)
|
||||||
@@ -252,32 +232,65 @@ class Ui_mainWindow(object):
|
|||||||
self.gridLayout_2 = QGridLayout(self.optionWidget)
|
self.gridLayout_2 = QGridLayout(self.optionWidget)
|
||||||
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||||
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||||
self.gammaBox = QCheckBox(self.optionWidget)
|
self.croppingBox = QCheckBox(self.optionWidget)
|
||||||
self.gammaBox.setObjectName(u"gammaBox")
|
self.croppingBox.setObjectName(u"croppingBox")
|
||||||
|
self.croppingBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
|
||||||
|
|
||||||
self.mangaBox = QCheckBox(self.optionWidget)
|
self.mangaBox = QCheckBox(self.optionWidget)
|
||||||
self.mangaBox.setObjectName(u"mangaBox")
|
self.mangaBox.setObjectName(u"mangaBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
|
||||||
|
|
||||||
|
self.webtoonBox = QCheckBox(self.optionWidget)
|
||||||
|
self.webtoonBox.setObjectName(u"webtoonBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
|
||||||
|
|
||||||
|
self.rotateBox = QCheckBox(self.optionWidget)
|
||||||
|
self.rotateBox.setObjectName(u"rotateBox")
|
||||||
|
self.rotateBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
|
||||||
|
|
||||||
self.borderBox = QCheckBox(self.optionWidget)
|
self.borderBox = QCheckBox(self.optionWidget)
|
||||||
self.borderBox.setObjectName(u"borderBox")
|
self.borderBox.setObjectName(u"borderBox")
|
||||||
self.borderBox.setTristate(True)
|
self.borderBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
|
||||||
|
|
||||||
|
self.gammaBox = QCheckBox(self.optionWidget)
|
||||||
|
self.gammaBox.setObjectName(u"gammaBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
|
||||||
|
|
||||||
self.interPanelCropBox = QCheckBox(self.optionWidget)
|
self.interPanelCropBox = QCheckBox(self.optionWidget)
|
||||||
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
|
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
|
||||||
self.interPanelCropBox.setTristate(True)
|
self.interPanelCropBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
|
||||||
|
|
||||||
self.fileFusionBox = QCheckBox(self.optionWidget)
|
self.colorBox = QCheckBox(self.optionWidget)
|
||||||
self.fileFusionBox.setObjectName(u"fileFusionBox")
|
self.colorBox.setObjectName(u"colorBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
|
||||||
|
|
||||||
|
self.qualityBox = QCheckBox(self.optionWidget)
|
||||||
|
self.qualityBox.setObjectName(u"qualityBox")
|
||||||
|
self.qualityBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
||||||
|
|
||||||
|
self.disableProcessingBox = QCheckBox(self.optionWidget)
|
||||||
|
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
|
||||||
|
|
||||||
|
self.maximizeStrips = QCheckBox(self.optionWidget)
|
||||||
|
self.maximizeStrips.setObjectName(u"maximizeStrips")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
|
||||||
|
|
||||||
self.authorEdit = QLineEdit(self.optionWidget)
|
self.authorEdit = QLineEdit(self.optionWidget)
|
||||||
self.authorEdit.setObjectName(u"authorEdit")
|
self.authorEdit.setObjectName(u"authorEdit")
|
||||||
@@ -291,100 +304,47 @@ class Ui_mainWindow(object):
|
|||||||
|
|
||||||
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
|
||||||
|
|
||||||
self.rotateFirstBox = QCheckBox(self.optionWidget)
|
|
||||||
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
|
|
||||||
|
|
||||||
self.eraseRainbowBox = QCheckBox(self.optionWidget)
|
|
||||||
self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
|
|
||||||
|
|
||||||
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
|
|
||||||
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
|
|
||||||
|
|
||||||
self.rotateBox = QCheckBox(self.optionWidget)
|
|
||||||
self.rotateBox.setObjectName(u"rotateBox")
|
|
||||||
self.rotateBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
|
|
||||||
|
|
||||||
self.outputSplit = QCheckBox(self.optionWidget)
|
|
||||||
self.outputSplit.setObjectName(u"outputSplit")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
|
|
||||||
|
|
||||||
self.metadataTitleBox = QCheckBox(self.optionWidget)
|
|
||||||
self.metadataTitleBox.setObjectName(u"metadataTitleBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
|
|
||||||
|
|
||||||
self.qualityBox = QCheckBox(self.optionWidget)
|
|
||||||
self.qualityBox.setObjectName(u"qualityBox")
|
|
||||||
self.qualityBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
|
||||||
|
|
||||||
self.spreadShiftBox = QCheckBox(self.optionWidget)
|
|
||||||
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
|
|
||||||
|
|
||||||
self.disableProcessingBox = QCheckBox(self.optionWidget)
|
|
||||||
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
|
|
||||||
|
|
||||||
self.webtoonBox = QCheckBox(self.optionWidget)
|
|
||||||
self.webtoonBox.setObjectName(u"webtoonBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
|
|
||||||
|
|
||||||
self.colorBox = QCheckBox(self.optionWidget)
|
|
||||||
self.colorBox.setObjectName(u"colorBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
|
|
||||||
|
|
||||||
self.croppingBox = QCheckBox(self.optionWidget)
|
|
||||||
self.croppingBox.setObjectName(u"croppingBox")
|
|
||||||
self.croppingBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
|
|
||||||
|
|
||||||
self.maximizeStrips = QCheckBox(self.optionWidget)
|
|
||||||
self.maximizeStrips.setObjectName(u"maximizeStrips")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
|
|
||||||
|
|
||||||
self.noRotateBox = QCheckBox(self.optionWidget)
|
|
||||||
self.noRotateBox.setObjectName(u"noRotateBox")
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
|
|
||||||
|
|
||||||
self.deleteBox = QCheckBox(self.optionWidget)
|
self.deleteBox = QCheckBox(self.optionWidget)
|
||||||
self.deleteBox.setObjectName(u"deleteBox")
|
self.deleteBox.setObjectName(u"deleteBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
|
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
|
||||||
|
|
||||||
self.upscaleBox = QCheckBox(self.optionWidget)
|
|
||||||
self.upscaleBox.setObjectName(u"upscaleBox")
|
|
||||||
self.upscaleBox.setTristate(True)
|
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
|
|
||||||
|
|
||||||
self.mozJpegBox = QCheckBox(self.optionWidget)
|
self.mozJpegBox = QCheckBox(self.optionWidget)
|
||||||
self.mozJpegBox.setObjectName(u"mozJpegBox")
|
self.mozJpegBox.setObjectName(u"mozJpegBox")
|
||||||
self.mozJpegBox.setTristate(True)
|
self.mozJpegBox.setTristate(True)
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
|
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
|
||||||
|
|
||||||
self.autoLevelBox = QCheckBox(self.optionWidget)
|
self.spreadShiftBox = QCheckBox(self.optionWidget)
|
||||||
self.autoLevelBox.setObjectName(u"autoLevelBox")
|
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
|
||||||
|
|
||||||
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
|
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
|
||||||
|
|
||||||
|
self.upscaleBox = QCheckBox(self.optionWidget)
|
||||||
|
self.upscaleBox.setObjectName(u"upscaleBox")
|
||||||
|
self.upscaleBox.setTristate(True)
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
|
||||||
|
|
||||||
|
self.outputSplit = QCheckBox(self.optionWidget)
|
||||||
|
self.outputSplit.setObjectName(u"outputSplit")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
|
||||||
|
|
||||||
|
self.noRotateBox = QCheckBox(self.optionWidget)
|
||||||
|
self.noRotateBox.setObjectName(u"noRotateBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
|
||||||
|
|
||||||
|
self.reduceRainbowBox = QCheckBox(self.optionWidget)
|
||||||
|
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
|
||||||
|
|
||||||
|
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
|
||||||
|
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
|
||||||
|
|
||||||
|
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||||
@@ -454,7 +414,9 @@ class Ui_mainWindow(object):
|
|||||||
self.statusBar.setSizeGripEnabled(False)
|
self.statusBar.setSizeGripEnabled(False)
|
||||||
mainWindow.setStatusBar(self.statusBar)
|
mainWindow.setStatusBar(self.statusBar)
|
||||||
QWidget.setTabOrder(self.convertButton, self.clearButton)
|
QWidget.setTabOrder(self.convertButton, self.clearButton)
|
||||||
QWidget.setTabOrder(self.clearButton, self.deviceBox)
|
QWidget.setTabOrder(self.clearButton, self.directoryButton)
|
||||||
|
QWidget.setTabOrder(self.directoryButton, self.fileButton)
|
||||||
|
QWidget.setTabOrder(self.fileButton, self.deviceBox)
|
||||||
QWidget.setTabOrder(self.deviceBox, self.formatBox)
|
QWidget.setTabOrder(self.deviceBox, self.formatBox)
|
||||||
QWidget.setTabOrder(self.formatBox, self.mangaBox)
|
QWidget.setTabOrder(self.formatBox, self.mangaBox)
|
||||||
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
|
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
|
||||||
@@ -474,8 +436,8 @@ class Ui_mainWindow(object):
|
|||||||
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
|
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
|
||||||
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
|
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
|
||||||
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
|
||||||
QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
|
QWidget.setTabOrder(self.interPanelCropBox, self.reduceRainbowBox)
|
||||||
QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
|
QWidget.setTabOrder(self.reduceRainbowBox, self.heightBox)
|
||||||
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
|
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
|
||||||
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
|
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
|
||||||
QWidget.setTabOrder(self.editorButton, self.wikiButton)
|
QWidget.setTabOrder(self.editorButton, self.wikiButton)
|
||||||
@@ -494,31 +456,26 @@ class Ui_mainWindow(object):
|
|||||||
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
|
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
|
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
|
||||||
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
|
|
||||||
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
|
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", 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.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
|
|
||||||
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add image folder", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
|
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
|
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.defaultOutputFolderButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use this to select the default output directory.</p></body></html>", None))
|
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.defaultOutputFolderButton.setText("")
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.defaultOutputFolderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - next to source<br/></span>Place output files next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - folder next to source<br/></span>Place output files in a folder next to source files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Custom<br/></span>Place output files in custom directory specified by right button</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.defaultOutputFolderBox.setText(QCoreApplication.translate("mainWindow", u"Output Folder", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
|
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // 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.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
|
||||||
|
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
@@ -539,101 +496,85 @@ class Ui_mainWindow(object):
|
|||||||
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
|
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
|
||||||
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
|
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
|
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
|
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
|
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
|
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in "Chunk size MB" before split occurs.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
|
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
|
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", 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)
|
||||||
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
|
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
|
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
|
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
|
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
|
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
|
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
|
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
|
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
|
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
|
||||||
#if QT_CONFIG(tooltip)
|
|
||||||
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
|
|
||||||
#endif // QT_CONFIG(tooltip)
|
|
||||||
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
|
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
|
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
|
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Set the most common dark pixel value to be the black point for leveling on a page by page basis.</p><p>Skipped for any images that were originally color.</p><p>Use only if default autocontrast still results in very gray faded blacks. </p><p>Reccomended to use with Custom Gamma = 1.0 (Disabled).</p></body></html>", None))
|
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
|
||||||
#endif // QT_CONFIG(tooltip)
|
#endif // QT_CONFIG(tooltip)
|
||||||
self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Aggressive Black Point", None))
|
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in "Chunk size MB" before split occurs.</p></body></html>", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
|
||||||
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
|
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
|
||||||
#if QT_CONFIG(tooltip)
|
#if QT_CONFIG(tooltip)
|
||||||
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))
|
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
## Form generated from reading UI file 'MetaEditor.ui'
|
## Form generated from reading UI file 'MetaEditor.ui'
|
||||||
##
|
##
|
||||||
## Created by: Qt User Interface Compiler version 6.9.1
|
## Created by: Qt User Interface Compiler version 6.8.2
|
||||||
##
|
##
|
||||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
__version__ = '9.1.0'
|
__version__ = '7.4.1'
|
||||||
__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'
|
||||||
|
|||||||
@@ -28,27 +28,23 @@ from copy import copy
|
|||||||
from glob import glob, escape
|
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 zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
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
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from natsort import os_sort_keygen, os_sorted
|
from natsort import os_sort_keygen
|
||||||
from slugify import slugify as slugify_ext
|
from slugify import slugify as slugify_ext
|
||||||
from PIL import Image, ImageFile
|
from PIL import Image, ImageFile
|
||||||
from pathlib import Path
|
|
||||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||||
from psutil import virtual_memory, disk_usage
|
from psutil import virtual_memory, disk_usage
|
||||||
from html import escape as hescape
|
from html import escape as hescape
|
||||||
import pymupdf
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
|
from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
|
||||||
from .comicarchive import SEVENZIP, available_archive_tools
|
|
||||||
from . import comic2panel
|
from . import comic2panel
|
||||||
from . import image
|
from . import image
|
||||||
from . import comicarchive
|
from . import comicarchive
|
||||||
|
from . import pdfjpgextract
|
||||||
from . import dualmetafix
|
from . import dualmetafix
|
||||||
from . import metadata
|
from . import metadata
|
||||||
from . import kindle
|
from . import kindle
|
||||||
@@ -81,7 +77,7 @@ def main(argv=None):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
def buildHTML(path, imgfile, imgfilepath):
|
||||||
key = pathlib.Path(imgfilepath).name
|
key = pathlib.Path(imgfilepath).name
|
||||||
filename = getImageFileName(imgfile)
|
filename = getImageFileName(imgfile)
|
||||||
deviceres = options.profileData[1]
|
deviceres = options.profileData[1]
|
||||||
@@ -107,13 +103,10 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
|||||||
os.makedirs(htmlpath)
|
os.makedirs(htmlpath)
|
||||||
htmlfile = os.path.join(htmlpath, filename[0] + '.xhtml')
|
htmlfile = os.path.join(htmlpath, filename[0] + '.xhtml')
|
||||||
imgsize = Image.open(os.path.join(head, "Images", postfix, imgfile)).size
|
imgsize = Image.open(os.path.join(head, "Images", postfix, imgfile)).size
|
||||||
imgsizeframe = list(imgsize)
|
|
||||||
imgsize2 = (0, 0)
|
|
||||||
if imgfile2:
|
|
||||||
imgsize2 = Image.open(os.path.join(head, "Images", postfix, imgfile2)).size
|
|
||||||
imgsizeframe[1] += imgsize2[1]
|
|
||||||
if options.hq:
|
if options.hq:
|
||||||
imgsizeframe = (int(imgsizeframe[0] // 1.5), int(imgsizeframe[1] // 1.5))
|
imgsizeframe = (int(imgsize[0] // 1.5), int(imgsize[1] // 1.5))
|
||||||
|
else:
|
||||||
|
imgsizeframe = imgsize
|
||||||
f = open(htmlfile, "w", encoding='UTF-8')
|
f = open(htmlfile, "w", encoding='UTF-8')
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||||
"<!DOCTYPE html>\n",
|
"<!DOCTYPE html>\n",
|
||||||
@@ -122,18 +115,14 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
|||||||
"<title>", hescape(filename[0]), "</title>\n",
|
"<title>", hescape(filename[0]), "</title>\n",
|
||||||
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
||||||
"<meta name=\"viewport\" "
|
"<meta name=\"viewport\" "
|
||||||
"content=\"width=" + str(imgsizeframe[0]) + ", height=" + str(imgsizeframe[1]) + "\"/>\n"
|
"content=\"width=" + str(imgsize[0]) + ", height=" + str(imgsize[1]) + "\"/>\n"
|
||||||
"</head>\n",
|
"</head>\n",
|
||||||
"<body style=\"" + additionalStyle + "\">\n",
|
"<body style=\"" + additionalStyle + "\">\n",
|
||||||
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
|
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
|
||||||
])
|
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
||||||
if options.iskindle:
|
'<div style="display:none;">.</div>\n',
|
||||||
# this display none div fixes formatting issues with virtual panel mode, for some reason
|
"<img width=\"" + str(imgsizeframe[0]) + "\" height=\"" + str(imgsizeframe[1]) + "\" ",
|
||||||
f.write('<div style="display:none;">.</div>\n')
|
"src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n</div>\n"])
|
||||||
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
|
|
||||||
if imgfile2:
|
|
||||||
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
|
||||||
f.write("</div>\n")
|
|
||||||
if options.iskindle and options.panelview:
|
if options.iskindle and options.panelview:
|
||||||
if options.autoscale:
|
if options.autoscale:
|
||||||
size = (getPanelViewResolution(imgsize, deviceres))
|
size = (getPanelViewResolution(imgsize, deviceres))
|
||||||
@@ -278,7 +267,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
|
|||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
def buildOPF(dstdir, title, filelist, cover=None):
|
||||||
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
||||||
deviceres = options.profileData[1]
|
deviceres = options.profileData[1]
|
||||||
if options.righttoleft:
|
if options.righttoleft:
|
||||||
@@ -325,8 +314,12 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
|||||||
"<item id=\"nav\" href=\"nav.xhtml\" ",
|
"<item id=\"nav\" href=\"nav.xhtml\" ",
|
||||||
"properties=\"nav\" media-type=\"application/xhtml+xml\"/>\n"])
|
"properties=\"nav\" media-type=\"application/xhtml+xml\"/>\n"])
|
||||||
if cover is not None:
|
if cover is not None:
|
||||||
mt = 'image/jpeg'
|
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
|
||||||
f.write("<item id=\"cover\" href=\"Images/cover.jpg" + "\" media-type=\"" + mt +
|
if '.png' == filename[1]:
|
||||||
|
mt = 'image/png'
|
||||||
|
else:
|
||||||
|
mt = 'image/jpeg'
|
||||||
|
f.write("<item id=\"cover\" href=\"Images/cover" + filename[1] + "\" media-type=\"" + mt +
|
||||||
"\" properties=\"cover-image\"/>\n")
|
"\" properties=\"cover-image\"/>\n")
|
||||||
reflist = []
|
reflist = []
|
||||||
for path in filelist:
|
for path in filelist:
|
||||||
@@ -339,16 +332,10 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
|||||||
".xhtml\" media-type=\"application/xhtml+xml\"/>\n")
|
".xhtml\" media-type=\"application/xhtml+xml\"/>\n")
|
||||||
if '.png' == filename[1]:
|
if '.png' == filename[1]:
|
||||||
mt = 'image/png'
|
mt = 'image/png'
|
||||||
elif '.gif' == filename[1]:
|
|
||||||
mt = 'image/gif'
|
|
||||||
else:
|
else:
|
||||||
mt = 'image/jpeg'
|
mt = 'image/jpeg'
|
||||||
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
||||||
mt + "\"/>\n")
|
mt + "\"/>\n")
|
||||||
if 'above' in path[1]:
|
|
||||||
bottom = path[1].replace('above', 'below')
|
|
||||||
uniqueid = uniqueid.replace('above', 'below')
|
|
||||||
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + bottom + "\" media-type=\"" + mt + "\"/>\n")
|
|
||||||
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
|
||||||
|
|
||||||
|
|
||||||
@@ -366,73 +353,68 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
|||||||
else:
|
else:
|
||||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
if originalpath.lower().endswith('.pdf'):
|
|
||||||
if pageside == "right":
|
|
||||||
pageside = "left"
|
|
||||||
else:
|
|
||||||
pageside = "right"
|
|
||||||
if options.spreadshift:
|
if options.spreadshift:
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
|
|
||||||
# initial spread order forwards
|
|
||||||
page_spread_property_list = []
|
|
||||||
for entry in reflist:
|
for entry in reflist:
|
||||||
if options.righttoleft:
|
if options.righttoleft:
|
||||||
if "-kcc-a" in entry or "-kcc-d" in entry:
|
if entry.endswith("-kcc-a"):
|
||||||
page_spread_property_list.append("center")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("center"))
|
||||||
|
)
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
elif "-kcc-b" in entry:
|
elif entry.endswith("-kcc-b"):
|
||||||
page_spread_property_list.append("right")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("right"))
|
||||||
|
)
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
elif "-kcc-c" in entry:
|
elif entry.endswith("-kcc-c"):
|
||||||
page_spread_property_list.append("left")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("left"))
|
||||||
|
)
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
else:
|
else:
|
||||||
page_spread_property_list.append(pageside)
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty(pageside))
|
||||||
|
)
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
else:
|
else:
|
||||||
if "-kcc-a" in entry or "-kcc-d" in entry:
|
if entry.endswith("-kcc-a"):
|
||||||
page_spread_property_list.append("center")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("center"))
|
||||||
|
)
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
elif "-kcc-b" in entry:
|
elif entry.endswith("-kcc-b"):
|
||||||
page_spread_property_list.append("left")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("left"))
|
||||||
|
)
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
elif "-kcc-c" in entry:
|
elif entry.endswith("-kcc-c"):
|
||||||
page_spread_property_list.append("right")
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty("right"))
|
||||||
|
)
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
page_spread_property_list.append(pageside)
|
f.write(
|
||||||
|
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
|
||||||
|
pageSpreadProperty(pageside))
|
||||||
|
)
|
||||||
if pageside == "right":
|
if pageside == "right":
|
||||||
pageside = "left"
|
pageside = "left"
|
||||||
else:
|
else:
|
||||||
pageside = "right"
|
pageside = "right"
|
||||||
|
|
||||||
# fix spread orders backward
|
|
||||||
spread_seen = False
|
|
||||||
for i in range(len(reflist) -1, -1, -1):
|
|
||||||
entry = reflist[i]
|
|
||||||
if "-kcc-x" not in entry:
|
|
||||||
spread_seen = True
|
|
||||||
if options.righttoleft:
|
|
||||||
pageside = "left"
|
|
||||||
else:
|
|
||||||
pageside = "right"
|
|
||||||
elif spread_seen:
|
|
||||||
page_spread_property_list[i] = pageside
|
|
||||||
if pageside == "right":
|
|
||||||
pageside = "left"
|
|
||||||
else:
|
|
||||||
pageside = "right"
|
|
||||||
|
|
||||||
for entry, prop in zip(reflist, page_spread_property_list):
|
|
||||||
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
|
|
||||||
|
|
||||||
f.write("</spine>\n</package>\n")
|
f.write("</spine>\n</package>\n")
|
||||||
f.close()
|
f.close()
|
||||||
os.mkdir(os.path.join(dstdir, 'META-INF'))
|
os.mkdir(os.path.join(dstdir, 'META-INF'))
|
||||||
@@ -445,9 +427,10 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
|||||||
"</container>"])
|
"</container>"])
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, len_tomes=0):
|
def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||||
filelist = []
|
filelist = []
|
||||||
chapterlist = []
|
chapterlist = []
|
||||||
|
cover = None
|
||||||
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
|
||||||
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8')
|
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8')
|
||||||
f.writelines(["@page {\n",
|
f.writelines(["@page {\n",
|
||||||
@@ -457,14 +440,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
|
|||||||
"display: block;\n",
|
"display: block;\n",
|
||||||
"margin: 0;\n",
|
"margin: 0;\n",
|
||||||
"padding: 0;\n",
|
"padding: 0;\n",
|
||||||
"}\n",
|
"}\n"])
|
||||||
])
|
|
||||||
if options.kindle_scribe_azw3:
|
|
||||||
f.writelines([
|
|
||||||
"img {\n",
|
|
||||||
"display: block;\n",
|
|
||||||
"}\n",
|
|
||||||
])
|
|
||||||
if options.iskindle and options.panelview:
|
if options.iskindle and options.panelview:
|
||||||
f.writelines(["#PV {\n",
|
f.writelines(["#PV {\n",
|
||||||
"position: absolute;\n",
|
"position: absolute;\n",
|
||||||
@@ -532,25 +508,22 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
|
|||||||
"}\n"])
|
"}\n"])
|
||||||
f.close()
|
f.close()
|
||||||
build_html_start = perf_counter()
|
build_html_start = perf_counter()
|
||||||
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
|
|
||||||
dot_clean(path)
|
|
||||||
options.covers.append((cover, options.uuid))
|
|
||||||
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||||
chapter = False
|
chapter = False
|
||||||
dirnames, filenames = walkSort(dirnames, filenames)
|
dirnames, filenames = walkSort(dirnames, filenames)
|
||||||
for afile in filenames:
|
for afile in filenames:
|
||||||
if afile == 'cover.jpg':
|
if cover is None:
|
||||||
continue
|
try:
|
||||||
if 'below' in afile:
|
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
|
||||||
continue
|
'cover' + getImageFileName(afile)[1])
|
||||||
|
except Exception as e:
|
||||||
|
raise UserWarning(f"{afile}: {e}")
|
||||||
|
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
|
||||||
|
tomenumber), options.uuid))
|
||||||
if not chapter:
|
if not chapter:
|
||||||
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
|
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
|
||||||
chapter = True
|
chapter = True
|
||||||
if 'above' in afile:
|
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
|
||||||
bottom = afile.replace('above', 'below')
|
|
||||||
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile), bottom))
|
|
||||||
else:
|
|
||||||
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
|
|
||||||
build_html_end = perf_counter()
|
build_html_end = perf_counter()
|
||||||
print(f"buildHTML: {build_html_end - build_html_start} seconds")
|
print(f"buildHTML: {build_html_end - build_html_start} seconds")
|
||||||
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
|
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
|
||||||
@@ -585,36 +558,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
|
|||||||
chapternames[filename] = aChapter[1]
|
chapternames[filename] = aChapter[1]
|
||||||
buildNCX(path, options.title, chapterlist, chapternames)
|
buildNCX(path, options.title, chapterlist, chapternames)
|
||||||
buildNAV(path, options.title, chapterlist, chapternames)
|
buildNAV(path, options.title, chapterlist, chapternames)
|
||||||
buildOPF(path, options.title, filelist, originalpath, cover)
|
buildOPF(path, options.title, filelist, cover)
|
||||||
|
|
||||||
|
|
||||||
def buildPDF(path, title, cover=None, output_file=None):
|
|
||||||
"""
|
|
||||||
Build a PDF file from processed comic images.
|
|
||||||
Images are combined into a single PDF optimized for e-readers.
|
|
||||||
"""
|
|
||||||
start = perf_counter()
|
|
||||||
# open empty PDF
|
|
||||||
with pymupdf.open() as doc:
|
|
||||||
doc.set_metadata({'title': title, 'author': options.authors[0]})
|
|
||||||
# Stream images to PDF
|
|
||||||
for root, dirs, files in os.walk(os.path.join(path, "OEBPS", "Images")):
|
|
||||||
files.sort(key=OS_SORT_KEY)
|
|
||||||
dirs.sort(key=OS_SORT_KEY)
|
|
||||||
for file in files:
|
|
||||||
w, h = Image.open(os.path.join(root, file)).size
|
|
||||||
page = doc.new_page(width=w, height=h)
|
|
||||||
page.insert_image(page.rect, filename=os.path.join(root, file))
|
|
||||||
|
|
||||||
# determine output filename if not provided
|
|
||||||
if output_file is None:
|
|
||||||
output_file = getOutputFilename(path, None, '.pdf', '')
|
|
||||||
|
|
||||||
# Save with optimizations for smaller file size
|
|
||||||
doc.save(output_file, deflate=True, garbage=4, clean=True)
|
|
||||||
end = perf_counter()
|
|
||||||
print(f"MuPDF output: {end-start} sec")
|
|
||||||
return output_file
|
|
||||||
|
|
||||||
|
|
||||||
def imgDirectoryProcessing(path):
|
def imgDirectoryProcessing(path):
|
||||||
@@ -676,168 +620,23 @@ def imgFileProcessing(work):
|
|||||||
workImg = image.ComicPageParser((dirpath, afile), opt)
|
workImg = image.ComicPageParser((dirpath, afile), opt)
|
||||||
for i in workImg.payload:
|
for i in workImg.payload:
|
||||||
img = image.ComicPage(opt, *i)
|
img = image.ComicPage(opt, *i)
|
||||||
is_color = (opt.forcecolor and img.color)
|
|
||||||
if opt.cropping == 2 and not opt.webtoon:
|
if opt.cropping == 2 and not opt.webtoon:
|
||||||
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
||||||
if opt.cropping == 1 and not opt.webtoon:
|
if opt.cropping == 1 and not opt.webtoon:
|
||||||
img.cropMargin(opt.croppingp, opt.croppingm)
|
img.cropMargin(opt.croppingp, opt.croppingm)
|
||||||
if opt.interpanelcrop > 0:
|
if opt.interpanelcrop > 0:
|
||||||
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
|
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
|
||||||
|
|
||||||
img.gammaCorrectImage()
|
|
||||||
|
|
||||||
img.autocontrastImage()
|
img.autocontrastImage()
|
||||||
img.resizeImage()
|
img.resizeImage()
|
||||||
img.optimizeForDisplay(opt.eraserainbow, is_color)
|
img.optimizeForDisplay(opt.reducerainbow)
|
||||||
|
if opt.forcepng and not opt.forcecolor:
|
||||||
if is_color:
|
img.quantizeImage()
|
||||||
pass
|
|
||||||
elif opt.forcepng:
|
|
||||||
img.convertToGrayscale()
|
|
||||||
if opt.format != 'PDF':
|
|
||||||
img.quantizeImage()
|
|
||||||
else:
|
|
||||||
img.convertToGrayscale()
|
|
||||||
output.append(img.saveToDir())
|
output.append(img.saveToDir())
|
||||||
return output
|
return output
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||||
|
|
||||||
|
|
||||||
def render_page(vector):
|
|
||||||
"""Render a page range of a document.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
The PyMuPDF document cannot be part of the argument, because that
|
|
||||||
cannot be pickled. So we are being passed in just its filename.
|
|
||||||
This is no performance issue, because we are a separate process and
|
|
||||||
need to open the document anyway.
|
|
||||||
Any page-specific function can be processed here - rendering is just
|
|
||||||
an example - text extraction might be another.
|
|
||||||
The work must however be self-contained: no inter-process communication
|
|
||||||
or synchronization is possible with this design.
|
|
||||||
Care must also be taken with which parameters are contained in the
|
|
||||||
argument, because it will be passed in via pickling by the Pool class.
|
|
||||||
So any large objects will increase the overall duration.
|
|
||||||
Args:
|
|
||||||
vector: a list containing required parameters.
|
|
||||||
"""
|
|
||||||
# recreate the arguments
|
|
||||||
idx = vector[0] # this is the segment number we have to process
|
|
||||||
cpu = vector[1] # number of CPUs
|
|
||||||
filename = vector[2] # document filename
|
|
||||||
output_dir = vector[3]
|
|
||||||
target_height = vector[4]
|
|
||||||
with pymupdf.open(filename) as doc: # open the document
|
|
||||||
num_pages = doc.page_count # get number of pages
|
|
||||||
|
|
||||||
# pages per segment: make sure that cpu * seg_size >= num_pages!
|
|
||||||
seg_size = int(num_pages / cpu + 1)
|
|
||||||
seg_from = idx * seg_size # our first page number
|
|
||||||
seg_to = min(seg_from + seg_size, num_pages) # last page number
|
|
||||||
|
|
||||||
for i in range(seg_from, seg_to): # work through our page segment
|
|
||||||
page = doc[i]
|
|
||||||
zoom = target_height / page.rect.height
|
|
||||||
mat = pymupdf.Matrix(zoom, zoom)
|
|
||||||
# TODO: decide colorspace earlier so later color check is cheaper.
|
|
||||||
pix = page.get_pixmap(matrix=mat, colorspace='RGB', alpha=False)
|
|
||||||
pix.save(os.path.join(output_dir, "p-%i.png" % i))
|
|
||||||
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_page(vector):
|
|
||||||
"""For pages with single image (and no text). Otherwise it's recommended to use render_page()
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
The PyMuPDF document cannot be part of the argument, because that
|
|
||||||
cannot be pickled. So we are being passed in just its filename.
|
|
||||||
This is no performance issue, because we are a separate process and
|
|
||||||
need to open the document anyway.
|
|
||||||
Any page-specific function can be processed here - rendering is just
|
|
||||||
an example - text extraction might be another.
|
|
||||||
The work must however be self-contained: no inter-process communication
|
|
||||||
or synchronization is possible with this design.
|
|
||||||
Care must also be taken with which parameters are contained in the
|
|
||||||
argument, because it will be passed in via pickling by the Pool class.
|
|
||||||
So any large objects will increase the overall duration.
|
|
||||||
Args:
|
|
||||||
vector: a list containing required parameters.
|
|
||||||
"""
|
|
||||||
# recreate the arguments
|
|
||||||
idx = vector[0] # this is the segment number we have to process
|
|
||||||
cpu = vector[1] # number of CPUs
|
|
||||||
filename = vector[2] # document filename
|
|
||||||
output_dir = vector[3]
|
|
||||||
|
|
||||||
|
|
||||||
with pymupdf.open(filename) as doc: # open the document
|
|
||||||
num_pages = doc.page_count # get number of pages
|
|
||||||
|
|
||||||
# pages per segment: make sure that cpu * seg_size >= num_pages!
|
|
||||||
seg_size = int(num_pages / cpu + 1)
|
|
||||||
seg_from = idx * seg_size # our first page number
|
|
||||||
seg_to = min(seg_from + seg_size, num_pages) # last page number
|
|
||||||
|
|
||||||
for i in range(seg_from, seg_to): # work through our page segment
|
|
||||||
output_path = os.path.join(output_dir, "p-%i.png" % i)
|
|
||||||
page = doc.load_page(i)
|
|
||||||
image_list = page.get_images()
|
|
||||||
if len(image_list) > 1:
|
|
||||||
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
|
|
||||||
if not image_list:
|
|
||||||
width, height = int(page.rect.width), int(page.rect.height)
|
|
||||||
blank_page = Image.new("RGB", (width, height), "white")
|
|
||||||
blank_page.save(output_path)
|
|
||||||
else:
|
|
||||||
xref = image_list[0][0]
|
|
||||||
d = doc.extract_image(xref)
|
|
||||||
if d['cs-name'] == 'DeviceCMYK':
|
|
||||||
pix = pymupdf.Pixmap(doc, xref)
|
|
||||||
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
|
|
||||||
pix.save(output_path)
|
|
||||||
else:
|
|
||||||
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout:
|
|
||||||
imgout.write(d["image"])
|
|
||||||
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
|
|
||||||
render = False
|
|
||||||
with pymupdf.open(filename) as doc:
|
|
||||||
for page in doc:
|
|
||||||
page_text = page.get_text().strip()
|
|
||||||
if page_text != "":
|
|
||||||
render = True
|
|
||||||
break
|
|
||||||
if len(page.get_images()) > 1:
|
|
||||||
render = True
|
|
||||||
break
|
|
||||||
if len(page.get_images()) == 1:
|
|
||||||
image = page.get_images()[0]
|
|
||||||
if not image[5] or image[8] == 'CCITTFaxDecode':
|
|
||||||
render = True
|
|
||||||
break
|
|
||||||
|
|
||||||
cpu = cpu_count()
|
|
||||||
|
|
||||||
# make vectors of arguments for the processes
|
|
||||||
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
|
|
||||||
print("Starting %i processes for '%s'." % (cpu, filename))
|
|
||||||
|
|
||||||
|
|
||||||
start = perf_counter()
|
|
||||||
with Pool() as pool:
|
|
||||||
results = pool.map(
|
|
||||||
render_page if render else extract_page, vectors
|
|
||||||
)
|
|
||||||
end = perf_counter()
|
|
||||||
print(f"MuPDF: {end - start} sec")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getWorkFolder(afile):
|
def getWorkFolder(afile):
|
||||||
if os.path.isdir(afile):
|
if os.path.isdir(afile):
|
||||||
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
|
||||||
@@ -856,26 +655,19 @@ def getWorkFolder(afile):
|
|||||||
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
|
||||||
raise UserWarning("Not enough disk space to perform conversion.")
|
raise UserWarning("Not enough disk space to perform conversion.")
|
||||||
if afile.lower().endswith('.pdf'):
|
if afile.lower().endswith('.pdf'):
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
pdf = pdfjpgextract.PdfJpgExtract(afile)
|
||||||
path = workdir
|
path, njpg = pdf.extract()
|
||||||
|
workdir = path
|
||||||
sanitizePermissions(path)
|
sanitizePermissions(path)
|
||||||
target_height = options.profileData[1][1]
|
if njpg == 0:
|
||||||
if options.cropping == 1:
|
|
||||||
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
|
|
||||||
elif options.cropping == 2:
|
|
||||||
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
|
|
||||||
try:
|
|
||||||
mupdf_pdf_process_pages_parallel(afile, workdir, target_height)
|
|
||||||
except Exception as e:
|
|
||||||
rmtree(path, True)
|
rmtree(path, True)
|
||||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
raise UserWarning("Failed to extract images from PDF file.")
|
||||||
else:
|
else:
|
||||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||||
try:
|
try:
|
||||||
cbx = comicarchive.ComicArchive(afile)
|
cbx = comicarchive.ComicArchive(afile)
|
||||||
path = cbx.extract(workdir)
|
path = cbx.extract(workdir)
|
||||||
sanitizePermissions(path)
|
sanitizePermissions(path)
|
||||||
|
|
||||||
tdir = os.listdir(workdir)
|
tdir = os.listdir(workdir)
|
||||||
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
|
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
|
||||||
tdir.remove('ComicInfo.xml')
|
tdir.remove('ComicInfo.xml')
|
||||||
@@ -886,7 +678,6 @@ def getWorkFolder(afile):
|
|||||||
)
|
)
|
||||||
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
|
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
|
||||||
path = os.path.join(workdir, tdir[0])
|
path = os.path.join(workdir, tdir[0])
|
||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
rmtree(workdir, True)
|
rmtree(workdir, True)
|
||||||
raise UserWarning(e)
|
raise UserWarning(e)
|
||||||
@@ -932,7 +723,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def getMetadata(path, originalpath):
|
def getComicInfo(path, originalpath):
|
||||||
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
||||||
options.comicinfo_chapters = []
|
options.comicinfo_chapters = []
|
||||||
options.summary = ''
|
options.summary = ''
|
||||||
@@ -951,16 +742,13 @@ def getMetadata(path, originalpath):
|
|||||||
else:
|
else:
|
||||||
defaultAuthor = False
|
defaultAuthor = False
|
||||||
options.authors = [options.author]
|
options.authors = [options.author]
|
||||||
|
|
||||||
if os.path.exists(xmlPath):
|
if os.path.exists(xmlPath):
|
||||||
try:
|
try:
|
||||||
xml = metadata.MetadataParser(xmlPath)
|
xml = metadata.MetadataParser(xmlPath)
|
||||||
except Exception:
|
except Exception:
|
||||||
os.remove(xmlPath)
|
os.remove(xmlPath)
|
||||||
return
|
return
|
||||||
if options.metadatatitle:
|
if defaultTitle:
|
||||||
options.title = xml.data['Title']
|
|
||||||
elif defaultTitle:
|
|
||||||
if xml.data['Series']:
|
if xml.data['Series']:
|
||||||
options.title = xml.data['Series']
|
options.title = xml.data['Series']
|
||||||
if xml.data['Volume']:
|
if xml.data['Volume']:
|
||||||
@@ -984,13 +772,6 @@ def getMetadata(path, originalpath):
|
|||||||
options.summary = xml.data['Summary']
|
options.summary = xml.data['Summary']
|
||||||
os.remove(xmlPath)
|
os.remove(xmlPath)
|
||||||
|
|
||||||
if originalpath.lower().endswith('.pdf'):
|
|
||||||
with pymupdf.open(originalpath) as doc:
|
|
||||||
if options.metadatatitle and doc.metadata['title']:
|
|
||||||
options.title = doc.metadata['title']
|
|
||||||
if defaultAuthor and doc.metadata['author']:
|
|
||||||
options.authors = [doc.metadata['author']]
|
|
||||||
|
|
||||||
|
|
||||||
def getDirectorySize(start_path='.'):
|
def getDirectorySize(start_path='.'):
|
||||||
total_size = 0
|
total_size = 0
|
||||||
@@ -1017,51 +798,26 @@ def getPanelViewSize(deviceres, size):
|
|||||||
return str(int(x)), str(int(y))
|
return str(int(x)), str(int(y))
|
||||||
|
|
||||||
|
|
||||||
def removeNonImages(filetree):
|
|
||||||
# clean dot from original file
|
|
||||||
dot_clean(filetree)
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(filetree):
|
|
||||||
for name in files:
|
|
||||||
_, ext = getImageFileName(name)
|
|
||||||
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
|
|
||||||
if os.path.exists(os.path.join(root, name)):
|
|
||||||
os.remove(os.path.join(root, name))
|
|
||||||
# remove empty nested folders
|
|
||||||
for root, dirs, files in os.walk(filetree, False):
|
|
||||||
if not files and not dirs:
|
|
||||||
os.rmdir(root)
|
|
||||||
|
|
||||||
if not os.listdir(Path(filetree).parent):
|
|
||||||
raise UserWarning('No images detected, nested archives are not supported.')
|
|
||||||
|
|
||||||
|
|
||||||
def sanitizeTree(filetree):
|
def sanitizeTree(filetree):
|
||||||
chapterNames = {}
|
chapterNames = {}
|
||||||
page = 1
|
page = 1
|
||||||
cover_path = None
|
|
||||||
for root, dirs, files in os.walk(filetree):
|
for root, dirs, files in os.walk(filetree):
|
||||||
|
dirs.sort(key=OS_SORT_KEY)
|
||||||
files.sort(key=OS_SORT_KEY)
|
files.sort(key=OS_SORT_KEY)
|
||||||
for name in files:
|
for name in files:
|
||||||
_, ext = getImageFileName(name)
|
splitname = os.path.splitext(name)
|
||||||
|
|
||||||
# 9999 page limit
|
# 9999 page limit
|
||||||
unique_name = f'kcc-{page:04}'
|
slugified = f'kcc-{page:04}'
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
newKey = os.path.join(root, unique_name + ext)
|
newKey = os.path.join(root, slugified + splitname[1])
|
||||||
key = os.path.join(root, name)
|
key = os.path.join(root, name)
|
||||||
if key != newKey:
|
if key != newKey:
|
||||||
os.replace(key, newKey)
|
os.replace(key, newKey)
|
||||||
if not cover_path:
|
|
||||||
cover_path = newKey
|
|
||||||
is_natural_sorted = False
|
|
||||||
if os_sorted(dirs) == sorted(dirs):
|
|
||||||
is_natural_sorted = True
|
|
||||||
dirs.sort(key=OS_SORT_KEY)
|
|
||||||
for i, name in enumerate(dirs):
|
for i, name in enumerate(dirs):
|
||||||
tmpName = name
|
tmpName = name
|
||||||
slugified = slugify(name, is_natural_sorted)
|
slugified = slugify(name)
|
||||||
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
|
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
|
||||||
slugified += "A"
|
slugified += "A"
|
||||||
chapterNames[slugified] = tmpName
|
chapterNames[slugified] = tmpName
|
||||||
@@ -1070,7 +826,7 @@ def sanitizeTree(filetree):
|
|||||||
if key != newKey:
|
if key != newKey:
|
||||||
os.replace(key, newKey)
|
os.replace(key, newKey)
|
||||||
dirs[i] = newKey
|
dirs[i] = newKey
|
||||||
return chapterNames, cover_path
|
return chapterNames
|
||||||
|
|
||||||
|
|
||||||
def flattenTree(filetree):
|
def flattenTree(filetree):
|
||||||
@@ -1087,15 +843,22 @@ def sanitizePermissions(filetree):
|
|||||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
||||||
for name in dirs:
|
for name in dirs:
|
||||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
|
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
|
||||||
|
# clean dot from original file
|
||||||
dot_clean(filetree)
|
dot_clean(filetree)
|
||||||
|
|
||||||
|
def dot_clean(filetree):
|
||||||
|
for root, _, files in os.walk(filetree, topdown=False):
|
||||||
|
for name in files:
|
||||||
|
if name.startswith('._'):
|
||||||
|
os.remove(os.path.join(root, name))
|
||||||
|
|
||||||
|
|
||||||
def chunk_directory(path):
|
def chunk_directory(path):
|
||||||
level = -1
|
level = -1
|
||||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||||
for f in files:
|
for f in files:
|
||||||
# Windows MAX_LEN = 260 plus some buffer
|
# Windows MAX_LENGTH = 260 plus some buffer
|
||||||
if os.name == 'nt' and len(os.path.join(root, f)) > 180:
|
if len(os.path.join(root, f)) > 180:
|
||||||
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||||
level = 1
|
level = 1
|
||||||
break
|
break
|
||||||
@@ -1141,12 +904,8 @@ def chunk_process(path, mode, parent):
|
|||||||
if mode < 3:
|
if mode < 3:
|
||||||
for root, dirs, files in walkLevel(path, 0):
|
for root, dirs, files in walkLevel(path, 0):
|
||||||
for name in files if mode == 1 else dirs:
|
for name in files if mode == 1 else dirs:
|
||||||
size = 0
|
|
||||||
if mode == 1:
|
if mode == 1:
|
||||||
if 'below' not in name:
|
size = os.path.getsize(os.path.join(root, name))
|
||||||
size = os.path.getsize(os.path.join(root, name))
|
|
||||||
if 'above' in name:
|
|
||||||
size += os.path.getsize(os.path.join(root, name.replace('above', 'below')))
|
|
||||||
else:
|
else:
|
||||||
size = getDirectorySize(os.path.join(root, name))
|
size = getDirectorySize(os.path.join(root, name))
|
||||||
if currentSize + size > targetSize:
|
if currentSize + size > targetSize:
|
||||||
@@ -1176,7 +935,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
|||||||
for root, _, files in os.walk(tmppath, False):
|
for root, _, files in os.walk(tmppath, False):
|
||||||
for name in files:
|
for name in files:
|
||||||
if getImageFileName(name) is not None:
|
if getImageFileName(name) is not None:
|
||||||
if not alreadyProcessed and '-kcc' in getImageFileName(name)[0]:
|
if not alreadyProcessed and getImageFileName(name)[0].endswith('-kcc'):
|
||||||
alreadyProcessed = True
|
alreadyProcessed = True
|
||||||
path = os.path.join(root, name)
|
path = os.path.join(root, name)
|
||||||
pathOrg = orgpath + path.split('OEBPS' + os.path.sep + 'Images')[1]
|
pathOrg = orgpath + path.split('OEBPS' + os.path.sep + 'Images')[1]
|
||||||
@@ -1186,7 +945,6 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
|||||||
try:
|
try:
|
||||||
img = Image.open(path)
|
img = Image.open(path)
|
||||||
imageNumber += 1
|
imageNumber += 1
|
||||||
# count images smaller than device resolution
|
|
||||||
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
|
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
|
||||||
imageSmaller += 1
|
imageSmaller += 1
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@@ -1201,6 +959,10 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
|||||||
os.remove(os.path.join(root, name))
|
os.remove(os.path.join(root, name))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise RuntimeError(f"{name}: {e}")
|
raise RuntimeError(f"{name}: {e}")
|
||||||
|
# remove empty nested folders
|
||||||
|
for root, dirs, files in os.walk(tmppath, False):
|
||||||
|
if not files and not dirs:
|
||||||
|
os.rmdir(root)
|
||||||
if alreadyProcessed:
|
if alreadyProcessed:
|
||||||
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
|
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
|
||||||
if GUI:
|
if GUI:
|
||||||
@@ -1223,26 +985,21 @@ def createNewTome(parent):
|
|||||||
return tomePath, tomePathRoot
|
return tomePath, tomePathRoot
|
||||||
|
|
||||||
|
|
||||||
def slugify(value, is_natural_sorted):
|
def slugify(value):
|
||||||
if options.format == 'CBZ' and is_natural_sorted:
|
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
|
||||||
return value
|
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
|
||||||
if options.format != 'CBZ':
|
|
||||||
# convert all unicode to ascii via slugify
|
|
||||||
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
|
|
||||||
if not is_natural_sorted:
|
|
||||||
# pad zeros to numbers
|
|
||||||
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def makeZIP(zipfilename, basedir, isepub=False):
|
def makeZIP(zipfilename, basedir, isepub=False):
|
||||||
start = perf_counter()
|
start = perf_counter()
|
||||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||||
if SEVENZIP in available_archive_tools():
|
if '7z' in available_archive_tools():
|
||||||
if isepub:
|
if isepub:
|
||||||
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
|
||||||
mimetypeFile.write('application/epub+zip')
|
mimetypeFile.write('application/epub+zip')
|
||||||
mimetypeFile.close()
|
mimetypeFile.close()
|
||||||
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
|
subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
|
||||||
else:
|
else:
|
||||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||||
if isepub:
|
if isepub:
|
||||||
@@ -1258,6 +1015,7 @@ def makeZIP(zipfilename, basedir, isepub=False):
|
|||||||
print(f"makeZIP time: {end - start} seconds")
|
print(f"makeZIP time: {end - start} seconds")
|
||||||
return zipfilename
|
return zipfilename
|
||||||
|
|
||||||
|
|
||||||
def makeParser():
|
def makeParser():
|
||||||
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
|
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
|
||||||
|
|
||||||
@@ -1290,12 +1048,10 @@ def makeParser():
|
|||||||
help="Output generated file to specified directory or file")
|
help="Output generated file to specified directory or file")
|
||||||
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
|
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||||
help="Comic title [Default=filename or directory name]")
|
help="Comic title [Default=filename or directory name]")
|
||||||
output_options.add_argument("--metadatatitle", action="store_true", dest="metadatatitle", default=False,
|
|
||||||
help="Write Title from ComicInfo.xml or other embedded metadata")
|
|
||||||
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
|
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
|
||||||
help="Author name [Default=KCC]")
|
help="Author name [Default=KCC]")
|
||||||
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
||||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
|
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
|
||||||
"[Default=Auto]")
|
"[Default=Auto]")
|
||||||
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
|
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
|
||||||
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
|
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
|
||||||
@@ -1306,8 +1062,6 @@ def makeParser():
|
|||||||
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("--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("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
|
|
||||||
help="Put rotated 2 page spread first in spread splitter option.")
|
|
||||||
|
|
||||||
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 profil or processing option")
|
help="Do not modify image and ignore any profil or processing option")
|
||||||
@@ -1319,8 +1073,6 @@ def makeParser():
|
|||||||
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
||||||
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
|
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
|
||||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
help="Apply gamma correction to linearize the image [Default=Auto]")
|
||||||
output_options.add_argument("--autolevel", action="store_true", dest="autolevel", default=False,
|
|
||||||
help="Set most common dark pixel value to be black point for leveling.")
|
|
||||||
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
|
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
|
||||||
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
|
||||||
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
|
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
|
||||||
@@ -1337,8 +1089,8 @@ def makeParser():
|
|||||||
help="Disable autodetection and force white borders")
|
help="Disable autodetection and force white borders")
|
||||||
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
|
||||||
help="Don't convert images to grayscale")
|
help="Don't convert images to grayscale")
|
||||||
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
|
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
|
||||||
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
|
help="Reduce rainbow effect on color eink by slightly blurring images.")
|
||||||
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||||
help="Create PNG files instead JPEG")
|
help="Create PNG files instead JPEG")
|
||||||
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
||||||
@@ -1385,8 +1137,6 @@ def checkOptions(options):
|
|||||||
options.format = 'CBZ'
|
options.format = 'CBZ'
|
||||||
elif options.profile in image.ProfileData.ProfilesKindle.keys():
|
elif options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||||
options.format = 'MOBI'
|
options.format = 'MOBI'
|
||||||
elif options.profile in image.ProfileData.ProfilesRemarkable.keys():
|
|
||||||
options.format = 'PDF'
|
|
||||||
else:
|
else:
|
||||||
options.format = 'EPUB'
|
options.format = 'EPUB'
|
||||||
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
if options.profile in image.ProfileData.ProfilesKindle.keys():
|
||||||
@@ -1404,8 +1154,9 @@ def checkOptions(options):
|
|||||||
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K34' or options.profile == 'KDX':
|
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K34' or options.profile == 'KDX':
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
options.hq = False
|
options.hq = False
|
||||||
if not options.hq and not options.autoscale:
|
if options.profile == 'KV' or options.profile in image.ProfileData.ProfilesKindlePDOC.keys():
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
|
options.hq = False
|
||||||
# Webtoon mode mandatory options
|
# Webtoon mode mandatory options
|
||||||
if options.webtoon:
|
if options.webtoon:
|
||||||
options.panelview = False
|
options.panelview = False
|
||||||
@@ -1447,13 +1198,13 @@ def checkTools(source):
|
|||||||
source = source.upper()
|
source = source.upper()
|
||||||
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
|
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
|
||||||
source.endswith('.ZIP') or source.endswith('.CBZ'):
|
source.endswith('.ZIP') or source.endswith('.CBZ'):
|
||||||
if SEVENZIP not in available_archive_tools():
|
if '7z' not in available_archive_tools():
|
||||||
print('ERROR: 7z is missing!')
|
print('ERROR: 7z is missing!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if options.format == 'MOBI':
|
if options.format == 'MOBI':
|
||||||
try:
|
try:
|
||||||
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
|
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
|
||||||
except (FileNotFoundError, CalledProcessError):
|
except FileNotFoundError:
|
||||||
print('ERROR: KindleGen is missing!')
|
print('ERROR: KindleGen is missing!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -1476,40 +1227,6 @@ def checkPre(source):
|
|||||||
raise UserWarning("Target directory is not writable.")
|
raise UserWarning("Target directory is not writable.")
|
||||||
|
|
||||||
|
|
||||||
def makeFusion(sources: List[str]):
|
|
||||||
if len(sources) < 2:
|
|
||||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
|
||||||
start = perf_counter()
|
|
||||||
first_path = Path(sources[0])
|
|
||||||
if first_path.is_file():
|
|
||||||
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
|
|
||||||
else:
|
|
||||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
|
||||||
print("Running Fusion")
|
|
||||||
|
|
||||||
for source in sources:
|
|
||||||
print(f"Processing {source}...")
|
|
||||||
checkPre(source)
|
|
||||||
print("Checking images...")
|
|
||||||
path = getWorkFolder(source)
|
|
||||||
pathfinder = os.path.join(path, "OEBPS", "Images")
|
|
||||||
sanitizeTree(pathfinder)
|
|
||||||
# TODO: remove flattenTree when subchapters are supported
|
|
||||||
flattenTree(pathfinder)
|
|
||||||
source_path = Path(source)
|
|
||||||
if source_path.is_file():
|
|
||||||
os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
|
|
||||||
else:
|
|
||||||
os.renames(pathfinder, fusion_path.joinpath(source_path.name))
|
|
||||||
|
|
||||||
|
|
||||||
end = perf_counter()
|
|
||||||
print(f"makefusion: {end - start} seconds")
|
|
||||||
print("Combined File: "+ str(fusion_path))
|
|
||||||
|
|
||||||
return str(fusion_path)
|
|
||||||
|
|
||||||
|
|
||||||
def makeBook(source, qtgui=None):
|
def makeBook(source, qtgui=None):
|
||||||
start = perf_counter()
|
start = perf_counter()
|
||||||
global GUI
|
global GUI
|
||||||
@@ -1518,17 +1235,13 @@ def makeBook(source, qtgui=None):
|
|||||||
GUI.progressBarTick.emit('1')
|
GUI.progressBarTick.emit('1')
|
||||||
else:
|
else:
|
||||||
checkTools(source)
|
checkTools(source)
|
||||||
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
|
|
||||||
checkPre(source)
|
checkPre(source)
|
||||||
print("Preparing source images...")
|
print("Preparing source images...")
|
||||||
path = getWorkFolder(source)
|
path = getWorkFolder(source)
|
||||||
print("Checking images...")
|
print("Checking images...")
|
||||||
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
|
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
|
||||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
|
||||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||||
cover = image.Cover(cover_path, options)
|
|
||||||
|
|
||||||
if options.webtoon:
|
if options.webtoon:
|
||||||
y = image.ProfileData.Profiles[options.profile][1][1]
|
y = image.ProfileData.Profiles[options.profile][1][1]
|
||||||
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
|
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
|
||||||
@@ -1541,7 +1254,7 @@ def makeBook(source, qtgui=None):
|
|||||||
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
|
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('1')
|
GUI.progressBarTick.emit('1')
|
||||||
if options.batchsplit > 0 or options.targetsize:
|
if options.batchsplit > 0:
|
||||||
tomes = chunk_directory(path)
|
tomes = chunk_directory(path)
|
||||||
else:
|
else:
|
||||||
tomes = [path]
|
tomes = [path]
|
||||||
@@ -1550,8 +1263,6 @@ def makeBook(source, qtgui=None):
|
|||||||
if GUI:
|
if GUI:
|
||||||
if options.format == 'CBZ':
|
if options.format == 'CBZ':
|
||||||
GUI.progressBarTick.emit('Compressing CBZ files')
|
GUI.progressBarTick.emit('Compressing CBZ files')
|
||||||
elif options.format == 'PDF':
|
|
||||||
GUI.progressBarTick.emit('Creating PDF files')
|
|
||||||
else:
|
else:
|
||||||
GUI.progressBarTick.emit('Compressing EPUB files')
|
GUI.progressBarTick.emit('Compressing EPUB files')
|
||||||
GUI.progressBarTick.emit(str(len(tomes) + 1))
|
GUI.progressBarTick.emit(str(len(tomes) + 1))
|
||||||
@@ -1573,31 +1284,21 @@ def makeBook(source, qtgui=None):
|
|||||||
else:
|
else:
|
||||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||||
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
|
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
|
||||||
elif options.format == 'PDF':
|
|
||||||
print("Creating PDF file with PyMuPDF...")
|
|
||||||
# determine output filename based on source and tome count
|
|
||||||
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
|
|
||||||
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
|
|
||||||
# use optimized buildPDF logic with streaming and compression
|
|
||||||
output_pdf = buildPDF(tome, options.title, None, output_file)
|
|
||||||
filepath.append(output_pdf)
|
|
||||||
else:
|
else:
|
||||||
print("Creating EPUB file...")
|
print("Creating EPUB file...")
|
||||||
if len(tomes) > 1:
|
if len(tomes) > 1:
|
||||||
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, len(tomes))
|
buildEPUB(tome, chapterNames, tomeNumber, True)
|
||||||
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
|
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
|
||||||
else:
|
else:
|
||||||
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source)
|
buildEPUB(tome, chapterNames, tomeNumber, False)
|
||||||
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
||||||
makeZIP(tome + '_comic', tome, True)
|
makeZIP(tome + '_comic', tome, True)
|
||||||
# Copy files to final destination (PDF files are already saved directly)
|
copyfile(tome + '_comic.zip', filepath[-1])
|
||||||
if options.format != 'PDF':
|
try:
|
||||||
copyfile(tome + '_comic.zip', filepath[-1])
|
os.remove(tome + '_comic.zip')
|
||||||
try:
|
except FileNotFoundError:
|
||||||
os.remove(tome + '_comic.zip')
|
# newly temporary created file is not found. It might have been already deleted
|
||||||
except FileNotFoundError:
|
pass
|
||||||
# newly temporary created file is not found. It might have been already deleted
|
|
||||||
pass
|
|
||||||
rmtree(tome, True)
|
rmtree(tome, True)
|
||||||
if GUI:
|
if GUI:
|
||||||
GUI.progressBarTick.emit('tick')
|
GUI.progressBarTick.emit('tick')
|
||||||
@@ -1628,15 +1329,10 @@ def makeBook(source, qtgui=None):
|
|||||||
if os.path.isfile(source):
|
if os.path.isfile(source):
|
||||||
os.remove(source)
|
os.remove(source)
|
||||||
elif os.path.isdir(source):
|
elif os.path.isdir(source):
|
||||||
rmtree(source, True)
|
rmtree(source)
|
||||||
|
|
||||||
end = perf_counter()
|
end = perf_counter()
|
||||||
print(f"makeBook: {end - start} seconds")
|
print(f"makeBook: {end - start} seconds")
|
||||||
# Clean up temporary workspace
|
|
||||||
try:
|
|
||||||
rmtree(path, True)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
|
||||||
@@ -1687,9 +1383,7 @@ def makeMOBIWorker(item):
|
|||||||
if kindlegenErrorCode > 0:
|
if kindlegenErrorCode > 0:
|
||||||
break
|
break
|
||||||
if ":I1036: Mobi file built successfully" in line:
|
if ":I1036: Mobi file built successfully" in line:
|
||||||
return [0, '', item]
|
break
|
||||||
if ":I1037: Mobi file built with WARNINGS!" in line:
|
|
||||||
return [0, '', item]
|
|
||||||
# ERROR: KCC unknown generic error
|
# ERROR: KCC unknown generic error
|
||||||
if kindlegenErrorCode == 0:
|
if kindlegenErrorCode == 0:
|
||||||
kindlegenErrorCode = err.returncode
|
kindlegenErrorCode = err.returncode
|
||||||
@@ -1716,4 +1410,3 @@ def makeMOBI(work, qtgui=None):
|
|||||||
makeMOBIWorkerPool.close()
|
makeMOBIWorkerPool.close()
|
||||||
makeMOBIWorkerPool.join()
|
makeMOBIWorkerPool.join()
|
||||||
return makeMOBIWorkerOutput
|
return makeMOBIWorkerOutput
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from argparse import ArgumentParser
|
|||||||
from shutil import rmtree, copytree, move
|
from shutil import rmtree, copytree, move
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
||||||
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
|
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||||
|
|
||||||
|
|
||||||
def mergeDirectoryTick(output):
|
def mergeDirectoryTick(output):
|
||||||
@@ -44,7 +44,6 @@ def mergeDirectory(work):
|
|||||||
imagesValid = []
|
imagesValid = []
|
||||||
sizes = []
|
sizes = []
|
||||||
targetHeight = 0
|
targetHeight = 0
|
||||||
dot_clean(directory)
|
|
||||||
for root, _, files in walkLevel(directory, 0):
|
for root, _, files in walkLevel(directory, 0):
|
||||||
for name in files:
|
for name in files:
|
||||||
if getImageFileName(name) is not None:
|
if getImageFileName(name) is not None:
|
||||||
@@ -254,7 +253,6 @@ def main(argv=None, qtgui=None):
|
|||||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||||
mergeWorkerOutput[0][1])
|
mergeWorkerOutput[0][1])
|
||||||
print("Splitting images...")
|
print("Splitting images...")
|
||||||
dot_clean(targetDir)
|
|
||||||
for root, _, files in os.walk(targetDir, False):
|
for root, _, files in os.walk(targetDir, False):
|
||||||
for name in files:
|
for name in files:
|
||||||
if getImageFileName(name) is not None:
|
if getImageFileName(name) is not None:
|
||||||
@@ -279,7 +277,7 @@ def main(argv=None, qtgui=None):
|
|||||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||||
splitWorkerOutput[0][1])
|
splitWorkerOutput[0][1])
|
||||||
if args.inPlace:
|
if args.inPlace:
|
||||||
rmtree(sourceDir, True)
|
rmtree(sourceDir)
|
||||||
move(targetDir, sourceDir)
|
move(targetDir, sourceDir)
|
||||||
else:
|
else:
|
||||||
rmtree(targetDir, True)
|
rmtree(targetDir, True)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
from functools import cached_property, lru_cache
|
from functools import cached_property
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import distro
|
import distro
|
||||||
@@ -28,7 +28,6 @@ from xml.parsers.expat import ExpatError
|
|||||||
from .shared import subprocess_run
|
from .shared import subprocess_run
|
||||||
|
|
||||||
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
|
||||||
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
|
|
||||||
|
|
||||||
|
|
||||||
class ComicArchive:
|
class ComicArchive:
|
||||||
@@ -40,7 +39,7 @@ class ComicArchive:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def type(self):
|
def type(self):
|
||||||
extraction_commands = [
|
extraction_commands = [
|
||||||
[SEVENZIP, 'l', '-y', '-p1', self.filepath],
|
['7z', 'l', '-y', '-p1', self.filepath],
|
||||||
]
|
]
|
||||||
|
|
||||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||||
@@ -69,12 +68,12 @@ class ComicArchive:
|
|||||||
|
|
||||||
extraction_commands = [
|
extraction_commands = [
|
||||||
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir],
|
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir],
|
||||||
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath],
|
['7z', 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath],
|
||||||
]
|
]
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
extraction_commands.append(
|
extraction_commands.append(
|
||||||
['unar', self.filepath, '-D', '-f', '-o', targetdir]
|
['unar', self.filepath, '-f', '-o', targetdir]
|
||||||
)
|
)
|
||||||
|
|
||||||
extraction_commands.reverse()
|
extraction_commands.reverse()
|
||||||
@@ -101,13 +100,13 @@ class ComicArchive:
|
|||||||
def addFile(self, sourcefile):
|
def addFile(self, sourcefile):
|
||||||
if self.type in ['RAR', 'RAR5']:
|
if self.type in ['RAR', 'RAR5']:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
process = subprocess_run([SEVENZIP, 'a', '-y', self.filepath, sourcefile],
|
process = subprocess_run(['7z', 'a', '-y', self.filepath, sourcefile],
|
||||||
stdout=PIPE, stderr=STDOUT)
|
stdout=PIPE, stderr=STDOUT)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError('Failed to add the file.')
|
raise OSError('Failed to add the file.')
|
||||||
|
|
||||||
def extractMetadata(self):
|
def extractMetadata(self):
|
||||||
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
|
process = subprocess_run(['7z', 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'],
|
||||||
stdout=PIPE, stderr=STDOUT)
|
stdout=PIPE, stderr=STDOUT)
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise OSError(EXTRACTION_ERROR)
|
raise OSError(EXTRACTION_ERROR)
|
||||||
@@ -115,16 +114,3 @@ class ComicArchive:
|
|||||||
return parseString(process.stdout)
|
return parseString(process.stdout)
|
||||||
except ExpatError:
|
except ExpatError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
def available_archive_tools():
|
|
||||||
available = []
|
|
||||||
|
|
||||||
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
|
|
||||||
try:
|
|
||||||
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
|
|
||||||
available.append(tool)
|
|
||||||
except (FileNotFoundError, CalledProcessError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return available
|
|
||||||
|
|||||||
@@ -20,13 +20,9 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import cached_property
|
|
||||||
import mozjpeg_lossless_optimization
|
import mozjpeg_lossless_optimization
|
||||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
|
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -89,14 +85,12 @@ class ProfileData:
|
|||||||
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
||||||
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
||||||
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
||||||
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
'K578': ("Kindle", (600, 800), Palette16, 1.8),
|
||||||
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
||||||
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
|
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfilesKindlePDOC = {
|
ProfilesKindlePDOC = {
|
||||||
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
|
||||||
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
|
||||||
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
||||||
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
||||||
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
||||||
@@ -150,9 +144,11 @@ class ComicPageParser:
|
|||||||
|
|
||||||
# Detect corruption in source image, let caller catch any exceptions triggered.
|
# Detect corruption in source image, let caller catch any exceptions triggered.
|
||||||
srcImgPath = os.path.join(source[0], source[1])
|
srcImgPath = os.path.join(source[0], source[1])
|
||||||
Image.open(srcImgPath).verify()
|
|
||||||
self.image = Image.open(srcImgPath)
|
self.image = Image.open(srcImgPath)
|
||||||
|
self.image.verify()
|
||||||
|
self.image = Image.open(srcImgPath).convert('RGB')
|
||||||
|
|
||||||
|
self.color = self.colorCheck()
|
||||||
self.fill = self.fillCheck()
|
self.fill = self.fillCheck()
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
@@ -183,13 +179,13 @@ class ComicPageParser:
|
|||||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||||
new_image.paste(pageone, (0, 0))
|
new_image.paste(pageone, (0, 0))
|
||||||
new_image.paste(pagetwo, (0, height))
|
new_image.paste(pagetwo, (0, height))
|
||||||
self.payload.append(['N', self.source, new_image, self.fill])
|
self.payload.append(['N', self.source, new_image, self.color, self.fill])
|
||||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
||||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||||
spread = self.image
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
self.payload.append(['R', self.source, spread, self.color, self.fill])
|
||||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||||
if self.opt.splitter != 1:
|
if self.opt.splitter != 1:
|
||||||
if width > height:
|
if width > height:
|
||||||
@@ -204,15 +200,38 @@ class ComicPageParser:
|
|||||||
else:
|
else:
|
||||||
pageone = self.image.crop(leftbox)
|
pageone = self.image.crop(leftbox)
|
||||||
pagetwo = self.image.crop(rightbox)
|
pagetwo = self.image.crop(rightbox)
|
||||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
|
||||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
|
||||||
if self.opt.splitter > 0:
|
if self.opt.splitter > 0:
|
||||||
spread = self.image
|
spread = self.image
|
||||||
if not self.opt.norotate:
|
if not self.opt.norotate:
|
||||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||||
self.payload.append(['R', self.source, spread, self.fill])
|
self.payload.append(['R', self.source, spread,
|
||||||
|
self.color, self.fill])
|
||||||
else:
|
else:
|
||||||
self.payload.append(['N', self.source, self.image, self.fill])
|
self.payload.append(['N', self.source, self.image, self.color, self.fill])
|
||||||
|
|
||||||
|
def colorCheck(self):
|
||||||
|
if self.opt.webtoon:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
img = self.image.copy()
|
||||||
|
bands = img.getbands()
|
||||||
|
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
|
||||||
|
thumb = img.resize((40, 40))
|
||||||
|
SSE, bias = 0, [0, 0, 0]
|
||||||
|
bias = ImageStat.Stat(thumb).mean[:3]
|
||||||
|
bias = [b - sum(bias) / 3 for b in bias]
|
||||||
|
for pixel in thumb.getdata():
|
||||||
|
mu = sum(pixel) / 3
|
||||||
|
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
|
||||||
|
MSE = float(SSE) / (40 * 40)
|
||||||
|
if MSE > 22:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def fillCheck(self):
|
def fillCheck(self):
|
||||||
if self.opt.bordersColor:
|
if self.opt.bordersColor:
|
||||||
@@ -254,139 +273,95 @@ class ComicPageParser:
|
|||||||
|
|
||||||
|
|
||||||
class ComicPage:
|
class ComicPage:
|
||||||
def __init__(self, options, mode, path, image, fill):
|
def __init__(self, options, mode, path, image, color, fill):
|
||||||
self.opt = options
|
self.opt = options
|
||||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||||
if self.opt.hq:
|
if self.opt.hq:
|
||||||
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
||||||
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
|
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
|
||||||
self.original_color_mode = image.mode
|
self.image = image
|
||||||
self.image = image.convert("RGB")
|
self.color = 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])
|
||||||
self.targetPathStart = os.path.join(path[0], os.path.splitext(path[1])[0])
|
|
||||||
if 'N' in mode:
|
if 'N' in mode:
|
||||||
self.targetPathOrder = '-kcc-x'
|
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc'
|
||||||
elif 'R' in mode:
|
elif 'R' in mode:
|
||||||
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
|
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-a'
|
||||||
if not options.norotate:
|
if not options.norotate:
|
||||||
self.rotated = True
|
self.rotated = True
|
||||||
elif 'S1' in mode:
|
elif 'S1' in mode:
|
||||||
self.targetPathOrder = '-kcc-b'
|
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-b'
|
||||||
elif 'S2' in mode:
|
elif 'S2' in mode:
|
||||||
self.targetPathOrder = '-kcc-c'
|
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-c'
|
||||||
# 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
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def color(self):
|
|
||||||
if self.original_color_mode in ("L", "1"):
|
|
||||||
return False
|
|
||||||
img = self.image.convert("YCbCr")
|
|
||||||
_, cb, cr = img.split()
|
|
||||||
|
|
||||||
cb_hist = cb.histogram()
|
|
||||||
cr_hist = cr.histogram()
|
|
||||||
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
|
|
||||||
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
|
|
||||||
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0
|
|
||||||
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0
|
|
||||||
|
|
||||||
SPREAD_THRESHOLD=20
|
|
||||||
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def saveToDir(self):
|
def saveToDir(self):
|
||||||
try:
|
try:
|
||||||
flags = []
|
flags = []
|
||||||
|
if not self.opt.forcecolor and not self.opt.forcepng:
|
||||||
|
self.image = self.image.convert('L')
|
||||||
if self.rotated:
|
if self.rotated:
|
||||||
flags.append('Rotated')
|
flags.append('Rotated')
|
||||||
if self.fill != 'white':
|
if self.fill != 'white':
|
||||||
flags.append('BlackBackground')
|
flags.append('BlackBackground')
|
||||||
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
|
if self.opt.forcepng:
|
||||||
w, h = self.image.size
|
self.image.info["transparency"] = None
|
||||||
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
|
self.targetPath += '.png'
|
||||||
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
|
self.image.save(self.targetPath, 'PNG', optimize=1)
|
||||||
elif self.opt.kindle_scribe_azw3:
|
|
||||||
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
|
|
||||||
else:
|
else:
|
||||||
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
|
self.targetPath += '.jpg'
|
||||||
|
if self.opt.mozjpeg:
|
||||||
|
with io.BytesIO() as output:
|
||||||
|
self.image.save(output, format="JPEG", optimize=1, quality=85)
|
||||||
|
input_jpeg_bytes = output.getvalue()
|
||||||
|
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
||||||
|
with open(self.targetPath, "wb") as output_jpeg_file:
|
||||||
|
output_jpeg_file.write(output_jpeg_bytes)
|
||||||
|
else:
|
||||||
|
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
|
||||||
if os.path.isfile(self.orgPath):
|
if os.path.isfile(self.orgPath):
|
||||||
os.remove(self.orgPath)
|
os.remove(self.orgPath)
|
||||||
return [Path(targetPath).name, flags]
|
return [Path(self.targetPath).name, flags]
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
raise RuntimeError('Cannot save image. ' + str(err))
|
raise RuntimeError('Cannot save image. ' + str(err))
|
||||||
|
|
||||||
def save_with_codec(self, image, targetPath):
|
def autocontrastImage(self):
|
||||||
if self.opt.forcepng:
|
|
||||||
image.info["transparency"] = None
|
|
||||||
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
|
|
||||||
targetPath += '.gif'
|
|
||||||
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
|
||||||
else:
|
|
||||||
targetPath += '.png'
|
|
||||||
image.save(targetPath, 'PNG', optimize=1)
|
|
||||||
else:
|
|
||||||
targetPath += '.jpg'
|
|
||||||
if self.opt.mozjpeg:
|
|
||||||
with io.BytesIO() as output:
|
|
||||||
image.save(output, format="JPEG", optimize=1, quality=85)
|
|
||||||
input_jpeg_bytes = output.getvalue()
|
|
||||||
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
|
||||||
with open(targetPath, "wb") as output_jpeg_file:
|
|
||||||
output_jpeg_file.write(output_jpeg_bytes)
|
|
||||||
else:
|
|
||||||
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
|
||||||
return targetPath
|
|
||||||
|
|
||||||
def gammaCorrectImage(self):
|
|
||||||
gamma = self.opt.gamma
|
gamma = self.opt.gamma
|
||||||
if gamma < 0.1:
|
if gamma < 0.1:
|
||||||
gamma = self.gamma
|
gamma = self.gamma
|
||||||
if self.gamma != 1.0 and self.color:
|
if self.gamma != 1.0 and self.color:
|
||||||
gamma = 1.0
|
gamma = 1.0
|
||||||
if gamma == 1.0:
|
if gamma == 1.0:
|
||||||
pass
|
self.image = ImageOps.autocontrast(self.image)
|
||||||
else:
|
else:
|
||||||
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
|
||||||
|
|
||||||
def autocontrastImage(self):
|
|
||||||
if self.opt.autolevel and not self.color:
|
|
||||||
self.convertToGrayscale()
|
|
||||||
h = self.image.histogram()
|
|
||||||
most_common_dark_pixel_count = max(h[:64])
|
|
||||||
black_point = h.index(most_common_dark_pixel_count)
|
|
||||||
bp = black_point
|
|
||||||
self.image = self.image.point(lambda p: p if p > bp else bp)
|
|
||||||
|
|
||||||
# don't autocontrast grayscale pages that were originally color
|
|
||||||
if not self.opt.forcecolor and self.color:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
|
||||||
|
|
||||||
def convertToGrayscale(self):
|
|
||||||
self.image = self.image.convert('L')
|
|
||||||
|
|
||||||
def quantizeImage(self):
|
def quantizeImage(self):
|
||||||
# remove all color pixels from image, since colorCheck() has some tolerance
|
colors = len(self.palette) // 3
|
||||||
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
|
if colors < 256:
|
||||||
self.image = self.image.convert("RGB")
|
self.palette += self.palette[:3] * (256 - colors)
|
||||||
|
|
||||||
palImg = Image.new('P', (1, 1))
|
palImg = Image.new('P', (1, 1))
|
||||||
palImg.putpalette(self.palette)
|
palImg.putpalette(self.palette)
|
||||||
|
self.image = self.image.convert('L')
|
||||||
|
self.image = self.image.convert('RGB')
|
||||||
|
# Quantize is deprecated but new function call it internally anyway...
|
||||||
self.image = self.image.quantize(palette=palImg)
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
def optimizeForDisplay(self, eraserainbow, is_color):
|
def optimizeForDisplay(self, reducerainbow):
|
||||||
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
|
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
|
||||||
if eraserainbow and all(dim > 1 for dim in self.image.size):
|
if reducerainbow and not self.color:
|
||||||
self.image = erase_rainbow_artifacts(self.image, is_color)
|
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100)
|
||||||
|
self.image = self.image.filter(unsharpFilter)
|
||||||
|
self.image = self.image.filter(ImageFilter.BoxBlur(1.0))
|
||||||
|
self.image = self.image.filter(unsharpFilter)
|
||||||
|
|
||||||
def resizeImage(self):
|
def resizeImage(self):
|
||||||
|
# kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub
|
||||||
|
if self.kindle_scribe_azw3:
|
||||||
|
self.size = (1440, 1920)
|
||||||
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()
|
||||||
@@ -397,9 +372,11 @@ class ComicPage:
|
|||||||
else: # if image bigger than device resolution or smaller with upscaling
|
else: # if image bigger than device resolution or smaller with upscaling
|
||||||
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
if 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') or self.opt.kfx) and not self.opt.white_borders:
|
elif (self.opt.format == 'CBZ' 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:
|
||||||
|
if self.kindle_scribe_azw3:
|
||||||
|
self.size = (1860, 1920)
|
||||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||||
|
|
||||||
def resize_method(self):
|
def resize_method(self):
|
||||||
@@ -423,29 +400,26 @@ class ComicPage:
|
|||||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
w, h = self.image.size
|
|
||||||
left, upper, right, lower = bbox
|
|
||||||
# don't crop more than 10% of image
|
|
||||||
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
|
||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
def cropMargin(self, power, minimum):
|
def cropMargin(self, power, minimum):
|
||||||
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
||||||
|
|
||||||
if bbox:
|
if bbox:
|
||||||
w, h = self.image.size
|
|
||||||
left, upper, right, lower = bbox
|
|
||||||
# don't crop more than 10% of image
|
|
||||||
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
|
|
||||||
self.maybeCrop(bbox, minimum)
|
self.maybeCrop(bbox, minimum)
|
||||||
|
|
||||||
def cropInterPanelEmptySections(self, direction):
|
def cropInterPanelEmptySections(self, direction):
|
||||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||||
|
|
||||||
class Cover:
|
class Cover:
|
||||||
def __init__(self, source, opt):
|
def __init__(self, source, target, opt, tomeid):
|
||||||
self.options = opt
|
self.options = opt
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.target = target
|
||||||
|
if tomeid == 0:
|
||||||
|
self.tomeid = 1
|
||||||
|
else:
|
||||||
|
self.tomeid = tomeid
|
||||||
self.image = Image.open(source)
|
self.image = Image.open(source)
|
||||||
# backwards compatibility for Pillow >9.1.0
|
# backwards compatibility for Pillow >9.1.0
|
||||||
if not hasattr(Image, 'Resampling'):
|
if not hasattr(Image, 'Resampling'):
|
||||||
@@ -457,49 +431,28 @@ class Cover:
|
|||||||
self.image = ImageOps.autocontrast(self.image)
|
self.image = ImageOps.autocontrast(self.image)
|
||||||
if not self.options.forcecolor:
|
if not self.options.forcecolor:
|
||||||
self.image = self.image.convert('L')
|
self.image = self.image.convert('L')
|
||||||
self.crop_main_cover()
|
|
||||||
|
|
||||||
size = list(self.options.profileData[1])
|
|
||||||
if self.options.kindle_scribe_azw3:
|
|
||||||
size[1] = min(size[1], 1920)
|
|
||||||
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
def crop_main_cover(self):
|
|
||||||
w, h = self.image.size
|
w, h = self.image.size
|
||||||
if w / h > 2:
|
if w / h > 2:
|
||||||
if self.options.righttoleft:
|
if self.options.righttoleft:
|
||||||
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
||||||
else:
|
else:
|
||||||
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
||||||
elif w / h > 1.34:
|
elif w / h > 1.3:
|
||||||
if self.options.righttoleft:
|
if self.options.righttoleft:
|
||||||
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
||||||
else:
|
else:
|
||||||
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
||||||
|
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
|
||||||
|
self.save()
|
||||||
|
|
||||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
def save(self):
|
||||||
try:
|
try:
|
||||||
if tomeid == 0:
|
self.image.save(self.target, "JPEG", optimize=1, quality=85)
|
||||||
self.image.save(target, "JPEG", optimize=1, quality=85)
|
|
||||||
else:
|
|
||||||
copy = self.image.copy()
|
|
||||||
draw = ImageDraw.Draw(copy)
|
|
||||||
w, h = copy.size
|
|
||||||
draw.text(
|
|
||||||
xy=(w/2, h * .85),
|
|
||||||
text=f'{tomeid}/{len_tomes}',
|
|
||||||
anchor='ms',
|
|
||||||
font_size=h//7,
|
|
||||||
fill=255,
|
|
||||||
stroke_fill=0,
|
|
||||||
stroke_width=25
|
|
||||||
)
|
|
||||||
copy.save(target, "JPEG", optimize=1, quality=85)
|
|
||||||
except IOError:
|
except IOError:
|
||||||
raise RuntimeError('Failed to save cover.')
|
raise RuntimeError('Failed to save cover.')
|
||||||
|
|
||||||
def saveToKindle(self, kindle, asin):
|
def saveToKindle(self, kindle, asin):
|
||||||
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
self.image = self.image.resize((300, 470), Image.Resampling.LANCZOS)
|
||||||
try:
|
try:
|
||||||
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
||||||
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
|
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
|
||||||
|
|||||||
@@ -123,4 +123,4 @@ class MetadataParser:
|
|||||||
cbx.addFile(tmpXML)
|
cbx.addFile(tmpXML)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise UserWarning(e)
|
raise UserWarning(e)
|
||||||
rmtree(workdir, True)
|
rmtree(workdir)
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
|
|||||||
'''
|
'''
|
||||||
threshold = threshold_from_power(power)
|
threshold = threshold_from_power(power)
|
||||||
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
||||||
ignore_pixels_near_edge(bw_img)
|
|
||||||
bw_bbox = bw_img.getbbox()
|
bw_bbox = bw_img.getbbox()
|
||||||
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
|
||||||
return None
|
return None
|
||||||
@@ -143,25 +142,8 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
|
|||||||
threshold = threshold_from_power(power)
|
threshold = threshold_from_power(power)
|
||||||
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
|
||||||
|
|
||||||
ignore_pixels_near_edge(bw_img)
|
|
||||||
|
|
||||||
return bw_img.getbbox()
|
return bw_img.getbbox()
|
||||||
|
|
||||||
def ignore_pixels_near_edge(bw_img):
|
|
||||||
w, h = bw_img.size
|
|
||||||
edge_bbox = [
|
|
||||||
(0, 0, w, int(0.02 * h)),
|
|
||||||
(0, int(0.98 * h), w, h),
|
|
||||||
(0, 0, int(0.02 * w), h),
|
|
||||||
(int(0.98 * w), 0, w, h)
|
|
||||||
]
|
|
||||||
for box in edge_bbox:
|
|
||||||
edge = bw_img.crop(box)
|
|
||||||
h = edge.histogram()
|
|
||||||
imperfections = h[255] / (edge.height * edge.width)
|
|
||||||
if imperfections > 0 and imperfections < .02:
|
|
||||||
bw_img.paste(im=0, box=box)
|
|
||||||
|
|
||||||
|
|
||||||
def box_intersect(box1, box2, max_dist):
|
def box_intersect(box1, box2, max_dist):
|
||||||
return not (box2[0]-max_dist[0] > box1[1]
|
return not (box2[0]-max_dist[0] > box1[1]
|
||||||
|
|||||||
79
kindlecomicconverter/pdfjpgextract.py
Normal file
79
kindlecomicconverter/pdfjpgextract.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
||||||
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
||||||
|
#
|
||||||
|
# Based upon the code snippet by Ned Batchelder
|
||||||
|
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for
|
||||||
|
# any purpose with or without fee is hereby granted, provided that the
|
||||||
|
# above copyright notice and this permission notice appear in all
|
||||||
|
# copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||||
|
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||||
|
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
||||||
|
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||||
|
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
from random import choice
|
||||||
|
from string import ascii_uppercase, digits
|
||||||
|
|
||||||
|
# skip stray images a few pixels in size in some PDFs
|
||||||
|
# typical images are many thousands in length
|
||||||
|
# https://github.com/ciromattia/kcc/pull/546
|
||||||
|
STRAY_IMAGE_LENGTH_THRESHOLD = 300
|
||||||
|
|
||||||
|
|
||||||
|
class PdfJpgExtract:
|
||||||
|
def __init__(self, fname):
|
||||||
|
self.fname = fname
|
||||||
|
self.filename = os.path.splitext(fname)
|
||||||
|
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
|
||||||
|
|
||||||
|
def getPath(self):
|
||||||
|
return self.path
|
||||||
|
|
||||||
|
def extract(self):
|
||||||
|
pdf = open(self.fname, "rb").read()
|
||||||
|
startmark = b"\xff\xd8"
|
||||||
|
startfix = 0
|
||||||
|
endmark = b"\xff\xd9"
|
||||||
|
endfix = 2
|
||||||
|
i = 0
|
||||||
|
njpg = 0
|
||||||
|
os.makedirs(self.path)
|
||||||
|
while True:
|
||||||
|
istream = pdf.find(b"stream", i)
|
||||||
|
if istream < 0:
|
||||||
|
break
|
||||||
|
istart = pdf.find(startmark, istream, istream + 20)
|
||||||
|
if istart < 0:
|
||||||
|
i = istream + 20
|
||||||
|
continue
|
||||||
|
iend = pdf.find(b"endstream", istart)
|
||||||
|
if iend < 0:
|
||||||
|
raise Exception("Didn't find end of stream!")
|
||||||
|
iend = pdf.find(endmark, iend - 20)
|
||||||
|
if iend < 0:
|
||||||
|
raise Exception("Didn't find end of JPG!")
|
||||||
|
istart += startfix
|
||||||
|
iend += endfix
|
||||||
|
i = iend
|
||||||
|
|
||||||
|
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
|
||||||
|
continue
|
||||||
|
|
||||||
|
jpg = pdf[istart:iend]
|
||||||
|
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
|
||||||
|
jpgfile.write(jpg)
|
||||||
|
jpgfile.close()
|
||||||
|
njpg += 1
|
||||||
|
|
||||||
|
return self.path, njpg
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
def fourier_transform_image(img):
|
|
||||||
"""
|
|
||||||
Memory-optimized version that modifies the array in place when possible.
|
|
||||||
"""
|
|
||||||
# Convert with minimal copy
|
|
||||||
img_array = np.asarray(img, dtype=np.float32)
|
|
||||||
|
|
||||||
# Use rfft2 if the image is real to save memory
|
|
||||||
# and computation time (approximately 2x faster)
|
|
||||||
fft_result = np.fft.rfft2(img_array)
|
|
||||||
|
|
||||||
return fft_result
|
|
||||||
|
|
||||||
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
|
|
||||||
angle_tolerance=10, attenuation_factor=0.10):
|
|
||||||
"""
|
|
||||||
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
|
|
||||||
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
|
|
||||||
target_angle: Target angle in degrees (default: 135)
|
|
||||||
angle_tolerance: Angular tolerance in degrees (default: 15)
|
|
||||||
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
np.ndarray: Modified FFT with applied attenuation (same format as input)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get dimensions of the rfft2 result
|
|
||||||
if fft_spectrum.ndim == 2:
|
|
||||||
height, width_rfft = fft_spectrum.shape
|
|
||||||
else: # 3D array (color channels)
|
|
||||||
height, width_rfft = fft_spectrum.shape[:2]
|
|
||||||
|
|
||||||
# For rfft2, the original width is (width_rfft - 1) * 2
|
|
||||||
width_original = (width_rfft - 1) * 2
|
|
||||||
|
|
||||||
# Create frequency grids for rfft2 format
|
|
||||||
freq_y = np.fft.fftfreq(height, d=1.0)
|
|
||||||
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
|
|
||||||
|
|
||||||
|
|
||||||
# Use broadcasting to create grids without meshgrid (more efficient)
|
|
||||||
freq_y_grid = freq_y.reshape(-1, 1) # Column
|
|
||||||
freq_x_grid = freq_x.reshape(1, -1) # Row
|
|
||||||
|
|
||||||
# Calculate squared radial frequencies (avoid sqrt)
|
|
||||||
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
|
|
||||||
freq_threshold_sq = freq_threshold**2
|
|
||||||
|
|
||||||
# Frequency condition
|
|
||||||
freq_condition = freq_radial_sq >= freq_threshold_sq
|
|
||||||
|
|
||||||
# Early exit if no frequency satisfies the condition
|
|
||||||
if not np.any(freq_condition):
|
|
||||||
return fft_spectrum
|
|
||||||
|
|
||||||
# Calculate angles only where necessary
|
|
||||||
# Use atan2 directly with broadcasting
|
|
||||||
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
|
|
||||||
|
|
||||||
# Convert to degrees and normalize in a single operation
|
|
||||||
angles_deg = np.rad2deg(angles_rad) % 360
|
|
||||||
|
|
||||||
# Calculation of complementary angle
|
|
||||||
target_angle_2 = (target_angle + 180) % 360
|
|
||||||
|
|
||||||
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
|
|
||||||
target_angle_3 = (target_angle + 90) % 360
|
|
||||||
target_angle_4 = (target_angle_3 + 180) % 360
|
|
||||||
|
|
||||||
# Create angular conditions in a vectorized way
|
|
||||||
angle_condition = np.zeros_like(angles_deg, dtype=bool)
|
|
||||||
|
|
||||||
# Process both angles simultaneously
|
|
||||||
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
|
|
||||||
min_angle = (angle - angle_tolerance) % 360
|
|
||||||
max_angle = (angle + angle_tolerance) % 360
|
|
||||||
|
|
||||||
if min_angle > max_angle: # Interval crosses 0°
|
|
||||||
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
|
|
||||||
else: # Normal interval
|
|
||||||
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
|
|
||||||
|
|
||||||
# Combine conditions
|
|
||||||
combined_condition = freq_condition & angle_condition
|
|
||||||
|
|
||||||
# Apply attenuation directly (avoid creating a full mask)
|
|
||||||
if attenuation_factor == 0:
|
|
||||||
# Special case: complete suppression
|
|
||||||
if fft_spectrum.ndim == 2:
|
|
||||||
fft_spectrum[combined_condition] = 0
|
|
||||||
else: # 3D array
|
|
||||||
fft_spectrum[combined_condition, :] = 0
|
|
||||||
return fft_spectrum
|
|
||||||
elif attenuation_factor == 1:
|
|
||||||
# Special case: no attenuation
|
|
||||||
return fft_spectrum
|
|
||||||
else:
|
|
||||||
# General case: partial attenuation
|
|
||||||
if fft_spectrum.ndim == 2:
|
|
||||||
fft_spectrum[combined_condition] *= attenuation_factor
|
|
||||||
else: # 3D array
|
|
||||||
fft_spectrum[combined_condition, :] *= attenuation_factor
|
|
||||||
return fft_spectrum
|
|
||||||
|
|
||||||
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
|
|
||||||
"""
|
|
||||||
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fft_spectrum: Fourier transform result (complex array from rfft2)
|
|
||||||
is_color: Boolean indicating if the image is to be treated as color
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PIL.Image: Reconstructed image
|
|
||||||
"""
|
|
||||||
# Perform inverse Fourier transform with original shape if provided
|
|
||||||
if original_shape is not None:
|
|
||||||
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
|
|
||||||
else:
|
|
||||||
img_reconstructed = np.fft.irfft2(fft_spectrum)
|
|
||||||
|
|
||||||
# Normalize values between 0 and 255
|
|
||||||
img_reconstructed = np.clip(img_reconstructed, 0, 255)
|
|
||||||
img_reconstructed = img_reconstructed.astype(np.uint8)
|
|
||||||
|
|
||||||
# Convert to PIL image
|
|
||||||
if is_color and img_reconstructed.ndim == 3:
|
|
||||||
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
|
|
||||||
else:
|
|
||||||
pil_image = Image.fromarray(img_reconstructed, mode='L')
|
|
||||||
|
|
||||||
return pil_image
|
|
||||||
|
|
||||||
def rgb_to_yuv(rgb_array):
|
|
||||||
"""
|
|
||||||
Convert RGB to YUV color space.
|
|
||||||
Y = luminance, U and V = chrominance
|
|
||||||
"""
|
|
||||||
# Coefficients for RGB to YUV conversion
|
|
||||||
rgb_to_yuv_matrix = np.array([
|
|
||||||
[0.299, 0.587, 0.114], # Y
|
|
||||||
[-0.14713, -0.28886, 0.436], # U
|
|
||||||
[0.615, -0.51499, -0.10001] # V
|
|
||||||
])
|
|
||||||
|
|
||||||
# Reshape for matrix multiplication
|
|
||||||
original_shape = rgb_array.shape
|
|
||||||
rgb_flat = rgb_array.reshape(-1, 3)
|
|
||||||
|
|
||||||
# Apply transformation
|
|
||||||
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
|
|
||||||
|
|
||||||
# Reshape back
|
|
||||||
yuv_array = yuv_flat.reshape(original_shape)
|
|
||||||
|
|
||||||
return yuv_array
|
|
||||||
|
|
||||||
def yuv_to_rgb(yuv_array):
|
|
||||||
"""
|
|
||||||
Convert YUV to RGB color space.
|
|
||||||
"""
|
|
||||||
# Coefficients for YUV to RGB conversion
|
|
||||||
yuv_to_rgb_matrix = np.array([
|
|
||||||
[1.0, 0.0, 1.13983], # R
|
|
||||||
[1.0, -0.39465, -0.58060], # G
|
|
||||||
[1.0, 2.03211, 0.0] # B
|
|
||||||
])
|
|
||||||
|
|
||||||
# Reshape for matrix multiplication
|
|
||||||
original_shape = yuv_array.shape
|
|
||||||
yuv_flat = yuv_array.reshape(-1, 3)
|
|
||||||
|
|
||||||
# Apply transformation
|
|
||||||
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
|
|
||||||
|
|
||||||
# Reshape back
|
|
||||||
rgb_array = rgb_flat.reshape(original_shape)
|
|
||||||
|
|
||||||
return rgb_array
|
|
||||||
|
|
||||||
def erase_rainbow_artifacts(img, is_color):
|
|
||||||
"""
|
|
||||||
Remove rainbow artifacts from grayscale or color images.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
img: PIL Image (grayscale or RGB)
|
|
||||||
is_color: Boolean indicating if the image is to be treated as color
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PIL.Image: Cleaned image
|
|
||||||
"""
|
|
||||||
# Auto-detect color mode if not specified
|
|
||||||
if is_color is None:
|
|
||||||
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
|
|
||||||
|
|
||||||
if is_color and img.mode in ('RGB', 'RGBA'):
|
|
||||||
# Convert to RGB if needed
|
|
||||||
if img.mode == 'RGBA':
|
|
||||||
img = img.convert('RGB')
|
|
||||||
|
|
||||||
# Convert to numpy array
|
|
||||||
img_array = np.array(img, dtype=np.float32)
|
|
||||||
|
|
||||||
# Convert to YUV color space
|
|
||||||
yuv_array = rgb_to_yuv(img_array)
|
|
||||||
|
|
||||||
# Extract luminance channel (Y)
|
|
||||||
luminance = yuv_array[:, :, 0]
|
|
||||||
|
|
||||||
# Process only the luminance channel
|
|
||||||
fft_spectrum = fourier_transform_image(luminance)
|
|
||||||
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
|
||||||
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
|
|
||||||
|
|
||||||
# Normalize and clip luminance
|
|
||||||
clean_luminance = np.clip(clean_luminance, 0, 255)
|
|
||||||
|
|
||||||
# Replace luminance in YUV array
|
|
||||||
yuv_array[:, :, 0] = clean_luminance
|
|
||||||
|
|
||||||
# Convert back to RGB
|
|
||||||
rgb_array = yuv_to_rgb(yuv_array)
|
|
||||||
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
|
|
||||||
|
|
||||||
# Convert back to PIL image
|
|
||||||
clean_image = Image.fromarray(rgb_array, mode='RGB')
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Grayscale processing (original behavior)
|
|
||||||
if img.mode != 'L':
|
|
||||||
img = img.convert('L')
|
|
||||||
|
|
||||||
# Get original image dimensions
|
|
||||||
original_shape = (img.height, img.width)
|
|
||||||
|
|
||||||
fft_spectrum = fourier_transform_image(img)
|
|
||||||
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
|
|
||||||
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
|
|
||||||
|
|
||||||
return clean_image
|
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
# PERFORMANCE OF THIS SOFTWARE.
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
import os
|
import os
|
||||||
|
from hashlib import md5
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
import subprocess
|
import subprocess
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
@@ -45,17 +47,15 @@ class HTMLStripper(HTMLParser):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def dot_clean(filetree):
|
|
||||||
for root, _, files in os.walk(filetree, topdown=False):
|
|
||||||
for name in files:
|
|
||||||
if name.startswith('._') or name == '.DS_Store':
|
|
||||||
if os.path.exists(os.path.join(root, name)):
|
|
||||||
os.remove(os.path.join(root, name))
|
|
||||||
|
|
||||||
|
|
||||||
def getImageFileName(imgfile):
|
def getImageFileName(imgfile):
|
||||||
name, ext = os.path.splitext(imgfile)
|
name, ext = os.path.splitext(imgfile)
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
|
if (name.startswith('.') and len(name) == 1):
|
||||||
|
return None
|
||||||
|
if name.startswith('._'):
|
||||||
|
return None
|
||||||
|
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
|
||||||
|
return None
|
||||||
return [name, ext]
|
return [name, ext]
|
||||||
|
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@ def dependencyCheck(level):
|
|||||||
if level > 2:
|
if level > 2:
|
||||||
try:
|
try:
|
||||||
from PySide6.QtCore import qVersion as qtVersion
|
from PySide6.QtCore import qVersion as qtVersion
|
||||||
if Version('6.0.0') > Version(qtVersion()):
|
if Version('6.5.1') > Version(qtVersion()):
|
||||||
missing.append('PySide 6.0.0')
|
missing.append('PySide 6.5.1+')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('PySide 6.0.0+')
|
missing.append('PySide 6.5.1+')
|
||||||
try:
|
try:
|
||||||
import raven
|
import raven
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -124,20 +124,27 @@ def dependencyCheck(level):
|
|||||||
missing.append('python-slugify 1.2.1+')
|
missing.append('python-slugify 1.2.1+')
|
||||||
try:
|
try:
|
||||||
from PIL import __version__ as pillowVersion
|
from PIL import __version__ as pillowVersion
|
||||||
if Version('8.3.0') > Version(pillowVersion):
|
if Version('5.2.0') > Version(pillowVersion):
|
||||||
missing.append('Pillow 8.3.0+')
|
missing.append('Pillow 5.2.0+')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
missing.append('Pillow 8.3.0+')
|
missing.append('Pillow 5.2.0+')
|
||||||
try:
|
|
||||||
from pymupdf import __version__ as pymupdfVersion
|
|
||||||
if Version('1.16.1') > Version(pymupdfVersion):
|
|
||||||
missing.append('PyMuPDF 1.16.1+')
|
|
||||||
except ImportError:
|
|
||||||
missing.append('PyMuPDF 1.16.1+')
|
|
||||||
if len(missing) > 0:
|
if len(missing) > 0:
|
||||||
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def available_archive_tools():
|
||||||
|
available = []
|
||||||
|
|
||||||
|
for tool in ['tar', '7z', 'unar', 'unrar']:
|
||||||
|
try:
|
||||||
|
subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
available.append(tool)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
def subprocess_run(command, **kwargs):
|
def subprocess_run(command, **kwargs):
|
||||||
if (os.name == 'nt'):
|
if (os.name == 'nt'):
|
||||||
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
|
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
PySide6==6.5.2
|
|
||||||
Pillow>=11.3.0
|
|
||||||
psutil>=5.9.5
|
|
||||||
requests>=2.31.0
|
|
||||||
python-slugify>=1.2.1
|
|
||||||
raven>=6.0.0
|
|
||||||
packaging>=23.2
|
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
|
||||||
natsort>=8.4.0
|
|
||||||
distro>=1.8.0
|
|
||||||
numpy<2
|
|
||||||
PyMuPDF>=1.26.1
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
PySide6==6.1.3
|
|
||||||
Pillow>=9
|
|
||||||
psutil>=5.9.5
|
|
||||||
requests>=2.31.0
|
|
||||||
python-slugify>=1.2.1
|
|
||||||
raven>=6.0.0
|
|
||||||
packaging>=23.2
|
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
|
||||||
natsort>=8.4.0
|
|
||||||
distro>=1.8.0
|
|
||||||
numpy==1.23.0
|
|
||||||
PyMuPDF>=1.16
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
PySide6>=6.5.1
|
PySide6>=6.5.1
|
||||||
Pillow>=11.3.0
|
Pillow>=5.2.0
|
||||||
psutil>=5.9.5
|
psutil>=5.9.5
|
||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
python-slugify>=1.2.1
|
python-slugify>=1.2.1
|
||||||
raven>=6.0.0
|
raven>=6.0.0
|
||||||
packaging>=23.2
|
packaging>=23.2
|
||||||
mozjpeg-lossless-optimization>=1.2.0
|
mozjpeg-lossless-optimization>=1.1.2
|
||||||
natsort>=8.4.0
|
natsort>=8.4.0
|
||||||
distro>=1.8.0
|
distro>=1.8.0
|
||||||
numpy>=1.22.4
|
numpy>=1.22.4,<2.0.0
|
||||||
PyMuPDF>=1.18.0
|
|
||||||
|
|||||||
19
setup.py
19
setup.py
@@ -38,17 +38,10 @@ class BuildBinaryCommand(setuptools.Command):
|
|||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
||||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
|
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
||||||
if min_os:
|
|
||||||
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
|
|
||||||
else:
|
|
||||||
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif sys.platform == 'win32':
|
elif sys.platform == 'win32':
|
||||||
if os.getenv('WINDOWS_7'):
|
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
|
||||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_' + VERSION + ' -w --noupx kcc.py')
|
|
||||||
else:
|
|
||||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif sys.platform == 'linux':
|
elif sys.platform == 'linux':
|
||||||
os.system(
|
os.system(
|
||||||
@@ -81,9 +74,8 @@ setuptools.setup(
|
|||||||
},
|
},
|
||||||
packages=['kindlecomicconverter'],
|
packages=['kindlecomicconverter'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyside6>=6.0.0',
|
'pyside6>=6.5.1',
|
||||||
'Pillow>=9.3.0',
|
'Pillow>=5.2.0',
|
||||||
'PyMuPDF>=1.18.0',
|
|
||||||
'psutil>=5.9.5',
|
'psutil>=5.9.5',
|
||||||
'python-slugify>=1.2.1,<9.0.0',
|
'python-slugify>=1.2.1,<9.0.0',
|
||||||
'raven>=6.0.0',
|
'raven>=6.0.0',
|
||||||
@@ -91,8 +83,7 @@ setuptools.setup(
|
|||||||
'mozjpeg-lossless-optimization>=1.1.2',
|
'mozjpeg-lossless-optimization>=1.1.2',
|
||||||
'natsort>=8.4.0',
|
'natsort>=8.4.0',
|
||||||
'distro',
|
'distro',
|
||||||
'numpy>=1.22.4',
|
'numpy>=1.22.4,<2.0.0'
|
||||||
'PyMuPDF>=1.16.1',
|
|
||||||
],
|
],
|
||||||
classifiers=[],
|
classifiers=[],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user