1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-22 00:48:52 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Xu
46a6103e19 Revert "huge speed optimization on HDD by removing md5 (#845)"
This reverts commit 01625904d1.
2025-03-06 13:54:11 -08:00
30 changed files with 1281 additions and 2940 deletions

15
.github/FUNDING.yml vendored
View File

@@ -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']

View File

@@ -70,5 +70,6 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
*.AppImage*

View File

@@ -89,6 +89,7 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile

View File

@@ -10,47 +10,47 @@ on:
jobs:
build:
strategy:
matrix:
entry: [ kcc-c2e, kcc-c2p ]
include:
- entry: kcc-c2e
capital: KCC_c2e
- entry: kcc-c2p
capital: KCC_c2p
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: 3.11
# cache: 'pip'
# - name: Install python dependencies
# run: |
# python -m pip install --upgrade pip setuptools wheel pyinstaller
# pip install -r requirements.txt
# - name: build binary
# run: |
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./${{ matrix.entry }}.spec
spec: ./kcc.spec
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2e.spec
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2p.spec
- name: rename binaries
run: |
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
mv dist/windows/kcc.exe dist/windows/KCC_${version_built}.exe
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${version_built}.exe
mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
- name: upload build
uses: actions/upload-artifact@v4
with:
name: windows-build-${{ matrix.entry }}
name: windows-build
path: dist/windows/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/windows/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
@@ -58,5 +58,6 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/windows/*.exe

View File

@@ -41,23 +41,11 @@ jobs:
- name: build binary
run: |
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
- name: upload build
uses: actions/upload-artifact@v4
with:
name: windows-build
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
@@ -65,5 +53,6 @@ jobs:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/*.exe
dist/*.exe

1
MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
exclude kindlecomicconverter/sentry.py

123
README.md
View File

@@ -1,49 +1,18 @@
<img src="header.jpg" alt="Header Image" width="400">
# KCC
[![GitHub release](https://img.shields.io/github/release/ciromattia/kcc.svg)](https://github.com/ciromattia/kcc/releases)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ciromattia/kcc/docker-publish.yml?label=docker%20build)](https://github.com/ciromattia/kcc/pkgs/container/kcc)
[![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](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, and CBZ.
If your source are super high resolution DRM-free PDFs from Kodansha/Humble Bundle/Fanatical,
you'll need to first [convert the PDFs to CBZ](https://github.com/ciromattia/kcc/issues/680) for use in KCC.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
Combining that with 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.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small storage capacities.
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:
**Kindle Comic Converter** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ.
It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
It can also optionally optimize images by applying a number of transformations.
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
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
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
@@ -55,29 +24,15 @@ If you have some **technical** problems using KCC please [file an issue here](ht
If you can fix an open issue, fork & make a pull request.
If you find **KCC** valuable you can consider donating to the authors:
- Ciro Mattia Gonano (founder, active 2012-2014):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- Ciro Mattia Gonano (founder, active 2013-2014):
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- [![Donate Flattr](https://img.shields.io/badge/Donate-Flattr-green.svg)](http://flattr.com/thing/2260449/ciromattiakcc-on-GitHub)
- Paweł Jastrzębski (active 2013-2019):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
[![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
- [![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
- Alex Xu (active 2023-Present)
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=QFJVE7A6LCP6U&no_recurring=0&item_name=Kindle+Comic+Converter&currency_code=USD)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q41BW8HS)
## Commissions
This section is subject to change:
Email (for commisions and inquiries): `kindle.comic.converter` gmail
## Sponsors
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
## DOWNLOADS
@@ -99,31 +54,9 @@ 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
## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output in Calibre will break the formatting.
Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.16.3+, you can get cover thumbnails simply by USB dropping into documents folder.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- 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
- 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.
- 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)
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
- 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
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
- 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.
## PREREQUISITES
@@ -173,12 +106,10 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
'K578': ("Kindle", (600, 800), Palette16, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), 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),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
@@ -230,12 +161,10 @@ PROCESSING:
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
-g GAMMA, --gamma GAMMA
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
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
--cp CROPPINGP, --croppingpower CROPPINGP
Set cropping power [Default=1.0]
--preservemargin After calculating crop, "back up" a specified percentage amount [Default=0]
--cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0]
--ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP
@@ -253,7 +182,6 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
--comicinfotitle Write title from ComicInfo.xml
-a AUTHOR, --author AUTHOR
Author name [Default=KCC]
-f FORMAT, --format FORMAT
@@ -263,8 +191,7 @@ OUTPUT SETTINGS:
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
--norotate Do not rotate double page spreads in spread splitter option.
--rotatefirst Put rotated spread first in spread splitter option.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE:
--customwidth CUSTOMWIDTH
@@ -300,25 +227,17 @@ OTHER:
This section is for developers who want to contribute to KCC or power users who want to run the latest code without waiting for an official release.
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone your fork of the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
Depending on your system [Python](https://www.python.org) may be called either `python` or `python3`. We use virtual environments (venv) to manage dependencies.
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
If you want to edit the `.ui` files, use [Qt Creator](https://www.qt.io/download-qt-installer-oss), included in **Qt for desktop development**.
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
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
Do not use `git merge` to merge master from upstream,
use the "Sync fork" button on your fork on GitHub in your branch
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
@@ -345,8 +264,6 @@ python setup.py build_binary
### 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:
```
python3 -m venv venv
@@ -385,12 +302,6 @@ The app relies and includes the following scripts:
- Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License.
## SAMPLE FILES CREATED BY KCC
https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples
Older links (dead):
* [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi)
* [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
* [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)

View File

@@ -4,7 +4,7 @@ channels:
- defaults
dependencies:
- python=3.11
- Pillow>=11.3.0
- Pillow>=5.2.0
- psutil>=5.9.5
- python-slugify>=1.2.1
- raven>=6.0.0

View File

@@ -27,6 +27,5 @@
<file>../icons/convert.png</file>
<file>../icons/document_new.png</file>
<file>../icons/folder_new.png</file>
<file>../icons/kofi_symbol.png</file>
</qresource>
</RCC>

1075
gui/KCC.ui

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2e.py'],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc-c2p.py'],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

View File

@@ -11,7 +11,7 @@ a = Analysis(['kcc.py'],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

View File

@@ -16,9 +16,6 @@
# 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 itertools
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
@@ -30,7 +27,7 @@ import sys
from urllib.parse import unquote
from time import sleep
from shutil import move, rmtree
from subprocess import STDOUT, PIPE, CalledProcessError
from subprocess import STDOUT, PIPE
import requests
from xml.sax.saxutils import escape
@@ -40,8 +37,7 @@ from packaging.version import Version
from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
from . import __version__
from . import comic2ebook
from . import metadata
@@ -123,8 +119,6 @@ class Icons:
self.CBZFormat.addPixmap(QPixmap(":/Formats/icons/CBZ.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon()
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off)
self.info = QIcon()
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
@@ -247,12 +241,9 @@ class WorkerThread(QThread):
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
options.cropping = GUI.croppingBox.checkState().value
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value()
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True
@@ -262,26 +253,18 @@ class WorkerThread(QThread):
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.reduceRainbowBox.isChecked():
options.reducerainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.comicinfoTitleBox.isChecked():
options.comicinfotitle = True
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
@@ -293,27 +276,12 @@ class WorkerThread(QThread):
options.output = GUI.targetDirectory
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text()))
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:
sleep(0.5)
if not self.conversionAlive:
@@ -450,8 +418,6 @@ class WorkerThread(QThread):
MW.addMessage.emit('Created EPUB file was too big.', 'error', False)
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
False)
if self.kindlegenErrorCode[0] == 3221226505:
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
else:
for item in outputPath:
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
@@ -459,12 +425,6 @@ class WorkerThread(QThread):
move(item, GUI.targetDirectory)
except Exception:
pass
if options.filefusion:
for path in currentJobs:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path)
GUI.progress.content = ''
GUI.progress.stop()
MW.hideProgressBar.emit()
@@ -494,33 +454,17 @@ class SystemTrayIcon(QSystemTrayIcon):
class KCCGUI(KCC_ui.Ui_mainWindow):
def selectDefaultOutputFolder(self):
dname = QFileDialog.getExistingDirectory(MW, 'Select default output folder', self.defaultOutputFolder)
if self.is_directory_on_kindle(dname):
return
def selectDir(self):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
GUI.defaultOutputFolder = dname
def is_directory_on_kindle(self, dname):
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
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom()
def selectFile(self):
if self.needClean:
@@ -582,10 +526,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass
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):
if mode == 1:
self.currentMode = 1
@@ -608,7 +548,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.setEnabled(status)
GUI.wikiButton.setEnabled(status)
GUI.deviceBox.setEnabled(status)
GUI.defaultOutputFolderButton.setEnabled(status)
GUI.directoryButton.setEnabled(status)
GUI.clearButton.setEnabled(status)
GUI.fileButton.setEnabled(status)
GUI.formatBox.setEnabled(status)
@@ -663,8 +603,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.rotateBox.setChecked(False)
GUI.upscaleBox.setEnabled(False)
GUI.upscaleBox.setChecked(True)
GUI.chunkSizeCheckBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False)
else:
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['PVOptions']:
@@ -672,23 +610,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.mangaBox.setEnabled(True)
GUI.rotateBox.setEnabled(True)
GUI.upscaleBox.setEnabled(True)
GUI.chunkSizeCheckBox.setEnabled(True)
def togglequalityBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if value == 2:
if profile['Label'] not in ('K57', 'KPW', 'K810') :
if profile['Label'] in ['KV', 'KO']:
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('Use the Kindle Scribe profile if you want higher resolution when zooming.', 'warning')
self.addMessage('On this device, quality improvement will be negligible.', 'warning')
GUI.upscaleBox.setEnabled(False)
GUI.upscaleBox.setChecked(True)
else:
GUI.upscaleBox.setEnabled(True)
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
def changeGamma(self, value):
valueRaw = int(5 * round(float(value) / 5))
@@ -722,10 +655,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if not GUI.webtoonBox.isChecked():
GUI.qualityBox.setEnabled(profile['PVOptions'])
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
if profile['Label'] == 'KS':
GUI.upscaleBox.setDisabled(True)
else:
GUI.upscaleBox.setEnabled(True)
GUI.mangaBox.setChecked(True)
if not profile['PVOptions']:
GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other':
@@ -745,12 +675,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
else:
GUI.outputSplit.setEnabled(False)
GUI.outputSplit.setChecked(False)
if (GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'EPUB-200MB' or
GUI.formats[str(GUI.formatBox.currentText())]['format'] == 'MOBI+EPUB-200MB'):
GUI.chunkSizeCheckBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
def stripTags(self, html):
s = HTMLStripper()
@@ -805,10 +729,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.worker.sync()
else:
if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ShiftModifier:
if not self.selectOutputFolder():
return
elif GUI.defaultOutputFolderBox.isChecked():
self.targetDirectory = self.defaultOutputFolder
dname = QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath)
if dname != '':
if sys.platform.startswith('win'):
dname = dname.replace('/', '\\')
GUI.targetDirectory = dname
else:
GUI.targetDirectory = ''
else:
GUI.targetDirectory = ''
self.progress.start()
@@ -819,12 +746,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('No files selected! Please choose files to convert.', 'error')
self.needClean = True
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):
GUI.jobList.clear()
self.addMessage('Target resolution is not set!', 'error')
@@ -856,7 +777,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
event.ignore()
self.settings.setValue('settingsVersion', __version__)
self.settings.setValue('lastPath', self.lastPath)
self.settings.setValue('defaultOutputFolder', self.defaultOutputFolder)
self.settings.setValue('lastDevice', GUI.deviceBox.currentIndex())
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
self.settings.setValue('startNumber', self.startNumber + 1)
@@ -865,32 +785,24 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'rotateBox': GUI.rotateBox.checkState().value,
'qualityBox': GUI.qualityBox.checkState().value,
'gammaBox': GUI.gammaBox.checkState().value,
'autoLevelBox': GUI.autoLevelBox.checkState().value,
'croppingBox': GUI.croppingBox.checkState().value,
'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(),
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
'upscaleBox': GUI.upscaleBox.checkState().value,
'borderBox': GUI.borderBox.checkState().value,
'webtoonBox': GUI.webtoonBox.checkState().value,
'outputSplit': GUI.outputSplit.checkState().value,
'colorBox': GUI.colorBox.checkState().value,
'eraseRainbowBox': GUI.eraseRainbowBox.checkState().value,
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value,
'mozJpegBox': GUI.mozJpegBox.checkState().value,
'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState().value,
'spreadShiftBox': GUI.spreadShiftBox.checkState().value,
'fileFusionBox': GUI.fileFusionBox.checkState().value,
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value,
'noRotateBox': GUI.noRotateBox.checkState().value,
'rotateFirstBox': GUI.rotateFirstBox.checkState().value,
'maximizeStrips': GUI.maximizeStrips.checkState().value,
'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value,
'chunkSizeBox': GUI.chunkSizeBox.value()})
'gammaSlider': float(self.gammaValue) * 100})
self.settings.sync()
self.tray.hide()
@@ -942,7 +854,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
except Exception:
pass
try:
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
self.kindleGen = True
for line in versionCheck.stdout.splitlines():
if 'Amazon kindlegen' in line:
@@ -951,7 +863,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('Your <a href="https://www.amazon.com/b?node=23496309011">KindleGen</a>'
' is outdated! MOBI conversion might fail.', 'warning')
break
except (FileNotFoundError, CalledProcessError):
except FileNotFoundError:
self.kindleGen = False
if startup:
self.display_kindlegen_missing()
@@ -967,9 +879,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings = QSettings('ciromattia', 'kcc')
self.settingsVersion = self.settings.value('settingsVersion', '', 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.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int)
@@ -998,7 +907,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.windowSize == '0x0':
MW.resize(500, 500)
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']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
@@ -1011,20 +920,17 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"EPUB (Calibre KFX)": {'icon': 'EPUB', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
"MOBI + EPUB (200MB limit)": {'icon': 'MOBI', 'format': 'MOBI+EPUB-200MB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}
}
self.profiles = {
"Kindle Oasis 9/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO'},
"Kindle 8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K810'},
"Kindle Oasis 8": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Scribe": {
@@ -1033,21 +939,21 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 11": {
'PVOptions': True, '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',
},
"Kindle Paperwhite 12": {
"Kindle PW 12": {
'PVOptions': True, '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',
},
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Paperwhite 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
"Kindle PW 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle PW 5/6": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KPW'},
"Kindle 4/5/7": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K57'},
"Kindle 4/5/7/8/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'K578'},
"Kindle DX": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2,
'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KDX'},
"Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1,
@@ -1102,10 +1008,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'OTHER'},
}
profilesGUI = [
"Kindle Colorsoft",
"Kindle Paperwhite 12",
"Kindle CS 12",
"Kindle PW 12",
"Kindle Scribe",
"Kindle Paperwhite 11",
"Kindle PW 11",
"Kindle 11",
"Kindle Oasis 9/10",
"Separator",
@@ -1123,12 +1029,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Other",
"Separator",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle Paperwhite 7/10",
"Kindle PW 7/10",
"Kindle Voyage",
"Kindle Paperwhite 5/6",
"Kindle 4/5/7",
"Kindle PW 5/6",
"Kindle 4/5/7/8/10",
"Kindle Touch",
"Kindle Keyboard",
"Kindle DX",
@@ -1147,44 +1052,34 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kobo Mini/Touch",
]
link_dict = {
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj",
'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 = QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
'NATE</a> - <a href="http://www.mobileread.com/forums/showthread.php?t=207461'
'">FORUM</a></b>')
statusBarLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', '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:
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>.',
'info')
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]):
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
self.detectKindleGen(True)
APP.messageFromOtherInstance.connect(self.handleMessage)
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
GUI.directoryButton.clicked.connect(self.selectDir)
GUI.clearButton.clicked.connect(self.clearJobs)
GUI.fileButton.clicked.connect(self.selectFile)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi)
GUI.convertButton.clicked.connect(self.convertStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
@@ -1192,7 +1087,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat)
MW.progressBarTick.connect(self.updateProgressbar)
@@ -1245,9 +1139,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if GUI.croppingPowerSlider.isEnabled():
GUI.croppingPowerSlider.setValue(int(self.options[option]))
self.changeCroppingPower(int(self.options[option]))
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
elif str(option) == "chunkSizeBox":
GUI.chunkSizeBox.setValue(int(self.options[option]))
else:
try:
if getattr(GUI, option).isEnabled():

View File

@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.9.1
# Created by: The Resource Compiler for Qt version 6.8.1
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -6062,588 +6062,6 @@ $\xb8I\x00B\xd9\xcb $]\xa6\x90qE\xb4{\
\x8a\xf6\x7f5\x09`\xd3%\x01\xf9'\xc1\xcd\xfa\x01\x0f\
\x02L\xdb\x8e|\xe3\xd9\x00\x00\x00\x00IEND\xae\
B`\x82\
\x00\x00$=\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x01A\x00\x00\x01\x02\x08\x06\x00\x00\x00`\xc2e\xf3\
\x00\x00\x00\x09pHYs\x00\x00,K\x00\x00,K\
\x01\xa5=\x96\xa9\x00\x00\x00\x01sRGB\x00\xae\xce\
\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfc\
a\x05\x00\x00#\xd2IDATx\x01\xed\x9dOl\
U\xe7\x99\xc6_P6\xd3\x11!\x95\x92\x15iq\x16\
\xb30\xa9\x94D\x8d\x94(\x15\x89S\xc9\xa8\x15\x8bB\
\x89\x94d\xd1\x09Fj\xe8f\x0a\x8c\xc2bfj\xd5\
\x94\xa8]\x18M\xec\xaeJ*\x81\x99\x8c\x14\x22AC\
\x16\xa8\x11V\x1b\x13\xd4\xaaH\xa9\xea\xa8-,s\x99\
\x94U*\x15\x075\xb3\xa4\xe7\xb9\xf7|\xf8\xf8\xe6\xfe\
\xbf\xdf\xfb~\x7f\xce\xf3\x93\x0e\xf7\xfa\xda\xd8\xbe\xbe\xf7\
<\xe7y\xff|\xef\xb7IH\x14\xdc\xb9sg\xa2\xf2\
\xa1\xbb\x7f_yH\x97\x8f\xb7w\xf8V\xed_\xd3\xef\
\xf1n\x0c\xfb\xf5!h\xc8\xf0\xdc*\x8fa\xbe\xe7Z\
\xdb\xffi\xff\x1e\x8d\x0e\xf7om\xda\xb4\xa9\xd7\xcf!\
\x91\xb0I\x88\x17\x0a\x11s\xa2\x81c\xa2r\xdf\x1d[\
\xdb>/\x92\x86\xd0\x90\xf1i\xc8F\xe1\xc4\xc7NX\
\xab\x9fk~L\xf1\xb4\x85\x22\xd8\x87\x8a\xb8=*\xeb\
\x02\xe6\x1c\x98{\x8cbF|\xd3\x90\x8a0\x16\xc7\x0d\
\xd9(\x9a\xab\x14K?P\x04K\x0a\xb1\x83\xa0\xe1\x98\
\x90\x96\xc8\xb9\xfb\x147\x12+N\x14q|X\x1c\xab\
\xd2r\x92\xabB\x06\xa6\x96\x22X\xe6\xdf\xa6\xa4%t\
\xcf\x94\xb7\x84\xe4\xc4jy@\x1cW(\x8c\xdd\xa9\x85\
\x08\x96!\xed\x1ei\x09\x1en\xe9\xeeH\xddh\x86\xd0\
\xc5qYZ\xa2\xb8\x22\xa4I\xb6\x22X\xba=\x08\xde\
\xb7\xa4\xe5\xfa\x08!\xeb8Q<#-QlHM\
\xc9J\x04K\xc7\xb7_(|\x84\x0cK\xa38\xde)\
\x8e\x0bus\x89Y\x88`!~S\xc5\xcd\x0f\x85\xc2\
G\x88\x0f\x1a\xc5\xb1R\x1c\x8bu\xc8%&+\x82\x15\
\xd7\xf7\x92\xb0\xb0A\x88\x16\x10\xc1E\xc98dNN\
\x04K\xf1;T\x1c\x87\x85\x05\x0eB,Y*\x8e3\
\xb9\x85\xcb\xc9\x88 \xc5\x8f\x90hh\xba\xc3B\x0c\x97\
$\x03\xa2\x17A\x8a\x1f!\xd1\xd2(\x8ec\xa9\x8ba\
\xd4\x22X\x16<N\xcb\xfaZ[BH|4$a\
1\x8cR\x04\xcb\x1e?\x88\xdf\x94\x10BR\xa1!\x09\
\x8a\xe1f\x89\x8cB\x00\x11\xf6\xfeA(\x80\x84\xa4\xc6\
Dq\x9c.\xce\xe1\x8f\x8ac\xbf$B4N\xb0t\
\x7fo\x0b\xdb]\x08\xc9\x85\x0b\xc5q$\xf6\xd6\x9a(\
\x9c`\xc5\xfdQ\x00\x09\xc9\x07,[\x85+\xfc\xa1D\
LP'XV~\x91\xfb\xdb#\x84\x90\x9ci\x14\xc7\
L\x8c=\x86\xc1\x9c`9\xbf\x0f\xee\x8f\x02HH\xfe\
L\x14\xc7{\xc5y\xffZi~\xa2!\x88\x13,\xfe\
\x08X\xea\xb6 \xec\xfb\x1b\x88\xbf\xfc\xe5/]?\xfe\
\xf4\xd3O\x9bG\xbf\xffS\xe5\xf6\xed\xdb\xb2\xb6\xb6&\
\xe3\x80\xef\xd1\xe9\xe7\xfa\xe0\xde{\xef\x95-[\xb6\x88\
/\x1e|\xf0\xc1\x91\xbe\x06\xbf\x07\x8en\x8fu\xfa<\
\x19\x88Fq<\x1bK\xae\xd0\x5c\x04\xcb\xfc\xc0\x9c\xd4\
\x00'D\xb8ub\xe5\x8e\xaa\x10\xdd\xbcys\xc3\xd7\
\xb7\xdf'\xf1S\x15D'\xa8N\xcc\xb7n\xdd\xda\xbc\
u_\xe3>\x8f\xdb\x9a\x0b\xe9\x5c!\x84\xc7$0\xa6\
\x22X\x08 \xf2\x7f\xfb%a `\x10('l\xee\
\xbesFU\xc1#dP \x88N\x1c\xb7m\xdbv\
W \xab\xb7\x838\xda\x04\x09^A6\x11\xc12\x07\
\x80\xf6\x97)I\x00\x08\xd9\xb5k\xd7\x9a\xb7pi\xee\
c\x8a\x1b\x09\x8d\x13\xc3\x1d;v4\xc5\x12\xb78\x12\
w\x93\x0d\x09\x18\x1e\xab\x8b`)\x80\xefI\xc4\xed/\
\xbf\xfb\xdd\xef\x9a\x22w\xf5\xea\xd5\xe6}\x0a\x1dI\x0d\
\x88 \xc4\xf0\x89'\x9eh\xde>\xf9\xe4\x93\xa9\x09#\
&]\xa3z|A\x8cQ\x15\xc1X\x05\x10\x22w\xee\
\xdc9Y^^\xbe\xeb\xf0\x08\xc9\x0d\xe7\x12\xa7\xa7\xa7\
S\x12E\xf3<\xa1\x9a\x08\xc6&\x80U\xe1\x83\xdb#\
\xa4n@\x08\x9d B\x1c#\xc6T\x08UD0&\
\x01\x84\xe0\x9d:u\xaa\x19\xea\xd2\xf1\x11\xd2\x02y\xc5\
]\xbbv\xc9\xcc\xccL\xac\x05\x97\x85B\x08\x8f\x88\x01\
Z\x22\x88\x22H\xd0&h\x88\xdf\xe2\xe2\x22]\x1f!\
}\x803\xdc\xb7o_S\x14#\x0b\x99\x97\x0a!\x9c\
\x11e\xbc\x8b`\xe8>@\x84\xbc\x10?\xf6\xd9\x112\
\x1c\x10@\x08\xe1\xa1C\x87br\x87\xeaB\xe8U\x04\
C\x0a \x9d\x1f!\xfe\x80;\x84\x18\xe26\x02T\x85\
\xd0\x9b\x08\x16\x02\x88\xf0\xf7m1\x06\x8e\xef\xe8\xd1\xa3\
\x14?B\x14p\xa1\xf2s\xcf='\x81Q\x13B/\
\x22X\xce\x02D!dB\x0cA\xc1\x03\xee\x8f\x05\x0f\
Bt\x81\x18\xce\xce\xce\x86\xae*\xabT\x8d}\x89\xe0\
Gb(\x80t\x7f\x84\x84\x01\x8e0p\xce\xd0\xbb\x10\
\x8e-\x82\xd6y@\x14>\x8e\x1f?N\xf7GH@\
\x0e\x1f>\xdc\x14\xc3@\xec/\x84\xf0\x8cxb,\x11\
,\xf7\x118-F@\xfc\x10\x02\x13B\xc2\x037\xf8\
\xe6\x9bo\x86p\x85Xb\x87\xb5\xc6\xab\xe2\x81\x91E\
\xd02\x0f\x88\xf0\xf7\xe0\xc1\x83\xcd%n\x84\x90\xb8\x08\
\xe4\x0a\x1b\xc5\xf1X!\x84\xb7dL\xc6\x11\xc1\xa5\xe2\
\xe6%Q\x06\x02\xf8\xe2\x8b/\xb2\xef\x8f\x90\x88\x09\xe4\
\x0aW\x0a\x11|V\xc6d\xa4\xf1\xfae\x18\xac.\x80\
p~\x14@B\xe2\x07\xe7\xe8\xee\xdd\xbb\x9b9{C\
\xa6|l\xe24\xb4\x13,\xd7\x05co\x90\x09Q\xc4\
\x09 \x0b \x84\xa4E\x80\xf0\xf8\xd9q6p\x1a\xc5\
\x09\xe2\xd9M\x88\x22\x14@B\xd2eaa\xa1\x99\xc3\
7<\x7fO\x8f\xb3y\xd3PN\xb0,\x86|$\x8a\
0\x07HH\x1e\x18\xe7\x09G\x9e:3\xac\x13\x9c\x13\
E(\x80\x84\xe4\x83\xf1\xf9|\xb80iS2\x02\x03\
;\xc1\xf2\x07\xbc'J\xc0:#\xb1J\x01\x5c\xdf\xad\
\xcc\xedPV}\x0c\xb4\xefP\xd6m\xc7\xb2^W`\
\x9fWg\x1f\xdfK\xebu\x1fd_\x98A\xb7-m\
\xff\xba\xf6\xadO\xddf[L\xe3l\xc4\xd0\x116d\
\x84\xb6\x99aD\x10\x028%J`\x19\x9cqeI\
\x1d'N\xd5\xdd\xc3\xbam\xb9\xc8=l\xf3\xa3\xba\xe5\
\xaa\xbb\xadn\xbbZ\xfd\x18\x02\x9a\xb3\x010\x14\xc2c\
\x85\x08\xce\x0d\xf3\x1f\x06\x12A\xed\x95!H\xa4b\x10\
Bj\xe0\x05\x85[\xc3\xa2r\xb7\x0b\x98;(jd\
\x14\xaa\xe2\xe8v<t\xfb\xe0\x5c\xbf~]R\xc6P\
\x08\x1f\x1af\xe7\xbaAE\x10-1*\xa3\xf2\xf1\x22\
\xef\xdc\xb9Sb\x06b699yw\xe3\x1a'z\
\x149b\x89\x13\xc6\xea\x91\x9a0\xe2\xbc\xb9x\xf1\xa2\
\xf6\xb93T\x13u_\x11\xd4\xcc\x05\xc6\x9a\x07\xc4\x0b\
\x84\xad\x0b1>\x08\x93v3\xdd\xf4\x9ad\x00\xce!\
LSrG\x0a\xa2\x88\xf3\x0a\x8eP\x99\x81{\x07\x07\
\x11A\xb5\x5c`L\x03\x11 |n\x9f\x85H\xa6\xe9\
\x12240\x14\x10C\xe4\xd7\xb1\xb9X\xac\x1c8p\
\xa09\x9fP\x91\x81\xdd`O\x11\xd4\xec\x0b\xc4\x0b\x85\
\xf2yh\xe0\xf8\xd0\xe1N\xe1#\xb9\xe1\x04\x11F#\
F\x878??\xaf=\xb1z\xef \x9b\xb9\xf7\x13\xc1\
%QZ#\x8c<`\xc80\xd8\x8d\x0c\xa7\xf8\x91:\
\x80s\x0d\x05\xc8\xf3\xe7\xcfK, \xfaB~P1\
\xdd\xd4(D\xf0\xa1~_\xd4U\x045]`\xc8j\
0\x9c\xdf\x89\x13'\x98\xe7#\xb5\x04b\x88P\x19b\
\x18C.\x1eEF\xe4\x07\x15\x0b%}s\x83\xbdV\
\x8cL\x89\x02\xf8\xc3\x87\xb8\x1a\xb9\xf2\xfc\xd9\xb3g)\
\x80\xa4\xb6\xe0\xbd\x8f\xf4\xcf\x95+W\xa2\xd8Z\x13\x15\
neC\xd4w\xcaL/'\xa8\xd2\x16\x13\xa2)\x1a\
IX\xbc\xe0li!d#\xb1\x84\xc90(\x8a\xa9\
\xa9\x9en\xb0\xa3\x08\x16\x02\x08\xf1\xfb\x83x\xc6\xba'\
\x10\xa2\x87\x0aT\x04\xdb\x05\x12\x125\xa1\xd7\xed#,\
F~P\x89\x9e\x95\xe2n\xe1\xf0aQ\xc02\x0f\xe8\
\xc2_\x0a !\xfd\xc1\xf9\xe2B\xe4\x10(\x87\xc5S\
e\x8d\xa3#\xdd\x9c\xa0\xf7-4-]`\xc0\x0d`\
\x08I\x9eP\xae\x10\x91\x1b\x84X)m\xd5uM\xf1\
\xe7\x9c`\xb9BdB<c\xe5\x02)\x80\x84\x8c\x87\
;\x87\xa6\xa7\xa7\xc5\x12\xac~Q\xd4\x89C\xdd\x06\xaf\
v\x0a\x87\xf7\x88\x02\x16\x1b\xa5\xe3\x0aB\x01$d|\
p\x0e\xbd\xfe\xfa\xeb\xe6\xe11\x1a\xbb\x95\x1c(\x04\xb0\
c\xa1\xb7\x93\x08>#\x9eA5\xd8\xc2Z\xa3\x03\x9d\
\x02H\x88?Bl\xa7\xa9\xe8\x06;\xb6\xcbl\x10\xc1\
2y\xe8\xbd-\xc6\xa2%\x06/\x16\xd6\xfd\x12B\xfc\
b-\x84\x8a\xa6\xe9\xd1N!q\xbb\x13\x9c\x12\xcf\xe0\
\xc9h/\xe4\x86\xfb\x0bU\xd5\x22\xa4\x0eX\x0b\xe1\xe9\
\xd3*\xe3K!\x80\xfb\xdb\x1fl\x17A\xef\xf9@\x0b\
\x17h0\x96\x87\x90\xda\x03!\xb4j9\x83n(m\
S\xf0\xad\xf6\x07\xdaE\xd0{>P\xbb\x13\x1d\x83\x10\
\x98\x07$\xc4\x06,>@c\xb36\x10@%7\xf8\
\xb9\x90\xf8\xae\x08\x96\xabDF\xde\xbb\xb3\x13n<\xb8\
&\xb8:\x11Bl@\x07\xc6\xc9\x93'M\x96\xa0*\
\xcd\x1a\xfd\x5c\x95\xb8\xea\x04\xa7\xc43\xdam1t\x81\
\x84\xd8c\x95\x83wS\xb3\x15\xd8\x90\xf6S\x15\xc1K\
\x97.\x89&t\x81\x84\x84\x01CI0\x96N\x9b\xe5\
\xe5eQ`C^\xb0*\x82\xdb\xc5#Pq\xcd\xaa\
0] !a\xc1\x5cN\xed\xb0X\xa9@2Q]\
K\xdc\x14\xc12Q\xe8\xb5?\x10\xf9@M8\x18\x81\
\x90\xb0\xc0\x84\xcc\xcc\xcc\x88&n\x87=\x05\xa6\xdc\x1d\
\xe7\x04\xbd7Hk\x86\xc2\xf8\xe3s,>\xc9\x0dw\
\xc2\xbb\x9d\xe3,\x0a\x8b\xe3\x82\xb0X\xdb\x0d*\x85\xc4\
w;a\xee)o\xbd\x8b\xa0\xa6\x13\xb4\xc8E\x10b\
\x81k\x05\xe9\xb7J\x02m)\xd5\xad`c1\x01\xf8\
\x9d\xe0\x065\x07\xa4\xe0o\xa3\xb03\xdd\x94\xbb\xd3\x1c\
\xa5U\x84\xc3\x0b\xc5\x8d\xd7r\xcf#\x8f<\xa2\xd5\xec\
\xa8=\x856N\xfe\xfa\xf1\xc6\x8f\xef\xff\x92d\xc1g\
\xc5{\xe4\xb3\xb5\xf5\x8fsy^\x03\x00\xa3p\xf0\xe0\
\xc1\x91\xdc\x1e\xc4\x07\xcbD\x91\x1b\x0f}.\xe0<\xc7\
\x98<\xad\xf3\x1d`\xe0\xaaB\x7f\xe2\x177m\xdat\
\xcb9\xc1G\xc4#xq5\xff \xd9\x0a \x84\xee\
\xdaoDn\xfcY\xe4\x93\xe2\xfe\x8d?\xb5\x04\xe2\xb3\
.\x7f\xcb/\x14a\xc8\x03_n\x09\xc7\xf6\x87\x0b\xbb\
\xf0\xb5\xe2\xf6+\xad\xc7c\x03\xcf\x09\xcf\xed\xff\xfe\xd4\
\xba\xff\xf7\xb5\xcf\x0b{\x15<\xa7\x07\xbe\xd4z>\xb8\
\xdf|n\x0fK.@\xf8F\x15@\x80\xf3\x0b\x0e\x09\
\x87kY\x09\x95'w{v+577Az@\
A\x04\x11\x01\xaf8'\xf87\xf1\xd8(\x8d| ^\
`\x0d\x8cv\xaf\xb7\xe3\xfaoE>\xf8e\xeb\xe8%\
\x0a\xc30\xf9\x94\xc8\xd3/\x14\xc2\xf1TXg\xe5\x9e\
\xdb\xfbg\xbb\x0b\xf90@\xdc'\x0b1|\xfc\x9b\xad\
#F\xb1\x1f\x10\x8d\xbdvpn\x84\x9a\xa4\xa4\xbd\x8f\
8f\x1bb\xb4\x97g\x8e\x14Np\xe1\x9e\xb22\xec\
u\xa5\x88f\x93t\x16\xf9@\x08\xc2\xbb\xc5\x0b\xfa\xcb\
\x93~\xc4\xa1\x1d\x88\x0f\x0e\xf0\xf4\xf3\x22\xfb\x8e\xda\x89\
\xa1\xe6s\xc3\xf7\xfb\xfd/[\xc7\x1b\xf7\xb6\x84\xd0\xf2\
\xb9yDcM=\xce\xbb\xdd\xbbw7]!\x0a\x16\
\x96@\x80qnj\xb5\xc5)}\xdff\x04\x8c\xea\xf0\
\x84x\xe6\xe6\xcd\x9b\xa2\x85\xc5\xbaE5\xe0\xf4N~\
_\xe4\xbb\xff\x22r~^G\x00\xdby\xff\xad\x22\xdb\
\xfbx\xeb\xe7\xfar\x9a\x9d\xc0s\xf9\xc5\x89\xe2g}\
\xd5\xe6\xb9\xe1\xfb\xbb\xe7\xf6\xea\xdeu\xd1O\x00\xcd\xa2\
!\xc2\xe4\xe3\xc7\x8f\x07\xd9\xd7[3M\x85\xe7\xa5P\
)o\x16\x84!\x82^] \xd0,\xeb')\x82N\
\xfe\xe3\xeb\xad\x137\x04N0\xf0{\xf8\x06\x02\xf4\
\x9f_\xb7\x13\xf6N?\x1fB\xa8-\xf4\x9e\xd0\xcc\x97\
;\xb0\x8d\xa6\xb5\x10j\xe7$\x15\x22\xcc\x09\xfc\xa3\xe2\
\x04\xb5\xaetH\xc0&\xb7J$\xb4@\xb4\x83\xdf\xe3\
\xf0\xe3~\xc4\x02\xcf\xe7\x8d\xd9\x96\x00}\x12\x81\xf8h\
\x0a}\x82@\x08\x95\x86\x10t\x04\xe7\xa6\xe6\xf9y\xfd\
\xfau\xf1\xcc}H\x07z\x17A\xcd\xab\x5cR.0\
6\x81\xa8\x82\xdf\x07b\xf1\xee\xcfed \xa2xn\
\xefzOV\x8f\x8fO\xa1O\x1c\x84\xc6\xda\xab\xb7\xaa\
h\xe6\xec?\xfeX\xe5\xf5\x9c\xf0\x1e\x0ek\xfe\xc1\xb7\
l\xd9\x22I\x10\xb3@Ty\xe3\x07\xa3\xb9&\xb4\xee\
\xb4\xc7m\xac|R\xbe\x06(\xa2\xd4\x1cT\xa2\xad\
\xd0\xcc\x0b*8A\xf0\xa8J8\xacE\x12N\xd0\x09\
`\xcc\x02Q\x05\xae\xe9\x8d!\xba\xf1\xaf\xbc\xd5z~\
1\x84\xf6\xfd\x80\x10\xfe\xf7\xfe\xda\x87\xc70&Va\
\xb1vqD\x81f8\xbcU<\xa2Y\x14\xb1\x18\xe4\
8\x16\xce!\xc5\x16\xfe\xf6\x03\x8eu\x10!\x84\x00\xfe\
\xec\xfbi\x08`\x15\x08}\xcd\x85\x10E\x12\x8b\x82\x0c\
r\x82Z\xe7\xa9R\x85xb\xb3xFS\x04\xa3.\
\x8a8\x07\x98\x9a@8 \x84\xbd\x84\xe2\xf7\xef\xb6\x04\
0Uj.\x84\x10\x10\xed\xf9\x9e\x0e\xcd\xf3TA\xc8\
\xb7B\x04\x1f\x92D\x88\xd6\x09\xa6.\x80\x0e\x08E\xa7\
b\x09\x9e\xdf\xcf\xfeM\x92\xa7\xdb\xf3\xab\x09\xda\xfb\xfd\
8&''E\x0b\x85\x9a\xc3\x17\x93r\x82Q\x8a \
\x84/\xc6\x0a\xf0\xa8\xa0X\x82\xb5\xbd\x8e\x5c\x04\xde\x81\
\xe7\x97Pc\xb5O\xd0gg\x15\x12'\x84\xff\x9c\xa0\
&Q\x8a N\xaa\x5c\x04\xd0\xf1\xdaK\xeb\xa2\x97\x93\
\xc0;P,\xa9i\xfb\x8c\xc5\x16\xb8\x9a\x22\xa8`\xb2\
\xb6\xab\xac\x18\xa9\x0d\xc8\xa3\x85Z\x01\xa2\x09D\xaf\x99\
C\x9b\xcfO\x00\x01&\xf3@\x08k\x88E\xcf\xa0\xa6\
Y\xb9}\xfb\xb6\xf8\xc6{8\xac\xf1KF\xc9_K\
\xa1\xc8\x15\x08\xfc\xf9\x8c\x0b\x09\xa8\xe4\xe7\xfc\xfc\xba\xa0\
\xb9\xef\x8fCS\x04\xd7\xd6\xd6\xc47\xdeEP\xe3\x97\
tD\x95k\x88e\x19\x1c\x19\x1d8\xdd\x9a\x85\xc5\x16\
\xe3\xfa\x13\xcb\x09\xfao\x91\xa9\x05\xcd\x19y\x19\x86\xc1\
u$\xe5\xb6\x9f\x11\x89}\xdf\x12k(\x82\xa3\xf0\xbf\
\xde\xf7; \xa1\xa8\xce^\xac\x09\x14\xc1\x8dP\x04\x87\
\x05\x0e0\xc7bA\x9d\xc99\xb7\xdb\x01\x8b6\x19-\
4f\x95&%\x82Q\x5c\xc1\xde=)$3j\xe6\
\x06S\x16A\x0d\xe8\x04\x87\xe1\xfao66\x12\x93|\
\xa8\x99\x1b$\xebP\x04\x87\x81\xc5\x90|\x81\x13\xe4\x05\
\xae\x96x\x17\xc1\xad[\x93Y\x802<\x1fp6]\
\xd6\xd4\xe4\xf5Mn:\xbb2\xdeE0\x99\xc1\xa7\xc3\
\x82\x13\x84}\x81y\x13\xfb\x10\x5c\xa2\x02\xc3\xe1A\xe1\
\x84\xe2\xfc\xc1r\xba\x1a\x14HRv\x82\xdb\xb6m\x13\
\xdfP\x04\x07\xa5\xa6\x93Gj\xc75\x8a`\xdd\xa0\x08\
\x0e\x02\xd6\x99\xb27\xb0\x1e\xa0\x03 c,\xb6\xa8H\
\xac\x05\xe7\x16D\xf0\x86\x90\xde\xb0jX\x1f2\x7f\xad\
5\x07\x9e:4EP\xa1\xf0z\x8bNp\x10n\xfc\
QHM@^0c\xd7\xbfk\xd7.\xd1FS\x04\
5\x0a\xaf\x14\xc1A\xa0\x13\xac\x17\x19\x87\xc4\x9a\xbb\xc1\
94g\x16*\xe43o$%\x82\xc1r\x0d\xcc\x07\
\xd6\x8bL_\xef\xe9\xe9i\x93\xe9\xecJ\xfb\x037\xd1\
\xf8\xfd)\x82\x83P\xd3Q\xec\xb5%\xd3\xd7\xfb\xb9\xe7\
\x9e\x13\x0b4g\x8a*\x88\xe0G\xdeE0\xbb\xf2\xfb\
gz/(\x89\x94\xbf\xe7\xd7\x14\x8f\xf3\xd2\x22\x1f\x08\
4\xa7W+T\xb7\xd3\x0a\x87\x83\x90\xe1\x09A\xfa\x90\
\xe1\x85\xef\xd0\xa1Cb\x01v\xb4\xd3\x02.P\xc1\x09\
\xb2:\xdc\x17:\xc1\xfa\xf1\xf7\xbc^\xf3}\xfb\xf6\x99\
\x85\xc2\x9aE\x11\xa5\x1e\xc7U\x88\xe0G\x92\x08Ar\
\x82\x14\xc1\xfa\xf1\xff\xf9\xb8\x7f\x84\xc1\x87\x0f\x1f\x16+\
.]\xba$Z(\x89`ZN0\x88\x08~!\xe3\
\xa98\xa43\xff\x14\xe1\xfe\xd6#\x80\xd0\xf1\xe4\xc9\x93\
fyz\x9c\x9f\x9a\xf9@\x8du\xc3\x9b6mZe\
a\xa4\x1f\x14\xc1\xfa\xf1\xcf\xba\xaf\xb9E\x9b\x0a~\xc6\
\xfc\xfc\xbc\xc929\x87\xa6\x0b\x04\x0a\xcfe\x15\xff\xdc\
#\x84\x10S\xb4E\x10\xdf\xff\xcd7\xdf4\x15@\xb0\
\xbc\xbc,\x9a(4z7s]I\xad\x1d\x0e\xb2\xc7\
\xc8\x03_\x12R3\x94\xdd?\xa2%\xad\x88\xe9\x89'\
\x9e\x90\x8b\x17/\x9a\x0b \xceMM'\xa8\xb4\xd2e\
\x05\xff\xb0:<\x08_\xc8#GD\x06\xc4\xe0\xc27\
33#>\x81\xfb\x9b\x9d\x9d\x95\xb3g\xcf\x06II\
i\xb6\xc6\x00\xad\xca0\xfe\xf1\x1e\x0eg9\xab\x0c\xce\
\x80S\xa5\xeb\x83\xc1E\xef\xc0\x81\x03\xcd\xe5e\xe7\xce\
\x9d\x93q\x80\xf8AP\xf1\xfd,r\x8d\xddX\x5c\x5c\
\x14M\xb0\xe4O\x81\x06\xfe\x81\x08\xde\x92D\x08\xb6\xe5\
\xe6\xc4W\xb8t\xaeNl\xff\x8aX\x80\xc2\x05*\x9e\
\xc3\x0a\x08\x8c\x06D\x01+@,\x06\x22\xf4\x03B\xae\
ynB\xdc\x15\x9e\xe7-T\x86q\xc7\xbb\x08\x86\xbc\
\x1a\xa9q?'\xf1\xd6\x8a\x07\xbe,V\xa0\x87\x0f\x8d\
\xcc\x08'\x91Sko\x03\x83\xe0\xe1\x9cryD\x88\
Al\xe7\x98\xb6\x0bD\x9eS\x81\xcb\xee\x8e\xf7pX\
\xf3\x05\x0a6@\xc1\xc8\x19\x90H\xd8\xfe\xb0X\x02q\
\x83\x10Z\xad\xea\xf0\x89\xb6\x0b\x04Jk\x9eW\xdc\x1d\
\x14F\x92\x09\x87o\xdf\xbe-A\x98\xfc\x9a\x90\x9a\xf0\
e[\x01L\x1dm\x17\x08\x94B\xfeUw\xc7\xbb\x08\
j\x16F\x829AT\x0bY!\xae\x07;x\xc1\x1b\
\x94\x85\x85\x05u\x17\x88\xdc\xa7\x82\xa6 \x1f\xb8\xe2>\
\xe0\xb2\xb9AaH\x5c\x0f&\x9f\x12\xd2\x1f\x88\x9f\x85\
\x0bT\x0a\x85/W?\x80\x086\xc43Zy\xc1\xa0\
\x22\xf8\xd5o\x0a\xa9\x01\xbc\xd8\x0d\xc4\x8b/\xbe(\xda\
\xb8\x5c\xa9\x02\x17\xaa\x1f\xa88\xc1,E\xf0q\x8a`\
\xf6 \x1f\xc8\x15B}\xb1\x08\x83\x81RU\x18\xacT\
?H\xca\x09\x82`\xbd\x8289\xee\xe7\x09\x925\xcf\
\xbc \xa47h\xe5\xb1\x08\x83\x81\xd2\x08\xb0\xcbE>\
\xb0Q}@\xc5\x09jl\x8b\x17\x05O?/$c\
\x98\xf2\xe8\x09\x0c\xc8\xd1\xa3G\xc5\x02\x0c\x82U*\xb2\
.\xb5?\xb0\xb9]\x15}\xa0Y!\x0e\xe6\x04\xc1\x0e\
&\xcd\xb3\x85\xa1pO\x90\x8aB\x1e\xd0\xea\xfcS\xec\
\x99\x5ci\x7f \xb9\x01\x0aAE\x10\xfd\x82\xac\x1e\xe6\
\xc97_\x16\xd2\x1dK\x01D_\xa0Ro\xe0\xe5N\
\xa6\xcf\x89\xa0\xd7qZY\xf6\x0a:\x182\xe5\x09\x1b\
\xe2\xbb\x82\x10Xs\xef\x90v\x147\x85Z\xea\xf4\xa0\
\x13\xc1\xbf\x89G\xb2\x5c:\xe7x\xe6y6N\xe7\xc6\
\xce\xe7\x19\x0aw\x01\x028\xee\xa4\x9ba@.P\xc9\
\x05bQ\xc8\x85N\x9f\xd8\x5c\xf9\x02odY\x1dv\
`\xac\xd67\x18:e\xc5>\x9bd\x7fJ\xb8\x1c\xa0\
\xa5\x00\x02\xc5M\xa1.\x14\xa1pG\x9dc8<\x0a\
O\xb3\x95\x22\x1b\x90\xe3\xa5\x0b\xdc\x00\x8c\x06\x04P{\
Pj;\x8a\x15a\xd0\xb5\xafG\xc5\x09j\x8a \x06\
Q\x06\x07'\xcdN\xb6\xcbd\xc1\xb7\xe9\x02\xab@\xf8\
\x80\x969@\xa0\xbc5\xe8e7;\xb0\x13\xc9\x85\
\xc3Q8A\xc0\x10*}\xe0\x02\xd9\xf6t\x97S\xa7\
N\x99V\x81\xab\xa0\x18\xa2h\x9e\x96z}\xd2\x89`\
C<\x02\x11\xd4\x5c:\x17\x85\x10\xd2\x0d\xa6\xcf\xc1\x9f\
\x0aY\x0f\x7f\x8f\x1f?.!P\x5c#\x0c\x1a\x85\x0b\
\x5c\xea\xf5\x05*N\x10d]\x1cq\xfc\xebqV\x8a\
S\x85\x15\xe1&p\x7f\xbbw\xef6\xcf\xff9\xdc\xf6\
\xa0\x8a\x1c\xeb\xf7\x05N\x04W\xc53\x9a[\xfeY\xe7\
+\xba\xc2Jq\xba\xd4<\x9d\x01\xd1\x83\xf8\xc1\xfd\x85\
\x8c\xac\x94\xc3\xe0\xbe.\x10\xb8\xf1\xfa\xde\x9d\xa0\xe6\xfa\
\xe1h\xf2\x82\x00+\x0d\xde\x7f\x8b\x1b1\xa5\x04\x8a!\
5u\x81n\x00B(\xe7W\x05\xd5`\xec\x92\xa7H\
_\x17\x08\x9aN\xb0\xec\x9fI\xa6B\x1c\x8d\x13\x04p\
\x83\xdf\xb3\x99\xaaA<\x80I@\xfb^\x91\xba\xe1\xaa\
\xbe!Z_:\xa1\x5c\x0d\x06\x03\xb9@P\xddhi\
\xad8\xee\x13Oh\x8a\xe0\xcd\x9b7%*\xb0\xe4\x0a\
9\xa6+o\x09\x89\x9c\x1f\xbc-u\x01\xb9\xf3\xf3\xe7\
\xcf7\xf3~QEO\x05\xc8\x03*\xefQ>\x90\x0b\
\x04U\x11D^p\xbbxB3'\x18Ma\xa4\x0a\
\x8a$\xd7\x7f\xcb\xb08fj\x10\x06\xc3\xe5]\xbdz\
\xb5y\x1b\x83\xe3\xeb\xc4\xec\xec\xac\xb6\x00\x0e\xec\x02A\
U\x04\x93Z:\x87+[T\xfb\xaf\xba\xb0\xf8\xd5o\
\x0b\x89\x90\x0c\xc3`\x9c\x07H\x0dA\xf4p\x1f\xa2\x17\
\x9b\xe3k\x07!\xb0r\x1e\x10\x1c\x19\xe6\x8b\xdb\x9d\xe0\
K\xe2\x09\xb7i\xb4\xd6\x8b\x82\x17]\xd3m\x8e\x04\xc2\
\xe2o|W\xe4\xdd\x9f\x0b\x89\x08\xb41E\x14\x06C\
\xb8\x10\xa2\x0e\x0b\xd2@\xaeO6\xcah\xa8\x0f\xe8\x05\
T\x9c\x10\xe3X*\x5c\xe0\x85a\xfe\x83\x9a\x13\x04\x10\
B\xad\x22\x06\xaez\xd1\x89 @\xeb\x05\xc2\xe2\x1b\x7f\
\x16\x12\x09\xfb\xe2\x0a\x83\xf1\xdeE\xae\xaeN`\xbf\x90\
\xf9\xf9yQ\x06\x1a6p.\xd0Q\x1d\xaa\xea\xbdW\
prrR\xb4\x88\xae8\xe2@X|\xe4\x0c\x9b\xa8\
c\x01}\x9c\x91\xf5r^\xbatI\xea\x04\xcc\xca\xeb\
\xaf\xbf.\x06,\x8e2)\xbf*\x82\x0d\xf1\x8c\xa6S\
\x8b5\xe9\xdb\x04\xae\x83K\xb2\xc2\x83\xed3\xbf\x13f\
)X/\xa2\x18\x02b\x044\x00\x95`\x83\xfc=\x8a\
!s2\x02wE\xb0\xec\x15Lf\xa4V\xf49\x11\
l\xd1\xf9\xed\xfa\xf5\xa3E\x03\x0a!G\x96$6\x90\
\x1e\x8a\xbdx\xe1\x0b\x84\xc0F\x02\x08\x9e\x95\x11i\xdf\
c\xc4kH\xac\xe9\x04\xf1F\x8a\xaai\xba\x13\xc8E\
q\x87:{\x5c!$\xc2v\x98\xa8#\x18\x8f\xa0\x08\
r\xf6\xecY+\x01<6\xce\x86q\xed\x22\xd8\x10\x8f\
\xb8\x0a\xb1\x16\xd1\x8b @8\xb6\xfda!\x86|\xef\
\xa7\xd1\xf6\x03\xa2\x9d%w\xd0\x06cP\x04q\xac\x8e\
\x1a\x06;TE\x10h\x86\xc4I\x5cUQ(\x81+\
\xe1\xc6\xed6\xe0\xa2\x13\xf1fX\xb9;A4B\x1b\
\xb4\xc18\x1a\xc5\xb1W\xc6D5\x1c\x06\xc8\x0bh\x91\
\xccU\x95Bh\x03V\x84D<\xd5'\xe7| \xcc\
\xce\xc5\x8b\x17-\x1a\xa1\xab\x8c\x15\x06;\xd4EP{\
\xf9\x5c2o*\x84g\x14B= \x80\x91\xaf\x08I\
\xb1\xc1y\x10\x5c\x01\xc4\xb8o\xf7\xd80K\xe3z\xb1\
A\x045*\xc4\xda\x7f\x98\xa4\xc2\x0b\x0a\xa1\x0e\x09\x08\
X^^\x96\x9c@\xbe\x1f\xe1/\x0a \xcak\x81\
\xdb\xb9<n\x1e\xb0\xca\xe6\x0e\x8fy\xaf\x10k\x16G\
\x92K4S\x08\xfd\x92\x88\x00\x82$\x0ay\x03\x02\xf7\
\x17 \xfc\x05\x8d\xe2\xd8/\x1eQ\x17A\xa0\xb9r$\
\xc9\xee{\x0a\xa1\x1f\x12\x12@7\xec u\x02\xba?\
\x80H\xf5Y\x1fy\xc0*&\x22\xc8\xbc`\x07(\x84\
\xe3\x91\x90\x00\x82\x1c\xaa\xc2\x98\x04\x1d\xc8\xfd9\xbc\x0b\
\xe8$\x82+\xe2\x99'\x9f|R4Iv-&\
\x85p4\xb0$1\xb1\xb1X)\xf7\x07\xba\xc2\xc7\x89\
\x13'B\xb8?\xc7L\xaf\xbd\x83\xc7\xe1s\x22\xa8Q\
\x1c\xa1\x08\xf6\xc0\x09!\x1b\xaa\xfb\x83\x95 \x10\xc0\x04\
W\xe1\xa4\xe8\x04\x9d\xf8!\xf4\xd5>\x87\xfb\xe0\xad\x12\
\xdc\x89\xcd]\x1e_\x11\x8f \x8f\xa0\x19\x12\xe3*\x9b\
t\xff\x95\x13B\xcc\x22$\x9d\xb9\xbf\xfc\x1b%*\x80\
)\xb5\xc7D$~\xe0\x98\xcfJp'\xba\x89`R\
M\xd3I\xac#\xee\x07\x1a\xaa\xbf\xf3*\x87.t\xc2\
\x09 \xa6\xc2$H\x0a\xb3\x03aT\xb0\xd2\xe3\xca\x95\
+\xb1\x88\x1fP\x17@`&\x82\xbbv\xed\x12M\xb2\
\xe9\xc1\xc2\xd0\x85\x83\x8b\x9cG\xe8\x98|J\xe4'\xbf\
Nzo\x90XCa\x08\x9fs}\x10?\xac\xf9\x0d\
\x98\xf3k\xc7D\x00\xc1\xa6n\x9f\xb8s\xe7\xce\xdf\xc4\
\xe3\xeespk;w\xeeT\x0b[\xf1\x82~\xf8\xe1\
\x87\x92\x0d\x9f|,\xf2\xea\xdezo\xdc\x94X\x05\xb8\
\x13n\xab\xcbX\xc0y2==\xddtz0&Q\
\xed\xd3\xb3\x8e\x99\x00\x82{z|\x0enpJ<\x81\
?6\xfa\x05\xb5\xaad\x10W\xbc\xe1\x22\xb1\xf1\xe3\x03\
\xe7\xf3\x93_\x89\xfc\xcfl\xfd\xb6\xf2\x84\x0b\xc6$\x98\
\x88\x07!\x0cJ\xe8P\xd8\x9dw\x10<\xe4\xe5\x138\
?f4\x8b \x9d\xe8%\x82\xef\x88G\x11\x04x!\
4[\x05\xce\x9d;\x97\x8f\x08\x82\xe6\x0ev\x85\x18L\
\x14\xb9\xb07f\xa5\x16 \xef\x87a\xa8\x19l\x8d\x89\
b\x08\xde\x93\x16@\xec\x9c\xe0!\xa4u\x82\x17Qx\
\xdb\x0ft\xa5\xec-\x04pE\x8c\xe9\x15\x0eO\x157\
\xef\x89G\xf0\xa6@H\xac\x05\xde\x04\xc8mDj\xf1\
\xc7\xa3\x0e\xe11&\xc0 '\x9aI>\x14\x02x\xf4\
\xe8Q\xd1\x02\x02\xe7&7'\xfe\x9eo\x88R#\xf4\
t+\x8cH\xa9\xc8^w\xa0\xc3\x8b\xa6]%\xb6\
\xba\xf2\x9a\xe3\xc2\xe3\x1c\xdbh z\xff\xbe\xd4\x9a\x05\
\x98QAhqqQ4\xc1\xb9\xa4=\xb8\xd8\x80\xcb\
\x12P\x00\xc1\xe6>\x9f\xbf,\x9e\xd1\x0eWs\x9b\xd4\
\xb1\x01\xd7F\x83\xeaq.\xabLP\xfd\xfd\xf1\xaf\xb3\
\xc8\xffU\xb1\xe8\x0d\xc4\x08\xfb\xc4\xc1\xeepS!\x05\
\x10\xf4\x13\xc1\x15\xf1\x8c\xb6\x08\xe2\xcd\x97\xfd>\x0eO\
\xbf\xd0\xea\x9b\xdb\x99\xf0\xfe%p|p~\x91\xee\x05\
2.\xda\x05\x118\xc0\x84\xf3\xdf\x880Q\x009,\
\x11\xd0O\x04\x97\xc43\x16\xc9\xda\xac\xdd\xa0\x03\xc2\x81\
\xa2I\x8a=\x85\xce\xfdE<\x05z\x1c,\x0a\x22\x9a\
i%e\x10]>f]\x01\xeeEO\x11,\xd7\x11\
{o\x9c\xc64\x0aM\xf0\x06\xac\xcb\xb6\x86MW\x08\
A\x81\xb0\xc4N\xe6\xee\xcf\xa1\x9d\x0b\x04\x89\x86\xc2\xc7\
b\x08\x7f\xdb\xe9\xe7\x04\xc1;\xe2\x19\xed\x17\x10\x02x\
\xfa\xf4i\xa9\x0dn\xedq\xcc\xb9\xc2\xcc\xdd\x9f\xc3\xc2\
\x05&\x18\x0a\xc3H=f\xd9\x00=\x0c\x83\x88\xe0\x8a\
xF\xbbJ\x0cN\x9d:%\xb5#\xc6\x5caM\xdc\
\x9f\x83.p\x03\x88$\xe1\xfe\x1e\xd3\x1a\x83\xe5\x83\xbe\
\x22\xa8\xd1*\x03\xb4\xd7\x12g\xdd.\xd3\x8bj\xae0\
\xb4+\xac\x89\xfbsX5Gk\xa7\x93<\xe1r\x7f\
s\x129\x838ApF<\x83\xab\x99v\x7fS\x0a\
\xd3;\xd4\x08\xe9\x0ak\xe6\xfe\x1c\x16.\x10\x02\x18\xf9\
*\x90\x86\xb4\xfa\xfe\xa2\xcb\xfducP\x11\xbc \x9e\
\x81\x00j_\xd1j\xd1.\xd3\x0b\xe7\x0a\x17>\xb0s\
\x85p}\x8b\xbf\xaf\x8d\xfbsX\xb9\xc0\x88CaD\
\x8bG\x0a\xe1{(\xc4\xd2\xb7q\x18H\x04S\x0d\x89\
\x81\xc5\xd59z \x86\x8b\x1f\xb4f\x15j\xb5\xd3`\
\xcd\xef\x7f\xfd\x22\xbbU\x1f\x83b\xf1>\x8b\xb4 \xd2\
\xcc\xfb\x15\x07\xc4oA\x12dP'\x08\xbc\xbf\xcax\
A\xb5\x0b$\xb5w\x83U\xb0.\x179:\x9f!2\
\x04\x0f\xe2\xfa\xe3_\x89\xec\xf8\x9a\xd4\x118@\x0b\x17\
\x88\xa1\xa7\x11Q\x15\xbf\xb9\xb2\x9d.I6\x0d\xfa\x85\
\x1a\x03\x15\x80\xc5\xbc5\x5cA1X\x81Tx\xffl\
\x914=1\xde@\x06\x88)\x84\xb5Fy\xbfN`\
(\x88\xf6\x12\xb9\x88\xde\xc3(x,\x14\xa2\xe7=E\
\x16\x8a\x81\x9d`\x19\x12\xaf\x88g,V\x90\xe0\x0dZ\
\xcb\x96\x99^\xa0p\x82\x10\x19!\xec0\xce\x10\xce\x0f\
\xf9>\xfc?\xe4\x1bk.\x80x_Y\xec\x1f\x12\xd8\
\x05\xc2\xe5!\x12t\x05\x8fl\x04\x10\x0c\xec\x04A\xe1\
\x06\xb1\xd6\xef5\xf1\xcc\xc2\xc2\x82zN%\xeb1[\
>\xf8\xec\xd3\xa2\xae\xf7G\x91\xeb\xbfm\xb9\xc3OJ\
\x87\x08\x91\xc3\xe0\x86\xfb\x1fl\x85\xbb\x89\xee\xf3\xa1\x01\
\xc4\x0fQ\x8c\x85\x0b\xc4\xc8\xac\x00Ua\xb8>\x08\xde\
R\xca\xe1n?\x86\x15A\x8c\xdb\xffH<\x8e\xdd\x07\
\xda\xa3\xf7\x1d\xd8C!\xb2\xbc\x0aI\x18\xcc\x0a\xb4\xc8\
\x05\x1a\xbfok!|U\x86)\x8c\xb8\xb5\xc4\xde\xc7\
k\xc1\x9d\xcd\xcc\xcc\x886p\x9c)m}H\xe2\xc5\
\xaa\x18\x02\xf7\xa7\xdcJ\x86s\x1a}\xc08\x01\xbfX\
\x86\xbb\x0bu\x11@0\x94\x08\x96\xa8\x94\xc1\x0f\x1c8\
`\x12\xaajN\xfa%\xf5\x00\x17R\xab\xd6+\xf4\x05\
*\x85\xc1\x0diUv!|\xfb1\xd5\xa5N\xc2W\
eh\x11\xd4*\x90X\xb9AT\xa3Y$!\xe3\x00\
\x01\xb4\x88( ~\x8aa\xf0\x99TVth3\x8a\
\x13\x04\xde'\xcb\x00+7h\xf5&&\xf9\x81\x94\x8a\
\xd5\x9at\xe5<\xe0\x92\x90&\xa3\x8a\xe0\x92(\xac \
\x81\x00Z$\x80Q\x80aXL\x86\xc52\x0c\x86\x0b\
T\x5c\x22\xb7D\x17\xb8\xceH\x22X\xe6\x0eT\xde\x0d\
p\x83\x16\xad\x00\x0c\x8b\xc90\xb8v\x18+\x94\xcd\xc0\
1!w\x19\xd5\x09\x02\x14HT\x12\xa9\xf3\xf3\xf3b\
\xc1\xf1\xe3\xc7\xe5\xda\xb5kBH?\xf0^\xb1J\xa1\
\xa0\x1aL\x17h\xc7\xc8\x22X\xbaA\xef#\xb6\x80\xc5\
\x9ab\xc7\xc1\x83\x07\x99\x1f$=A\x1e\xf0\xd2\xa5K\
b\x05\xfa\x02\x15\xa1\x0blc\x1c'\x08\xd4\xa6F\x9c\
8qB,\x80\x002?H\xba\x81\x94\x89\xe5$\x22\
\x08\xa0b:\x88.\xb0\x03c\x89`\xf9\x07Uq\x83\
\xca\xed\x01\x1b@~\x10\xe1\x0e!U\x90*\xb1|_\
\x18\xbc\xe7\xe9\x02;0\xae\x13\x04s\xa2\x84U\x91\x04\
X_\xf1I\xdcX\x17B\x80vK\x0c]`g\xc6\
\x16AM7\x88\x96\x19\xab\x22\x09@\xee\x87\x15c\xe2\
\x04\xd0r\xdbV\xe5b\x08\xa0\x0b\xec\x82\x0f'\x08\xe6\
D\x09\x14I\xa6\xa7\xa7\xc5\x0a\x84?\xb5\xdc\xa0\x894\
\xb1\x9a\x0cS\x05\xd1\x8er1\x84.\xb0\x07CM\x91\
\xe9\xc5\x9d;wP$Q\xf1\xf3VSf\xaa\xc0\x81\
&\xba\xc15\x19\x91\x10\x02\x080&Kyl\xfeC\
\x14\xc1\xee\xf8r\x82`N\x94\xfa\x06\x11\x16\xcf\xce\xce\
\x8a%Vc\x92H\x1c\x84\x12@8@e\x01<F\
\x01\xec\x8d7\x11\xd4\x5cE\x02\xe0\xca,\xc3b\x00!\
d\xb1$\x7fP\x05\xde\xbd{\xb7\xb9\x00\x1aT\x83\x1b\
\xc25\xc2}\xf1\xe9\x04\x81\xda*\x12\x80\xdeA\xeb\xe9\
\xba\x16S\xafI8\xe0\xf6\xad\x8b \x00\xd1\x0d\xc2`\
e\xe8\x02\x07\xc0\xab\x08\x96n\xf0\x88(a]-v\
@\x08\xb1\xb2\xc4\xfaD!\xba\xe0u\x85\xdb\x0f\xf1\xba\
\x22\xbd\xa3|Ao`F\xa0\x90\xbe\xf8v\x82R\xfe\
\xe1WD\x09\xe4OB\x8c\xc8\xc7\xb2\xa9\x10!\x13\xf1\
\x0fD\x0f\x17\xb5P\x0e\x1fy@\x83\xa2\xdb\xb3B\x06\
\xc2[u\xb8\x8a\xd6\xf6\x9cU^x\xe1\x05\xb9z\xf5\
\xaaX\xe3\x8a4\xac\x1c\xa7\x09\xf2\x7f!\xd7\x8b\xef\xd8\
\xb1C.^\xbc(\xca\xa0%F\x7fBq&\xa8\x88\
\xd0l\x99\x01\xb8\x9a\x87tfX\xcd\x02G\xca\xdd\
\xeb\xd2\xc1\xad\x0a\x0a\x95\xd60\xda5\xae!\xad\xad1\
\x1bB\x06BS\x04Uv\xa6\xab\x82\xabz\x88\xa4\xb6\
\x03of\xe4(\x95[\x1c\xc8\x98\xb8!\x19X#\x1e\
\x0a\x5c,\xe1\x00\x0d\x0a{3\xcc\x05\x0e\x87\xf7\x9c\xa0\
C\xbbH\x02\x10ZX\xf7\x0fVq\xbde8\xc1\x98\
+\x8c\x13\xb8?D\x0c!\x05\x10\xe0bi \x80K\
\x14\xc0\xe1Qs\x82\x8e\xc2\x11\x2278%\x8a\xc4\xd0\
\xc6\xe2z\xbe\x98+\x8c\x03\x88\x1e\xde\x13\xa1\xc5\x0f\x18\
\xed\x1b\xdc\x10\x86\xc1#a!\x82\x13\xc5\xcd\x1fD1\
,\x06?\xfa\xd1\x8f\xe4\xf4\xe9\xd3\x12\x1a\x88!\xdc\xe9\
\xae]\xbb\x84\xd8\xe3\xf6\x01\x89e\xb5\x8f\xe1\xc6\xe9\x0c\
\x83GD]\x04A!\x84X\x1d\xfe\x9a(\xf3\xf2\xcb\
/\xcb\xf2\xf2\xb2\xc4\x80k\xe5a\xbe\xd0\x06\xe4\x85q\
\x11D\xf8\x1bK?\xa7\xa1\x00\xb2\x1a<\x06&\x22\x08\
,\xc2b\xbc\xf9\x91\xa3\x8bi\xdf\x10\x88 \xf6S\xa6\
3\xd4!F\xf1\x03\xe8\x1e0\xcaW7\x84a\xf0X\
X\x8a\xe0\x84\x18\x84\xc51\x0a!p9C\x88!\xdb\
j\xc6\x07\xb9>\xb8~\x84\xbd\xb1\xad\xe4A^\xd8p\
e\x13'\xc4\x8c\x89\x99\x08\x82B\x08\xf7\x147o\x8b\
2\xa1&\x82\x0c\x02\x04\x10B\x08w\x88\xea6\x19\x1c\
\x88\x1dV\xee\x9c?\x7f>\x8a\x82G'\x8c\x05\x10k\
\x83\xe7\x84\x8c\x85\xa9\x08\x02\xed&jG\xccB\xe8\x80\
;t\xa1\xb2\xf5`\x88\x94\x88\xd9\xf5U1\x0c\x81\xc1\
\x85B\x00\xf7\x0a\x19\x9b\x10\x22\x88p\x18\xf9\xc1GE\
\x19\x08 \x96H\xa5\xb0\xb70\x5c\xa1\x9b\xa2]\xf7b\
\x0a\x84\x0e\xafY\x0a\xc2\xe70,\x82\x80\x860\x0f\xe8\
\x0ds\x11\x04V\xf9A\x10k\x8e\xb0\x17\x08\x99\xdd\xde\
\xcb\xb8\xadC\xd8\x8c\xd7\xc79>\xdcOib\x8f\xb1\
\x00b\x11\xc2c\x14@\x7f\x04\x11A`\x95\x1ft\xa4\
<)\x1a\xa2\x08!\xc4\x01aD\xe8\x9c\xb20\xc2\xa1\
C\xe80\x00\x03\xb7\xa9\x89^\x95\x00\xdb0\x1c)\x04\
Pm\xbf\xef:\x12L\x04A!\x84s\xc5\xcd\x0f\xc5\
\x88\xdc\x06\xa4B\x08!\x88\x93\x93\x93\xcd[\x1c\x10L\
w\x1b\x12\x08\x9d\x0bkq\x1f\xc7\xf5\xeb\xd7\xef>\x9e\
:\xf8\xfb\x9e<y\xd2:u\xc1B\x88\x02AE\x10\
\x14B\x087\xb8G\x8c\xa8\xd3\xa4h'\x8c\x00'\xed\
\x96-[\xee>>.\xb7o\xdf\x96\xb5\xb5\xb5\xbb\xf7\
!l\xae\x08\x95\xfb:j\xa3i0\xed\xb0\x10\xa2D\
\x0c\x22\x88\xbc \xf2\x83\x13bD\xe8\x99r$]P\
\xb8\xc26\x0f\xc6N{UZ\x85\x10\xb5\xad+\xeaL\
p\x11\x04e\xa1\x04\x15\xe3\x091\x22\x85\x16\x1a\x12\x17\
h\x7fA\x1b\x8c1\x0da%X\x15\xb5QZ\xc3P\
\xbe\xc0\xb0\xfafW:\x842W\xae\x5ci\xf6\xe9\x11\
\xd2\x0b\x17\xfeR\x00\xf3$\x0a'\xe8(\x1c\xe1\xfe\xe2\
\xc6|\x14\x0c\xaa\xc6\xc8\x13\xd2\x15\x92v\x02\x85\xbf\x80\
\xad0FD%\x82\xc0j\xe2L;\x10\xc0W^y\
%\xc8\xbe%$> z\xe8\xfd\x0b\xe0\xfe\x00\x04\x10\
\x0epU\x88:\xd1\x89 \xb0n\x9d\xa9\x12z\x1f\x0a\
\x12\x1e\xf4b\x86\xd8\xe3\xba\xc2\xdeB\x00/\x081!\
J\x11\x04!\x85\x10\xae\x10CZc\x99MHl\x08\
\xec\xfe\x1c\x1c\x8ejL\xb4\x22\x08B\x0a!`\xae\xb0\
>D\xb2{ \x050\x00Q\x8b \x08-\x84\x10@\
\x84\xc81\x8c\xee'\xfeA\xe8\x8b\xb5\xbf\x81\x87V4\
7%\xa3\x00\x86!z\x11\x04\xa1\x85\x10@\x0c\xb1\xda\
\x04\xb3\xecH\xfaD\xb4\x17\x0c\x8b \x81IB\x04A\
\x0cB\x08\xb0\xda\x04\xf9BV\x91\xd3$\x92\xbc\x9f\x83\
\x02\x18\x01\xc9\x88 \x88E\x08\x01\xc6>\xc1\x19R\x0c\
\xd3\x00\xe2\x87\xc6x\x88_$\xdb\x1b4\x84\x8d\xd0Q\
\x90\x94\x08\x82P\x0d\xd5\xdd\x80\x18\xa2\x80\xc209N\
0ig\xdf\xbe}\xcdqW\x11\xed\xed\xd2\x10\x0a`\
4$'\x82\xa0\x10BL\xa5\xc6\xf4\x99\x09\x89\x04\xe6\
\x0c\xe3\x22\x92\x82G'.\x17\xc7\x1e\x0eC\x88\x87$\
E\x10\x84\x18\xba0\x08\x10C\xb8C\xb6\xd6\xd8\xe3B\
^\x14;\x22\x1d:\xbbX\x88\xdfa!Q\x91\xac\x08\
\x82R\x08\xe1\x08\xd5\xf7+\x19\x05\x17*#oHA\
\xd4\xc3\xb9>\x08_\xc4\xdb\x99r\x22t\xa4$-\x82\
\x8e\x98\x0a&\xdd\xc0V\x918\xb0\x0a\x85K\xf2\xc6\x07\
\xc2\x07\xc7\x17Y\xae\xaf\x13\x0di-\x83c\x058R\
\xb2\x10AP\x0e^\x80\x10\xaao\xde4.t\x88\xc3\
\x03\xa1s\xc2\x97\xd0\x06\xf6\xc8\xff\xedg\x01$n\xb2\
\x11A\x10k\x9e\xb0\x17n\x975\x1c\x10E\xba\xc4u\
\xdcn{\xeeH\x0c\xee\x07\x92\x08Y\x89 (\xc7\xf5\
\xcf\x89\xc1\x06\xef\x1a\xb8\xdd\xd7\x10:\xbb\xcd\x89\xea\x80\
szn\x8b\xd1\xc8\xf3{\xbdhHk\x0d\xf0\x8a\x90\
$\xc8N\x04\x1de?!\xc2\xe3\x09I\x18\xb7c\x9b\
\xdb\xb5\x0d\xb7\x10\xc6T\x1d#\x84m\xdb\xb6mw\x85\
\xcem\x1f\x1apl\x95O0\xfej\x86\xed/i\x91\
\xad\x08\x822<Fc\xf5\x94d\x86\xdb\xdd\xad\xfd\xc0\
\xe38\xb0\x03\x9cu\xbe\x11\x02\xe7D\xce\xddw;\xde\
9g\x97\x89\xd8\xb5\x03\xd1;\xc6\xeao\x9ad-\x82\
\x8e\x94\x8a&\x1a81t\x02\xe9\x03'r\xed\xf7k\
\x08\x8b\x1f\x89S\x0b\x11\x04\xa5+\x9c+\x8e\x97\x84\x90\
\xf1\x81\xfb\x9b\xe1\x04\xe8\xf4\x89b\xb79\x0bp\xa5.\
\x8e\xfd\xc5]l/\xd7\x10BFg\xb18\x1e\xa2\x00\
\xe6Am\x9c`\x95\xd2\x15\x22DN\xb2\x82L\x82\x81\
\xd0w\x8e\x95\xdf\xbc\xa8\xa5\x08:\x18\x22\x93\x01\xe1\xe4\
\xe7\x8c\xa9\xb5\x08:ri\xa7!\xde\x81\xf8!\xf4]\
`\xdbK\xbeP\x04+P\x0cI\x09\xc5\xafFP\x04\
;@1\xac-\x14\xbf\x1aB\x11\xec\x01\xc5\xb06P\
\xfcj\x0cEp\x00J1\xc4\xf1\x8c\x90\x9c\xa0\xf8\x11\
\x8a\xe00\xb0\x9a\x9c\x0dhuY`\x9f\x1f\x01\x14\xc1\
\x11(\xc5pJ\x18*\xa7\x04\x9c\xde\x99\xe2\xb8\xc0>\
?R\x85\x228&\xe5\xa6Oh\xbcF\xa8<!$\
6\x9a\xae\xaf8V\x18\xf2\x92NP\x04=R\x08\xe2\
\x9e\xe2\x06\xc7\xb7\xa4\xa6\xc3\x1a\x22\x01\xc2\x87Pw\x89\
\xc2G\xfaA\x11T\xa2\x22\x88t\x886P\xf8\xc8H\
P\x04\x0d(\x04qJZ\x82\x88\xd0\x99\x15f?@\
\xe8\xde)\x8e\x15i\xe5\xf9(|d$(\x82\xc6\x94\
\xe3\xff\xa7*\xc7#B\x06\x01\x22\x07\xb7\xb7\x22-\xd1\
k\x08!\x1e\xa0\x08\x06\xa6\x14E8\xc4)Y\x17\xc5\
\xba\xe7\x13!x7\xa4%x\xab\xd2*j4\x84\x10\
\x05(\x82\x11R\x11F'\x8e\xf88Wq\x84\xd8A\
\xe8\x1a\xe5\xed*\xf7\xe8%\x96P\x04\x13\xa2\x22\x8e\x13\
m\x07\x1e\xdf.q\x8a$D\x0e\xcen\xb5\xbcm\xc8\
\xba\xe0\xddb.\x8f\x84\x86\x22\x98\x19e#\xf7}\xb2\
.\x88\x13m\xb7\xedT\xbf\xb6\x1f\xb7\xca\xa3\xdbc\x8d\
\xca\xc7\x148\x92\x04\xff\x000H\x87\xfd\xc2`\x8f\x83\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x09S\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@@ -12131,10 +11549,6 @@ qt_resource_name = b"\
\x00\x1cX\x87\
\x00w\
\x00i\x00k\x00i\x00.\x00p\x00n\x00g\
\x00\x0f\
\x00\xb3\xceG\
\x00k\
\x00o\x00f\x00i\x00_\x00s\x00y\x00m\x00b\x00o\x00l\x00.\x00p\x00n\x00g\
\x00\x0e\
\x08\x9f\xcbG\
\x00f\
@@ -12185,7 +11599,7 @@ qt_resource_name = b"\
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00J\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00J\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1c\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00&\x00\x02\x00\x00\x00\x01\x00\x00\x00\x14\
\x00\x00\x00\x00\x00\x00\x00\x00\
@@ -12197,51 +11611,49 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x07\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xe4\x00\x00\x00\x00\x00\x01\x00\x02S.\
\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x02.\xed\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x02\xa7\xc8\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x02\x83\x87\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x02\x0e\x00\x00\x00\x00\x00\x01\x00\x02}\xcd\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02Y\x8c\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x02rj\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02N)\
\x00\x00\x01\x89\x89D9.\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x0c\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x01(\x97\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x011\xef\
\x00\x00\x01\x96\x16b\x1f\x99\
\x00\x00\x01\x94\x1a\xa2\xa2\x92\
\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01:\x05\
\x00\x00\x01\x88;p\xbcB\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x11\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02f\x00\x00\x00\x00\x00\x01\x00\x02\xda\x14\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xd3\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x028\x00\x00\x00\x00\x00\x01\x00\x02\xc4\x17\
\x00\x00\x02\x14\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd6\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x02N\x00\x00\x00\x00\x00\x01\x00\x02\xcdt\
\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x02\xa93\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x00X\x00\x02\x00\x00\x00\x08\x00\x00\x00\x15\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\x00\x00\x00\x15\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\
\x00\x00\x01\x88;p\xbcJ\
\x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\
\x00\x00\x01\x97\xc9|\x88\xde\
\x00\x00\x01V\x00\x00\x00\x00\x00\x01\x00\x01\x9d\x9a\
\x00\x00\x01\x88;p\xbcI\
\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x01\xf6n\
\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\
\x00\x00\x01\x94\xb4\xd4\xf0a\
\x00\x00\x01\x9e\x00\x00\x00\x00\x00\x01\x00\x01\xb1'\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\
\x00\x00\x01\x88;p\xbcF\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01?\xe9\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x01x\x00\x00\x00\x00\x00\x01\x00\x01\xa6\xf1\
\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x01\x82\xb0\
\x00\x00\x01\x88;p\xbcH\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1e\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x88;p\xbcH\

View File

@@ -3,7 +3,7 @@
################################################################################
## 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.1
##
## 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):
if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow")
mainWindow.resize(566, 573)
mainWindow.resize(482, 448)
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon)
@@ -35,14 +35,236 @@ class Ui_mainWindow(object):
self.gridLayout = QGridLayout(self.centralWidget)
self.gridLayout.setObjectName(u"gridLayout")
self.gridLayout.setContentsMargins(-1, -1, -1, 5)
self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList")
self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.optionWidget = QWidget(self.centralWidget)
self.optionWidget.setObjectName(u"optionWidget")
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
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.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
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.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
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.setObjectName(u"authorEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy)
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
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.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 6, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.horizontalLayout_3 = QHBoxLayout(self.croppingWidget)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.horizontalLayout_3.addWidget(self.croppingPowerLabel)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_3.addWidget(self.croppingPowerSlider)
self.gridLayout.addWidget(self.croppingWidget, 8, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
self.buttonWidget.setSizePolicy(sizePolicy1)
self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.directoryButton = QPushButton(self.buttonWidget)
self.directoryButton.setObjectName(u"directoryButton")
self.directoryButton.setMinimumSize(QSize(0, 30))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/folder_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.directoryButton.setIcon(icon1)
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))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon2)
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.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30))
font = QFont()
font.setBold(True)
self.convertButton.setFont(font)
icon3 = QIcon()
icon3.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon3)
self.gridLayout_4.addWidget(self.convertButton, 1, 2, 1, 1)
self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton")
self.clearButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
icon4.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon4)
self.gridLayout_4.addWidget(self.clearButton, 0, 2, 1, 1)
self.directoryButton.raise_()
self.clearButton.raise_()
self.fileButton.raise_()
self.deviceBox.raise_()
self.convertButton.raise_()
self.formatBox.raise_()
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
self.toolWidget = QWidget(self.centralWidget)
self.toolWidget.setObjectName(u"toolWidget")
@@ -52,115 +274,32 @@ class Ui_mainWindow(object):
self.editorButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton")
self.editorButton.setMinimumSize(QSize(0, 30))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon1)
icon5 = QIcon()
icon5.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon5)
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":/Other/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.setObjectName(u"wikiButton")
self.wikiButton.setMinimumSize(QSize(0, 30))
icon3 = QIcon()
icon3.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon3)
icon6 = QIcon()
icon6.addFile(u":/Other/icons/wiki.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.wikiButton.setIcon(icon6)
self.horizontalLayout.addWidget(self.wikiButton)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
self.buttonWidget.setSizePolicy(sizePolicy)
self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30))
font = QFont()
font.setBold(True)
self.convertButton.setFont(font)
icon4 = QIcon()
icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon4)
self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList")
self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.jobList.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton")
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.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 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.clearButton.raise_()
self.deviceBox.raise_()
self.convertButton.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.jobList, 2, 0, 1, 2)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
@@ -189,7 +328,7 @@ class Ui_mainWindow(object):
self.widthBox = QSpinBox(self.customWidget)
self.widthBox.setObjectName(u"widthBox")
self.widthBox.setMaximum(2400)
self.widthBox.setMaximum(2160)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
@@ -207,245 +346,7 @@ class Ui_mainWindow(object):
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.gridLayout_5 = QGridLayout(self.croppingWidget)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
self.preserveMarginLabel = QLabel(self.croppingWidget)
self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy1)
self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0)
self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
self.optionWidget = QWidget(self.centralWidget)
self.optionWidget.setObjectName(u"optionWidget")
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
sizePolicy3.setHorizontalStretch(0)
sizePolicy3.setVerticalStretch(0)
sizePolicy3.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy3)
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False)
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.comicinfoTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 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.setObjectName(u"deleteBox")
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.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.autoLevelBox = QCheckBox(self.optionWidget)
self.autoLevelBox.setObjectName(u"autoLevelBox")
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
self.chunkSizeWidget = QWidget(self.centralWidget)
self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
sizePolicy3.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
self.chunkSizeWidget.setSizePolicy(sizePolicy3)
self.chunkSizeWidget.setVisible(False)
self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.chunkSizeLabel = QLabel(self.chunkSizeWidget)
self.chunkSizeLabel.setObjectName(u"chunkSizeLabel")
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
sizePolicy4.setHorizontalStretch(0)
sizePolicy4.setVerticalStretch(0)
sizePolicy4.setHeightForWidth(self.chunkSizeLabel.sizePolicy().hasHeightForWidth())
self.chunkSizeLabel.setSizePolicy(sizePolicy4)
self.horizontalLayout_4.addWidget(self.chunkSizeLabel)
self.chunkSizeBox = QSpinBox(self.chunkSizeWidget)
self.chunkSizeBox.setObjectName(u"chunkSizeBox")
self.chunkSizeBox.setMinimum(100)
self.chunkSizeBox.setMaximum(600)
self.chunkSizeBox.setValue(400)
self.horizontalLayout_4.addWidget(self.chunkSizeBox)
self.chunkSizeWarnLabel = QLabel(self.chunkSizeWidget)
self.chunkSizeWarnLabel.setObjectName(u"chunkSizeWarnLabel")
sizePolicy4.setHeightForWidth(self.chunkSizeWarnLabel.sizePolicy().hasHeightForWidth())
self.chunkSizeWarnLabel.setSizePolicy(sizePolicy4)
self.horizontalLayout_4.addWidget(self.chunkSizeWarnLabel)
self.gridLayout.addWidget(self.chunkSizeWidget, 6, 0, 1, 1)
self.gridLayout.addWidget(self.customWidget, 7, 0, 1, 2)
mainWindow.setCentralWidget(self.centralWidget)
self.statusBar = QStatusBar(mainWindow)
@@ -453,7 +354,9 @@ class Ui_mainWindow(object):
self.statusBar.setSizeGripEnabled(False)
mainWindow.setStatusBar(self.statusBar)
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.formatBox, self.mangaBox)
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
@@ -464,23 +367,18 @@ class Ui_mainWindow(object):
QWidget.setTabOrder(self.gammaBox, self.borderBox)
QWidget.setTabOrder(self.borderBox, self.outputSplit)
QWidget.setTabOrder(self.outputSplit, self.colorBox)
QWidget.setTabOrder(self.colorBox, self.mozJpegBox)
QWidget.setTabOrder(self.colorBox, self.croppingBox)
QWidget.setTabOrder(self.croppingBox, self.mozJpegBox)
QWidget.setTabOrder(self.mozJpegBox, self.maximizeStrips)
QWidget.setTabOrder(self.maximizeStrips, self.croppingBox)
QWidget.setTabOrder(self.croppingBox, self.spreadShiftBox)
QWidget.setTabOrder(self.spreadShiftBox, self.deleteBox)
QWidget.setTabOrder(self.maximizeStrips, self.deleteBox)
QWidget.setTabOrder(self.deleteBox, self.disableProcessingBox)
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
QWidget.setTabOrder(self.disableProcessingBox, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.wikiButton)
QWidget.setTabOrder(self.wikiButton, self.jobList)
QWidget.setTabOrder(self.jobList, self.gammaSlider)
QWidget.setTabOrder(self.gammaSlider, self.widthBox)
QWidget.setTabOrder(self.widthBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
self.retranslateUi(mainWindow)
@@ -490,34 +388,107 @@ class Ui_mainWindow(object):
def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
#if QT_CONFIG(tooltip)
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.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)
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.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
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)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
#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))
#endif // QT_CONFIG(tooltip)
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)
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)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", 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.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><pre style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Do not process any image, ignore profile and processing options</pre></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#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))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", 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.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
#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))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", 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.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"Reduce Rainbow", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip)
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.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add directory", None))
#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))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file", 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)
#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))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add file(s)", None))
#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))
#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)
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)
#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)
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)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
#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))
#endif // QT_CONFIG(tooltip)
@@ -532,112 +503,5 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip)
self.heightBox.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)
#if QT_CONFIG(tooltip)
self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>After calculating the cropping boundaries, &quot;back up&quot; a specified percentage amount.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", 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.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)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left 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 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.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 &quot;Chunk size MB&quot; 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.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None))
#endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo 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)
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)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#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))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#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))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", 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.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
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)
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)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#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))
#endif // QT_CONFIG(tooltip)
self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Aggressive Black Point", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#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))
#endif // QT_CONFIG(tooltip)
self.chunkSizeLabel.setText(QCoreApplication.translate("mainWindow", u"Chunk size MB:", None))
self.chunkSizeWarnLabel.setText(QCoreApplication.translate("mainWindow", u"Greater than default may cause performance issues on older ereaders.", None))
# retranslateUi

View File

@@ -3,7 +3,7 @@
################################################################################
## 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.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

View File

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

View File

@@ -28,27 +28,23 @@ from copy import copy
from glob import glob, escape
from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool, cpu_count
from multiprocessing import Pool
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 PIL import Image, ImageFile
from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError
from subprocess import STDOUT, PIPE
from psutil import virtual_memory, disk_usage
from html import escape as hescape
import pymupdf
import numpy as np
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from .shared import available_archive_tools, md5Checksum, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from . import comic2panel
from . import image
from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -81,15 +77,15 @@ def main(argv=None):
return 0
def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
key = pathlib.Path(imgfilepath).name
def buildHTML(path, imgfile, imgfilepath):
imgfilepath = md5Checksum(imgfilepath)
filename = getImageFileName(imgfile)
deviceres = options.profileData[1]
if not options.noprocessing and "Rotated" in options.imgMetadata[key]:
if not options.noprocessing and "Rotated" in options.imgMetadata[imgfilepath]:
rotatedPage = True
else:
rotatedPage = False
if not options.noprocessing and "BlackBackground" in options.imgMetadata[key]:
if not options.noprocessing and "BlackBackground" in options.imgMetadata[imgfilepath]:
additionalStyle = 'background-color:#000000;'
else:
additionalStyle = ''
@@ -107,13 +103,10 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
os.makedirs(htmlpath)
htmlfile = os.path.join(htmlpath, filename[0] + '.xhtml')
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:
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.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<!DOCTYPE html>\n",
@@ -122,17 +115,12 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
"<title>", hescape(filename[0]), "</title>\n",
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"<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",
"<body style=\"" + additionalStyle + "\">\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
'<div style="display:none;">.</div>\n',
])
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
if imgfile2:
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
f.write("</div>\n")
"<img width=\"" + str(imgsizeframe[0]) + "\" height=\"" + str(imgsizeframe[1]) + "\" ",
"src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n</div>\n"])
if options.iskindle and options.panelview:
if options.autoscale:
size = (getPanelViewResolution(imgsize, deviceres))
@@ -228,7 +216,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
navID = folder.replace('/', '_').replace('\\', '_')
if options.comicinfo_chapters:
if options.chapters:
title = chapternames[chapter[1]]
navID = filename[0].replace('/', '_').replace('\\', '_')
elif os.path.basename(folder) != "Text":
@@ -256,7 +244,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.comicinfo_chapters:
if options.chapters:
title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)]
@@ -268,7 +256,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.comicinfo_chapters:
if options.chapters:
title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)]
@@ -295,9 +283,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
if len(options.summary) > 0:
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
f.writelines(["<dc:description>", options.summary, "</dc:description>\n"])
for author in options.authors:
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
f.writelines(["<dc:creator>", author, "</dc:creator>\n"])
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
"<meta name=\"cover\" content=\"cover\"/>\n"])
if options.iskindle and options.profile != 'Custom':
@@ -324,8 +312,12 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<item id=\"nav\" href=\"nav.xhtml\" ",
"properties=\"nav\" media-type=\"application/xhtml+xml\"/>\n"])
if cover is not None:
mt = 'image/jpeg'
f.write("<item id=\"cover\" href=\"Images/cover.jpg" + "\" media-type=\"" + mt +
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
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")
reflist = []
for path in filelist:
@@ -338,16 +330,10 @@ def buildOPF(dstdir, title, filelist, cover=None):
".xhtml\" media-type=\"application/xhtml+xml\"/>\n")
if '.png' == filename[1]:
mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
else:
mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
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")
@@ -370,63 +356,63 @@ def buildOPF(dstdir, title, filelist, cover=None):
pageside = "left"
else:
pageside = "right"
# initial spread order forwards
page_spread_property_list = []
for entry in reflist:
if options.righttoleft:
if "-kcc-a" in entry or "-kcc-d" in entry:
page_spread_property_list.append("center")
if entry.endswith("-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
pageside = "right"
elif "-kcc-b" in entry:
page_spread_property_list.append("right")
elif entry.endswith("-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "right"
elif "-kcc-c" in entry:
page_spread_property_list.append("left")
elif entry.endswith("-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "right"
else:
page_spread_property_list.append(pageside)
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
if pageside == "right":
pageside = "left"
else:
pageside = "right"
else:
if "-kcc-a" in entry or "-kcc-d" in entry:
page_spread_property_list.append("center")
if entry.endswith("-a"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center"))
)
pageside = "left"
elif "-kcc-b" in entry:
page_spread_property_list.append("left")
elif entry.endswith("-b"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left"))
)
pageside = "left"
elif "-kcc-c" in entry:
page_spread_property_list.append("right")
elif entry.endswith("-c"):
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right"))
)
pageside = "left"
else:
page_spread_property_list.append(pageside)
f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty(pageside))
)
if pageside == "right":
pageside = "left"
else:
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.close()
os.mkdir(os.path.join(dstdir, 'META-INF'))
@@ -439,9 +425,11 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"])
f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len_tomes=0):
def buildEPUB(path, chapternames, tomenumber, ischunked):
filelist = []
chapterlist = []
cover = None
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8')
f.writelines(["@page {\n",
@@ -451,14 +439,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
"display: block;\n",
"margin: 0;\n",
"padding: 0;\n",
"}\n",
])
if options.kindle_scribe_azw3:
f.writelines([
"img {\n",
"display: block;\n",
"}\n",
])
"}\n"])
if options.iskindle and options.panelview:
f.writelines(["#PV {\n",
"position: absolute;\n",
@@ -525,32 +506,21 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
"display: none;\n",
"}\n"])
f.close()
build_html_start = perf_counter()
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False
dirnames, filenames = walkSort(dirnames, filenames)
for afile in filenames:
if afile == 'cover.jpg':
continue
if 'below' in afile:
continue
if cover is None:
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
'cover' + getImageFileName(afile)[1])
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
tomenumber), options.uuid))
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), afile))
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
chapter = True
if 'above' in 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()
print(f"buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
if ischunked:
options.comicinfo_chapters = []
if not chapternames and options.comicinfo_chapters:
if not chapternames and options.chapters and not ischunked:
chapterlist = []
global_diff = 0
@@ -563,7 +533,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
elif options.splitter == 2:
diff_delta = 2
for aChapter in options.comicinfo_chapters:
for aChapter in options.chapters:
pageid = aChapter[0]
cur_diff = global_diff
global_diff = 0
@@ -586,6 +556,7 @@ def imgDirectoryProcessing(path):
workerPool = Pool(maxtasksperchild=100)
workerOutput = []
options.imgMetadata = {}
options.imgOld = []
work = []
pagenumber = 0
for dirpath, _, filenames in os.walk(path):
@@ -595,23 +566,19 @@ def imgDirectoryProcessing(path):
if GUI:
GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0:
img_processing_start = perf_counter()
for i in work:
workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick)
workerPool.close()
workerPool.join()
img_processing_end = perf_counter()
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
# macOS 15 likes to add ._ files after multiprocessing
dot_clean(path)
if GUI and not GUI.conversionAlive:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
for file in options.imgOld:
if os.path.isfile(file):
os.remove(file)
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.")
@@ -625,6 +592,7 @@ def imgFileProcessingTick(output):
for page in output:
if page is not None:
options.imgMetadata[page[0]] = page[1]
options.imgOld.append(page[2])
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI.conversionAlive:
@@ -640,162 +608,23 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
if opt.cropping > 0 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.gammaCorrectImage()
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.eraserainbow, is_color)
if is_color:
pass
elif opt.forcepng:
img.convertToGrayscale()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor:
img.quantizeImage()
else:
img.convertToGrayscale()
output.append(img.saveToDir())
return output
except Exception:
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)
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
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):
if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
@@ -814,26 +643,19 @@ def getWorkFolder(afile):
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
path = workdir
pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract()
workdir = path
sanitizePermissions(path)
target_height = options.profileData[1][1]
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:
if njpg == 0:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
raise UserWarning("Failed to extract images from PDF file.")
else:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
@@ -843,8 +665,7 @@ def getWorkFolder(afile):
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
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:
rmtree(workdir, True)
raise UserWarning(e)
@@ -892,7 +713,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
def getComicInfo(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml')
options.comicinfo_chapters = []
options.chapters = []
options.summary = ''
titleSuffix = ''
if options.title == 'defaulttitle':
@@ -915,11 +736,11 @@ def getComicInfo(path, originalpath):
except Exception:
os.remove(xmlPath)
return
if options.comicinfotitle:
options.title = xml.data['Title']
if xml.data['Title']:
options.title = hescape(xml.data['Title'])
elif defaultTitle:
if xml.data['Series']:
options.title = xml.data['Series']
options.title = hescape(xml.data['Series'])
if xml.data['Volume']:
titleSuffix += ' V' + xml.data['Volume'].zfill(2)
if xml.data['Number']:
@@ -929,16 +750,16 @@ def getComicInfo(path, originalpath):
options.authors = []
for field in ['Writers', 'Pencillers', 'Inkers', 'Colorists']:
for person in xml.data[field]:
options.authors.append(person)
options.authors.append(hescape(person))
if len(options.authors) > 0:
options.authors = list(set(options.authors))
options.authors.sort()
else:
options.authors = ['KCC']
if xml.data['Bookmarks']:
options.comicinfo_chapters = xml.data['Bookmarks']
options.chapters = xml.data['Bookmarks']
if xml.data['Summary']:
options.summary = xml.data['Summary']
options.summary = hescape(xml.data['Summary'])
os.remove(xmlPath)
@@ -967,51 +788,30 @@ def getPanelViewSize(deviceres, size):
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):
chapterNames = {}
page = 1
cover_path = None
for root, dirs, files in os.walk(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY)
for name in files:
_, ext = getImageFileName(name)
splitname = os.path.splitext(name)
# 9999 page limit
unique_name = f'kcc-{page:04}'
# file needs kcc at front AND back to avoid renaming issues
slugified = f'kcc-{page:04}'
page += 1
for suffix in '-KCC', '-KCC-A', '-KCC-B', '-KCC-C':
if splitname[0].endswith(suffix):
slugified += suffix.lower()
break
newKey = os.path.join(root, unique_name + ext)
newKey = os.path.join(root, slugified + splitname[1])
key = os.path.join(root, name)
if 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):
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():
slugified += "A"
chapterNames[slugified] = tmpName
@@ -1020,15 +820,7 @@ def sanitizeTree(filetree):
if key != newKey:
os.replace(key, newKey)
dirs[i] = newKey
return chapterNames, cover_path
def flattenTree(filetree):
for root, dirs, files in os.walk(filetree, topdown=False):
for name in files:
os.rename(os.path.join(root, name), os.path.join(filetree, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
return chapterNames
def sanitizePermissions(filetree):
@@ -1037,30 +829,16 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
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):
level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files:
# Windows MAX_LENGTH = 260 plus some buffer
if len(os.path.join(root, f)) > 180:
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
break
if getImageFileName(f):
newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep)
if level != -1 and level != newLevel:
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
level = 0
break
else:
level = newLevel
@@ -1087,23 +865,11 @@ def chunk_process(path, mode, parent):
targetSize = 419430400
if options.batchsplit == 2 and mode == 2:
mode = 3
if options.batchsplit == 1 and mode == 2:
with os.scandir(path) as it:
for entry in it:
if not entry.name.startswith('.') and entry.is_dir():
if getDirectorySize(os.path.join(path, entry)) > targetSize:
flattenTree(path)
mode = 1
break
if mode < 3:
for root, dirs, files in walkLevel(path, 0):
for name in files if mode == 1 else dirs:
size = 0
if mode == 1:
if 'below' not in 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')))
if mode == 1:
size = os.path.getsize(os.path.join(root, name))
else:
size = getDirectorySize(os.path.join(root, name))
if currentSize + size > targetSize:
@@ -1133,7 +899,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
for root, _, files in os.walk(tmppath, False):
for name in files:
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
path = os.path.join(root, name)
pathOrg = orgpath + path.split('OEBPS' + os.path.sep + 'Images')[1]
@@ -1153,8 +919,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err)))
else:
try:
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
if alreadyProcessed:
@@ -1179,27 +944,21 @@ def createNewTome(parent):
return tomePath, tomePathRoot
def slugify(value, is_natural_sorted):
if options.format == 'CBZ' and is_natural_sorted:
return value
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))
def slugify(value):
value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value
def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools():
if '7z' in available_archive_tools():
if isepub:
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
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:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub:
@@ -1248,8 +1007,6 @@ def makeParser():
help="Output generated file to specified directory or file")
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Comic title [Default=filename or directory name]")
output_options.add_argument("--comicinfotitle", action="store_true", dest="comicinfotitle", default=False,
help="Write Title from ComicInfo.xml")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
@@ -1264,8 +1021,6 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--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,
help="Do not modify image and ignore any profil or processing option")
@@ -1277,14 +1032,10 @@ def makeParser():
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",
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",
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",
help="Set cropping power [Default=1.0]")
processing_options.add_argument("--preservemargin", type=int, dest="preservemargin", default="0",
help="After calculating crop, back up specified percentage amount. [Default=0]")
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]")
processing_options.add_argument("--ipc", "--interpanelcrop", type=int, dest="interpanelcrop", default="0",
@@ -1295,8 +1046,8 @@ def makeParser():
help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale")
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False,
help="Reduce rainbow effect on color eink by slightly blurring images.")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
@@ -1328,12 +1079,6 @@ def checkOptions(options):
options.format = 'EPUB'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'MOBI+EPUB-200MB':
options.keep_epub = True
options.targetsize = 195
options.format = 'MOBI'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'MOBI+EPUB':
options.keep_epub = True
options.format = 'MOBI'
@@ -1360,8 +1105,6 @@ def checkOptions(options):
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K34' or options.profile == 'KDX':
options.panelview = False
options.hq = False
if not options.hq and not options.autoscale:
options.panelview = False
# Webtoon mode mandatory options
if options.webtoon:
options.panelview = False
@@ -1403,13 +1146,13 @@ def checkTools(source):
source = source.upper()
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
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!')
sys.exit(1)
if options.format == 'MOBI':
try:
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
except (FileNotFoundError, CalledProcessError):
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT)
except FileNotFoundError:
print('ERROR: KindleGen is missing!')
sys.exit(1)
@@ -1432,40 +1175,6 @@ def checkPre(source):
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):
start = perf_counter()
global GUI
@@ -1474,17 +1183,12 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
checkPre(source)
print("Preparing source images...")
path = getWorkFolder(source)
print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
cover = image.Cover(cover_path, options)
if options.webtoon:
y = image.ProfileData.Profiles[options.profile][1][1]
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
@@ -1497,6 +1201,7 @@ def makeBook(source, qtgui=None):
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI:
GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit > 0:
tomes = chunk_directory(path)
else:
@@ -1530,10 +1235,10 @@ def makeBook(source, qtgui=None):
else:
print("Creating EPUB file...")
if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True, cover, len(tomes))
buildEPUB(tome, chapterNames, tomeNumber, True)
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover)
buildEPUB(tome, chapterNames, tomeNumber, False)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
copyfile(tome + '_comic.zip', filepath[-1])
@@ -1609,30 +1314,27 @@ def makeMOBIWorker(item):
try:
if os.path.getsize(item) < 629145600:
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
for line in output.stdout.splitlines():
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
break
else:
# ERROR: EPUB too big
kindlegenErrorCode = 23026
return [kindlegenErrorCode, kindlegenError, item]
except CalledProcessError as err:
for line in err.stdout.splitlines():
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
return [0, '', item]
if ":I1037: Mobi file built with WARNINGS!" in line:
return [0, '', item]
except Exception as err:
# ERROR: KCC unknown generic error
if kindlegenErrorCode == 0:
kindlegenErrorCode = err.returncode
kindlegenError = err.stdout
kindlegenErrorCode = 1
kindlegenError = format(err)
return [kindlegenErrorCode, kindlegenError, item]

View File

@@ -181,7 +181,7 @@ def splitImage(work):
panelImg = imgOrg.crop((0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1]))
newPage.paste(panelImg, (0, targetHeight))
targetHeight += panelsProcessed[panel][2]
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber).zfill(4) + '.png'), 'PNG')
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG')
pageNumber += 1
os.remove(filePath)
except Exception:

View File

@@ -18,7 +18,7 @@
# PERFORMANCE OF THIS SOFTWARE.
#
from functools import cached_property, lru_cache
from functools import cached_property
import os
import platform
import distro
@@ -28,7 +28,6 @@ from xml.parsers.expat import ExpatError
from .shared import subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
class ComicArchive:
@@ -40,7 +39,7 @@ class ComicArchive:
@cached_property
def type(self):
extraction_commands = [
[SEVENZIP, 'l', '-y', '-p1', self.filepath],
['7z', 'l', '-y', '-p1', self.filepath],
]
if distro.id() == 'fedora' or distro.like() == 'fedora':
@@ -69,12 +68,12 @@ class ComicArchive:
extraction_commands = [
['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':
extraction_commands.append(
['unar', self.filepath, '-D', '-f', '-o', targetdir]
['unar', self.filepath, '-f', '-o', targetdir]
)
extraction_commands.reverse()
@@ -87,7 +86,7 @@ class ComicArchive:
for cmd in extraction_commands:
try:
subprocess_run(cmd, capture_output=True, check=True)
return targetdir
return targetdir
except FileNotFoundError:
missing.append(cmd[0])
except CalledProcessError:
@@ -101,13 +100,13 @@ class ComicArchive:
def addFile(self, sourcefile):
if self.type in ['RAR', 'RAR5']:
raise NotImplementedError
process = subprocess_run([SEVENZIP, 'a', '-y', self.filepath, sourcefile],
process = subprocess_run(['7z', 'a', '-y', self.filepath, sourcefile],
stdout=PIPE, stderr=STDOUT)
if process.returncode != 0:
raise OSError('Failed to add the file.')
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)
if process.returncode != 0:
raise OSError(EXTRACTION_ERROR)
@@ -115,16 +114,3 @@ class ComicArchive:
return parseString(process.stdout)
except ExpatError:
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

View File

@@ -20,13 +20,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from .shared import md5Checksum
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
@@ -89,14 +85,12 @@ class ProfileData:
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), 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),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
}
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),
'K11': ("Kindle 11", (1072, 1448), 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.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -183,13 +179,13 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0))
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 \
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
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:
if self.opt.splitter != 1:
if width > height:
@@ -204,15 +200,38 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
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:
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):
if self.opt.bordersColor:
@@ -254,166 +273,121 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, fill):
def __init__(self, options, mode, path, image, color, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
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.original_color_mode = image.mode
self.image = image.convert("RGB")
self.image = image
self.color = color
self.fill = fill
self.rotated = False
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:
self.targetPathOrder = '-kcc-x'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC'
elif 'R' in mode:
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
if not options.norotate:
self.rotated = True
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A'
self.rotated = True
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:
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
if not hasattr(Image, 'Resampling'):
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):
try:
flags = []
if not self.opt.forcecolor and not self.opt.forcepng:
self.image = self.image.convert('L')
if self.rotated:
flags.append('Rotated')
if self.fill != 'white':
flags.append('BlackBackground')
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
w, h = self.image.size
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
elif self.opt.kindle_scribe_azw3:
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
if self.opt.forcepng:
self.image.info["transparency"] = None
self.targetPath += '.png'
self.image.save(self.targetPath, 'PNG', optimize=1)
else:
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
return [Path(targetPath).name, flags]
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)
return [md5Checksum(self.targetPath), flags, self.orgPath]
except IOError as err:
raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath):
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):
def autocontrastImage(self):
gamma = self.opt.gamma
if gamma < 0.1:
gamma = self.gamma
if self.gamma != 1.0 and self.color:
gamma = 1.0
if gamma == 1.0:
pass
self.image = ImageOps.autocontrast(self.image)
else:
self.image = 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')
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
def quantizeImage(self):
# remove all color pixels from image, since colorCheck() has some tolerance
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.image = self.image.convert("RGB")
colors = len(self.palette) // 3
if colors < 256:
self.palette += self.palette[:3] * (256 - colors)
palImg = Image.new('P', (1, 1))
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)
def optimizeForDisplay(self, eraserainbow, is_color):
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
if eraserainbow:
self.image = erase_rainbow_artifacts(self.image, is_color)
def optimizeForDisplay(self, reducerainbow):
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array
if reducerainbow and not self.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):
# 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_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()
if self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
if self.opt.format == 'CBZ' or self.opt.kfx:
borderw = int((self.size[0] - self.image.size[0]) / 2)
borderh = int((self.size[1] - self.image.size[1]) / 2)
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill)
if self.image.size[0] != self.size[0] or self.image.size[1] != self.size[1]:
self.image = ImageOps.fit(self.image, self.size, method=method)
else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders:
elif self.opt.format == 'CBZ' or self.opt.kfx:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else:
if self.kindle_scribe_azw3:
self.size = (1860, 1920)
self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self):
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
return Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
def maybeCrop(self, box, minimum):
w, h = self.image.size
left, upper, right, lower = box
if self.opt.preservemargin:
ratio = 1 - self.opt.preservemargin / 100
box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio)
box_area = (box[2] - box[0]) * (box[3] - box[1])
image_area = self.image.size[0] * self.image.size[1]
if (box_area / image_area) >= minimum:
@@ -435,9 +409,14 @@ class ComicPage:
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
class Cover:
def __init__(self, source, opt):
def __init__(self, source, target, opt, tomeid):
self.options = opt
self.source = source
self.target = target
if tomeid == 0:
self.tomeid = 1
else:
self.tomeid = tomeid
self.image = Image.open(source)
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -449,52 +428,17 @@ class Cover:
self.image = ImageOps.autocontrast(self.image)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.crop_main_cover()
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
self.save()
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
if w / h > 2:
if self.options.righttoleft:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.3:
if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
def save_to_epub(self, target, tomeid, len_tomes=0):
def save(self):
try:
if tomeid == 0:
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)
dot_cover = Path(target).with_stem('._' + Path(target).stem)
if os.path.exists(dot_cover):
os.remove(dot_cover)
self.image.save(self.target, "JPEG", optimize=1, quality=85)
except IOError:
raise RuntimeError('Failed to save cover.')
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:
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)

View File

@@ -20,7 +20,6 @@ import os
from xml.dom.minidom import parse, Document
from tempfile import mkdtemp
from shutil import rmtree
from xml.sax.saxutils import unescape
from . import comicarchive
@@ -53,19 +52,19 @@ class MetadataParser:
def parseXML(self):
if len(self.rawdata.getElementsByTagName('Series')) != 0:
self.data['Series'] = unescape(self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue)
self.data['Series'] = self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Volume')) != 0:
self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Number')) != 0:
self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Summary')) != 0:
self.data['Summary'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue)
self.data['Summary'] = self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Title')) != 0:
self.data['Title'] = unescape(self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue)
self.data['Title'] = self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue
for field in ['Writer', 'Penciller', 'Inker', 'Colorist']:
if len(self.rawdata.getElementsByTagName(field)) != 0:
for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '):
self.data[field + 's'].append(unescape(person))
self.data[field + 's'].append(person)
self.data[field + 's'] = list(set(self.data[field + 's']))
self.data[field + 's'].sort()
if len(self.rawdata.getElementsByTagName('Page')) != 0:

View 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

View File

@@ -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

View File

@@ -18,7 +18,9 @@
# PERFORMANCE OF THIS SOFTWARE.
#
from functools import lru_cache
import os
from hashlib import md5
from html.parser import HTMLParser
import subprocess
from packaging.version import Version
@@ -48,6 +50,8 @@ class HTMLStripper(HTMLParser):
def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile)
ext = ext.lower()
if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None
return [name, ext]
@@ -71,6 +75,16 @@ def walkLevel(some_dir, level=1):
del dirs[:]
def md5Checksum(fpath):
with open(fpath, 'rb') as fh:
m = md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
return m.hexdigest()
def sanitizeTrace(traceback):
return ''.join(format_tb(traceback))\
@@ -116,20 +130,27 @@ def dependencyCheck(level):
missing.append('python-slugify 1.2.1+')
try:
from PIL import __version__ as pillowVersion
if Version('11.3.0') > Version(pillowVersion):
missing.append('Pillow 11.3.0+')
if Version('5.2.0') > Version(pillowVersion):
missing.append('Pillow 5.2.0+')
except ImportError:
missing.append('Pillow 11.3.0+')
try:
from pymupdf import __version__ as pymupdfVersion
if Version('1.26.1') > Version(pymupdfVersion):
missing.append('PyMuPDF 1.26.1+')
except ImportError:
missing.append('PyMuPDF 1.26.1+')
missing.append('Pillow 5.2.0+')
if len(missing) > 0:
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
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):
if (os.name == 'nt'):
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)

View File

@@ -1,12 +1,11 @@
PySide6>=6.5.1
Pillow>=11.3.0
Pillow>=5.2.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
mozjpeg-lossless-optimization>=1.1.2
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4
PyMuPDF>=1.26.1
numpy>=1.22.4,<2.0.0

View File

@@ -75,7 +75,7 @@ setuptools.setup(
packages=['kindlecomicconverter'],
install_requires=[
'pyside6>=6.5.1',
'Pillow>=11.3.0',
'Pillow>=5.2.0',
'psutil>=5.9.5',
'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
@@ -83,8 +83,7 @@ setuptools.setup(
'mozjpeg-lossless-optimization>=1.1.2',
'natsort>=8.4.0',
'distro',
'numpy>=1.22.4',
'PyMuPDF>=1.26.1',
'numpy>=1.22.4,<2.0.0'
],
classifiers=[],
zip_safe=False,