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

Compare commits

...

183 Commits

Author SHA1 Message Date
Alex Xu
984d44b371 Bump version to 9.3.3 2025-11-25 19:06:17 -08:00
Alex Xu
1111263893 downscale nonrotated spreads to 2x device width (#1147)
* don't downscale nonrotated spreads

* maximum 2x screen downscale

* only downscale if needed

* don't do for kindle scribe
2025-11-25 19:04:22 -08:00
dependabot[bot]
5035c7403e Bump signpath/github-action-submit-signing-request from 1.3 to 2.0 (#1139)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.3 to 2.0.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.3...v2.0)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 18:44:52 -08:00
dependabot[bot]
067aa68162 Bump actions/upload-artifact from 4 to 5 (#1140)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 18:44:43 -08:00
dependabot[bot]
72d07d53ea Bump actions/checkout from 5 to 6 (#1169)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 18:44:30 -08:00
Alex Xu
8c242d45d7 PDOC only (#1171) 2025-11-25 18:44:10 -08:00
Alex Xu
c655922a57 rename RTL to Right-to-left (manga) (#1172) 2025-11-25 18:42:18 -08:00
kiryl
77e8952f12 Keep sorted and unique list of sources (#1108)
- sort items on the sources list alphabetically -
- don't add item if it is already on the list
2025-11-24 22:21:32 -08:00
kiryl
5b069322a4 Update progress notification for bulk jobs (#1109) 2025-11-24 13:45:02 -08:00
Alex Xu
2444a28127 Bump version to 9.3.2 2025-11-17 13:09:48 -08:00
Alex Xu
3aad79fc30 fix -o in c2e for real (#1167) 2025-11-17 13:09:27 -08:00
Alex Xu
2dbc13303f Revert "fix -o in c2e (#1161)" (#1166)
This reverts commit 9429bed91c.
2025-11-17 12:04:27 -08:00
Alex Xu
4c36c7c586 update faq 2025-11-14 08:00:20 -08:00
Alex Xu
65007aec07 better error when attempting to add images directly (#1158) 2025-11-12 15:52:34 -08:00
Alex Xu
9429bed91c fix -o in c2e (#1161) 2025-11-12 11:46:33 -08:00
José Cerezo
3a3ee15cba Optimize docker image (#1160)
* Updated workflow to checkout

* Added kindlegen ln -s instruction
2025-11-12 09:45:49 -08:00
Alex Xu
2394aa3747 Bump version to 9.3.1 2025-11-11 18:43:26 -08:00
José Cerezo
b57992a754 Optimize docker image (#1157)
* Optimized Docker image

* Divided Dockerfile into two images

* Fixed dockerfile path

* Updated workflows

* Added remaining packages in Dockerfile-base

* Updated workflows

* Unified workflows for docker images

* Added LABELs to docker images

* pull from ciromattia cache

* docker2

* Optimized Docker image

* fix tags

* Added installation stage and optimized workflow for Docker image

* Deleted one stage from the dockerfile to optimize build

* copy requirements file after numpy and pymupdf

* don't change primary requirements

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-11-11 18:42:44 -08:00
Alex Xu
c7a62fdcd6 Clarify memory and storage capacities in README 2025-11-10 11:46:22 -08:00
Alex Xu
8861299d24 bump 9.2.2 2025-11-04 13:08:02 -08:00
Alex Xu
636447bb62 Partially check W/B Margins if you don't want KCC to extend the image margins in CBZ/PDF (#1152) 2025-11-04 13:05:16 -08:00
Alex Xu
b23c7744cb Make webtoon tall error more informative (#1151)
* Make webtoon tall error more informative

* fix mistake
2025-11-04 12:54:29 -08:00
Alex Xu
2398a5b1ac raise max res to 6000x8000 (#1150) 2025-11-04 12:40:23 -08:00
José Cerezo
2b2ac8ff55 Added file fusion to C2E (#1149)
* Added file fusion to C2E

* Updated README.md
2025-11-02 19:01:02 -08:00
Alex Xu
5209d9a7b8 fix cover autocontrast (#1141) 2025-10-30 11:55:06 -07:00
Alex Xu
5336870097 fix webtoon too tall (#1146) 2025-10-30 11:54:18 -07:00
Alex Xu
4371d14391 bump 9.2.1 2025-10-26 15:45:23 -07:00
Alex Xu
f96b7cb22b further refine color detection 2025-10-26 14:11:13 -07:00
Alex Xu
4dfd2ea942 weird file structure message 2025-10-26 11:02:02 -07:00
Alex Xu
ba7f4336a5 add threshold comment 2025-10-25 23:16:52 -07:00
Alex Xu
9561b04bec make color detection super precise (#1137) 2025-10-25 23:14:53 -07:00
Alex Xu
2a8f8e9ab4 Fix png transparency (#1136)
* remove transparency

* pop transparency
2025-10-25 18:45:10 -07:00
Alex Xu
b9cef59912 remove pyinstaller-action 2025-10-24 21:12:07 -07:00
Alex Xu
f2ab730691 Fix tint color detection (#1135)
* make color detection even more precise

* fix tinted images
2025-10-24 20:55:18 -07:00
Alex Xu
44401583e4 fix typo 2025-10-24 10:58:03 -07:00
Alex Xu
28faf524c4 build windows command line versions directly using faster Python 3.11 (#1134)
* draft CLI

* fix windows c2e

* fix typos

* update github workflows
2025-10-24 10:52:31 -07:00
Alex Xu
2d288f72ea fix truncated file read (#1133) 2025-10-21 20:41:18 -07:00
Alex Xu
fb9b3c676b add panel view to faq 2025-10-21 14:17:40 -07:00
Alex Xu
cff1de4fa5 Remove LICENSE.txt from package-macos.yml files list
Removed LICENSE.txt from the files list in the workflow.
2025-10-21 14:00:54 -07:00
Alex Xu
0e56cde791 Bump version to 9.2.0 2025-10-21 13:46:14 -07:00
Alex Xu
9a021ad5d4 no autocontrast option, don't autocontrast extremely low contrast images (#1128) 2025-10-21 13:13:42 -07:00
Alex Xu
d648081086 Add comic bundle announcements 2025-10-21 12:43:07 -07:00
Alex Xu
b2a8b364d9 disable RTL on webtoon 2025-10-21 12:04:35 -07:00
Alex Xu
1f935a0635 add webtoon tip 2025-10-21 11:54:47 -07:00
Alex Xu
58b9651ff3 add colorsoft warning (#1131) 2025-10-21 11:49:31 -07:00
Alex Xu
11a56dd892 add webtoon note 2025-10-21 10:46:27 -07:00
Alex Xu
c23af709cf Revert "webtoon increase minimum vertical blank space threshold"
This reverts commit 80ea17ff02.
2025-10-20 23:47:36 -07:00
Alex Xu
87a7197c16 warn scribe png mobi is broken (#1130) 2025-10-20 20:44:24 -07:00
Alex Xu
5813f914fc split image in half for better color detection comment 2025-10-20 19:29:54 -07:00
Alex Xu
530ae410d4 detect silver color (#1129) 2025-10-20 18:38:35 -07:00
Alex Xu
a7428f18b6 Fine tune color detection (#1126)
* initial commit

* refactor

* pdf colorspace note

* refactor

* webtoons are always color
2025-10-20 17:09:01 -07:00
Alex Xu
06194b33ad disable default gamma correction/darkening of 1.8 (1.0 is disabled) (#1030)
* disable default gamma correction of 1.8 to 1.0 (disabled)

* update gamma tooltip

* custom gamma
2025-10-20 11:42:59 -07:00
Alex Xu
80ea17ff02 webtoon increase minimum vertical blank space threshold 2025-10-20 10:53:10 -07:00
dependabot[bot]
e0466ad040 Bump actions/setup-node from 5 to 6 (#1125)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-19 19:32:25 -07:00
Alex Xu
df924824aa Update package-windows7.yml 2025-10-19 11:33:32 -07:00
Alex Xu
82f1c1bb0a remove webtoon cover (#1124) 2025-10-19 11:27:28 -07:00
Alex Xu
6f801a3334 fix all other webtoon bugs (#1123)
* adjust webtoon thresholds

* fix arbitrary width webtoons

* virtual pages can exceed height by 20%

* prevent webtoon bad options

* adjust virtual page ratio

* don't change history too much

* GUI change

* add TODO

* add scroll illusion
2025-10-19 11:15:14 -07:00
Alex Xu
ee8fc77ca9 webtoon avoid tiny panels (#1122) 2025-10-17 14:06:36 -07:00
Alex Xu
1da06b43e2 windows7-build 2025-10-16 22:56:00 -07:00
Alex Xu
d28740491e adjust webtoon edge detection threshold 2025-10-16 22:47:37 -07:00
Alex Xu
e2b98db1a2 fix webtoon perf_counters 2025-10-16 22:06:21 -07:00
Alex Xu
4c5ec95a9b fix webtoon source directory is empty 2025-10-16 22:06:21 -07:00
Alex Xu
e56612228c make webtoon faster/more accurate (#1117) 2025-10-16 11:44:40 -07:00
Alex Xu
3470d01367 fix webtoon panel detection by doubling max height (#1115) 2025-10-15 14:31:06 -07:00
Alex Xu
a795a84899 raise maximum dimensions from 2400x3840 tp 3200x5120 (#1114) 2025-10-14 14:24:53 -07:00
Alex Xu
ef44f6f285 Differentiate source directory is empty errors (#1113) 2025-10-14 14:13:36 -07:00
Alex Xu
6acdebae3c fix webtoon cannot identify image file (#1112) 2025-10-14 13:03:02 -07:00
Alex Xu
dc8136f6fd fix webtoon panel detection (#1111) 2025-10-14 12:51:40 -07:00
kiryl85
28ec2d23fc Remove duplicated import from PySide6.QtWidgets (#1107)
- QApplication imported twice
2025-10-13 13:40:17 -07:00
dependabot[bot]
108363ce2f Bump github/codeql-action from 3 to 4 (#1105)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 18:49:51 -07:00
Alex Xu
543fb81027 limit pyside6 to 6.9 2025-10-12 09:48:42 -07:00
Airat Valiullin
e6c6c05d40 fix typo: change "profil" to "profile" in the cli and README (#1103) 2025-10-12 08:58:24 -07:00
kiryl85
18cd55f439 Open metadata editor after doubleclick on source (#1102) 2025-10-10 19:38:41 -07:00
Alex Xu
7ce4438886 Merge pull request #1101 from kiryl85/update-title-generation
Update title generation
2025-10-09 11:10:49 -07:00
kiryl
841ec517ac Allow custom title on fusion or single source only 2025-10-09 09:17:43 +02:00
kiryl
4614fdd07a Add Title gui
Allow to set custom title on mobi without editing metadata.
2025-10-09 09:15:29 +02:00
kiryl
d0b72e1c83 Update Metadata Title checkbox behaviour
- `Metadata Title` is now tristate checkbox, where metadata title
is combined with generated title or used explicit or just ignored
2025-10-09 09:15:29 +02:00
Alex Xu
e11755b118 Update README.md 2025-10-07 19:34:51 -07:00
Alex Xu
a9bfc152bf Update README.md 2025-10-06 23:51:31 -07:00
Alex Xu
f228ed261e Update README.md 2025-10-06 23:49:45 -07:00
Alex Xu
b8020758a6 Update README.md 2025-10-06 23:46:16 -07:00
Alex Xu
6540638e03 Update README.md 2025-10-06 23:37:03 -07:00
Alex Xu
565e4aec45 don't modify metadata in calibre note 2025-10-06 23:36:00 -07:00
Alex Xu
0453fcfc90 add blank page to readme 2025-10-06 23:34:56 -07:00
kiryl85
a6ee867da2 fix bookmarks generation (#1100)
- generate bookmarks if they are present in
ComicInfo.xml file
2025-10-06 13:01:39 -07:00
Alex Xu
1e4434fc7c fix various metadata editor bugs (#1099) 2025-10-05 18:05:16 -07:00
kiryl85
7122945fa2 Add Title field to Metadata editor (#1096)
* Add Title field to Metadata editor

* Add missing colon to Title label
2025-10-05 16:42:49 -07:00
Alex Xu
788163f3df label win7 as legacy 2025-10-02 10:22:09 -07:00
Alex Xu
3fa90735d5 fix interpanel crop with color images with black borders; (#1094) 2025-09-30 10:27:52 -07:00
Luís Melo
e6cd26c773 feat: update image.py to support remarkable paper pro move profile (#1092)
* feat: update image.py to support remarkable paper pro move profile

* fix: update KCC_gui.py to include "reMarkable Paper Pro Move"
2025-09-29 10:58:31 -07:00
Alex Xu
e92b5c74de fix color detection false positives (#1088) 2025-09-21 09:12:41 -07:00
Alex Xu
a031e4622e fix multiframe gif images (#1089) 2025-09-21 09:12:26 -07:00
Alex Xu
ae1989335e Update README.md 2025-09-16 12:50:00 -07:00
Alex Xu
a7cb3565aa Merge pull request #1087 from jtronicus/fix-zip-folders
Fix makeZIP function to allow for special characters in folder names
2025-09-16 12:48:27 -07:00
Alex Xu
ec8098291f cwd in all 7z locations 2025-09-15 14:50:35 -07:00
Alex Xu
297e8757f0 Update README.md 2025-09-15 11:24:11 -07:00
Jtronicus
2cfd6d9728 Fix makeZIP function to allow for special characters in folder names 2025-09-15 07:23:17 -07:00
Alex Xu
5733786dc6 Update README.md 2025-09-13 20:22:59 -07:00
Alex Xu
8a862f11ac bump to 9.1.0 2025-09-11 14:20:09 -07:00
Alex Xu
c32620cfeb Add manga bundle announcements (#1083)
* add announcements

* fix url

* don't check version if frozen

* comment

* update

* use bindle and python 3.11

* privacy update
2025-09-11 13:59:12 -07:00
dependabot[bot]
512cac7f8a Bump actions/setup-python from 5 to 6 (#1077)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 13:26:09 -07:00
dependabot[bot]
5e86acc740 Bump actions/setup-node from 4 to 5 (#1078)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 13:25:59 -07:00
Alex Xu
20388304e8 PDF input: spreadshift behavior is inverted so Humble PDF's don't need to check it (#1082)
* pdf input: spreadshift behavior is inverted so Humble PDF's don't need to check it

* remove pdf spreadshift from readme
2025-09-11 13:25:36 -07:00
Alex Xu
420bed995b fix mars pdf input (#1081) 2025-09-11 13:03:27 -07:00
Alex Xu
bc92c2dd85 Revert "increase color threshold from 20 to 50 (#1075)" (#1079)
This reverts commit a1f4e040ba.
2025-09-08 20:16:24 -07:00
Alex Xu
a1f4e040ba increase color threshold from 20 to 50 (#1075) 2025-09-03 07:40:23 -07:00
Alex Xu
137d53672a fix crop checkbox 2025-09-02 11:54:11 -07:00
Alex Xu
0cc75ab1e7 ignore osx10.11 2025-08-30 12:12:01 -07:00
Alex Xu
4cecf6fc4d Experimental Windows 7 support (#1069)
* win7

* windows-2022

* downgrade

* bat

* lower requirements

* downgrade pyside6

* downgrade pyside6 more

* delete

* fix win7

* don't crash when settings load fails

* remove with_stem
2025-08-27 16:10:03 -07:00
Alex Xu
2f0c9ae95d partially checked w border are untouched 2025-08-26 15:58:05 -07:00
dependabot[bot]
b856a176b0 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-26 15:51:33 -07:00
Alex Xu
e50163fb59 Merge pull request #1071 from axu2/pdf-metadata
PDF input/output Metadata support
2025-08-26 15:50:01 -07:00
Alex Xu
0ab20a5ce3 pdf metadata 2025-08-26 15:48:04 -07:00
Alex Xu
9c69a6fdcc rename comicinfotitle to metadata title 2025-08-26 14:43:15 -07:00
Alex Xu
26f0bf9989 Update README.md 2025-08-24 08:35:14 -07:00
Alex Xu
b3db3256d6 Create package-windows7.yml 2025-08-22 20:21:22 -07:00
Alex Xu
96a92fb9bb Update setup.py 2025-08-21 21:51:56 -07:00
Alex Xu
18f02df3a1 minimum macos is 10.14 mojave (#1068) 2025-08-21 21:28:27 -07:00
dependabot[bot]
235db43fa6 Bump actions/checkout from 4 to 5 (#1066) 2025-08-17 19:30:11 -07:00
Alex Xu
cab7cae714 pdf output: margin fill (#1065) 2025-08-14 09:12:45 -07:00
Alex Xu
a41f947030 remove print 2025-08-13 22:23:43 -07:00
dependabot[bot]
d2828877af Bump signpath/github-action-submit-signing-request from 1.2 to 1.3 (#1062)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1.2 to 1.3.
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1.2...v1.3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '1.3'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 10:40:11 -07:00
Belgian Coder
704dcd6dbe Add PDF output support (#1032)
* Add PDF output support

* optimize

* use with statement

* OS_SORT_KEY

* fix import

* simplify

* fix None

* fix conditional

---------

Co-authored-by: Belgian Coder <>
Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-08-11 10:36:33 -07:00
Its-my-right
6a7500441d Prevents rainbow eraser crash on images with 1px dimensions
Don't apply rainbow eraser on images with 1px dimensions
2025-08-06 12:12:21 -07:00
Alex Xu
753eeb64eb Update README.md 2025-08-05 07:30:51 -07:00
Alex Xu
a63d9e3ad0 fix cbz split 2025-08-04 09:18:33 -07:00
Alex Xu
cc01dc611a add macOS 10.13+ legacy support (#1055)
* add macOS 10.13+ legacy support

* fix space
2025-08-04 09:15:11 -07:00
Alex Xu
9a605c2d8a dot_clean in c2p 2025-08-04 09:00:14 -07:00
Alex Xu
a7005748c7 rmtree ignores errors 2025-08-04 08:36:43 -07:00
Alex Xu
55193119fb 10% page number crop 2025-07-30 10:39:28 -07:00
Alex Xu
741cab68f3 min crop with page numbers (#1050)
* min crop with page numbers

* fix order
2025-07-26 19:13:15 -07:00
Alex Xu
7f8f3e67b7 Update README.md 2025-07-26 07:36:58 -07:00
Alex Xu
be12661f38 only flatten super long paths on Windows 2025-07-26 07:26:38 -07:00
Alex Xu
06e2ee2968 ignore 2% of pixels near edges and don't crop more than 10% per edge (#1044)
* max crop and ignore edges

* make edge_bbox neater
2025-07-26 07:26:10 -07:00
Alex Xu
27296565a3 display non kindle only 2025-07-26 07:17:45 -07:00
Alex Xu
bb70337a35 crop_main_cover ratio adjustment 2025-07-26 07:12:44 -07:00
Alex Xu
cf0586ae70 Update README.md 2025-07-22 17:27:09 -07:00
Alex Xu
4100141b46 Update README.md 2025-07-22 10:38:43 -07:00
Alex Xu
8eb81b7d67 fix 5.15.3 typo 2025-07-22 10:38:02 -07:00
Alex Xu
e2dbc05a83 add OCLP note 2025-07-22 10:36:47 -07:00
Alex Xu
50b82786a1 install pymupdf on armv7 2025-07-22 09:34:59 -07:00
Alex Xu
88d1643f64 Update README.md 2025-07-20 22:56:55 -07:00
Alex Xu
ddf2fa360f Update README.md 2025-07-20 22:56:15 -07:00
Alex Xu
43e974f20d add humble/fanatical pdf note 2025-07-20 22:55:15 -07:00
Alex Xu
e3a3e9b3c2 bump to 9.0.0 2025-07-20 21:59:24 -07:00
Alex Xu
380dc5c42c add pymupdf requirement 2025-07-20 21:57:08 -07:00
Alex Xu
8ab7520754 nested archives are not supported 2025-07-20 13:49:18 -07:00
Alex Xu
3cd6e09bcb extract_image instead of Pixmap if possible 20x faster 2025-07-20 12:20:13 -07:00
Alex Xu
cb5f4db5c4 don't try catch so many layers of pdf 2025-07-20 12:04:25 -07:00
Its-my-right
f1ffb2c4e8 Update rainbow_artifacts_eraser.py
Enable odd width for BW image when using Rainbow Eraser by passing original image dimensions to irfft2
2025-07-20 11:16:18 -07:00
Its-my-right
26327728d0 Update rainbow_artifacts_eraser.py
add param s=luminance.shape to irfft2 to avoid dimensions error on luminance
2025-07-20 10:55:09 -07:00
Alex Xu
61bfb0a51f fix pdf resolution 2025-07-20 09:19:40 -07:00
Alex Xu
2f03119926 add mupdf perf_counters 2025-07-20 09:02:19 -07:00
Alex Xu
8f08c63f4e pdf input uses all cpu cores 2025-07-20 09:02:19 -07:00
Adrian
eb24a400b4 Improve pdf support by using mupdf (#983)
* Improve pdf support with mupdf

* parallel page ranges not pages

* fix black blank

* remove full=True

* add TODO

* fix doc close

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-07-18 17:45:20 -07:00
Its-my-right
cc2eb9dcf3 Feature/rainbow eraser for color images (#1034)
* Add rainbow_artifacts_eraser helper

This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform

* Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens

* High performance improvements by using rfft2 instead of fft2

* Fine-tuned the settings and added the perpendicular direction for a better final rendering

The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect.

Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle).

* Revert the addition of perpendicular angles and lower attenuation_factor

It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale).

The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles.

In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images.

It should be my latest commit for this feature

* Also attenuate high frequencies at 45°

CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions

* fix imports

* Update comic2ebook.py

Calculate is_color with (opt.forcecolor and img.color)

pass is_color to img.optimizeForDisplay

* Update image.py

Remove color check condition, because now we process colored images too.

Pass is_color to erase_rainbow_artifacts

* Update rainbow_artifacts_eraser.py

Add support for colored images: Convert to YUV, extract luminance channel, do FFT -> Filter -> IFFT on luminance channel, insert back to YUV, convert back to RGB

To maximize compatibility until we know for sure the orientation of CFA for each device, filtering is now done on 135° + 45° axis

After more testing, attenuation_factor is decreased to 0.10

* Update comic2ebook.py

Rename rainbow eraser param

* Update image.py

rename rainbow eraser param

* Update KCC.ui

Rename rainbow eraser checkbox and tooltip

* Update KCC_ui.py

Rename erase rainbow checkbox and tooltip

* Update KCC_gui.py

Rename erase rainbow checkbox and option

* Update README.md

rename erase rainbow param

* Update KCC_gui.py

correct param name for eraserainbow

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-07-18 10:48:56 -07:00
Alex Xu
d1a443b3d8 Update README.md 2025-07-18 10:23:36 -07:00
Alex Xu
f8c35ce634 Update README.md 2025-07-18 10:23:01 -07:00
Alex Xu
580a800d25 Update README.md 2025-07-18 10:18:01 -07:00
Alex Xu
023797f012 fix imports 2025-07-18 07:38:50 -07:00
Its-my-right
63a18627d3 Also attenuate high frequencies at 45°
CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions
2025-07-18 07:38:50 -07:00
Its-my-right
0d967084a0 Revert the addition of perpendicular angles and lower attenuation_factor
It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale).

The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles.

In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images.

It should be my latest commit for this feature
2025-07-18 07:38:50 -07:00
Its-my-right
2da5b11858 Fine-tuned the settings and added the perpendicular direction for a better final rendering
The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect.

Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle).
2025-07-18 07:38:50 -07:00
Its-my-right
f6d10337d8 High performance improvements by using rfft2 instead of fft2 2025-07-18 07:38:50 -07:00
Its-my-right
cf047ecf6f Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens 2025-07-18 07:38:50 -07:00
Its-my-right
e7ee8bed9d Add rainbow_artifacts_eraser helper
This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform
2025-07-18 07:38:50 -07:00
Alex Xu
9c8a1759cf update level comment 2025-07-13 22:29:11 -07:00
Alex Xu
6299754964 draft: add black point level (#1028)
* initial black point

* convert to L

* add GUI
2025-07-13 21:52:17 -07:00
Alex Xu
a3db86a29b convert to grayscale as last step 2025-07-13 15:23:54 -07:00
Alex Xu
67714a9b06 fix color autocontrast (#1026) 2025-07-11 16:28:19 -07:00
Alex Xu
95f9a3cda9 put dot_clean in sanitize 2025-07-10 23:07:15 -07:00
Alex Xu
0e12fc30c6 restore lenient ComicInfo.xml handling (#1024) 2025-07-10 13:57:29 -07:00
Alex Xu
90c9ba7539 bump 8.0.4 2025-07-10 09:19:57 -07:00
Alex Xu
84da718167 more extraction fixes (#1023) 2025-07-10 09:14:06 -07:00
Alex Xu
fe7559e6a9 Add note about RTL CBZ output 2025-07-10 06:58:03 -07:00
Alex Xu
a79c740387 don't autocontrast color content (#1021) 2025-07-09 13:59:54 -07:00
Alex Xu
bc98eecae9 bump 8.0.3 2025-07-09 11:28:25 -07:00
Alex Xu
e7a07377ef add avif input support (#1019)
* add avif input support

* add avif
2025-07-09 11:27:58 -07:00
Alex Xu
07ef11013a fix cbz metadata (#1018) 2025-07-09 11:14:25 -07:00
Alex Xu
551fe6edbf bump 8.0.2 2025-07-05 07:41:08 -07:00
Alex Xu
dbf8a3ddbd fix ._cover.jpg on macos 15 (#1016) 2025-07-05 07:40:09 -07:00
Alex Xu
8f9e230b62 rotate first (#1015) 2025-07-04 19:52:34 -07:00
Alex Xu
36d9a4151e Update README.md 2025-07-04 14:23:28 -07:00
41 changed files with 4841 additions and 2192 deletions

View File

@@ -1,13 +1,40 @@
.git .git
.github .github
build build
dist dist
KindleComicConverter.egg-info KindleComicConverter.egg-info
.dockerignore .dockerignore
.gitignore .gitignore
.travis.yml .travis.yml
Dockerfile Dockerfile
venv venv
.venv
__pycache__/
*/__pycache__/
*.pyc
*.md *.md
LICENSE.txt *.txt
!requirements-docker.txt
MANIFEST.in MANIFEST.in
*.yml
*.spec
*.svg
*.jpg
*.json
gen_ui_files.bat
gen_ui_files.sh
gui/
icons/
kindlecomicconverter/KCC_gui.py
kindlecomicconverter/KCC_rc.py
kindlecomicconverter/KCC_ui_editor.py
kindlecomicconverter/KCC_ui.py

View File

@@ -38,11 +38,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,6 +69,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -1,34 +0,0 @@
name: Docker base
on:
workflow_dispatch:
push:
tags: [ 'docker-base-*' ]
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- 'docs/**'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build_and_push:
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
with:
docker_build_file: ./Dockerfile-base
platform_linux_arm32v7_enabled: true
platform_linux_arm64v8_enabled: true
platform_linux_amd64_enabled: true
push_enabled: true
build_nohealthcheck: false
ghcr_repo_owner: ${{ github.repository_owner }}
ghcr_repo: ${{ github.repository }}
build_latest: false
secrets:
ghcr_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,10 +1,10 @@
name: Docker name: Build and Publish Docker Image
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
# Publish semver tags as releases. tags:
tags: [ 'v*.*.*' ] - 'v*.*.*'
# Don't trigger if it's just a documentation update # Don't trigger if it's just a documentation update
paths-ignore: paths-ignore:
@@ -15,19 +15,53 @@ on:
- 'LICENSE' - 'LICENSE'
- '.gitattributes' - '.gitattributes'
- '.gitignore' - '.gitignore'
- '.dockerignore'
jobs: jobs:
build_and_push: build_and_publish_base_image:
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main runs-on: ubuntu-latest
with: steps:
platform_linux_arm32v7_enabled: true - name: Checkout
platform_linux_arm64v8_enabled: true uses: actions/checkout@v6
platform_linux_amd64_enabled: true
push_enabled: true - name: Login to GitHub Container Registry
build_nohealthcheck: false uses: docker/login-action@v3
ghcr_repo_owner: ${{ github.repository_owner }} with:
ghcr_repo: ${{ github.repository }} registry: ghcr.io
secrets: username: ${{ github.repository_owner }}
ghcr_token: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set Release Date
id: release_date
run: |
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/kcc
# Always creates the "latest" tag
flavor: |
latest=true
tags: |
type=ref,event=tag
type=raw,value=${{ steps.release_date.outputs.release_date }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: true
tags: |
${{ steps.meta.outputs.tags }}
cache-from: |
type=registry,ref=ghcr.io/ciromattia/kcc:cache
type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max

View File

@@ -25,9 +25,9 @@ jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'
@@ -59,7 +59,7 @@ jobs:
env: env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact - name: upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: AppImage name: AppImage
path: './*.AppImage*' path: './*.AppImage*'

View File

@@ -28,9 +28,9 @@ jobs:
os: [ macos-13, macos-14 ] os: [ macos-13, macos-14 ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'
@@ -69,7 +69,7 @@ jobs:
# apply provisioning profile # apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: 16 node-version: 16
- run: npm install -g appdmg - run: npm install -g appdmg
@@ -78,7 +78,7 @@ jobs:
run: | run: |
python setup.py build_binary python setup.py build_binary
- name: upload build - name: upload build
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: mac-os-build-${{ runner.arch }} name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg path: dist/*.dmg
@@ -89,7 +89,6 @@ jobs:
prerelease: true prerelease: true
generate_release_notes: true generate_release_notes: true
files: | files: |
LICENSE.txt
dist/*.dmg dist/*.dmg
- name: Clean up keychain and provisioning profile - name: Clean up keychain and provisioning profile
# TODO signing # TODO signing

View File

@@ -0,0 +1,66 @@
name: build KCC for osx legacy
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- '**.sh'
- 'docs/**'
- 'Dockerfile'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build:
strategy:
matrix:
os: [ macos-13 ]
runs-on: ${{ matrix.os }}
env:
# We need the official Python, because the GA ones only support newer macOS versions
# The deployment target is picked up by the Python build tools automatically
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- uses: actions/checkout@v6
- name: Get Python
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
- name: Install Python
run: |
sudo installer -pkg python.pkg -target /
- name: Install Python dependencies
run: |
python3 --version
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- uses: actions/setup-node@v6
with:
node-version: 16
- run: npm install -g appdmg
- name: build binary
run: |
python3 setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v5
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
files: |
LICENSE.txt
dist/*.dmg

View File

@@ -1,62 +0,0 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build KCC for windows with docker
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
jobs:
build:
strategy:
matrix:
entry: [ kcc-c2e, kcc-c2p ]
include:
- entry: kcc-c2e
capital: KCC_c2e
- entry: kcc-c2p
capital: KCC_c2p
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./${{ matrix.entry }}.spec
- name: rename binaries
run: |
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: windows-build-${{ matrix.entry }}
path: dist/windows/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/windows/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
files: |
LICENSE.txt
dist/windows/*.exe

View File

@@ -23,11 +23,21 @@ on:
jobs: jobs:
build: build:
strategy:
matrix:
entry: [ kcc, kcc-c2e, kcc-c2p ]
include:
- entry: kcc
command: build_binary
- entry: kcc-c2e
command: build_c2e
- entry: kcc-c2p
command: build_c2p
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'
@@ -40,15 +50,15 @@ jobs:
pip install certifi pyinstaller --no-binary pyinstaller pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary - name: build binary
run: | run: |
python setup.py build_binary python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact - name: upload-unsigned-artifact
id: upload-unsigned-artifact id: upload-unsigned-artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: windows-build name: windows-build-${{ matrix.entry }}
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2 uses: signpath/github-action-submit-signing-request@v2.0
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -65,5 +75,4 @@ jobs:
prerelease: true prerelease: true
generate_release_notes: true generate_release_notes: true
files: | files: |
LICENSE.txt
dist/*.exe dist/*.exe

60
.github/workflows/package-windows7.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build KCC for windows 7
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- '**.sh'
- 'docs/**'
- 'Dockerfile'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build:
runs-on: windows-2022
env:
WINDOWS_7: 1
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.8
cache: 'pip'
- name: Install dependencies
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements-win7.txt
pip install certifi pyinstaller --no-binary pyinstaller
.\gen_ui_files.bat
- name: build binary
run: |
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v5
with:
name: windows7-build
path: dist/*.exe
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
files: |
dist/*.exe

3
.gitignore vendored
View File

@@ -8,6 +8,9 @@ dist/
build/ build/
KindleComicConverter*.egg-info/ KindleComicConverter*.egg-info/
.idea/ .idea/
.vscode/
win7
osx10.11
/venv/ /venv/
/kindlegen* /kindlegen*
/kcc.bat /kcc.bat

View File

@@ -1,19 +1,77 @@
# Select final stage based on TARGETARCH ARG # STAGE 1: BUILDER
FROM ghcr.io/ciromattia/kcc:docker-base-20241116 # Contains all build tools and dev dependencies, will be discarded
LABEL com.kcc.name="Kindle Comic Converter" FROM python:3.13-slim-bullseye AS builder
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
LABEL org.opencontainers.image.description='Kindle Comic Converter'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.authors='darodi'
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.vendor='ciromattia'
LABEL org.opencontainers.image.licenses='ISC'
LABEL org.opencontainers.image.title="Kindle Comic Converter"
COPY . /opt/kcc # Install system dependencies
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION RUN set -x && \
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev python3-setuptools python3-wheel" && \
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"] RUN \
set -x && \
python -m venv /opt/venv && \
. /opt/venv/bin/activate && \
pip install --upgrade pip
# Install numpy first, as it is unlikely to change and takes too long to compile
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir numpy==2.3.4
# Install PyMuPDF separately, as it is likely to change but still takes too long to compile
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir PyMuPDF==1.26.6
# Install Python dependencies using virtual environment
COPY requirements-docker.txt .
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir -r requirements-docker.txt
# STAGE 2: FINAL
# Clean, small and secure image with only runtime dependencies
FROM python:3.13-slim-bullseye
# Install runtime dependencies only
RUN \
set -x && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends p7zip-full && \
rm -rf /var/lib/apt/lists/*
# Copy artifacts from builder
COPY --from=builder /opt/venv /opt/venv
COPY . /opt/kcc/
WORKDIR /opt/kcc
ENV PATH="/opt/venv/bin:$PATH"
# Setup executable and version file
RUN \
chmod +x /opt/kcc/entrypoint.sh && \
ln -s /opt/kcc/kcc-c2e.py /usr/local/bin/c2e && \
ln -s /opt/kcc/kcc-c2p.py /usr/local/bin/c2p && \
ln -s /opt/kcc/entrypoint.sh /usr/local/bin/entrypoint && \
ln -s /opt/kcc/kindlegen/kindlegen /usr/local/bin/kindlegen && \
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
LABEL com.kcc.name="Kindle Comic Converter" \
com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" \
org.opencontainers.image.title="Kindle Comic Converter" \
org.opencontainers.image.description='Kindle Comic Converter' \
org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' \
org.opencontainers.image.source='https://github.com/ciromattia/kcc' \
org.opencontainers.image.authors='Darodi and José Cerezo' \
org.opencontainers.image.url='https://github.com/ciromattia/kcc' \
org.opencontainers.image.vendor='ciromattia' \
org.opencontainers.image.licenses='ISC'
ENTRYPOINT ["entrypoint"]
CMD ["-h"] CMD ["-h"]

View File

@@ -1,164 +0,0 @@
FROM --platform=linux/amd64 python:3.13-slim-bullseye as compile-amd64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 && \
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install -r /opt/kcc/requirements.txt
######################################################################################
FROM --platform=linux/arm64 python:3.13-slim-bullseye as compile-arm64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install -r /opt/kcc/requirements.txt
######################################################################################
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as compile-armv7
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libffi-dev) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy
######################################################################################
FROM --platform=linux/amd64 python:3.13-slim-bullseye as build-amd64
COPY --from=compile-amd64 /opt/venv /opt/venv
FROM --platform=linux/arm64 python:3.13-slim-bullseye as build-arm64
COPY --from=compile-arm64 /opt/venv /opt/venv
FROM --platform=linux/arm/v7 python:3.13-slim-bullseye as build-armv7
COPY --from=compile-armv7 /opt/venv /opt/venv
######################################################################################
# Select final stage based on TARGETARCH ARG
FROM build-${TARGETARCH}${TARGETVARIANT}
LABEL com.kcc.name="Kindle Comic Converter base image"
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
LABEL org.opencontainers.image.description='Kindle Comic Converter base image'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.authors='darodi'
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.vendor='ciromattia'
LABEL org.opencontainers.image.licenses='ISC'
LABEL org.opencontainers.image.title="Kindle Comic Converter"
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y p7zip-full unrar-free && \
ln -s /app/kindlegen /bin/kindlegen && \
echo docker-base-20241116 > /IMAGE_VERSION

View File

@@ -11,18 +11,22 @@
like Kindle, Kobo, ReMarkable, and more. like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, Pages display in fullscreen without margins,
with proper fixed layout support. with proper fixed layout support.
Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs. Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ. Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
If your source are super high resolution DRM-free PDFs from Kodansha/Humble Bundle/Fanatical, **NEW**: PDF output is now supported for direct conversion to reMarkable devices!
you'll need to first [convert the PDFs to CBZ](https://github.com/ciromattia/kcc/issues/680) for use in KCC. When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
which can be directly converted by KCC.
Its main feature is various optional image processing steps to look good on eink screens, Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens. which have different requirements than normal LCD screens.
Combining that with downscaling to your specific device's screen resolution Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink. can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small storage capacities. on underpowered ereaders with small memory and storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as: KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain. 1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
@@ -88,19 +92,34 @@ Click on **Assets** of the latest release.
You probably want either You probably want either
- `KCC_*.*.*.exe` (Windows) - `KCC_*.*.*.exe` (Windows)
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later) - `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip) - `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 12+)
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
The `c2e` and `c2p` versions are command line tools for power users. The `c2e` and `c2p` versions are command line tools for power users.
On Windows 11, you may need to run in compatibility mode for an older Windows version.
On Mac, right click open to get past the security warning. On Mac, right click open to get past the security warning.
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
## FAQ ## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- Blank pages?
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
- All options have additional information in tooltips if you hover over the option. - All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB - To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Kindle panel view not working?
- Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted? - Colors inverted?
- Disable Kindle dark mode - Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS - Cannot connect Kindle Scribe or 2024+ Kindle to macOS
@@ -109,11 +128,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
- How to make AZW3 instead of MOBI? - How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons. - The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678) - [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
- Image too dark? - Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0 - The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
- Huge margins / slow page turns? - Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB. - You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -215,13 +231,16 @@ MAIN:
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others] the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
PROCESSING: PROCESSING:
-n, --noprocessing Do not modify image and ignore any profil or processing option -n, --noprocessing Do not modify image and ignore any profile or processing option
-u, --upscale Resize images smaller than device's resolution -u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution -s, --stretch Stretch images to device's resolution
-r SPLITTER, --splitter SPLITTER -r SPLITTER, --splitter SPLITTER
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0] Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
-g GAMMA, --gamma GAMMA -g GAMMA, --gamma GAMMA
Apply gamma correction to linearize the image [Default=Auto] Apply gamma correction to linearize the image [Default=Auto]
--autolevel Set most common dark pixel value to be black point for leveling.
--noautocontrast Disable autocontrast
--colorautocontrast Force autocontrast for all pages. Skipped when near blacks and whites don't exist
-c CROPPING, --cropping CROPPING -c CROPPING, --cropping CROPPING
Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2] Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
--cp CROPPINGP, --croppingpower CROPPINGP --cp CROPPINGP, --croppingpower CROPPINGP
@@ -244,17 +263,19 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file Output generated file to specified directory or file
-t TITLE, --title TITLE -t TITLE, --title TITLE
Comic title [Default=filename or directory name] Comic title [Default=filename or directory name]
--comicinfotitle Write title from ComicInfo.xml --metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
-f FORMAT, --format FORMAT -f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto] Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
-b BATCHSPLIT, --batchsplit BATCHSPLIT -b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment --spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option. --norotate Do not rotate double page spreads in spread splitter option.
--reducerainbow Reduce rainbow effect on color eink by slightly blurring images --rotatefirst Put rotated spread first in spread splitter option.
--filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
CUSTOM PROFILE: CUSTOM PROFILE:
--customwidth CUSTOMWIDTH --customwidth CUSTOMWIDTH
@@ -301,6 +322,8 @@ Then use the `gen_ui_files` scripts to autogenerate the python UI.
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785 An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
Do not use `git merge` to merge master from upstream, Do not use `git merge` to merge master from upstream,
use the "Sync fork" button on your fork on GitHub in your branch use the "Sync fork" button on your fork on GitHub in your branch
to avoid weird looking merges in pull requests. to avoid weird looking merges in pull requests.
@@ -391,7 +414,7 @@ Older links (dead):
## PRIVACY ## PRIVACY
**KCC** is initiating internet connections in two cases: **KCC** is initiating internet connections in two cases:
* During startup - Version check. * During startup - Version check and announcement check.
* When error occurs - Automatic reporting on Windows and macOS. * When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES ## KNOWN ISSUES
@@ -400,3 +423,6 @@ Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
## COPYRIGHT ## COPYRIGHT
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu. Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details. **KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
## Verification
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180

22
entrypoint.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
set -e
MODE=${KCC_MODE:-c2e}
case "$MODE" in
"c2e")
echo "Starting C2E..."
exec c2e "$@"
;;
"c2p")
echo "Starting C2P..."
exec c2p "$@"
;;
*)
echo "Error: Unknown mode '$MODE'" >&2
exit 1
;;
esac

View File

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

View File

@@ -27,6 +27,10 @@
<file>../icons/convert.png</file> <file>../icons/convert.png</file>
<file>../icons/document_new.png</file> <file>../icons/document_new.png</file>
<file>../icons/folder_new.png</file> <file>../icons/folder_new.png</file>
</qresource>
<qresource prefix="Brand">
<file>../icons/kofi_symbol.png</file> <file>../icons/kofi_symbol.png</file>
<file>../icons/Humble_H-Red.png</file>
<file>../icons/Bindle_Red.png</file>
</qresource> </qresource>
</RCC> </RCC>

1023
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -62,56 +62,66 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QLineEdit" name="volumeLine"/> <widget class="QLineEdit" name="volumeLine"/>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Number:</string> <string>Number:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="QLineEdit" name="numberLine"/> <widget class="QLineEdit" name="numberLine"/>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_4"> <widget class="QLabel" name="label_4">
<property name="text"> <property name="text">
<string>Writer:</string> <string>Writer:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="writerLine"/> <widget class="QLineEdit" name="writerLine"/>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Penciller:</string> <string>Penciller:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<widget class="QLineEdit" name="pencillerLine"/> <widget class="QLineEdit" name="pencillerLine"/>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="text"> <property name="text">
<string>Inker:</string> <string>Inker:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="6" column="1">
<widget class="QLineEdit" name="inkerLine"/> <widget class="QLineEdit" name="inkerLine"/>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>Colorist:</string> <string>Colorist:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<widget class="QLineEdit" name="coloristLine"/> <widget class="QLineEdit" name="coloristLine"/>
</item> </item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="titleLine"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

BIN
icons/Bindle_Red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
icons/Humble_H-Red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -17,11 +17,12 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
from datetime import datetime, timezone
import itertools import itertools
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QApplication, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog) from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer) from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os import os
@@ -124,7 +125,7 @@ class Icons:
self.EPUBFormat = QIcon() self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off) self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon() self.KFXFormat = QIcon()
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off) self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.info = QIcon() self.info = QIcon()
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off) self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
@@ -136,6 +137,15 @@ class Icons:
self.programIcon = QIcon() self.programIcon = QIcon()
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off) self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.kofi = QIcon()
self.kofi.addPixmap(QPixmap(":/Brand/icons/kofi_symbol.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.humble = QIcon()
self.humble.addPixmap(QPixmap(":/Brand/icons/Humble_H-Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.bindle = QIcon()
self.bindle.addPixmap(QPixmap(":/Brand/icons/Bindle_Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
class VersionThread(QThread): class VersionThread(QThread):
def __init__(self): def __init__(self):
@@ -150,19 +160,52 @@ class VersionThread(QThread):
def run(self): def run(self):
try: try:
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json() # unauthenticated API requests limit is 60 req/hour
if getattr(sys, 'frozen', False):
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
html_url = json_parser["html_url"] html_url = json_parser["html_url"]
latest_version = json_parser["tag_name"] latest_version = json_parser["tag_name"]
latest_version = re.sub(r'^v', "", latest_version) latest_version = re.sub(r'^v', "", latest_version)
if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \ if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \
or ("b" in __version__ or ("b" in __version__
and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))): and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))):
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning', MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
False) False)
except Exception: except Exception:
return pass
try:
announcements = requests.get('https://api.github.com/repos/axu2/kcc-messages/contents/links.json',
headers={
'Accept': 'application/vnd.github.raw+json',
'X-GitHub-Api-Version': '2022-11-28'}).json()
for category, payloads in announcements.items():
for payload in payloads:
expiration = datetime.fromisoformat(payload['expiration'])
if expiration < datetime.now(timezone.utc):
continue
delta = expiration - datetime.now(timezone.utc)
time_left = f"{delta.days} day(s) left"
icon = 'info'
if category == 'humbleMangaBundles':
icon = 'humble'
if category == 'humbleComicBundles':
icon = 'bindle'
if category == 'kofi':
icon = 'kofi'
message = f"<b>{payload.get('name')}</b>"
if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'):
message += f': {time_left}'
if category == 'humbleBundles':
message += ' [referral]'
MW.addMessage.emit(message, icon , False)
except Exception as e:
print(e)
def setAnswer(self, dialoganswer): def setAnswer(self, dialoganswer):
self.answer = dialoganswer self.answer = dialoganswer
@@ -247,11 +290,29 @@ class WorkerThread(QThread):
options.upscale = True options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09: if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue) options.gamma = float(GUI.gammaValue)
options.cropping = GUI.croppingBox.checkState().value if GUI.autoLevelBox.isChecked():
options.autolevel = True
if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
options.noautocontrast = True
elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
options.colorautocontrast = True
if GUI.croppingBox.isChecked():
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
options.cropping = 1
else:
options.cropping = 2
else:
options.cropping = 0
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked: if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue) options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value() options.preservemargin = GUI.preserveMarginBox.value()
options.interpanelcrop = GUI.interPanelCropBox.checkState().value if GUI.interPanelCropBox.isChecked():
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
options.interpanelcrop = 1
else:
options.interpanelcrop = 2
else:
options.interpanelcrop = 0
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked: elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
@@ -260,14 +321,16 @@ 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(): if GUI.eraseRainbowBox.isChecked():
options.reducerainbow = True options.eraserainbow = True
if GUI.maximizeStrips.isChecked(): if GUI.maximizeStrips.isChecked():
options.maximizestrips = True options.maximizestrips = True
if GUI.disableProcessingBox.isChecked(): if GUI.disableProcessingBox.isChecked():
options.noprocessing = True options.noprocessing = True
if GUI.comicinfoTitleBox.isChecked(): if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.comicinfotitle = True options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
options.metadatatitle = 2
if GUI.deleteBox.isChecked(): if GUI.deleteBox.isChecked():
options.delete = True options.delete = True
if GUI.spreadShiftBox.isChecked(): if GUI.spreadShiftBox.isChecked():
@@ -278,6 +341,8 @@ class WorkerThread(QThread):
options.filefusion = False options.filefusion = False
if GUI.noRotateBox.isChecked(): if GUI.noRotateBox.isChecked():
options.norotate = True options.norotate = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked: elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
@@ -287,6 +352,8 @@ class WorkerThread(QThread):
options.customheight = str(GUI.heightBox.value()) options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '': if GUI.targetDirectory != '':
options.output = GUI.targetDirectory options.output = GUI.targetDirectory
if GUI.titleEdit.text():
options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text(): if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text()) options.author = str(GUI.authorEdit.text())
if GUI.chunkSizeCheckBox.isChecked(): if GUI.chunkSizeCheckBox.isChecked():
@@ -310,16 +377,25 @@ class WorkerThread(QThread):
except Exception as e: except Exception as e:
print('Fusion Failed. ' + str(e)) print('Fusion Failed. ' + str(e))
MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True) MW.addMessage.emit('Fusion Failed. ' + str(e), 'error', True)
for job in currentJobs: elif len(currentJobs) > 1 and options.title != 'defaulttitle':
currentJobs.clear()
error_message = 'Process Failed. Custom title can\'t be set when processing more than 1 source.\nDid you forget to check fusion?'
print(error_message)
MW.addMessage.emit(error_message, 'error', True)
for i, job in enumerate(currentJobs, start=1):
job_progress_number = f'[{i}/{len(currentJobs)}] '
sleep(0.5) sleep(0.5)
if not self.conversionAlive: if not self.conversionAlive:
self.clean() self.clean()
return return
self.errors = False self.errors = False
MW.addMessage.emit('<b>Source:</b> ' + job, 'info', False) MW.addMessage.emit(f'<b>{job_progress_number}Source:</b> ' + job, 'info', False)
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False) MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files' GUI.progress.content = 'Creating CBZ files'
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files', 'info', False)
GUI.progress.content = 'Creating PDF files'
else: else:
MW.addMessage.emit('Creating EPUB files', 'info', False) MW.addMessage.emit('Creating EPUB files', 'info', False)
GUI.progress.content = 'Creating EPUB files' GUI.progress.content = 'Creating EPUB files'
@@ -327,7 +403,7 @@ class WorkerThread(QThread):
jobargv.append(job) jobargv.append(job)
try: try:
comic2ebook.options = comic2ebook.checkOptions(copy(options)) comic2ebook.options = comic2ebook.checkOptions(copy(options))
outputPath = comic2ebook.makeBook(job, self) outputPath = comic2ebook.makeBook(job, self, job_progress_number)
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
except UserWarning as warn: except UserWarning as warn:
if not self.conversionAlive: if not self.conversionAlive:
@@ -364,10 +440,12 @@ class WorkerThread(QThread):
GUI.progress.content = '' GUI.progress.content = ''
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else: else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format: if 'MOBI' in gui_current_format:
MW.progressBarTick.emit('Creating MOBI files') MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1)) MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
MW.progressBarTick.emit('tick') MW.progressBarTick.emit('tick')
MW.addMessage.emit('Creating MOBI files', 'info', False) MW.addMessage.emit('Creating MOBI files', 'info', False)
@@ -417,8 +495,10 @@ class WorkerThread(QThread):
k = kindle.Kindle(options.profile) k = kindle.Kindle(options.profile)
if k.path and k.coverSupport: if k.path and k.coverSupport:
for item in outputPath: for item in outputPath:
comic2ebook.options.covers[outputPath.index(item)][0].saveToKindle( cover = comic2ebook.options.covers[outputPath.index(item)][0]
k, comic2ebook.options.covers[outputPath.index(item)][1]) if cover:
cover.saveToKindle(
k, comic2ebook.options.covers[outputPath.index(item)][1])
MW.addMessage.emit('Kindle detected. Uploading covers... <b>Done!</b>', 'info', False) MW.addMessage.emit('Kindle detected. Uploading covers... <b>Done!</b>', 'info', False)
else: else:
GUI.progress.content = '' GUI.progress.content = ''
@@ -443,7 +523,7 @@ class WorkerThread(QThread):
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '': if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error') MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
if self.kindlegenErrorCode[0] == 23026: if self.kindlegenErrorCode[0] == 23026:
MW.addMessage.emit('Created EPUB file was too big.', 'error', False) MW.addMessage.emit('Created EPUB file was too big. Weird file structure?', 'error', False)
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error', MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
False) False)
if self.kindlegenErrorCode[0] == 3221226505: if self.kindlegenErrorCode[0] == 3221226505:
@@ -460,7 +540,7 @@ class WorkerThread(QThread):
if os.path.isfile(path): if os.path.isfile(path):
os.remove(path) os.remove(path)
elif os.path.isdir(path): elif os.path.isdir(path):
rmtree(path) rmtree(path, True)
GUI.progress.content = '' GUI.progress.content = ''
GUI.progress.stop() GUI.progress.stop()
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
@@ -536,31 +616,26 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.addItem(fname) GUI.jobList.addItem(fname)
GUI.jobList.scrollToBottom() GUI.jobList.scrollToBottom()
def selectFileMetaEditor(self): def selectFileMetaEditor(self, sname):
sname = '' if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier: if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath) dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '': if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml') sname = os.path.join(dname, 'ComicInfo.xml')
if sys.platform.startswith('win'): self.lastPath = os.path.dirname(sname)
sname = sname.replace('/', '\\')
self.lastPath = os.path.abspath(sname)
else:
if self.sevenzip:
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
else: else:
fname = [''] if self.sevenzip:
self.showDialog("Editor is disabled due to a lack of 7z.", 'error') fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>' 'Comic (*.cbz *.cbr *.cb7)')
' to enable metadata editing.', 'warning')
if fname[0] != '':
if sys.platform.startswith('win'):
sname = fname[0].replace('/', '\\')
else: else:
fname = ['']
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable metadata editing.', 'warning')
if fname[0] != '':
sname = fname[0] sname = fname[0]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir)) self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname != '': if sname:
try: try:
self.editor.loadData(sname) self.editor.loadData(sname)
except Exception as err: except Exception as err:
@@ -651,24 +726,42 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def togglewebtoonBox(self, value): def togglewebtoonBox(self, value):
if value: if value:
self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
self.addMessage('Try reading webtoon panels side by side in landscape!', 'info')
GUI.qualityBox.setEnabled(False) GUI.qualityBox.setEnabled(False)
GUI.qualityBox.setChecked(False) GUI.qualityBox.setChecked(False)
GUI.mangaBox.setEnabled(False) GUI.mangaBox.setEnabled(False)
GUI.mangaBox.setChecked(False) GUI.mangaBox.setChecked(False)
GUI.rotateBox.setEnabled(False) GUI.rotateBox.setEnabled(False)
GUI.rotateBox.setChecked(False) GUI.rotateBox.setChecked(False)
GUI.borderBox.setEnabled(False)
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.upscaleBox.setEnabled(False) GUI.upscaleBox.setEnabled(False)
GUI.upscaleBox.setChecked(True) GUI.upscaleBox.setChecked(False)
GUI.chunkSizeCheckBox.setEnabled(False) GUI.croppingBox.setEnabled(False)
GUI.chunkSizeCheckBox.setChecked(False) GUI.croppingBox.setChecked(False)
GUI.interPanelCropBox.setEnabled(False)
GUI.interPanelCropBox.setChecked(False)
GUI.autoLevelBox.setEnabled(False)
GUI.autoLevelBox.setChecked(False)
GUI.autocontrastBox.setEnabled(False)
GUI.autocontrastBox.setChecked(False)
else: else:
profile = GUI.profiles[str(GUI.deviceBox.currentText())] profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['PVOptions']: if profile['PVOptions']:
GUI.qualityBox.setEnabled(True) GUI.qualityBox.setEnabled(True)
GUI.mangaBox.setEnabled(True) GUI.mangaBox.setEnabled(True)
GUI.rotateBox.setEnabled(True) GUI.rotateBox.setEnabled(True)
GUI.upscaleBox.setEnabled(True) GUI.borderBox.setEnabled(True)
GUI.chunkSizeCheckBox.setEnabled(True) profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['Label'] != 'KS':
GUI.upscaleBox.setEnabled(True)
GUI.croppingBox.setEnabled(True)
GUI.interPanelCropBox.setEnabled(True)
GUI.autoLevelBox.setEnabled(True)
GUI.autocontrastBox.setEnabled(True)
GUI.autocontrastBox.setChecked(True)
def togglequalityBox(self, value): def togglequalityBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())] profile = GUI.profiles[str(GUI.deviceBox.currentText())]
@@ -682,10 +775,42 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
else: else:
GUI.upscaleBox.setEnabled(True) GUI.upscaleBox.setEnabled(True)
GUI.upscaleBox.setChecked(profile['DefaultUpscale']) GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
def toggleImageFormatBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if value == 1:
if profile['Label'] == 'KS':
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
for bad_format in ('MOBI', 'EPUB'):
if bad_format in current_format:
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
break
def togglechunkSizeCheckBox(self, value): def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value) GUI.chunkSizeWidget.setVisible(value)
def toggletitleEdit(self, value):
if value:
self.metadataTitleBox.setChecked(False)
def togglefileFusionBox(self, value):
if value:
GUI.metadataTitleBox.setChecked(False)
GUI.metadataTitleBox.setEnabled(False)
else:
GUI.metadataTitleBox.setEnabled(True)
def togglemetadataTitleBox(self, value):
if value:
GUI.titleEdit.setText(None)
def editSourceMetadata(self, item):
if item.icon().isNull():
sname = item.text()
if os.path.isdir(sname):
sname = os.path.join(sname, "ComicInfo.xml")
self.selectFileMetaEditor(sname)
def changeGamma(self, value): def changeGamma(self, value):
valueRaw = int(5 * round(float(value) / 5)) valueRaw = int(5 * round(float(value) / 5))
value = '%.2f' % (float(valueRaw) / 100) value = '%.2f' % (float(valueRaw) / 100)
@@ -713,15 +838,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.modeChange(1) self.modeChange(1)
GUI.colorBox.setChecked(profile['ForceColor']) GUI.colorBox.setChecked(profile['ForceColor'])
self.changeFormat() self.changeFormat()
GUI.gammaSlider.setValue(0)
self.changeGamma(0)
if not GUI.webtoonBox.isChecked(): if not GUI.webtoonBox.isChecked():
GUI.qualityBox.setEnabled(profile['PVOptions']) GUI.qualityBox.setEnabled(profile['PVOptions'])
GUI.upscaleBox.setChecked(profile['DefaultUpscale']) GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
if profile['Label'] == 'KS': if profile['Label'] == 'KS':
GUI.upscaleBox.setDisabled(True) GUI.upscaleBox.setDisabled(True)
else: else:
GUI.upscaleBox.setEnabled(True) if not GUI.webtoonBox.isChecked():
GUI.upscaleBox.setEnabled(True)
if profile['Label'] == 'KCS':
current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
for bad_format in ('MOBI', 'EPUB'):
if bad_format in current_format:
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
break
if not profile['PVOptions']: if not profile['PVOptions']:
GUI.qualityBox.setChecked(False) GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other': if str(GUI.deviceBox.currentText()) == 'Other':
@@ -747,6 +877,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.chunkSizeCheckBox.setChecked(False) GUI.chunkSizeCheckBox.setChecked(False)
elif not GUI.webtoonBox.isChecked(): elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True) GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
def stripTags(self, html): def stripTags(self, html):
s = HTMLStripper() s = HTMLStripper()
@@ -857,33 +989,36 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex()) self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
self.settings.setValue('startNumber', self.startNumber + 1) self.settings.setValue('startNumber', self.startNumber + 1)
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height())) self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState().value, self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'rotateBox': GUI.rotateBox.checkState().value, 'rotateBox': GUI.rotateBox.checkState(),
'qualityBox': GUI.qualityBox.checkState().value, 'qualityBox': GUI.qualityBox.checkState(),
'gammaBox': GUI.gammaBox.checkState().value, 'gammaBox': GUI.gammaBox.checkState(),
'croppingBox': GUI.croppingBox.checkState().value, 'autoLevelBox': GUI.autoLevelBox.checkState(),
'autocontrastBox': GUI.autocontrastBox.checkState(),
'croppingBox': GUI.croppingBox.checkState(),
'croppingPowerSlider': float(self.croppingPowerValue) * 100, 'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(), 'preserveMarginBox': self.preserveMarginBox.value(),
'interPanelCropBox': GUI.interPanelCropBox.checkState().value, 'interPanelCropBox': GUI.interPanelCropBox.checkState(),
'upscaleBox': GUI.upscaleBox.checkState().value, 'upscaleBox': GUI.upscaleBox.checkState(),
'borderBox': GUI.borderBox.checkState().value, 'borderBox': GUI.borderBox.checkState(),
'webtoonBox': GUI.webtoonBox.checkState().value, 'webtoonBox': GUI.webtoonBox.checkState(),
'outputSplit': GUI.outputSplit.checkState().value, 'outputSplit': GUI.outputSplit.checkState(),
'colorBox': GUI.colorBox.checkState().value, 'colorBox': GUI.colorBox.checkState(),
'reduceRainbowBox': GUI.reduceRainbowBox.checkState().value, 'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState().value, 'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value, 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState().value, 'mozJpegBox': GUI.mozJpegBox.checkState(),
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(), 'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState().value, 'deleteBox': GUI.deleteBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState().value, 'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState().value, 'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value, 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState().value, 'noRotateBox': GUI.noRotateBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState().value, 'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100, 'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value, 'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState(),
'chunkSizeBox': GUI.chunkSizeBox.value()}) 'chunkSizeBox': GUI.chunkSizeBox.value()})
self.settings.sync() self.settings.sync()
self.tray.hide() self.tray.hide()
@@ -893,7 +1028,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
MW.activateWindow() MW.activateWindow()
if type(message) is bytes: if type(message) is bytes:
message = message.decode('UTF-8') message = message.decode('UTF-8')
if not self.conversionAlive and message != 'ARISE': if not self.conversionAlive and message != 'ARISE' and not GUI.jobList.findItems(message, Qt.MatchFlag.MatchExactly):
if self.needClean: if self.needClean:
self.needClean = False self.needClean = False
GUI.jobList.clear() GUI.jobList.clear()
@@ -924,6 +1059,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if message[-1] == '/': if message[-1] == '/':
message = message[:-1] message = message[:-1]
self.handleMessage(message) self.handleMessage(message)
GUI.jobList.sortItems()
def forceShutdown(self): def forceShutdown(self):
self.saveSettings(None) self.saveSettings(None)
@@ -958,7 +1094,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW) self.setupUi(MW)
self.editor = KCCGUI_MetaEditor() self.editor = KCCGUI_MetaEditor()
self.icons = Icons() self.icons = Icons()
self.settings = QSettings('ciromattia', 'kcc') self.settings = QSettings('ciromattia', 'kcc9')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str) self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str)) self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
@@ -968,7 +1104,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.currentFormat = self.settings.value('currentFormat', 0, type=int) self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int) self.startNumber = self.settings.value('startNumber', 0, type=int)
self.windowSize = self.settings.value('windowSize', '0x0', type=str) self.windowSize = self.settings.value('windowSize', '0x0', type=str)
self.options = self.settings.value('options', {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}) default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}
try:
self.options = self.settings.value('options', default_options)
except Exception:
self.options = default_options
self.worker = WorkerThread() self.worker = WorkerThread()
self.versionCheck = VersionThread() self.versionCheck = VersionThread()
self.progress = ProgressThread() self.progress = ProgressThread()
@@ -1005,6 +1145,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'}, "MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'}, "EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'}, "CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'}, "KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'}, "MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}, "EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1034,7 +1175,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO',
}, },
"Kindle Colorsoft": { "Kindle Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KO', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
}, },
"Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, "Kindle Paperwhite 7/10": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'}, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
@@ -1086,12 +1227,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'KoS'}, 'Label': 'KoS'},
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoE'}, 'Label': 'KoE'},
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk1'}, 'Label': 'Rmk1'},
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk2'}, 'Label': 'Rmk2'},
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True, "reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'RmkPP'}, 'Label': 'RmkPP'},
"reMarkable Paper Pro Move": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'RmkPPMove'},
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False, "Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
'Label': 'OTHER'}, 'Label': 'OTHER'},
} }
@@ -1114,6 +1257,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"reMarkable 1", "reMarkable 1",
"reMarkable 2", "reMarkable 2",
"reMarkable Paper Pro", "reMarkable Paper Pro",
"reMarkable Paper Pro Move",
"Separator", "Separator",
"Other", "Other",
"Separator", "Separator",
@@ -1157,7 +1301,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
statusBarLabel.setOpenExternalLinks(True) statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1) GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info') self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info') self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
if self.startNumber < 5: if self.startNumber < 5:
@@ -1186,9 +1329,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower) GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox) GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox) GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox) GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice) GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat) GUI.formatBox.activated.connect(self.changeFormat)
GUI.titleEdit.textChanged.connect(self.toggletitleEdit)
GUI.fileFusionBox.stateChanged.connect(self.togglefileFusionBox)
GUI.metadataTitleBox.stateChanged.connect(self.togglemetadataTitleBox)
GUI.jobList.itemDoubleClicked.connect(self.editSourceMetadata)
MW.progressBarTick.connect(self.updateProgressbar) MW.progressBarTick.connect(self.updateProgressbar)
MW.modeConvert.connect(self.modeConvert) MW.modeConvert.connect(self.modeConvert)
MW.addMessage.connect(self.addMessage) MW.addMessage.connect(self.addMessage)
@@ -1277,15 +1425,17 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
self.editorWidget.setEnabled(True) self.editorWidget.setEnabled(True)
self.okButton.setEnabled(True) self.okButton.setEnabled(True)
self.statusLabel.setText('Separate authors with a comma.') self.statusLabel.setText('Separate authors with a comma.')
for field in (self.seriesLine, self.volumeLine, self.numberLine): for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
field.setText(self.parser.data[field.objectName().capitalize()[:-4]]) field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's'])) field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
if self.seriesLine.text() == '': for field in (self.seriesLine, self.titleLine):
if file.endswith('.xml'): if field.text() == '':
self.seriesLine.setText(file.split('\\')[-2]) path = Path(file)
else: if file.endswith('.xml'):
self.seriesLine.setText(file.split('\\')[-1].split('/')[-1].split('.')[0]) field.setText(path.parent.name)
else:
field.setText(path.stem)
def saveData(self): def saveData(self):
for field in (self.volumeLine, self.numberLine): for field in (self.volumeLine, self.numberLine):
@@ -1295,7 +1445,8 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.') self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break break
else: else:
self.parser.data['Series'] = self.cleanData(self.seriesLine.text()) for field in (self.seriesLine, self.titleLine):
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine): for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
values = self.cleanData(field.text()).split(',') values = self.cleanData(field.text()).split(',')
tmpData = [] tmpData = []

File diff suppressed because it is too large Load Diff

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.9.1 ## Created by: Qt User Interface Compiler version 6.9.3
## ##
## 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!
################################################################################ ################################################################################
@@ -35,8 +35,167 @@ class Ui_mainWindow(object):
self.gridLayout = QGridLayout(self.centralWidget) self.gridLayout = QGridLayout(self.centralWidget)
self.gridLayout.setObjectName(u"gridLayout") self.gridLayout.setObjectName(u"gridLayout")
self.gridLayout.setContentsMargins(-1, -1, -1, 5) self.gridLayout.setContentsMargins(-1, -1, -1, 5)
self.optionWidget = QWidget(self.centralWidget)
self.optionWidget.setObjectName(u"optionWidget")
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.titleEdit = QLineEdit(self.optionWidget)
self.titleEdit.setObjectName(u"titleEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.titleEdit.sizePolicy().hasHeightForWidth())
self.titleEdit.setSizePolicy(sizePolicy)
self.titleEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.titleEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.titleEdit, 0, 0, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.deleteBox = QCheckBox(self.optionWidget)
self.deleteBox.setObjectName(u"deleteBox")
self.gridLayout_2.addWidget(self.deleteBox, 5, 1, 1, 1)
self.rotateFirstBox = QCheckBox(self.optionWidget)
self.rotateFirstBox.setObjectName(u"rotateFirstBox")
self.gridLayout_2.addWidget(self.rotateFirstBox, 8, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 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.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.spreadShiftBox = QCheckBox(self.optionWidget)
self.spreadShiftBox.setObjectName(u"spreadShiftBox")
self.gridLayout_2.addWidget(self.spreadShiftBox, 5, 0, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.eraseRainbowBox = QCheckBox(self.optionWidget)
self.eraseRainbowBox.setObjectName(u"eraseRainbowBox")
self.gridLayout_2.addWidget(self.eraseRainbowBox, 7, 2, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
sizePolicy.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy)
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.authorEdit, 0, 1, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.metadataTitleBox = QCheckBox(self.optionWidget)
self.metadataTitleBox.setObjectName(u"metadataTitleBox")
self.metadataTitleBox.setTristate(True)
self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
self.mozJpegBox = QCheckBox(self.optionWidget)
self.mozJpegBox.setObjectName(u"mozJpegBox")
self.mozJpegBox.setTristate(True)
self.gridLayout_2.addWidget(self.mozJpegBox, 4, 0, 1, 1)
self.autoLevelBox = QCheckBox(self.optionWidget)
self.autoLevelBox.setObjectName(u"autoLevelBox")
self.gridLayout_2.addWidget(self.autoLevelBox, 8, 2, 1, 1)
self.autocontrastBox = QCheckBox(self.optionWidget)
self.autocontrastBox.setObjectName(u"autocontrastBox")
self.autocontrastBox.setTristate(True)
self.gridLayout_2.addWidget(self.autocontrastBox, 9, 2, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.jobList = QListWidget(self.centralWidget) self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList") self.jobList.setObjectName(u"jobList")
self.jobList.setMinimumSize(QSize(0, 150))
self.jobList.setStyleSheet(u"") self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
@@ -44,6 +203,66 @@ class Ui_mainWindow(object):
self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2) self.gridLayout.addWidget(self.jobList, 2, 0, 1, 2)
self.customWidget = QWidget(self.centralWidget)
self.customWidget.setObjectName(u"customWidget")
self.customWidget.setVisible(False)
self.gridLayout_3 = QGridLayout(self.customWidget)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
self.hLabel.setSizePolicy(sizePolicy1)
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
self.widthBox = QSpinBox(self.customWidget)
self.widthBox.setObjectName(u"widthBox")
self.widthBox.setMaximum(6000)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
self.wLabel = QLabel(self.customWidget)
self.wLabel.setObjectName(u"wLabel")
sizePolicy1.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy1)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
self.heightBox = QSpinBox(self.customWidget)
self.heightBox.setObjectName(u"heightBox")
self.heightBox.setMaximum(8000)
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
self.toolWidget = QWidget(self.centralWidget) self.toolWidget = QWidget(self.centralWidget)
self.toolWidget.setObjectName(u"toolWidget") self.toolWidget.setObjectName(u"toolWidget")
self.horizontalLayout = QHBoxLayout(self.toolWidget) self.horizontalLayout = QHBoxLayout(self.toolWidget)
@@ -62,7 +281,7 @@ class Ui_mainWindow(object):
self.kofiButton.setObjectName(u"kofiButton") self.kofiButton.setObjectName(u"kofiButton")
self.kofiButton.setMinimumSize(QSize(0, 30)) self.kofiButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon() icon2 = QIcon()
icon2.addFile(u":/Other/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon2.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.kofiButton.setIcon(icon2) self.kofiButton.setIcon(icon2)
self.kofiButton.setIconSize(QSize(19, 16)) self.kofiButton.setIconSize(QSize(19, 16))
@@ -80,21 +299,70 @@ class Ui_mainWindow(object):
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2) self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMinimumSize(QSize(0, 30))
font = QFont()
font.setBold(True)
self.progressBar.setFont(font)
self.progressBar.setVisible(False)
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.gridLayout_5 = QGridLayout(self.croppingWidget)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
self.preserveMarginLabel = QLabel(self.croppingWidget)
self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy2)
self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0)
self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
self.buttonWidget = QWidget(self.centralWidget) self.buttonWidget = QWidget(self.centralWidget)
self.buttonWidget.setObjectName(u"buttonWidget") self.buttonWidget.setObjectName(u"buttonWidget")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy3.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy3.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth()) sizePolicy3.setHeightForWidth(self.buttonWidget.sizePolicy().hasHeightForWidth())
self.buttonWidget.setSizePolicy(sizePolicy) self.buttonWidget.setSizePolicy(sizePolicy3)
self.gridLayout_4 = QGridLayout(self.buttonWidget) self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4") self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.convertButton = QPushButton(self.buttonWidget) self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton") self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30)) self.convertButton.setMinimumSize(QSize(0, 30))
font = QFont()
font.setBold(True)
self.convertButton.setFont(font) self.convertButton.setFont(font)
icon4 = QIcon() icon4 = QIcon()
icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon4.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
@@ -137,11 +405,8 @@ class Ui_mainWindow(object):
self.defaultOutputFolderBox = QCheckBox(self.buttonWidget) self.defaultOutputFolderBox = QCheckBox(self.buttonWidget)
self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox") self.defaultOutputFolderBox.setObjectName(u"defaultOutputFolderBox")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) sizePolicy2.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
sizePolicy1.setHorizontalStretch(0) self.defaultOutputFolderBox.setSizePolicy(sizePolicy2)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.defaultOutputFolderBox.sizePolicy().hasHeightForWidth())
self.defaultOutputFolderBox.setSizePolicy(sizePolicy1)
self.defaultOutputFolderBox.setTristate(True) self.defaultOutputFolderBox.setTristate(True)
self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1) self.gridLayout_4.addWidget(self.defaultOutputFolderBox, 0, 4, 1, 1)
@@ -162,248 +427,10 @@ class Ui_mainWindow(object):
self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2) self.gridLayout.addWidget(self.buttonWidget, 3, 0, 1, 2)
self.progressBar = QProgressBar(self.centralWidget)
self.progressBar.setObjectName(u"progressBar")
self.progressBar.setMinimumSize(QSize(0, 30))
self.progressBar.setFont(font)
self.progressBar.setVisible(False)
self.progressBar.setAlignment(Qt.AlignmentFlag.AlignJustify|Qt.AlignmentFlag.AlignVCenter)
self.gridLayout.addWidget(self.progressBar, 1, 0, 1, 2)
self.customWidget = QWidget(self.centralWidget)
self.customWidget.setObjectName(u"customWidget")
self.customWidget.setVisible(False)
self.gridLayout_3 = QGridLayout(self.customWidget)
self.gridLayout_3.setObjectName(u"gridLayout_3")
self.gridLayout_3.setContentsMargins(0, 0, 0, 0)
self.hLabel = QLabel(self.customWidget)
self.hLabel.setObjectName(u"hLabel")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.hLabel.sizePolicy().hasHeightForWidth())
self.hLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.hLabel, 0, 2, 1, 1)
self.widthBox = QSpinBox(self.customWidget)
self.widthBox.setObjectName(u"widthBox")
self.widthBox.setMaximum(2400)
self.gridLayout_3.addWidget(self.widthBox, 0, 1, 1, 1)
self.wLabel = QLabel(self.customWidget)
self.wLabel.setObjectName(u"wLabel")
sizePolicy2.setHeightForWidth(self.wLabel.sizePolicy().hasHeightForWidth())
self.wLabel.setSizePolicy(sizePolicy2)
self.gridLayout_3.addWidget(self.wLabel, 0, 0, 1, 1)
self.heightBox = QSpinBox(self.customWidget)
self.heightBox.setObjectName(u"heightBox")
self.heightBox.setMaximum(3840)
self.gridLayout_3.addWidget(self.heightBox, 0, 3, 1, 1)
self.gridLayout.addWidget(self.customWidget, 8, 0, 1, 2)
self.croppingWidget = QWidget(self.centralWidget)
self.croppingWidget.setObjectName(u"croppingWidget")
self.croppingWidget.setVisible(False)
self.gridLayout_5 = QGridLayout(self.croppingWidget)
self.gridLayout_5.setObjectName(u"gridLayout_5")
self.gridLayout_5.setContentsMargins(0, 0, 0, 0)
self.preserveMarginLabel = QLabel(self.croppingWidget)
self.preserveMarginLabel.setObjectName(u"preserveMarginLabel")
self.gridLayout_5.addWidget(self.preserveMarginLabel, 1, 0, 1, 1)
self.croppingPowerLabel = QLabel(self.croppingWidget)
self.croppingPowerLabel.setObjectName(u"croppingPowerLabel")
self.gridLayout_5.addWidget(self.croppingPowerLabel, 0, 0, 1, 1)
self.croppingPowerSlider = QSlider(self.croppingWidget)
self.croppingPowerSlider.setObjectName(u"croppingPowerSlider")
self.croppingPowerSlider.setMaximum(300)
self.croppingPowerSlider.setSingleStep(1)
self.croppingPowerSlider.setOrientation(Qt.Orientation.Horizontal)
self.gridLayout_5.addWidget(self.croppingPowerSlider, 0, 1, 1, 1)
self.preserveMarginBox = QSpinBox(self.croppingWidget)
self.preserveMarginBox.setObjectName(u"preserveMarginBox")
sizePolicy1.setHeightForWidth(self.preserveMarginBox.sizePolicy().hasHeightForWidth())
self.preserveMarginBox.setSizePolicy(sizePolicy1)
self.preserveMarginBox.setMaximum(99)
self.preserveMarginBox.setSingleStep(5)
self.preserveMarginBox.setValue(0)
self.gridLayout_5.addWidget(self.preserveMarginBox, 1, 1, 1, 1)
self.gridLayout.addWidget(self.croppingWidget, 9, 0, 1, 2)
self.optionWidget = QWidget(self.centralWidget)
self.optionWidget.setObjectName(u"optionWidget")
self.gridLayout_2 = QGridLayout(self.optionWidget)
self.gridLayout_2.setObjectName(u"gridLayout_2")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.croppingBox = QCheckBox(self.optionWidget)
self.croppingBox.setObjectName(u"croppingBox")
self.croppingBox.setTristate(True)
self.gridLayout_2.addWidget(self.croppingBox, 4, 2, 1, 1)
self.mangaBox = QCheckBox(self.optionWidget)
self.mangaBox.setObjectName(u"mangaBox")
self.gridLayout_2.addWidget(self.mangaBox, 1, 0, 1, 1)
self.webtoonBox = QCheckBox(self.optionWidget)
self.webtoonBox.setObjectName(u"webtoonBox")
self.gridLayout_2.addWidget(self.webtoonBox, 2, 0, 1, 1)
self.rotateBox = QCheckBox(self.optionWidget)
self.rotateBox.setObjectName(u"rotateBox")
self.rotateBox.setTristate(True)
self.gridLayout_2.addWidget(self.rotateBox, 1, 1, 1, 1)
self.borderBox = QCheckBox(self.optionWidget)
self.borderBox.setObjectName(u"borderBox")
self.borderBox.setTristate(True)
self.gridLayout_2.addWidget(self.borderBox, 3, 0, 1, 1)
self.gammaBox = QCheckBox(self.optionWidget)
self.gammaBox.setObjectName(u"gammaBox")
self.gridLayout_2.addWidget(self.gammaBox, 2, 2, 1, 1)
self.interPanelCropBox = QCheckBox(self.optionWidget)
self.interPanelCropBox.setObjectName(u"interPanelCropBox")
self.interPanelCropBox.setTristate(True)
self.gridLayout_2.addWidget(self.interPanelCropBox, 6, 2, 1, 1)
self.colorBox = QCheckBox(self.optionWidget)
self.colorBox.setObjectName(u"colorBox")
self.gridLayout_2.addWidget(self.colorBox, 3, 2, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox")
self.qualityBox.setTristate(True)
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
self.disableProcessingBox = QCheckBox(self.optionWidget)
self.disableProcessingBox.setObjectName(u"disableProcessingBox")
self.gridLayout_2.addWidget(self.disableProcessingBox, 5, 2, 1, 1)
self.maximizeStrips = QCheckBox(self.optionWidget)
self.maximizeStrips.setObjectName(u"maximizeStrips")
self.gridLayout_2.addWidget(self.maximizeStrips, 4, 1, 1, 1)
self.authorEdit = QLineEdit(self.optionWidget)
self.authorEdit.setObjectName(u"authorEdit")
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
sizePolicy3.setHorizontalStretch(0)
sizePolicy3.setVerticalStretch(0)
sizePolicy3.setHeightForWidth(self.authorEdit.sizePolicy().hasHeightForWidth())
self.authorEdit.setSizePolicy(sizePolicy3)
self.authorEdit.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.authorEdit.setClearButtonEnabled(False)
self.gridLayout_2.addWidget(self.authorEdit, 0, 0, 1, 1)
self.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.fileFusionBox = QCheckBox(self.optionWidget)
self.fileFusionBox.setObjectName(u"fileFusionBox")
self.gridLayout_2.addWidget(self.fileFusionBox, 6, 0, 1, 1)
self.upscaleBox = QCheckBox(self.optionWidget)
self.upscaleBox.setObjectName(u"upscaleBox")
self.upscaleBox.setTristate(True)
self.gridLayout_2.addWidget(self.upscaleBox, 2, 1, 1, 1)
self.outputSplit = QCheckBox(self.optionWidget)
self.outputSplit.setObjectName(u"outputSplit")
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.noRotateBox = QCheckBox(self.optionWidget)
self.noRotateBox.setObjectName(u"noRotateBox")
self.gridLayout_2.addWidget(self.noRotateBox, 6, 1, 1, 1)
self.reduceRainbowBox = QCheckBox(self.optionWidget)
self.reduceRainbowBox.setObjectName(u"reduceRainbowBox")
self.gridLayout_2.addWidget(self.reduceRainbowBox, 7, 2, 1, 1)
self.chunkSizeCheckBox = QCheckBox(self.optionWidget)
self.chunkSizeCheckBox.setObjectName(u"chunkSizeCheckBox")
self.gridLayout_2.addWidget(self.chunkSizeCheckBox, 7, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
self.gammaWidget = QWidget(self.centralWidget)
self.gammaWidget.setObjectName(u"gammaWidget")
self.gammaWidget.setVisible(False)
self.horizontalLayout_2 = QHBoxLayout(self.gammaWidget)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.gammaLabel = QLabel(self.gammaWidget)
self.gammaLabel.setObjectName(u"gammaLabel")
self.horizontalLayout_2.addWidget(self.gammaLabel)
self.gammaSlider = QSlider(self.gammaWidget)
self.gammaSlider.setObjectName(u"gammaSlider")
self.gammaSlider.setMaximum(250)
self.gammaSlider.setSingleStep(5)
self.gammaSlider.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_2.addWidget(self.gammaSlider)
self.gridLayout.addWidget(self.gammaWidget, 7, 0, 1, 2)
self.chunkSizeWidget = QWidget(self.centralWidget) self.chunkSizeWidget = QWidget(self.centralWidget)
self.chunkSizeWidget.setObjectName(u"chunkSizeWidget") self.chunkSizeWidget.setObjectName(u"chunkSizeWidget")
sizePolicy3.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.chunkSizeWidget.sizePolicy().hasHeightForWidth())
self.chunkSizeWidget.setSizePolicy(sizePolicy3) self.chunkSizeWidget.setSizePolicy(sizePolicy)
self.chunkSizeWidget.setVisible(False) self.chunkSizeWidget.setVisible(False)
self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget) self.horizontalLayout_4 = QHBoxLayout(self.chunkSizeWidget)
self.horizontalLayout_4.setSpacing(0) self.horizontalLayout_4.setSpacing(0)
@@ -463,8 +490,8 @@ class Ui_mainWindow(object):
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox) QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox) QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox) QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
QWidget.setTabOrder(self.interPanelCropBox, self.reduceRainbowBox) QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
QWidget.setTabOrder(self.reduceRainbowBox, self.heightBox) QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider) QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton) QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.wikiButton) QWidget.setTabOrder(self.editorButton, self.wikiButton)
@@ -479,12 +506,139 @@ 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)
self.titleEdit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Default Title</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.titleEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Title", None))
#if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left (manga)", 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.croppingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled</span></p><p>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Margins<br/></span>Margins</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Margins + page numbers<br/></span>Margins +page numbers</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.croppingBox.setText(QCoreApplication.translate("mainWindow", u"Cropping mode", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip)
self.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.rotateFirstBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>When the spread splitter option is partially checked,</p><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Rotate Last<br/></span>Put the rotated 2 page spread after the split spreads.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate First<br/></span>Put the rotated 2 page spread before the split spreads.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateFirstBox.setText(QCoreApplication.translate("mainWindow", u"Rotate First", None))
#if QT_CONFIG(tooltip)
self.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.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Set a custom gamma correction.</p><p>1.0 is default (disabled).<br/>&lt; 1.0 makes the image brighter.<br/>&gt; 1.0 makes the image darker. </p><p>1.8 was the default in KCC 9.1.0 and earlier.</p><p>Use if you want to make midtones darker.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", 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.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
#endif // QT_CONFIG(tooltip)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.eraseRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Erase rainbow effect on color eink screen by attenuating interfering frequencies", None))
#endif // QT_CONFIG(tooltip)
self.eraseRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow eraser", None))
#if QT_CONFIG(tooltip)
self.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.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
#if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
#if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.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.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Don't use metadata Title<br/></span>Write default title.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Add metadata Title to the default schema<br/></span>Write default title with Title from ComicInfo.xml or other embedded metadata.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Use metadata Title only<br/></span>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
#if QT_CONFIG(tooltip)
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.autoLevelBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>By default, KCC maps the darkest pixel value to pure black (the black point.)</p><p>Extreme black point sets the black point to be the most common dark pixel value.</p><p>Useful when text is black but artwork is gray.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.autoLevelBox.setText(QCoreApplication.translate("mainWindow", u"Extreme Black Point", None))
#if QT_CONFIG(tooltip)
self.autocontrastBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - BW only<br/></span>Only autocontrast bw pages. Ignored for pages where near blacks or whites don't exist.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Disabled<br/></span>Disable autocontrast</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - BW and Color<br/></span>BW and color images will be autocontrasted. Ignored for pages where near blacks or whites don't exist.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.autocontrastBox.setText(QCoreApplication.translate("mainWindow", u"Autocontrast", None))
#if QT_CONFIG(tooltip)
self.jobList.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Double click on source to open it in metadata editor.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.hLabel.setText(QCoreApplication.translate("mainWindow", u"Custom height:", None))
#if QT_CONFIG(tooltip)
self.widthBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.wLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.wLabel.setText(QCoreApplication.translate("mainWindow", u"Custom width:", None))
#if QT_CONFIG(tooltip)
self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None)) self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None)) self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None)) self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None)) self.wikiButton.setText(QCoreApplication.translate("mainWindow", u"Wiki", None))
#if QT_CONFIG(tooltip)
self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>After calculating the cropping boundaries, &quot;back up&quot; a specified percentage amount.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None)) self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
@@ -508,114 +662,6 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None)) self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.hLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.hLabel.setText(QCoreApplication.translate("mainWindow", u"Custom height:", None))
#if QT_CONFIG(tooltip)
self.widthBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.wLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.wLabel.setText(QCoreApplication.translate("mainWindow", u"Custom width:", None))
#if QT_CONFIG(tooltip)
self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.preserveMarginLabel.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>After calculating the cropping boundaries, &quot;back up&quot; a specified percentage amount.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.preserveMarginLabel.setText(QCoreApplication.translate("mainWindow", u"Preserve Margin %", None))
self.croppingPowerLabel.setText(QCoreApplication.translate("mainWindow", u"Cropping power:", None))
#if QT_CONFIG(tooltip)
self.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.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.webtoonBox.setText(QCoreApplication.translate("mainWindow", u"Webtoon mode", None))
#if QT_CONFIG(tooltip)
self.rotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Split and rotate<br/></span>Double page spreads will be displayed twice. First split and then rotated. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.rotateBox.setText(QCoreApplication.translate("mainWindow", u"Spread splitter", None))
#if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip)
self.gammaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable automatic gamma correction.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.gammaBox.setText(QCoreApplication.translate("mainWindow", u"Custom gamma", None))
#if QT_CONFIG(tooltip)
self.interPanelCropBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Disabled<br/></span>Disabled</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Horizontal<br/></span>Crop empty horizontal lines.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Both<br/></span>Crop empty horizontal and vertical lines.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.interPanelCropBox.setText(QCoreApplication.translate("mainWindow", u"Inter-panel crop", None))
#if QT_CONFIG(tooltip)
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.colorBox.setText(QCoreApplication.translate("mainWindow", u"Color mode", None))
#if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
#if QT_CONFIG(tooltip)
self.disableProcessingBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Do not process any image, ignore profile and processing options.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.disableProcessingBox.setText(QCoreApplication.translate("mainWindow", u"Disable processing", None))
#if QT_CONFIG(tooltip)
self.maximizeStrips.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 1x4<br/></span>Keep format 1x4 panels strips.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2x2<br/></span>Turn 1x4 strips to 2x2 to maximize screen usage.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.maximizeStrips.setText(QCoreApplication.translate("mainWindow", u"1x4 to 2x2 strips", None))
#if QT_CONFIG(tooltip)
self.authorEdit.setToolTip(QCoreApplication.translate("mainWindow", u"Default Author is KCC", None))
#endif // QT_CONFIG(tooltip)
self.authorEdit.setPlaceholderText(QCoreApplication.translate("mainWindow", u"Default Author", None))
#if QT_CONFIG(tooltip)
self.deleteBox.setToolTip(QCoreApplication.translate("mainWindow", u"Delete input file(s) or directory. It's not recoverable!", None))
#endif // QT_CONFIG(tooltip)
self.deleteBox.setText(QCoreApplication.translate("mainWindow", u"Delete input", None))
#if QT_CONFIG(tooltip)
self.mozJpegBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - JPEG<br/></span>Use JPEG files</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - force PNG<br/></span>Create PNG files instead JPEG</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - mozJpeg<br/></span>10-20% smaller JPEG file, with the same image quality, but processing time multiplied by 2</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mozJpegBox.setText(QCoreApplication.translate("mainWindow", u"JPEG/PNG/mozJpeg", None))
#if QT_CONFIG(tooltip)
self.spreadShiftBox.setToolTip(QCoreApplication.translate("mainWindow", u"Shift first page to opposite side in landscape for two page spread alignment", None))
#endif // QT_CONFIG(tooltip)
self.spreadShiftBox.setText(QCoreApplication.translate("mainWindow", u"Spread shift", None))
#if QT_CONFIG(tooltip)
self.fileFusionBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Combines all selected files into a single file. (Helpful for combining chapters into volumes.)</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileFusionBox.setText(QCoreApplication.translate("mainWindow", u"File Fusion", None))
#if QT_CONFIG(tooltip)
self.upscaleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.upscaleBox.setText(QCoreApplication.translate("mainWindow", u"Stretch/Upscale", None))
#if QT_CONFIG(tooltip)
self.outputSplit.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Automatic mode<br/></span>The output will be split automatically.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Checked - Volume mode<br/></span>Every subdirectory will be considered as a separate volume.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip)
self.noRotateBox.setToolTip(QCoreApplication.translate("mainWindow", u"Do not rotate double page spreads in spread splitter option.", None))
#endif // QT_CONFIG(tooltip)
self.noRotateBox.setText(QCoreApplication.translate("mainWindow", u"No rotate", None))
#if QT_CONFIG(tooltip)
self.reduceRainbowBox.setToolTip(QCoreApplication.translate("mainWindow", u"Reduce rainbow effect on color eink by slightly blurring images", None))
#endif // QT_CONFIG(tooltip)
self.reduceRainbowBox.setText(QCoreApplication.translate("mainWindow", u"Rainbow blur", None))
#if QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:700; text-decoration: underline;\">Unchecked<br/></span>Maximal output file size is 100 MB for Webtoon, 400 MB for others before split occurs.</p><p><span style=\" font-weight:700; text-decoration: underline;\">Checked</span><br/>Output file size specified in &quot;Chunk size MB&quot; before split occurs.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.chunkSizeCheckBox.setText(QCoreApplication.translate("mainWindow", u"Chunk size", None))
#if QT_CONFIG(tooltip)
self.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None))
#endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None)) self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // 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.9.1 ## Created by: Qt User Interface Compiler version 6.9.3
## ##
## 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!
################################################################################ ################################################################################
@@ -60,52 +60,62 @@ class Ui_editorDialog(object):
self.label_3 = QLabel(self.editorWidget) self.label_3 = QLabel(self.editorWidget)
self.label_3.setObjectName(u"label_3") self.label_3.setObjectName(u"label_3")
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
self.numberLine = QLineEdit(self.editorWidget) self.numberLine = QLineEdit(self.editorWidget)
self.numberLine.setObjectName(u"numberLine") self.numberLine.setObjectName(u"numberLine")
self.gridLayout.addWidget(self.numberLine, 2, 1, 1, 1) self.gridLayout.addWidget(self.numberLine, 3, 1, 1, 1)
self.label_4 = QLabel(self.editorWidget) self.label_4 = QLabel(self.editorWidget)
self.label_4.setObjectName(u"label_4") self.label_4.setObjectName(u"label_4")
self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1) self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1)
self.writerLine = QLineEdit(self.editorWidget) self.writerLine = QLineEdit(self.editorWidget)
self.writerLine.setObjectName(u"writerLine") self.writerLine.setObjectName(u"writerLine")
self.gridLayout.addWidget(self.writerLine, 3, 1, 1, 1) self.gridLayout.addWidget(self.writerLine, 4, 1, 1, 1)
self.label_5 = QLabel(self.editorWidget) self.label_5 = QLabel(self.editorWidget)
self.label_5.setObjectName(u"label_5") self.label_5.setObjectName(u"label_5")
self.gridLayout.addWidget(self.label_5, 4, 0, 1, 1) self.gridLayout.addWidget(self.label_5, 5, 0, 1, 1)
self.pencillerLine = QLineEdit(self.editorWidget) self.pencillerLine = QLineEdit(self.editorWidget)
self.pencillerLine.setObjectName(u"pencillerLine") self.pencillerLine.setObjectName(u"pencillerLine")
self.gridLayout.addWidget(self.pencillerLine, 4, 1, 1, 1) self.gridLayout.addWidget(self.pencillerLine, 5, 1, 1, 1)
self.label_6 = QLabel(self.editorWidget) self.label_6 = QLabel(self.editorWidget)
self.label_6.setObjectName(u"label_6") self.label_6.setObjectName(u"label_6")
self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) self.gridLayout.addWidget(self.label_6, 6, 0, 1, 1)
self.inkerLine = QLineEdit(self.editorWidget) self.inkerLine = QLineEdit(self.editorWidget)
self.inkerLine.setObjectName(u"inkerLine") self.inkerLine.setObjectName(u"inkerLine")
self.gridLayout.addWidget(self.inkerLine, 5, 1, 1, 1) self.gridLayout.addWidget(self.inkerLine, 6, 1, 1, 1)
self.label_7 = QLabel(self.editorWidget) self.label_7 = QLabel(self.editorWidget)
self.label_7.setObjectName(u"label_7") self.label_7.setObjectName(u"label_7")
self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1) self.gridLayout.addWidget(self.label_7, 7, 0, 1, 1)
self.coloristLine = QLineEdit(self.editorWidget) self.coloristLine = QLineEdit(self.editorWidget)
self.coloristLine.setObjectName(u"coloristLine") self.coloristLine.setObjectName(u"coloristLine")
self.gridLayout.addWidget(self.coloristLine, 6, 1, 1, 1) self.gridLayout.addWidget(self.coloristLine, 7, 1, 1, 1)
self.label_8 = QLabel(self.editorWidget)
self.label_8.setObjectName(u"label_8")
self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1)
self.titleLine = QLineEdit(self.editorWidget)
self.titleLine.setObjectName(u"titleLine")
self.gridLayout.addWidget(self.titleLine, 2, 1, 1, 1)
self.verticalLayout.addWidget(self.editorWidget) self.verticalLayout.addWidget(self.editorWidget)
@@ -161,6 +171,7 @@ class Ui_editorDialog(object):
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None)) self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None)) self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None))
self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None)) self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None))
self.label_8.setText(QCoreApplication.translate("editorDialog", u"Title:", None))
self.statusLabel.setText("") self.statusLabel.setText("")
self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None)) self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None))
self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None)) self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None))

View File

@@ -1,4 +1,4 @@
__version__ = '8.0.1' __version__ = '9.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

@@ -32,7 +32,7 @@ from typing import List
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree, copyfile
from multiprocessing import Pool from multiprocessing import Pool, cpu_count
from uuid import uuid4 from uuid import uuid4
from natsort import os_sort_keygen, os_sorted from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext from slugify import slugify as slugify_ext
@@ -41,13 +41,13 @@ from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage from psutil import virtual_memory, disk_usage
from html import escape as hescape from html import escape as hescape
import pymupdf
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .comicarchive import SEVENZIP, available_archive_tools from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel from . import comic2panel
from . import image from . import image
from . import comicarchive from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix from . import dualmetafix
from . import metadata from . import metadata
from . import kindle from . import kindle
@@ -71,12 +71,23 @@ def main(argv=None):
if len(sources) == 0: if len(sources) == 0:
print('No matching files found.') print('No matching files found.')
return 1 return 1
if options.filefusion:
fusion_path = makeFusion(list(sources))
sources.clear()
sources.add(fusion_path)
for source in sources: for source in sources:
source = source.rstrip('\\').rstrip('/') source = source.rstrip('\\').rstrip('/')
options = copy(args) options = copy(args)
options = checkOptions(options) options = checkOptions(options)
print('Working on ' + source + '...') print('Working on ' + source + '...')
makeBook(source) makeBook(source)
if options.filefusion:
for path in sources:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
return 0 return 0
@@ -125,9 +136,10 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
"</head>\n", "</head>\n",
"<body style=\"" + additionalStyle + "\">\n", "<body style=\"" + additionalStyle + "\">\n",
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n", "<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
# this display none div fixes formatting issues with virtual panel mode, for some reason
'<div style="display:none;">.</div>\n',
]) ])
if options.iskindle:
# this display none div fixes formatting issues with virtual panel mode, for some reason
f.write('<div style="display:none;">.</div>\n')
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n') f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
if imgfile2: if imgfile2:
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n') f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
@@ -276,7 +288,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
f.close() f.close()
def buildOPF(dstdir, title, filelist, cover=None): def buildOPF(dstdir, title, filelist, originalpath, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
deviceres = options.profileData[1] deviceres = options.profileData[1]
if options.righttoleft: if options.righttoleft:
@@ -297,8 +309,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"]) f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
for author in options.authors: for author in options.authors:
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"]) f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n", f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n")
"<meta name=\"cover\" content=\"cover\"/>\n"]) if cover:
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
if options.iskindle and options.profile != 'Custom': if options.iskindle and options.profile != 'Custom':
f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n", f.writelines(["<meta name=\"fixed-layout\" content=\"true\"/>\n",
"<meta name=\"original-resolution\" content=\"", "<meta name=\"original-resolution\" content=\"",
@@ -364,6 +377,11 @@ def buildOPF(dstdir, title, filelist, cover=None):
else: else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n") f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left" pageside = "left"
if originalpath.lower().endswith('.pdf'):
if pageside == "right":
pageside = "left"
else:
pageside = "right"
if options.spreadshift: if options.spreadshift:
if pageside == "right": if pageside == "right":
pageside = "left" pageside = "left"
@@ -438,7 +456,7 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"]) "</container>"])
f.close() f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len_tomes=0): def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, job_progress='', len_tomes=0):
filelist = [] filelist = []
chapterlist = [] chapterlist = []
os.mkdir(os.path.join(path, 'OEBPS', 'Text')) os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
@@ -525,7 +543,9 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
"}\n"]) "}\n"])
f.close() f.close()
build_html_start = perf_counter() build_html_start = perf_counter()
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes) if cover:
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
dot_clean(path)
options.covers.append((cover, options.uuid)) options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')): for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False chapter = False
@@ -544,12 +564,12 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
else: else:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile))) filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
build_html_end = perf_counter() build_html_end = perf_counter()
print(f"buildHTML: {build_html_end - build_html_start} seconds") print(f"{job_progress}buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks # Overwrite chapternames if ComicInfo.xml has bookmarks
if ischunked: if ischunked:
options.comicinfo_chapters = [] options.comicinfo_chapters = []
if not chapternames and options.comicinfo_chapters: if options.comicinfo_chapters:
chapterlist = [] chapterlist = []
global_diff = 0 global_diff = 0
@@ -577,10 +597,39 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
chapternames[filename] = aChapter[1] chapternames[filename] = aChapter[1]
buildNCX(path, options.title, chapterlist, chapternames) buildNCX(path, options.title, chapterlist, chapternames)
buildNAV(path, options.title, chapterlist, chapternames) buildNAV(path, options.title, chapterlist, chapternames)
buildOPF(path, options.title, filelist, cover) buildOPF(path, options.title, filelist, originalpath, cover)
def imgDirectoryProcessing(path): def buildPDF(path, title, job_progress='', cover=None, output_file=None):
"""
Build a PDF file from processed comic images.
Images are combined into a single PDF optimized for e-readers.
"""
start = perf_counter()
# open empty PDF
with pymupdf.open() as doc:
doc.set_metadata({'title': title, 'author': options.authors[0]})
# Stream images to PDF
for root, dirs, files in os.walk(os.path.join(path, "OEBPS", "Images")):
files.sort(key=OS_SORT_KEY)
dirs.sort(key=OS_SORT_KEY)
for file in files:
w, h = Image.open(os.path.join(root, file)).size
page = doc.new_page(width=w, height=h)
page.insert_image(page.rect, filename=os.path.join(root, file))
# determine output filename if not provided
if output_file is None:
output_file = getOutputFilename(path, None, '.pdf', '')
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
print(f"{job_progress}MuPDF output: {end-start} sec")
return output_file
def imgDirectoryProcessing(path, job_progress=''):
global workerPool, workerOutput global workerPool, workerOutput
workerPool = Pool(maxtasksperchild=100) workerPool = Pool(maxtasksperchild=100)
workerOutput = [] workerOutput = []
@@ -600,7 +649,7 @@ def imgDirectoryProcessing(path):
workerPool.close() workerPool.close()
workerPool.join() workerPool.join()
img_processing_end = perf_counter() img_processing_end = perf_counter()
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds") print(f"{job_progress}imgFileProcessing: {img_processing_end - img_processing_start} seconds")
# macOS 15 likes to add ._ files after multiprocessing # macOS 15 likes to add ._ files after multiprocessing
dot_clean(path) dot_clean(path)
@@ -613,7 +662,7 @@ def imgDirectoryProcessing(path):
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])
else: else:
rmtree(os.path.join(path, '..', '..'), True) rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.") raise UserWarning("C2E: Source directory is empty.")
def imgFileProcessingTick(output): def imgFileProcessingTick(output):
@@ -639,19 +688,26 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt) workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload: for i in workImg.payload:
img = image.ComicPage(opt, *i) img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon: if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon: if opt.cropping == 1 and not opt.webtoon:
img.cropMargin(opt.croppingp, opt.croppingm) img.cropMargin(opt.croppingp, opt.croppingm)
if opt.interpanelcrop > 0: if opt.interpanelcrop > 0:
img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both") img.cropInterPanelEmptySections("horizontal" if opt.interpanelcrop == 1 else "both")
img.gammaCorrectImage()
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow) img.optimizeForDisplay(opt.eraserainbow, is_color)
if opt.forcecolor and img.color:
if is_color:
pass pass
elif opt.forcepng: elif opt.forcepng:
img.quantizeImage() img.convertToGrayscale()
if opt.format != 'PDF':
img.quantizeImage()
else: else:
img.convertToGrayscale() img.convertToGrayscale()
output.append(img.saveToDir()) output.append(img.saveToDir())
@@ -660,6 +716,141 @@ def imgFileProcessing(work):
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
def render_page(vector):
"""Render a page range of a document.
Notes:
The PyMuPDF document cannot be part of the argument, because that
cannot be pickled. So we are being passed in just its filename.
This is no performance issue, because we are a separate process and
need to open the document anyway.
Any page-specific function can be processed here - rendering is just
an example - text extraction might be another.
The work must however be self-contained: no inter-process communication
or synchronization is possible with this design.
Care must also be taken with which parameters are contained in the
argument, because it will be passed in via pickling by the Pool class.
So any large objects will increase the overall duration.
Args:
vector: a list containing required parameters.
"""
# recreate the arguments
idx = vector[0] # this is the segment number we have to process
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
target_height = vector[4]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
# pages per segment: make sure that cpu * seg_size >= num_pages!
seg_size = int(num_pages / cpu + 1)
seg_from = idx * seg_size # our first page number
seg_to = min(seg_from + seg_size, num_pages) # last page number
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
zoom = target_height / page.rect.height
mat = pymupdf.Matrix(zoom, zoom)
# TODO: decide colorspace earlier so later color check is cheaper.
# This is actually pretty hard when you have to deal with color vector text
pix = page.get_pixmap(matrix=mat, colorspace='RGB', alpha=False)
pix.save(os.path.join(output_dir, "p-%i.png" % i))
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
def extract_page(vector):
"""For pages with single image (and no text). Otherwise it's recommended to use render_page()
Notes:
The PyMuPDF document cannot be part of the argument, because that
cannot be pickled. So we are being passed in just its filename.
This is no performance issue, because we are a separate process and
need to open the document anyway.
Any page-specific function can be processed here - rendering is just
an example - text extraction might be another.
The work must however be self-contained: no inter-process communication
or synchronization is possible with this design.
Care must also be taken with which parameters are contained in the
argument, because it will be passed in via pickling by the Pool class.
So any large objects will increase the overall duration.
Args:
vector: a list containing required parameters.
"""
# recreate the arguments
idx = vector[0] # this is the segment number we have to process
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
# pages per segment: make sure that cpu * seg_size >= num_pages!
seg_size = int(num_pages / cpu + 1)
seg_from = idx * seg_size # our first page number
seg_to = min(seg_from + seg_size, num_pages) # last page number
for i in range(seg_from, seg_to): # work through our page segment
output_path = os.path.join(output_dir, "p-%i.png" % i)
page = doc.load_page(i)
image_list = page.get_images()
if len(image_list) > 1:
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
if not image_list:
width, height = int(page.rect.width), int(page.rect.height)
blank_page = Image.new("RGB", (width, height), "white")
blank_page.save(output_path)
else:
xref = image_list[0][0]
d = doc.extract_image(xref)
if d['cs-name'] == 'DeviceCMYK':
pix = pymupdf.Pixmap(doc, xref)
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
pix.save(output_path)
else:
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout:
imgout.write(d["image"])
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
page_text = page.get_text().strip()
if page_text != "":
render = True
break
if len(page.get_images()) > 1:
render = True
break
if len(page.get_images()) == 1:
image = page.get_images()[0]
if not image[5] or image[8] == 'CCITTFaxDecode':
render = True
break
cpu = cpu_count()
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
start = perf_counter()
with Pool() as pool:
results = pool.map(
render_page if render else extract_page, vectors
)
end = perf_counter()
print(f"MuPDF: {end - start} sec")
def getWorkFolder(afile): def getWorkFolder(afile):
if os.path.isdir(afile): if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5: if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
@@ -678,19 +869,37 @@ def getWorkFolder(afile):
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5: if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.") raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'): if afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile) workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
path, njpg = pdf.extract() path = workdir
workdir = path
sanitizePermissions(path) sanitizePermissions(path)
if njpg == 0: target_height = options.profileData[1][1]
if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
elif options.cropping == 2:
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
try:
mupdf_pdf_process_pages_parallel(afile, workdir, target_height)
except Exception as e:
rmtree(path, True) rmtree(path, True)
raise UserWarning("Failed to extract images from PDF file.") raise UserWarning(f"Failed to extract images from PDF file. {e}")
else: else:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile)) workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try: try:
cbx = comicarchive.ComicArchive(afile) cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir) path = cbx.extract(workdir)
sanitizePermissions(path) sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
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)
@@ -711,13 +920,18 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
else: else:
ext = '.kepub.epub' ext = '.kepub.epub'
if wantedname is not None: if wantedname is not None:
wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext): if wantedname.endswith(ext):
filename = os.path.abspath(wantedname) filename = os.path.abspath(wantedname)
elif os.path.isdir(srcpath): # output directory
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext) elif not wanted_ext:
abs_path = os.path.abspath(options.output)
if not os.path.exists(abs_path):
os.mkdir(abs_path)
filename = os.path.join(os.path.abspath(options.output), Path(srcpath).stem + ext)
# output file
else: else:
filename = os.path.join(os.path.abspath(options.output), filename = os.path.abspath(wanted_root) + ext
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
elif os.path.isdir(srcpath): elif os.path.isdir(srcpath):
filename = srcpath + tomenumber + ext filename = srcpath + tomenumber + ext
else: else:
@@ -736,7 +950,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
return filename return filename
def getComicInfo(path, originalpath): def getMetadata(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml') xmlPath = os.path.join(path, 'ComicInfo.xml')
options.comicinfo_chapters = [] options.comicinfo_chapters = []
options.summary = '' options.summary = ''
@@ -755,21 +969,24 @@ def getComicInfo(path, originalpath):
else: else:
defaultAuthor = False defaultAuthor = False
options.authors = [options.author] options.authors = [options.author]
if os.path.exists(xmlPath): if os.path.exists(xmlPath):
try: try:
xml = metadata.MetadataParser(xmlPath) xml = metadata.MetadataParser(xmlPath)
except Exception: except Exception:
os.remove(xmlPath) os.remove(xmlPath)
return return
if options.comicinfotitle: if options.metadatatitle == 2:
options.title = xml.data['Title'] options.title = xml.data['Title']
elif defaultTitle: elif defaultTitle:
if xml.data['Series']: if xml.data['Series']:
options.title = xml.data['Series'] options.title = xml.data['Series']
if xml.data['Volume']: if xml.data['Volume']:
titleSuffix += ' V' + xml.data['Volume'].zfill(2) titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
if xml.data['Number']: if xml.data['Number']:
titleSuffix += ' #' + xml.data['Number'].zfill(3) titleSuffix += ' #' + xml.data['Number'].zfill(3)
if options.metadatatitle == 1 and xml.data['Title']:
titleSuffix += ': ' + xml.data['Title']
options.title += titleSuffix options.title += titleSuffix
if defaultAuthor: if defaultAuthor:
options.authors = [] options.authors = []
@@ -787,6 +1004,13 @@ def getComicInfo(path, originalpath):
options.summary = xml.data['Summary'] options.summary = xml.data['Summary']
os.remove(xmlPath) os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'):
with pymupdf.open(originalpath) as doc:
if options.metadatatitle and doc.metadata['title']:
options.title = doc.metadata['title']
if defaultAuthor and doc.metadata['author']:
options.authors = [doc.metadata['author']]
def getDirectorySize(start_path='.'): def getDirectorySize(start_path='.'):
total_size = 0 total_size = 0
@@ -820,13 +1044,16 @@ def removeNonImages(filetree):
for root, dirs, files in os.walk(filetree): for root, dirs, files in os.walk(filetree):
for name in files: for name in files:
_, ext = getImageFileName(name) _, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2'): if ext not in IMAGE_TYPES:
if os.path.exists(os.path.join(root, name)): if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name)) os.remove(os.path.join(root, name))
# remove empty nested folders # remove empty nested folders
for root, dirs, files in os.walk(filetree, False): for root, dirs, files in os.walk(filetree, False):
if not files and not dirs: if not files and not dirs:
os.rmdir(root) os.rmdir(root)
if not os.listdir(Path(filetree).parent):
raise UserWarning('No images detected, nested archives are not supported.')
def sanitizeTree(filetree): def sanitizeTree(filetree):
@@ -880,21 +1107,15 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs: for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
dot_clean(filetree)
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
for name in files:
if name.startswith('._'):
os.remove(os.path.join(root, name))
def chunk_directory(path): def chunk_directory(path):
level = -1 level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')): for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files: for f in files:
# Windows MAX_LENGTH = 260 plus some buffer # Windows MAX_LEN = 260 plus some buffer
if len(os.path.join(root, f)) > 180: if os.name == 'nt' and len(os.path.join(root, f)) > 180:
flattenTree(os.path.join(path, 'OEBPS', 'Images')) flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1 level = 1
break break
@@ -985,6 +1206,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
try: try:
img = Image.open(path) img = Image.open(path)
imageNumber += 1 imageNumber += 1
# count images smaller than device resolution
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]: if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1 imageSmaller += 1
except Exception as err: except Exception as err:
@@ -1032,8 +1254,7 @@ def slugify(value, is_natural_sorted):
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2)) value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value return value
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip' zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools(): if SEVENZIP in available_archive_tools():
@@ -1041,7 +1262,7 @@ def makeZIP(zipfilename, basedir, isepub=False):
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile.write('application/epub+zip') mimetypeFile.write('application/epub+zip')
mimetypeFile.close() mimetypeFile.close()
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, os.path.join(basedir, "*")], capture_output=True, check=True) subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
else: else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED) zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
if isepub: if isepub:
@@ -1054,10 +1275,9 @@ def makeZIP(zipfilename, basedir, isepub=False):
zipOutput.write(path, aPath) zipOutput.write(path, aPath)
zipOutput.close() zipOutput.close()
end = perf_counter() end = perf_counter()
print(f"makeZIP time: {end - start} seconds") print(f"{job_progress}makeZIP time: {end - start} seconds")
return zipfilename return zipfilename
def makeParser(): def makeParser():
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False) psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
@@ -1090,12 +1310,13 @@ def makeParser():
help="Output generated file to specified directory or file") help="Output generated file to specified directory or file")
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle", output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Comic title [Default=filename or directory name]") help="Comic title [Default=filename or directory name]")
output_options.add_argument("--comicinfotitle", action="store_true", dest="comicinfotitle", default=False, output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0,
help="Write Title from ComicInfo.xml") help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema "
"2: Use Title only")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor", output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]") help="Author name [Default=KCC]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto", output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) " help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]") "[Default=Auto]")
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False, output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'") help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
@@ -1106,9 +1327,11 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment") help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False, output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.") help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False, processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profil or processing option") help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False, processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
help="Resize images smaller than device's resolution") help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False, processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
@@ -1117,6 +1340,14 @@ def makeParser():
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]") help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0", processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
help="Apply gamma correction to linearize the image [Default=Auto]") help="Apply gamma correction to linearize the image [Default=Auto]")
output_options.add_argument("--autolevel", action="store_true", dest="autolevel", default=False,
help="Set most common dark pixel value to be black point for leveling.")
output_options.add_argument("--noautocontrast", action="store_true", dest="noautocontrast", default=False,
help="Disable autocontrast.")
output_options.add_argument("--colorautocontrast", action="store_true", dest="colorautocontrast", default=False,
help="Autocontrast color pages too. Skipped for pages without near blacks or whites.")
output_options.add_argument("--filefusion", action="store_true", dest="filefusion", default=False,
help="Combines all input files into a single file.")
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2", processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]") help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0", processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
@@ -1133,8 +1364,8 @@ def makeParser():
help="Disable autodetection and force white borders") help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False, processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale") help="Don't convert images to grayscale")
output_options.add_argument("--reducerainbow", action="store_true", dest="reducerainbow", default=False, output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Reduce rainbow effect on color eink by slightly blurring images.") help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
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,
@@ -1181,6 +1412,8 @@ def checkOptions(options):
options.format = 'CBZ' options.format = 'CBZ'
elif options.profile in image.ProfileData.ProfilesKindle.keys(): elif options.profile in image.ProfileData.ProfilesKindle.keys():
options.format = 'MOBI' options.format = 'MOBI'
elif options.profile in image.ProfileData.ProfilesRemarkable.keys():
options.format = 'PDF'
else: else:
options.format = 'EPUB' options.format = 'EPUB'
if options.profile in image.ProfileData.ProfilesKindle.keys(): if options.profile in image.ProfileData.ProfilesKindle.keys():
@@ -1204,8 +1437,10 @@ def checkOptions(options):
if options.webtoon: if options.webtoon:
options.panelview = False options.panelview = False
options.righttoleft = False options.righttoleft = False
options.upscale = True options.upscale = False
options.hq = False options.hq = False
options.white_borders = True
options.bordersColor = 'white'
# Disable all Kindle features for other e-readers # Disable all Kindle features for other e-readers
if options.profile == 'OTHER': if options.profile == 'OTHER':
options.panelview = False options.panelview = False
@@ -1304,7 +1539,7 @@ def makeFusion(sources: List[str]):
return str(fusion_path) return str(fusion_path)
def makeBook(source, qtgui=None): def makeBook(source, qtgui=None, job_progress=''):
start = perf_counter() start = perf_counter()
global GUI global GUI
GUI = qtgui GUI = qtgui
@@ -1312,30 +1547,33 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
else: else:
checkTools(source) checkTools(source)
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format) options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
checkPre(source) checkPre(source)
print("Preparing source images...") print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source)
print("Checking images...") print(f"{job_progress}Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source) getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images")) removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
cover = image.Cover(cover_path, options) cover = None
if not options.webtoon:
cover = image.Cover(cover_path, options)
if options.webtoon: if options.webtoon:
y = image.ProfileData.Profiles[options.profile][1][1] x, y = image.ProfileData.Profiles[options.profile][1]
comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtgui) comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
if options.noprocessing: if options.noprocessing:
print("Do not process image, ignore any profile or processing option") print(f"{job_progress}Do not process image, ignore any profile or processing option")
else: else:
print("Processing images...") print(f"{job_progress}Processing images...")
if GUI: if GUI:
GUI.progressBarTick.emit('Processing images') GUI.progressBarTick.emit(f'{job_progress}Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images")) imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"), job_progress)
if GUI: if GUI:
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
if options.batchsplit > 0: if options.batchsplit > 0 or options.targetsize:
tomes = chunk_directory(path) tomes = chunk_directory(path)
else: else:
tomes = [path] tomes = [path]
@@ -1343,9 +1581,11 @@ def makeBook(source, qtgui=None):
tomeNumber = 0 tomeNumber = 0
if GUI: if GUI:
if options.format == 'CBZ': if options.format == 'CBZ':
GUI.progressBarTick.emit('Compressing CBZ files') GUI.progressBarTick.emit(f'{job_progress}Compressing CBZ files')
elif options.format == 'PDF':
GUI.progressBarTick.emit(f'{job_progress}Creating PDF files')
else: else:
GUI.progressBarTick.emit('Compressing EPUB files') GUI.progressBarTick.emit(f'{job_progress}Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1)) GUI.progressBarTick.emit(str(len(tomes) + 1))
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
options.baseTitle = options.title options.baseTitle = options.title
@@ -1359,61 +1599,76 @@ def makeBook(source, qtgui=None):
tomeNumber += 1 tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']' options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.format == 'CBZ': if options.format == 'CBZ':
print("Creating CBZ file...") print(f"{job_progress}Creating CBZ file...")
if len(tomes) > 1: if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber))) filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images")) makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...")
# determine output filename based on source and tome count
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
# use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
filepath.append(output_pdf)
else: else:
print("Creating EPUB file...") print(f"{job_progress}Creating EPUB file...")
if len(tomes) > 1: if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True, cover, len(tomes)) buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, job_progress, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber))) filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else: else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover) buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True) makeZIP(tome + '_comic', tome, job_progress, True)
copyfile(tome + '_comic.zip', filepath[-1]) # Copy files to final destination (PDF files are already saved directly)
try: if options.format != 'PDF':
os.remove(tome + '_comic.zip') copyfile(tome + '_comic.zip', filepath[-1])
except FileNotFoundError: try:
# newly temporary created file is not found. It might have been already deleted os.remove(tome + '_comic.zip')
pass except FileNotFoundError:
# newly temporary created file is not found. It might have been already deleted
pass
rmtree(tome, True) rmtree(tome, True)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI': if not GUI and options.format == 'MOBI':
print("Creating MOBI files...") print(f"{job_progress}Creating MOBI files...")
work = [] work = []
for i in filepath: for i in filepath:
work.append([i]) work.append([i])
output = makeMOBI(work, GUI) output = makeMOBI(work, GUI)
for errors in output: for errors in output:
if errors[0] != 0: if errors[0] != 0:
print('Error: KindleGen failed to create MOBI!') print(f"{job_progress}Error: KindleGen failed to create MOBI!")
print(errors) print(errors)
return filepath return filepath
k = kindle.Kindle(options.profile) k = kindle.Kindle(options.profile)
if k.path and k.coverSupport: if k.path and k.coverSupport:
print("Kindle detected. Uploading covers...") print(f"{job_progress}Kindle detected. Uploading covers...")
for i in filepath: for i in filepath:
output = makeMOBIFix(i, options.covers[filepath.index(i)][1]) output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
if not output[0]: if not output[0]:
print('Error: Failed to tweak KindleGen output!') print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath return filepath
else: else:
os.remove(i.replace('.epub', '.mobi') + '_toclean') os.remove(i.replace('.epub', '.mobi') + '_toclean')
if k.path and k.coverSupport: if cover and k.path and k.coverSupport:
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1]) options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
if options.delete: if options.delete:
if os.path.isfile(source): if os.path.isfile(source):
os.remove(source) os.remove(source)
elif os.path.isdir(source): elif os.path.isdir(source):
rmtree(source) rmtree(source, True)
end = perf_counter() end = perf_counter()
print(f"makeBook: {end - start} seconds") print(f"{job_progress}makeBook: {end - start} seconds")
# Clean up temporary workspace
try:
rmtree(path, True)
except Exception:
pass
return filepath return filepath
@@ -1493,3 +1748,4 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close() makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join() makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput return makeMOBIWorkerOutput

View File

@@ -18,13 +18,17 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
import math
import os import os
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from shutil import rmtree, copytree, move from shutil import rmtree
from multiprocessing import Pool from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace from PIL.Image import Dither
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
ImageFile.LOAD_TRUNCATED_IMAGES = True
def mergeDirectoryTick(output): def mergeDirectoryTick(output):
@@ -44,6 +48,7 @@ def mergeDirectory(work):
imagesValid = [] imagesValid = []
sizes = [] sizes = []
targetHeight = 0 targetHeight = 0
dot_clean(directory)
for root, _, files in walkLevel(directory, 0): for root, _, files in walkLevel(directory, 0):
for name in files: for name in files:
if getImageFileName(name) is not None: if getImageFileName(name) is not None:
@@ -57,8 +62,8 @@ def mergeDirectory(work):
imagesValid.append(i[0]) imagesValid.append(i[0])
# Silently drop directories that contain too many images # Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4 # 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072: if targetHeight > 131072 * 3:
return None raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
result = Image.new('RGB', (targetWidth, targetHeight)) result = Image.new('RGB', (targetWidth, targetHeight))
y = 0 y = 0
for i in imagesValid: for i in imagesValid:
@@ -100,7 +105,11 @@ def splitImage(work):
Image.warnings.simplefilter('error', Image.DecompressionBombWarning) Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
Image.MAX_IMAGE_PIXELS = 1000000000 Image.MAX_IMAGE_PIXELS = 1000000000
imgOrg = Image.open(filePath).convert('RGB') imgOrg = Image.open(filePath).convert('RGB')
imgProcess = Image.open(filePath).convert('1') # I experimented with custom vertical edge kernel [-1, 2, -1] but got poor results
imgEdges = Image.open(filePath).convert('L').filter(ImageFilter.FIND_EDGES)
# threshold of 8 is too high. 5 is too low.
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
widthImg, heightImg = imgOrg.size widthImg, heightImg = imgOrg.size
if heightImg > opt.height: if heightImg > opt.height:
if opt.debug: if opt.debug:
@@ -111,47 +120,71 @@ def splitImage(work):
yWork = 0 yWork = 0
panelDetected = False panelDetected = False
panels = [] panels = []
# check git history for how these constant values changed
h_pad = int(widthImg / 20)
v_pad = int(widthImg / 80)
if v_pad % 2:
v_pad += 1
while yWork < heightImg: while yWork < heightImg:
tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4)) tmpImg = imgProcess.crop((h_pad, yWork, widthImg - h_pad, yWork + v_pad))
solid = detectSolid(tmpImg) solid = detectSolid(tmpImg)
if not solid and not panelDetected: if not solid and not panelDetected:
panelDetected = True panelDetected = True
panelY1 = yWork - 2 panelY1 = yWork
if heightImg - yWork <= 5: if heightImg - yWork <= (v_pad // 2):
if not solid and panelDetected: if not solid and panelDetected:
panelY2 = heightImg panelY2 = heightImg
panelDetected = False panelDetected = False
panels.append((panelY1, panelY2, panelY2 - panelY1)) panels.append((panelY1, panelY2, panelY2 - panelY1))
if solid and panelDetected: if solid and panelDetected:
panelDetected = False panelDetected = False
panelY2 = yWork + 6 panelY2 = yWork
# skip short panel at start
if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
continue
panels.append((panelY1, panelY2, panelY2 - panelY1)) panels.append((panelY1, panelY2, panelY2 - panelY1))
yWork += 5 yWork += v_pad // 2
max_width = 1072
virtual_width = min((max_width, opt.width, widthImg))
if opt.width > max_width:
virtual_height = int(opt.height/max_width*virtual_width)
else:
virtual_height = int(opt.height/opt.width*virtual_width)
opt.height = virtual_height
# Split too big panels # Split too big panels
panelsProcessed = [] panelsProcessed = []
for panel in panels: for panel in panels:
# 1.52 too high
if panel[2] <= opt.height * 1.5: if panel[2] <= opt.height * 1.5:
panelsProcessed.append(panel) panelsProcessed.append(panel)
elif panel[2] < opt.height * 2: elif panel[2] <= opt.height * 2:
diff = panel[2] - opt.height diff = panel[2] - opt.height
panelsProcessed.append((panel[0], panel[1] - diff, opt.height)) panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height)) panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
else: else:
parts = round(panel[2] / opt.height) # split super long panels with overlap
parts = math.ceil(panel[2] / opt.height)
diff = panel[2] // parts diff = panel[2] // parts
for x in range(0, parts): panelsProcessed.append((panel[0], panel[0] + opt.height, opt.height))
panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff)) for x in range(1, parts - 1):
start = panel[0] + (x * diff)
panelsProcessed.append((start, start + opt.height, opt.height))
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
if opt.debug: if opt.debug:
for panel in panelsProcessed: for panel in panelsProcessed:
draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255)) draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255))
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg) debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
# debugImage.show()
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG') debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
# Create virtual pages # Create virtual pages
pages = [] pages = []
currentPage = [] currentPage = []
# TODO: 1.25 way too high, 1.1 too high, 1.05 slightly too high(?), optimized for 2 page landscape reading
# opt.height = max_height = virtual_height * 1.00
pageLeft = opt.height pageLeft = opt.height
panelNumber = 0 panelNumber = 0
for panel in panelsProcessed: for panel in panelsProcessed:
@@ -188,7 +221,7 @@ def splitImage(work):
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
def main(argv=None, qtgui=None): def main(argv=None, job_progress='', qtgui=None):
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False) parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
@@ -200,6 +233,8 @@ def main(argv=None, qtgui=None):
" with spaces.") " with spaces.")
main_options.add_argument("-y", "--height", type=int, dest="height", default=0, main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
help="Height of the target device screen") help="Height of the target device screen")
main_options.add_argument("-x", "--width", type=int, dest="width", default=0,
help="Width of the target device screen")
main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False, main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
help="Overwrite source directory") help="Overwrite source directory")
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False, main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
@@ -221,13 +256,13 @@ def main(argv=None, qtgui=None):
targetDir = sourceDir + "-Splitted" targetDir = sourceDir + "-Splitted"
if os.path.isdir(sourceDir): if os.path.isdir(sourceDir):
rmtree(targetDir, True) rmtree(targetDir, True)
copytree(sourceDir, targetDir) os.renames(sourceDir, targetDir)
work = [] work = []
pagenumber = 1 pagenumber = 1
splitWorkerOutput = [] splitWorkerOutput = []
splitWorkerPool = Pool(maxtasksperchild=10) splitWorkerPool = Pool(maxtasksperchild=10)
if args.merge: if args.merge:
print("Merging images...") print(f"{job_progress}Merging images...")
directoryNumer = 1 directoryNumer = 1
mergeWork = [] mergeWork = []
mergeWorkerOutput = [] mergeWorkerOutput = []
@@ -239,7 +274,7 @@ def main(argv=None, qtgui=None):
directoryNumer += 1 directoryNumer += 1
mergeWork.append([os.path.join(root, directory)]) mergeWork.append([os.path.join(root, directory)])
if GUI: if GUI:
GUI.progressBarTick.emit('Combining images') GUI.progressBarTick.emit(f'{job_progress}Combining images')
GUI.progressBarTick.emit(str(directoryNumer)) GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork: for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick) mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
@@ -252,7 +287,8 @@ def main(argv=None, qtgui=None):
rmtree(targetDir, True) rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1]) mergeWorkerOutput[0][1])
print("Splitting images...") print(f"{job_progress}Splitting images...")
dot_clean(targetDir)
for root, _, files in os.walk(targetDir, False): for root, _, files in os.walk(targetDir, False):
for name in files: for name in files:
if getImageFileName(name) is not None: if getImageFileName(name) is not None:
@@ -261,7 +297,7 @@ def main(argv=None, qtgui=None):
else: else:
os.remove(os.path.join(root, name)) os.remove(os.path.join(root, name))
if GUI: if GUI:
GUI.progressBarTick.emit('Splitting images') GUI.progressBarTick.emit(f'{job_progress}Splitting images')
GUI.progressBarTick.emit(str(pagenumber)) GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if len(work) > 0: if len(work) > 0:
@@ -269,6 +305,7 @@ def main(argv=None, qtgui=None):
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick) splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
splitWorkerPool.close() splitWorkerPool.close()
splitWorkerPool.join() splitWorkerPool.join()
dot_clean(targetDir)
if GUI and not GUI.conversionAlive: if GUI and not GUI.conversionAlive:
rmtree(targetDir, True) rmtree(targetDir, True)
raise UserWarning("Conversion interrupted.") raise UserWarning("Conversion interrupted.")
@@ -277,11 +314,10 @@ def main(argv=None, qtgui=None):
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0], raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
splitWorkerOutput[0][1]) splitWorkerOutput[0][1])
if args.inPlace: if args.inPlace:
rmtree(sourceDir) os.renames(targetDir, sourceDir)
move(targetDir, sourceDir)
else: else:
rmtree(targetDir, True) rmtree(targetDir, True)
raise UserWarning("Source directory is empty.") raise UserWarning("C2P: Source directory is empty.")
else: else:
raise UserWarning("Provided input is not a directory.") raise UserWarning("Provided input is not a directory.")
else: else:

View File

@@ -20,12 +20,13 @@
from functools import cached_property, lru_cache from functools import cached_property, lru_cache
import os import os
from pathlib import Path
import platform import platform
import distro import distro
from subprocess import STDOUT, PIPE, CalledProcessError from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
from .shared import subprocess_run from .shared import IMAGE_TYPES, subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.' EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z' SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
@@ -36,21 +37,22 @@ class ComicArchive:
self.filepath = filepath self.filepath = filepath
if not os.path.isfile(self.filepath): if not os.path.isfile(self.filepath):
raise OSError('File not found.') raise OSError('File not found.')
self.dirname, self.basename = os.path.split(filepath)
@cached_property @cached_property
def type(self): def type(self):
extraction_commands = [ extraction_commands = [
[SEVENZIP, 'l', '-y', '-p1', self.filepath], [SEVENZIP, 'l', '-y', '-p1', self.basename],
] ]
if distro.id() == 'fedora' or distro.like() == 'fedora': if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append( extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.filepath], ['unrar', 'l', '-y', '-p1', self.basename],
) )
for cmd in extraction_commands: for cmd in extraction_commands:
try: try:
process = subprocess_run(cmd, capture_output=True, check=True) process = subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
for line in process.stdout.splitlines(): for line in process.stdout.splitlines():
if b'Type =' in line: if b'Type =' in line:
return line.rstrip().decode().split(' = ')[1].upper() return line.rstrip().decode().split(' = ')[1].upper()
@@ -64,30 +66,33 @@ class ComicArchive:
def extract(self, targetdir): def extract(self, targetdir):
if not os.path.isdir(targetdir): if not os.path.isdir(targetdir):
raise OSError('Target directory doesn\'t exist.') raise OSError('Target directory doesn\'t exist.')
if Path(self.basename).suffix.lower() in IMAGE_TYPES:
raise UserWarning('Put images into folder and drag and drop folder into KCC window.')
missing = [] missing = []
extraction_commands = [ extraction_commands = [
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.filepath, '-C', targetdir], ['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.filepath], [SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
] ]
if platform.system() == 'Darwin': if platform.system() == 'Darwin':
extraction_commands.append( extraction_commands.append(
['unar', self.filepath, '-f', '-o', targetdir] ['unar', self.basename, '-D', '-f', '-o', targetdir]
) )
extraction_commands.reverse() 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.basename, targetdir]
) )
for cmd in extraction_commands: for cmd in extraction_commands:
try: try:
subprocess_run(cmd, capture_output=True, check=True) subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
return targetdir return targetdir
except FileNotFoundError: except FileNotFoundError:
missing.append(cmd[0]) missing.append(cmd[0])
except CalledProcessError: except CalledProcessError:
@@ -101,14 +106,14 @@ class ComicArchive:
def addFile(self, sourcefile): def addFile(self, sourcefile):
if self.type in ['RAR', 'RAR5']: if self.type in ['RAR', 'RAR5']:
raise NotImplementedError raise NotImplementedError
process = subprocess_run([SEVENZIP, 'a', '-y', self.filepath, sourcefile], process = subprocess_run([SEVENZIP, 'a', '-y', self.basename, sourcefile],
stdout=PIPE, stderr=STDOUT) stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
if process.returncode != 0: if process.returncode != 0:
raise OSError('Failed to add the file.') raise OSError('Failed to add the file.')
def extractMetadata(self): def extractMetadata(self):
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.filepath, 'ComicInfo.xml'], process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.basename, 'ComicInfo.xml'],
stdout=PIPE, stderr=STDOUT) stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
if process.returncode != 0: if process.returncode != 0:
raise OSError(EXTRACTION_ERROR) raise OSError(EXTRACTION_ERROR)
try: try:

View File

@@ -24,11 +24,14 @@ import numpy as np
from pathlib import Path from pathlib import Path
from functools import cached_property from functools import cached_property
import mozjpeg_lossless_optimization import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
from .inter_panel_crop_alg import crop_empty_inter_panel from .inter_panel_crop_alg import crop_empty_inter_panel
AUTO_CROP_THRESHOLD = 0.015 AUTO_CROP_THRESHOLD = 0.015
ImageFile.LOAD_TRUNCATED_IMAGES = True
class ProfileData: class ProfileData:
@@ -83,22 +86,23 @@ class ProfileData:
] ]
ProfilesKindleEBOK = { ProfilesKindleEBOK = {
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
} }
ProfilesKindlePDOC = { ProfilesKindlePDOC = {
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8), 'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8), 'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8), 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8), 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8), 'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8), 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
} }
ProfilesKindle = { ProfilesKindle = {
@@ -107,34 +111,35 @@ class ProfileData:
} }
ProfilesKobo = { ProfilesKobo = {
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8), 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8), 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8), 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8), 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8), 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8), 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8), 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8), 'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8), 'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8), 'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8), 'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8), 'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8), 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8), 'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8), 'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
} }
ProfilesRemarkable = { ProfilesRemarkable = {
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8), 'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8), 'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8), 'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
} }
Profiles = { Profiles = {
**ProfilesKindle, **ProfilesKindle,
**ProfilesKobo, **ProfilesKobo,
**ProfilesRemarkable, **ProfilesRemarkable,
'OTHER': ("Other", (0, 0), Palette16, 1.8), 'OTHER': ("Other", (0, 0), Palette16, 1.0),
} }
@@ -149,7 +154,8 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered. # Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1]) srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify() Image.open(srcImgPath).verify()
self.image = Image.open(srcImgPath) with Image.open(srcImgPath) as im:
self.image = im.copy()
self.fill = self.fillCheck() self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
@@ -257,8 +263,8 @@ class ComicPage:
_, self.size, self.palette, self.gamma = self.opt.profileData _, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq: if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5)) self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.original_color_mode = image.mode self.original_color_mode = image.mode
# TODO: color check earlier
self.image = image.convert("RGB") self.image = image.convert("RGB")
self.fill = fill self.fill = fill
self.rotated = False self.rotated = False
@@ -267,7 +273,7 @@ class ComicPage:
if 'N' in mode: if 'N' in mode:
self.targetPathOrder = '-kcc-x' self.targetPathOrder = '-kcc-x'
elif 'R' in mode: elif 'R' in mode:
self.targetPathOrder = '-kcc-d' self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
if not options.norotate: if not options.norotate:
self.rotated = True self.rotated = True
elif 'S1' in mode: elif 'S1' in mode:
@@ -282,21 +288,89 @@ class ComicPage:
def color(self): def color(self):
if self.original_color_mode in ("L", "1"): if self.original_color_mode in ("L", "1"):
return False return False
img = self.image.convert("YCbCr") if self.opt.webtoon:
_, cb, cr = img.split() return True
if self.calculate_color():
return True
return False
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
# for better accuracy, you could split the image in half and analyze each half separately
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
if cutoff == (0, 0):
return cb_hist, cr_hist
for h in cb_hist, cr_hist:
# get number of pixels
n = sum(h)
# remove cutoff% pixels from the low end
cut = int(n * cutoff[0] // 100)
for lo in range(256):
if cut > h[lo]:
cut = cut - h[lo]
h[lo] = 0
else:
h[lo] -= cut
cut = 0
if cut <= 0:
break
# remove cutoff% samples from the high end
cut = int(n * cutoff[1] // 100)
for hi in range(255, -1, -1):
if cut > h[hi]:
cut = cut - h[hi]
h[hi] = 0
else:
h[hi] -= cut
cut = 0
if cut <= 0:
break
return cb_hist, cr_hist
def color_precision(self, cb_hist_original, cr_hist_original, cutoff, diff_threshold):
cb_hist, cr_hist = self.histograms_cutoff(cb_hist_original.copy(), cr_hist_original.copy(), cutoff)
cb_hist = cb.histogram()
cr_hist = cr.histogram()
cb_nonzero = [i for i, e in enumerate(cb_hist) if e] cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e] cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0 cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0 cr_spread = cr_nonzero[-1] - cr_nonzero[0]
SPREAD_THRESHOLD=20 # bias adjustment, don't go lower than 7
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD: SPREAD_THRESHOLD = 7
return False if self.opt.forcecolor:
else: if any([
return True cb_nonzero[0] > 128,
cr_nonzero[0] > 128,
cb_nonzero[-1] < 128,
cr_nonzero[-1] < 128,
]):
return True, True
elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return True, False
DIFF_THRESHOLD = diff_threshold
if any([
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
]):
return True, True
return False, None
def calculate_color(self):
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist_original = cb.histogram()
cr_hist_original = cr.histogram()
# you can increase 22 but don't increase 10. 4 maybe can go higher
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
if done:
return decision
return False
def saveToDir(self): def saveToDir(self):
try: try:
@@ -321,7 +395,7 @@ class ComicPage:
def save_with_codec(self, image, targetPath): def save_with_codec(self, image, targetPath):
if self.opt.forcepng: if self.opt.forcepng:
image.info["transparency"] = None image.info.pop('transparency', None)
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format): if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
targetPath += '.gif' targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False) image.save(targetPath, 'GIF', optimize=1, interlace=False)
@@ -341,16 +415,52 @@ class ComicPage:
image.save(targetPath, 'JPEG', optimize=1, quality=85) image.save(targetPath, 'JPEG', optimize=1, quality=85)
return targetPath return targetPath
def autocontrastImage(self): def gammaCorrectImage(self):
gamma = self.opt.gamma gamma = self.opt.gamma
if gamma < 0.1: if gamma < 0.1:
gamma = self.gamma gamma = self.gamma
if self.gamma != 1.0 and self.color: if self.gamma != 1.0 and self.color:
gamma = 1.0 gamma = 1.0
if gamma == 1.0: if gamma == 1.0:
self.image = ImageOps.autocontrast(self.image) pass
else: else:
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))) self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
def autocontrastImage(self):
if self.opt.webtoon:
return
if self.opt.noautocontrast:
return
if self.color and not self.opt.colorautocontrast:
return
# if image is extremely low contrast, that was probably intentional
extrema = self.image.convert('L').getextrema()
if extrema[1] - extrema[0] < (255 - 32 * 3):
return
if self.opt.autolevel:
self.autolevelImage()
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
def autolevelImage(self):
img = self.image
if self.color:
img = self.image.convert("YCbCr")
y, cb, cr = img.split()
img = y
else:
img = img.convert('L')
h = img.histogram()
most_common_dark_pixel_count = max(h[:64])
black_point = h.index(most_common_dark_pixel_count)
bp = black_point
img = img.point(lambda p: p if p > bp else bp)
if self.color:
self.image = Image.merge(mode='YCbCr', bands=[img, cb, cr]).convert('RGB')
else:
self.image = img
def convertToGrayscale(self): def convertToGrayscale(self):
self.image = self.image.convert('L') self.image = self.image.convert('L')
@@ -358,21 +468,26 @@ class ComicPage:
def quantizeImage(self): def quantizeImage(self):
# remove all color pixels from image, since colorCheck() has some tolerance # remove all color pixels from image, since colorCheck() has some tolerance
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results # quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.image = self.image.convert("L").convert("RGB") self.image = self.image.convert("RGB")
palImg = Image.new('P', (1, 1)) palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette) palImg.putpalette(self.palette)
self.image = self.image.quantize(palette=palImg) self.image = self.image.quantize(palette=palImg)
def optimizeForDisplay(self, reducerainbow): def optimizeForDisplay(self, eraserainbow, is_color):
# Reduce rainbow artifacts for grayscale images by breaking up dither patterns that cause Moire interference with color filter array # Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
if reducerainbow and not self.color: if eraserainbow and all(dim > 1 for dim in self.image.size):
unsharpFilter = ImageFilter.UnsharpMask(radius=1, percent=100) self.image = erase_rainbow_artifacts(self.image, is_color)
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):
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
# TODO: Kindle Scribe case
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
elif self.image.size[0] > self.size[0] * 2 or self.image.size[1] > self.size[1]:
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1], Image.Resampling.LANCZOS))
return
ratio_device = float(self.size[1]) / float(self.size[0]) ratio_device = float(self.size[1]) / float(self.size[0])
ratio_image = float(self.image.size[1]) / float(self.image.size[0]) ratio_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method() method = self.resize_method()
@@ -383,7 +498,7 @@ class ComicPage:
else: # if image bigger than device resolution or smaller with upscaling else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD: if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method) self.image = ImageOps.fit(self.image, self.size, method=method)
elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders: elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill) self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else: else:
self.image = ImageOps.contain(self.image, self.size, method=method) self.image = ImageOps.contain(self.image, self.size, method=method)
@@ -409,12 +524,20 @@ class ComicPage:
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill) bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
if bbox: if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum): def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.fill) bbox = get_bbox_crop_margin(self.image, power, self.fill)
if bbox: if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction): def cropInterPanelEmptySections(self, direction):
@@ -432,7 +555,7 @@ class Cover:
def process(self): def process(self):
self.image = self.image.convert('RGB') self.image = self.image.convert('RGB')
self.image = ImageOps.autocontrast(self.image) self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor: if not self.options.forcecolor:
self.image = self.image.convert('L') self.image = self.image.convert('L')
self.crop_main_cover() self.crop_main_cover()
@@ -449,7 +572,7 @@ class Cover:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h)) self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h)) self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.3: elif w / h > 1.34:
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h)) self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else: else:

View File

@@ -1,8 +1,10 @@
from PIL import Image, ImageFilter, ImageOps from PIL import Image, ImageFilter, ImageOps, ImageFile
import numpy as np import numpy as np
from typing import Literal from typing import Literal
from .common_crop import threshold_from_power, group_close_values from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
''' '''
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins). Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
@@ -19,10 +21,10 @@ def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "bo
img_temp = img img_temp = img
if img.mode != 'L': if img.mode != 'L':
img_temp = ImageOps.grayscale(img) img_temp = ImageOps.grayscale(img_temp)
if background_color != 'white': if background_color != 'white':
img_temp = ImageOps.invert(img) img_temp = ImageOps.invert(img_temp)
img_mat = np.array(img) img_mat = np.array(img)

View File

@@ -123,4 +123,4 @@ class MetadataParser:
cbx.addFile(tmpXML) cbx.addFile(tmpXML)
except OSError as e: except OSError as e:
raise UserWarning(e) raise UserWarning(e)
rmtree(workdir) rmtree(workdir, True)

View File

@@ -1,7 +1,9 @@
from PIL import ImageOps, ImageFilter from PIL import ImageOps, ImageFilter, ImageFile
import numpy as np import numpy as np
from .common_crop import threshold_from_power, group_close_values from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
''' '''
Some assupmptions on the page number sizes Some assupmptions on the page number sizes
@@ -52,6 +54,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
''' '''
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
bw_bbox = bw_img.getbbox() bw_bbox = bw_img.getbbox()
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
return None return None
@@ -141,9 +144,26 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
''' '''
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
return bw_img.getbbox() return bw_img.getbbox()
def ignore_pixels_near_edge(bw_img):
w, h = bw_img.size
edge_bbox = [
(0, 0, w, int(0.02 * h)),
(0, int(0.98 * h), w, h),
(0, 0, int(0.02 * w), h),
(int(0.98 * w), 0, w, h)
]
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)
def box_intersect(box1, box2, max_dist): def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1] return not (box2[0]-max_dist[0] > box1[1]

View File

@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
#
# Based upon the code snippet by Ned Batchelder
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all
# copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
import os
from random import choice
from string import ascii_uppercase, digits
# skip stray images a few pixels in size in some PDFs
# typical images are many thousands in length
# https://github.com/ciromattia/kcc/pull/546
STRAY_IMAGE_LENGTH_THRESHOLD = 300
class PdfJpgExtract:
def __init__(self, fname):
self.fname = fname
self.filename = os.path.splitext(fname)
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
def getPath(self):
return self.path
def extract(self):
pdf = open(self.fname, "rb").read()
startmark = b"\xff\xd8"
startfix = 0
endmark = b"\xff\xd9"
endfix = 2
i = 0
njpg = 0
os.makedirs(self.path)
while True:
istream = pdf.find(b"stream", i)
if istream < 0:
break
istart = pdf.find(startmark, istream, istream + 20)
if istart < 0:
i = istream + 20
continue
iend = pdf.find(b"endstream", istart)
if iend < 0:
raise Exception("Didn't find end of stream!")
iend = pdf.find(endmark, iend - 20)
if iend < 0:
raise Exception("Didn't find end of JPG!")
istart += startfix
iend += endfix
i = iend
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
continue
jpg = pdf[istart:iend]
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
jpgfile.write(jpg)
jpgfile.close()
njpg += 1
return self.path, njpg

View File

@@ -0,0 +1,249 @@
import numpy as np
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
def fourier_transform_image(img):
"""
Memory-optimized version that modifies the array in place when possible.
"""
# Convert with minimal copy
img_array = np.asarray(img, dtype=np.float32)
# Use rfft2 if the image is real to save memory
# and computation time (approximately 2x faster)
fft_result = np.fft.rfft2(img_array)
return fft_result
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
angle_tolerance=10, attenuation_factor=0.10):
"""
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
Args:
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
target_angle: Target angle in degrees (default: 135)
angle_tolerance: Angular tolerance in degrees (default: 15)
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
Returns:
np.ndarray: Modified FFT with applied attenuation (same format as input)
"""
# Get dimensions of the rfft2 result
if fft_spectrum.ndim == 2:
height, width_rfft = fft_spectrum.shape
else: # 3D array (color channels)
height, width_rfft = fft_spectrum.shape[:2]
# For rfft2, the original width is (width_rfft - 1) * 2
width_original = (width_rfft - 1) * 2
# Create frequency grids for rfft2 format
freq_y = np.fft.fftfreq(height, d=1.0)
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
# Use broadcasting to create grids without meshgrid (more efficient)
freq_y_grid = freq_y.reshape(-1, 1) # Column
freq_x_grid = freq_x.reshape(1, -1) # Row
# Calculate squared radial frequencies (avoid sqrt)
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
freq_threshold_sq = freq_threshold**2
# Frequency condition
freq_condition = freq_radial_sq >= freq_threshold_sq
# Early exit if no frequency satisfies the condition
if not np.any(freq_condition):
return fft_spectrum
# Calculate angles only where necessary
# Use atan2 directly with broadcasting
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
# Convert to degrees and normalize in a single operation
angles_deg = np.rad2deg(angles_rad) % 360
# Calculation of complementary angle
target_angle_2 = (target_angle + 180) % 360
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
target_angle_3 = (target_angle + 90) % 360
target_angle_4 = (target_angle_3 + 180) % 360
# Create angular conditions in a vectorized way
angle_condition = np.zeros_like(angles_deg, dtype=bool)
# Process both angles simultaneously
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
min_angle = (angle - angle_tolerance) % 360
max_angle = (angle + angle_tolerance) % 360
if min_angle > max_angle: # Interval crosses 0°
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
else: # Normal interval
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
# Combine conditions
combined_condition = freq_condition & angle_condition
# Apply attenuation directly (avoid creating a full mask)
if attenuation_factor == 0:
# Special case: complete suppression
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] = 0
else: # 3D array
fft_spectrum[combined_condition, :] = 0
return fft_spectrum
elif attenuation_factor == 1:
# Special case: no attenuation
return fft_spectrum
else:
# General case: partial attenuation
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] *= attenuation_factor
else: # 3D array
fft_spectrum[combined_condition, :] *= attenuation_factor
return fft_spectrum
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
"""
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
Args:
fft_spectrum: Fourier transform result (complex array from rfft2)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Reconstructed image
"""
# Perform inverse Fourier transform with original shape if provided
if original_shape is not None:
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
else:
img_reconstructed = np.fft.irfft2(fft_spectrum)
# Normalize values between 0 and 255
img_reconstructed = np.clip(img_reconstructed, 0, 255)
img_reconstructed = img_reconstructed.astype(np.uint8)
# Convert to PIL image
if is_color and img_reconstructed.ndim == 3:
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
else:
pil_image = Image.fromarray(img_reconstructed, mode='L')
return pil_image
def rgb_to_yuv(rgb_array):
"""
Convert RGB to YUV color space.
Y = luminance, U and V = chrominance
"""
# Coefficients for RGB to YUV conversion
rgb_to_yuv_matrix = np.array([
[0.299, 0.587, 0.114], # Y
[-0.14713, -0.28886, 0.436], # U
[0.615, -0.51499, -0.10001] # V
])
# Reshape for matrix multiplication
original_shape = rgb_array.shape
rgb_flat = rgb_array.reshape(-1, 3)
# Apply transformation
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
# Reshape back
yuv_array = yuv_flat.reshape(original_shape)
return yuv_array
def yuv_to_rgb(yuv_array):
"""
Convert YUV to RGB color space.
"""
# Coefficients for YUV to RGB conversion
yuv_to_rgb_matrix = np.array([
[1.0, 0.0, 1.13983], # R
[1.0, -0.39465, -0.58060], # G
[1.0, 2.03211, 0.0] # B
])
# Reshape for matrix multiplication
original_shape = yuv_array.shape
yuv_flat = yuv_array.reshape(-1, 3)
# Apply transformation
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
# Reshape back
rgb_array = rgb_flat.reshape(original_shape)
return rgb_array
def erase_rainbow_artifacts(img, is_color):
"""
Remove rainbow artifacts from grayscale or color images.
Args:
img: PIL Image (grayscale or RGB)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Cleaned image
"""
# Auto-detect color mode if not specified
if is_color is None:
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
if is_color and img.mode in ('RGB', 'RGBA'):
# Convert to RGB if needed
if img.mode == 'RGBA':
img = img.convert('RGB')
# Convert to numpy array
img_array = np.array(img, dtype=np.float32)
# Convert to YUV color space
yuv_array = rgb_to_yuv(img_array)
# Extract luminance channel (Y)
luminance = yuv_array[:, :, 0]
# Process only the luminance channel
fft_spectrum = fourier_transform_image(luminance)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
# Normalize and clip luminance
clean_luminance = np.clip(clean_luminance, 0, 255)
# Replace luminance in YUV array
yuv_array[:, :, 0] = clean_luminance
# Convert back to RGB
rgb_array = yuv_to_rgb(yuv_array)
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
# Convert back to PIL image
clean_image = Image.fromarray(rgb_array, mode='RGB')
else:
# Grayscale processing (original behavior)
if img.mode != 'L':
img = img.convert('L')
# Get original image dimensions
original_shape = (img.height, img.width)
fft_spectrum = fourier_transform_image(img)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
return clean_image

View File

@@ -27,6 +27,9 @@ import sys
from traceback import format_tb from traceback import format_tb
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
class HTMLStripper(HTMLParser): class HTMLStripper(HTMLParser):
def __init__(self): def __init__(self):
HTMLParser.__init__(self) HTMLParser.__init__(self)
@@ -45,6 +48,14 @@ class HTMLStripper(HTMLParser):
pass pass
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
for name in files:
if name.startswith('._') or name == '.DS_Store':
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
def getImageFileName(imgfile): def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile) name, ext = os.path.splitext(imgfile)
ext = ext.lower() ext = ext.lower()
@@ -90,10 +101,10 @@ def dependencyCheck(level):
if level > 2: if level > 2:
try: try:
from PySide6.QtCore import qVersion as qtVersion from PySide6.QtCore import qVersion as qtVersion
if Version('6.5.1') > Version(qtVersion()): if Version('6.0.0') > Version(qtVersion()):
missing.append('PySide 6.5.1+') missing.append('PySide 6.0.0')
except ImportError: except ImportError:
missing.append('PySide 6.5.1+') missing.append('PySide 6.0.0+')
try: try:
import raven import raven
except ImportError: except ImportError:
@@ -116,10 +127,16 @@ def dependencyCheck(level):
missing.append('python-slugify 1.2.1+') missing.append('python-slugify 1.2.1+')
try: try:
from PIL import __version__ as pillowVersion from PIL import __version__ as pillowVersion
if Version('5.2.0') > Version(pillowVersion): if Version('8.3.0') > Version(pillowVersion):
missing.append('Pillow 5.2.0+') missing.append('Pillow 8.3.0+')
except ImportError: except ImportError:
missing.append('Pillow 5.2.0+') missing.append('Pillow 8.3.0+')
try:
from pymupdf import __version__ as pymupdfVersion
if Version('1.16.1') > Version(pymupdfVersion):
missing.append('PyMuPDF 1.16.1+')
except ImportError:
missing.append('PyMuPDF 1.16.1+')
if len(missing) > 0: if len(missing) > 0:
print('ERROR: ' + ', '.join(missing) + ' is not installed!') print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1) sys.exit(1)

11
requirements-docker.txt Normal file
View File

@@ -0,0 +1,11 @@
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
# Below requirements are compiled in Dockefile
# numpy==2.3.4
# PyMuPDF==1.26.6

View File

@@ -0,0 +1,12 @@
PySide6==6.5.2
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy<2
PyMuPDF>=1.26.1

12
requirements-win7.txt Normal file
View File

@@ -0,0 +1,12 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy==1.23.0
PyMuPDF>=1.16

View File

@@ -1,5 +1,5 @@
PySide6>=6.5.1 PySide6<6.10
Pillow>=5.2.0 Pillow>=11.3.0
psutil>=5.9.5 psutil>=5.9.5
requests>=2.31.0 requests>=2.31.0
python-slugify>=1.2.1 python-slugify>=1.2.1
@@ -9,3 +9,4 @@ mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.8.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.18.0

View File

@@ -8,6 +8,8 @@ Install as Python package:
Create EXE/APP: Create EXE/APP:
python3 setup.py build_binary python3 setup.py build_binary
python3 setup.py build_c2e
python3 setup.py build_c2p
""" """
import os import os
@@ -38,10 +40,17 @@ class BuildBinaryCommand(setuptools.Command):
if sys.platform == 'darwin': if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py') os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v # TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg') min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
if min_os:
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
else:
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
sys.exit(0) sys.exit(0)
elif sys.platform == 'win32': elif sys.platform == 'win32':
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py') if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_legacy_' + VERSION + ' -w --noupx kcc.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
sys.exit(0) sys.exit(0)
elif sys.platform == 'linux': elif sys.platform == 'linux':
os.system( os.system(
@@ -50,10 +59,75 @@ class BuildBinaryCommand(setuptools.Command):
else: else:
sys.exit(0) sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2ECommand(setuptools.Command):
description = 'build binary c2e release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "KCC C2E" -c -s kcc-c2e.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_win7_legacy_' + VERSION + ' -c --noupx kcc-c2e.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_' + VERSION + ' -c --noupx kcc-c2e.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2e_linux_' + VERSION + ' kcc-c2e.py')
sys.exit(0)
else:
sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2PCommand(setuptools.Command):
description = 'build binary c2p release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -n "KCC C2P" -c -s kcc-c2p.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_win7_legacy_' + VERSION + ' -c --noupx kcc-c2p.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_' + VERSION + ' -c --noupx kcc-c2p.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2p_linux_' + VERSION + ' kcc-c2p.py')
sys.exit(0)
else:
sys.exit(0)
setuptools.setup( setuptools.setup(
cmdclass={ cmdclass={
'build_binary': BuildBinaryCommand, 'build_binary': BuildBinaryCommand,
'build_c2e': BuildC2ECommand,
'build_c2p': BuildC2PCommand,
}, },
name=NAME, name=NAME,
version=VERSION, version=VERSION,
@@ -74,8 +148,9 @@ setuptools.setup(
}, },
packages=['kindlecomicconverter'], packages=['kindlecomicconverter'],
install_requires=[ install_requires=[
'pyside6>=6.5.1', 'pyside6>=6.0.0',
'Pillow>=5.2.0', 'Pillow>=9.3.0',
'PyMuPDF>=1.18.0',
'psutil>=5.9.5', 'psutil>=5.9.5',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0', 'raven>=6.0.0',
@@ -83,7 +158,8 @@ setuptools.setup(
'mozjpeg-lossless-optimization>=1.1.2', 'mozjpeg-lossless-optimization>=1.1.2',
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro', 'distro',
'numpy>=1.22.4,<2.0.0' 'numpy>=1.22.4',
'PyMuPDF>=1.16.1',
], ],
classifiers=[], classifiers=[],
zip_safe=False, zip_safe=False,