1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 13:38:46 +00:00

Compare commits

...

48 Commits

Author SHA1 Message Date
Alex Xu
271a129537 bump 7.3.3 2025-03-12 19:10:29 -07:00
Alex Xu
23099cee81 fix landscape alignment (#865) 2025-03-12 19:09:58 -07:00
simesherbs
b957fcf3fe "Disable processing" tool tip font fix (#864)
* initial fix

* applied fix to appropriate file

* changed .ui file and recompiled

* final fixes
2025-03-12 18:46:30 -07:00
Alex Xu
187475a424 add SignPath to Windows GUI workflow (#862) 2025-03-11 15:41:12 -07:00
Alex Xu
88fd54e2ba bump 7.3.2 2025-03-11 15:30:02 -07:00
Alex Xu
b23b67bbbe revert build windows gui with docker (#861) 2025-03-11 15:29:33 -07:00
Alex Xu
9992d895cf fix comicrack large files again (#860) 2025-03-11 15:07:48 -07:00
Alex Xu
561951a349 restore imgMetadata dict (#858)
* Revert "memory optimization: store metadata in filenames, not a global dict (…"

This reverts commit 9a2a09eab9.

* only remove imgOld
2025-03-11 09:57:11 -07:00
Alex Xu
b8b7926366 use release-signing 2025-03-10 09:04:55 -07:00
Alex Xu
92f3308e1c bump to 7.3.1 2025-03-10 08:59:18 -07:00
Alex Xu
9e87ccef4e Free code signing on Windows provided by SignPath.io, certificate by SignPath Foundation 2025-03-10 08:58:37 -07:00
Alex Xu
9a2a09eab9 memory optimization: store metadata in filenames, not a global dict (#853)
* remove options.imgMetadata and store metadata in filename

* make kcc much more memory efficient by removing imgMetadata and imgOld dicts
2025-03-09 16:52:07 -07:00
Alex Xu
88cf2fd21f fix sanitize-first bug 2025-03-09 16:50:15 -07:00
Vinh Quang Tran
7a3ed262b1 fix: disabled overriding title (#857) 2025-03-09 16:22:34 -07:00
Alex Xu
9e204aad76 rename images before processing to fix splitting issues (#852)
* rename images before processing to fix splitting issues

* import pathlib

* 9999 page limit
2025-03-07 07:59:04 -08:00
Alex Xu
e1f9d12676 add test-signing 2025-03-06 14:08:48 -08:00
Ciro Mattia Gonano
24ab72fcbc Update LICENSE.txt to reflect Alex is current main maintainer 2025-03-06 12:06:55 +01:00
Ciro Mattia Gonano
4af6a75874 Update LICENSE.txt to reflect Alex is current main maintainer 2025-03-06 12:06:08 +01:00
Alex Xu
28b6188a3f Delete appveyor.yml 2025-03-05 19:03:14 -08:00
iwa
f000af1207 Delete .travis.yml (#849)
* Delete .travis.yml

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-03-05 13:13:59 -08:00
Alex Xu
299f916580 add screenshot 2025-03-04 19:59:54 -08:00
Alex Xu
c39e403595 add screenshot 2025-03-04 19:58:24 -08:00
Alex Xu
e787dd2897 bump 7.3.0 2025-03-04 11:29:44 -08:00
Alex Xu
01625904d1 huge speed optimization on HDD by removing md5 (#845)
* Eliminate unnecessary use of MD5 checksum

md5checksum() computes the actual checksum of a specified file, which is appropriately expensive, but the code seemed to be using the checksum result as a key into the imgMetadata dictionary to avoid handling image files being renamed during processing steps. This seems like a very expensive way to handle the rename so instead, I now update the imgMetadata keys with the new filename in the one place that the rename happens, and avoid MD5 checksums entirely.

* merge conflicts

* Add missing handling for image path renames due to nested chapter folder name

* merge conflicts

* merge conflicts

* add perf_counters

* imgFileProcessing perf_counter

* use startswith and removeprefix

---------

Co-authored-by: utopiafallen <utopiafallen@gmail.com>
2025-03-04 11:28:23 -08:00
Alex Xu
5f8526da44 all image files have unique ordered names (#848) 2025-03-03 07:50:21 -08:00
Alex Xu
1159e737a0 small correction to 7z (#847) 2025-03-02 19:55:45 -08:00
Alex Xu
5bbdb715e9 7z is faster note 2025-03-02 12:39:51 -08:00
Alex Xu
1a3cd6c916 add perf_counter to makeBook (#846) 2025-03-02 12:24:49 -08:00
Alex Xu
e1e6d587f4 makeZIP now prefers 7z for SPEED (#844) 2025-03-02 11:08:27 -08:00
Alex Xu
ca5c0bdd61 fix error messages that only say a single number (#843) 2025-03-01 19:40:55 -08:00
Alex Xu
c6f491d27e bump 7.2.3 2025-02-28 20:44:45 -08:00
Alex Xu
c9ed3feef1 deprioritize tar (#842) 2025-02-28 20:17:41 -08:00
Alex Xu
be147fe7e5 saves several seconds per file (#841) 2025-02-28 20:11:11 -08:00
utopiafallen
62ffa2bc80 Improve processing performance by partially undoing "Refactored detection of corrupted files"
Commit f952634971 moved image corruption detection out from the ComicPage constructor and into a standalone detectCorruption() function. This led to a performance regression because now corruption detection happens in a single thread when it used to be distributed across worker threads, and because a source image is now loaded twice in memory: once during corruption detection and once when actually going to process the image.

Image file corruption detection is now back inside the ComicPage constructor and the extra load() has been removed because the convert() call will automatically invoke load() and most likely throw the same exceptions.
2025-02-28 19:49:13 -08:00
Alex Xu
11186d07c0 fix file splitting/chunking for real in certain situations (#839)
* fix file splitting without ComicInfo.xml

* remove dead var
2025-02-28 19:19:11 -08:00
Alex Xu
4b3cd6882a Revert "Build windows GUI version normally without docker" (#835)
* Revert "remove GUI windows docker"

This reverts commit 4fc5cc9dfb.

* build windows gui version with docker

* cffi

* bump 7.2.2
2025-02-25 08:57:40 -08:00
Alex Xu
b35a2baf05 bump 7.2.1 2025-02-20 18:24:53 -08:00
Alex Xu
11a395e983 fix pdf workdir (#830) 2025-02-20 18:24:06 -08:00
Alex Xu
2e39a8c227 add jpeg2000 (#828) 2025-02-19 16:29:26 -08:00
Alex Xu
02535421a0 rename batch to chunk (#826) 2025-02-12 15:31:40 -08:00
Alex Xu
3d4fae62d8 reduce rainbow checkbox (#824) 2025-02-09 19:10:56 -08:00
Alex Xu
2b550b8b98 bump to 7.2.0 2025-02-06 14:08:11 -08:00
Alex Xu
ecee7cf6f5 don't sanitize twice 2025-02-06 14:07:09 -08:00
Alex Xu
b0a5558da1 write temp files next to source instead of on main ssd (#820)
* extract to same folder

* rename split to batch

* write temp files to source
2025-01-30 12:42:02 -08:00
Alex Xu
1b487c18d6 with certain file structures: fix large file chunking, ComicInfo.xml, permissions (#819)
* fix nested folder extraction

* add comicinfo.xml handling

* sanitize

* add error handling

* space
2025-01-30 11:31:33 -08:00
Alex Xu
a3546d19c3 add build_binary commands to readme (#818) 2025-01-29 19:14:08 -08:00
X5Games / Neyney10
2f703ef92c Inter-panel cropping method. (#810)
* Inter-panel cropping method.

* 1. Save interpanelcrop option.
2. Update readme with the the new interpanelcrop argument.
3. Add a tooltip to the inter-panel crop box.
2025-01-27 13:44:23 -08:00
Alex Xu
4fb993b38b add example new checkbox PR (#815) 2025-01-27 13:42:06 -08:00
20 changed files with 624 additions and 419 deletions

View File

@@ -40,11 +40,25 @@ jobs:
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g") version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${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 mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
- name: upload build
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: windows-build name: windows-build
path: dist/windows/*.exe path: dist/windows/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.1
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/signed/'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@@ -54,4 +68,4 @@ jobs:
files: | files: |
CHANGELOG.md CHANGELOG.md
LICENSE.txt LICENSE.txt
dist/windows/*.exe dist/windows/signed/*.exe

View File

@@ -41,11 +41,22 @@ jobs:
- name: build binary - name: build binary
run: | run: |
python setup.py build_binary python setup.py build_binary
- name: upload build - name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: windows-build name: windows-build
path: dist/*.exe path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.1
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/signed/'
- name: Release - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@@ -55,4 +66,4 @@ jobs:
files: | files: |
CHANGELOG.md CHANGELOG.md
LICENSE.txt LICENSE.txt
dist/*.exe dist/windows/signed/*.exe

View File

@@ -1,30 +0,0 @@
matrix:
include:
- os: osx
language: generic
osx_image: xcode11.1
before_install:
- pip3 install --upgrade pip setuptools wheel
install:
- pip3 install -r requirements.txt
- pip3 install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip
- npm install -g appdmg
script: python3 setup.py build_binary
before_deploy:
- shopt -s extglob
- rm -r dist/!(*.deb|*.dmg)
deploy:
provider: gcs
access_key_id: GOOG1EC62457RKUYFR2TIZUWV4EFSV2EP5LVLPPFXUAKADWJFDYPFW63BQSLA
secret_access_key:
secure: sxYjeho7U3im0Ezf6cz6TjYDiLvf0kAM2ETQHYoFNbD1VVvhJJyymDCnPH80zpFKmhc1MWTB6ndwsrPfcyZDLR2meSdWGPjZfFPY3RcrfImndKi7ln+mYQDBQ7W1lGit4YcH3Ju7LHceaTbRA7fVTX8pWKOcbXL2oM+lQxTJHH32+crVma+ChhbjzTWsSLRoakt3Nhiveec5p/qSW7AFe4Zq+b3C85IgwjSJI/xVwzaWrs6p915h1zZi7KL7YCMIxfQFrvRPFR2KTbh/DoLCCrqfbD4qh0PVy1li51Ac3hd/u3foiNnTNchzgE3Nv/nbKmtFU6huuLNgzkQGuLA+yn7mKYzBwA3ZmFgoimdH9+yRCMkZ8B5VHpvfN1hgpJcyEl1T98Kv4cdtRYNB4w9iAMy1qSVxhjeI+2rjuWGoXro0lU6L4LIRCOruY3AuLCAKG8Qw5Ak9ksmIKBhZ9soxpoIwu/TYDUQkFj29IrUQucg9TEp7uAoxu8/7EHxB7hWnBRaBAAQbMuIRg7yysT3FT0Os6SB0t9+RBsVMSPuIti9JJZ2Lu0uRI1+Se+g7ItzYtJoPhBJAzAa+J9OONj0RNj2z8Vq2oIBhH4z6b6zTRMVroos3cdfYl5qIKs9SQ7rmeHoPRROcqpCznsUZ/ESa4f2MewFU/7AYcEnCesZV4xg=
bucket: kcc-deploy
local-dir: dist
skip_cleanup: true
on:
repo: AcidWeb/KCC

View File

@@ -1,8 +1,9 @@
ISC LICENSE ISC LICENSE
Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com> Copyright (c) 2012-2025 Ciro Mattia Gonano <ciromattia@gmail.com>
Copyright (c) 2013-2019 Paweł Jastrzębski <pawelj@iosphe.re> Copyright (c) 2013-2019 Paweł Jastrzębski <pawelj@iosphe.re>
Copyright (c) 2021-2023 Darodi Copyright (c) 2021-2023 Darodi (https://github.com/darodi)
Copyright (c) 2023-2025 Alex Xu (https://github.com/axu2)
Permission to use, copy, modify, and/or distribute this software for Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the any purpose with or without fee is hereby granted, provided that the

View File

@@ -11,6 +11,8 @@ It was initially developed for Kindle but since version 4.6 it outputs valid EPU
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_. 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. It can also optionally optimize images by applying a number of transformations.
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
### A word of warning ### 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. **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. Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
@@ -31,6 +33,9 @@ If you find **KCC** valuable you can consider donating to the authors:
- Alex Xu (active 2023-Present) - 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) - [![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)
## Sponsors
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
## DOWNLOADS ## DOWNLOADS
@@ -68,9 +73,11 @@ If you have issues detecting it, get stuck on the MOBI conversion step, or use L
### 7-Zip ### 7-Zip
This is only required for certain files and advanced features. This is optional but will make conversions much faster.
KCC will ask you to install if needed. This is required for certain files and advanced features.
KCC will ask you to install if needed.
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
@@ -163,6 +170,8 @@ PROCESSING:
Set cropping power [Default=1.0] Set cropping power [Default=1.0]
--cm CROPPINGM, --croppingminimum CROPPINGM --cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0] Set cropping minimum area ratio [Default=0.0]
--ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--forcecolor Don't convert images to grayscale --forcecolor Don't convert images to grayscale
@@ -185,6 +194,7 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment --spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option. --norotate Do not rotate double page spreads in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images
CUSTOM PROFILE: CUSTOM PROFILE:
--customwidth CUSTOMWIDTH --customwidth CUSTOMWIDTH
@@ -229,6 +239,8 @@ If you want to edit the code, a good code editor is [VS Code](https://code.visua
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**. 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. 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
### Windows install from source ### Windows install from source
@@ -247,6 +259,12 @@ venv\Scripts\activate.bat
python kcc.py python kcc.py
``` ```
You can build a `.exe` of KCC like the downloads we offer with
```
python setup.py build_binary
```
### macOS install from source ### macOS install from source
One time setup and running for the first time: One time setup and running for the first time:
@@ -264,6 +282,12 @@ source venv/bin/activate
python kcc.py python kcc.py
``` ```
You can build a `.app` of KCC like the downloads we offer with
```
python setup.py build_binary
```
## CREDITS ## CREDITS
**KCC** is made by **KCC** is made by
@@ -300,5 +324,5 @@ The app relies and includes the following scripts:
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues). Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
## COPYRIGHT ## COPYRIGHT
Copyright (c) 2012-2023 Ciro Mattia Gonano, Paweł Jastrzębski and Darodi. Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details. **KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.

View File

@@ -1,14 +0,0 @@
environment:
PYTHON: "C:\\Python37-x64"
install:
- set PATH="%PYTHON%\\Scripts";%PATH%
- "%PYTHON%\\python.exe -m pip install --upgrade pip setuptools wheel"
- "%PYTHON%\\python.exe -m pip install -r requirements.txt"
- "%PYTHON%\\python.exe -m pip install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip"
build_script:
- "%PYTHON%\\python.exe setup.py build_binary"
artifacts:
- path: dist\KCC*

View File

@@ -37,36 +37,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mangaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Manga mode</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="4" column="2"> <item row="4" column="2">
<widget class="QCheckBox" name="croppingBox"> <widget class="QCheckBox" name="croppingBox">
<property name="toolTip"> <property name="toolTip">
@@ -80,39 +50,23 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="1" column="0">
<widget class="QCheckBox" name="mozJpegBox"> <widget class="QCheckBox" name="mangaBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>JPEG/PNG/mozJpeg</string> <string>Manga mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="0">
<widget class="QCheckBox" name="upscaleBox"> <widget class="QCheckBox" name="webtoonBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Stretch/Upscale</string> <string>Webtoon mode</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -129,46 +83,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="2">
<widget class="QCheckBox" name="gammaBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Custom gamma</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
</property>
<property name="text">
<string>Spread shift</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="webtoonBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable special parsing mode for Korean Webtoons.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Webtoon mode</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QCheckBox" name="borderBox"> <widget class="QCheckBox" name="borderBox">
<property name="toolTip"> <property name="toolTip">
@@ -182,13 +96,36 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="2" column="2">
<widget class="QCheckBox" name="noRotateBox"> <widget class="QCheckBox" name="gammaBox">
<property name="toolTip"> <property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable automatic gamma correction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>No rotate</string> <string>Custom gamma</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QCheckBox" name="interPanelCropBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Disabled&lt;br/&gt;&lt;/span&gt;Disabled&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Horizontal&lt;br/&gt;&lt;/span&gt;Crop empty horizontal lines.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Both&lt;br/&gt;&lt;/span&gt;Crop empty horizontal and vertical lines.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Inter-panel crop</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="colorBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Disable conversion to grayscale.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Color mode</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -208,13 +145,23 @@
<item row="5" column="2"> <item row="5" column="2">
<widget class="QCheckBox" name="disableProcessingBox"> <widget class="QCheckBox" name="disableProcessingBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;pre style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Do not process any image, ignore profile and processing options&lt;/pre&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Do not process any image, ignore profile and processing options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>Disable processing</string> <string>Disable processing</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1">
<widget class="QCheckBox" name="maximizeStrips">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - 1x4&lt;br/&gt;&lt;/span&gt;Keep format 1x4 panels strips.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - 2x2&lt;br/&gt;&lt;/span&gt;Turn 1x4 strips to 2x2 to maximize screen usage.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>1x4 to 2x2 strips</string>
</property>
</widget>
</item>
<item row="0" column="0"> <item row="0" column="0">
<widget class="QLineEdit" name="authorEdit"> <widget class="QLineEdit" name="authorEdit">
<property name="sizePolicy"> <property name="sizePolicy">
@@ -237,6 +184,82 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1">
<widget class="QCheckBox" name="deleteBox">
<property name="toolTip">
<string>Delete input file(s) or directory. It's not recoverable!</string>
</property>
<property name="text">
<string>Delete input</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="mozJpegBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - JPEG&lt;br/&gt;&lt;/span&gt;Use JPEG files&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - force PNG&lt;br/&gt;&lt;/span&gt;Create PNG files instead JPEG&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - mozJpeg&lt;br/&gt;&lt;/span&gt;10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>JPEG/PNG/mozJpeg</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="spreadShiftBox">
<property name="toolTip">
<string>Shift first page to opposite side in landscape for two page spread alignment</string>
</property>
<property name="text">
<string>Spread shift</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="upscaleBox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Nothing&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will not be resized.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - Stretching&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be not preserved.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Upscaling&lt;br/&gt;&lt;/span&gt;Images smaller than device resolution will be resized. Aspect ratio will be preserved.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Stretch/Upscale</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="outputSplit">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Automatic mode&lt;br/&gt;&lt;/span&gt;The output will be split automatically.&lt;/p&gt;&lt;p style='white-space:pre'&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Volume mode&lt;br/&gt;&lt;/span&gt;Every subdirectory will be considered as a separate volume.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Output split</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="noRotateBox">
<property name="toolTip">
<string>Do not rotate double page spreads in spread splitter option.</string>
</property>
<property name="text">
<string>No rotate</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QCheckBox" name="reduceRainbowBox">
<property name="toolTip">
<string>Reduce rainbow effect on color eink by slightly blurring images</string>
</property>
<property name="text">
<string>Reduce Rainbow</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

39
kcc.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['kcc.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='kcc',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None , icon='icons\\comic2ebook.ico')

View File

@@ -37,7 +37,7 @@ from packaging.version import Version
from raven import Client from raven import Client
from tempfile import gettempdir from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run from .shared import HTMLStripper, available_archive_tools, sanitizeTrace, walkLevel, subprocess_run
from . import __version__ from . import __version__
from . import comic2ebook from . import comic2ebook
from . import metadata from . import metadata
@@ -244,6 +244,7 @@ class WorkerThread(QThread):
options.cropping = GUI.croppingBox.checkState().value options.cropping = GUI.croppingBox.checkState().value
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked: if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue) options.croppingp = float(GUI.croppingPowerValue)
options.interpanelcrop = GUI.interPanelCropBox.checkState().value
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked: elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
@@ -252,6 +253,8 @@ class WorkerThread(QThread):
options.batchsplit = 2 options.batchsplit = 2
if GUI.colorBox.isChecked(): if GUI.colorBox.isChecked():
options.forcecolor = True options.forcecolor = True
if GUI.reduceRainbowBox.isChecked():
options.reducerainbow = True
if GUI.maximizeStrips.isChecked(): if GUI.maximizeStrips.isChecked():
options.maximizestrips = True options.maximizestrips = True
if GUI.disableProcessingBox.isChecked(): if GUI.disableProcessingBox.isChecked():
@@ -314,13 +317,8 @@ class WorkerThread(QThread):
GUI.progress.content = '' GUI.progress.content = ''
self.errors = True self.errors = True
_, _, traceback = sys.exc_info() _, _, traceback = sys.exc_info()
if len(err.args) == 1: MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
else:
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err.args[0]), err.args[1]), 'error')
GUI.sentry.extra_context({'realTraceback': err.args[1]})
if ' is corrupted.' not in str(err): if ' is corrupted.' not in str(err):
GUI.sentry.captureException() GUI.sentry.captureException()
MW.addMessage.emit('Error during conversion! Please consult ' MW.addMessage.emit('Error during conversion! Please consult '
@@ -789,11 +787,13 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'gammaBox': GUI.gammaBox.checkState().value, 'gammaBox': GUI.gammaBox.checkState().value,
'croppingBox': GUI.croppingBox.checkState().value, 'croppingBox': GUI.croppingBox.checkState().value,
'croppingPowerSlider': float(self.croppingPowerValue) * 100, 'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'interPanelCropBox': GUI.interPanelCropBox.checkState().value,
'upscaleBox': GUI.upscaleBox.checkState().value, 'upscaleBox': GUI.upscaleBox.checkState().value,
'borderBox': GUI.borderBox.checkState().value, 'borderBox': GUI.borderBox.checkState().value,
'webtoonBox': GUI.webtoonBox.checkState().value, 'webtoonBox': GUI.webtoonBox.checkState().value,
'outputSplit': GUI.outputSplit.checkState().value, 'outputSplit': GUI.outputSplit.checkState().value,
'colorBox': GUI.colorBox.checkState().value, 'colorBox': GUI.colorBox.checkState().value,
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value,
'disableProcessingBox': GUI.disableProcessingBox.checkState().value, 'disableProcessingBox': GUI.disableProcessingBox.checkState().value,
'mozJpegBox': GUI.mozJpegBox.checkState().value, 'mozJpegBox': GUI.mozJpegBox.checkState().value,
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
@@ -1066,19 +1066,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.addMessage('Since you are a new user of <b>KCC</b> please see few ' self.addMessage('Since you are a new user of <b>KCC</b> please see few '
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.', '<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
'info') 'info')
try:
subprocess_run(['tar'], stdout=PIPE, stderr=STDOUT) self.tar = 'tar' in available_archive_tools()
self.tar = True self.sevenzip = '7z' in available_archive_tools()
except FileNotFoundError: if not any([self.tar, self.sevenzip]):
self.tar = False self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
try: ' to enable CBZ/CBR/ZIP/etc processing.', 'warning')
subprocess_run(['7z'], stdout=PIPE, stderr=STDOUT)
self.sevenzip = True
except FileNotFoundError:
self.sevenzip = False
if not self.tar:
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) self.detectKindleGen(True)
APP.messageFromOtherInstance.connect(self.handleMessage) APP.messageFromOtherInstance.connect(self.handleMessage)

View File

@@ -1,6 +1,6 @@
# Resource object code (Python 3) # Resource object code (Python 3)
# Created by: object code # Created by: object code
# Created by: The Resource Compiler for Qt version 6.8.1 # Created by: The Resource Compiler for Qt version 6.8.2
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
from PySide6 import QtCore from PySide6 import QtCore
@@ -11612,51 +11612,51 @@ qt_resource_struct = b"\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x07\ \x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x07\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x02.\xed\ \x00\x00\x01\xc0\x00\x00\x00\x00\x00\x01\x00\x02.\xed\
\x00\x00\x01\x90(\xef\xc4\x03\ \x00\x00\x01\x95\x8bV),\
\x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x02\x83\x87\ \x00\x00\x01\xfe\x00\x00\x00\x00\x00\x01\x00\x02\x83\x87\
\x00\x00\x01\x90(\xef\xc4\x00\ \x00\x00\x01\x95\x8bV)&\
\x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02Y\x8c\ \x00\x00\x01\xea\x00\x00\x00\x00\x00\x01\x00\x02Y\x8c\
\x00\x00\x01\x90(\xef\xc3\xff\ \x00\x00\x01\x95\x8bV)&\
\x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02N)\ \x00\x00\x01\xd6\x00\x00\x00\x00\x00\x01\x00\x02N)\
\x00\x00\x01\x90(\xef\xc4\x01\ \x00\x00\x01\x95\x8bV),\
\x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x0c\ \x00\x00\x00X\x00\x02\x00\x00\x00\x04\x00\x00\x00\x0c\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x01(\x97\ \x00\x00\x00\xa6\x00\x00\x00\x00\x00\x01\x00\x01(\x97\
\x00\x00\x01\x90(\xef\xc4\x03\ \x00\x00\x01\x95\x8bV),\
\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x011\xef\ \x00\x00\x00\xbc\x00\x00\x00\x00\x00\x01\x00\x011\xef\
\x00\x00\x01\x94o>\xe74\ \x00\x00\x01\x95\x8bV)5\
\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\ \x00\x00\x00\x8c\x00\x00\x00\x00\x00\x01\x00\x01\x1d\x90\
\x00\x00\x01\x90(\xef\xc4\x02\ \x00\x00\x01\x95\x8bV),\
\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01:\x05\ \x00\x00\x00\xd0\x00\x00\x00\x00\x00\x01\x00\x01:\x05\
\x00\x00\x01\x90(\xef\xc4\x04\ \x00\x00\x01\x95\x8bV)4\
\x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x11\ \x00\x00\x00X\x00\x02\x00\x00\x00\x03\x00\x00\x00\x11\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xd3\ \x00\x00\x02B\x00\x00\x00\x00\x00\x01\x00\x02\xb5\xd3\
\x00\x00\x01\x90(\xef\xc4!\ \x00\x00\x01\x95\x8bV)b\
\x00\x00\x02\x14\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd6\ \x00\x00\x02\x14\x00\x00\x00\x00\x00\x01\x00\x02\x9f\xd6\
\x00\x00\x01\x90(\xef\xc4\x1d\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x02\xa93\ \x00\x00\x02*\x00\x00\x00\x00\x00\x01\x00\x02\xa93\
\x00\x00\x01\x90(\xef\xc4\x19\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x00X\x00\x02\x00\x00\x00\x07\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\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\ \x00\x00\x01\x1c\x00\x00\x00\x00\x00\x01\x00\x01P\xb1\
\x00\x00\x01\x90(\xef\xc4\x22\ \x00\x00\x01\x95\x8bV)b\
\x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\ \x00\x00\x012\x00\x00\x00\x00\x00\x01\x00\x01yY\
\x00\x00\x01\x90(\xef\xc4\x1c\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\ \x00\x00\x01\x94\x00\x00\x00\x00\x00\x01\x00\x01\xd2-\
\x00\x00\x01\x90(\xef\xc4\x1e\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\ \x00\x00\x01z\x00\x00\x00\x00\x00\x01\x00\x01\x8c\xe6\
\x00\x00\x01\x90(\xef\xc4\x18\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\ \x00\x00\x01\x04\x00\x00\x00\x00\x00\x01\x00\x01LR\
\x00\x00\x01\x90(\xef\xc4\x0e\ \x00\x00\x01\x95\x8bV)@\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01?\xe9\ \x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x01?\xe9\
\x00\x00\x01\x90(\xef\xc4\x17\ \x00\x00\x01\x95\x8bV)Q\
\x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x01\x82\xb0\ \x00\x00\x01T\x00\x00\x00\x00\x00\x01\x00\x01\x82\xb0\
\x00\x00\x01\x90(\xef\xc4\x18\ \x00\x00\x01\x95\x8bV)Y\
\x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\ \x00\x00\x00X\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1d\
\x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x00h\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x90(\xef\xc4\x16\ \x00\x00\x01\x95\x8bV)Q\
" "
def qInitResources(): def qInitResources():

View File

@@ -3,7 +3,7 @@
################################################################################ ################################################################################
## Form generated from reading UI file 'KCC.ui' ## Form generated from reading UI file 'KCC.ui'
## ##
## Created by: Qt User Interface Compiler version 6.8.1 ## Created by: Qt User Interface Compiler version 6.8.2
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
@@ -40,43 +40,21 @@ class Ui_mainWindow(object):
self.gridLayout_2 = QGridLayout(self.optionWidget) self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2") self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget) self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox") self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True) self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1) self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget) self.mangaBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox") self.mangaBox.setObjectName(u"mangaBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1) self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget) self.webtoonBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox") self.webtoonBox.setObjectName(u"webtoonBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1) 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.rotateBox = QCheckBox(self.optionWidget) self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox") self.rotateBox.setObjectName(u"rotateBox")
@@ -84,36 +62,27 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1) self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget) self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox") self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True) self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1) self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget) self.gammaBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox") self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1) 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 = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox") self.qualityBox.setObjectName(u"qualityBox")
@@ -126,6 +95,11 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1) self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget) self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit") self.authorEdit.setObjectName(u"authorEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
@@ -138,6 +112,43 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1) self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.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.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -377,73 +388,81 @@ class Ui_mainWindow(object):
def retranslateUi(self, mainWindow): def retranslateUi(self, mainWindow):
mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None)) mainWindow.setWindowTitle(QCoreApplication.translate("mainWindow", u"Kindle Comic Converter", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None)) self.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None)) self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None)) self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None)) self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Manga mode", None))
#if QT_CONFIG(tooltip) #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)) self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None)) self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon 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.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.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.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) #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)) 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) #endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None)) self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", 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.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.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.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) #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)) 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) #endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None)) self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None)) self.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) #endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None)) 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) #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)) self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None)) self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.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)) 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) #endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None)) 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) #if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None)) self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None)) 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.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None)) self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)

View File

@@ -3,7 +3,7 @@
################################################################################ ################################################################################
## Form generated from reading UI file 'MetaEditor.ui' ## Form generated from reading UI file 'MetaEditor.ui'
## ##
## Created by: Qt User Interface Compiler version 6.8.1 ## Created by: Qt User Interface Compiler version 6.8.2
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################

View File

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

View File

@@ -23,7 +23,7 @@ import pathlib
import re import re
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from time import strftime, gmtime from time import perf_counter, strftime, gmtime
from copy import copy from copy import copy
from glob import glob, escape from glob import glob, escape
from re import sub from re import sub
@@ -33,14 +33,14 @@ from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool from multiprocessing import Pool
from uuid import uuid4 from uuid import uuid4
from natsort import os_sorted from natsort import os_sort_keygen
from slugify import slugify as slugify_ext from slugify import slugify as slugify_ext
from PIL import Image, ImageFile from PIL import Image, ImageFile
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
from psutil import virtual_memory, disk_usage from psutil import virtual_memory, disk_usage
from html import escape as hescape from html import escape as hescape
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run from .shared import available_archive_tools, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run
from . import comic2panel from . import comic2panel
from . import image from . import image
from . import comicarchive from . import comicarchive
@@ -51,7 +51,7 @@ from . import kindle
from . import __version__ from . import __version__
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
OS_SORT_KEY = os_sort_keygen()
def main(argv=None): def main(argv=None):
global options global options
@@ -78,14 +78,14 @@ def main(argv=None):
def buildHTML(path, imgfile, imgfilepath): def buildHTML(path, imgfile, imgfilepath):
imgfilepath = md5Checksum(imgfilepath) key = pathlib.Path(imgfilepath).name
filename = getImageFileName(imgfile) filename = getImageFileName(imgfile)
deviceres = options.profileData[1] deviceres = options.profileData[1]
if not options.noprocessing and "Rotated" in options.imgMetadata[imgfilepath]: if not options.noprocessing and "Rotated" in options.imgMetadata[key]:
rotatedPage = True rotatedPage = True
else: else:
rotatedPage = False rotatedPage = False
if not options.noprocessing and "BlackBackground" in options.imgMetadata[imgfilepath]: if not options.noprocessing and "BlackBackground" in options.imgMetadata[key]:
additionalStyle = 'background-color:#000000;' additionalStyle = 'background-color:#000000;'
else: else:
additionalStyle = '' additionalStyle = ''
@@ -216,7 +216,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\') folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1])) filename = getImageFileName(os.path.join(folder, chapter[1]))
navID = folder.replace('/', '_').replace('\\', '_') navID = folder.replace('/', '_').replace('\\', '_')
if options.chapters: if options.comicinfo_chapters:
title = chapternames[chapter[1]] title = chapternames[chapter[1]]
navID = filename[0].replace('/', '_').replace('\\', '_') navID = filename[0].replace('/', '_').replace('\\', '_')
elif os.path.basename(folder) != "Text": elif os.path.basename(folder) != "Text":
@@ -244,7 +244,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters: for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\') folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1])) filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.chapters: if options.comicinfo_chapters:
title = chapternames[chapter[1]] title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text": elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)] title = chapternames[os.path.basename(folder)]
@@ -256,7 +256,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
for chapter in chapters: for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\') folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1])) filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.chapters: if options.comicinfo_chapters:
title = chapternames[chapter[1]] title = chapternames[chapter[1]]
elif os.path.basename(folder) != "Text": elif os.path.basename(folder) != "Text":
title = chapternames[os.path.basename(folder)] title = chapternames[os.path.basename(folder)]
@@ -358,19 +358,19 @@ def buildOPF(dstdir, title, filelist, cover=None):
pageside = "right" pageside = "right"
for entry in reflist: for entry in reflist:
if options.righttoleft: if options.righttoleft:
if entry.endswith("-a"): if entry.endswith("-kcc-a"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center")) pageSpreadProperty("center"))
) )
pageside = "right" pageside = "right"
elif entry.endswith("-b"): elif entry.endswith("-kcc-b"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right")) pageSpreadProperty("right"))
) )
pageside = "right" pageside = "right"
elif entry.endswith("-c"): elif entry.endswith("-kcc-c"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left")) pageSpreadProperty("left"))
@@ -386,19 +386,19 @@ def buildOPF(dstdir, title, filelist, cover=None):
else: else:
pageside = "right" pageside = "right"
else: else:
if entry.endswith("-a"): if entry.endswith("-kcc-a"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("center")) pageSpreadProperty("center"))
) )
pageside = "left" pageside = "left"
elif entry.endswith("-b"): elif entry.endswith("-kcc-b"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("left")) pageSpreadProperty("left"))
) )
pageside = "left" pageside = "left"
elif entry.endswith("-c"): elif entry.endswith("-kcc-c"):
f.write( f.write(
"<itemref idref=\"page_%s\" %s/>\n" % (entry, "<itemref idref=\"page_%s\" %s/>\n" % (entry,
pageSpreadProperty("right")) pageSpreadProperty("right"))
@@ -425,7 +425,6 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"]) "</container>"])
f.close() f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked): def buildEPUB(path, chapternames, tomenumber, ischunked):
filelist = [] filelist = []
chapterlist = [] chapterlist = []
@@ -506,6 +505,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
"display: none;\n", "display: none;\n",
"}\n"]) "}\n"])
f.close() f.close()
build_html_start = perf_counter()
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')): for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False chapter = False
dirnames, filenames = walkSort(dirnames, filenames) dirnames, filenames = walkSort(dirnames, filenames)
@@ -515,12 +515,17 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
'cover' + getImageFileName(afile)[1]) 'cover' + getImageFileName(afile)[1])
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options, options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
tomenumber), options.uuid)) tomenumber), options.uuid))
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
if not chapter: if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1])) chapterlist.append((dirpath.replace('Images', 'Text'), afile))
chapter = True chapter = True
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 # Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
if not chapternames and options.chapters and not ischunked: if ischunked:
options.comicinfo_chapters = []
if not chapternames and options.comicinfo_chapters:
chapterlist = [] chapterlist = []
global_diff = 0 global_diff = 0
@@ -533,7 +538,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
elif options.splitter == 2: elif options.splitter == 2:
diff_delta = 2 diff_delta = 2
for aChapter in options.chapters: for aChapter in options.comicinfo_chapters:
pageid = aChapter[0] pageid = aChapter[0]
cur_diff = global_diff cur_diff = global_diff
global_diff = 0 global_diff = 0
@@ -556,7 +561,6 @@ def imgDirectoryProcessing(path):
workerPool = Pool(maxtasksperchild=100) workerPool = Pool(maxtasksperchild=100)
workerOutput = [] workerOutput = []
options.imgMetadata = {} options.imgMetadata = {}
options.imgOld = []
work = [] work = []
pagenumber = 0 pagenumber = 0
for dirpath, _, filenames in os.walk(path): for dirpath, _, filenames in os.walk(path):
@@ -566,19 +570,19 @@ def imgDirectoryProcessing(path):
if GUI: if GUI:
GUI.progressBarTick.emit(str(pagenumber)) GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0: if len(work) > 0:
img_processing_start = perf_counter()
for i in work: for i in work:
workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick) workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick)
workerPool.close() workerPool.close()
workerPool.join() workerPool.join()
img_processing_end = perf_counter()
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
if GUI and not GUI.conversionAlive: if GUI and not GUI.conversionAlive:
rmtree(os.path.join(path, '..', '..'), True) rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Conversion interrupted.") raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0: if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True) rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1]) 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: else:
rmtree(os.path.join(path, '..', '..'), True) rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.") raise UserWarning("Source directory is empty.")
@@ -592,7 +596,6 @@ def imgFileProcessingTick(output):
for page in output: for page in output:
if page is not None: if page is not None:
options.imgMetadata[page[0]] = page[1] options.imgMetadata[page[0]] = page[1]
options.imgOld.append(page[2])
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if not GUI.conversionAlive: if not GUI.conversionAlive:
@@ -612,8 +615,11 @@ def imgFileProcessing(work):
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping > 0 and not opt.webtoon: if opt.cropping > 0 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm) img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcepng and not opt.forcecolor: if opt.forcepng and not opt.forcecolor:
img.quantizeImage() img.quantizeImage()
output.append(img.saveToDir()) output.append(img.saveToDir())
@@ -626,7 +632,7 @@ def getWorkFolder(afile):
if os.path.isdir(afile): if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5: if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.") raise UserWarning("Not enough disk space to perform conversion.")
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try: try:
os.rmdir(workdir) os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
@@ -642,26 +648,34 @@ def getWorkFolder(afile):
if afile.lower().endswith('.pdf'): if afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile) pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract() path, njpg = pdf.extract()
workdir = path
sanitizePermissions(path)
if njpg == 0: if njpg == 0:
rmtree(path, True) rmtree(path, True)
raise UserWarning("Failed to extract images from PDF file.") raise UserWarning("Failed to extract images from PDF file.")
else: else:
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try: try:
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir) path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir) tdir = os.listdir(workdir)
if 'ComicInfo.xml' in tdir: if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml') tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
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])
except OSError as e: except OSError as e:
rmtree(workdir, True) rmtree(workdir, True)
raise UserWarning(e) raise UserWarning(e)
else: else:
raise UserWarning("Failed to open source file/directory.") raise UserWarning("Failed to open source file/directory.")
sanitizePermissions(path) newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
newpath = mkdtemp('', 'KCC-') os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
copytree(path, os.path.join(newpath, 'OEBPS', 'Images'))
rmtree(path, True)
return newpath return newpath
@@ -702,7 +716,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
def getComicInfo(path, originalpath): def getComicInfo(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml') xmlPath = os.path.join(path, 'ComicInfo.xml')
options.chapters = [] options.comicinfo_chapters = []
options.summary = '' options.summary = ''
titleSuffix = '' titleSuffix = ''
if options.title == 'defaulttitle': if options.title == 'defaulttitle':
@@ -725,9 +739,7 @@ def getComicInfo(path, originalpath):
except Exception: except Exception:
os.remove(xmlPath) os.remove(xmlPath)
return return
if xml.data['Title']: if defaultTitle:
options.title = hescape(xml.data['Title'])
elif defaultTitle:
if xml.data['Series']: if xml.data['Series']:
options.title = hescape(xml.data['Series']) options.title = hescape(xml.data['Series'])
if xml.data['Volume']: if xml.data['Volume']:
@@ -746,7 +758,7 @@ def getComicInfo(path, originalpath):
else: else:
options.authors = ['KCC'] options.authors = ['KCC']
if xml.data['Bookmarks']: if xml.data['Bookmarks']:
options.chapters = xml.data['Bookmarks'] options.comicinfo_chapters = xml.data['Bookmarks']
if xml.data['Summary']: if xml.data['Summary']:
options.summary = hescape(xml.data['Summary']) options.summary = hescape(xml.data['Summary'])
os.remove(xmlPath) os.remove(xmlPath)
@@ -779,22 +791,22 @@ def getPanelViewSize(deviceres, size):
def sanitizeTree(filetree): def sanitizeTree(filetree):
chapterNames = {} chapterNames = {}
for root, dirs, files in os.walk(filetree, False): page = 1
for i, name in enumerate(os_sorted(files)): for root, dirs, files in os.walk(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY)
for name in files:
splitname = os.path.splitext(name) splitname = os.path.splitext(name)
# file needs kcc at front AND back to avoid renaming issues # 9999 page limit
slugified = f'kcc-{i:04}' slugified = f'kcc-{page:04}'
for suffix in '-KCC', '-KCC-A', '-KCC-B', '-KCC-C': page += 1
if splitname[0].endswith(suffix):
slugified += suffix.lower()
break
newKey = os.path.join(root, slugified + splitname[1]) newKey = os.path.join(root, slugified + splitname[1])
key = os.path.join(root, name) key = os.path.join(root, name)
if key != newKey: if key != newKey:
os.replace(key, newKey) os.replace(key, newKey)
for name in dirs: for i, name in enumerate(dirs):
tmpName = name tmpName = name
slugified = slugify(name) slugified = slugify(name)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper(): while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
@@ -804,6 +816,7 @@ def sanitizeTree(filetree):
key = os.path.join(root, name) key = os.path.join(root, name)
if key != newKey: if key != newKey:
os.replace(key, newKey) os.replace(key, newKey)
dirs[i] = newKey
return chapterNames return chapterNames
@@ -815,12 +828,11 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
def splitDirectory(path): def chunk_directory(path):
level = -1 level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')): for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files: for f in files:
if f.endswith('.jpg') or f.endswith('.jpeg') or f.endswith('.png') or f.endswith('.gif') or \ if getImageFileName(f):
f.endswith('.webp'):
newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep) newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep)
if level != -1 and level != newLevel: if level != -1 and level != newLevel:
level = 0 level = 0
@@ -828,16 +840,17 @@ def splitDirectory(path):
else: else:
level = newLevel level = newLevel
if level > 0: if level > 0:
splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), level) parent = pathlib.Path(path).parent
chunker = chunk_process(os.path.join(path, 'OEBPS', 'Images'), level, parent)
path = [path] path = [path]
for tome in splitter: for tome in chunker:
path.append(tome) path.append(tome)
return path return path
else: else:
raise UserWarning('Unsupported directory structure.') raise UserWarning('Unsupported directory structure.')
def splitProcess(path, mode): def chunk_process(path, mode, parent):
output = [] output = []
currentSize = 0 currentSize = 0
currentTarget = path currentTarget = path
@@ -857,7 +870,7 @@ def splitProcess(path, mode):
else: else:
size = getDirectorySize(os.path.join(root, name)) size = getDirectorySize(os.path.join(root, name))
if currentSize + size > targetSize: if currentSize + size > targetSize:
currentTarget, pathRoot = createNewTome() currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot) output.append(pathRoot)
currentSize = size currentSize = size
else: else:
@@ -869,15 +882,14 @@ def splitProcess(path, mode):
for root, dirs, _ in walkLevel(path, 0): for root, dirs, _ in walkLevel(path, 0):
for name in dirs: for name in dirs:
if not firstTome: if not firstTome:
currentTarget, pathRoot = createNewTome() currentTarget, pathRoot = createNewTome(parent)
output.append(pathRoot) output.append(pathRoot)
move(os.path.join(root, name), os.path.join(currentTarget, name)) move(os.path.join(root, name), os.path.join(currentTarget, name))
else: else:
firstTome = False firstTome = False
return output return output
def detectSuboptimalProcessing(tmppath, orgpath):
def detectCorruption(tmppath, orgpath):
imageNumber = 0 imageNumber = 0
imageSmaller = 0 imageSmaller = 0
alreadyProcessed = False alreadyProcessed = False
@@ -893,9 +905,6 @@ def detectCorruption(tmppath, orgpath):
raise RuntimeError('Image file %s is corrupted.' % pathOrg) raise RuntimeError('Image file %s is corrupted.' % pathOrg)
try: try:
img = Image.open(path) img = Image.open(path)
img.verify()
img = Image.open(path)
img.load()
imageNumber += 1 imageNumber += 1
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]: if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1 imageSmaller += 1
@@ -906,7 +915,10 @@ def detectCorruption(tmppath, orgpath):
else: else:
raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err))) raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err)))
else: else:
os.remove(os.path.join(root, name)) try:
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
if alreadyProcessed: if alreadyProcessed:
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.") print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
if GUI: if GUI:
@@ -922,8 +934,8 @@ def detectCorruption(tmppath, orgpath):
GUI.addMessage.emit('', '', False) GUI.addMessage.emit('', '', False)
def createNewTome(): def createNewTome(parent):
tomePathRoot = mkdtemp('', 'KCC-') tomePathRoot = mkdtemp('', 'KCC-', parent)
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images') tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath) os.makedirs(tomePath)
return tomePath, tomePathRoot return tomePath, tomePathRoot
@@ -936,17 +948,27 @@ def slugify(value):
def makeZIP(zipfilename, basedir, isepub=False): def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip' zipfilename = os.path.abspath(zipfilename) + '.zip'
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED) if '7z' in available_archive_tools():
if isepub: if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED) mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
for dirpath, _, filenames in os.walk(basedir): mimetypeFile.write('application/epub+zip')
for name in filenames: mimetypeFile.close()
path = os.path.normpath(os.path.join(dirpath, name)) subprocess_run(['7z', 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True)
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name)) else:
if os.path.isfile(path): zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
zipOutput.write(path, aPath) if isepub:
zipOutput.close() zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
for name in filenames:
path = os.path.normpath(os.path.join(dirpath, name))
aPath = os.path.normpath(os.path.join(dirpath.replace(basedir, ''), name))
if os.path.isfile(path):
zipOutput.write(path, aPath)
zipOutput.close()
end = perf_counter()
print(f"makeZIP time: {end - start} seconds")
return zipfilename return zipfilename
@@ -1013,12 +1035,16 @@ def makeParser():
help="Set cropping power [Default=1.0]") help="Set cropping power [Default=1.0]")
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0", processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]") help="Set cropping minimum area ratio [Default=0.0]")
processing_options.add_argument("--ipc", "--interpanelcrop", type=int, dest="interpanelcrop", default="0",
help="Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]")
processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False, processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False,
help="Disable autodetection and force black borders") help="Disable autodetection and force black borders")
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False, processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
help="Disable autodetection and force white borders") help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False, processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale") help="Don't convert images to grayscale")
output_options.add_argument("--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, processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG") help="Create PNG files instead JPEG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False, processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
@@ -1117,9 +1143,7 @@ def checkTools(source):
source = source.upper() source = source.upper()
if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \ if source.endswith('.CB7') or source.endswith('.7Z') or source.endswith('.RAR') or source.endswith('.CBR') or \
source.endswith('.ZIP') or source.endswith('.CBZ'): source.endswith('.ZIP') or source.endswith('.CBZ'):
try: if '7z' not in available_archive_tools():
subprocess_run(['7z'], stdout=PIPE, stderr=STDOUT)
except FileNotFoundError:
print('ERROR: 7z is missing!') print('ERROR: 7z is missing!')
sys.exit(1) sys.exit(1)
if options.format == 'MOBI': if options.format == 'MOBI':
@@ -1149,6 +1173,7 @@ def checkPre(source):
def makeBook(source, qtgui=None): def makeBook(source, qtgui=None):
start = perf_counter()
global GUI global GUI
GUI = qtgui GUI = qtgui
if GUI: if GUI:
@@ -1160,7 +1185,8 @@ def makeBook(source, qtgui=None):
path = getWorkFolder(source) path = getWorkFolder(source)
print("Checking images...") print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source) getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
detectCorruption(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.webtoon: if options.webtoon:
y = image.ProfileData.Profiles[options.profile][1][1] y = image.ProfileData.Profiles[options.profile][1][1]
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui) comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui)
@@ -1173,9 +1199,8 @@ def makeBook(source, qtgui=None):
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images")) imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI: if GUI:
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit > 0: if options.batchsplit > 0:
tomes = splitDirectory(path) tomes = chunk_directory(path)
else: else:
tomes = [path] tomes = [path]
filepath = [] filepath = []
@@ -1251,6 +1276,8 @@ def makeBook(source, qtgui=None):
elif os.path.isdir(source): elif os.path.isdir(source):
rmtree(source) rmtree(source)
end = perf_counter()
print(f"makeBook: {end - start} seconds")
return filepath return filepath

View File

@@ -76,6 +76,8 @@ class ComicArchive:
['unar', self.filepath, '-f', '-o', targetdir] ['unar', self.filepath, '-f', '-o', targetdir]
) )
extraction_commands.reverse()
if distro.id() == 'fedora' or distro.like() == 'fedora': if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append( extraction_commands.append(
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir] ['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.filepath, targetdir]

View File

@@ -0,0 +1,28 @@
def threshold_from_power(power):
return 240-(power*64)
'''
Groups close values together
'''
def group_close_values(vals, max_dist_tolerated):
groups = []
group_start = -1
group_end = 0
for i in range(len(vals)):
dist = vals[i] - group_end
if group_start == -1:
group_start = vals[i]
group_end = vals[i]
elif dist <= max_dist_tolerated:
group_end = vals[i]
else:
groups.append((group_start, group_end))
group_start = -1
group_end = -1
if group_start != -1:
groups.append((group_start, group_end))
return groups

View File

@@ -20,10 +20,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import io import io
import os import os
from pathlib import Path
import mozjpeg_lossless_optimization import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter 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 .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
AUTO_CROP_THRESHOLD = 0.015 AUTO_CROP_THRESHOLD = 0.015
@@ -140,7 +141,13 @@ class ComicPageParser:
self.source = source self.source = source
self.size = self.opt.profileData[1] self.size = self.opt.profileData[1]
self.payload = [] self.payload = []
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck() self.color = self.colorCheck()
self.fill = self.fillCheck() self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
@@ -278,14 +285,14 @@ class ComicPage:
self.rotated = False self.rotated = False
self.orgPath = os.path.join(path[0], path[1]) self.orgPath = os.path.join(path[0], path[1])
if 'N' in mode: if 'N' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC' self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc'
elif 'R' in mode: elif 'R' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A' self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-a'
self.rotated = True self.rotated = True
elif 'S1' in mode: elif 'S1' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-B' self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-b'
elif 'S2' in mode: elif 'S2' in mode:
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-C' self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-kcc-c'
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@@ -314,7 +321,9 @@ class ComicPage:
output_jpeg_file.write(output_jpeg_bytes) output_jpeg_file.write(output_jpeg_bytes)
else: else:
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85) self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
return [md5Checksum(self.targetPath), flags, self.orgPath] if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
return [Path(self.targetPath).name, flags]
except IOError as err: except IOError as err:
raise RuntimeError('Cannot save image. ' + str(err)) raise RuntimeError('Cannot save image. ' + str(err))
@@ -340,6 +349,14 @@ class ComicPage:
# Quantize is deprecated but new function call it internally anyway... # Quantize is deprecated but new function call it internally anyway...
self.image = self.image.quantize(palette=palImg) self.image = self.image.quantize(palette=palImg)
def optimizeForDisplay(self, 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): def resizeImage(self):
# kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub # kindle scribe conversion to mobi is limited in resolution by kindlegen, same with send to kindle and epub
if self.kindle_scribe_azw3: if self.kindle_scribe_azw3:
@@ -390,6 +407,8 @@ class ComicPage:
if bbox: if bbox:
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
class Cover: class Cover:
def __init__(self, source, target, opt, tomeid): def __init__(self, source, target, opt, tomeid):

View File

@@ -0,0 +1,76 @@
from PIL import Image, ImageFilter, ImageOps
import numpy as np
from typing import Literal
from .common_crop import threshold_from_power, group_close_values
'''
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
Parameters:
img (PIL image): A PIL image.
direction (horizontal or vertical or both): To crop rows (horizontal), cols (vertical) or both.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
background_color (string): 'white' for white background, anything else for black.
Returns:
img (PIL image): A PIL image after cropping empty sections.
'''
def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "both"], keep=0.04, background_color='white'):
img_temp = img
if img.mode != 'L':
img_temp = ImageOps.grayscale(img)
if background_color != 'white':
img_temp = ImageOps.invert(img)
img_mat = np.array(img)
power = 1
img_temp = ImageOps.autocontrast(img_temp, 1).filter(ImageFilter.BoxBlur(1))
img_temp = img_temp.point(lambda p: 255 if p <= threshold_from_power(power) else 0)
if direction in ["horizontal", "both"]:
rows_idx_to_remove = empty_sections(img_temp, keep, horizontal=True)
img_mat = np.delete(img_mat, rows_idx_to_remove, 0)
if direction in ["vertical", "both"]:
cols_idx_to_remove = empty_sections(img_temp, keep, horizontal=False)
img_mat = np.delete(img_mat, cols_idx_to_remove, 1)
return Image.fromarray(img_mat)
'''
Finds empty sections (excluding near borders).
Parameters:
img (PIL image): A PIL image.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
horizontal (boolean): True to find empty rows, False to find empty columns.
Returns:
Itertable (list or NumPy array): indices of rows or columns to remove.
'''
def empty_sections(img, keep, horizontal=True):
axis = 1 if horizontal else 0
img_mat = np.array(img)
img_mat_max = np.max(img_mat, axis=axis)
img_mat_empty_idx = np.where(img_mat_max == 0)[0]
empty_sections = group_close_values(img_mat_empty_idx, 1)
sections_to_remove = []
for section in empty_sections:
if section[1] < img.size[1] * 0.99 and section[0] > img.size[1] * 0.01: # if not near borders
sections_to_remove.append(section)
if len(sections_to_remove) != 0:
sections_to_remove_after_keep = [(int(x1+(keep/2)*(x2-x1)), int(x2-(keep/2)*(x2-x1))) for x1,x2 in sections_to_remove]
idx_to_remove = np.concatenate([np.arange(x1, x2) for x1,x2 in sections_to_remove_after_keep])
return idx_to_remove
return []

View File

@@ -1,5 +1,7 @@
from PIL import ImageOps, ImageFilter from PIL import ImageOps, ImageFilter
import numpy as np import numpy as np
from .common_crop import threshold_from_power, group_close_values
''' '''
Some assupmptions on the page number sizes Some assupmptions on the page number sizes
@@ -51,12 +53,11 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
bw_bbox = bw_img.getbbox() bw_bbox = bw_img.getbbox()
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
return None return None
left, top_y_pos, right, bot_y_pos = bw_bbox left, top_y_pos, right, bot_y_pos = bw_bbox
''' '''
We inspect the lower bottom part of the image where we suspect might be a page number. We inspect the lower bottom part of the image where we suspect might be a page number.
We assume that page number consist of 1 to 3 digits and the total min and max size of the number We assume that page number consist of 1 to 3 digits and the total min and max size of the number
@@ -73,7 +74,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
img_part_mat = np.array(img_part) img_part_mat = np.array(img_part)
window_groups = [] window_groups = []
for i in range(img_part.size[1]): for i in range(img_part.size[1]):
row_groups = [(g[0], g[1], i, i) for g in group_pixels(img_part_mat[i], img.size[0]*max_dist_size[0], threshold)] row_groups = [(g[0], g[1], i, i) for g in group_close_values(np.where(img_part_mat[i] <= threshold)[0], img.size[0]*max_dist_size[0])]
window_groups.extend(row_groups) window_groups.extend(row_groups)
window_groups = np.array(window_groups) window_groups = np.array(window_groups)
@@ -109,7 +110,6 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1)) cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1))
cropped_bbox = bw_img.crop(cropped_bbox).getbbox() cropped_bbox = bw_img.crop(cropped_bbox).getbbox()
return cropped_bbox return cropped_bbox
@@ -145,33 +145,6 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
return bw_img.getbbox() return bw_img.getbbox()
'''
Groups close pixels together (x axis)
'''
def group_pixels(row, max_dist_tolerated, threshold):
groups = []
idx = np.where(row <= threshold)[0]
group_start = -1
group_end = 0
for i in range(len(idx)):
dist = idx[i] - group_end
if group_start == -1:
group_start = idx[i]
group_end = idx[i]
elif dist <= max_dist_tolerated:
group_end = idx[i]
else:
groups.append((group_start, group_end))
group_start = -1
group_end = -1
if group_start != -1:
groups.append((group_start, group_end))
return groups
def box_intersect(box1, box2, max_dist): def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1] return not (box2[0]-max_dist[0] > box1[1]
or box2[1]+max_dist[0] < box1[0] or box2[1]+max_dist[0] < box1[0]
@@ -209,7 +182,3 @@ def merge_boxes(boxes, max_dist_tolerated):
else: else:
j += 1 j += 1
return boxes return boxes
def threshold_from_power(power):
return 240-(power*64)

View File

@@ -18,6 +18,7 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
from functools import lru_cache
import os import os
from hashlib import md5 from hashlib import md5
from html.parser import HTMLParser from html.parser import HTMLParser
@@ -49,7 +50,7 @@ class HTMLStripper(HTMLParser):
def getImageFileName(imgfile): def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile) name, ext = os.path.splitext(imgfile)
ext = ext.lower() ext = ext.lower()
if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']: if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None return None
return [name, ext] return [name, ext]
@@ -74,16 +75,6 @@ def walkLevel(some_dir, level=1):
del dirs[:] 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): def sanitizeTrace(traceback):
return ''.join(format_tb(traceback))\ return ''.join(format_tb(traceback))\
@@ -137,6 +128,19 @@ def dependencyCheck(level):
print('ERROR: ' + ', '.join(missing) + ' is not installed!') print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1) sys.exit(1)
@lru_cache
def available_archive_tools():
available = []
for tool in ['tar', '7z', 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
available.append(tool)
except FileNotFoundError:
pass
return available
def subprocess_run(command, **kwargs): def subprocess_run(command, **kwargs):
if (os.name == 'nt'): if (os.name == 'nt'):
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW) kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)